186 lines
6.2 KiB
GDScript
186 lines
6.2 KiB
GDScript
@tool
|
|
extends EditorScript
|
|
|
|
# Editor tool: convert each dasher_*.glb into an AnimationLibrary .tres file
|
|
# with tracks retargeted to the player rig (GeneralSkeleton + Mixamo bone names).
|
|
#
|
|
# Run once in the Godot editor:
|
|
# File > Run (Ctrl+Shift+X) on this script, OR
|
|
# from a @tool script: EditorInterface.get_resource_filesystem().scan()
|
|
#
|
|
# Output:
|
|
# assets/characters/animations/dasher_<name>.tres (one per glb)
|
|
# Plus a combined library:
|
|
# assets/characters/animations/dasher-pack.tres
|
|
#
|
|
# Run build_dasher_pack.gd after this to consolidate.
|
|
|
|
const DASHER_DIR := "res://assets/characters/dashers"
|
|
const ANIM_DIR := "res://assets/characters/animations"
|
|
|
|
# Map dasher glb bone names (Blender/standard) -> player rig bone names (Mixamo).
|
|
# Tracks whose node name is not in this map are dropped (e.g. helper "head_end" empties).
|
|
const BONE_REMAP := {
|
|
"bob-hold": "Hips", # root of the dasher glb
|
|
"spine": "Hips",
|
|
"spine.001": "Spine",
|
|
"head": "Head",
|
|
"hand.L": "LeftHand",
|
|
"hand.R": "RightHand",
|
|
"forearm.L": "LeftLowerArm",
|
|
"forearm.R": "RightLowerArm",
|
|
"upper_arm.L": "LeftUpperArm",
|
|
"upper_arm.R": "RightUpperArm",
|
|
"shoulder.L": "LeftShoulder",
|
|
"shoulder.R": "RightShoulder",
|
|
"leg.L": "LeftLowerLeg",
|
|
"leg.R": "RightLowerLeg",
|
|
"thigh.L": "LeftUpperLeg",
|
|
"thigh.R": "RightUpperLeg",
|
|
"foot.L": "LeftFoot",
|
|
"foot.R": "RightFoot",
|
|
"toe.L": "LeftToeBase",
|
|
"toe.R": "RightToeBase",
|
|
}
|
|
|
|
const TRACK_TYPES := [
|
|
"position", # 3D position tracks
|
|
"rotation", # 3D rotation tracks (quaternion)
|
|
"scale", # 3D scale tracks
|
|
]
|
|
|
|
func _run() -> void:
|
|
var dasher_dir := DirAccess.open(DASHER_DIR)
|
|
if not dasher_dir:
|
|
push_error("[Convert] Cannot open %s" % DASHER_DIR)
|
|
return
|
|
|
|
var converted: Array[String] = []
|
|
for fname in dasher_dir.get_files():
|
|
if not fname.ends_with(".glb"): continue
|
|
if not fname.begins_with("dasher_"): continue
|
|
var stem := fname.get_basename() # e.g. "dasher_hold"
|
|
var out_path := "%s/%s.res" % [ANIM_DIR, stem]
|
|
var ok := _convert_one("%s/%s" % [DASHER_DIR, fname], stem, out_path)
|
|
if ok:
|
|
converted.append(out_path)
|
|
else:
|
|
push_warning("[Convert] Skipped %s" % fname)
|
|
|
|
print("[Convert] %d dasher glb(s) converted: %s" % [converted.size(), converted])
|
|
|
|
func _convert_one(glb_path: String, anim_name: String, out_path: String) -> bool:
|
|
print("[Convert] %s -> %s" % [glb_path, out_path])
|
|
var doc := GLTFDocument.new()
|
|
var state := GLTFState.new()
|
|
var err := doc.append_from_file(glb_path, state)
|
|
if err != OK:
|
|
push_error("[Convert] append_from_file failed: %d" % err)
|
|
return false
|
|
|
|
var scene := doc.generate_scene(state)
|
|
if not scene:
|
|
push_error("[Convert] generate_scene returned null")
|
|
return false
|
|
|
|
var src_player: AnimationPlayer = scene.find_child("AnimationPlayer", true, false)
|
|
if not src_player:
|
|
push_error("[Convert] No AnimationPlayer in %s" % glb_path)
|
|
scene.queue_free()
|
|
return false
|
|
|
|
var out_lib := AnimationLibrary.new()
|
|
for lib_name in src_player.get_animation_library_list():
|
|
var src_lib := src_player.get_animation_library(lib_name)
|
|
for src_anim_name in src_lib.get_animation_list():
|
|
var src_anim: Animation = src_lib.get_animation(src_anim_name)
|
|
var retargeted := _retarget(src_anim)
|
|
if retargeted and retargeted.get_track_count() > 0:
|
|
out_lib.add_animation(anim_name, retargeted)
|
|
print(" + %s (%d tracks)" % [anim_name, retargeted.get_track_count()])
|
|
else:
|
|
push_warning(" - %s produced 0 tracks after retarget" % src_anim_name)
|
|
|
|
scene.queue_free()
|
|
|
|
if out_lib.get_animation_list().is_empty():
|
|
push_error("[Convert] Output library is empty for %s" % glb_path)
|
|
return false
|
|
|
|
var save_err := ResourceSaver.save(out_lib, out_path)
|
|
if save_err != OK:
|
|
push_error("[Convert] ResourceSaver.save failed: %d" % save_err)
|
|
return false
|
|
return true
|
|
|
|
func _retarget(src: Animation) -> Animation:
|
|
var dst := Animation.new()
|
|
dst.length = src.length
|
|
dst.loop_mode = src.loop_mode
|
|
dst.step = src.step
|
|
|
|
# Map: original_track_idx -> new track path or -1 to drop
|
|
for i in src.get_track_count():
|
|
var orig_path: String = src.track_get_path(i)
|
|
var bone := _extract_bone_name(orig_path)
|
|
if not BONE_REMAP.has(bone):
|
|
# drop helper nodes like head_end, hand.L_end, etc.
|
|
continue
|
|
|
|
var new_bone: String = BONE_REMAP[bone]
|
|
var new_path := "%%GeneralSkeleton:%s" % new_bone
|
|
|
|
# Match the suffix type (:position/:rotation/:scale)
|
|
var type_suffix := ""
|
|
if orig_path.ends_with(":position"): type_suffix = "position"
|
|
elif orig_path.ends_with(":rotation"): type_suffix = "rotation"
|
|
elif orig_path.ends_with(":scale"): type_suffix = "scale"
|
|
else:
|
|
push_warning(" Unrecognized track path: %s" % orig_path)
|
|
continue
|
|
|
|
match type_suffix:
|
|
"position":
|
|
if new_bone != "Hips": continue
|
|
dst.add_track(Animation.TYPE_POSITION_3D)
|
|
"rotation":
|
|
dst.add_track(Animation.TYPE_ROTATION_3D)
|
|
"scale":
|
|
if new_bone != "Hips": continue
|
|
dst.add_track(Animation.TYPE_SCALE_3D)
|
|
|
|
var new_idx := dst.get_track_count() - 1
|
|
dst.track_set_path(new_idx, NodePath(new_path))
|
|
dst.track_set_interpolation_type(new_idx, src.track_get_interpolation_type(i))
|
|
dst.track_set_imported(new_idx, src.track_is_imported(i))
|
|
dst.track_set_enabled(new_idx, src.track_is_enabled(i))
|
|
|
|
# Copy keyframes
|
|
var key_count := src.track_get_key_count(i)
|
|
for k in key_count:
|
|
var t: float = src.track_get_key_time(i, k)
|
|
var v: Variant = src.track_get_key_value(i, k)
|
|
var trans := src.track_get_key_transition(i, k)
|
|
match type_suffix:
|
|
"position":
|
|
dst.track_insert_key(new_idx, t, v)
|
|
"rotation":
|
|
dst.track_insert_key(new_idx, t, v)
|
|
"scale":
|
|
dst.track_insert_key(new_idx, t, v)
|
|
dst.track_set_key_transition(new_idx, k, trans)
|
|
return dst
|
|
|
|
func _extract_bone_name(path: String) -> String:
|
|
# Tracks look like: "bob-hold/Skeleton3D:hand.L:rotation"
|
|
# We want the bone name "hand.L".
|
|
# Find the last ":" that precedes a type suffix; everything between the previous
|
|
# ":" (or start) and that ":" is the bone name. Easier: split on ":" and take
|
|
# the second-to-last segment.
|
|
var parts := path.split(":")
|
|
if parts.size() < 2:
|
|
return ""
|
|
# parts[-1] is the type ("rotation" etc), parts[-2] is the bone name
|
|
# but bone names can contain "." (e.g. "hand.L"), so just take parts[-2] as-is.
|
|
return parts[-2]
|