первые наработки по рендеру
This commit is contained in:
@@ -5,7 +5,27 @@
|
||||
## add_child(node)
|
||||
class_name WMOBuilder
|
||||
|
||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
const M2_RIGHT_YAW_OFFSET := PI * 0.5
|
||||
|
||||
static var _texture_cache: Dictionary = {}
|
||||
# rel_path → Node3D prototype (built once, duplicated per usage)
|
||||
static var _m2_prototype_cache: Dictionary = {}
|
||||
# rel_path → true (resolved missing — skip)
|
||||
static var _m2_missing_cache: Dictionary = {}
|
||||
|
||||
|
||||
# Bake scripts should call this before SceneTree.quit() to release cached
|
||||
# Node3D prototypes (Node-derived objects aren't RefCounted, so the static
|
||||
# dict alone won't free them at exit).
|
||||
static func clear_caches() -> void:
|
||||
for proto in _m2_prototype_cache.values():
|
||||
if is_instance_valid(proto):
|
||||
(proto as Node).free()
|
||||
_m2_prototype_cache.clear()
|
||||
_m2_missing_cache.clear()
|
||||
_texture_cache.clear()
|
||||
_wmo_liquid_material_cache.clear()
|
||||
|
||||
# Returns a Node3D containing one MeshInstance3D per WMO group.
|
||||
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||
@@ -23,7 +43,7 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||
for mat_def in materials:
|
||||
godot_materials.append(_build_material(mat_def, textures, extracted_dir))
|
||||
|
||||
# Build one MeshInstance3D per group
|
||||
# Build one MeshInstance3D per group, plus optional liquid mesh as sibling
|
||||
var groups: Array = data.get("groups", [])
|
||||
for gi in groups.size():
|
||||
var g: Dictionary = groups[gi]
|
||||
@@ -33,9 +53,117 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||
mesh_inst.name = "Group_%d" % gi
|
||||
root.add_child(mesh_inst)
|
||||
|
||||
var liquid_dict: Variant = g.get("liquid", null)
|
||||
if liquid_dict is Dictionary and not (liquid_dict as Dictionary).is_empty():
|
||||
var liquid_inst := _build_group_liquid(liquid_dict)
|
||||
if liquid_inst != null:
|
||||
liquid_inst.name = "Group_%d_Liquid" % gi
|
||||
root.add_child(liquid_inst)
|
||||
|
||||
# Doodad set 0 ($DefaultGlobal) — global decorations always active.
|
||||
# Non-default sets (Day/Night/etc) are not baked yet — see TODO below.
|
||||
_build_default_doodads(root, data, extracted_dir)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
# Instantiates M2 doodads for set 0. Each placement is a M2 prototype
|
||||
# duplicated under the WMO root with a local Transform3D.
|
||||
static func _build_default_doodads(root: Node3D, data: Dictionary,
|
||||
extracted_dir: String) -> void:
|
||||
|
||||
var sets: Array = data.get("doodad_sets", [])
|
||||
var placements: Array = data.get("doodad_placements", [])
|
||||
if placements.is_empty() or sets.is_empty():
|
||||
return
|
||||
|
||||
var first_set: Dictionary = sets[0]
|
||||
var start: int = int(first_set.get("start", 0))
|
||||
var count: int = int(first_set.get("count", 0))
|
||||
if count <= 0:
|
||||
return
|
||||
|
||||
var doodad_root := Node3D.new()
|
||||
doodad_root.name = "Doodads"
|
||||
root.add_child(doodad_root)
|
||||
|
||||
var end: int = mini(start + count, placements.size())
|
||||
for i in range(start, end):
|
||||
var p: Dictionary = placements[i]
|
||||
var rel_path: String = str(p.get("name", "")).replace("\\", "/").to_lower()
|
||||
if rel_path.is_empty():
|
||||
continue
|
||||
# WoW 3.3.5 stores .mdx/.mdl in MODN; the actual file on disk is .m2.
|
||||
if rel_path.ends_with(".mdx") or rel_path.ends_with(".mdl"):
|
||||
rel_path = rel_path.get_basename() + ".m2"
|
||||
|
||||
var prototype: Node3D = _get_or_load_m2_prototype(rel_path, extracted_dir)
|
||||
if prototype == null:
|
||||
continue
|
||||
|
||||
var inst := prototype.duplicate() as Node3D
|
||||
if inst == null:
|
||||
continue
|
||||
inst.name = "%s_%d" % [rel_path.get_file().get_basename(), i]
|
||||
|
||||
var pos: Vector3 = p.get("pos", Vector3.ZERO)
|
||||
var rot_q: Quaternion = p.get("rot", Quaternion.IDENTITY)
|
||||
var scl: float = float(p.get("scale", 1.0))
|
||||
var basis := (Basis(rot_q) * Basis(Vector3.UP, M2_RIGHT_YAW_OFFSET)).scaled(Vector3.ONE * maxf(scl, 0.001))
|
||||
inst.transform = Transform3D(basis, pos)
|
||||
doodad_root.add_child(inst)
|
||||
|
||||
|
||||
static func _get_or_load_m2_prototype(rel_path: String,
|
||||
extracted_dir: String) -> Node3D:
|
||||
|
||||
if _m2_prototype_cache.has(rel_path):
|
||||
return _m2_prototype_cache[rel_path]
|
||||
if _m2_missing_cache.has(rel_path):
|
||||
return null
|
||||
|
||||
var prototype: Node3D = _build_raw_m2_prototype(rel_path, extracted_dir)
|
||||
if prototype != null:
|
||||
_m2_prototype_cache[rel_path] = prototype
|
||||
return prototype
|
||||
|
||||
# Fall back to pre-baked cache when the raw M2 is unavailable.
|
||||
var cache_paths := [
|
||||
"res://data/cache/m2_glb".path_join(rel_path.get_basename() + ".tscn"),
|
||||
"res://data/cache/m2_glb".path_join(rel_path.get_basename() + ".glb"),
|
||||
]
|
||||
for cache_path in cache_paths:
|
||||
if not ResourceLoader.exists(cache_path):
|
||||
continue
|
||||
var resource: Resource = load(cache_path)
|
||||
if resource is PackedScene:
|
||||
var node = (resource as PackedScene).instantiate()
|
||||
if node is Node3D:
|
||||
_m2_prototype_cache[rel_path] = node as Node3D
|
||||
return node as Node3D
|
||||
break
|
||||
|
||||
_m2_missing_cache[rel_path] = true
|
||||
return null
|
||||
|
||||
|
||||
static func _build_raw_m2_prototype(rel_path: String, extracted_dir: String) -> Node3D:
|
||||
if not ClassDB.class_exists("M2Loader"):
|
||||
return null
|
||||
|
||||
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(rel_path))
|
||||
if not FileAccess.file_exists(abs_path):
|
||||
return null
|
||||
|
||||
var loader = ClassDB.instantiate("M2Loader")
|
||||
var m2_data: Dictionary = loader.call("load_m2", abs_path)
|
||||
if m2_data.is_empty() or (m2_data.get("vertices", PackedVector3Array()) as PackedVector3Array).is_empty():
|
||||
return null
|
||||
|
||||
var prototype: Node3D = M2_BUILDER_SCRIPT.build(m2_data, extracted_dir)
|
||||
return prototype
|
||||
|
||||
|
||||
static func _build_group_mesh(
|
||||
g: Dictionary,
|
||||
godot_mats: Array[StandardMaterial3D]) -> MeshInstance3D:
|
||||
@@ -138,6 +266,100 @@ static func _build_material(
|
||||
return mat
|
||||
|
||||
|
||||
static var _wmo_liquid_material_cache: Dictionary = {}
|
||||
|
||||
# WMO MLIQ → MeshInstance3D in WMO-local coordinates.
|
||||
# Corner & vertex grid are in WMO-local (Z-up); convert to Godot Y-up using the
|
||||
# same basis change as the WMO geometry: (x, y, z)_local → (-y, z, -x)_godot.
|
||||
static func _build_group_liquid(liquid: Dictionary) -> MeshInstance3D:
|
||||
var xverts: int = int(liquid.get("xverts", 0))
|
||||
var yverts: int = int(liquid.get("yverts", 0))
|
||||
var xtiles: int = int(liquid.get("xtiles", 0))
|
||||
var ytiles: int = int(liquid.get("ytiles", 0))
|
||||
if xverts <= 1 or yverts <= 1 or xtiles <= 0 or ytiles <= 0:
|
||||
return null
|
||||
|
||||
var corner: Vector3 = liquid.get("corner", Vector3.ZERO)
|
||||
var unit: float = float(liquid.get("unit_size", 4.1666666))
|
||||
var heights: PackedFloat32Array = liquid.get("heights", PackedFloat32Array())
|
||||
var tiles: PackedByteArray = liquid.get("tiles", PackedByteArray())
|
||||
if heights.size() < xverts * yverts:
|
||||
return null
|
||||
|
||||
var verts := PackedVector3Array()
|
||||
var indices := PackedInt32Array()
|
||||
|
||||
# Pre-compute Godot-space position for each grid vertex.
|
||||
# WMO local (lx, ly, lz) → Godot (-ly, lz, -lx).
|
||||
var pos := PackedVector3Array()
|
||||
pos.resize(xverts * yverts)
|
||||
for vy in yverts:
|
||||
for vx in xverts:
|
||||
var lx := corner.x + float(vx) * unit
|
||||
var ly := corner.y + float(vy) * unit
|
||||
var lz := heights[vy * xverts + vx]
|
||||
pos[vy * xverts + vx] = Vector3(-ly, lz, -lx)
|
||||
|
||||
for ty in ytiles:
|
||||
for tx in xtiles:
|
||||
var flag: int = 0
|
||||
if tiles.size() > ty * xtiles + tx:
|
||||
flag = int(tiles[ty * xtiles + tx])
|
||||
# wowdev: high bit (0x80) marks "don't render". Some sources also
|
||||
# treat 0x0F lower-nibble as "no liquid" — skip both cases.
|
||||
if (flag & 0x80) != 0:
|
||||
continue
|
||||
if (flag & 0x0F) == 0x0F:
|
||||
continue
|
||||
var i00 := ty * xverts + tx
|
||||
var i10 := ty * xverts + tx + 1
|
||||
var i01 := (ty + 1) * xverts + tx
|
||||
var i11 := (ty + 1) * xverts + tx + 1
|
||||
var base := verts.size()
|
||||
verts.append(pos[i00])
|
||||
verts.append(pos[i10])
|
||||
verts.append(pos[i11])
|
||||
verts.append(pos[i01])
|
||||
# Two triangles, winding chosen to face up after the (-y, z, -x) flip.
|
||||
indices.append(base + 0)
|
||||
indices.append(base + 1)
|
||||
indices.append(base + 2)
|
||||
indices.append(base + 0)
|
||||
indices.append(base + 2)
|
||||
indices.append(base + 3)
|
||||
|
||||
if verts.is_empty() or indices.is_empty():
|
||||
return null
|
||||
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
mesh.surface_set_material(0, _wmo_liquid_material(int(liquid.get("material_id", 0))))
|
||||
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
return mi
|
||||
|
||||
|
||||
static func _wmo_liquid_material(material_id: int) -> StandardMaterial3D:
|
||||
if _wmo_liquid_material_cache.has(material_id):
|
||||
return _wmo_liquid_material_cache[material_id]
|
||||
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
# Default: bluish water. Without DBC liquid_type lookup we can't pick magma/slime
|
||||
# precisely yet — material_id from MLIQ is the WMO MOMT slot, not the DBC id.
|
||||
mat.albedo_color = Color(0.2, 0.4, 0.7, 0.6)
|
||||
_wmo_liquid_material_cache[material_id] = mat
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user