победа над водопадами, шейдеры m2
This commit is contained in:
Binary file not shown.
@@ -3,6 +3,8 @@
|
||||
## Usage:
|
||||
## godot --headless --path <project> --script res://src/tools/bake_m2_cache.gd -- \
|
||||
## --map Azeroth --extracted res://data/extracted --output res://data/cache/m2_glb --force
|
||||
## Add --only-pattern waterfall to rebuild only matching model paths.
|
||||
## Add --glb-animations to also emit animated GLB scenes through src/tools/m2_to_gltf.py.
|
||||
extends SceneTree
|
||||
|
||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
@@ -13,6 +15,10 @@ func _initialize() -> void:
|
||||
var extracted := _res(_arg(args, "--extracted", "res://data/extracted"))
|
||||
var output_dir := _res(_arg(args, "--output", "res://data/cache/m2_glb"))
|
||||
var force := args.has("--force")
|
||||
var glb_animations := args.has("--glb-animations")
|
||||
var only_patterns := _arg_values(args, "--only-pattern")
|
||||
var python_exe := _arg(args, "--python", "python")
|
||||
var converter := _res(_arg(args, "--converter", "res://src/tools/m2_to_gltf.py"))
|
||||
|
||||
if not ClassDB.class_exists("ADTLoader") or not ClassDB.class_exists("M2Loader"):
|
||||
push_error("GDExtension not loaded. Rebuild first.")
|
||||
@@ -47,7 +53,7 @@ func _initialize() -> void:
|
||||
var norm := str(rel).replace("\\", "/").to_lower()
|
||||
if norm.ends_with(".mdx") or norm.ends_with(".mdl"):
|
||||
norm = norm.get_basename() + ".m2"
|
||||
if not norm.is_empty():
|
||||
if not norm.is_empty() and _matches_only_patterns(norm, only_patterns):
|
||||
unique[norm] = true
|
||||
scanned += 1
|
||||
|
||||
@@ -55,8 +61,10 @@ func _initialize() -> void:
|
||||
|
||||
var m2_loader = ClassDB.instantiate("M2Loader")
|
||||
var baked := 0
|
||||
var baked_glb := 0
|
||||
var skipped := 0
|
||||
var failed := 0
|
||||
var failed_glb := 0
|
||||
var total := unique.size()
|
||||
var i := 0
|
||||
|
||||
@@ -64,16 +72,22 @@ func _initialize() -> void:
|
||||
i += 1
|
||||
var stem: String = rel_path.get_basename()
|
||||
var out_path := output_dir.path_join(stem + ".tscn")
|
||||
|
||||
if not force and ResourceLoader.exists(out_path):
|
||||
skipped += 1
|
||||
continue
|
||||
var out_glb_path := output_dir.path_join(stem + ".glb")
|
||||
|
||||
var abs_m2 := ProjectSettings.globalize_path(extracted.path_join(rel_path))
|
||||
if not FileAccess.file_exists(abs_m2):
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
if not force and ResourceLoader.exists(out_path):
|
||||
if glb_animations:
|
||||
if _bake_glb_animation_cache(python_exe, converter, abs_m2, output_dir, out_glb_path, false):
|
||||
baked_glb += 1
|
||||
else:
|
||||
failed_glb += 1
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
var data: Dictionary = m2_loader.call("load_m2", abs_m2)
|
||||
if data.is_empty() or data.get("vertices", PackedVector3Array()).is_empty():
|
||||
failed += 1
|
||||
@@ -102,10 +116,17 @@ func _initialize() -> void:
|
||||
continue
|
||||
|
||||
baked += 1
|
||||
if glb_animations:
|
||||
if _bake_glb_animation_cache(python_exe, converter, abs_m2, output_dir, out_glb_path, force):
|
||||
baked_glb += 1
|
||||
else:
|
||||
failed_glb += 1
|
||||
if i % 50 == 0 or i == total:
|
||||
print("[%d/%d] baked=%d skipped=%d failed=%d" % [i, total, baked, skipped, failed])
|
||||
print("[%d/%d] baked=%d glb=%d skipped=%d failed=%d glb_failed=%d" % [
|
||||
i, total, baked, baked_glb, skipped, failed, failed_glb])
|
||||
|
||||
print("Done. baked=%d skipped=%d failed=%d" % [baked, skipped, failed])
|
||||
print("Done. baked=%d glb=%d skipped=%d failed=%d glb_failed=%d" % [
|
||||
baked, baked_glb, skipped, failed, failed_glb])
|
||||
quit(0)
|
||||
|
||||
|
||||
@@ -116,12 +137,65 @@ func _arg(args: PackedStringArray, name: String, default: String) -> String:
|
||||
return default
|
||||
|
||||
|
||||
func _arg_values(args: PackedStringArray, name: String) -> PackedStringArray:
|
||||
var result := PackedStringArray()
|
||||
for i in args.size():
|
||||
if args[i] != name or i + 1 >= args.size():
|
||||
continue
|
||||
for part in String(args[i + 1]).split(",", false):
|
||||
var value := part.strip_edges().to_lower()
|
||||
if not value.is_empty():
|
||||
result.append(value)
|
||||
return result
|
||||
|
||||
|
||||
func _matches_only_patterns(path: String, patterns: PackedStringArray) -> bool:
|
||||
if patterns.is_empty():
|
||||
return true
|
||||
var lower := path.to_lower()
|
||||
for pattern in patterns:
|
||||
if lower.contains(pattern):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _res(path: String) -> String:
|
||||
if path.begins_with("res://") or path.begins_with("user://"):
|
||||
return path
|
||||
return "res://" + path.trim_prefix("./").trim_prefix("/")
|
||||
|
||||
|
||||
func _bake_glb_animation_cache(
|
||||
python_exe: String,
|
||||
converter: String,
|
||||
abs_m2: String,
|
||||
output_dir: String,
|
||||
out_glb_path: String,
|
||||
force: bool) -> bool:
|
||||
var abs_out_glb := ProjectSettings.globalize_path(out_glb_path)
|
||||
if not force and FileAccess.file_exists(abs_out_glb):
|
||||
return true
|
||||
var abs_converter := ProjectSettings.globalize_path(converter)
|
||||
var abs_output := ProjectSettings.globalize_path(output_dir)
|
||||
if not FileAccess.file_exists(abs_converter):
|
||||
push_warning("M2 GLB converter not found: %s" % converter)
|
||||
return false
|
||||
var stdout := []
|
||||
var exit_code := OS.execute(
|
||||
python_exe,
|
||||
[abs_converter, abs_m2, abs_output],
|
||||
stdout,
|
||||
true,
|
||||
false)
|
||||
if exit_code != 0:
|
||||
push_warning("animated GLB bake failed for %s exit=%d\n%s" % [
|
||||
abs_m2,
|
||||
exit_code,
|
||||
"\n".join(stdout)])
|
||||
return false
|
||||
return FileAccess.file_exists(abs_out_glb)
|
||||
|
||||
|
||||
func _set_owner_recursive(node: Node, owner_root: Node) -> void:
|
||||
for child in node.get_children():
|
||||
child.owner = owner_root
|
||||
|
||||
+29
-28
@@ -650,7 +650,11 @@ class SkinParser:
|
||||
|
||||
class GltfBuilder:
|
||||
def __init__(self):
|
||||
self.asset = {"version": "2.0", "generator": "WoW M2 to GLTF converter"}
|
||||
self.asset = {
|
||||
"version": "2.0",
|
||||
"generator": "WoW M2 to GLTF converter",
|
||||
"extras": {"openwc_m2_anim_schema": "pivot_prefix_v1"},
|
||||
}
|
||||
self.nodes = []
|
||||
self.meshes = []
|
||||
self.skins = []
|
||||
@@ -1027,36 +1031,41 @@ class GltfBuilder:
|
||||
|
||||
def _build_skeleton(self, m2: M2Parser):
|
||||
bone_node_indices = []
|
||||
pivot_node_indices = []
|
||||
|
||||
for i, bone in enumerate(m2.bones):
|
||||
node_idx = len(self.nodes)
|
||||
bone_node_indices.append(node_idx)
|
||||
pivot_idx = len(self.nodes)
|
||||
anim_idx = pivot_idx + 1
|
||||
pivot_node_indices.append(pivot_idx)
|
||||
bone_node_indices.append(anim_idx)
|
||||
|
||||
# Pivot in GLTF space (world-space first; parent-relative below)
|
||||
# Pivot node holds the bind-pose offset. The child bone node stays
|
||||
# at identity and receives animation channels. This mirrors WoW's
|
||||
# T(pivot) * anim * T(-pivot) behavior closely enough for GLTF skinning.
|
||||
px, py, pz = wow_pos_to_gltf(*bone['pivot'])
|
||||
|
||||
node = {
|
||||
"name": f"bone_{i}",
|
||||
self.nodes.append({
|
||||
"name": f"bone_{i}_pivot",
|
||||
"translation": [px, py, pz],
|
||||
"rotation": [0.0, 0.0, 0.0, 1.0],
|
||||
"scale": [1.0, 1.0, 1.0],
|
||||
}
|
||||
self.nodes.append(node)
|
||||
"children": [anim_idx],
|
||||
})
|
||||
self.nodes.append({
|
||||
"name": f"bone_{i}",
|
||||
})
|
||||
|
||||
# Wire up children
|
||||
# Wire pivot nodes into the hierarchy.
|
||||
for i, bone in enumerate(m2.bones):
|
||||
parent = bone['parent']
|
||||
if parent >= 0 and parent < len(m2.bones):
|
||||
parent_node = bone_node_indices[parent]
|
||||
child_node = bone_node_indices[i]
|
||||
child_node = pivot_node_indices[i]
|
||||
self.nodes[parent_node].setdefault("children", []).append(child_node)
|
||||
|
||||
# Convert world-space pivots to parent-relative translations
|
||||
# Convert world-space pivots to parent-relative pivot translations.
|
||||
for i, bone in enumerate(m2.bones):
|
||||
parent = bone['parent']
|
||||
if parent >= 0 and parent < len(m2.bones):
|
||||
ppx, ppy, ppz = wow_pos_to_gltf(*m2.bones[parent]['pivot'])
|
||||
node = self.nodes[bone_node_indices[i]]
|
||||
node = self.nodes[pivot_node_indices[i]]
|
||||
t = node["translation"]
|
||||
node["translation"] = [t[0]-ppx, t[1]-ppy, t[2]-ppz]
|
||||
|
||||
@@ -1064,7 +1073,7 @@ class GltfBuilder:
|
||||
# WoW models have multiple root bones (main skeleton + attachment/socket
|
||||
# bones each with parent=-1). Wrap them all under a single virtual
|
||||
# "Armature" node at the origin so the skin has a valid common root.
|
||||
root_bone_nodes = [bone_node_indices[i]
|
||||
root_bone_nodes = [pivot_node_indices[i]
|
||||
for i, b in enumerate(m2.bones) if b['parent'] < 0]
|
||||
armature_idx = len(self.nodes)
|
||||
self.nodes.append({
|
||||
@@ -1310,20 +1319,12 @@ class GltfBuilder:
|
||||
node_idx = bone_node_indices[bone_idx]
|
||||
|
||||
# Translation
|
||||
# M2 t_track values are OFFSETS from the bone's rest position (pivot
|
||||
# relative to parent pivot). GLTF animation overwrites node.translation
|
||||
# entirely, so we must emit: rest_local + m2_offset (both in GLTF space).
|
||||
# Pivot offset lives on the parent pivot node. The animated bone node
|
||||
# receives only the M2 translation offset.
|
||||
ts, vals = m2._resolve_track(bone['t_track'], seq_idx, 'vec3')
|
||||
if ts and vals:
|
||||
rest = self.nodes[node_idx].get("translation", [0.0, 0.0, 0.0])
|
||||
rx, ry, rz = rest[0], rest[1], rest[2]
|
||||
# Use a factory to capture rx/ry/rz by value (avoid late-binding bug)
|
||||
def _make_trans_fn(rx, ry, rz):
|
||||
def fn(v):
|
||||
cx, cy, cz = wow_pos_to_gltf(*v)
|
||||
return (rx + cx, ry + cy, rz + cz)
|
||||
return fn
|
||||
s_idx = self._anim_sampler(ts, vals, 'VEC3', _make_trans_fn(rx, ry, rz))
|
||||
s_idx = self._anim_sampler(ts, vals, 'VEC3',
|
||||
lambda v: wow_pos_to_gltf(*v))
|
||||
if s_idx is not None:
|
||||
samplers.append(s_idx)
|
||||
channels.append({"sampler": len(samplers)-1,
|
||||
|
||||
Reference in New Issue
Block a user