Last active
September 5, 2023 22:13
-
-
Save jamestthompson3/362c7f172ef4a47e3e269751993cb4b3 to your computer and use it in GitHub Desktop.
render 2D tilemap WebGL
This file contains 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
<!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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
tile img