## Converts raw WMOLoader data into a Godot Node3D scene tree. ## Usage: ## var data = WMOLoader.new().load_wmo(abs_path) ## var node = WMOBuilder.build(data, "res://extracted") ## add_child(node) class_name WMOBuilder static var _texture_cache: Dictionary = {} # Returns a Node3D containing one MeshInstance3D per WMO group. static func build(data: Dictionary, extracted_dir: String = "") -> Node3D: var root := Node3D.new() root.name = "WMO" if not data.has("groups"): return root var textures: PackedStringArray = data.get("textures", PackedStringArray()) var materials: Array = data.get("materials", []) # Build Godot materials from WMO material definitions var godot_materials: Array[StandardMaterial3D] = [] for mat_def in materials: godot_materials.append(_build_material(mat_def, textures, extracted_dir)) # Build one MeshInstance3D per group var groups: Array = data.get("groups", []) for gi in groups.size(): var g: Dictionary = groups[gi] if g.is_empty(): continue var mesh_inst := _build_group_mesh(g, godot_materials) mesh_inst.name = "Group_%d" % gi root.add_child(mesh_inst) return root static func _build_group_mesh( g: Dictionary, godot_mats: Array[StandardMaterial3D]) -> MeshInstance3D: var verts: PackedVector3Array = g.get("vertices", PackedVector3Array()) var normals: PackedVector3Array = g.get("normals", PackedVector3Array()) var uvs: PackedVector2Array = g.get("uvs", PackedVector2Array()) var colors: PackedColorArray = g.get("colors", PackedColorArray()) var indices: PackedInt32Array = g.get("indices", PackedInt32Array()) var batches: Array = g.get("batches", []) var mesh := ArrayMesh.new() if verts.is_empty() or indices.is_empty(): var empty := MeshInstance3D.new() empty.mesh = mesh return empty if batches.is_empty(): batches = [{ "index_start": 0, "index_count": indices.size(), "material_id": -1, }] for batch_variant in batches: if not (batch_variant is Dictionary): continue var batch: Dictionary = batch_variant var idx_start: int = int(batch.get("index_start", 0)) var idx_count: int = int(batch.get("index_count", 0)) var mat_id: int = int(batch.get("material_id", -1)) if idx_count <= 0 or idx_start >= indices.size(): continue var batch_indices := PackedInt32Array() for i in range(idx_start, mini(idx_start + idx_count, indices.size()), 3): if i + 2 >= indices.size(): break # Reverse winding for the current local basis conversion. batch_indices.append(indices[i + 0]) batch_indices.append(indices[i + 2]) batch_indices.append(indices[i + 1]) if batch_indices.is_empty(): continue var arrays := [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = verts if normals.size() == verts.size(): arrays[Mesh.ARRAY_NORMAL] = normals if uvs.size() == verts.size(): arrays[Mesh.ARRAY_TEX_UV] = uvs if colors.size() == verts.size(): arrays[Mesh.ARRAY_COLOR] = colors arrays[Mesh.ARRAY_INDEX] = batch_indices mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) var surf_idx := mesh.get_surface_count() - 1 if mat_id >= 0 and mat_id < godot_mats.size(): mesh.surface_set_material(surf_idx, godot_mats[mat_id]) var mi := MeshInstance3D.new() mi.mesh = mesh return mi static func _build_material( mat_def: Dictionary, textures: PackedStringArray, extracted_dir: String = "") -> StandardMaterial3D: var mat := StandardMaterial3D.new() mat.cull_mode = BaseMaterial3D.CULL_DISABLED mat.roughness = 0.85 mat.metallic = 0.0 mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.specular_mode = BaseMaterial3D.SPECULAR_DISABLED mat.vertex_color_use_as_albedo = false var blend_mode: int = mat_def.get("blend_mode", 0) match blend_mode: 0: mat.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED 1: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR _: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA # Texture is loaded later by the scene assembler (needs BLP→Image conversion) # Store the path as metadata for the assembler to pick up var tex0_id: int = mat_def.get("texture0", -1) if tex0_id >= 0 and tex0_id < textures.size(): var tex_path: String = str(textures[tex0_id]).replace("\\", "/") mat.set_meta("texture0_path", tex_path) var tex := _load_texture(tex_path, extracted_dir) if tex: mat.albedo_texture = tex mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC mat.set_meta("wow_flags", mat_def.get("flags", 0)) mat.set_meta("wow_shader", mat_def.get("shader", 0)) return mat static func _load_texture(rel_path: String, extracted_dir: String) -> Texture2D: if rel_path.is_empty() or extracted_dir.is_empty(): return null var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(rel_path.replace("\\", "/"))) if _texture_cache.has(abs_path): return _texture_cache[abs_path] if not ClassDB.class_exists("BLPLoader"): return null var loader = ClassDB.instantiate("BLPLoader") if loader == null: return null var img: Image = loader.call("load_image", abs_path) if img == null or img.is_empty(): _texture_cache[abs_path] = null return null img.generate_mipmaps() var tex := ImageTexture.create_from_image(img) _texture_cache[abs_path] = tex return tex