Summary
Render an entire TMXLayer as a single screen-aligned quad using a GPU shader, instead of drawing each tile individually. The shader samples from a tile index texture (which tile goes where) and a tileset texture (the actual tile graphics) to render the entire layer in one draw call.
Current State
melonJS renders tilemaps tile-by-tile:
- `TMXLayer.draw()` iterates over visible tiles
- Each tile is a `drawImage()` call through the quad batcher
- Multi-texture batching helps (up to 16 textures per flush) but the CPU-side loop and vertex pushing is the bottleneck
- A 100x100 visible tile area = 10,000 `addQuad()` calls per frame
Proposed Architecture
Data textures
-
Tile index texture — a `DataTexture` (or `UNSIGNED_SHORT` texture) where each pixel encodes the tile GID at that map position. Updated only when tiles change (rare). Size = map width × map height.
-
Tileset texture — the existing tileset spritesheet, already loaded as a GL texture.
Shader
The fragment shader:
- Receives the camera's visible area as uniforms (scroll position, viewport size)
- For each screen pixel, computes which tile and which pixel within that tile
- Looks up the tile GID from the index texture
- Samples the correct tile from the tileset texture
- Handles tile flipping flags (horizontal, vertical, diagonal) encoded in the GID
uniform sampler2D uTileIndex; // tile GID map
uniform sampler2D uTileset; // tileset spritesheet
uniform vec2 uMapSize; // map dimensions in tiles
uniform vec2 uTileSize; // tile size in pixels
uniform vec2 uTilesetSize; // tileset texture size in pixels
uniform vec2 uTilesetColumns; // tiles per row in tileset
uniform vec2 uScroll; // camera scroll position
vec4 apply(vec4 color, vec2 uv) {
// compute which tile this pixel is in
vec2 pixelPos = uv * uViewportSize + uScroll;
vec2 tileCoord = floor(pixelPos / uTileSize);
vec2 tileUV = fract(pixelPos / uTileSize);
// look up tile GID from index texture
float gid = texture2D(uTileIndex, tileCoord / uMapSize).r * 255.0;
if (gid == 0.0) discard; // empty tile
// compute tileset UV from GID
float col = mod(gid - 1.0, uTilesetColumns);
float row = floor((gid - 1.0) / uTilesetColumns);
vec2 tileOrigin = vec2(col, row) * uTileSize / uTilesetSize;
vec2 tileTexel = tileOrigin + tileUV * uTileSize / uTilesetSize;
return texture2D(uTileset, tileTexel);
}
Integration
- New `TMXGPULayer` class extending or wrapping `TMXLayer`
- Builds the tile index texture on layer load and when tiles change (`setTile()`)
- Renders as a single quad via the existing batcher with a custom shader
- Falls back to the standard tile-by-tile renderer for Canvas mode
- Needs to handle: multiple tilesets per layer, tile flipping, animated tiles, tile opacity
Challenges
- Multiple tilesets: a single layer can reference tiles from multiple tilesets. Options: merge into one mega-texture, use texture arrays (WebGL2), or split into one quad per tileset.
- Animated tiles: GIDs change over time. Either update the index texture per frame (cheap if few animated tiles) or encode animation data in the shader.
- Tile flipping: Tiled encodes flip flags in the upper bits of the GID. The shader needs to handle UV flipping.
- GID encoding: with 16-bit textures (`UNSIGNED_SHORT`), supports up to 65535 unique tiles. For larger tilesets, use `RGBA` encoding (4 bytes per tile).
- Isometric/hexagonal: initial implementation targets orthogonal maps only.
Performance expectations
- CPU: near-zero per-frame cost — no tile iteration, no vertex pushing, no per-tile draw calls
- GPU: single quad, single draw call, single shader. The shader does per-pixel work but GPUs are built for this.
- Memory: one extra texture (tile index map). For a 256x256 map with 16-bit GIDs = 128KB.
API Sketch
// automatic — TMXLayer uses GPU rendering when available
const layer = map.getLayer("Background");
layer.gpuRendering = true; // opt-in per layer
// or globally via application settings
new Application(800, 600, {
gpuTilemap: true, // enable for all layers
});
References
- `TMXLayer.draw()`: `src/level/tiled/TMXLayer.js`
- `QuadBatcher`: `src/video/webgl/batchers/quad_batcher.js`
- `ShaderEffect`: `src/video/webgl/shadereffect.js`
- WebGL2 texture formats: `gl.R16UI`, `gl.RGBA8` for tile index data
Summary
Render an entire TMXLayer as a single screen-aligned quad using a GPU shader, instead of drawing each tile individually. The shader samples from a tile index texture (which tile goes where) and a tileset texture (the actual tile graphics) to render the entire layer in one draw call.
Current State
melonJS renders tilemaps tile-by-tile:
Proposed Architecture
Data textures
Tile index texture — a `DataTexture` (or `UNSIGNED_SHORT` texture) where each pixel encodes the tile GID at that map position. Updated only when tiles change (rare). Size = map width × map height.
Tileset texture — the existing tileset spritesheet, already loaded as a GL texture.
Shader
The fragment shader:
Integration
Challenges
Performance expectations
API Sketch
References