Skip to content

Multi-texture batching for WebGL renderer #1376

@obiot

Description

@obiot

Summary

Implement multi-texture batching in the WebGL quad batcher to significantly reduce draw calls. Instead of flushing the batch every time a different texture is needed, bind multiple textures simultaneously to different texture units and carry a per-vertex texture ID.

Current Behavior

The QuadBatcher binds one texture at a time via uSampler. When a sprite uses a different texture than the current one, the sampler uniform is updated. While the texture cache manages multiple texture units, the shader only samples from a single uniform per draw call, limiting batching across different sprite sheets.

Proposed Change

Vertex format

Add a textureId attribute (float) to identify which texture unit to sample from:

// current: x, y, u, v, tint (5 floats)
// proposed: x, y, u, v, tint, textureId (5 floats + 1)

Fragment shader

Replace the single uSampler with a texture array:

uniform sampler2D uTextures[N]; // N = max batch textures (e.g. 8 or 16)
varying float vTextureId;

void main(void) {
    // sample from the correct texture unit based on vertex attribute
    gl_FragColor = texture2D(uTextures[int(vTextureId)], vRegion) * vColor;
}

Note: GLSL ES does not allow dynamic indexing of sampler arrays. The standard workaround is an if/else chain or a switch statement in the shader, which GPUs optimize well for small N.

Batcher changes

  • Track up to N bound textures per batch (N = gl.MAX_TEXTURE_IMAGE_UNITS, clamped to a practical limit like 8 or 16)
  • When addQuad receives a new texture, assign it the next available slot instead of flushing
  • Only flush when all texture slots are full and a new texture is needed
  • Push the assigned textureId into the vertex data alongside existing attributes

Batch break conditions (updated)

A batch flush would only be needed when:

  • All texture slots are occupied AND a new texture appears
  • The blend mode changes
  • A custom shader is set
  • The vertex buffer is full

Expected Impact

For a typical 2D game with 5-10 different sprite sheets/textures, this could reduce draw calls by 5-10x since sprites using different textures would no longer break the batch. This is the single highest-impact rendering optimization for scenes with diverse texture sources.

ShaderEffect / Custom Shader Compatibility

Fully backward compatible — no changes needed for ShaderEffect or custom GLShader users:

  • useShader() already flushes the current batch before binding a custom shader (batcher.js:219)
  • The custom shader draws only its own renderable's quads using the single-texture uSampler path
  • After postDraw clears customShader, the next draw call switches back to the default multi-texture shader (another flush)
  • The custom shader is already fully isolated per-renderable — multi-texture batching falls back to single-texture mode naturally for just that renderable

References

  • QuadBatcher.addQuad(): src/video/webgl/batchers/quad_batcher.js:141
  • MaterialBatcher.uploadTexture(): src/video/webgl/batchers/material_batcher.js:259
  • Batcher.useShader() (flush on shader change): src/video/webgl/batchers/batcher.js:214
  • TextureCache: src/video/texture/cache.js
  • Max texture units: webgl_renderer.js:104
  • Quad vertex shader: src/video/webgl/shaders/quad.vert
  • Quad fragment shader: src/video/webgl/shaders/quad.frag
  • VertexArrayBuffer.push(): src/video/webgl/buffer/vertex.js:45

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions