0

I am learning webgl, and I am trying to do the following: Fetch a series of images, and then draw them to the webgl canvas as the fetch calls complete. Each image has a position on the canvas, so as each image is fetched and drawn, it should "fill in" part of a complete image. Instead what I am getting is that each time an image is drawn to the canvas, the others disappear. For example, 4 images:

enter image description here enter image description here enter image description here enter image description here

What I would expect instead is that after the 4 images are drawn, I would see this:

enter image description here

Taking a look at my code, here is the main function where I define the tiles and iterate over them:

function main() {
  const tiles: Tile[] = [
    {
      path: topleft,
      position: { x: 0, y: 0 }
    },
    {
      path: topright,
      position: { x: 256, y: 0 }
    },
    {
      path: bottomleft,
      position: { x: 0, y: 256 }
    },
    {
      path: bottomright,
      position: { x: 256, y: 256 }
    }
  ];

  const canvas = document.getElementById("canvas") as HTMLCanvasElement;
  canvas.height = 256 * 2;
  canvas.width = 256 * 2;

  tiles.forEach((tile, i) => {
    setTimeout(() => {
      const image = new Image();
      image.crossOrigin = "anonymous";
      image.onload = () => render(image, i, tile);
      image.src = tile.path;
    }, i * 1000);
  });
}

In a real life scenario, the image sources and positions would be a bit more dynamic, but the goal is the same - load images async, and as they load in, render them. My render function is where most of the gl work happens:

function render(tileImage: HTMLImageElement, i: number, tile: Tile) {
    // look up where the vertex data needs to go.
    var positionLocation = gl.getAttribLocation(program, 'a_position');
    var texcoordLocation = gl.getAttribLocation(program, 'a_texCoord');

    // Create a buffer to put three 2d clip space points in
    var positionBuffer = gl.createBuffer();

    // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    // Set a rectangle the same size as the image.
    setRectangle(
        gl,
        tile.position.x,
        tile.position.y,
        tileImage.width,
        tileImage.height
    );

    // provide texture coordinates for the rectangle.
    var texcoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);

    gl.bufferData(
        gl.ARRAY_BUFFER,
        // prettier-ignore
        new Float32Array([
            0.0, 0.0,
        1.0, 0.0,
        0.0, 1.0,
        0.0, 1.0,
        1.0, 0.0,
        1.0, 1.0,
        ]),
        gl.STATIC_DRAW
    );

    // Create a texture and bing it to the gl context
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);

    // Set the parameters so we can render any size image.
    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);

    // Upload the tile image to the texture
    gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        gl.RGBA,
        gl.RGBA,
        gl.UNSIGNED_BYTE,
        tileImage
    );

    // lookup uniforms
    var resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
    var textureSizeLocation = gl.getUniformLocation(program, 'u_textureSize');

    // Tell WebGL how to convert from clip space to pixels
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    // Tell it to use our program (pair of shaders)
    gl.useProgram(program);

    // Turn on the position attribute
    gl.enableVertexAttribArray(positionLocation);

    // Bind the position buffer.
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

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

    // Turn on the texcoord attribute
    gl.enableVertexAttribArray(texcoordLocation);

    // bind the texcoord buffer.
    gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);

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

    // set the resolution
    gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
    // set the size of the image
    gl.uniform2f(textureSizeLocation, 256, 256);

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

const gl = getGlContext();
const { program } = setup(gl, vert1, frag1);

// Fills the buffer with the values that define a rectangle.
export function setRectangle(
    gl: WebGLRenderingContext,
    x: number,
    y: number,
    width: number,
    height: number
) {
    const x1 = x,
        x2 = x + width,
        y1 = y,
        y2 = y + height;

    gl.bufferData(
        gl.ARRAY_BUFFER,
        // prettier-ignore
        new Float32Array([
      x1, y1, 
      x2, y1, 
      x1, y2, 
      x1, y2, 
      x2, y1, 
      x2, y2]),
        gl.STATIC_DRAW
    );
}

Where setup is a function to create a program and link the shaders, and getGlContext just gets and returns the webgl context from the canvas.

I have a feeling I am not understanding something in the procedure of creating and binding the buffer, or creating and binding the texture, so that the old texture data is "forgotten" when the new texture data is drawn.

Here is a codesandbox demonstrating the issue. It uses typescript, and webpack to import the GLSL files, so that they can be written separately and with GLSL syntax highlighting.

How do I draw a new texture to the canvas at a specified location while still maintaining the old textures?

Seth Lutske
  • 9,154
  • 5
  • 29
  • 78

1 Answers1

1

I think your issue is that the browser reads the contents of your canvas and composites it with the rest of the page at regular intervals. It also clears the canvas buffer (effectively does a gl.clear()) when it does so in order to not composite two frames on the next interval and for performance reasons.

You can force the canvas to persist by passing the preserveDrawingBuffer: true option when getting the WebGL context:

export const gl = canvas.getContext("webgl", {
  preserveDrawingBuffer: true
}) as WebGLRenderingContext;

Ideally, you want your rendering to happen at something like 60fps, so you never have to worry about the page compositor clearing your contents because you're redrawing them over and over and over again.f

In your case, you probably want to create multiple textures and render all four tiles in a single render pass, each quadrant reading from the a different texture uniform.

Or you can render the canvas content to an output texture (look up a renderbuffer) and as each tile loads, combine the output texture and each new tile until all the blank spots are filled in. Then render the complete output texture on the canvas.

teddybeard
  • 1,964
  • 1
  • 12
  • 14
  • 1
    Teddy! Howzit my dude, hope you're doing well. `preserveDrawingBuffer: true` indeed works, but based on [this discussion](https://stackoverflow.com/questions/27746091/preservedrawingbuffer-false-is-it-worth-the-effort), it seems like its not the most efficient way to go about it. Considering I'm moving towards dynamically adding map tiles to a gl context as they're loaded from an api, I'm gonna dig in to the renderbuffer concept. FInally getting around to spending some time with GL. I may return with more questions for you. – Seth Lutske May 08 '23 at 22:43
  • 1
    Life's good. Welcome to the WebGL world. On shademap, I stitch tiles using a large canvas and putImageData. I then use the stitched canvas as a source for my WebGL texture. I think renderbuffer will be faster and less memory intensive, but it took me a few months to wrap my head around those. Good luck! – teddybeard May 08 '23 at 22:49