Skip to content

GLTFLoader: Add support for importing single root files #31112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from

Conversation

aaronfranke
Copy link
Contributor

@aaronfranke aaronfranke commented May 15, 2025

Description

This PR adds support for importing files with the GODOT_single_root extension to GLTFLoader. This vendor extension is recognized by Khronos in their repository's vendor extensions folder (but not developed by or officially endorsed by Khronos).

Old text collapsed, I removed the export code so this is no longer correct:

With this PR, if you round-trip a non-scene Three.JS object like this:

const loader = new GLTFLoader();
const exporter = new GLTFExporter();

exporter.parse(myobject, function (gltfJson) {
	// We need to make a URL so that GLTFLoader can load the data.
	const blob = new Blob([JSON.stringify(gltfJson)], { type: "application/json" });
	const url = URL.createObjectURL(blob);
	loader.load(url, function (generated) {
		console.log(generated.scene);
	});
]);

... then the generated.scene object corresponds to the same object that was exported, with the same name, and with any data that can survive a round-trip through glTF node extensions. Without this PR, in the current dev branch, it will instead return a Group named "AuxScene", and the exported node will be a child of that, needlessly creating an extra node.

Also with this PR, if you round-trip a glTF file with the GODOT_single_root flag:

const loader = new GLTFLoader();
const exporter = new GLTFExporter();

loader.load("/path/to/my/file.gltf", function (generated) {
	exporter.parse(generated.scene, function (gltfJson) {
		console.log(gltfJson);
	});
]);

... then the exported glTF JSON data will have the GODOT_single_root flag set, because generated.scene is a single non-Scene object. This also applies to other objects regardless of if they came from a GODOT_single_root input file.

This improves interoperability with Godot Engine, including exporting files from Godot to Three.JS. I could imagine it being useful to use Godot as a visual tool to make objects for Three.JS. Other apps and engines may choose to support GODOT_single_root if they wish, such as a potential future Blender extension geared towards game development.

The code for this is heavily integrated into loadScene() so it can't be done with an extension to GLTFLoader, at most you could have code that wraps around GLTFLoader. Also, the code for this is quite minimal, most of the diffs are from indenting already existing code one scope further in.

Note: The "GODOT_single_root" extension name is prefixed with "GODOT_" because that is the context in which that extension was developed, however this does not mean it is only intended to be used for Godot. Quite the opposite actually, this is designed to improve interoperability between engines for glTF files containing "one object". The "EXT_" prefix is reserved for extensions developed by multiple vendors, it does not prescribe which apps can use it.

Also, I noticed the docs for GLTFLoader.parse said "Parses the given FBX data and returns the resulting group.". This is wrong in two ways: it's glTF not FBX, and it doesn't return the Group, it returns (in the callback) an object holding a bunch of generated data, which includes the single root node (which is Group without the single root flag, or another Object3D generated from the glTF root node with the single root flag). I've updated the description of this function.

This is my first contribution to Three.JS. I've tested this and I've done my best to follow the existing code style. If any changes are needed I will be happy to update the PR.

@aaronfranke
Copy link
Contributor Author

aaronfranke commented May 15, 2025

The CI failures seem to be unrelated to this PR. If I run npm run test-e2e on the dev branch, I also get lots of errors. This PR does not change behavior when files do not have GODOT_single_root (const isSingleRoot being false causes it to run the same code as in dev, the only other thing is I moved const nodeIds up).

@gkjohnson gkjohnson requested a review from donmccurdy May 15, 2025 05:00
@mrdoob mrdoob added this to the r177 milestone May 15, 2025
@aaronfranke aaronfranke changed the title GLTFLoader/GLTFExporter: Add support for single root files GLTFLoader: Add support for single root files May 17, 2025
@aaronfranke aaronfranke changed the title GLTFLoader: Add support for single root files GLTFLoader: Add support for importing single root files May 17, 2025
Mugen87
Mugen87 previously approved these changes May 18, 2025
Copy link
Collaborator

@Mugen87 Mugen87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@donmccurdy At least implementation-wise the PR looks good now. Not sure it makes sense to move the code to a separate extension class since the changes are so minor.

@takahirox
Copy link
Collaborator

Hmm, personally, I’m hesitant to support vendor extensions directly in the core glTF loader. Even if support for this extension only takes a few lines of code, accepting every vendor-specific extension just because “it’s only a few lines” quickly becomes unsustainable and unmaintainable.

In other words, if this vendor extension is to be included in the core glTF loader, I’d want to see a strong motivation for it — such as the extension being widely used and recognized in the broader ecosystem.

If it’s not widely adopted, then I think we should treat it the same way as other vendor extensions: start by exploring whether it can be handled through the plugin system. If that’s not currently possible, then we should first focus on improving the plugin system itself.

Also, what happens when you try to load a glTF file that uses this extension without applying this change? Does it fail to load entirely, or just produce incorrect results?

@Mugen87
Copy link
Collaborator

Mugen87 commented May 19, 2025

start by exploring whether it can be handled through the plugin system.

Good point! Without the additional validation in loadScene() it's only a single place where code needs to be changed. We should indeed try to move the extensions specific code into a separate class.

@Mugen87
Copy link
Collaborator

Mugen87 commented May 19, 2025

@aaronfranke Do you mind sharing a glTF asset using the GODOT_single_root extension in this thread?

@aaronfranke
Copy link
Contributor Author

aaronfranke commented May 19, 2025

@takahirox What about the feature itself? Is it valuable to be able to use glTF files to represent a "single object", and get that object as the returned Object3D directly?

Worth noting that this is already the implicit behavior of UnityGLTF, it skips generating an extra root node if the glTF file contains only one root node. This extension makes that behavior explicit.

Would this be easier to accept if it was simply named differently (KHR or EXT prefix)? I'm of the opinion that the name shouldn't matter if the feature is good, and that I am concerned with the idea of one organization (Khronos) gatekeeping what is allowed to exist in the ecosystem.

If it’s not widely adopted, then I think we should treat it the same way as other vendor extensions: start by exploring whether it can be handled through the plugin system. If that’s not currently possible, then we should first focus on improving the plugin system itself.

Ok, but how? Most glTF extensions are about changing the types or properties of returned objects. This extension very unique because it changes which object gets returned as the root.

Also, what happens when you try to load a glTF file that uses this extension without applying this change? Does it fail to load entirely, or just produce incorrect results?

All that will happen is it will have an extra Group root node generated, which needs to be handled by the user. The user could just ignore it and keep the extra Group object in the tree, or if they want the root node from the glTF, they would need to remove the Group via a heuristic decision, like checking for a Group with one child. But that has the problem of different behavior, where adding an additional root node actually changes the returned node and adds two generated nodes. The extension aims to make this explicit to avoid such surprise behavior.

Do you mind sharing a glTF asset using the GODOT_single_root extension in this thread?

Sure! boom_box.zip

With this PR, the imported .scene has the { name: "BoomBox", type: "Object3D" } properties. Without this PR, or if removing the flag from the file, the imported .scene has the { name: "", type: "Group" } properties.

Note that in this glTF file, the root node has the "OMI_physics_body" extension, defining the motion properties if the implementation chooses to treat it as a physics object. Three.js doesn't import that data yet (nor should it, at least not officially, since the physics extensions are still WIP, and Khronos has one under their own name which will very likely be the one used by the ecosystem).

The major use case of promoting the glTF root node to the returned root node is that for physics objects, their transform is meant to represent the transform of the entire object, since all child nodes follow that parent. Setting the "position" of the Group is not really a meaningful operation, but setting the position of the physics object is meaningful. Therefore, the Group is an obstacle in the way of the desired operation. If you kept the Group in the tree, code that sets positions of objects would have to deal with this as a special case.

@donmccurdy
Copy link
Collaborator

donmccurdy commented May 20, 2025

In the abstract – I'm OK with three.js supporting vendor-prefixed extensions like GODOT_*. I think of implementation as a "vote" by three.js for that extension eventually being further standardized as an EXT_ or KHR_ extension. So I agree that main question is whether the feature itself is a net positive, and we feel comfortable maintaining it. On that question, I'm conflicted:

Pros: On one hand a "flatter" result would indeed be nice; most three.js users are conceptually loading a model into an existing scene, and probably don't need a representation of a scene in the file. It's a pretty simple change – even simpler if we remove the nodeIds.length !== 1 validation warning, which I'd prefer.

Cons: trying to flatten the scene graph conditionally is how we got #29768 and several related issues. I'm hesitant to create more conditions we'd need to explain to users about how loader output will be structured.

@takahirox
Copy link
Collaborator

Lately, I've been thinking that the core parts of GLTFLoader and GLTFExporter are better off being as compact as possible, doing only the minimal necessary work. In my view, GLTFLoader should simply convert glTF data into Three.js structures as directly and accurately as possible, and GLTFExporter should do the reverse. This would simplify both tools and improve their maintainability.

As for optimization, that can be done after running GLTFLoader or GLTFExporter. If you want reusable, Three.js-friendly post-processing, you can use the plugin system. By keeping the conversion between glTF and Three.js data as straightforward and lossless as possible, we can avoid issues like #29768

Whether scene flattening is useful really depends on the use case. GLTFLoader could output with a Group node in between, which might seem redundant in some cases, but for use cases where flattening is beneficial, that could be handled as a post-process step. I think this approach would allow us to cover a wider range of use cases. In fact, I've been considering writing such a plugin for scene hierarchy optimization to do just that — though I haven’t had the time to get to it yet...

That's why I’m also cautious about adopting this extension as part of the core. If this were to be handled via a plugin, I wonder if the afterRoot hook could be used?

Regarding vendor extension support in the core GLTFLoader, I understand Don’s perspective. But at the very least, I’d like to see some prospect of the extension being promoted to an EXT_ or KHR_ standard. For example, if it’s being discussed within the glTF working group.

I think the overhead of debating every single vendor extension is non-trivial, and having some sign that an extension might eventually become standardized would help reduce that cost. At least, that’s how I feel personally.

@Mugen87 Mugen87 removed this from the r177 milestone May 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants