## Converts raw M2Loader data into a Godot Node3D scene tree. ## Usage: ## var data = M2Loader.new().load_m2(abs_path) ## var node = M2Builder.build(data, "res://data/extracted") ## add_child(node) class_name M2Builder static var _texture_cache: Dictionary = {} static func build(data: Dictionary, extracted_dir: String = "") -> Node3D: var root := Node3D.new() root.name = "M2" var verts: PackedVector3Array = data.get("vertices", PackedVector3Array()) var normals: PackedVector3Array = data.get("normals", PackedVector3Array()) var uvs: PackedVector2Array = data.get("uvs", PackedVector2Array()) var indices: PackedInt32Array = data.get("indices", PackedInt32Array()) var batches: Array = data.get("batches", []) if verts.is_empty() or indices.is_empty() or batches.is_empty(): return root var textures: PackedStringArray = data.get("textures", PackedStringArray()) var texture_types: PackedInt32Array = data.get("texture_types", PackedInt32Array()) var texture_flags: PackedInt32Array = data.get("texture_flags", PackedInt32Array()) var materials: Array = data.get("materials", []) var tex_combos: PackedInt32Array = data.get("texture_combos", PackedInt32Array()) var model_path: String = str(data.get("model_path", "")) var mesh := ArrayMesh.new() 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)) var tc_idx: int = int(batch.get("texture_combo_index", -1)) if idx_count <= 0 or idx_start + idx_count > indices.size(): continue var batch_indices := indices.slice(idx_start, idx_start + idx_count) 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 arrays[Mesh.ARRAY_INDEX] = batch_indices mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) var surf_idx := mesh.get_surface_count() - 1 var mat_def: Dictionary = materials[mat_id] if mat_id >= 0 and mat_id < materials.size() else {} mesh.surface_set_material( surf_idx, _build_material( mat_def, tc_idx, textures, texture_types, texture_flags, tex_combos, extracted_dir, model_path)) if mesh.get_surface_count() == 0: return root var mi := MeshInstance3D.new() mi.name = "Mesh" mi.mesh = mesh root.add_child(mi) return root static func _build_material( mat_def: Dictionary, texture_combo_index: int, textures: PackedStringArray, texture_types: PackedInt32Array, texture_flags: PackedInt32Array, tex_combos: PackedInt32Array, extracted_dir: String, model_path: String) -> StandardMaterial3D: var mat := StandardMaterial3D.new() var flags: int = int(mat_def.get("flags", 0)) mat.cull_mode = BaseMaterial3D.CULL_DISABLED if (flags & 0x04) != 0 else BaseMaterial3D.CULL_BACK mat.roughness = 0.85 mat.metallic = 0.0 mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED if (flags & 0x01) != 0 else BaseMaterial3D.SHADING_MODE_PER_PIXEL mat.specular_mode = BaseMaterial3D.SPECULAR_DISABLED mat.vertex_color_use_as_albedo = false mat.set_flag(BaseMaterial3D.FLAG_USE_TEXTURE_REPEAT, 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.alpha_scissor_threshold = 0.5 3, 4, 6: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.blend_mode = BaseMaterial3D.BLEND_MODE_ADD mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_OPAQUE_ONLY _: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_OPAQUE_ONLY if texture_combo_index >= 0 and texture_combo_index < tex_combos.size(): var tex_idx: int = tex_combos[texture_combo_index] if tex_idx >= 0 and tex_idx < textures.size(): var tex_path: String = str(textures[tex_idx]).replace("\\", "/") var tex_type := int(texture_types[tex_idx]) if tex_idx < texture_types.size() else 0 var tex_flags := int(texture_flags[tex_idx]) if tex_idx < texture_flags.size() else 0 if tex_path.is_empty(): tex_path = _default_texture_path(model_path, tex_type) if not tex_path.is_empty(): 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_flag(BaseMaterial3D.FLAG_USE_TEXTURE_REPEAT, (tex_flags & 0x03) != 0) return mat static func _default_texture_path(model_path: String, texture_type: int) -> String: var normalized := model_path.replace("\\", "/") var rel_model := _relative_extracted_model_path(normalized) var rel_lower := rel_model.to_lower() if rel_lower.begins_with("item/objectcomponents/weapon/") and texture_type == 2: var stem := rel_model.get_file().get_basename() if stem.to_lower() == "axe_1h_horde_a_01": return "Item/ObjectComponents/Weapon/Axe_1H_Horde_A_01Gray.blp" if not stem.is_empty(): return "Item/ObjectComponents/Weapon/%s.blp" % stem if rel_lower == "creature/wolf/wolf.m2" or rel_lower == "creature/wolf/wolf.mdx": if texture_type == 11: return "Creature/Wolf/WolfSkinCoyote.blp" if texture_type == 12: return "Creature/Wolf/WolfSkinCoyoteAlpha.blp" if rel_lower == "creature/boar/boar.m2" or rel_lower == "creature/boar/boar.mdx": if texture_type == 11: return "Creature/Boar/BoarSkinIvory.blp" if rel_lower == "creature/kobold/kobold.m2" or rel_lower == "creature/kobold/kobold.mdx": if texture_type == 11: return "Creature/Kobold/koboldskinAlbino.blp" if rel_lower == "creature/murloc/murloc.m2" or rel_lower == "creature/murloc/murloc.mdx": if texture_type == 11: return "Creature/Murloc/SahauginskinBlue.blp" return "" static func _relative_extracted_model_path(path: String) -> String: var marker := "/data/extracted/" var lower := path.to_lower() var marker_pos := lower.find(marker) if marker_pos >= 0: return path.substr(marker_pos + marker.length()) return path 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)) 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