skip to Main Content

I want to change the color of one pixel for a webGL canvas by first getting the current value and then replace it with a new slightly modified color. For a 2d canvas context this was easy, but I quickly get lost trying to do the same for a webGl canvas context.

Here is my attempt at first reading the pixel color and then drawing it back with changed color. I would prefer just getting an array, change one value and then putting it back instead of drawing, but I found no way to do this either.

var gl = canvas.getContext('webgl');// get the context of Canvas or OffscreenCanvas
// allocate buffer
const out = new Uint8Array(4);
// read RGBA value of pixel
gl.readPixels(98, 122, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, out);
// change red value of pixel
out[0] = 255;
// convert changed array to Float32Array
var float32Data = new Float32Array(out);
// So far everything works but I don't know how to put the changed values back onto the canvas/context
var vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, float32Data, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.POINTS, 0, float32Data.length / 3);

EDIT
Added comments to clarify that getting the pixel color value works. I want to know how I can put the changed value back. Do I really have to draw? Can’t I simply "edit" the underlaying data buffer/array?

For example, the 2d canvas has getImageData() and putImageData()

EDIT2
I am writing proxies for OffscreenCanvas.convertToBlob and canvas.toDataURL to alter the canvas before convertToBlob or toDataURL returns. For a 2d canvas getImageData and putImage data exist and serves my prurpose perfectly. Are there any equivalent for WebGL, changing a value in an underlaying buffer/array? The data returned by toDataURL and convertToBlob must still be valid objects

2

Answers


  1. If I understand the question here is a solution.

    First I draw in the canvas (I use a basic WebGL function, the scissor).
    Then I run the drawing instructions again, but this time the destination is not the canvas, c’est à dire, the framebuffer(null), but a framebuffer with a texture attached.

    gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,copietextureEcran,0) 
    

    Now this texture contains the drawing.

    Then I build a WebGL code that reads this texture and draws a point at one of the coordinates.

    if (gl_FragCoord.xy==vec2(3.5,3.5)) gl_FragColor=vec4(0,1,0,1);  
    
    const canvas = document.querySelector('canvas');
    let width=7;
    let height=7;
    canvas.width=7;
    canvas.height=7;
    const gl = canvas.getContext('webgl');
    
    function a() {
      gl.viewport(0, 0, width, height);
      gl.clearColor(1,0,0,1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.enable(gl.SCISSOR_TEST);
      gl.scissor(2, 2, 3, 3);
      gl.clearColor(1, 0, 1, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.disable(gl.SCISSOR_TEST);
      
      let copietextureEcran = gl.createTexture();
      gl.activeTexture(gl.TEXTURE0);
      gl.bindTexture(gl.TEXTURE_2D, copietextureEcran);
      gl.texImage2D(
        gl.TEXTURE_2D,
        0,             
        gl.RGBA,
        width,
        height,
        0,
        gl.RGBA,
        gl.UNSIGNED_BYTE,
        null
      );  
      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);  
      
      let fbEcran = gl.createFramebuffer();  
      gl.bindFramebuffer(gl.FRAMEBUFFER,fbEcran);  gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,copietextureEcran,0);  
      
      gl.viewport(0, 0, 7, 7);  
      gl.clearColor(1,0,0,1);  
      gl.clear(gl.COLOR_BUFFER_BIT);  
      gl.enable(gl.SCISSOR_TEST);  
      gl.scissor(2, 2, 3, 3);  
      gl.clearColor(1, 0, 1, 1);  
      gl.clear(gl.COLOR_BUFFER_BIT);  
      gl.disable(gl.SCISSOR_TEST);
      
    }
    a();
    
    function b() {  
      let vs=`  
        attribute vec2 aPosition;  
        varying vec2 vtexCoords;
        
        void main() {
          gl_Position=vec4(aPosition,0,1);
          vtexCoords = (aPosition+1.0)/2.0;
        }
      `;
      let fs=`  
        precision highp float;
        
        uniform sampler2D copietextureEcran;
        varying vec2 vtexCoords;
        
        void main() {
          vec4 color = texture2D(copietextureEcran,vtexCoords);
          gl_FragColor = vec4(color);  
          if (gl_FragCoord.xy==vec2(3.5,3.5)) gl_FragColor=vec4(0,1,0,1);
        }
      `;
      
      const vertexShader = gl.createShader(gl.VERTEX_SHADER);
      gl.shaderSource(vertexShader,vs);
      gl.compileShader(vertexShader);
      const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
      gl.shaderSource(fragmentShader,fs);
      gl.compileShader(fragmentShader);
      const program = gl.createProgram();
      gl.attachShader(program,vertexShader);
      gl.attachShader(program,fragmentShader);
      gl.linkProgram(program);
      gl.useProgram(program);
      
      let positionAttributeLocation = gl.getAttribLocation(program, "aPosition");
      let positionBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER,positionBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
            -1, -1,
            +1, -1,
            -1, +1,
            -1, +1,
            +1, -1,
            +1, +1,
            ]), gl.STATIC_DRAW);
       gl.enableVertexAttribArray(positionAttributeLocation);
       gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
       
       gl.bindFramebuffer(gl.FRAMEBUFFER,null);
       gl.viewport(0, 0, 7, 7);
       
       gl.clearColor(0,0,0,1);
       gl.clear(gl.COLOR_BUFFER_BIT);
       
       gl.drawArrays(gl.TRIANGLES, 0, 6);
    
    }
    canvas {
      width: 100x;
      height: 100px;
      image-rendering: pixelated;
     }
    <canvas id="canvas"></canvas>
    <button onclick="b()">Click me</button>
    Login or Signup to reply.
  2. You could just draw the webgl canvas to another offscreen 2D canvas and then use the same code-path as you do for the 2D canvas:

    const ctx = new OffscreenCanvas(1,1).getContext('2d');
    export function applyProxy (canvas) {
      ['toDataURL', 'convertToBlob'].forEach(m=>{
        canvas[m] = (...args) => {
          // transfer image
          ctx.canvas.width = canvas.width;
          ctx.canvas.height = canvas.height;
          ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height);
    
          // modify pixel
          const x = 98,y = 122, imageData = ctx.getImageData(x,y,1,1);
          imageData.data[0] = 255;
          ctx.putImageData(imageData,x,y);
    
          // mimic original call
          return ctx.canvas[m](...args);
        }
      });
    }
    

    This solution should be rather efficient while also keeping code-complexity low.

    If you insist on using WebGL you could use the scissor solution given in the answer you already got, its the least amount of code, albeit a bit hacky.

    const x = 98, y = 122, out = new Uint8Array(4);
    gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, out);
    out[0] = 255;
    
    // get current state
    const oldScissorRect = gl.getParameter(gl.SCISSOR_BOX);
    const oldClearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE);
    const oldScissorEnabled = gl.isEnabled(gl.SCISSOR_TEST);
    
    // "draw" the pixel
    gl.enable(gl.SCISSOR_TEST);
    gl.scissor(x, y, 1, 1);
    gl.clearColor(out[0],out[1],out[2],out[3]);
    gl.clear(gl.COLOR_BUFFER_BIT);
    
    // restore state
    gl.scissor(...oldScissorRect);
    gl.clearColor(...oldClearColor);
    if (!oldScissorEnabled) gl.disable(gl.SCISSOR_TEST);
    

    Since WebGL is a state machine, to do this properly you have to ensure to restore the state afterwards, for that you have to query it from the driver like I do in the example, which can be slow, or cache it which is error prone. This is also the downfall of the "proper" WebGL solution as it does require setting up a shader and drawing a point thus requiring a lot of state memorization (assuming we need to be host application agnostic) which is why I won’t make the effort of writing that out as it’s impractical.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search