Textures not applying in ShaderMaterial in the automated material/scene creation script

Godot Version

Godot 4.3

Question

I have a Quixel asset pack taken from Fab. There’s quite a lot of assets and manually creating each material and scene containing the mesh would be too time-consuming, so I decided to make an editor script that will automate the process. With the given input folder(s) it will process each asset folder, look for textures and FBX model and create material and scene with the mesh, applying a material to that mesh.

The script mostly works, except for one issue: I’m trying to set material textures to the textures in folder and apparently set_shader_parameter doesn’t seem to do anything to the texture. (or the texture I set isn’t being saved in material file?)

Here’s my editor script:

@tool
extends EditorScript

# Define texture patterns for detection
const TEXTURE_PATTERNS = {
	"texture_ao": ["ao", "ambientocclusion"],
	"texture_basecolor": ["basecolor", "albedo"],
	"texture_bump": ["bump"],
	"texture_cavity": ["cavity"],
	"texture_height": ["displacement", "height"],
	"texture_gloss": ["gloss"],
	"texture_normal": ["normal"],
	"texture_roughness": ["roughness"],
	"texture_specular": ["specular"],
	"texture_opacity": ["opacity", "transparency"]
}

const PICTURE_FORMATS = ["png", "jpg", "jpeg"]
const MODEL_FORMATS = ["fbx"]

const REWRITE = true

const FOLDER_PATHS = ["res://assets/junkyard"]#, "res://assets/warehouse", "res://assets/slate_quarry"]

# Path to the custom shader
const CUSTOM_SHADER_PATH = "res://assets/shaders/SurfaceMaterial.gdshader"

func _run():
	print("Auto creating materials and scenes...")

	# Process each subfolder in the selected directory
	for folder_path in FOLDER_PATHS:
		var dir = DirAccess.open(folder_path)
		if dir:
			for subfolder_name in dir.get_directories():
				var subfolder_path = folder_path + "/" + subfolder_name
				await process_subfolder(subfolder_path)

	print("Materials and scenes auto created.")

# Process each subfolder and generate materials and scenes
func process_subfolder(subfolder_path):
	print("Processing subfolder " + subfolder_path)
	var texture_paths = {}
	var fbx_path = ""

	# Open the directory
	var dir = DirAccess.open(subfolder_path)
	if dir:
		for file_name in dir.get_files():
			var file_path = subfolder_path + "/" + file_name
			var lower_name = file_name.to_lower()

			# Check for model format (FBX)
			if fbx_path == "" and MODEL_FORMATS.has(lower_name.get_extension()):
				fbx_path = file_path
				continue # Skip further checks for this file

			# Check for picture formats
			if PICTURE_FORMATS.has(lower_name.get_extension()):
				# Match each texture pattern to identify its type
				for param_name in TEXTURE_PATTERNS.keys():
					for pattern in TEXTURE_PATTERNS[param_name]:
						if lower_name.find(pattern) != -1:
							texture_paths[param_name] = file_path
							break

	# Create a material if base color or other textures are found
	var material_path = ""
	if "texture_basecolor" in texture_paths:
		print(texture_paths)
		var material = create_material(texture_paths)
		print("Creating material")
		material_path = subfolder_path + "/GeneratedMaterial.tres"
		
		if FileAccess.file_exists(material_path) and REWRITE:
			dir.remove(material_path)
		ResourceSaver.save(material, material_path)

	# Create a scene if an FBX file is found
	if fbx_path != "":
		print("Creating a scene")
		create_scene_with_fbx(fbx_path, material_path)

# Function to create a material with optional textures
func create_material(texture_paths):
	var shader: Shader = load(CUSTOM_SHADER_PATH) as Shader
	if shader == null:
		push_error("Failed to load custom shader.")
		return null	

	var material: ShaderMaterial = ShaderMaterial.new()
	material.shader = shader

	# Assign textures to shader parameters based on detected paths
	for param_name in TEXTURE_PATTERNS.keys():
		if param_name in texture_paths:
			material.set_shader_parameter(param_name, load(texture_paths[param_name]) as Texture2D)

	return material

# Function to create a scene for an FBX file with the given material
func create_scene_with_fbx(fbx_path, material_path):
	var scene_root = StaticBody3D.new()
	var mesh_instance = MeshInstance3D.new()
	
	# Load the FBX file as a scene
	var fbx_scene = load(fbx_path)
	if fbx_scene == null:
		push_error("Failed to load FBX file: " + fbx_path)
		return
	
	# Instantiate the FBX scene and search for a MeshInstance3D node
	if fbx_scene is PackedScene:
		var fbx_root = fbx_scene.instantiate()
		var original_mesh_instance = find_mesh_instance(fbx_root)
		if original_mesh_instance:
			mesh_instance.mesh = original_mesh_instance.mesh
	
	# Load the generated material and assign it to the MeshInstance3D
	if material_path != "" and FileAccess.file_exists(material_path):
		var material = load(material_path)
		if material is Material:
			mesh_instance.material_override = material
	
	scene_root.add_child(mesh_instance)
	mesh_instance.owner = scene_root

	# Create a new scene with the MeshInstance3D as the root node
	var scene = PackedScene.new()
	scene.pack(scene_root)

	# Save the new scene
	var output_scene_path = fbx_path.get_base_dir() + "/GeneratedScene.tscn"
	ResourceSaver.save(scene, output_scene_path)

# Recursive function to find the first MeshInstance3D node in the hierarchy
func find_mesh_instance(node):
	if node is MeshInstance3D:
		return node
	for child in node.get_children():
		var result = find_mesh_instance(child)
		if result:
			return result
	return null

My custom shader contains and uses all the uniforms for the textures, if I set textures manually they are applied as expected.

don’t know about this. assets I’ve downloaded needed editing to work in engine. things like packing the textures together for better performance, applying scale and removing duplicated vertices.

welcome to game dev, it’s not always fun.

you are loading images as texture2D. I think you need to load as image and then convert to texture2D so it is imported.

Note: Files have to be imported into the engine first to load them using this function. If you want to load Images at run-time, you may use Image.load. If you want to import audio files, you can use the snippet described in AudioStreamMP3.data.

but the whole automation of this process is a bad idea. you will get very bad performance this way and get lots of glitches and problems.

you have to open your assets in blender, apply scale, make sure everything is correct, and then export as GLB, godot’s format.
it is also a good idea to do what I’m doing, take a bunch of textures, pack them into atlases, and then adjust the UVs on each individual mesh in blender, you can set the UV view from median-point to cursor2D and scale to 0.5, and again if smaller, then translate.
if there are 16 objects with 512x512 textures, pack them into a single 2048x2048 texture. sharing a material means all the objecs are rendered in the same pass and that leads to better performance. this depends on your PC, but 4K is an average good size for atlases, bigger is better if your comp can handle it.
also resize textures of objects that are too small. these asset packs are very generic. for an FPS you would want high quality small objects, for an open world or third person view, a bottle should NEVER have a 4K texture, it is a waste of resources and space.
these premade assets are made very general, because they don’t know what your game is. sometimes there will be a single fire extinguisher or a cardboard box, and have the biggest texture possible. you have to adapt assets to your game, they are a help, not an instant solution.

when using quality assets, and SPECIALLY when using SO MANY OF THEM, you want them to be as optimized as possible, because you can’t do this step later, it would mean manually replacing each mesh in each meshinstance3D in every scene, and also materials.

1 Like