Summary
Introduce a runtime atlas material system that lets meshes using different source
materials, but the same Babylon shading family, render through a single shared
material bind.
This is not an offline UV packer. It is a rendering feature with three parts:
AtlasStandardMaterialandAtlasPBRMaterial, which normalize multiple
source materials into one shared shader family.- A resource builder that either packs textures into atlases or, on future
backends, exposes an equivalent resource table without changing the public API. - A grouped render path that binds material resources once per batch and then
emits many geometry draws.
The immediate target is CPU time spent in material binding and repeated texture
rebinding. The longer-term target is a submission model that is compatible with
WEBGL_multi_draw, WebGPU multi draw indirect, and eventual bindless-capable backends.
Motivation
Rendering on web suffers from performance issues, there has been disucssions for years about reducing “draw calls”, which is draw commands sent from cpu to gpu to draw a mesh, and considered the major bottleneck towards high FPS on web.
But recent benchmarks show that binding materials can take considerable amount of time, even more than draw calls.
On this sample with the NodePerformanceTest.glb on sandbox, bindForSubMesh takes half the CPU time, while drawElements, the actual draw call, takes 0.8% the CPU time.
Tools like glTF-Transform can detect and merge materials with exactly the same props, and merge materials with baseColor only.
But merging materials isn’t always the option, since materials can have different prop, or even textures, the easist way to merge masterials with textures is Texture atlas, but this can not handle meshs with uvs out of the 0~1 range with customized wrapping.
Also, making texture atlas in models can lead to uv precision loss, especially for quantized attributes.
Goals
- Reduce CPU time spent in
bindForSubMesh,enableEffect, and repeated
texture/sampler rebinding. - Support runtime batching of multiple
StandardMaterialorPBRBaseMaterial
sources without rewriting mesh UVs. - Preserve source UV range, source wrap mode, and source texture transform
semantics. - Fill missing channels with neutral resources so the rendered result matches the
original no-texture behavior. - Work with Babylon’s standard render-pass ecosystem: main forward rendering,
shadow maps, depth, geometry buffer, prepass, custom render targets, mirror
textures,ObjectRenderer, and frame graph object renderer tasks. - Keep the public API stable even if the underlying backend evolves from physical
texture atlases to arrays or bindless-style resource indexing. - Make the per-draw data model reusable for future multi-draw paths.
Non-Goals
ShaderMaterialNodeMaterialMultiMaterialCustomMaterialMixMaterial- offline asset processing
- automatic glTF import-time atlas generation
- full transparency batching in v1
- general bindless texture support in v1
- refraction atlasing in v1
- reflection cube/environment texture merging in v1
- video or external texture support in v1
- arbitrary compressed source-format merging in v1
- frequently-mutating procedural or render-target sources without explicit
rebuild/invalidation handling in v1
Design Principles
- Do not mutate source mesh UVs.
- Do not require source materials to have identical texture-channel sets.
- Keep batching inside Babylon’s existing pass orchestration rather than creating
a parallel renderer. - Fall back to the current per-submesh path whenever compatibility fails.
- Treat atlas packing as one backend implementation, not as the public API’s only
possible realization.
Current Constraints
Current Babylon rendering is fundamentally per-submesh. Atlas materials must fit
inside that model first, then optimize it.
Important constraints:
StandardMaterialandPBRBaseMaterialcompile many feature-dependent effect
variants.- render-state differences such as alpha mode, culling, and fill mode remain
batch boundaries. - custom render passes often consume the same submesh lists via
RenderingGroup,RenderTargetTexture.customRenderFunction,ObjectRenderer,
or frame graph tasks. - WebGPU already has strong material-context caching, while WebGL benefits more
directly from reduced rebinding.
Material Compatibility Model
The merge rule is not “same shading model”. It is:
same shading family plus the same normalized feature mask and compatible render
states.
Must Match
- material class family
- transparency bucket
- alpha mode bucket
- culling and fill state
- features that fundamentally change shader code
- pass-affecting flags
Can Be Normalized
Use neutral resources for missing channels:
- missing diffuse/albedo → white tile + color multiplier
- missing opacity → white tile
- missing emissive → black tile
- missing normal map → flat normal tile
(0.5, 0.5, 1.0) - missing metallic/reflectivity → neutral reflectivity tile
- missing roughness/microSurface → neutral roughness tile
- missing occlusion/lightmap → multiplicative neutral tile or the exact
material-family no-texture default
The neutral value must be defined per channel and per shading family so that
“texture absent” and “neutral resource bound” are visually equivalent.
Explicitly Out of Scope in v1
- alpha-blended batching as a correctness target
- refraction atlasing
- reflection cube merging
- video/external textures
- frequently-updated render target or procedural textures without explicit
invalidation handling - arbitrary compressed source formats that cannot be copied into a common atlas
Rendering Model
Current Model
for each subMesh:
bindGeometryIfNeeded();
material.bindForSubMesh();
draw();
Proposed Atlas Batch Model
for each atlasBatch:
collectMeshEntries();
updateMeshTableOnce();
bindAtlasMaterialOnce();
bindPassStateOnce();
for each draw in currentAtlasBatch:
bindGeometry();
bindMeshSelector(draw.meshId);
draw();
This still draws once per submesh in v1. The win comes from moving most material
work out of the inner loop.
The same batch contract could later become:
for each atlasBatch:
collectMeshEntries();
updateMeshTableOnce();
bindAtlasMaterialOnce();
bindPassStateOnce();
dispatchMultiDraw();
Render-Pass Integration
Main Forward Rendering
- integrate through
RenderingGroup - preserve opaque / alpha-test / transparent buckets
- batch only within each existing bucket
Shadow Maps
- atlas materials provide shadow-depth variants
shadowDepthWrapperremains the specialization entry point- grouped submission is reused inside shadow passes
Depth / Geometry Buffer / Prepass
- reuse the same grouped render primitive
- preserve the current pass-specific filtering and draw order
Custom Render Targets, Mirror, Object Renderer, Frame Graph
- atlas batching remains an internal acceleration inside existing pass
orchestration - if a pass consumes submesh lists through
customRenderFunction,
ObjectRenderer, or frame graph object renderer tasks, atlas batching groups
those lists in place - unsupported cases fall back to the current per-submesh path
Atlas Construction
Packing
Use a potpack-style rectangle packer rather than extending the existing
TexturePacker layout system.
Requirements:
- deterministic packing
- mip gutter support
- stable rect output for caching
- shared rect layout across all packed channels of a batch
Neutral Fill Policy
If materials do not all expose the same texture channels:
- do not reject the merge automatically
- generate or reuse neutral-filled resources for missing channels
- point missing channels at those neutral resources
GPU Construction
Fast path:
- WebGPU
copyTextureToTexture - full mip-chain copy when formats, mip counts, and padding make it safe
Fallback path:
- shader blit through Babylon’s copy-texture helper path
- regenerate atlas mipmaps when source mips cannot be preserved
WebGL path:
- framebuffer-backed copy when legal
copyTexSubImage2Donly when the source is already renderable and compatible- otherwise shader blit
UV and Sampling Semantics
Original mesh UVs remain untouched.
Per channel, atlas materials need:
- UV set selector
- texture matrix
- wrapU / wrapV
- atlas rect or resource index
Logical sampling order:
baseUV = selectUV(uv0, uv1, entry.uvSet); // can be lifted to vertex stage
transformedUV = applyTextureMatrix(baseUV, entry.textureMatrix); // can be lifted to vertex stage
wrappedUV = applyWrap(transformedUV, entry.wrapU, entry.wrapV);
sampleUV = resolveBackendUVOrIndex(wrappedUV, entry);
sample(sampleUV);
This preserves source texture transform semantics instead of baking them into the
mesh.
Seams and Mips
Required mitigations:
- mip gutters
- texel-aligned rect placement
- sampling clamped inside padded regions
- copied source mips when possible
- regenerated atlas mips otherwise
Backend Strategy
WebGL v1
Use:
- atlas textures
- texture-backed mesh table
- small per-draw mesh selector uniform
WebGL1 support appears feasible against the current Babylon.js codebase, but only
as a constrained backend:
- no
RawTexture2DArraydependency - no storage-buffer dependency
- no uniform-buffer dependency
- no assumption that
texelFetchis available - no assumption that MRT-only paths are available everywhere
Implications for a WebGL1 backend:
- the resource backend should stay atlas-based, not array-based
- the mesh table should use ordinary 2D textures with normalized UV lookup, not
texture arrays or storage buffers - metadata formats should fall back to widely-supported encodings such as RGBA8
when float-texture filtering/support is not available - atlas sizing must obey
engine.needPOTTexturesandmaxTextureSize - copy/build steps must rely on shader blits or framebuffer-compatible copy paths
instead of WebGPU-style direct texture copies
Recommendation:
- keep WebGL1 support as a compatibility backend for
AtlasStandardMaterial - allow the first
AtlasPBRMaterialimplementation to require a stricter feature
subset or WebGL2+ if necessary - gate optional quality/perf features on caps such as
standardDerivatives,textureLOD,textureFloatLinearFiltering, and
instancedArrays
WebGPU v1
Start with:
- atlas textures or a simple equivalent backend
- resource table compatible with future
StorageBufferusage - shared material contexts to maximize bind-group reuse
Future Backends
The public API stays the same while the backend can evolve:
atlas: proposed physical texture atlasarray: texture-array backend when dimensions/sampler semantics allow itbindless: future resource-index backend that does not physically merge
textures
Performance Expectations
Expected CPU wins:
- fewer material binds
- fewer sampler/texture updates
- fewer material uniform updates
- fewer
enableEffecttransitions - better WebGPU bind-group reuse
Expected GPU costs:
- extra indirection for material metadata
- extra fragment ALU for wrap/transform logic
- potential atlas cache pressure
This tradeoff is acceptable because the target scenes are CPU-bound on material
binding.
Risks
- atlas VRAM growth
- shader complexity growth
- rebuild churn on frequently-mutated sources
- partial pass adoption reducing real wins
- backend divergence over time
Mitigations:
- hard texture-size limits
- configurable padding
- explicit rebuild/invalidation API
- shared grouped render primitive reused across passes
- backend abstraction from the start
Alternatives
- Use bindless texture so it’s possible to render without the need to bind, but this is only for webgpu without even a draft spec.
- Unwrap the UV and bake it offline so all materials can become one, but this is complex and time consuming.
- Create texture atlas and merge materials before importing models, causing uv warpping issues and uv precision loss.
- Use BABYLON.TexturePacker to merge textures, and merge materials somehow.
Footnote
I know this change is huge and may not land in years, so I’ll just keep this as “draft”.




