Search code examples
javascriptglslbufferwebgl

Draw pixels on mouse event in webgl - Vertex buffer is not big enough for the draw call


I am learning webgl and trying to understand how to communicate mouse positioning events to the GPU. My goal is to create a simple draw program, where a mouseover or mousedown event on the webgl canvas will color the pixel at that position. I am aware this is easier with 2d canvas, but the point is to learn about communicating between JS and GPU via buffers and uniforms.

My idea is that on every mousemove, to create a new buffer, bind it, set the data, and drawArrays. (I chose this path as opposed to modifying an existing buffer based on advice in (WebGL) How to add vertices to an already initialized vertex buffer?. Some simple code:

const canvas = document.getElementById('canvas');
const gl = getGlContext('canvas', { preserveDrawingBuffer: true }); 

// simple util to compile and attach shaders
const { program } = setup(gl, vert1, frag1); 

gl.useProgram(program);

// Initialize placeholder buffer buffer - without this,
// webgl says there is no buffer attached, even though I 
// create and attach another buffer later?
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
const resolutionUniformLocation = gl.getUniformLocation(program, 'u_resolution');

gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);

// set the resolution
gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);

canvas.addEventListener('mousemove', (e) => {
    const x = e.x - canvas.offsetLeft + 1;
    const y = e.y - canvas.offsetTop + 1;

    const buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([x, y]), gl.STATIC_DRAW);

    gl.vertexAttrib2f(positionAttributeLocation, x, y);
    gl.drawArrays(gl.POINTS, 0, 3);
});

When I run this code, and mouse into the canvas, the canvas goes blank and I get the error:

Vertex buffer is not big enough for the draw call

Clearly I am not understanding how to set up my buffers and buffer data. My expectations were that a small buffer of length 2 is all that is needed to capture a single [x,y] coordinate. Once the buffer is bound and the vertex attribute for position is set in the GPU, the draw call should draw that pixel coordinate in the color set in the fragment shader. What is wrong with my event listener that my code is not giving the expected behavior?

Appendix:

For reference, my shaders are very simple:

// vertex shader
// Some extra code here to convert pixels to clip space within the shader,
// as opposed to within the javascript code

attribute vec2 a_position;
uniform vec2 u_resolution;

void main() {
  // convert the position from pixels to 0.0 to 1.0
  vec2 zeroToOne = a_position / u_resolution;

  // convert from 0->1 to 0->2
  vec2 zeroToTwo = zeroToOne * 2.0;

  // convert from 0->2 to -1->+1 (clip space)
  vec2 clipSpace = zeroToTwo - 1.0;

  gl_Position = vec4(clipSpace, 0.0, 1.0);
}
// fragment shader

precision mediump float;

uniform vec4 u_color;

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


Solution

  • There's multiple things wrong with your code.

    1. Your drawArrays call specifies 3 indices, despite you only having a single 2D point in your buffer, the correct number of indices would be 1.
    2. vertexAttrib2f sets a static value to an attribute that's being used when the attribute array is not enabled, what you want to use here is vertexAttribPointer.
    3. On that note, the previous dummy buffer and attribute pointer setup is redundant as it's not being used.

    Effectively you can do this specific example of yours (plotting an individual pixel) without any buffers by just calling:

    gl.vertexAttrib2f(positionAttributeLocation, x, y);
    gl.drawArrays(gl.POINTS, 0, 1);
    

    So your code would become:

    const canvas = document.getElementById('canvas');
    const gl = canvas.getContext('webgl', { preserveDrawingBuffer: true }); 
    
    // simple util to compile and attach shaders
    const program = setup(gl, vert1.textContent, frag1.textContent); 
    
    gl.useProgram(program);
    
    // disable position attribute
    const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
    gl.disableVertexAttribArray(positionAttributeLocation);
    
    // set the resolution
    const resolutionUniformLocation = gl.getUniformLocation(program, 'u_resolution');
    gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height);
    
    canvas.addEventListener('mousemove', (e) => {
        const br = canvas.getBoundingClientRect();
        const x = e.clientX - br.left;
        const y = br.height - (e.clientY - br.top);
        gl.vertexAttrib2f(positionAttributeLocation, x, y);
        gl.drawArrays(gl.POINTS, 0, 1);
    });
    
    function setup(ctx, vert, frag) {
      const vs = ctx.createShader(ctx.VERTEX_SHADER);
      ctx.shaderSource(vs, vert);
      ctx.compileShader(vs);
    
      const fs = ctx.createShader(ctx.FRAGMENT_SHADER);
      ctx.shaderSource(fs, frag);
      ctx.compileShader(fs);
    
      const program = ctx.createProgram();
      ctx.attachShader(program, vs);
      ctx.attachShader(program, fs);
      ctx.linkProgram(program);
      console.log(ctx.getProgramInfoLog(program));
      return program;
    }
    canvas { background: #efe; }
    <script id="vert1" type="x-vertex-shader">
    // Some extra code here to convert pixels to clip space within the shader,
    // as opposed to within the javascript code
    
    attribute vec2 a_position;
    uniform vec2 u_resolution;
    
    void main() {
      // convert the position from pixels to 0.0 to 1.0
      vec2 zeroToOne = a_position / u_resolution;
    
      // convert from 0->1 to 0->2
      vec2 zeroToTwo = zeroToOne * 2.0;
    
      // convert from 0->2 to -1->+1 (clip space)
      vec2 clipSpace = zeroToTwo - 1.0;
    
      gl_Position = vec4(clipSpace, 0.0, 1.0);
    }
    </script>
    <script id="frag1" type="x-fragment-shader">
    precision mediump float;
    
    uniform vec4 u_color;
    
    void main() {
        gl_FragColor = vec4(1,0,1,1);
    }
    </script>
    <canvas id="canvas" width="600" height="400"></canvas>

    If you decide to still use buffers (when you need an arbitrary number of points / attributes) you should reuse a single buffer rather than manually allocating and setting up new buffers with every event.