Search code examples
three.jsglslwebglwebgl2

WebGL2: How to render via shaders onto a TEXTURE_3D


I've read that WebGL2 gives us access to 3d textures. I'm trying to use this to perform some GPU-side computations and then store the output in a 64x64x64 3D texture. The render flow is

compute shader -> render to 3dTexture -> read shader -> render to screen

This is my simple compute shader, the texture's RGB channels should correspond to the XYZ fragment coordinates.

#version 300 es
precision mediump sampler3D;
precision highp float;
layout(location = 0) out highp vec4 pc_fragColor;

void main() {
    vec3 color = vec3(gl_FragCoord.x / 64.0, gl_FragCoord.y / 64.0, gl_FragDepth);
    pc_fragColor.rgb = color;
    pc_fragColor.a = 1.0;
}

However, this only seems to be rendering to a single "slice" of the 3DTexture, where depth is 0.0. All subsequent depths from 1 to 63 px remain black:

enter image description here

I've created a working demo below to demonstrate this issue.

var renderer, target3d, camera;
const SIDE = 64;
var computeMaterial, computeMesh;
var readDataMaterial, readDataMesh, 
    read3dTargetMaterial, read3dTargetMesh;
var textField = document.querySelector("#textField");

function init() {
    // Three.js boilerplate
    renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setClearColor(new THREE.Color(0x000000), 1.0);
    document.body.appendChild(renderer.domElement);
    camera = new THREE.Camera();

    // Create volume material to render to 3dTexture
    computeMaterial = new THREE.RawShaderMaterial({
        vertexShader: SIMPLE_VERTEX,
        fragmentShader: COMPUTE_FRAGMENT,
        uniforms: {
            uZCoord: { value: 0.0 },
        },
        depthTest: false,
    });
    computeMaterial.type = "VolumeShader";
    computeMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), computeMaterial);

    // Left material, reads Data3DTexture
    readDataMaterial = new THREE.RawShaderMaterial({
        vertexShader: SIMPLE_VERTEX,
        fragmentShader: READ_FRAGMENT,
        uniforms: {
            uZCoord: { value: 0.0 },
            tDiffuse: { value: create3dDataTexture() }
        },
        depthTest: false
    });
    readDataMaterial.type = "DebugShader";
    readDataMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), readDataMaterial);

    // Right material, reads 3DRenderTarget texture
    target3d = new THREE.WebGL3DRenderTarget(SIDE, SIDE, SIDE);
    target3d.depthBuffer = false;

    read3dTargetMaterial = readDataMaterial.clone();
    read3dTargetMaterial.uniforms.tDiffuse.value = target3d.texture;
    read3dTargetMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), read3dTargetMaterial);
}

// Creates 3D texture with RGB gradient along the XYZ axes
function create3dDataTexture() {
    const d = new Uint8Array( SIDE * SIDE * SIDE * 4 );
    window.dat = d;
    let i4 = 0;

    for ( let z = 0; z < SIDE; z ++ ) {
        for ( let y = 0; y < SIDE; y ++ ) {
            for ( let x = 0; x < SIDE; x ++ ) {
                d[i4 + 0] = (x / SIDE) * 255;
                d[i4 + 1] = (y / SIDE) * 255;
                d[i4 + 2] = (z / SIDE) * 255;
                d[i4 + 3] = 1.0;
                i4 += 4;
            }
        }
    }

    const texture = new THREE.Data3DTexture( d, SIDE, SIDE, SIDE );
    texture.format = THREE.RGBAFormat;
    texture.minFilter = THREE.NearestFilter;
    texture.magFilter = THREE.NearestFilter;
    texture.unpackAlignment = 1;
    texture.needsUpdate = true;

    return texture;
}

function onResize() {
    renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate(t) {
    // Render volume shader to target3d buffer
    renderer.setRenderTarget(target3d);
    renderer.render(computeMesh, camera);

    // Update z texture coordinate along sine wave
    renderer.autoClear = false;
    const sinZCoord = Math.sin(t / 1000);
    readDataMaterial.uniforms.uZCoord.value = sinZCoord;
    read3dTargetMaterial.uniforms.uZCoord.value = sinZCoord;
    textField.innerText = sinZCoord.toFixed(4);

    // Render data3D texture to screen
    renderer.setViewport(0, window.innerHeight - SIDE*4, SIDE * 4, SIDE * 4);
    renderer.setRenderTarget(null);
    renderer.render(readDataMesh, camera);

    // Render 3dRenderTarget texture to screen
    renderer.setViewport(SIDE * 4, window.innerHeight - SIDE*4, SIDE * 4, SIDE * 4);
    renderer.setRenderTarget(null);
    renderer.render(read3dTargetMesh, camera);

    renderer.autoClear = true;
    requestAnimationFrame(animate);
}

init();
window.addEventListener("resize", onResize);
requestAnimationFrame(animate);
html, body {
    width: 100%;
    height: 100%;
    margin: 0;
    overflow: hidden;
}
#title {
    position: absolute;
    top: 0;
    left: 0;
    color: white;
    font-family: sans-serif;
}
h3 {
    margin: 2px;
}
<div id="title">
    <h3>texDepth</h3><h3 id="textField"></h3>
</div>
<script src="https://threejs.org/build/three.js"></script>
<script>

/////////////////////////////////////////////////////////////////////////////////////
// Compute frag shader
// It should output an RGB gradient in the XYZ axes to the 3DRenderTarget
// But gl_FragCoord.z is always 0.5 and gl_FragDepth is always 0.0

const COMPUTE_FRAGMENT = `#version 300 es
precision mediump sampler3D;
precision highp float;
precision highp int;
layout(location = 0) out highp vec4 pc_fragColor;

void main() {
    vec3 color = vec3(gl_FragCoord.x / 64.0, gl_FragCoord.y / 64.0, gl_FragDepth);
    pc_fragColor.rgb = color;
    pc_fragColor.a = 1.0;
}`;

/////////////////////////////////////////////////////////////////////////////////////
// Reader frag shader
// Samples the 3D texture along uv.x, uv.y, and uniform Z coordinate

const READ_FRAGMENT = `#version 300 es
precision mediump sampler3D;
precision highp float;
precision highp int;
layout(location = 0) out highp vec4 pc_fragColor;

in vec2 vUv;
uniform sampler3D tDiffuse;
uniform float uZCoord;

void main() {
    vec3 UV3 = vec3(vUv.x, vUv.y, uZCoord);
    vec3 diffuse = texture(tDiffuse, UV3).rgb;
    pc_fragColor.rgb = diffuse;
    pc_fragColor.a = 1.0;
}
`;

/////////////////////////////////////////////////////////////////////////////////////
// Simple vertex shader,
// renders a full-screen quad with UVs without any transformations
const SIMPLE_VERTEX = `#version 300 es
precision highp float;
precision highp int;

in vec2 uv;
in vec3 position;
out vec2 vUv;

void main() {
    vUv = uv;
    gl_Position = vec4(position, 1.0);
}`;


/////////////////////////////////////////////////////////////////////////////////////

</script>

  • On the left side, I’m sampling a Data3DTexture that I created via JavaScript. The blue channel smoothly transitions as I move up and down the depth axis, as expected.
  • On the right side I’m sampling the WebGL3DRenderTarget texture rendered in the frag shader I showed above. As you can see, it's only rendering to the texture when the depth coordinate is 0.0. All the other “slices” are black.

enter image description here enter image description here

How can I render my computations to all 64 depth slices? I'm using Three.js for this demo, but I could use any other library like TWGL or vanilla WebGL to achieve the same results.


Solution

  • It doesn't look documented but you can use a second argument to setRenderTarget to set the "layer" of the 3d render target to render to. Here are the changes to make:

    1. When rendering to the render target perform a new render for every layer:
    for ( let i = 0; i < SIDE; i ++ ) {
       
      // set the uZCoord color value for the shader
      computeMesh.material.uniforms.uZCoord.value = i / (SIDE - 1);
    
      // Set the 3d target "layer" to render into before rendering
      renderer.setRenderTarget(target3d, i);
      renderer.render(computeMesh, camera);
    
    } 
    
    1. Use the "uZCoord" uniform in the compute fragment shader:
        uniform float uZCoord;
        void main() {
            vec3 color = vec3(gl_FragCoord.x / 64.0, gl_FragCoord.y / 64.0, uZCoord);
            pc_fragColor.rgb = color;
            pc_fragColor.a = 1.0;
        }
    

    Other than that I don't believe theres a way to render to the full 3d volume of the target in a single draw call. This three.js example shows how to do this but with render target arrays, as well:

    https://threejs.org/examples/?q=array#webgl2_rendertarget_texture2darray

    var renderer, target3d, camera;
    const SIDE = 64;
    var computeMaterial, computeMesh;
    var readDataMaterial, readDataMesh, 
        read3dTargetMaterial, read3dTargetMesh;
    var textField = document.querySelector("#textField");
    
    function init() {
        // Three.js boilerplate
        renderer = new THREE.WebGLRenderer({antialias: true});
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(new THREE.Color(0x000000), 1.0);
        document.body.appendChild(renderer.domElement);
        camera = new THREE.Camera();
    
        // Create volume material to render to 3dTexture
        computeMaterial = new THREE.RawShaderMaterial({
            vertexShader: SIMPLE_VERTEX,
            fragmentShader: COMPUTE_FRAGMENT,
            uniforms: {
                uZCoord: { value: 0.0 },
            },
            depthTest: false,
        });
        computeMaterial.type = "VolumeShader";
        computeMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), computeMaterial);
    
        // Left material, reads Data3DTexture
        readDataMaterial = new THREE.RawShaderMaterial({
            vertexShader: SIMPLE_VERTEX,
            fragmentShader: READ_FRAGMENT,
            uniforms: {
                uZCoord: { value: 0.0 },
                tDiffuse: { value: create3dDataTexture() }
            },
            depthTest: false
        });
        readDataMaterial.type = "DebugShader";
        readDataMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), readDataMaterial);
    
        // Right material, reads 3DRenderTarget texture
        target3d = new THREE.WebGL3DRenderTarget(SIDE, SIDE, SIDE);
        target3d.depthBuffer = false;
    
        read3dTargetMaterial = readDataMaterial.clone();
        read3dTargetMaterial.uniforms.tDiffuse.value = target3d.texture;
        read3dTargetMesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), read3dTargetMaterial);
    }
    
    // Creates 3D texture with RGB gradient along the XYZ axes
    function create3dDataTexture() {
        const d = new Uint8Array( SIDE * SIDE * SIDE * 4 );
        window.dat = d;
        let i4 = 0;
    
        for ( let z = 0; z < SIDE; z ++ ) {
            for ( let y = 0; y < SIDE; y ++ ) {
                for ( let x = 0; x < SIDE; x ++ ) {
                    d[i4 + 0] = (x / SIDE) * 255;
                    d[i4 + 1] = (y / SIDE) * 255;
                    d[i4 + 2] = (z / SIDE) * 255;
                    d[i4 + 3] = 1.0;
                    i4 += 4;
                }
            }
        }
    
        const texture = new THREE.Data3DTexture( d, SIDE, SIDE, SIDE );
        texture.format = THREE.RGBAFormat;
        texture.minFilter = THREE.NearestFilter;
        texture.magFilter = THREE.NearestFilter;
        texture.unpackAlignment = 1;
        texture.needsUpdate = true;
    
        return texture;
    }
    
    function onResize() {
        renderer.setSize(window.innerWidth, window.innerHeight);
    }
    
    function animate(t) {
        for ( let i = 0; i < SIDE; i ++ ) {
       
          // Render volume shader to target3d buffer
          computeMesh.material.uniforms.uZCoord.value = i / ( SIDE - 1 );
          renderer.setRenderTarget(target3d, i);
          renderer.render(computeMesh, camera);
    
        } 
    
        // Update z texture coordinate along sine wave
        renderer.autoClear = false;
        const sinZCoord = Math.sin(t / 1000);
        readDataMaterial.uniforms.uZCoord.value = sinZCoord;
        read3dTargetMaterial.uniforms.uZCoord.value = sinZCoord;
        textField.innerText = sinZCoord.toFixed(4);
    
        // Render data3D texture to screen
        renderer.setViewport(0, window.innerHeight - SIDE*4, SIDE * 4, SIDE * 4);
        renderer.setRenderTarget(null);
        renderer.render(readDataMesh, camera);
    
        // Render 3dRenderTarget texture to screen
        renderer.setViewport(SIDE * 4, window.innerHeight - SIDE*4, SIDE * 4, SIDE * 4);
        renderer.setRenderTarget(null);
        renderer.render(read3dTargetMesh, camera);
    
        renderer.autoClear = true;
        requestAnimationFrame(animate);
    }
    
    init();
    window.addEventListener("resize", onResize);
    requestAnimationFrame(animate);
    html, body {
        width: 100%;
        height: 100%;
        margin: 0;
        overflow: hidden;
    }
    #title {
        position: absolute;
        top: 0;
        left: 0;
        color: white;
        font-family: sans-serif;
    }
    h3 {
        margin: 2px;
    }
    <div id="title">
        <h3>texDepth</h3><h3 id="textField"></h3>
    </div>
    <script src="https://threejs.org/build/three.js"></script>
    <script>
    
    /////////////////////////////////////////////////////////////////////////////////////
    // Compute frag shader
    // It should output an RGB gradient in the XYZ axes to the 3DRenderTarget
    // But gl_FragCoord.z is always 0.5 and gl_FragDepth is always 0.0
    
    const COMPUTE_FRAGMENT = `#version 300 es
    precision mediump sampler3D;
    precision highp float;
    precision highp int;
    layout(location = 0) out highp vec4 pc_fragColor;
    
    uniform float uZCoord;
    void main() {
        vec3 color = vec3(gl_FragCoord.x / 64.0, gl_FragCoord.y / 64.0, uZCoord);
        pc_fragColor.rgb = color;
        pc_fragColor.a = 1.0;
    }`;
    
    /////////////////////////////////////////////////////////////////////////////////////
    // Reader frag shader
    // Samples the 3D texture along uv.x, uv.y, and uniform Z coordinate
    
    const READ_FRAGMENT = `#version 300 es
    precision mediump sampler3D;
    precision highp float;
    precision highp int;
    layout(location = 0) out highp vec4 pc_fragColor;
    
    in vec2 vUv;
    uniform sampler3D tDiffuse;
    uniform float uZCoord;
    
    void main() {
        vec3 UV3 = vec3(vUv.x, vUv.y, uZCoord);
        vec3 diffuse = texture(tDiffuse, UV3).rgb;
        pc_fragColor.rgb = diffuse;
        pc_fragColor.a = 1.0;
    }
    `;
    
    /////////////////////////////////////////////////////////////////////////////////////
    // Simple vertex shader,
    // renders a full-screen quad with UVs without any transformations
    const SIMPLE_VERTEX = `#version 300 es
    precision highp float;
    precision highp int;
    
    in vec2 uv;
    in vec3 position;
    out vec2 vUv;
    
    void main() {
        vUv = uv;
        gl_Position = vec4(position, 1.0);
    }`;
    
    
    /////////////////////////////////////////////////////////////////////////////////////
    
    </script>