@tool extends RefCounted # Validator: Texture Size # # Checks: # - All textures referenced by materials in the scene are ≤ 1024×1024 # - Checks StandardMaterial3D, ORMMaterial3D, and ShaderMaterial textures # - Warns on textures > 512×512 (suggested for secondary surfaces) # - Skips LightmapGI textures (baked lightmaps are handled separately) const MAX_TEXTURE_SIZE := 1024 const WARN_TEXTURE_SIZE := 512 func validate(scene_root: Node, scene_path: String) -> Dictionary: var errors: Array[String] = [] var warnings: Array[String] = [] var inspected := 0 var oversized: Array[String] = [] # Collect all MeshInstance3D nodes and their materials var mesh_instances: Array[MeshInstance3D] = [] _collect_mesh_instances(scene_root, mesh_instances) # Track visited textures to avoid duplicate reports var visited_textures: Dictionary = {} for mi in mesh_instances: var mesh: Mesh = mi.mesh if mesh == null: continue for surf_idx in mesh.get_surface_count(): var mat := mesh.surface_get_material(surf_idx) if mat == null: # Check override material on MeshInstance3D mat = mi.get_surface_override_material(surf_idx) if mat == null: # Check material_override on MeshInstance3D mat = mi.material_override if mat == null: continue _check_material_textures(mat, visited_textures, errors, warnings, inspected) # Check material_override if no per-surface materials if mesh_instances.is_empty(): _find_materials_on_node(scene_root, visited_textures, errors, warnings, inspected) # Check LightmapGI light_data texture sizes separately var lightmaps: Array[LightmapGI] = [] _find_nodes_by_type(scene_root, "LightmapGI", lightmaps) for lm in lightmaps: if lm.light_data != null: var lightmap_tex = lm.light_data.get_texture() if lightmap_tex != null and not visited_textures.has(lightmap_tex.resource_path): visited_textures[lightmap_tex.resource_path] = true var size := _get_texture_size(lightmap_tex) if size > 0: print(" Lightmap texture: ", lightmap_tex.resource_path, " — ", size, "×") if size > MAX_TEXTURE_SIZE: warnings.append("Lightmap texture exceeds %d: %s (%d×)" % [MAX_TEXTURE_SIZE, lightmap_tex.resource_path, size]) print("") print(" Textures inspected: ", inspected) if oversized.is_empty(): print(" ✓ All textures within ", MAX_TEXTURE_SIZE, "×", MAX_TEXTURE_SIZE, " limit") else: for o in oversized: print(" ✖ ", o) return { "pass": errors.is_empty(), "errors": errors, "warnings": warnings, } func _check_material_textures(mat: Material, visited: Dictionary, errors: Array, warnings: Array, inspected: int) -> void: if mat is StandardMaterial3D: var smat := mat as StandardMaterial3D _check_single_texture(smat.albedo_texture, "albedo", smat.resource_path, visited, errors, warnings, inspected) _check_single_texture(smat.normal_texture, "normal", smat.resource_path, visited, errors, warnings, inspected) _check_single_texture(smat.roughness_texture, "roughness", smat.resource_path, visited, errors, warnings, inspected) _check_single_texture(smat.metallic_texture, "metallic", smat.resource_path, visited, errors, warnings, inspected) _check_single_texture(smat.emission_texture, "emission", smat.resource_path, visited, errors, warnings, inspected) _check_single_texture(smat.ao_texture, "ambient_occlusion", smat.resource_path, visited, errors, warnings, inspected) elif mat is ORMMaterial3D: var orm := mat as ORMMaterial3D _check_single_texture(orm.albedo_texture, "albedo", orm.resource_path, visited, errors, warnings, inspected) _check_single_texture(orm.normal_texture, "normal", orm.resource_path, visited, errors, warnings, inspected) _check_single_texture(orm.orm_texture, "ORM", orm.resource_path, visited, errors, warnings, inspected) _check_single_texture(orm.emission_texture, "emission", orm.resource_path, visited, errors, warnings, inspected) elif mat is ShaderMaterial: var sm := mat as ShaderMaterial # ShaderMaterial textures are set via shader params — check all Texture2D params var param_list = sm.get_shader_parameter_list() if sm.shader else [] for param in param_list: var val = sm.get_shader_parameter(param) if val is Texture2D: _check_single_texture(val as Texture2D, param, sm.resource_path, visited, errors, warnings, inspected) func _check_single_texture(tex: Texture2D, slot_name: String, material_path: String, visited: Dictionary, errors: Array, warnings: Array, inspected: int) -> void: if tex == null: return var path := tex.resource_path if path.is_empty(): path = tex.name # built-in/placeholder texture if visited.has(path): return visited[path] = true inspected += 1 var size := _get_texture_size(tex) if size <= 0: return var label := "%s/%s" % [material_path.get_file(), slot_name] if size > MAX_TEXTURE_SIZE: errors.append("Texture exceeds %d×%d: %s (%s) — %d× (path: %s)" % [MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE, label, path, size, path]) elif size > WARN_TEXTURE_SIZE: warnings.append("Texture > %d×%d: %s (%s) — %d× (consider downscaling)" % [WARN_TEXTURE_SIZE, WARN_TEXTURE_SIZE, label, path, size]) func _get_texture_size(tex: Texture2D) -> int: # Return the larger dimension return max(tex.get_width(), tex.get_height()) func _collect_mesh_instances(node: Node, result: Array) -> void: if node is MeshInstance3D: result.append(node) for child in node.get_children(): _collect_mesh_instances(child, result) func _find_nodes_by_type(root: Node, expected_type: String, output) -> void: if root.get_class() == expected_type: output.append(root) for child in root.get_children(): _find_nodes_by_type(child, expected_type, output) func _find_materials_on_node(node: Node, visited: Dictionary, errors: Array, warnings: Array, inspected: int) -> void: # Fallback: walk all nodes and check their material_override for child in node.get_children(): if child.has_method("get_material_override"): var mat = child.get("material_override") if mat != null and mat is Material: _check_material_textures(mat, visited, errors, warnings, inspected) _find_materials_on_node(child, visited, errors, warnings, inspected)