Skip to content

createInstance() overwrites morph target buffers of the primary instance #9914

@mp0rta

Description

@mp0rta

Describe the bug
When AssetLoader::createInstance() is called for an asset that contains morph targets, the later instance creation overwrites the morphTargetBuffer pointer inside the shared Primitive stored in MeshCache.
As a result, ResourceLoader uploads morph target data to the wrong MorphTargetBuffer, and morphing stops working on the first-created instance (for example, texelFetch returns zero-equivalent data and visible deformation does not occur).
This appears to be caused by storing a per-instance MorphTargetBuffer* in Primitive, even though Primitive is shared across instances through MeshCache.

To Reproduce
Steps to reproduce the behavior:

  1. Load a GLB file that has morph targets (e.g., a VRM model with facial blend shapes)
  2. Create the first-created instance with assetLoader.createAsset(buffer)
  3. Create a second instance with assetLoader.createInstance(asset)
  4. Load resources with resourceLoader.loadResources(asset)
  5. Set morph weights on the primary instance's renderable entities via RenderableManager.setMorphWeights()
  6. Morph targets have no visible effect on the primary instance

Expected behavior
Morph targets should deform the mesh correctly for the first-created instance regardless of whether additional instances exist.

Smartphone (please complete the following information):

  • Device: Pixel 6a
  • OS: Android 16 (API 36)
  • Backend: OpenGL ES (Filament default)

Additional context
The Primitive struct (FFilamentAsset.h:96-104) is shared across all instances via MeshCache:
https://github.com/google/filament/blob/v1.71.0/libs/gltfio/src/FFilamentAsset.h#L96-L104

struct Primitive {
     VertexBuffer* vertices = nullptr;
     IndexBuffer* indices = nullptr;
     Aabb aabb;
     UvMap uvmap;
     MorphTargetBuffer* morphTargetBuffer = nullptr;  // shared, but written per-instance
     uint32_t morphTargetOffset;
     std::vector<int> slotIndices;
 };
 using MeshCache = utils::FixedCapacityVector<utils::FixedCapacityVector<Primitive>>;

In AssetLoader.cpp, createRenderable() (line 798-810) creates a new MorphTargetBuffer per instance but stores it in this shared Primitive:
https://github.com/google/filament/blob/v1.71.0/libs/gltfio/src/AssetLoader.cpp#L798-L810

  if (numMorphTargets) {
      MorphTargetBuffer* morphTargetBuffer = MorphTargetBuffer::Builder()
              .count(numMorphTargets)
              .vertexCount(morphingVertexCount)
              .build(mEngine);

      fAsset->mMorphTargetBuffers.push_back(morphTargetBuffer);
      builder.morphing(morphTargetBuffer);

      outputPrim = prims.data();
      inputPrim = &mesh->primitives[0];
      for (cgltf_size index = 0; index < primitiveCount; ++index, ++outputPrim, ++inputPrim) {
          outputPrim->morphTargetBuffer = morphTargetBuffer;  // line 810: overwrites shared Primitive

The BufferSlot entries (line 836, 854) also read from the overwritten outputPrim->morphTargetBuffer, so ResourceLoader::uploadBuffers() uploads morph data to the last-created
instance's buffer for all instances.

Introduced in commit bef004e Confirmed unchanged through v1.71.0.

Metadata

Metadata

Assignees

Labels

gltfSpecific to glTF supportsecurity

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions