Extensions

Source: renderers/LightRenderer.js

import { arraySet } from "../../extensions/utils/arraySet";
import { Buffer } from "../core/Buffer";
import { Utils } from "../core/Utils";
import { Light } from "../display/Light";
import { BlendMode } from "../rendering/BlendMode";
import { BASE_VERTEX_SHADER_ATTRIBUTES, BASE_VERTEX_SHADER_UNIFORMS } from "../utils/shaderUtils";
import { BatchRenderer } from "./BatchRenderer";

/**
 * @typedef {Object} LightRendererConfig
 * @extends {RendererConfig}
 * @property {number} maxRenderCount
 * @property {TextureInfo} sourceTexture
 * @property {TextureInfo} normalMap
 * @property {TextureInfo} heightMap
 * @property {TextureInfo} roughnessMap
 * @property {number} offsetX // Offset x
 * @property {number} offsetY // Offset y
 */

/**
 * <pre>
 *  Light renderer
 *  - Renders lights and shadows based on height, normal and roughness map
 *  - Height map could store the following values:
 *    - Red channel: start of a vertical object
 *    - Green channel: end of a vertical object
 *    - Blue channel: shiness of the surface
 *  - If the roughness map exists, the shiness and roughness values are
 *    derived from its red and green channels.
 *  - Every input texture are optional
 * </pre>
 * @extends {BatchRenderer}
 */
export class LightRenderer extends BatchRenderer {
  /**
   * Creates an instance of LightRenderer.
   * @constructor
   * @param {LightRendererConfig} config
   */
  constructor(config = {}) {
    config = Utils.initRendererConfig(config);

    // prettier-ignore
    Utils.setLocations(config, [
      "aC",
      "uC",
      "uF",
      "uG",
      "uM",
      "uN",
      "uO"
    ]);

    super(config);

    this.clearBeforeRender = true;
    this.clearColor.set(0, 0, 0, 1);

    this._sizable = this;

    this.sourceTexture = config.sourceTexture;
    this.normalMap = config.normalMap;
    this.heightMap = config.heightMap;
    this.roughnessMap = config.roughnessMap;
    this.offsetX = config.offsetX ?? 0;
    this.offsetY = config.offsetY ?? 0;

    // prettier-ignore
    this._extensionBuffer = new Buffer(
      "aC",
      this.$MAX_RENDER_COUNT,
      2,
      3
    );
  }

  get sourceTexture() {
    return this._sourceTexture;
  }

  set sourceTexture(v) {
    this._sourceTexture = v;
    if (v) {
      this._sizable = v;
    }
  }

  get normalMap() {
    return this._normalMap;
  }

  set normalMap(v) {
    this._normalMap = v;
    if (v) {
      this._sizable = v;
    }
  }

  get heightMap() {
    return this._heightMap;
  }

  set heightMap(v) {
    this._heightMap = v;
    if (v) {
      this._sizable = v;
    }
  }

  get roughnessMap() {
    return this._roughnessMap;
  }

  set roughnessMap(v) {
    this._roughnessMap = v;
    if (v) {
      this._sizable = v;
    }
  }

  /**
   * Sets the offset for the light renderer.
   * @param {number} x - The x offset.
   * @param {number} y - The y offset.
   */
  setOffset(x, y) {
    this.offsetX = x;
    this.offsetY = y;
  }

  /**
   * Register a Light instance for rendering
   * @param {Light} light
   */
  addLightForRender(light) {
    const { _batchItems } = this;
    const { parent } = light;

    if (_batchItems < this.$MAX_RENDER_COUNT && parent) {
      const matrixBufferId = _batchItems * 16;
      const extensionBufferId = _batchItems * 6;
      const matrixBufferData = this.$matrixBuffer.data;
      const extensionBufferData = this._extensionBuffer.data;

      arraySet(matrixBufferData, light.matrixCache, matrixBufferId);
      matrixBufferData[matrixBufferId + 6] = light.transform.width;
      matrixBufferData[matrixBufferId + 7] = light.spotAngle;
      arraySet(matrixBufferData, light.colorCache, matrixBufferId + 8);
      matrixBufferData[matrixBufferId + 11] *= light.alpha * parent.getPremultipliedAlpha();
      matrixBufferData[matrixBufferId + 12] = light.shadowLength;
      matrixBufferData[matrixBufferId + 13] = light.angle;
      matrixBufferData[matrixBufferId + 14] = light.type;
      // matrixBufferData[matrixBufferId + 15] = reserved

      extensionBufferData[extensionBufferId] = light.flags;
      extensionBufferData[extensionBufferId + 1] = light.transform.z;
      extensionBufferData[extensionBufferId + 2] = light.precision;
      extensionBufferData[extensionBufferId + 3] = light.maxShadowStep;
      extensionBufferData[extensionBufferId + 4] = light.specularStrength;
      extensionBufferData[extensionBufferId + 5] = light.attenuation;

      ++this._batchItems;
    }
  }

  /**
   * @ignore
   */
  $render() {
    const { $gl, $locations, _sourceTexture, _normalMap, _roughnessMap, _heightMap } = this;
    const sourceTextureBoolean = !!_sourceTexture;
    const normalMapBoolean = !!_normalMap;
    const roughnessMapBoolean = !!_roughnessMap;
    const heightMapBoolean = !!_heightMap;

    this.context.setBlendMode(BlendMode.ADD);

    sourceTextureBoolean && this.$useTexture(_sourceTexture, $locations.uC);

    normalMapBoolean && this.$useTexture(_normalMap, $locations.uM);

    roughnessMapBoolean && this.$useTexture(_roughnessMap, $locations.uN);

    heightMapBoolean && this.$useTexture(_heightMap, $locations.uB);

    $gl.uniform3f($locations.uO, sourceTextureBoolean, roughnessMapBoolean, normalMapBoolean);
    $gl.uniform2f($locations.uF, this._sizable.width, this._sizable.height);
    $gl.uniform2f($locations.uG, this.offsetX, this.offsetY);

    this.$uploadBuffers();

    this.$drawInstanced(this._batchItems);

    this._batchItems = 0;
  }

  /**
   * @ignore
   */
  $uploadBuffers() {
    this._extensionBuffer.upload(this.$gl);
    super.$uploadBuffers();
  }

  /**
   * @ignore
   */
  $createBuffers() {
    super.$createBuffers();
    this._extensionBuffer.create(this.$gl, this.$locations);
  }

  // prettier-ignore
  /**
   * @returns {string}
   * @ignore
   */
  $createVertexShader() {
    return `${Utils.GLSL.DEFINE.Z}` +
    `${Utils.GLSL.DEFINE.PI}` +
    `#define P vec4(1,-1,2,-2)\n` +

    `${BASE_VERTEX_SHADER_ATTRIBUTES}` +
    `in mat4 ` +
      `aB;` +
    `in mat2x3 ` +
      `aC;` +

    `${BASE_VERTEX_SHADER_UNIFORMS}` +

    `out vec2 ` +
      `v0;` +
    `out vec4 ` +
      `v1;` +
    `flat out vec4 ` +
      `v2,` +
      `v3,` +
      `v4,` +
      `v5;` +

    `void main(){` +
      `vec3 ` + 
        `pos=vec3(aA*2.-1.,1);` +
      
      `v1.xy=pos.xy;` +

      `mat3 mt=mat3(aB[0].xy,0,aB[0].zw,0,aB[1].xy,1);` +
      `v2.x=aB[1].z;` + 
      `v2.y=v2.x*aB[3].x;` +

      `v3=vec4(aC[1],PI-aB[1].w);` +
      `v4=vec4(aB[3].z,aC[0]);` +
      `v5=aB[2];` +

      `if(v4.x<1.){` +
        `gl_Position=vec4(mt*pos,1);` +
        `v0=(gl_Position.xy+P.xy)/P.zw;` +
        `v1.zw=(aB[1].xy+P.xy)/P.zw;` +
        `v2.zw=vec2(sin(aB[3].y),cos(aB[3].y));` +
      `}else{` +
        `mt[2].xy=Z.zy;` +
        `gl_Position=vec4(pos,1);` +
        `v0=(gl_Position.xy+P.xy)/P.zw;` +
        `v1.zw=v0+((mt*vec3(1)).xy+P.xy)/P.zw;` +
      `}` +

      `gl_Position.y*=uA;` +
    `}`;
  }

  // prettier-ignore
  /**
   * @returns {string}
   * @ignore
   */
  $createFragmentShader() {
    const loop = (core) => `for(i=m;i<l;i+=st){` +
            `p=ivec2((v1.zw+i*opdm)*uF);` +
            `tc=texelFetch(uB,p,0)*HEIGHT;` +
            `${core}` +
          `}`;

    return `${Utils.GLSL.DEFINE.HEIGHT}` +
    `${Utils.GLSL.DEFINE.Z}` +

    `in vec2 ` +
      `v0;` +
    `in vec4 ` +
      `v1;` +
    `flat in vec4 ` +
      `v2,` +
      `v3,` +
      `v4,` +
      `v5;` +

    `uniform vec2 ` +
      `uF;` +
    `uniform vec2 ` +
      `uG;` +
    `uniform vec3 ` +
      `uO;` +
    `uniform sampler2D ` +
      `uB,` +
      `uC,` +
      `uM,` +
      `uN;` +

    `out vec4 ` +
      `oCl;` +

    `${Utils.GLSL.RANDOM}` +

    `void main(){` +
      `float ` +
        `vol=v5.a;` +

      `if(vol<=0.)discard;` +

      `vec2 ` +
        `tUv=v0*uF,` +
        `tCnt=v1.zw*uF;` +

      `vec4 ` +
        `tc=texelFetch(uB,ivec2(tUv),0);` +

      `int ` + 
        `flg=int(v4.y);` +

      `float ` +
        `ph=tc.g*HEIGHT,` +
        `spc=0.;` +

      `vec3 ` +
        `sf=vec3(tUv,ph),` +
        `lp=vec3(tCnt,v4.z),` +
        `sftla=lp-sf;` +

      `if(v4.x<1.){` +
        `float ` + 
          `d=length(sftla)/v2.x,` +
          `od=1.-d,` +
          `dv=(flg&16)>0?pow(od,v3.z):1.;` +

        `vol*=dv;` +

        `float ` +
          `slh=(v4.z-ph)/HEIGHT;` +

        `vec2 ` +
          `sl=vec2(` +
            `slh*v2.w-v1.x*v2.z,` +
            `slh*v2.z+v1.x*v2.w` +
          `);` +

        `if(` +
          `vol<=0.||` + 
          `od<=0.||` +
          `atan(` +
            `sl.x,` +
            `length(vec2(sl.y,v1.y))` +
          `)+${Utils.ALPHA}-v3.w<0.)discard;` +
      `}` +

      `float ` +
        `fltDst=distance(tCnt,tUv),` +
        `shdw=1.;` +

      `if((flg&2)>0){` +
        `vec3 ` +
          `nm=uO.z>0.` + 
            `?normalize((texture(uM,v0).rgb*2.-1.)*Z.yzy)` +
            `:Z.xxy,` +
          `sftl=normalize(sftla),` +
          `sftv=normalize(vec3(` +
            `(flg&8)>0?uF*.5:tUv,` +
            `HEIGHT` +
          `)-sf);` +

        `vol*=dot(nm,sftl);` +

        `if(vol<=0.)discard;` +
        
        `float ` + 
          `shn=uO.y>0.?texture(uN,v0).r:tc.b,` +
          `ndh=max(dot(nm,normalize(sftl+sftv)),0.);` +

        `spc=pow(ndh,32.)*shn*v3.y;` +
      `}` +

      `if((flg&1)>0){` +
        `ivec2 ` +
          `p;` +

        `vec2 ` +
          `opd=(tUv-tCnt)/fltDst,` +
          `opdm=opd/uF;` +

        `float ` +
          `i,` +
          `pc,` +
          `st=max(1.,ceil(fltDst/v3.x)),` + // loop step length
          `l=min(fltDst-st,4096.),` +
          `m=max(st,l-v2.y);` +
        
        `if((flg&4)>0)` + 
          `${loop(`if(tc.g>=v4.z)discard;`)}` +
        `else{` +
          `float ` +
            `opdL=length(opd),` + // horizontal step
            `hst=(ph-v4.z)/fltDst,` + // vertical step
            `rnd=v4.w*rand(v0+floor(uG)/floor(uF));` +
          `${loop(
            `st+=rnd;` +
            `pc=v4.z+i*hst;` +
            `shdw*=mix(1.,(fltDst-i*opdL)/v2.y,step(tc.r,pc)*step(pc,tc.g));`
          )}` +
        `}` +
      `}` +

      `vec3 ` +
        `stCl=uO.x>0.?texture(uC,v0).rgb:Z.yyy;` +

      `oCl=vec4((stCl+spc)*v5.rgb*vol*min(shdw,1.),1);` +
    `}`;
  }
}