react-three-fiber icon indicating copy to clipboard operation
react-three-fiber copied to clipboard

Materials returned by buildGraph are not reliable

Open AlaricBaraou opened this issue 3 years ago • 8 comments

While using the GltfLoader from three.js through drei useGltf hook, I noticed that the render varied from time to time after reloading. After investigating I found out that the order of objects returned by GltfLoader in the imported scene may vary from one load to the other.

Since buildGraph will group material by name, it'll use the first material with a specific name as the "reference" for other materials using the same name. Since the order may vary on load, the material returned for a particular name might vary too. That's also an expectation that might need to be discussed, the Gltf specs specify that a material name isn't necessarily unique.

https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html

Any top-level glTF object MAY have a name string property for this purpose. These property values are not guaranteed to be unique as they are intended to contain values created when the asset was authored.

And three.js also doesn't expect a material name to be unique https://threejs.org/docs/#api/en/materials/Material.name

Optional name of the object (doesn't need to be unique). Default is an empty string.

Therefore, I believe buildGraph should group by uuid, since three.js already reuse material in the GltfLoader https://github.com/utsuboco/three.js/blob/dev/examples/jsm/loaders/GLTFLoader.js#L3145

Materials could still return an object containing the names of the materials but with a constant unique identifier to it to differentiate them. Maybe something like the cacheKey that three.js use in GltfLoader https://github.com/utsuboco/three.js/blob/dev/examples/jsm/loaders/GLTFLoader.js#L3138

Doing so, there wouldn't be any "randomness" in which material gets to be returned by buildGraph under the common name. But this would be a breaking change and should probably be targeted for v9.

That said, here is a simple example.

  1. Loading a GLTf containing two mesh with these two materials
  2. Mesh1 has a material with the name 'cloth' Mesh2 has a similar material also using the name 'cloth' but with a vertexColors property set to true
  3. After loading, buildGraph traverse the scene, since the order isn't guaranteed, Mesh1 or Mesh2 can come up first.
  4. The first will assign see its material assigned to materials.cloth and the next one will be ignored
  5. Tools like gltfjsx will then assign the same cloth material to both mesh while they don't share it initially.

I hope I could make the issue clear enough, otherwise, let me know if something isn't clear.

If/once everyone agrees with this and on the best way to solve this, I'd be happy to make a PR myself. 👍

AlaricBaraou avatar Sep 16 '22 03:09 AlaricBaraou

@AlaricBaraou yes, gltfloader is async, the order is not guaranteed. it wasn't always like that and previously useGraph/gltfjsx was order based, but threejs had a breaking change.

if meshes have the same name, that's too bad. :-/ uuid, ... i don't know how that would work since the uuid is runtime generated, no? so how could gltfjsx create an object index when the uuid's will be different on load?

what could work is that we use unique names by adding "-[index]" for duplicates. would be relatively easy, too. if you want, go ahead with a pr.

drcmda avatar Sep 25 '22 19:09 drcmda

Thanks for sharing more info on this. I'm not sure why I suggested uuid to fix this, I remember I mostly felt like material should be grouped the same way three GLTFLoader group them. Like here if I remember correctly https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/GLTFLoader.js#L3199

I'll get back to this one in a few days, I'm a bit busy with the Sougen sale coming

AlaricBaraou avatar Sep 26 '22 00:09 AlaricBaraou

Can you share that link but pointed to https://github.com/mrdoob/three.js? The Utsubo fork is private.

CodyJasonBennett avatar Sep 26 '22 01:09 CodyJasonBennett

Updated it, forgot I was on the Utsubo fork my bad!

AlaricBaraou avatar Sep 26 '22 01:09 AlaricBaraou

if meshes have the same name, that's too bad. :-/ uuid, ... i don't know how that would work since the uuid is runtime generated, no? so how could gltfjsx create an object index when the uuid's will be different on load?

Just to clarify, he wasn't talking about meshes having the same name but materials.

A GLTF of a chess board for example would have pieces that may both have the material named "Body" but be totally opposite colors.

Here is a link to relative material loading in the core three loader: https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/GLTFLoader.js#L3374

It shows there is a cacheKey for the material and there is also a UUID...

I'm not familiar enough with the buildGraph to point to relevant code yet though..

DennisSmolek avatar Nov 09 '22 14:11 DennisSmolek

Is this solvable without a modification in GLTFLoader and/or glTF? https://github.com/KhronosGroup/glTF/issues/2337

CodyJasonBennett avatar Oct 21 '23 11:10 CodyJasonBennett

See https://github.com/mrdoob/three.js/pull/27052.

CodyJasonBennett avatar Nov 16 '23 04:11 CodyJasonBennett

With materials specifically, there's another problem. Even if your glTF model has only ONE material, three.js might need to clone that material to configure it for use by incompatible meshes. Vertex colors would be one example, but there are others:

https://github.com/mrdoob/three.js/blob/273eb1a4e0b506998438698dbb4c38ff6cd39b3f/examples/jsm/loaders/GLTFLoader.js#L3323-L3339

I don't think this is solvable by https://github.com/KhronosGroup/glTF/issues/2337. At best that would ensure that every name occurring in the glTF file is unique. But the issue above remains - glTF objects cannot translate 1:1 into three.js objects, and we must clone materials and meshes at runtime in order to construct a valid three.js scene graph.

Ultimately runtime-generated IDs cannot be stable across all three.js releases, it's an unworkable goal for GLTFLoader to meet. We do intend for names to be deterministic and stable within any given release – if that's not the case, a bug could be filed.

I realize stable names are really necessary for JSX, though, and I think sanitizing the glTF file — at the time the JSX is generated — is the safer approach. See:

  • https://github.com/pmndrs/gltfjsx/issues/231

donmccurdy avatar Nov 16 '23 04:11 donmccurdy

R3F will follow Three's lead here. Thanks for the discussion everyone. The current functionality will maintain in v9.

krispya avatar Apr 26 '24 17:04 krispya