Extracting and applying materials in the Godot engine

I have been doing a fair amount of development with the Godot engine. I have gone so far as to make 3D stuff with it. In my pursuit of making a 3D game, I have found various assets to use in the engine. Usually these come from Open Game Art (which is run by a guy who deserves your money), with their wide variety of art assets. I have found some nice 3D art assets and have tried using them in the Godot engine. While most art assets easily import into the engine, some do not. For example, here’s a picture of a barrel from the modular dungeon set 2 from Fertile Soil Productions (also run by a guy who deserves your money).

Drab barrel

Note how dark it is. This is because the “Metallic” property of the 3D model was too high. For some odd reason, Godot can’t seem to set this correctly (or the property is quite strange to begin with). Since the engine doesn’t allow editing and saving the materials already attached to the model, I had to manually extract the material from the object and modify the property. Then I had to manually assign it to the model. This is how it’s suppose to look without “Metallic” maxed out.

Better barrel

For a few models, manually adding the materials is fine. But for the 90 or so items included with the set, it would take a while just to extract them manually. That’s why I came up with a way to extract all of the materials from the models in the set and put them in their own place. What I first did was add all of the models to a scene.

List of nodes

Then I attached a tool script to the root node which will copy all of those materials into a special directory.

extends Node

# Script to save all of the materials off certain groups of "*.obj" files (e.g. Fertile Soil Production's modular meshes). Note that this script (on the scene's load) will go through *every* mesh in the scene, extract the material, and save it to a directory, so this may slow down the editor.

# This is for Fertile Soil Production's "Modular Dungeon 2" set. Change this as necessary.
var materials_dir: String = "res://assets/3D/Dungeon Set 2/Materials"

func _ready():
    # For checking the existence of a file.
    var file: File = File.new()
    # We're inside the editor.
    if Engine.editor_hint:
        for node in get_children():
            if node is MeshInstance:
                # Cycle through the surfaces on the mesh and get the material associated with that surface.
                for material_number in node.get_mesh().get_surface_count():
                    var material: Material = node.get_mesh().surface_get_material( material_number )
                    # We break for no materials.
                    if not material: break
                    var material_name: String = material.get_name()
                    # Skip materials that are already there.
                    if not file.file_exists(materials_dir + material_name + ".tres"):
                        # Combine the name of the resource with the resource directory and save the material to that directory.
                        var number: int = ResourceSaver.save( materials_dir + material_name + ".tres", material )
                        if number != OK:
                            push_error("Couldn't save the materials.")

What this script does is it checks each child node for a MeshInstance and looks through the node’s surfaces to find materials. If it finds some materials, it tries to save them to a specified directory. After that I look at the saved materials and edit them as necessary.

That’s all nice and good. But what happens when I have to apply them to all of the models in the set? That’s why I created another script to apply the materials.

extends Node

# This is for adding the materials to each of the objects from the Modular Dungeon 2 pack so as to make a meshlib out of them.

# All of the materials.
var blue_material: SpatialMaterial = preload("res://assets/3D/Dungeon Set 2/Materials/blue material.tres")
# ...More materials go here.

func _ready():
    if Engine.editor_hint:
        for node in get_children():
            if node is MeshInstance:
                for material_number in node.get_mesh().get_surface_count():
                    var material: Material = node.get_mesh().surface_get_material( material_number )
                    if not material:
                        push_error("Material isn't there!")
                    # Assign the matching materials to each mesh.
                    match material.get_name():
                            node.set_surface_material( material_number, blue_material )
                        # ...More matches go here.
                            push_error( "Material not found: " + material.get_name() )

This, again, goes through each MeshInstance and applies the modified materiel to the model. This way, I don’t have to manually add all of the materails to the meshes.

There are some drawbacks to this method. Since it goes through each MeshInstance, it may take a while to get all of the materials from the models. Also, this is best used for models that have a few surfaces. When you have a 3D model with hundreds of surfaces, it’s not very practical. Also you still have to edit the materials however you want them (in my case, it was the “Metallic” property). Still, this is nice to use when you’re prepping a “meshlib” to be used for a GridMap.

If you want, try out the scripts. They may save you some time.

Jason Anderson

Jason Anderson has been hacking up computers for nearly 20 years and has been using Linux for over 15 years. Among that, he has a BBA in Accounting. Look him up on Twitter at @FakeJasonA and on Mastodon on @ertain@mast.linuxgamecast.com

Leave a Reply

Your email address will not be published. Required fields are marked *