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:
What I would expect instead is that after the 4 images are drawn, I would see this:
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?