Last active
May 27, 2022 19:00
-
-
Save rhmoller/69950b275cf8ca83d338a70cc2a50638 to your computer and use it in GitHub Desktop.
Sprite Renderer using WebGL2 Instancing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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