196 lines
7.0 KiB
GDScript
196 lines
7.0 KiB
GDScript
## 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
|