Skip to content
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

Import custom glTF properties as node metadata #8271

Closed
CarpenterBlue opened this issue Oct 27, 2023 · 18 comments · Fixed by godotengine/godot#86183
Closed

Import custom glTF properties as node metadata #8271

CarpenterBlue opened this issue Oct 27, 2023 · 18 comments · Fixed by godotengine/godot#86183

Comments

@CarpenterBlue
Copy link

Describe the project you are working on

Game with lot of shooting that requires lot of level iteration.
Constantly switching between engine and the 3D software produces lot of issues during the development.

Describe the problem or limitation you are having in your project

The custom properties seem to be completely ignored on import which makes achieving workflow like this:
https://youtu.be/HGbuLGH0GWs?si=Hub_eiUloGIcYgmZ&t=1073
Significantly harder.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

The custom properties would be written on import to the metadata.
From there, user can easily decide what to do with them in the post_import script.
For example material could have metadata included as "StepSound" : "grass"

They are already bundled in the GLTF file when exported from 3D software.
image

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

These can already be assigned in Blender or other 3D modeling software of choice.
image
Ideally they would be assignable during import too.
image
In Godot, user could then work with them in post_import script or even in the running game.
image

If this enhancement will not be used often, can it be worked around with a few lines of script?

I think it would make building workflows and pipelines significantly easier.

Is there a reason why this should be core and not an add-on in the asset library?

QOL change that seems like missing functionality if anything. most of the logic is already in the engine.

@CarpenterBlue CarpenterBlue changed the title Pass Custom Properties from GLTF to associated Nodes as Metadata allow assigning them in the importer. Pass Custom Properties from GLTF to associated Nodes as Metadata. Oct 27, 2023
@Calinou
Copy link
Member

Calinou commented Oct 27, 2023

@Calinou Calinou changed the title Pass Custom Properties from GLTF to associated Nodes as Metadata. Import custom glTF properties as node metadata Oct 27, 2023
@noidexe
Copy link

noidexe commented Oct 29, 2023

I was about to create a proposal similar to this one. A couple things I've found out while trying to workaround this limitation:

Extras are being parsed already

gltf_document.cpp is actually parsing the extras section, but it only cares about finding a key named targetNames and completely ignores the rest https://github.com/godotengine/godot/blob/214405350f3893bb6960c5200ec6f683dd10b41d/modules/gltf/gltf_document.cpp#L2563

Workaround with GLTFDocumentExtension has two problems

It is possible to create a GLTFDocumentExtension plugin and override _import_node() to set the extras dictionary as node metadata like this:

	func _import_node(state: GLTFState, gltf_node: GLTFNode, json: Dictionary, node: Node) -> Error:
		if json.has("extras"):
			node.set_meta("extras", json.extras.duplicate(true))
		return OK

This has two problems:

a) _import_node() only handles what will become Godot nodes, so it won't parse things like meshes or materials. That means at least with that method override it's impossible to set extra properties as resource metadata.

b) With the code listed above you'll notice lights and cameras have metadata set properly but it's misteriously missing from MeshInstance3D nodes, probable the main use case for the feature.

The problem is that _import_node() doesn't receive a MeshInstance3D in it's node param but an ImporterMeshInstance3D. Those are later converted by an engine-provided document extension. The converted only copies key properties like name, mesh, transform, etc but it won't copy metadata. https://github.com/godotengine/godot/blob/214405350f3893bb6960c5200ec6f683dd10b41d/modules/gltf/extensions/gltf_document_extension_convert_importer_mesh.cpp#L55

I couldn't find any easy workarounds that worked well.

Blender importer setting confusingly appear to support the feature but don't

If you're working with Blender and drop the .blend files directly into Godot the import settings show a checkbox for "custom properties", which blender exports as extras. This made me believe I could find that data somewhere in the imported scene but I was wrong. The imported tells blender to export the custom properties as gltf extras, but does nothing with it.

All in all, I think it'd really be worth it to have that feature out of the box. It's not a big change to the current importer, it doesn't force every user to get familiar with the gltf internals and overall it can be really helpful to automate the 3d import process.

I'm attaching a little Godot project with a .blend file using custom properties and my attempt at importing them, in case it's useful.

GLTF Extras.zip

@noidexe
Copy link

noidexe commented Oct 29, 2023

@CarpenterBlue I have a workaround that will handle metadata in every node, plus meshes and materials

You need to add an EditorPlugin with the following:

@tool
extends EditorPlugin

var importer

func _enter_tree() -> void:
	importer = ExtrasImporter.new()
	GLTFDocument.register_gltf_document_extension(importer)
	
	# Initialization of the plugin goes here.
	pass


func _exit_tree() -> void:
	GLTFDocument.unregister_gltf_document_extension(importer)
	pass

class ExtrasImporter extends GLTFDocumentExtension:
	func _import_post(state: GLTFState, root: Node) -> Error:
		# Add metadata to materials
		var materials_json : Array = state.json.get("materials", [])
		var materials : Array[Material] = state.get_materials()
		for i in materials_json.size():
			if materials_json[i].has("extras"):
				materials[i].set_meta("extras", materials_json[i]["extras"])
		
		# Add metadata to ImporterMeshes
		var meshes_json : Array = state.json.get("meshes", [])
		var meshes : Array[GLTFMesh] = state.get_meshes()
		for i in meshes_json.size():
			if meshes_json[i].has("extras"):
				meshes[i].mesh.set_meta("extras", meshes_json[i]["extras"])
		
		# Add metadata to nodes
		var nodes_json : Array = state.json.get("nodes", [])
		for i in nodes_json.size():
			var node = state.get_scene_node(i)
			if !node:
				continue
			if nodes_json[i].has("extras"):
				# Handle special case
				if node is ImporterMeshInstance3D:
					# ImporterMeshInstance3D nodes will be converted later to either
					# MeshInstance3D or StaticBody3D and metadata will be lost
					# A sibling is created preserving the metadata. It can be later 
					# merged back in using a EditorScenePostImport script
					var metadata_node = Node.new()
					metadata_node.set_meta("extras", nodes_json[i]["extras"])
					
					# Meshes are also ImporterMeshes that will be later converted either
					# to ArrayMesh or some form of collision shape. 
					# We'll save it as another metadata item. If the mesh is reused we'll 
					# have duplicated info but at least it will always be accurate
					if node.mesh and node.mesh.has_meta("extras"):
						metadata_node.set_meta("mesh_extras", node.mesh.get_meta("extras"))
					
					# Well add it as sibling so metadata node always follows the actual metadata owner
					node.add_sibling(metadata_node)
					# Make sure owner is set otherwise it won't get serialized to disk
					metadata_node.owner = node.owner
					# Add a suffix to the generated name so it's easy to find
					metadata_node.name += "_meta"
				# In all other cases just set_meta
				else:
					node.set_meta("extras", nodes_json[i]["extras"])
		return OK

Then you need to add the following to your EditorScenePostImport script:

@tool
extends EditorScenePostImport

var verbose := true

func _post_import(scene: Node) -> Object:
	_merge_extras(scene)
	### YOUR CODE HERE ###
	return scene

func _merge_extras(scene : Node) -> void:
	var verbose_output = []
	var nodes : Array[Node] = scene.find_children("*" + "_meta", "Node")
	verbose_output.append_array(["Metadata nodes:",  nodes])
	for node in nodes:
		var extras = node.get_meta("extras")
		if !extras:
			verbose_output.append("Node %s contains no 'extras' metadata" % node)
			continue
		var parent = node.get_parent()
		if !parent:
			verbose_output.append("Node %s has no parent" % node)
			continue
		var idx_original = node.get_index() - 1
		if idx_original < 0 or parent.get_child_count() <= idx_original:
			verbose_output.append("Original node index %s is out of bounds. Parent child count: %s" % [idx_original, parent.get_child_count()])
			continue
		var original = node.get_parent().get_child(idx_original)
		if original:
			verbose_output.append("Setting extras metadata for %s" % original)
			original.set_meta("extras", extras)
			if node.has_meta("mesh_extras"):
				if original is MeshInstance3D and original.mesh:
					verbose_output.append("Setting extras metadata for mesh %s" % original.mesh)
					original.mesh.set_meta("extras", node.get_meta("mesh_extras"))
				else:
					verbose_output.append("Metadata node %s has 'mesh_extras' but original %s has no mesh, preserving as 'mesh_extras'" % [node, original])
					original.set_meta("mesh_extras", node.get_meta("mesh_extras"))
		else:
			verbose_output.append("Original node not found for %s" % node)
		node.queue_free()
	
	if verbose:
		for item in verbose_output:
			print(item)

Here's the sample project I uploaded in my earlier comment updated to reflect these changes:

GLTF Extras [working].zip

It'd still be much easier for the average user if this worked out of the box

@CarpenterBlue
Copy link
Author

Damn....
that requires A LOT of setup for something that seems so basic.

Blender importer setting confusingly appear to support the feature but don't

If you're working with Blender and drop the .blend files directly into Godot the import settings show a checkbox for "custom properties", which blender exports as extras. This made me believe I could find that data somewhere in the imported scene but I was wrong. The imported tells blender to export the custom properties as gltf extras, but does nothing with it.

This seems like there was an intent to support it then. Maybe open up an issue?

@filonli
Copy link

filonli commented Nov 11, 2023

I have the same problem, and i think its wery important to ad this as soon as posible, because godot lacks level editing tools for 3d(3d tilemaps not always that you need). Yes there options like qodot plugin, but with trenchbroom(qodot plugin adds support of that level editor) your artstyle very limited. So with metadata import support its way to make posible to use the Blender as level editor. With blender will be posible to create any (typical) level with any art style you can imagine, with hight control of optimization.

Source has hammer. Let godot will be with Blender :)

@Calinou
Copy link
Member

Calinou commented Nov 12, 2023

The proposal seems self-contained and a logical extension of what the importer is already doing, so it makes sense to me.

Yes there options like qodot plugin, but with trenchbroom(qodot plugin adds support of that level editor) your artstyle very limited.

It's possible to use TrenchBroom to create realistic-looking 3D scenes by using brushes for basic level geometry, and (a lot of) meshes on top of that. This is how maps were designed in the late Source 1 era with Hammer, such as recent CS:GO maps.

Of course, this approach compromises on iteration speed, but so does choosing a realistic art direction in the first place.

Source has hammer. Let godot will be with Blender :)

I don't think this comparison is apt, because Hammer is much closer to TrenchBroom than a traditional 3D modeler like Blender. Even though Source 2 Hammer works with meshes and not BSPs, it still imposes a convex requirement on brushes you create – just like TrenchBroom.

Blender is also notoriously obtuse when it comes to level design (in terms of iteration speed). It's one of the areas of Blender that have evolved the least since the early days, since level design is relatively niche in the Blender community and most studios stick to Maya, Modo or even SketchUp for this. But I digress 🙂

@CarpenterBlue
Copy link
Author

CarpenterBlue commented Nov 12, 2023

I feel like throwing Trenchbroom into the mix is not really viable. You can't possibly expect me to be constantly switching between Trenchbroom, Godot and Blender to iterate on a simple level when the three are not compatible with each other in the slightest. Having the custom properties be translated to metadata would alleviate many pains...

@demolke
Copy link

demolke commented Dec 15, 2023

I believe I've implemented most of this in godotengine/godot#86183

To elaborate on why this should be part of the engine - W4 Demo team said so :) : https://youtu.be/9kcjFJJxO6I?feature=shared&t=1014

I found myself in similar position - needing to build a pipeline to Godot that would allow for importing additional data about assets, so that artists can tag things directly in their software.

@lyuma
Copy link

lyuma commented Jan 24, 2024

@fire reminded me about this proposal and I want to drop a comment since I haven't done a good job of communicating my thoughts.

I would like to pose a question if it might be better to use a custom game-specific GLTFDocumentExtension to parse out the extras data needed for that game? GLTFDocumentExtension was brought up earlier in this thread, but were rejected here for two reasons. I'll reply to both reasons and ask to reconsider GLTFDocumentExtension if we resolve both of these issues in the engine.

a) _import_node() only handles what will become Godot nodes, so it won't parse things like meshes or materials. That means at least with that method override it's impossible to set extra properties as resource metadata.

We'd welcome any additions to this API that would make this type of processing easier. That said, operations without a dedicated function can always be done in _import_post.

b) With the code listed above you'll notice lights and cameras have metadata set properly but it's misteriously missing from MeshInstance3D nodes, probable the main use case for the feature. The problem is that _import_node() doesn't receive a MeshInstance3D in it's node param but an ImporterMeshInstance3D. Those are later converted by an engine-provided document extension.

I think this is a Godot bug and we should fix it. I have a 17-line workaround script for the ImporterMeshInstance3D attributes, but I'm not sure this approach works for the ImporterMesh.


The features we include mainline in the engine ultimately guide users how to use assets, so we need to be careful how we build this type of functionality.

I don't want to make it sound like I'm rejecting this proposal. And there's two open PRs which implement this for some of the node types.... I'm not doing a technical review of those because in order to review them I need to better understand the problem we are trying to solve.

I still think document extensions might be a better fit than adding all extras directly to node and/or resource meta. I fear this proposal may encourage users to become dependent unstructured data in extras and meta, which I think is not a resilient design practice. All I'm asking is to reconsider if GLTFDocumentExtension would satisfy the original usecases: things such as "StepSound" : "grass" (IMHO, sound materials really should be made into an official GLTF extension and engine feature but that's another subject for another day)

Finally, since I think this is important, I would like to invite you all to discuss this at the next asset-pipeline meeting. I'm hoping we'll have one late next week, but I'm not the one who organizes them so I can't promise. Stay tuned in #asset-pipeline in godot contributors chat, and I'll try to remember to ping here if we have a meeting time.

@noidexe
Copy link

noidexe commented Jan 24, 2024

I would like to pose a question if it might be better to use a custom game-specific GLTFDocumentExtension to parse out the extras data needed for that game? GLTFDocumentExtension was brought up earlier in this thread, but were rejected here for two reasons. I'll reply to both reasons and ask to reconsider GLTFDocumentExtension if we resolve both of these issues in the engine.

I would answer no. My workaround took a lot of work and involved a lot of reading the engine source code. All that to only have gltf extras appear as node metadata. I still have to write all the project specific logic.
Even if that process were easier here's what the UX would look like to the user based on what I went through:

  • Users exports file as gltf using custom properties
  • User tries to find that data somewhere inside Godot after importing the file
  • User wonders if the export or import processes are bugged or the feature is not supported
  • User checks their DCC tool to make sure the data is exported
  • User opens the GLTF file to make sure the data is there
  • User tries to find options in import setings, project settings, or search the docs
  • User decides to search on issues in case it is a bug
  • User decides to seach on proposals in case it is a missing feature
  • User finds this thread an realizes they needs to build the feature by themselves
  • User reads all the (hopefully by then more complete) documentation on GLTFDocumentExtension
  • User manages to code the extension to somehow expose that data to Godot

You are pushing all those steps to a user who still has to design and write the project specific logic that will do something useful with the gltf extras. And even though I wrote it as a sequence of steps its actually a funnel where a percentage of users just give up at every step.

Node metadata in godot and node extras in GLTF are basically the same data structure for the same purpose: having a key-value store of custom data. On top of that discoverability is pretty high. It's easy to notice the metadata in the inspector, and otherwise at least for me it'd be the first place I'd look. You don't need to send users to RTFM because the feature is right there in the most obvious place. The docs mention over and over that one of the main focus of the engine is usability so I think it aligns with that.

I fear this proposal may encourage users to become dependent unstructured data in extras and meta, which I think is not a resilient design practice.

Users can choose to use it or not. Just in the same way they can use get_node("../../.."), or use groups to have a global ref to a single node, or have singletons everywhere, . The main reason I started using Godot was that it didn't force me into someone's notion of good practices. I can choose to be quick and dirty or slow and clean and what fundamentally what makes Godot stand out compared to every other alternative is not only that I can do both, but that it provides a smooth path from one to the other.
Flexibility is invaluable for gameplay programmers, which deal with constantly changing requirements.

Finally, since I think this is important, I would like to invite you all to discuss this at the next asset-pipeline meeting. I'm hoping we'll have one late next week, but I'm not the one who organizes them so I can't promise. Stay tuned in #asset-pipeline in godot contributors chat, and I'll try to remember to ping here if we have a meeting time.

I'll be happy to be there if my feedback can be of any use.

@demolke
Copy link

demolke commented Jan 25, 2024

Looking at it from the other side - GLTF export should put Godot meta into GLTF node extra without any addons/extensions/customization.

And that I think subsequently informs the decision for import - if we're exporting it, we should also be symmetrically importing it. godotengine/godot#86183 does both export and import.

@vadimmos
Copy link

vadimmos commented Jan 25, 2024

Why not, in addition to reading hints from the names of the meshes (-col, -colonly, -convcolonly, etc.), check the values in the metadata.
image
I think, and I think many developers will agree with me, importing the metadata of 3d models and processing them in the engine out of the box would make life easier, and development much faster and more convenient.

@nklbdev
Copy link

nklbdev commented Apr 12, 2024

I also want to vote that Blender Custom Properties should be imported as node's metadata. This is the most obvious option. In addition, their structure is completely consistent with each other.
If the user needs some post-processing of this data, he can write a post-import script in which he will apply these values to the properties he needs. Or completely change the import strategy for this node.

@jamie-pate
Copy link

jamie-pate commented May 13, 2024

We'd welcome any additions to this API that would make this type of processing easier. That said, operations without a dedicated function can always be done in _import_post.

It seems like you still need a custom import script after the _import_post since the _import_post has no way to attach metadata to the created node?

If you look at the workaround script there's a bunch of kludges in there to make the data available later by creating duplicate nodes etc.

The shortest point between where we are and 'this is usable' to me is to add metadata to importerMeshInstance3D and have it propagate to MeshInstance3D/StaticBody3D automatically, or add another hook we can use to _post_post_import the scene similar to EditorScenePostImport inside the GLTFDocumentExtension framework.

@gomes42
Copy link

gomes42 commented Jun 13, 2024

Working on something to copy metadata or automatic apply Godot properties (Experimental)

https://github.com/lgxm3z/godot-gltf-extras

@bikemurt
Copy link

bikemurt commented Jun 19, 2024

Chiming in here - I have been working on a customizable pipeline product for over half a year (apologies that it is paid, I have sunk many many hours into developing, so trying to recoup somewhat) that relies on the applying an import script, so this issue is near and dear to my heart.

Here's the Blender addon, FYI: https://blendermarket.com/products/blender-godot-pipeline-addon

The TLDR is that the addon sets a lot of "custom" properties in Blender, which of course get tagged with the nodes and meshes in the GLTF file export as extras. On the Godot side of things, I parse out the extras to do custom functions for the purposes of an import pipeline.

Here's my solution on the Godot side of things, for parsing out the Blender custom data (GLTF extras) and saving the data as metas on nodes:
https://github.com/bikemurt/blender-godot-pipeline/blob/main/addons/blender_godot_pipeline/GLTFImporter.gd

This works great, except for the fact that the custom data is dropped entirely when .blend file importing is used.

Let me provide a real concrete example.

I have a cube, which I want to apply an StaticBody3D (say, collision layer 2), a collision shape and a mesh to. This is all set up on the Blender side, and we can see the "custom" instructions get applied:

image

This data in particular translates to the following extra data that gets tagged with the GLTF node:

	"nodes":[
		{
			"extras":{
				"collision":"box",
				"size_x":"2.0",
				"size_z":"2.0",
				"size_y":"2.0",
				"prop_file":"res://assets/addon_test/param_file.txt"
			},
			"mesh":0,
			"name":"Cube"
		}
	],

When attaching my import script (GLTFImporter.gd mentioned above), the whole pipeline works perfectly, I get my custom functions (bodies and collisions in this case) as desired:

image

Doing the exact same process with a .blend file, we can see that it does not generate any collisions or bodies, which is clear evidence that none of the extras were retained from the GLTF export, as mentioned above:

image

Another edit: I reviewed everything again, and I think my comment here stands. If GLTF custom properties were automatically brought in as meta data, then that would solve this issue. My issue specifically has to do with get_source_file() on EditorScenePostImport referencing the .blend file and not the generated .gltf file. Since it only references in the .blend, there's no way to parse the actual .gltf file when .blend importing is done.

@bikemurt
Copy link

As an update to this thread, I found a workaround, so I'm posting it here if it helps others.

It may not be completely risk free, since it requires accessing the godot/imported or .godot/imported folder to access the .gltf file that's generated from the .blend file.

But effectively, this allows parsing of the extras in either a .gltf or .blend import:

@tool
extends EditorScenePostImport

func _post_import(scene):
	var source := get_source_file()
	
	if ".blend" in source:
		var use_hidden: bool = ProjectSettings.get_setting("application/config/use_hidden_project_data_directory")
		
		var data_folder := "godot"
		if use_hidden: data_folder = ".godot"
		
		var imported_file := "res://"+data_folder+"/imported/" + source.get_file().replace(".blend", "") + "-" + source.md5_text() + ".gltf"
		source = imported_file

	var file = FileAccess.open(source, FileAccess.READ)
	var content = file.get_as_text()
	
	var json = JSON.new()
	var error = json.parse(content)
	if error == OK:
		find_extras(json.data)
		
	return scene

func find_extras(json):
	if "nodes" in json:
		for node in json["nodes"]:
			if "mesh" in node:
				var mesh_index = node["mesh"]
				var mesh = json["meshes"][mesh_index]
				if "extras" in mesh:
					print(mesh)
				
			if "extras" in node:
				print(node)

@aaronfranke
Copy link
Member

The implementation of this proposal is already approved and will be available in Godot 4.4 and later: godotengine/godot#86183

@lyuma (IMHO, sound materials really should be made into an official GLTF extension and engine feature but that's another subject for another day)

We should totally have this! If anyone is interested in collaborating on this, please contact me and get involved with OMI.

@vadimmos I think, and I think many developers will agree with me, importing the metadata of 3d models and processing them in the engine out of the box would make life easier, and development much faster and more convenient.

This is what OMI_physics_body is for. The physics options can be explicitly specified in the 3D model, and then Godot will import those as physics objects. Anyone is welcome and encouraged to make a Blender implementation of this.

Note that the critical difference between OMI_physics_body and extras is that OMI_physics_body is structured while extras is unstructured. OMI_physics_body defines which properties are allowed, what values they can have, etc. How does Godot know what to do with body_type? What if somebody defines an extra called foobar, what does Godot do with it, or what does any other engine do with it? With OMI_physics_body, it explicitly defines which properties are allowed, so for example, { "motion": { "type": "dynamic" } } defines a body with dynamic motion.

Once there is a Blender implementation, this will all be much easier with far less magic. Users won't need to memorize a special "body_type" key, they would open a menu for physics options and select "Dynamic" from a dropdown. As a bonus, structuring the data as a standard allows coordination with other game engines, so the same models designed for Godot will work with Unity/Unreal, and vice versa.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.