Skip to content

glTF texture handles shift when image loading fails, assigning wrong textures to materials #23465

@Elogain

Description

@Elogain

Bevy version

0.18.0

What you did

Loaded a glTF/GLB file containing JPEG textures without the jpeg feature enabled. The GLB had 12 embedded textures (6 JPEGs, 6 PNGs) across 3 materials, each with baseColor (JPEG), metallicRoughness (PNG), emissive (JPEG), and normal (PNG) textures.

What went wrong

Instead of showing missing textures as white (1x1 fallback), the model displayed wrong textures on each material — metallicRoughness data rendered as base color, normal maps appeared in wrong slots, and some materials got no textures at all. This made debugging extremely difficult because the model appeared to render "something" rather than clearly failing.

Root cause

In bevy_gltf/src/loader/mod.rs, the parallel texture loading path builds texture_handles as a Vec:

.into_iter()
.zip(gltf.textures())
.for_each(|(result, texture)| match result {
    Ok(image) => {
        image.process_loaded_texture(load_context, &mut texture_handles);
    }
    Err(err) => {
        warn!("Error loading glTF texture: {}", err);
    }
});

When a texture fails to load (Err branch), nothing is pushed to texture_handles, but subsequent successful textures continue to be pushed. This causes every handle after a failure to shift down by one index per failed texture. Later, load_material looks up textures by their original JSON index via textures.get(info.texture().index()), which now points to the wrong handle.

With 6 JPEG failures out of 12 textures, texture_handles had 6 entries instead of 12. Material lookups at indices 0-11 were completely wrong — some got the wrong texture type (e.g., a normal map loaded as base color), and indices ≥6 fell off the end, hitting unwrap_or_default() which returns the 1x1 white fallback.

Expected behavior

When a texture fails to load, the material slots that reference it should get the default 1x1 fallback texture. Other materials' textures should be unaffected. The model should render with some white/missing patches but otherwise correct textures on the materials that loaded successfully. Or crash out.

Suggested fix

Either:

Option A: Push a default handle on failure to preserve index alignment:

Err(err) => {
    warn!("Error loading glTF texture: {}", err);
    texture_handles.push(Handle::default());
}

This ensures texture_handles[i] always corresponds to gltf.textures()[i], regardless of load failures. Materials referencing a failed texture would get the 1x1 white fallback, while all other materials render correctly.

Option B: Panic on out-of-bounds texture lookup in load_material:

Currently, textures.get(info.texture().index()).cloned().unwrap_or_default() silently returns a default handle when the index is out of bounds. This masks the root cause. Consider panicking instead:

textures[info.texture().index()].clone()

An out-of-bounds texture index is always a bug (either in the glTF file or in the loader), and silently substituting a default handle makes it much harder to diagnose.

How to reproduce

  1. Create a project with Bevy 0.18 without the jpeg feature
  2. Load any GLB with multiple materials that use both JPEG and PNG textures
  3. Observe that PNG textures are assigned to wrong material slots instead of only JPEG slots showing as missing

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-AssetsLoad files from disk to use for things like images, models, and soundsC-BugAn unexpected or incorrect behavior

    Type

    No type

    Projects

    Status

    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions