5 Jan 2018

Using a Dynamic Texture inside Custom Shaders

    In my last article High-Performance 3D markups with PointCloud in the Forge Viewer I explored how to turn a PointCloud entity into a fast-rendering markup 3D feature. It was using a custom shader to support rendering an image loaded as a texture. I wanted to push that a bit further by using a texture programmatically generated instead of a simple image, which brings a lot of flexibility and potential uses to the demo.

    Below is the most relevant part of the code which powers that demo. You can refer to the complete sample at Viewing.Extension.PointCloudMarkup and the live demo is running on that url: PointCloudMarkup

/////////////////////////////////////////////////////////
// Generates custom shader using an updatable 
// dynamic texture generated programmatically
//
/////////////////////////////////////////////////////////
createShader (options) {

  // Vertex Shader code
  const vertexShader = options.vertexShader || `
    attribute float pointSize;
    attribute vec4 color;
    varying vec4 vColor;
    void main() {
      vec4 vPosition = modelViewMatrix * vec4(position, 1.0);
      gl_Position = projectionMatrix * vPosition;
      gl_PointSize = pointSize;
      vColor = color;
    }
  `

  // Fragment Shader code
  const fragmentShader = options.fragmentShader || `
    uniform sampler2D texture;
    varying vec4 vColor;
    void main() {
      vec4 tex = texture2D(texture, gl_PointCoord);
      if (tex.a < 0.2) discard;
      if (vColor.a == 0.0) {
        gl_FragColor = vec4(tex.r, tex.g, tex.b, tex.a);
      } else {
        gl_FragColor = vColor;
      }
    }
  `

  const tex = options.texture || defaultTex

  // Shader material parameters
  const shaderParams = options.shaderParams || {
      side: THREE.DoubleSide,
      depthWrite: false,
      depthTest: false,
      fragmentShader,
      vertexShader,
      opacity: 0.5,
      attributes: {
        pointSize: {
          type: 'f',
          value: []
        },
        color: {
          type: 'v4',
          value: []
        }
      },
      uniforms: {
        texture: {
          value: THREE.ImageUtils.loadTexture(tex),
          type: 't'
        }
      }
    }

  // creates shader material
  const material =
    new THREE.ShaderMaterial(
      shaderParams)

  const generateTexture = (size, radius) => {

    const pixels = []

    for (let u = 0; u < size; ++u) {

      for (let v = 0; v < size ; ++v) {

        const dist = Math.sqrt(
          (u/size - 0.5) * (u/size - 0.5) +
          (v/size - 0.5) * (v/size - 0.5))

       if (dist < 0.1) {

         pixels.push(0xff, 0x00, 0x00, 0xff)

       } else if (dist < (radius - 0.05)) {

          pixels.push(0xff, 0x00, 0x00, 0x00)

        } else if (dist < radius) {

          pixels.push(0xff, 0x00, 0x00, 0xff)

        } else {

          pixels.push(0x00, 0x00, 0x00, 0x00)
        }
      }
    }

    const dataTexture = new THREE.DataTexture (
      Uint8Array.from (pixels),
      size, size,
      THREE.RGBAFormat,
      THREE.UnsignedByteType,
      THREE.UVMapping
    )

    dataTexture.minFilter = THREE.LinearMipMapLinearFilter
    dataTexture.magFilter = THREE.LinearFilter // THREE.NearestFilter
    dataTexture.needsUpdate = true

    return dataTexture
  }

  const stopwatch = new Stopwatch()

  let radius = 0.0

  return {
    setTexture: (tex) => {

      const {texture} = shaderParams.uniforms

      texture.value = THREE.ImageUtils.loadTexture(tex)

      texture.needsUpdate = true

    },
    update: () => {

      const dt = stopwatch.getElapsedMs() * 0.001

      radius += dt * 0.25

      radius = radius > 0.5 ? 0.0 : radius

      const {texture} = shaderParams.uniforms

      texture.value = generateTexture(96, radius)

      texture.needsUpdate = true

    },
    material
  }
}

     Here is a quick recording that showcases that demo:

Related Article