Draft: Atlas Material

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:

  1. AtlasStandardMaterial and AtlasPBRMaterial, which normalize multiple
    source materials into one shared shader family.
  2. A resource builder that either packs textures into atlases or, on future
    backends, exposes an equivalent resource table without changing the public API.
  3. 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 StandardMaterial or PBRBaseMaterial
    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

  • ShaderMaterial
  • NodeMaterial
  • MultiMaterial
  • CustomMaterial
  • MixMaterial
  • 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:

  • StandardMaterial and PBRBaseMaterial compile 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
  • shadowDepthWrapper remains 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
  • copyTexSubImage2D only 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 RawTexture2DArray dependency
  • no storage-buffer dependency
  • no uniform-buffer dependency
  • no assumption that texelFetch is 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.needPOTTextures and maxTextureSize
  • 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 AtlasPBRMaterial implementation 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 StorageBuffer usage
  • 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 atlas
  • array: texture-array backend when dimensions/sampler semantics allow it
  • bindless: 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 enableEffect transitions
  • 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”.

5 Likes

I like the idea. I’m wondering if it can be used with some kind of geometry atlas a la Nanite as well. Basically you set the geometry/texture budget and the engine determines alone what to load/unload.

Yeah, I think so, this targets mainly about how bind and draw happen, and efficiently create and use atlas from multiple material/texture, and should not conflict with a gpu-based culling/lod pass.
Also, since both mesh shader and multi draw indirect not landed on web there is still a long way to go.

There is already a tool for this. That I created a long time ago. It works with both Standard and PBR materials.

4 Likes

That’s great! A good place to start with, this works just like the model-targeted atlas generator, but there is still things remaining:

  1. Textures merged but not materials, the bind performance issue not addressed.
  2. The construction of the texture atlas is done on cpu, which means a lot of gpu-cpu copies are made, and the mipmaps are regenerated so there could lead to bleeding on rendering lower level mipmaps.
  3. Texture transforms not considered, and since it transforms mesh uv at vertex level, meshes uses uv warpping might change.
  4. UVs are dequantized and denormalized, might lead to more VRAM usage for quantized meshes.

It used the same material I’m pretty sure. Its been a few years but if I’m not mistaken. Yes there is some CPU load but it all is at time of incident so there is no impact after compilation. I dont see how you can do uv wrapping with an atlases texture unless we mod the value after it’s base uv cell is assigned. The uvs are actually normalized in this example so quantized ones I’m not sure how that is impacted.

1 Like

It just does not uses the same material after pack in the example playground. Also, the main thing I want to avoid is not the count or textures or materials, but the material.bindForSubMesh call, which can take massive amount of CPU time, merging textures and materials is the proposed way to this, the major change of this proposal is the rendering process.


Yes there is no impact compared to before packing, but this proposal is to change the rendering process to make it more efficient.

Current rendering model in pesudo code:

for each mesh:
    mesh._bind();
    material.bindForSubMesh();
    mesh._processRendering();// the draw call

Proposed rendering model:

for each atlas material:
    for each mesh:
       collectMeshInfo();// per-mesh props

    updateMeshTexture();
    bindMeshTexture();
    bindMaterial()
    bindLightAndShadow();

    for each mesh:
       bindGeometry();
       bindMeshId();
       dispatchDraw();// the draw call

To do this, uv wrapping need to be done at fragment stage, where uv is interpolated for the exact pixel.

Here quantized means the mesh (or vertex buffer) uses non-float attributes to save VRAM, like common UV mapping is float32 [0,1], 8 bytes per vertex, but quantized UV can be uint16 [0, 65535] normalized, 4 bytes per vertex, that’s half the size or original, where normalized mean GPU will map [0, 65535] to [0,1] in shaders.

1 Like

The material thing would be an oversight. They can all have the same material in theory since the atlas is bound the same. Would be worth a hot fix to correct that. Let me think about your other insights.

Great input btw. Sorry I’m on my phone off roading right now otherwise I’d be more engaged with you. But yes let’s get this all correct.

1 Like

Well, material can have props other than textures, like diffuseColor, emissiveColor, in case of StandardMaterial in this playground, that’s why a mesh texture is need, where these props can be kept and passed to shader.

1 Like

We can bind all that no problem. It will take some updates. I’ll pm you tomorrow and let’s discuss this more.

Are there any updates on this?
In my scenes, there are a lot of materials, so if this is implemented, it would significantly improve performance.


Click this btn for update history of the main post. It’s updated now with the help of the AI, but as the footnote said, “may not land in years”, so for now just stay with offline uv unwrap and baking, which is solid, compatible, and widely-used by the industry for many years

1 Like

Thank you for your information.

In my use case, users are able to freely place various types of furniture in a room (and they can also freely customize the furniture’s colors—that is, the materials—themselves), so I can’t pre-bake or create atlases in advance; I have to do it online.
I’m looking forward to seeing this implemented!

if color is the only to change between materials, have a try with instance.instancedBuffers.color
Docs:

Example:

1 Like

Thank you. I’m not sure if that will work in my situation, but I’ll look into it.

For your reference, here’s a demo video of what I’m working on:

You can place furniture anywhere you like, and customize the color—and in some cases, the texture—for each individual furniture part.
As a result, when there’s a lot of furniture, the large number of materials can cause performance issues.

1 Like