Search code examples
javascriptwebglwebgl2transform-feedback

Animate vertex positions with webgl2 transform feedback using two programs


I try to follow example from webgl2fundamentals regarding transform feedback.

My goal is to create animation of vertex positions in a way that there are two programs:

  1. doing animation calculations each frame and writing output to a buffer
  2. doing drawing on screen using buffer with calculated values from 1st program.

I made this code snippet on codesandbox.

It seems that varying vPosition from first program is not being feed to the second program. What am I doing wrong?

Is setup enough to handle this use case? I have one TFO and one VAO and one buffer. What would be best approach?

The output should look something like this: animation

here is the code:

const canvas = document.querySelector("canvas");
canvas.width = 500;
canvas.height = 500;

const gl = canvas.getContext("webgl2");

const genPointsVSGLSL = `#version 300 es
uniform float time;

in vec2 position;

out vec2 vPosition;

void main() {
  
    vPosition = vec2(position * (sin(time) + 1.0));
}
`;

const genPointsFSGLSL = `#version 300 es
void main() {
  discard;
}
`;

const drawVSGLSL = `#version 300 es
uniform float time;

in vec2 vPosition;

void main() {
  gl_PointSize = 20.0;
  gl_Position = vec4(vPosition, 0.0, 1.0);
}
`;

const drawFSGLSL = `#version 300 es
precision highp float;
out vec4 outColor;

void main() {
    outColor = vec4(0.0, 0.0, 0.0, 1.0);
}
`;

function generatePoints(num) {
  const arr = [];
  for (let i = 0; i < num; i++) {
    const u = i / num;
    const a = u * Math.PI * 2.0;
    arr.push(Math.cos(a) * 0.8, Math.sin(a) * 0.8);
  }

  return new Float32Array(arr);
}

const numPoints = 12;
let time = 0.0;

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.getProgramInfoLog(prg));
  }

  return prg;
};

// gen prog
const genProg = createProgram(gl, genPointsVSGLSL, genPointsFSGLSL, [
  "vPosition"
]);

gl.bindAttribLocation(genProg, 0, "position");
const timeLocGen = gl.getUniformLocation(genProg, "time");

const drawProg = createProgram(gl, drawVSGLSL, drawFSGLSL);

gl.bindAttribLocation(drawProg, 0, "vPosition");
const timeLocDraw = gl.getUniformLocation(drawProg, "time");

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.bufferData(gl.ARRAY_BUFFER, generatePoints(12), gl.DYNAMIC_DRAW);

gl.enableVertexAttribArray(0);

gl.vertexAttribPointer(
  0, // 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)
);

gl.bindVertexArray(null);

const tf = gl.createTransformFeedback();

function loop() {
  // update positions
  gl.useProgram(genProg);

  gl.enable(gl.RASTERIZER_DISCARD);

  gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);

  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, positionBuffer);

  gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);

  gl.beginTransformFeedback(gl.POINTS);

  gl.uniform1f(timeLocGen, time);

  gl.drawArrays(gl.POINTS, 0, numPoints);

  gl.endTransformFeedback();

  gl.disable(gl.RASTERIZER_DISCARD);

  // draw vertices

  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  gl.bindVertexArray(dotVertexArray);

  gl.useProgram(drawProg);

  gl.uniform1f(timeLocDraw, time);

  gl.drawArrays(gl.POINTS, 0, numPoints);
}

function animate() {
  time += 0.01;

  loop();

  window.requestAnimationFrame(animate);
}

animate();
<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <canvas></canvas>

    <script src="src/index.js"></script>
  </body>
</html>


Solution

  • Actually you only have 1 buffer. You cannot read and write the same buffer. This is undefined behavior. Read the points from the 1st buffer and write the transformed points to the 2nd buffer. Draw the points from the 2nd buffer.

    const canvas = document.querySelector("canvas");
    canvas.width = 500;
    canvas.height = 500;
    
    const gl = canvas.getContext("webgl2");
    
    const genPointsVSGLSL = `#version 300 es
    uniform float time;
    
    in vec2 position;
    
    out vec2 vPosition;
    
    void main() {
      
        vPosition = vec2(position * (sin(time) + 1.0)*0.5);
    }
    `;
    
    const genPointsFSGLSL = `#version 300 es
    void main() {
      discard;
    }
    `;
    
    const drawVSGLSL = `#version 300 es
    uniform float time;
    
    in vec2 vPosition;
    
    void main() {
      gl_PointSize = 20.0;
      gl_Position = vec4(vPosition, 0.0, 1.0);
    }
    `;
    
    const drawFSGLSL = `#version 300 es
    precision highp float;
    out vec4 outColor;
    
    void main() {
        outColor = vec4(0.0, 0.0, 0.0, 1.0);
    }
    `;
    
    function generatePoints(num) {
      const arr = [];
      for (let i = 0; i < num; i++) {
        const u = i / num;
        const a = u * Math.PI * 2.0;
        arr.push(Math.cos(a) * 0.8, Math.sin(a) * 0.8);
      }
    
      return new Float32Array(arr);
    }
    
    const numPoints = 12;
    let time = 0.0;
    
    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.getProgramInfoLog(prg));
      }
    
      return prg;
    };
    
    // gen prog
    const genProg = createProgram(gl, genPointsVSGLSL, genPointsFSGLSL, [
      "vPosition"
    ]);
    
    gl.bindAttribLocation(genProg, 0, "position");
    const timeLocGen = gl.getUniformLocation(genProg, "time");
    
    const drawProg = createProgram(gl, drawVSGLSL, drawFSGLSL);
    
    gl.bindAttribLocation(drawProg, 0, "vPosition");
    const timeLocDraw = gl.getUniformLocation(drawProg, "time");
    
    // source buffer
    const positionBuffer1 = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer1);
    gl.bufferData(gl.ARRAY_BUFFER, generatePoints(12), gl.DYNAMIC_DRAW);
    
    // trnsform feedback VAO
    const dotVertexArray1 = gl.createVertexArray();
    gl.bindVertexArray(dotVertexArray1);
    gl.enableVertexAttribArray(0);
    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
    
    // traget buffer
    const positionBuffer2 = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer2);
    gl.bufferData(gl.ARRAY_BUFFER, 12*2*4, gl.DYNAMIC_DRAW);
    
    // draw points VAO
    const dotVertexArray2 = gl.createVertexArray();
    gl.bindVertexArray(dotVertexArray2);
    gl.enableVertexAttribArray(0);
    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
    
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    
    const tf = gl.createTransformFeedback();
    
    function loop() {
      // update positions
      gl.useProgram(genProg);
    
      gl.enable(gl.RASTERIZER_DISCARD);
      gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
      gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, positionBuffer2);
      gl.beginTransformFeedback(gl.POINTS);
      gl.uniform1f(timeLocGen, time);
      gl.bindVertexArray(dotVertexArray1);
      gl.drawArrays(gl.POINTS, 0, numPoints);
      gl.endTransformFeedback();
      gl.disable(gl.RASTERIZER_DISCARD);
      gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
    
      // draw vertices
    
      gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    
      gl.bindVertexArray(dotVertexArray2);
      gl.useProgram(drawProg);
      gl.uniform1f(timeLocDraw, time);
      gl.drawArrays(gl.POINTS, 0, numPoints);
    }
    
    function animate() {
      time += 0.01;
    
      loop();
    
      window.requestAnimationFrame(animate);
    }
    
    animate();
    <canvas></canvas>