Skip to content

Instantly share code, notes, and snippets.

@rhmoller
Last active May 27, 2022 19:00
Show Gist options
  • Save rhmoller/69950b275cf8ca83d338a70cc2a50638 to your computer and use it in GitHub Desktop.
Save rhmoller/69950b275cf8ca83d338a70cc2a50638 to your computer and use it in GitHub Desktop.
Sprite Renderer using WebGL2 Instancing
import { createPixelatedCanvas } from "./engine/gfx";
import { SpriteRenderer } from "./SpriteRenderer";
const canvas = document.getElementById("game") as HTMLCanvasElement;
const loadImage = (url: string) =>
new Promise((resolve) => {
const image = new Image();
image.addEventListener("load", () => resolve(image));
image.src = url;
});
const run = async () => {
const spriteSheet = (await loadImage("/assets/spritesheet.png")) as HTMLImageElement;
const renderer = new SpriteRenderer(canvas, spriteSheet, 16, 4);
let scrollX = 0;
let scrollY = 0;
function loop(time: number) {
requestAnimationFrame(loop);
renderer.clearScreen();
scrollX += 1;
scrollY += 1;
for (let y = -16; y < 216; y += 16) {
for (let x = -16; x < 384; x += 16) {
renderer.addSprite(x + (scrollX % 16), y + (scrollY % 16), 1);
}
}
for (let i = 0; i < 100; i++) {
const x = 192 + 172 * Math.cos(0.003 * time + 0.03 * i);
const y = 108 + 88 * Math.sin(0.002 * time + 0.05 * i);
renderer.addSprite(x, y, 2);
}
renderer.render();
}
requestAnimationFrame(loop);
};
run();
const BYTES_PER_QUAD = 16;
const BUFFER_SIZE = BYTES_PER_QUAD * 200000;
export class SpriteRenderer {
gl: WebGL2RenderingContext;
dataView: DataView;
index = 0;
constructor(canvas: HTMLCanvasElement, spriteSheet: HTMLImageElement, spriteSize = BYTES_PER_QUAD, pixelSize = 4) {
const gl = canvas.getContext("webgl2");
gl.viewport(0, 0, canvas.width, canvas.height);
const program = gl.createProgram();
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
gl.attachShader(program, vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getShaderInfoLog(vertexShader));
console.error(gl.getShaderInfoLog(fragmentShader));
}
gl.useProgram(program);
const uSamplerPosition = gl.getUniformLocation(program, "uSampler");
const uScaleLocation = gl.getUniformLocation(program, "uScale");
gl.uniform2f(uScaleLocation, (2 * pixelSize) / canvas.width, (2 * pixelSize) / canvas.height);
const uSpritesPerRow = gl.getUniformLocation(program, "uSpritesPerRow");
gl.uniform1i(uSpritesPerRow, spriteSheet.width / spriteSize);
const atlasSpriteSize = 1 / (spriteSheet.width / spriteSize);
const nudge = 0.001; // avoid tiles bleeding into each other
// prettier-ignore
const bufferData = new Float32Array([
0, spriteSize, nudge, atlasSpriteSize - nudge,
spriteSize, spriteSize, atlasSpriteSize - nudge, atlasSpriteSize - nudge,
spriteSize, 0, atlasSpriteSize - nudge, nudge,
spriteSize,0, atlasSpriteSize - nudge, nudge,
0, 0, nudge, nudge,
0,spriteSize, nudge, atlasSpriteSize - nudge,
]);
const dataView = new DataView(new ArrayBuffer(BUFFER_SIZE));
this.dataView = dataView;
const vertexArray = gl.createVertexArray();
gl.bindVertexArray(vertexArray);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, bufferData, gl.STATIC_DRAW);
const aPositionLocation = gl.getAttribLocation(program, "aPosition");
const aTexCoordLocation = gl.getAttribLocation(program, "aTexCoord");
const aTransformLocation = gl.getAttribLocation(program, "aTransform");
const aSpriteLocation = gl.getAttribLocation(program, "aSpriteIdx");
gl.enableVertexAttribArray(aPositionLocation);
gl.vertexAttribPointer(aPositionLocation, 2, gl.FLOAT, false, BYTES_PER_QUAD, 0);
gl.enableVertexAttribArray(aTexCoordLocation);
gl.vertexAttribPointer(aTexCoordLocation, 2, gl.FLOAT, false, BYTES_PER_QUAD, 8);
const transformBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, transformBuffer);
gl.bufferData(gl.ARRAY_BUFFER, dataView, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(aTransformLocation, 2, gl.FLOAT, false, BYTES_PER_QUAD, 0);
gl.vertexAttribDivisor(aTransformLocation, 1);
gl.enableVertexAttribArray(aTransformLocation);
gl.vertexAttribIPointer(aSpriteLocation, 1, gl.INT, BYTES_PER_QUAD, 8);
gl.vertexAttribDivisor(aSpriteLocation, 1);
gl.enableVertexAttribArray(aSpriteLocation);
const textureSlot = 1;
gl.activeTexture(gl.TEXTURE0 + textureSlot);
gl.uniform1i(uSamplerPosition, textureSlot);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
spriteSheet.width,
spriteSheet.height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
spriteSheet
);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.bindBuffer(gl.ARRAY_BUFFER, transformBuffer);
this.gl = gl;
}
clearScreen() {
this.gl.clearColor(0.0, 0.0, 0.0, 1);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
}
addSprite(x: number, y: number, spriteIndex: number) {
this.dataView.setFloat32(this.index * BYTES_PER_QUAD, x, true);
this.dataView.setFloat32(this.index * BYTES_PER_QUAD + 4, y, true);
this.dataView.setInt32(this.index * BYTES_PER_QUAD + 8, spriteIndex, true);
this.index++;
}
render() {
this.gl.bufferSubData(this.gl.ARRAY_BUFFER, 0, this.dataView, 0);
this.gl.drawArraysInstanced(this.gl.TRIANGLES, 0, 6, this.index);
this.index = 0;
}
}
const vertexShaderSrc = `#version 300 es
uniform vec2 uScale;
in vec2 aPosition;
in vec2 aTexCoord;
in vec2 aTransform;
in int aSpriteIdx;
out vec2 vTexCoord;
flat out int vSpriteIdx;
void main() {
gl_Position = vec4((uScale * (aPosition + aTransform) - vec2(1)) * vec2(1, -1), 0.0, 1.0);
vTexCoord = aTexCoord;
vSpriteIdx = aSpriteIdx;
}
`;
const fragmentShaderSrc = `#version 300 es
precision mediump float;
in vec2 vTexCoord;
flat in int vSpriteIdx;
uniform int uSpritesPerRow;
uniform sampler2D uSampler;
out vec4 fragColor;
void main() {
float spriteX = float(vSpriteIdx % uSpritesPerRow) / float(uSpritesPerRow);
float spriteY = float(vSpriteIdx / uSpritesPerRow) / float(uSpritesPerRow);
fragColor = texture(uSampler, vTexCoord + vec2(spriteX, spriteY));
}
`;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment