Search code examples
webgl2glreadpixelsglteximage2d

pass data between shader programs


Ok I'm going to keep this as simple as possible. I want to pass data between shader programs. I'm using readPixels currently to do that but I feel it may be slowing operations down and I'm exploring faster options.

what my program does:

  1. program1 does my rendering to the canvas.
  2. program2 does some wonderful operations in it's shaders that I want to pass to program1.

MY QUESTIONS:

  1. is it possible to use the vbo from program2 and pass that to program1 for rendering? From what it sounds like in the link I give below, you can't share data across contexts, meaning the data from one buffer can't be used for another. But maybe I'm missing something.
  2. I believe the method mentioned in this article would do what I'm looking for by rendering to a canvas and then using texImage2D to update program1 (Copy framebuffer data from one WebGLRenderingContext to another?). Am I correct? If so, would this be faster than using readPixels? ( i ask because if using texImage2D is about the same I won't bother ).

thanks in advance to anyone who answers.


Solution

  • The normal way to pass data from one shader to the next is to render to a texture (by attaching that texture to a framebuffer). Then pass that texture to the second shader.

    function main() {
      const gl = document.querySelector('canvas').getContext('webgl2');
      if (!gl) {
        return alert('need webgl2');
      }
    
      const vs1 = `#version 300 es
      void main () {
        gl_Position = vec4(0, 0, 0, 1);
        gl_PointSize = 64.0;
      }
      `;
      
      const fs1 = `#version 300 es
      precision highp float;
      out vec4 myOutColor;
      void main() {
        myOutColor = vec4(fract(gl_PointCoord * 4.), 0, 1);
      }
      `;
      
      const vs2 = `#version 300 es
      in vec4 position;
      void main () {
        gl_Position = position;
        gl_PointSize = 32.0;
      }
      `;
      
      const fs2 = `#version 300 es
      precision highp float;
      uniform sampler2D tex;
      out vec4 myOutColor;
      void main() {
        myOutColor = texture(tex, gl_PointCoord);
      }
      `;
    
      // make 2 programs
      const prg1 = twgl.createProgram(gl, [vs1, fs1]);
      const prg2 = twgl.createProgram(gl, [vs2, fs2]);
    
      // make a texture
      const tex = gl.createTexture();
      const texWidth = 64;
      const texHeight = 64;
      gl.bindTexture(gl.TEXTURE_2D, tex);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, texWidth, texHeight, 0,
                    gl.RGBA, gl.UNSIGNED_BYTE, null);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    
      // attach texture to framebuffer
      const fb = gl.createFramebuffer();
      gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
                              gl.TEXTURE_2D, tex, 0);
    
      // render to texture
      gl.viewport(0, 0, texWidth, texHeight);
      gl.useProgram(prg1);
      gl.drawArrays(gl.POINTS, 0, 1);
      
      // render texture (output of prg1) to canvas using prg2
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
      gl.useProgram(prg2);
      // note: the texture is already bound to texture unit 0
      // and uniforms default to 0 so the texture is already setup
      const posLoc = gl.getAttribLocation(prg2, 'position')
      const numDraws = 12
      for (let i = 0; i < numDraws; ++i) {
        const a = i / numDraws * Math.PI * 2;
        gl.vertexAttrib2f(posLoc, Math.sin(a) * .7, Math.cos(a) * .7);
        gl.drawArrays(gl.POINTS, 0, 1);
      }
    }
    main();
    <script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
    <canvas></canvas>

    You can also use "transform feedback" to store the outputs of a vertex shader to one or more buffers and of course those buffers can be used as input to another shader.

    // this example from
    // https://webgl2fundamentals.org/webgl/lessons/resources/webgl-state-diagram.html?exampleId=transform-feedback
    const canvas = document.querySelector('canvas');
    const gl = canvas.getContext('webgl2');
    
    const genPointsVSGLSL = `#version 300 es
    uniform int numPoints;
    out vec2 position;
    out vec4 color;
    
    #define PI radians(180.0)
    
    void main() {
        float u = float(gl_VertexID) / float(numPoints);
        float a = u * PI * 2.0;
        position = vec2(cos(a), sin(a)) * 0.8;
        color = vec4(u, 0, 1.0 - u, 1);
    }
    `;
    
    const genPointsFSGLSL = `#version 300 es
    void main() {
      discard;
    }
    `;
    
    const drawVSGLSL = `#version 300 es
    in vec4 position;
    in vec4 color;
    
    out vec4 v_color;
    
    void main() {
      gl_PointSize = 20.0;
      gl_Position = position;
      v_color = color;
    }
    `;
    
    const drawFSGLSL = `#version 300 es
    precision highp float;
    
    in vec4 v_color;
    
    out vec4 outColor;
    
    void main() {
        outColor = v_color;
    }
    `;
    
    const createShader = function(gl, type, glsl) {
      const shader = gl.createShader(type)
      gl.shaderSource(shader, glsl)
      gl.compileShader(shader)
      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        throw new Error(gl.getShaderInfoLog(shader))
      }
      return shader
    };
    
    const createProgram = function(gl, vsGLSL, fsGLSL, outVaryings) {
      const vs = createShader(gl, gl.VERTEX_SHADER, vsGLSL)
      const fs = createShader(gl, gl.FRAGMENT_SHADER, fsGLSL)
      const prg = gl.createProgram()
      gl.attachShader(prg, vs)
      gl.attachShader(prg, fs)
      if (outVaryings) {
        gl.transformFeedbackVaryings(prg, outVaryings, gl.SEPARATE_ATTRIBS)
      }
      gl.linkProgram(prg)
      if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {
        throw new Error(gl.getProgramParameter(prg))
      }
      return prg
    };
    
    const genProg = createProgram(gl, genPointsVSGLSL, genPointsFSGLSL, ['position', 'color']);
    const drawProg = createProgram(gl, drawVSGLSL, drawFSGLSL);
    
    const numPointsLoc = gl.getUniformLocation(genProg, 'numPoints');
    
    const posLoc = gl.getAttribLocation(drawProg, 'position');
    const colorLoc = gl.getAttribLocation(drawProg, 'color');
    
    const numPoints = 24;
    
    // make a vertex array and attach 2 buffers
    // one for 2D positions, 1 for colors.
    const dotVertexArray = gl.createVertexArray();
    gl.bindVertexArray(dotVertexArray);
    
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, numPoints * 2 * 4, gl.DYNAMIC_DRAW);
    gl.enableVertexAttribArray(posLoc);
    gl.vertexAttribPointer(
        posLoc,       // location
        2,            // size (components per iteration)
        gl.FLOAT,     // type of to get from buffer
        false,        // normalize
        0,            // stride (bytes to advance each iteration)
        0,            // offset (bytes from start of buffer)
    );
    
    const colorBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, numPoints * 4 * 4, gl.DYNAMIC_DRAW);
    gl.enableVertexAttribArray(colorLoc);
    gl.vertexAttribPointer(
        colorLoc,   // location
        4,          // size (components per iteration)
        gl.FLOAT,   // type of to get from buffer
        false,      // normalize
        0,          // stride (bytes to advance each iteration)
        0,          // offset (bytes from start of buffer)
    );
    
    // This is not really needed but if we end up binding anything
    // to ELEMENT_ARRAY_BUFFER, say we are generating indexed geometry
    // we'll change cubeVertexArray's ELEMENT_ARRAY_BUFFER. By binding
    // null here that won't happen.
    gl.bindVertexArray(null);
    
    // setup a transform feedback object to write to
    // the position and color buffers
    const tf = gl.createTransformFeedback();
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, positionBuffer);
    gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, colorBuffer);
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
    
    // above this line is initialization code
    // --------------------------------------
    // below is rendering code.
    
    // --------------------------------------
    // First compute points into buffers
    
    // no need to call the fragment shader
    gl.enable(gl.RASTERIZER_DISCARD);
    
    // unbind the buffers so we don't get errors.
    gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    
    gl.useProgram(genProg);
    
    // generate numPoints of positions and colors
    // into the buffers
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
    gl.beginTransformFeedback(gl.POINTS);
    gl.uniform1i(numPointsLoc, numPoints);
    gl.drawArrays(gl.POINTS, 0, numPoints);
    gl.endTransformFeedback();
    gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
    
    // turn on using fragment shaders again
    gl.disable(gl.RASTERIZER_DISCARD);
    
    // --------------------------------------
    // Now draw using the buffers we just computed
    
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    
    gl.bindVertexArray(dotVertexArray);
    gl.useProgram(drawProg);
    gl.drawArrays(gl.POINTS, 0, numPoints);
    <script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
    <canvas></canvas>

    Also this answer might be useful.