baal_imago
baal_imago

Reputation: 313

Need help rendering positions to a texture using framebuffers in vanilla webgl

I'm trying to do position updates for particles being rendered as points using webgl. I've asked some questions before about the same project I'm playing around with here and here which lead me a fair bit on the way. Unfortunately, most of the answers use twgl which, to me, takes a lot of shortcuts which I have a hard time understanding (so I didn't want to just try to copy it either but start with the basics).

Basically, I'm trying to render to a texture with one framebuffer + program and then use this texture in another program.

I don't know if I'm failing to render to the posTexture, of if the posTexture gets successfully rendered and that it fails to load into the renderProgram afterwards (since both are happening in the 'blackbox' GPU).

I made a snippet here without the renderFramebuffer (it simply renders directly to canvas instead) to show the problem. The core of the problem is at the end of the javascript bit, the rest is setup (which may be related):

function initShaderProgram(gl, vShader, fShader) {
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vShader);
  gl.attachShader(shaderProgram, fShader);
  gl.linkProgram(shaderProgram);

  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    throw new Error('Unable to initiate webgl shaders. Breaking.');
  }

  return shaderProgram;
}

function loadShader(gl, type, source) {
  const shader = gl.createShader(type);

  gl.shaderSource(shader, source);

  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    let err = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw new Error(`Unable to compile shaders. ${err}`);
  }

  return shader;
}

const c = document.getElementById("c");
const gl = c.getContext('webgl2');
const amParticles = 1;

if (gl === null || gl === undefined) {
  throw new Error('Unable to initiate webgl context. Breaking.');
}

// Extensions used for anti aliasing in rendering dots
let ext = gl.getExtension('EXT_color_buffer_float');
if (!ext) {
  throw new Error("need EXT_color_buffer_float");
}

ext = gl.getExtension('EXT_float_blend');
if (!ext) {
  throw new Error("need EXT_float_blend");
}

// Setup programs
const VsPos = document.getElementById("posVs").textContent;
const FsPos = document.getElementById("posFs").textContent;
const VsRender = document.getElementById("renderVs").textContent;
const FsRender = document.getElementById("renderFs").textContent;

const vShaderRender = loadShader(gl,
  gl.VERTEX_SHADER, VsRender);
const vShaderPosUpd = loadShader(gl,
  gl.VERTEX_SHADER, VsPos);
const fShaderRender = loadShader(gl,
  gl.FRAGMENT_SHADER, FsRender);
const fShaderPosUpd = loadShader(gl,
  gl.FRAGMENT_SHADER, FsPos);

// Setup shader
const renderProgram = initShaderProgram(gl,
  vShaderRender, fShaderRender);
const posProgram = initShaderProgram(gl,
  vShaderPosUpd, fShaderPosUpd);

// Setup global GL settings
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

// Blending to allow opacity (probably unrelated)
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

// Setup posTexture to render new positions to
let posTexture, posFrameBuffer; {
  posTexture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, posTexture);
  // Make texture non-mips
  gl.texParameteri(gl.TEXTURE_2D,
    gl.TEXTURE_WRAP_S,
    gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D,
    gl.TEXTURE_WRAP_T,
    gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D,
    gl.TEXTURE_MIN_FILTER,
    gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D,
    gl.TEXTURE_MAG_FILTER,
    gl.NEAREST);
  const level = 0;
  const internalFormat = gl.RGBA32F;
  const border = 0;
  const format = gl.RGBA;
  const type = gl.FLOAT;
  // Example position pre-render
  const data = new Float32Array([.5, .5, 0, 0]);
  // height = 1, amount pixels = width, rgba = position
  gl.texImage2D(gl.TEXTURE_2D,
    level,
    internalFormat,
    amParticles,
    1,
    border,
    format,
    type,
    data);

  // Pos framebuffer
  posFrameBuffer = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER,
    posFrameBuffer);

  // Bind it to posTexture
  gl.framebufferTexture2D(gl.FRAMEBUFFER,
    gl.COLOR_ATTACHMENT0,
    gl.TEXTURE_2D,
    posTexture,
    level);

  if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !==
    gl.FRAMEBUFFER_COMPLETE) {
    console.error(`Something went wrong with setting up the the posFrameBuffer. Status: ${
    gl.checkFramebufferStatus(gl.FRAMEBUFFER)}`);
  }
}

gl.useProgram(posProgram);
gl.bindFramebuffer(gl.FRAMEBUFFER, posFrameBuffer);
gl.viewport(0, 0, amParticles, 1);
// Now (it should be?) drawing new positions to 
// texture posTexture
gl.drawArrays(gl.POINTS, 0, amParticles);

// Set new posTexture to texture unit 1
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, posTexture);

gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.useProgram(renderProgram);
// Set uniform location to texture unit 1
const loc = gl.getUniformLocation(renderProgram, "t0_pos_tex");
gl.uniform1i(loc, 1);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Should draw with new position
gl.drawArrays(gl.POINTS, 0, amParticles);
#c {
  width: 400px;
  height: 200px;
}

.hide {
  display: none;
}
<canvas id="c"></canvas>

<p>
  If the circle is at the <b>left</b> side of the canvas, rendered by the posProgram, the experiment is successfull.
</p>

<div id="posFs" class="hide"># version 300 es
  #define M_PI 3.1415927

  precision highp float;

  out vec4 outColor;

  // Only renders one particle for the sake
  // of the example to a predetermined position
  void main() {
  // New position to render to (should appear
  // top-left ish)
  float new_x = -.5;
  float new_y = .5;

  outColor = vec4(new_x, new_y, 0., 1.);
  }
</div>

<div id="posVs" class="hide">#version 300 es
  // Does nothing since the fragment shader sets
  // the new position depending on the pixel
  // which indicates which index of the texture
  // = index of the new positions to update
  void main() {}
</div>

<div id="renderVs" class="hide"># version 300 es
  #define M_PI 3.1415927

  uniform sampler2D t0_pos_tex;

  out vec4 color;

  void main() {
    vec4 t0_pos = texelFetch(t0_pos_tex, ivec2(gl_VertexID,     0), 0);
    gl_Position = vec4(t0_pos.x, t0_pos.y, 0., 1.);
    color = vec4(1., 1., 1., 1.);
    gl_PointSize = 50.0;
  }
</div>

<div id="renderFs" class="hide"># version 300 es
  precision highp float;

  in vec4 color;

  out vec4 outColor;

// Turns point into a circle and adds
// antialiasing to make it smoothly round
  void main() {
    float r = 0.0, delta = 0.0, alpha = 1.0;
    vec2 cxy = 2.0 * gl_PointCoord - 1.0;
    r = dot(cxy, cxy);
    delta = fwidth(r);
    alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, r);

    outColor = color * alpha;
  }
</div>

Upvotes: 1

Views: 237

Answers (1)

user128511
user128511

Reputation:

Your posVS vertex shader does nothing so nothing will be rendered by the fragment shader. In order to render something the vertex shader must either generate a point by setting gl_Position and gl_PointSize OR it must generate a line by being called twice and setting gl_Position to different values each time or a triangle by being called 3 times and setting gl_Position to different values each time so that's the first issue.

changed it to this

  void main() {
    // draw a single pixel
    gl_PointSize = 1.0;
    // in the center of the viewport
    gl_Position = vec4(0, 0, 0, 1);
  }

function initShaderProgram(gl, vShader, fShader) {
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vShader);
  gl.attachShader(shaderProgram, fShader);
  gl.linkProgram(shaderProgram);

  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    throw new Error('Unable to initiate webgl shaders. Breaking.');
  }

  return shaderProgram;
}

function loadShader(gl, type, source) {
  const shader = gl.createShader(type);

  gl.shaderSource(shader, source);

  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    let err = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw new Error(`Unable to compile shaders. ${err}`);
  }

  return shader;
}

const c = document.getElementById("c");
const gl = c.getContext('webgl2');
const amParticles = 1;

if (gl === null || gl === undefined) {
  throw new Error('Unable to initiate webgl context. Breaking.');
}

// Extensions used for anti aliasing in rendering dots
let ext = gl.getExtension('EXT_color_buffer_float');
if (!ext) {
  throw new Error("need EXT_color_buffer_float");
}

ext = gl.getExtension('EXT_float_blend');
if (!ext) {
  throw new Error("need EXT_float_blend");
}

// Setup programs
const VsPos = document.getElementById("posVs").textContent;
const FsPos = document.getElementById("posFs").textContent;
const VsRender = document.getElementById("renderVs").textContent;
const FsRender = document.getElementById("renderFs").textContent;

const vShaderRender = loadShader(gl,
  gl.VERTEX_SHADER, VsRender);
const vShaderPosUpd = loadShader(gl,
  gl.VERTEX_SHADER, VsPos);
const fShaderRender = loadShader(gl,
  gl.FRAGMENT_SHADER, FsRender);
const fShaderPosUpd = loadShader(gl,
  gl.FRAGMENT_SHADER, FsPos);

// Setup shader
const renderProgram = initShaderProgram(gl,
  vShaderRender, fShaderRender);
const posProgram = initShaderProgram(gl,
  vShaderPosUpd, fShaderPosUpd);

// Setup global GL settings
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

// Blending to allow opacity (probably unrelated)
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

// Setup posTexture to render new positions to
let posTexture, posFrameBuffer; {
  posTexture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, posTexture);
  // Make texture non-mips
  gl.texParameteri(gl.TEXTURE_2D,
    gl.TEXTURE_WRAP_S,
    gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D,
    gl.TEXTURE_WRAP_T,
    gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D,
    gl.TEXTURE_MIN_FILTER,
    gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D,
    gl.TEXTURE_MAG_FILTER,
    gl.NEAREST);
  const level = 0;
  const internalFormat = gl.RGBA32F;
  const border = 0;
  const format = gl.RGBA;
  const type = gl.FLOAT;
  // Example position pre-render
  const data = new Float32Array([.5, .5, 0, 0]);
  // height = 1, amount pixels = width, rgba = position
  gl.texImage2D(gl.TEXTURE_2D,
    level,
    internalFormat,
    amParticles,
    1,
    border,
    format,
    type,
    data);

  // Pos framebuffer
  posFrameBuffer = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER,
    posFrameBuffer);

  // Bind it to posTexture
  gl.framebufferTexture2D(gl.FRAMEBUFFER,
    gl.COLOR_ATTACHMENT0,
    gl.TEXTURE_2D,
    posTexture,
    level);

  if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !==
    gl.FRAMEBUFFER_COMPLETE) {
    console.error(`Something went wrong with setting up the the posFrameBuffer. Status: ${
    gl.checkFramebufferStatus(gl.FRAMEBUFFER)}`);
  }
}

gl.useProgram(posProgram);
gl.bindFramebuffer(gl.FRAMEBUFFER, posFrameBuffer);
gl.viewport(0, 0, amParticles, 1);
// Now (it should be?) drawing new positions to 
// texture posTexture
gl.drawArrays(gl.POINTS, 0, amParticles);

// Set new posTexture to texture unit 1
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, posTexture);

gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.useProgram(renderProgram);
// Set uniform location to texture unit 1
const loc = gl.getUniformLocation(renderProgram, "t0_pos_tex");
gl.uniform1i(loc, 1);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Should draw with new position
gl.drawArrays(gl.POINTS, 0, amParticles);
#c {
  width: 400px;
  height: 200px;
}

.hide {
  display: none;
}
<canvas id="c"></canvas>

<p>
  If the circle is at the <b>left</b> side of the canvas, rendered by the posProgram, the experiment is successfull.
</p>

<div id="posFs" class="hide"># version 300 es
  #define M_PI 3.1415927

  precision highp float;

  out vec4 outColor;

  // Only renders one particle for the sake
  // of the example to a predetermined position
  void main() {
  // New position to render to (should appear
  // top-left ish)
  float new_x = -.5;
  float new_y = .5;

  outColor = vec4(new_x, new_y, 0., 1.);
  }
</div>

<div id="posVs" class="hide">#version 300 es
  void main() {
    // draw a single pixel
    gl_PointSize = 1.0;
    // in the center of the viewport
    gl_Position = vec4(0, 0, 0, 1);
  }
</div>

<div id="renderVs" class="hide"># version 300 es
  #define M_PI 3.1415927

  uniform sampler2D t0_pos_tex;

  out vec4 color;

  void main() {
    vec4 t0_pos = texelFetch(t0_pos_tex, ivec2(gl_VertexID,     0), 0);
    gl_Position = vec4(t0_pos.x, t0_pos.y, 0., 1.);
    color = vec4(1., 1., 1., 1.);
    gl_PointSize = 50.0;
  }
</div>

<div id="renderFs" class="hide"># version 300 es
  precision highp float;

  in vec4 color;

  out vec4 outColor;

// Turns point into a circle and adds
// antialiasing to make it smoothly round
  void main() {
    float r = 0.0, delta = 0.0, alpha = 1.0;
    vec2 cxy = 2.0 * gl_PointCoord - 1.0;
    r = dot(cxy, cxy);
    delta = fwidth(r);
    alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, r);

    outColor = color * alpha;
  }
</div>

But, I suggest you spend a couple of minutes to try to understand the example linked. Yes it uses TWGL because the point of explaining how to do particles does not also want to be a tutorial on the entirely of WebGL. It should be pretty obvious what twgl.createTexture does just by looking at the inputs. Similarly with, twgl.createFramebufferInfo and twgl.createBufferInfoFromArrays if not obvious are probably just a few seconds away from understanding. twgl.setBuffersAndAttributes and twgl.setUniforms do exactly what they say. If you've done either of those things manually in webgl it should be pretty clear what it means to "set buffers and attributes" and to "set uniforms". All that's left is twgl.drawBufferInfo

In any case, it's going to be slower computing the new particle positions using gl.POINTS, one point per particle, rather than drawing a quad with N pixels, one for pixel for each point. Drawing the particles you might use gl.POINTS but not updating the positions.

One other note: like you print the shader info log when compiling fails you probably want to print the program info log when linking fails. There are plenty of errors that only happen during linking.

Upvotes: 1

Related Questions