I am writing a small sprite sheet renderer in WebGL, where I render quad with set with and height then I take one big sprite sheet and use the texture coordinates to pick what sprite should be shown on what quad. I do this all in one big object with position and texture coordinates array where i push new items in. I’m also using TWGL library to make WebGL a bit less verbose. Currently this system of batching items into one draw per frame isn’t working that well, how can it be improved?
class Render {
constructor(selector) {
this.vertexShaderSource = `#version 300 es
in vec2 position;
in vec2 texcoord;
uniform vec2 u_resolution;
out vec2 v_texCoord;
void main() {
vec2 zeroToOne = position / u_resolution;
vec2 zeroToTwo = zeroToOne * 2.0;
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
v_texCoord = texcoord;
}`;
this.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);
}`;
this.bufferInfo;
this.gl = document.querySelector(selector).getContext("webgl2");
this.programInfo = twgl.createProgramInfo(this.gl, [this.vertexShaderSource, this.fragmentShaderSource]);
this.textures = [];
this.uniforms = {
u_resolution: [innerWidth, innerHeight],
u_image: null,
}
this.arrays = {
position: [],
texcoord: [],
};
this.gl.canvas.width = innerWidth;
this.gl.canvas.height = innerHeight;
}
loadImage(url, callback) {
const texture = twgl.createTexture(this.gl, { src: url, mag: this.gl.NEAREST }, () => { callback ? callback() : "" });
this.textures.push(texture);
return {
texture: texture,
index: this.textures.length - 1
};
}
clearBuffer() {
this.arrays.position = [];
this.arrays.texcoord = [];
}
setTexture(texture) {
this.uniforms.u_image = texture;
}
addQuad(x, y, width, height, srcWidth, srcHeight, srcX, srcY, realWidth, realHeight) {
const x1 = x;
const x2 = x + width;
const y1 = y;
const y2 = y + height;
this.arrays.position.push(
x1, y1, 0,
x2, y1, 0,
x1, y2, 0,
x1, y2, 0,
x2, y1, 0,
x2, y2, 0,
);
this.arrays.texcoord.push(
srcX / srcWidth / 1, srcY / srcHeight / 1,
(srcX + realWidth) / srcWidth / 1, srcY / srcHeight / 1,
srcX / srcWidth / 1, (srcY + realHeight) / srcHeight / 1,
srcX / srcWidth / 1, (srcY + realHeight) / srcHeight / 1,
(srcX + realWidth) / srcWidth / 1, srcY / srcHeight / 1,
(srcX + realWidth) / srcWidth / 1, (srcY + realHeight) / srcHeight / 1,
);
}
setBuffer() {
this.bufferInfo = twgl.createBufferInfoFromArrays(this.gl, this.arrays);
}
resizeCanvas() {
twgl.resizeCanvasToDisplaySize(this.gl.canvas);
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
}
render() {
this.gl.useProgram(this.programInfo.program);
twgl.setUniforms(this.programInfo, this.uniforms);
twgl.setBuffersAndAttributes(this.gl, this.programInfo, this.bufferInfo);
twgl.drawBufferInfo(this.gl, this.bufferInfo);
}
}
const render = new Render("#game");
const texture = render.loadImage("https://i.imgur.com/TQnyNHU.png", () => {
requestAnimationFrame(renderStuff);
render.resizeCanvas();
function renderStuff() {
render.clearBuffer();
render.setTexture(texture);
for(let y = 0; y < 100; y++) {
for(let x = 0; x < 100; x++) {
render.addQuad(x * 48, y * 48, 48, 48, 512, 512, 32, 0, 16, 16);
}
}
render.setBuffer();
render.render();
requestAnimationFrame(renderStuff);
}
});
* {
margin: 0px;
padding: 0px;
}
body, html {
width: 100%;
height: 100%;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/twgl.js/4.19.5/twgl.min.js"></script>
<script src="main.js" type="module" defer></script>
<link rel="stylesheet" href="style.css">
<title>Prometeus</title>
</head>
<body>
<canvas id="game"></canvas>
</body>
</html>