Search code examples
javascriptwebgltextureswebgl2

webgl2 trouble rendering float texture to canvas


If I use webgl2 to render something (e.g. a triangle) to an RGBA UNSIGNED_BYTE texture, then render that texture to the canvas, everything works fine. But I'm having trouble getting my example to work when rendering a RGBA32F FLOAT texture to the canvas. Instead I just get a solid black box, but no error message. Any ideas how to get the float example working?

Working int texture jsfiddle

Not working float texture jsfiddle

This example is with an int texture, which works as expected (outputs purple triangle with white background as the texture, which is overlayed on a larger grey background)

"use strict";



function main() {

  // Get the WebGL context
  const canvas = document.querySelector("#canvas");
  const gl = canvas.getContext("webgl2");
  const textureWidth = 100;
  const textureHeight = 100;

  // Render purple triangle to texture
  {
    const vertexShaderSource = `#version 300 es

    in vec4 a_position;

    void main() {

    gl_Position = a_position;
    }
    `;

    const fragmentShaderSource = `#version 300 es

    precision highp float;

    out vec4 outColor;

    void main() {
    outColor = vec4(1,0,1, 1);
    }
    `;


    // create/compile shaders
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

    const program = createProgram(gl, vertexShader, fragmentShader);

    // set a_position attribute
    const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    const positions = [
      -1, -1,
      0, 1,
      1, 0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);
    gl.enableVertexAttribArray(positionAttributeLocation);

    // Tell the attribute how to get data out of positionBuffer 
    const size = 2;          // 2 components per iteration
    let type = gl.FLOAT;   // the data is 32bit floats
    const normalize = false; // don't normalize the data
    const stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
    let offset = 0;        // start at the beginning of the buffer
    gl.vertexAttribPointer(
      positionAttributeLocation, size, type, normalize, stride, offset);


    
    gl.useProgram(program);


    // set up the target texture
    const targetTexture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, targetTexture);
  
    const level = 0;
    const internalFormat = gl.RGBA;
    const border = 0;
    const format = gl.RGBA;
    type = gl.UNSIGNED_BYTE;
    const data = null;
    gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
                  textureWidth, textureHeight, border,
                  format, type, data);

    // set the filtering so we don't need mips
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    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);

    // Create and bind the framebuffer
    const fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    
    // attach the texture as the first color attachment
    const attachmentPoint = gl.COLOR_ATTACHMENT0;
    gl.framebufferTexture2D(
        gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level);

    gl.viewport(0, 0, textureWidth, textureHeight);

    // Clear the canvas
    gl.clearColor(1,1,1,1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    const primitiveType = gl.TRIANGLES;
    offset = 0;
    const count = 3;

    gl.drawArrays(primitiveType, offset, count);

  }

  // Now draw the texture to the canvas
  {

    const vertexShaderSource = `#version 300 es

    in vec2 a_position;
    in vec2 a_texCoord;

    out vec2 v_texCoord;

    void main() {

      gl_Position = vec4(a_position, 0, 1);
      v_texCoord = a_texCoord;
    }
    `;

    const fragmentShaderSource = `#version 300 es

    precision highp float;

    uniform sampler2D u_image;

    in vec2 v_texCoord;

    out vec4 outColor;

    void main() {

      outColor = texture(u_image , v_texCoord);
    }
    `;

    // render to the canvas
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);

    const program = webglUtils.createProgramFromSources(gl,
      [vertexShaderSource, fragmentShaderSource]);

    const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
    const texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");

    // lookup uniforms
    const imageLocation = gl.getUniformLocation(program, "u_image");

    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);


    const positionBuffer = gl.createBuffer();
    gl.enableVertexAttribArray(positionAttributeLocation);
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

    let size = 2;          // 2 components per iteration
    let type = gl.FLOAT;   // the data is 32bit floats
    let normalize = false; // don't normalize the data
    let stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
    let offset = 0;        // start at the beginning of the buffer
    gl.vertexAttribPointer(
        positionAttributeLocation, size, type, normalize, stride, offset);

    const texCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        0, 0,
        0, 1,
        1, 0,
        1, 0,
        1, 1,
        0, 1 
    ]), gl.STATIC_DRAW);

    gl.enableVertexAttribArray(texCoordAttributeLocation);
    size = 2;          // 2 components per iteration
    type = gl.FLOAT;   // the data is 32bit floats
    normalize = false; // don't normalize the data
    stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
    offset = 0;        // start at the beginning of the buffer
    gl.vertexAttribPointer(
        texCoordAttributeLocation, size, type, normalize, stride, offset);

    window.gl = gl
    webglUtils.resizeCanvasToDisplaySize(gl.canvas);

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

    // Clear the canvas to grey
    gl.clearColor(0.5,0.5,0.5,1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.useProgram(program);

    gl.uniform1i(imageLocation, 0);

    // Bind the position buffer so gl.bufferData that will be called
    // in setRectangle puts data in the position buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    // Set a rectangle the same size as the image.
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      -0.5, -0.5,
      -0.5, 0.5,
      0.5, -0.5,
      0.5, -0.5,
      0.5, 0.5,
      -0.5, 0.5
    ]), gl.STATIC_DRAW);

    // Draw the rectangle.
    const primitiveType = gl.TRIANGLES;
    offset = 0;
    const count = 6;
    gl.drawArrays(primitiveType, offset, count);

  }

}

main();

function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (success) {
    return shader;
  }

  console.log(gl.getShaderInfoLog(shader));  
  gl.deleteShader(shader);
  return undefined;
}

function createProgram(gl, vertexShader, fragmentShader) {
  const program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  const success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }

  console.log(gl.getProgramInfoLog(program));  
  gl.deleteProgram(program);
  return undefined;
}

And this is the very similar code with the changes to enable a float32 texture. No errors in the console, but I just get a black square on a grey background. No purple triangle :(

"use strict";



function main() {

  // Get the WebGL context
  const canvas = document.querySelector("#canvas");
  const gl = canvas.getContext("webgl2");
  const textureWidth = 100;
  const textureHeight = 100;

  // Render purple triangle to texture
  {
    const vertexShaderSource = `#version 300 es

    in vec4 a_position;

    void main() {

    gl_Position = a_position;
    }
    `;

    const fragmentShaderSource = `#version 300 es

    precision highp float;

    out vec4 outColor;

    void main() {
    outColor = vec4(1,0,1, 1);
    }
    `;


    // create/compile shaders
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

    const program = createProgram(gl, vertexShader, fragmentShader);

    // set a_position attribute
    const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    const positions = [
      -1, -1,
      0, 1,
      1, 0
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);
    gl.enableVertexAttribArray(positionAttributeLocation);

    // Tell the attribute how to get data out of positionBuffer 
    const size = 2;          // 2 components per iteration
    let type = gl.FLOAT;   // the data is 32bit floats
    const normalize = false; // don't normalize the data
    const stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
    let offset = 0;        // start at the beginning of the buffer
    gl.vertexAttribPointer(
      positionAttributeLocation, size, type, normalize, stride, offset);


    
    gl.useProgram(program);


    // set up the target texture

    // enable float texture rendering
    const ext = gl.getExtension("EXT_color_buffer_float");


    const targetTexture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, targetTexture);
  
    const level = 0;
    const internalFormat = gl.RGBA32F;
    const border = 0;
    const format = gl.RGBA;
    type = gl.FLOAT;
    const data = null;
    gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
                  textureWidth, textureHeight, border,
                  format, type, data);

    // set the filtering so we don't need mips
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    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);

    // Create and bind the framebuffer
    const fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    
    // attach the texture as the first color attachment
    const attachmentPoint = gl.COLOR_ATTACHMENT0;
    gl.framebufferTexture2D(
        gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level);

    gl.viewport(0, 0, textureWidth, textureHeight);

    // Clear the canvas
    gl.clearColor(1,1,1,1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    const primitiveType = gl.TRIANGLES;
    offset = 0;
    const count = 3;

    gl.drawArrays(primitiveType, offset, count);

  }

  // Now draw the texture to the canvas
  {

    const vertexShaderSource = `#version 300 es

    in vec2 a_position;
    in vec2 a_texCoord;

    out vec2 v_texCoord;

    void main() {

      gl_Position = vec4(a_position, 0, 1);
      v_texCoord = a_texCoord;
    }
    `;

    const fragmentShaderSource = `#version 300 es

    precision highp float;

    uniform sampler2D u_image;

    in vec2 v_texCoord;

    out vec4 outColor;

    void main() {

      outColor = texture(u_image , v_texCoord);
    }
    `;

    // render to the canvas
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);

    const program = webglUtils.createProgramFromSources(gl,
      [vertexShaderSource, fragmentShaderSource]);

    const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
    const texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");

    // lookup uniforms
    const imageLocation = gl.getUniformLocation(program, "u_image");

    const vao = gl.createVertexArray();
    gl.bindVertexArray(vao);


    const positionBuffer = gl.createBuffer();
    gl.enableVertexAttribArray(positionAttributeLocation);
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

    let size = 2;          // 2 components per iteration
    let type = gl.FLOAT;   // the data is 32bit floats
    let normalize = false; // don't normalize the data
    let stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
    let offset = 0;        // start at the beginning of the buffer
    gl.vertexAttribPointer(
        positionAttributeLocation, size, type, normalize, stride, offset);

    const texCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        0, 0,
        0, 1,
        1, 0,
        1, 0,
        1, 1,
        0, 1 
    ]), gl.STATIC_DRAW);

    gl.enableVertexAttribArray(texCoordAttributeLocation);
    size = 2;          // 2 components per iteration
    type = gl.FLOAT;   // the data is 32bit floats
    normalize = false; // don't normalize the data
    stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
    offset = 0;        // start at the beginning of the buffer
    gl.vertexAttribPointer(
        texCoordAttributeLocation, size, type, normalize, stride, offset);

    window.gl = gl
    webglUtils.resizeCanvasToDisplaySize(gl.canvas);

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

    // Clear the canvas to grey
    gl.clearColor(0.5,0.5,0.5,1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.useProgram(program);

    gl.uniform1i(imageLocation, 0);

    // Bind the position buffer so gl.bufferData that will be called
    // in setRectangle puts data in the position buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    // Set a rectangle the same size as the image.
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      -0.5, -0.5,
      -0.5, 0.5,
      0.5, -0.5,
      0.5, -0.5,
      0.5, 0.5,
      -0.5, 0.5
    ]), gl.STATIC_DRAW);

    // Draw the rectangle.
    const primitiveType = gl.TRIANGLES;
    offset = 0;
    const count = 6;
    gl.drawArrays(primitiveType, offset, count);

  }

}

main();

function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (success) {
    return shader;
  }

  console.log(gl.getShaderInfoLog(shader));  
  gl.deleteShader(shader);
  return undefined;
}

function createProgram(gl, vertexShader, fragmentShader) {
  const program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  const success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }

  console.log(gl.getProgramInfoLog(program));  
  gl.deleteProgram(program);
  return undefined;
}

Solution

  • According to the documentation, the internal format RGBA32F is not filterable. So it's invalid to set TEXTURE_MIN_FILTER to LINEAR, and you should change it to NEAREST. However, this is not enough, because the default value of TEXTURE_MAG_FILTER is LINEAR, so you have to change it to NEAREST as well. So change

    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    

    to

    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    

    I'm not sure why this is not an error in the console.