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
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
QuadBatcherbinds one texture at a time viauSampler. 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
textureIdattribute (float) to identify which texture unit to sample from:Fragment shader
Replace the single
uSamplerwith a texture array: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
gl.MAX_TEXTURE_IMAGE_UNITS, clamped to a practical limit like 8 or 16)addQuadreceives a new texture, assign it the next available slot instead of flushingtextureIdinto the vertex data alongside existing attributesBatch break conditions (updated)
A batch flush would only be needed when:
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
ShaderEffector customGLShaderusers:useShader()already flushes the current batch before binding a custom shader (batcher.js:219)uSamplerpathpostDrawclearscustomShader, the next draw call switches back to the default multi-texture shader (another flush)References
QuadBatcher.addQuad():src/video/webgl/batchers/quad_batcher.js:141MaterialBatcher.uploadTexture():src/video/webgl/batchers/material_batcher.js:259Batcher.useShader()(flush on shader change):src/video/webgl/batchers/batcher.js:214TextureCache:src/video/texture/cache.jswebgl_renderer.js:104src/video/webgl/shaders/quad.vertsrc/video/webgl/shaders/quad.fragVertexArrayBuffer.push():src/video/webgl/buffer/vertex.js:45