Skip to content

Instantly share code, notes, and snippets.

@jamestthompson3
Last active September 5, 2023 22:13
Show Gist options
  • Save jamestthompson3/362c7f172ef4a47e3e269751993cb4b3 to your computer and use it in GitHub Desktop.
Save jamestthompson3/362c7f172ef4a47e3e269751993cb4b3 to your computer and use it in GitHub Desktop.
render 2D tilemap WebGL
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=yes"
/>
<style>
canvas {
border: 1px solid black;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.4.3/gl-matrix-min.js"></script>
<script>
// ============
// Util Functions
// ============
function createShader(gl, type, sourceId) {
const shaderContent = document.getElementById(sourceId).text;
const shader = gl.createShader(type);
gl.shaderSource(shader, shaderContent);
gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!success) {
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return;
}
return shader;
}
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!success) {
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return;
}
// Clean up
gl.detachShader(program, vertexShader);
gl.detachShader(program, fragmentShader);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
gl.useProgram(null);
return program;
}
class Material {
constructor(program, gl) {
this.program = program;
this.gl = gl;
this.parameters = {
uniforms: {},
attributes: {},
};
this.setup();
}
setup() {
const gl = this.gl;
const uniformCount = gl.getProgramParameter(
this.program,
gl.ACTIVE_UNIFORMS,
);
for (let i = 0; i < uniformCount; i++) {
const details = gl.getActiveUniform(this.program, i);
const location = gl.getUniformLocation(this.program, details.name);
this.parameters.uniforms[details.name] = {
location,
type: details.type,
};
}
const attribCount = gl.getProgramParameter(
this.program,
gl.ACTIVE_ATTRIBUTES,
);
for (let i = 0; i < attribCount; i++) {
const details = gl.getActiveAttrib(this.program, i);
const location = gl.getAttribLocation(this.program, details.name);
this.parameters.attributes[details.name] = {
location,
type: details.type,
};
}
}
getAttribLocation(name) {
return this.parameters.attributes[name].location;
}
getUniformLocation(name) {
return this.parameters.uniforms[name].location;
}
setUniform(name, ...rest) {
const param = this.parameters.uniforms[name];
const { gl } = this;
if (param) {
switch (param.type) {
case gl.FLOAT:
gl.uniform1f(param.location, ...rest);
break;
case gl.FLOAT_VEC2:
gl.uniform2f(param.location, ...rest);
break;
case gl.FLOAT_VEC3:
gl.uniform3f(param.location, ...rest);
break;
case gl.FLOAT_VEC4:
gl.uniform4f(param.location, ...rest);
break;
case gl.FLOAT_MAT3:
gl.uniformMatrix3fv(param.location, false, ...rest);
break;
case gl.FLOAT_MAT4:
gl.uniformMatrix4fv(param.location, false, ...rest);
break;
case gl.SAMPLER_2D:
gl.uniform1i(param.location, ...rest);
break;
}
} else {
console.warn(name, " is not in the uniforms list");
}
}
}
</script>
</head>
<body>
<canvas id="c"></canvas>
</body>
<script id="tilemap-fs" type="fragment-shader">
precision mediump float;
uniform sampler2D tilemap;
uniform sampler2D tiles;
uniform vec2 tilemapSize;
uniform vec2 tilesetSize;
varying vec2 v_texcoord;
void main() {
vec2 tilemapCoord = floor(v_texcoord);
vec2 texcoord = fract(v_texcoord);
vec2 tileFoo = fract(tilemapCoord / tilemapSize);
vec4 tile = floor(texture2D(tilemap, tileFoo) * 256.0);
vec2 tileCoord = (tile.xy + texcoord) / tilesetSize;
vec4 color = texture2D(tiles, tileCoord);
gl_FragColor = color;
}
</script>
<script id="tilemap-vs" type="vertex-shader">
attribute vec2 position;
uniform mat3 matrix;
uniform mat3 texMatrix;
varying vec2 v_texcoord;
void main() {
gl_Position = vec4(matrix * vec3(position, 1), 1);
v_texcoord = vec4(texMatrix * vec3(position, 1), 1).xy;
}
</script>
<script>
// prettier-ignore
const tileMap = [
1, 1, 1, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 1, 1, 1
];
// We have a tile map that is 4x4 tiles and each tile image is 16x16 pixels.
// This brings the map size to 64 x 64 pixels.
const numTiles = 4;
const tileSize = 16;
const mapSize = numTiles * tileSize;
const { mat3, vec2 } = glMatrix;
// ============
// Setup WebGL
// ============
const gl = document.getElementById("c").getContext("webgl2");
gl.clearColor(0, 0, 0, 1.0);
const vertexShader = createShader(gl, gl.VERTEX_SHADER, "tilemap-vs");
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, "tilemap-fs");
const program = createProgram(gl, vertexShader, fragmentShader);
const material = new Material(program, gl);
let tileInfoTexture;
let tileImageTexture;
let positionBuffer;
let textureBuffer;
// ====================================
// Load Tile Image, set up textures
// ====================================
const tileImg = new Image();
tileImg.src = "tile.png";
tileImg.addEventListener("load", () => {
const textureData = new Uint8Array(mapSize ** 2);
for (let i = 0; i < tileMap.length; ++i) {
const offset = i * 4;
// if the value of the tile is 1, draw a the ground, if it's 0, draw the sky.
const tileId = tileMap[i];
textureData[offset + 0] = tileId % numTiles;
textureData[offset + 1] = tileId / numTiles;
}
gl.useProgram(program);
// Create texture for the tile information.
tileInfoTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tileInfoTexture);
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);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
tileSize,
tileSize,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
textureData,
);
// Create texture for the tilemap atlas image.
tileImageTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tileImageTexture);
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);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
tileImg,
);
// Unbind the texture
gl.bindTexture(gl.TEXTURE_2D, null);
// Setup position buffer
positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// prettier-ignore
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0, 0, 1,
0, 0, 1,
0, 1, 1,
0, 1, 1,
]),
gl.STATIC_DRAW,
);
// Setup texture buffer
textureBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
// prettier-ignore
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0, 0, 1,
0, 0, 1,
0, 1, 1,
0, 1, 1,
]),
gl.STATIC_DRAW,
);
// Now we're ready to draw.
requestAnimationFrame(render);
});
function render() {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindTexture(gl.TEXTURE_2D, tileImageTexture);
// Set Attributes
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positionLocation = material.getAttribLocation("position");
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// Set the world matrix
let worldMat = mat3.fromScaling(
mat3.create(),
vec2.fromValues(
gl.canvas.width / (numTiles * tileSize),
gl.canvas.height / (numTiles * tileSize),
),
);
// Set the texture matrix
let texMat = mat3.fromScaling(
mat3.create(),
vec2.fromValues(
gl.canvas.width / numTiles,
gl.canvas.height / numTiles,
),
);
// Set Uniforms
material.setUniform("matrix", worldMat);
material.setUniform("texMatrix", texMat);
material.setUniform("tilemap", tileInfoTexture);
material.setUniform("tiles", tileImageTexture);
material.setUniform("tilemapSize", numTiles, numTiles);
material.setUniform("tilesetSize", numTiles, numTiles);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.flush();
gl.useProgram(null);
requestAnimationFrame(render);
}
</script>
</html>
@jamestthompson3
Copy link
Author

tile

tile img

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment