29 Mar 2018

Using a Canvas Texture inside Custom Shaders

Learn how to use advanced yet simple technique to render a 2d canvas into a custom shader, opening very interesting use cases ...

This post can be seen as part III coming after two of my previous posts:

High-Performance 3D markups with PointCloud in the Forge Viewer

Using a Dynamic Texture inside Custom Shaders

This time I am showing you another trick in order to display advanced graphics inside our custom shader: using a texture generated from a 2d canvas.

This allows to easily display more advanced shapes without the hassle to implement logic generating the texture. An obvious use case for that approach is displaying text inside our shader. It is also pretty easy to update that text by simply updating the shader similarly to the approach described in the previous article.

Here is how easy it is to use a canvas to generate a texture with Three.js:

const generateCanvasTexture = () => {

  const canvas = document.createElement("canvas")
  const ctx = canvas.getContext('2d')

  ctx.font = '20pt Arial'
  ctx.textAlign = 'center'
  ctx.textBaseline = 'middle'
  ctx.fillText(new Date().toLocaleString(),
    canvas.width / 2, canvas.height / 2)

  const canvasTexture = new THREE.Texture(canvas)

  canvasTexture.needsUpdate = true
  canvasTexture.flipX = false
  canvasTexture.flipY = false

  return canvasTexture
}

You can simply switch the generateTexture() method by the new generateCanvasTexture() in the previous shader code and go to town :)

  /////////////////////////////////////////////////////////
  // 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 generateCanvasTexture = () => {

      const canvas = document.createElement("canvas")
      const ctx = canvas.getContext('2d')

      ctx.font = '20pt Arial'
      ctx.textAlign = 'center'
      ctx.textBaseline = 'middle'
      ctx.fillText(new Date().toLocaleString(),
        canvas.width / 2, canvas.height / 2)

      const canvasTexture = new THREE.Texture(canvas)

      canvasTexture.needsUpdate = true
      canvasTexture.flipX = false
      canvasTexture.flipY = false

      return canvasTexture
    }

    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 = generateCanvasTexture()

        texture.needsUpdate = true

      },
      material
    }
  }

See complete sample code at Viewing.Extension.PointCloudMarkup.

 

Related Article