The THREE.Object3D
hierarchy that results from GLTF2Loader can be quite deep. There may be quite a few places where a node can be merged with its parent or even eliminiated. An obvious example is the creation of the THREE.Group
node to represent the glTF mesh
entity, as in e.g.
THREE.Scene
(glTF scene)THREE.Object3D
(root node)THREE.Group
(mesh)THREE.Object3D
(primitive)In many (most?) use cases, mesh
exists only to hold an array of primitives
(and optionally one of associated morph target weights
). The THREE.Group
will always have the identity transform, so has no influence on its descendants -- but because it can have a unique name as well as optional extra
and extension
fields, it is not a given that it can always be eliminated.
@donmccurdy outlined his own, even deeper hierarchy resulting from a trivial 1-node model, in https://github.com/KhronosGroup/glTF/issues/1065
For the project I'm working on, our team must cope with complex scenes on mobile browsers, with weak CPUs. Any gratutious transform multiplications that can be eliminated from render loops is very valuable to us. Then again, it's not clear to which degree the user should be able to expect the glTF hierarchy to be mirrored in the resulting scene graph. It seems particularly unpleasant to perform node-collapsing optimization only in some cases (when no optional fields are present) in a loaded model, so that the user has no way of knowing what topology to expect.
I had mentioned the possibility of an optimizing loader option, where the user opts into a mode where they guarantee their mesh
structs will never have unique names, extras or extensions, and thus THREE.Group
can always be eliminated / merged into its parent. But I don't know if that's the three.js way.
Copied over from @stevenvergenz's comment in the glTF repo, we should be able to reduce a single-node single-mesh glTF file to this:
I hadn't realized three.js geometry.groups
existed, so that should simplify things here.
The only open question IMO would be, what to do with extras
and name
on a "mesh" object, since those aren't normally used on a THREE.BufferGeometry:
userData
and name
onto the BufferGeometry, even if it's non-default in three.js.extras
and name on anything that isn't a node, which is already true-ish because things like glTF buffers (which could have extras
too) are not exposed in the GLTF2Loader output at all.We could also start returning an array of meshes
and materials
with the GLTF2Loader response, but that's a slightly different issue for another time.
We recently added userData
to Material
#11915
Wouldn't be weird to add userData
to Geometry
and BufferGeometry
too.
With multi-material support, I think we've done essentially everything that can be done to flatten the node heirarchy.
Reopening this – I'm thinking we should actually be a bit _less_ aggressive about flattening while parsing the glTF model. We're pretty good today about flattening a node that only has one mesh, or merging primitives that can be a single geometry, but that's getting complex (e.g. to handle userData) and doesn't help us in cases where the node hierarchy in the original file is already silly, like this recent case:
We should just naively output something as close as we can to the original file, without merging nodes, meshes, or primitives, hopefully simplifying GLTFLoader a bit. And then at the end (either in the loader, or in a utility method users can call) we have a flatten() method that operates on the result. For example:
loader.load( 'foo.glb', function ( gltf ) {
SceneUtils.flatten( gltf.scene );
scene.add( gltf.scene );
} );
@takahirox thoughts?
Proposed https://github.com/mrdoob/three.js/pull/15889 to simplify this case.
Most helpful comment
Reopening this – I'm thinking we should actually be a bit _less_ aggressive about flattening while parsing the glTF model. We're pretty good today about flattening a node that only has one mesh, or merging primitives that can be a single geometry, but that's getting complex (e.g. to handle userData) and doesn't help us in cases where the node hierarchy in the original file is already silly, like this recent case:
We should just naively output something as close as we can to the original file, without merging nodes, meshes, or primitives, hopefully simplifying GLTFLoader a bit. And then at the end (either in the loader, or in a utility method users can call) we have a flatten() method that operates on the result. For example:
@takahirox thoughts?