Replace dasher-pack with unified animation-pack using original Blender bone names
This commit is contained in:
+33
-46
@@ -1,63 +1,50 @@
|
||||
# Dasher Animation Tools
|
||||
# Tools
|
||||
|
||||
The dasher character (`assets/characters/dashers/dasher_*.glb`) provides six
|
||||
animation clips. They need to be retargeted to the player rig (which uses
|
||||
`GeneralSkeleton` + Mixamo bone names) and packed into a single
|
||||
`AnimationLibrary` that `player.tscn` can reference.
|
||||
## rename_rig_in_glb.py
|
||||
|
||||
## One-time generation (headless, no editor needed)
|
||||
Renames the rig root node in each character GLB to a consistent name
|
||||
(`Character`). This makes all characters share the same skeleton path
|
||||
`Character/Skeleton3D:bone_name`, enabling a shared animation library.
|
||||
|
||||
```bash
|
||||
godot --headless --path /home/beng/Godot/Projects/tekton-enet \
|
||||
--script res://tools/convert_dasher_animations_headless.gd
|
||||
|
||||
godot --headless --path /home/beng/Godot/Projects/tekton-enet \
|
||||
--script res://tools/build_dasher_pack_headless.gd
|
||||
python3 tools/rename_rig_in_glb.py
|
||||
```
|
||||
|
||||
Output:
|
||||
- `assets/characters/animations/dasher_<name>.res` (one per glb)
|
||||
- `assets/characters/animations/dasher-pack.res` (combined library)
|
||||
Modifies: `Bob.glb`, `Oldpop.glb`, `Masbro.glb`, `Gatot.glb`
|
||||
- bob-rig → Character
|
||||
- oldpop-rig → Character
|
||||
- masbro-tpose → Character
|
||||
- gatot-tpose → Character
|
||||
|
||||
`scenes/player.tscn` already references `dasher-pack.res` as a second
|
||||
AnimationLibrary on the AnimationPlayer node, alongside the original
|
||||
`animation-pack.res`.
|
||||
Creates `.glb.bak` backups on first run. Re-runnable; restores from backup
|
||||
each time to ensure idempotency.
|
||||
|
||||
## Or run inside the editor
|
||||
After running, you must reimport in the Godot editor (or via
|
||||
`godot --headless --import`).
|
||||
|
||||
Use the EditorScript variants:
|
||||
- `tools/convert_dasher_animations.gd` (`@tool extends EditorScript`)
|
||||
- `tools/build_dasher_pack.gd` (`@tool extends EditorScript`)
|
||||
## build_animation_pack.gd
|
||||
|
||||
Open each in the editor and `File > Run` (`Ctrl+Shift+X`).
|
||||
Headless script that extracts all animations from `animation-0.glb`,
|
||||
applies rest-pose correction against `Bob.glb` (the reference rig), and
|
||||
saves as `assets/characters/animations/animation-pack.res`.
|
||||
|
||||
## Usage
|
||||
|
||||
```gdscript
|
||||
anim_player.play("dasher-pack/dasher_hit")
|
||||
anim_player.play("dasher-pack/dasher_stun")
|
||||
# etc.
|
||||
```bash
|
||||
godot --headless --path . --script tools/build_animation_pack.gd
|
||||
```
|
||||
|
||||
## How it works
|
||||
Run `rename_rig_in_glb.py` first so the reference skeleton has the
|
||||
expected path.
|
||||
|
||||
Each `dasher_*.glb` contains three source animations in a single
|
||||
`AnimationLibrary` (e.g. `dasher_hold.glb` has `bob-rig|Hold|Anima_Layer`,
|
||||
`bob-rig|Put|Anima_Layer`, `bob-rig|bob ani|Anima_Layer`). The converter
|
||||
picks the most relevant one based on the glb filename
|
||||
(`ANIM_PICK` constant in `convert_dasher_animations_headless.gd`) and
|
||||
retargets the bone names. The resulting `.res` has one animation per glb
|
||||
named after the glb (`dasher_hold`, `dasher_hit`, etc.), retargeted to
|
||||
`GeneralSkeleton:<MixamoBone>` paths so they apply against the player's
|
||||
shared skeleton.
|
||||
The animation-pack contains 15 animations used by the player:
|
||||
- idle, walk_forward, backflip_1
|
||||
- take_tile_1, take_tile_2, drop_tile_1, drop_tile_2, spawn_tile_1
|
||||
- pickup_1, put_1, holding_1
|
||||
- hit_1, hitted_1, getting_hit_1, stun_1
|
||||
|
||||
The `BONE_REMAP` table in the converter defines Blender → Mixamo bone
|
||||
name translations (e.g. `head` → `Head`, `hand.L` → `LeftHand`).
|
||||
Helper nodes (`head_end`, `hand.L_end`, etc.) that don't exist in the
|
||||
player rig are silently dropped.
|
||||
All tracks use original Blender bone names with path
|
||||
`Character/Skeleton3D:bone_name` so they resolve regardless of which
|
||||
character model is active.
|
||||
|
||||
## Editing the bone remap
|
||||
## generate_version_json.py
|
||||
|
||||
If a dasher animation doesn't apply correctly, the most likely cause is a
|
||||
bone name in the glb that's not in `BONE_REMAP`. Add the entry in
|
||||
`convert_dasher_animations_headless.gd` and re-run both tools.
|
||||
Generates version.json for game update checks.
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
extends SceneTree
|
||||
|
||||
## Headless script: loads animation-0.glb, extracts animations with
|
||||
## original Blender bone names, applies rest-pose correction per bone,
|
||||
## and saves animation-pack.res.
|
||||
##
|
||||
## Usage: godot --headless --path <project> --script tools/build_animation_pack.gd
|
||||
|
||||
const SOURCE_GLB := "res://assets/characters/animation-0.glb"
|
||||
const TARGET_SKELETON_GLB := "res://assets/characters/Bob.glb"
|
||||
const OUTPUT_PATH := "res://assets/characters/animations/animation-pack.res"
|
||||
|
||||
var _corrections: Dictionary = {} # bone_name -> Quaternion correction
|
||||
var _correction_src_rest: Dictionary = {} # "Skeleton3D:bone" -> Vector3
|
||||
var _correction_tgt_rest: Dictionary = {} # "Skeleton3D:bone" -> Vector3
|
||||
|
||||
func _init() -> void:
|
||||
print("[build_animation_pack] Starting...")
|
||||
|
||||
# Step 1: Load source skeleton (animation-0.glb)
|
||||
var gltf_doc := GLTFDocument.new()
|
||||
var gltf_state := GLTFState.new()
|
||||
var err := gltf_doc.append_from_file(
|
||||
ProjectSettings.globalize_path(SOURCE_GLB), gltf_state
|
||||
)
|
||||
if err != OK:
|
||||
push_error("[build_animation_pack] Failed to load source GLB: error %d" % err)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var src_scene := gltf_doc.generate_scene(gltf_state)
|
||||
if not src_scene:
|
||||
push_error("[build_animation_pack] source generate_scene returned null")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var src_skel: Skeleton3D = src_scene.find_child("Skeleton3D", true, false)
|
||||
if not src_skel:
|
||||
push_error("[build_animation_pack] No Skeleton3D in source")
|
||||
src_scene.queue_free()
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# Step 2: Load target skeleton (Bob, raw)
|
||||
var tgt_gltf_doc := GLTFDocument.new()
|
||||
var tgt_gltf_state := GLTFState.new()
|
||||
err = tgt_gltf_doc.append_from_file(
|
||||
ProjectSettings.globalize_path(TARGET_SKELETON_GLB), tgt_gltf_state
|
||||
)
|
||||
if err != OK:
|
||||
push_error("[build_animation_pack] Failed to load target GLB: error %d" % err)
|
||||
src_scene.queue_free()
|
||||
quit(1)
|
||||
return
|
||||
var tgt_scene: Node = tgt_gltf_doc.generate_scene(tgt_gltf_state)
|
||||
if not tgt_scene:
|
||||
push_error("[build_animation_pack] target generate_scene returned null")
|
||||
src_scene.queue_free()
|
||||
quit(1)
|
||||
return
|
||||
var tgt_skel: Skeleton3D = tgt_scene.find_child("Skeleton3D", true, false)
|
||||
if not tgt_skel:
|
||||
push_error("[build_animation_pack] No Skeleton3D in target")
|
||||
src_scene.queue_free()
|
||||
tgt_scene.queue_free()
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# Step 3: Build per-bone correction quaternions (original bone names, 1:1)
|
||||
var src_bone_index: Dictionary = {}
|
||||
for i in src_skel.get_bone_count():
|
||||
src_bone_index[src_skel.get_bone_name(i)] = i
|
||||
|
||||
for i in tgt_skel.get_bone_count():
|
||||
var bone_name: String = tgt_skel.get_bone_name(i)
|
||||
if not src_bone_index.has(bone_name):
|
||||
continue
|
||||
var src_idx: int = src_bone_index[bone_name]
|
||||
var src_rest: Transform3D = src_skel.get_bone_rest(src_idx)
|
||||
var tgt_rest: Transform3D = tgt_skel.get_bone_rest(i)
|
||||
# correction = tgt_rest.rot * src_rest.rot.inverse()
|
||||
var correction: Quaternion = Quaternion(tgt_rest.basis) * Quaternion(src_rest.basis.inverse())
|
||||
_corrections[bone_name] = correction
|
||||
_correction_src_rest["Character/Skeleton3D:%s" % bone_name] = src_rest.origin
|
||||
_correction_tgt_rest["Character/Skeleton3D:%s" % bone_name] = tgt_rest.origin
|
||||
|
||||
print("[build_animation_pack] Built %d rest-pose corrections" % _corrections.size())
|
||||
|
||||
# Step 4: Extract animations with original bone names
|
||||
# We use the generated scene's AnimationPlayer, which contains the
|
||||
# already-stripped tracks (immutable ones removed by GLTF importer).
|
||||
var anim_player: AnimationPlayer = src_scene.find_child("AnimationPlayer", true, false)
|
||||
if not anim_player:
|
||||
push_error("[build_animation_pack] No AnimationPlayer in source")
|
||||
src_scene.queue_free()
|
||||
tgt_scene.queue_free()
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var out_lib := AnimationLibrary.new()
|
||||
var count := 0
|
||||
var total_tracks := 0
|
||||
var corrected_keys := 0
|
||||
|
||||
for lib_name in anim_player.get_animation_library_list():
|
||||
var src_lib := anim_player.get_animation_library(lib_name)
|
||||
for anim_name in src_lib.get_animation_list():
|
||||
var src_anim: Animation = src_lib.get_animation(anim_name)
|
||||
if not src_anim:
|
||||
continue
|
||||
|
||||
var new_anim := Animation.new()
|
||||
new_anim.length = src_anim.length
|
||||
new_anim.loop_mode = src_anim.loop_mode
|
||||
|
||||
for i in src_anim.get_track_count():
|
||||
var path_str: String = str(src_anim.track_get_path(i))
|
||||
var track_type: int = src_anim.track_get_type(i)
|
||||
|
||||
# Parse "retarget/Skeleton3D:bone_name" -> bone_name
|
||||
var bone_name := ""
|
||||
if ":" in path_str:
|
||||
bone_name = path_str.split(":")[1]
|
||||
else:
|
||||
# Non-bone track: skip GLB root "retarget"
|
||||
if "retarget" in path_str:
|
||||
continue
|
||||
_copy_track(src_anim, i, new_anim, path_str, Quaternion.IDENTITY, track_type)
|
||||
continue
|
||||
|
||||
# Skip the GLB scene root "retarget" — not a real bone.
|
||||
if bone_name == "retarget":
|
||||
continue
|
||||
|
||||
# Use original bone name with character-rig-relative path.
|
||||
# All characters have rig root renamed to "Character" via GLB modification,
|
||||
# so the path "Character/Skeleton3D:bone" is consistent across all of them.
|
||||
var new_path := "Character/Skeleton3D:%s" % bone_name
|
||||
var correction: Quaternion = _corrections.get(bone_name, Quaternion())
|
||||
_copy_track(src_anim, i, new_anim, new_path, correction, track_type)
|
||||
total_tracks += 1
|
||||
corrected_keys += _last_key_count
|
||||
|
||||
out_lib.add_animation(anim_name, new_anim)
|
||||
count += 1
|
||||
print(" + %s (%d tracks)" % [anim_name, new_anim.get_track_count()])
|
||||
|
||||
print("[build_animation_pack] Extracted %d animations, %d tracks, corrected %d rotation keys" % [count, total_tracks, corrected_keys])
|
||||
|
||||
err = ResourceSaver.save(out_lib, OUTPUT_PATH)
|
||||
if err != OK:
|
||||
push_error("[build_animation_pack] Failed to save: error %d" % err)
|
||||
src_scene.queue_free()
|
||||
tgt_scene.queue_free()
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("[build_animation_pack] Saved to %s" % OUTPUT_PATH)
|
||||
src_scene.queue_free()
|
||||
tgt_scene.queue_free()
|
||||
quit(0)
|
||||
|
||||
var _last_key_count: int = 0
|
||||
|
||||
func _copy_track(src_anim: Animation, src_idx: int, dst_anim: Animation, new_path: String, correction: Quaternion, track_type: int = -1) -> void:
|
||||
if track_type < 0:
|
||||
track_type = src_anim.track_get_type(src_idx)
|
||||
var dst_idx := dst_anim.add_track(track_type)
|
||||
dst_anim.track_set_path(dst_idx, NodePath(new_path))
|
||||
dst_anim.track_set_interpolation_type(dst_idx, src_anim.track_get_interpolation_type(src_idx))
|
||||
|
||||
var key_count := src_anim.track_get_key_count(src_idx)
|
||||
_last_key_count = 0
|
||||
for k in key_count:
|
||||
var time := src_anim.track_get_key_time(src_idx, k)
|
||||
var value = src_anim.track_get_key_value(src_idx, k)
|
||||
var transition := src_anim.track_get_key_transition(src_idx, k)
|
||||
|
||||
match track_type:
|
||||
Animation.TYPE_POSITION_3D:
|
||||
var src_pos: Vector3 = value
|
||||
var src_rest_pos: Vector3 = _correction_src_rest.get(new_path, Vector3.ZERO)
|
||||
var tgt_rest_pos: Vector3 = _correction_tgt_rest.get(new_path, Vector3.ZERO)
|
||||
var corrected_pos: Vector3 = correction * (src_pos - src_rest_pos) + tgt_rest_pos
|
||||
dst_anim.position_track_insert_key(dst_idx, time, corrected_pos)
|
||||
Animation.TYPE_ROTATION_3D:
|
||||
var src_q: Quaternion = value
|
||||
var corrected_q: Quaternion = correction * src_q
|
||||
dst_anim.rotation_track_insert_key(dst_idx, time, corrected_q)
|
||||
_last_key_count += 1
|
||||
Animation.TYPE_SCALE_3D:
|
||||
dst_anim.scale_track_insert_key(dst_idx, time, value)
|
||||
Animation.TYPE_BLEND_SHAPE:
|
||||
dst_anim.blend_shape_track_insert_key(dst_idx, time, value)
|
||||
_:
|
||||
dst_anim.track_insert_key(dst_idx, time, value, transition)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkltni8e2osfr
|
||||
@@ -1,52 +0,0 @@
|
||||
@tool
|
||||
extends EditorScript
|
||||
|
||||
# Editor tool: combine all dasher_<name>.tres AnimationLibrary files into a
|
||||
# single dasher-pack.tres for player.tscn to reference.
|
||||
#
|
||||
# Run AFTER convert_dasher_animations.gd.
|
||||
# File > Run (Ctrl+Shift+X) on this script.
|
||||
#
|
||||
# Output: res://assets/characters/animations/dasher-pack.tres
|
||||
#
|
||||
# Then add an ext_resource to scenes/player.tscn:
|
||||
# [ext_resource type="AnimationLibrary" path="res://assets/characters/animations/dasher-pack.tres" id="..."]
|
||||
# And on the AnimationPlayer node:
|
||||
# libraries/dasher-pack = ExtResource("...")
|
||||
|
||||
const ANIM_DIR := "res://assets/characters/animations"
|
||||
const OUTPUT := "res://assets/characters/animations/dasher-pack.res"
|
||||
|
||||
func _run() -> void:
|
||||
var combined := AnimationLibrary.new()
|
||||
|
||||
var dir := DirAccess.open(ANIM_DIR)
|
||||
if not dir:
|
||||
push_error("[Build] Cannot open %s" % ANIM_DIR)
|
||||
return
|
||||
|
||||
var loaded: Array[String] = []
|
||||
for fname in dir.get_files():
|
||||
if not fname.begins_with("dasher_"): continue
|
||||
if not fname.ends_with(".res"): continue
|
||||
if fname == "dasher-pack.res": continue
|
||||
var path := "%s/%s" % [ANIM_DIR, fname]
|
||||
var res: Resource = load(path)
|
||||
if not res is AnimationLibrary:
|
||||
push_warning("[Build] %s is not an AnimationLibrary, skipping" % fname)
|
||||
continue
|
||||
var lib: AnimationLibrary = res
|
||||
for anim_name in lib.get_animation_list():
|
||||
if combined.has_animation(anim_name):
|
||||
push_warning("[Build] Duplicate anim name '%s' in %s, skipping" % [anim_name, fname])
|
||||
continue
|
||||
combined.add_animation(anim_name, lib.get_animation(anim_name))
|
||||
loaded.append("%s/%s" % [fname, anim_name])
|
||||
|
||||
var err := ResourceSaver.save(combined, OUTPUT)
|
||||
if err != OK:
|
||||
push_error("[Build] ResourceSaver.save failed: %d" % err)
|
||||
return
|
||||
print("[Build] %d animation(s) written to %s" % [loaded.size(), OUTPUT])
|
||||
for l in loaded:
|
||||
print(" - %s" % l)
|
||||
@@ -1,48 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
# Headless equivalent of build_dasher_pack.gd.
|
||||
# Run from CLI AFTER convert_dasher_animations_headless.gd:
|
||||
# godot --headless --path /home/beng/Godot/Projects/tekton-enet --script res://tools/build_dasher_pack_headless.gd
|
||||
#
|
||||
# Produces: res://assets/characters/animations/dasher-pack.tres
|
||||
|
||||
const ANIM_DIR := "res://assets/characters/animations"
|
||||
const OUTPUT := "res://assets/characters/animations/dasher-pack.res"
|
||||
|
||||
func _init() -> void:
|
||||
print("[Build] Headless run starting...")
|
||||
var combined := AnimationLibrary.new()
|
||||
|
||||
var dir := DirAccess.open(ANIM_DIR)
|
||||
if not dir:
|
||||
push_error("[Build] Cannot open %s" % ANIM_DIR)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var loaded: Array[String] = []
|
||||
for fname in dir.get_files():
|
||||
if not fname.begins_with("dasher_"): continue
|
||||
if not fname.ends_with(".res"): continue
|
||||
if fname == "dasher-pack.res": continue
|
||||
var path := "%s/%s" % [ANIM_DIR, fname]
|
||||
var res: Resource = load(path)
|
||||
if not res is AnimationLibrary:
|
||||
push_warning("[Build] %s is not an AnimationLibrary, skipping" % fname)
|
||||
continue
|
||||
var lib: AnimationLibrary = res
|
||||
for anim_name in lib.get_animation_list():
|
||||
if combined.has_animation(anim_name):
|
||||
push_warning("[Build] Duplicate anim name '%s' in %s, skipping" % [anim_name, fname])
|
||||
continue
|
||||
combined.add_animation(anim_name, lib.get_animation(anim_name))
|
||||
loaded.append("%s/%s" % [fname, anim_name])
|
||||
|
||||
var err := ResourceSaver.save(combined, OUTPUT)
|
||||
if err != OK:
|
||||
push_error("[Build] ResourceSaver.save failed: %d" % err)
|
||||
quit(1)
|
||||
return
|
||||
print("[Build] Done. %d animation(s) written to %s" % [loaded.size(), OUTPUT])
|
||||
for l in loaded:
|
||||
print(" - %s" % l)
|
||||
quit(0)
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env -S godot --headless -s
|
||||
extends MainLoop
|
||||
|
||||
func _initialize():
|
||||
print("--- Starting Automated Patch Build ---")
|
||||
|
||||
var output_file = "patch.pck"
|
||||
var changed_files_txt = "changed_files.txt"
|
||||
|
||||
if not FileAccess.file_exists(changed_files_txt):
|
||||
print("ERROR: missing changed_files.txt. Cannot build patch.")
|
||||
return
|
||||
|
||||
var pck_packer = PCKPacker.new()
|
||||
var err = pck_packer.pck_start(output_file)
|
||||
if err != OK:
|
||||
print("ERROR: Could not start PCK file: ", output_file)
|
||||
return
|
||||
|
||||
var file = FileAccess.open(changed_files_txt, FileAccess.READ)
|
||||
var count = 0
|
||||
|
||||
while not file.eof_reached():
|
||||
var line = file.get_line().strip_edges()
|
||||
if line.is_empty(): continue
|
||||
|
||||
var res_path = "res://" + line
|
||||
# Include automatically compiled scripts for GDScript
|
||||
if line.ends_with(".gd"):
|
||||
var remap_path = res_path.replace(".gd", ".gdc")
|
||||
if FileAccess.file_exists(remap_path):
|
||||
pck_packer.add_file(res_path, remap_path)
|
||||
else:
|
||||
pck_packer.add_file(res_path, res_path)
|
||||
count += 1
|
||||
print("Adding (Script): ", res_path)
|
||||
elif FileAccess.file_exists(res_path):
|
||||
print("Adding to patch: ", res_path)
|
||||
pck_packer.add_file(res_path, res_path)
|
||||
count += 1
|
||||
else:
|
||||
print("Warning: Changed file not found or Is Directory, skipping: ", res_path)
|
||||
|
||||
# Always package our version/changelog list so clients see the new changelog
|
||||
var version_manifest = "res://assets/data/version.json"
|
||||
pck_packer.add_file(version_manifest, version_manifest)
|
||||
print("Adding Version Manifest: ", version_manifest)
|
||||
|
||||
pck_packer.flush(true)
|
||||
print("--- Patch Build Complete! Packed %d files into %s ---" % [count + 1, output_file])
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
return true # True tells Godot to gracefully shut down the engine now!
|
||||
@@ -1,185 +0,0 @@
|
||||
@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]
|
||||
@@ -1,256 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
# Headless: convert each dasher_*.glb into a dasher_<name>.res with proper retargeting.
|
||||
# Reads rest poses from both source GLB skeleton and target GeneralSkeleton (player.tscn)
|
||||
# to compute per-bone correction quaternions.
|
||||
|
||||
|
||||
const DASHER_DIR := "res://assets/characters/dashers"
|
||||
const ANIM_DIR := "res://assets/characters/animations"
|
||||
|
||||
const BONE_REMAP := {
|
||||
"bob-hold": "Hips",
|
||||
"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 ANIM_PICK := {
|
||||
"dasher_getting_hit": "Getting Hit",
|
||||
"dasher_hit": "Hit",
|
||||
"dasher_hold": "Hold",
|
||||
"dasher_put": "Put",
|
||||
"dasher_stun": "Stun",
|
||||
"dasher_take": "bob ani",
|
||||
}
|
||||
|
||||
# Source bone rest transforms (from GLB)
|
||||
var src_rest: Dictionary = {} # bone_name -> Transform3D
|
||||
# Target bone rest transforms (from GeneralSkeleton)
|
||||
var tgt_rest: Dictionary = {} # bone_name -> Transform3D
|
||||
# Precomputed correction quaternions
|
||||
var corrections: Dictionary = {} # bone_name -> Quaternion
|
||||
|
||||
func _init() -> void:
|
||||
print("[Convert] Headless run starting...")
|
||||
|
||||
# Step 1: Load target skeleton rest poses from player scene
|
||||
_load_target_rest()
|
||||
|
||||
# Step 2: Convert each GLB
|
||||
var dasher_dir := DirAccess.open(DASHER_DIR)
|
||||
if not dasher_dir:
|
||||
push_error("[Convert] Cannot open %s" % DASHER_DIR)
|
||||
quit(1); 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()
|
||||
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] Done. %d dasher glb(s) converted:" % converted.size())
|
||||
for p in converted:
|
||||
print(" - %s" % p)
|
||||
quit(0)
|
||||
|
||||
func _load_target_rest() -> void:
|
||||
# Load Masbro.glb directly to extract GeneralSkeleton rest poses
|
||||
# (player.tscn may fail to load if dasher-pack.res doesn't exist yet)
|
||||
var doc := GLTFDocument.new()
|
||||
var state := GLTFState.new()
|
||||
var err := doc.append_from_file("res://assets/characters/Masbro.glb", state)
|
||||
if err != OK:
|
||||
push_error("[Convert] Cannot load Masbro.glb: %d" % err)
|
||||
return
|
||||
|
||||
var instance = doc.generate_scene(state)
|
||||
if not instance:
|
||||
push_error("[Convert] generate_scene returned null for Masbro.glb")
|
||||
return
|
||||
|
||||
# Find the Skeleton3D (named GeneralSkeleton in the scene tree)
|
||||
var skeleton: Skeleton3D = instance.find_child("GeneralSkeleton", true, false)
|
||||
if not skeleton:
|
||||
# Try finding any Skeleton3D
|
||||
skeleton = instance.find_child("Skeleton3D", true, false)
|
||||
if not skeleton:
|
||||
push_error("[Convert] No Skeleton3D in Masbro.glb")
|
||||
instance.free()
|
||||
return
|
||||
|
||||
print("[Convert] Target skeleton: '%s' with %d bones" % [skeleton.name, skeleton.get_bone_count()])
|
||||
for bone_idx in skeleton.get_bone_count():
|
||||
var bone_name := skeleton.get_bone_name(bone_idx)
|
||||
tgt_rest[bone_name] = skeleton.get_bone_rest(bone_idx)
|
||||
|
||||
instance.free()
|
||||
print("[Convert] Loaded %d target bone rest poses" % tgt_rest.size())
|
||||
|
||||
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: Node = doc.generate_scene(state)
|
||||
if not scene:
|
||||
push_error("[Convert] generate_scene returned null")
|
||||
return false
|
||||
|
||||
# Extract source skeleton rest poses
|
||||
src_rest.clear()
|
||||
var src_skeleton: Skeleton3D = scene.find_child("Skeleton3D", true, false)
|
||||
if src_skeleton:
|
||||
for bone_idx in src_skeleton.get_bone_count():
|
||||
var bone_name := src_skeleton.get_bone_name(bone_idx)
|
||||
src_rest[bone_name] = src_skeleton.get_bone_rest(bone_idx)
|
||||
print(" Source skeleton: %d bones" % src_rest.size())
|
||||
else:
|
||||
push_error("[Convert] No Skeleton3D in %s" % glb_path)
|
||||
scene.queue_free()
|
||||
return false
|
||||
|
||||
# Compute per-bone correction quaternions
|
||||
corrections.clear()
|
||||
for src_bone in BONE_REMAP:
|
||||
var tgt_bone: String = BONE_REMAP[src_bone]
|
||||
if src_rest.has(src_bone) and tgt_rest.has(tgt_bone):
|
||||
var src_basis: Basis = src_rest[src_bone].basis
|
||||
var tgt_basis: Basis = tgt_rest[tgt_bone].basis
|
||||
# correction = tgt_rest.inverse() * src_rest
|
||||
# Applied as: final_q = correction * keyframe_q
|
||||
corrections[src_bone] = tgt_basis.inverse() * src_basis
|
||||
# print(" Correction for %s -> %s: %s" % [src_bone, tgt_bone, str(corrections[src_bone])])
|
||||
|
||||
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
|
||||
|
||||
# Pick the animation whose middle segment matches the keyword
|
||||
var pick_keyword: String = ANIM_PICK.get(anim_name, anim_name.replace("dasher_", ""))
|
||||
var picked: Animation = null
|
||||
for lib_name in src_player.get_animation_library_list():
|
||||
var src_lib: AnimationLibrary = src_player.get_animation_library(lib_name)
|
||||
for src_anim_name in src_lib.get_animation_list():
|
||||
var segments := src_anim_name.split("|")
|
||||
var mid_segment: String = segments[1] if segments.size() >= 3 else src_anim_name
|
||||
if pick_keyword.to_lower() == mid_segment.to_lower():
|
||||
picked = src_lib.get_animation(src_anim_name)
|
||||
break
|
||||
if picked: break
|
||||
if not picked:
|
||||
for lib_name in src_player.get_animation_library_list():
|
||||
var src_lib: AnimationLibrary = src_player.get_animation_library(lib_name)
|
||||
if src_lib.get_animation_list().size() > 0:
|
||||
picked = src_lib.get_animation(src_lib.get_animation_list()[0])
|
||||
break
|
||||
if not picked:
|
||||
push_error("[Convert] No animations in %s" % glb_path)
|
||||
scene.queue_free()
|
||||
return false
|
||||
|
||||
var retargeted := _retarget(picked)
|
||||
scene.queue_free()
|
||||
|
||||
if retargeted.get_track_count() == 0:
|
||||
push_error("[Convert] Retarget produced 0 tracks for %s" % glb_path)
|
||||
return false
|
||||
|
||||
var out_lib := AnimationLibrary.new()
|
||||
out_lib.add_animation(anim_name, retargeted)
|
||||
print(" + %s (%d tracks from %s)" % [anim_name, retargeted.get_track_count(), picked.resource_name if picked.resource_name else "<anon>"])
|
||||
|
||||
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
|
||||
|
||||
var usable: Array[int] = []
|
||||
for i in src.get_track_count():
|
||||
var orig_path: String = src.track_get_path(i)
|
||||
var bone := _extract_bone_name(orig_path)
|
||||
if BONE_REMAP.has(bone):
|
||||
usable.append(i)
|
||||
|
||||
for i in usable:
|
||||
var orig_path: String = src.track_get_path(i)
|
||||
var bone: String = _extract_bone_name(orig_path)
|
||||
var new_bone: String = BONE_REMAP[bone]
|
||||
var new_path := "%%GeneralSkeleton:%s" % new_bone
|
||||
|
||||
var track_type := src.track_get_type(i)
|
||||
|
||||
match track_type:
|
||||
Animation.TYPE_POSITION_3D:
|
||||
if new_bone != "Hips": continue
|
||||
dst.add_track(Animation.TYPE_POSITION_3D)
|
||||
Animation.TYPE_ROTATION_3D:
|
||||
dst.add_track(Animation.TYPE_ROTATION_3D)
|
||||
Animation.TYPE_SCALE_3D:
|
||||
if new_bone != "Hips": continue
|
||||
dst.add_track(Animation.TYPE_SCALE_3D)
|
||||
Animation.TYPE_BLEND_SHAPE: continue
|
||||
_: continue
|
||||
|
||||
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))
|
||||
|
||||
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)
|
||||
|
||||
# Apply rest-pose correction to rotation tracks
|
||||
if track_type == Animation.TYPE_ROTATION_3D and corrections.has(bone):
|
||||
var orig_q: Quaternion = v
|
||||
var corr_q: Quaternion = corrections[bone]
|
||||
v = corr_q * orig_q
|
||||
|
||||
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:
|
||||
var parts := path.split(":")
|
||||
if parts.size() < 2:
|
||||
return ""
|
||||
return parts[-1]
|
||||
@@ -1 +0,0 @@
|
||||
uid://dgwmo0tdt8dwl
|
||||
@@ -1,20 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
const DASHER_DIR := "res://assets/characters/dashers"
|
||||
|
||||
func _init() -> void:
|
||||
var dir := DirAccess.open(DASHER_DIR)
|
||||
for fname in dir.get_files():
|
||||
if not fname.ends_with(".glb"): continue
|
||||
var doc = GLTFDocument.new()
|
||||
var state = GLTFState.new()
|
||||
doc.append_from_file("%s/%s" % [DASHER_DIR, fname], state)
|
||||
var scene = doc.generate_scene(state)
|
||||
var ap: AnimationPlayer = scene.find_child("AnimationPlayer", true, false)
|
||||
if ap:
|
||||
print("=== %s ===" % fname)
|
||||
for lib_name in ap.get_animation_library_list():
|
||||
var lib = ap.get_animation_library(lib_name)
|
||||
for anim_name in lib.get_animation_list():
|
||||
print(" ", anim_name)
|
||||
quit()
|
||||
@@ -1 +0,0 @@
|
||||
uid://cau1rt1kfa32y
|
||||
@@ -1,32 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var scene = preload("res://scenes/player.tscn").instantiate()
|
||||
var skel: Skeleton3D = null
|
||||
|
||||
# Find GeneralSkeleton anywhere in the player scene
|
||||
var queue = [scene]
|
||||
while queue.size() > 0:
|
||||
var n = queue.pop_front()
|
||||
if n is Skeleton3D and n.name == "GeneralSkeleton":
|
||||
skel = n
|
||||
break
|
||||
for c in n.get_children():
|
||||
queue.append(c)
|
||||
|
||||
if skel:
|
||||
print("Found GeneralSkeleton! Bones:")
|
||||
for i in skel.get_bone_count():
|
||||
print(" - ", skel.get_bone_name(i))
|
||||
else:
|
||||
print("GeneralSkeleton not found in player.tscn!")
|
||||
print("Tree:")
|
||||
_print_tree(scene, 0)
|
||||
|
||||
quit()
|
||||
|
||||
func _print_tree(n: Node, depth: int) -> void:
|
||||
var indent = " ".repeat(depth)
|
||||
print("%s- %s (%s)" % [indent, n.name, n.get_class()])
|
||||
for c in n.get_children():
|
||||
_print_tree(c, depth + 1)
|
||||
@@ -1 +0,0 @@
|
||||
uid://dxxtbdm3usgdp
|
||||
@@ -1,15 +0,0 @@
|
||||
extends SceneTree
|
||||
func _init() -> void:
|
||||
var scene = preload("res://scenes/player.tscn").instantiate()
|
||||
var masbro = scene.get_node("Masbro")
|
||||
if masbro:
|
||||
print("Masbro found! Children:")
|
||||
_print_tree(masbro, 0)
|
||||
else:
|
||||
print("No Masbro")
|
||||
quit()
|
||||
func _print_tree(n: Node, depth: int) -> void:
|
||||
var indent = " ".repeat(depth)
|
||||
print("%s- %s (%s)" % [indent, n.name, n.get_class()])
|
||||
for c in n.get_children():
|
||||
_print_tree(c, depth + 1)
|
||||
@@ -1 +0,0 @@
|
||||
uid://dt8bew0d4r6bp
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rename rig root node in character GLBs to 'Character' for consistent paths.
|
||||
|
||||
This must run before Godot imports the GLBs. It modifies the .glb JSON to rename
|
||||
the rig root node (oldpop-rig, bob-rig, etc.) to 'Character' so all characters
|
||||
share the same skeleton path: Character/Skeleton3D:bone_name
|
||||
|
||||
Backups are created as .glb.bak on first run.
|
||||
|
||||
Re-runnable: if backup exists, restore from backup first to ensure idempotency.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
CHARACTERS = {
|
||||
'Bob.glb': 'bob-rig',
|
||||
'Oldpop.glb': 'oldpop-rig',
|
||||
'Masbro.glb': 'masbro-tpose',
|
||||
'Gatot.glb': 'gatot-tpose',
|
||||
}
|
||||
NEW_NAME = 'Character'
|
||||
CHARS_DIR = os.path.join(os.path.dirname(__file__), '..', 'assets', 'characters')
|
||||
|
||||
|
||||
def find_rig_node(nodes, old_name):
|
||||
for i, n in enumerate(nodes):
|
||||
if n.get('name') == old_name:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def process_glb(path, old_name):
|
||||
backup = path + '.bak'
|
||||
if os.path.exists(backup):
|
||||
shutil.copy(backup, path)
|
||||
else:
|
||||
shutil.copy(path, backup)
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
f.read(12) # magic + version + length
|
||||
chunk_len = struct.unpack('<I', f.read(4))[0]
|
||||
f.read(4) # chunk type
|
||||
json_data = f.read(chunk_len)
|
||||
rest = f.read()
|
||||
|
||||
data = json.loads(json_data)
|
||||
nodes = data.get('nodes', [])
|
||||
|
||||
idx = find_rig_node(nodes, old_name)
|
||||
if idx < 0:
|
||||
print(f' {os.path.basename(path)}: rig root "{old_name}" NOT FOUND, skipping')
|
||||
return False
|
||||
|
||||
nodes[idx]['name'] = NEW_NAME
|
||||
|
||||
new_json = json.dumps(data, separators=(',', ':')).encode('utf-8')
|
||||
new_chunk_len = len(new_json)
|
||||
new_length = 12 + 8 + new_chunk_len + len(rest)
|
||||
|
||||
with open(path, 'wb') as f:
|
||||
f.write(b'glTF')
|
||||
f.write(struct.pack('<I', 2))
|
||||
f.write(struct.pack('<I', new_length))
|
||||
f.write(struct.pack('<I', new_chunk_len))
|
||||
f.write(b'JSON')
|
||||
f.write(new_json)
|
||||
f.write(rest)
|
||||
|
||||
print(f' {os.path.basename(path)}: renamed {old_name} -> {NEW_NAME}')
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
os.chdir(CHARS_DIR)
|
||||
print(f'Processing GLBs in {CHARS_DIR}')
|
||||
for glb, old_name in CHARACTERS.items():
|
||||
path = os.path.join(CHARS_DIR, glb)
|
||||
if not os.path.exists(path):
|
||||
print(f' {glb}: not found, skipping')
|
||||
continue
|
||||
process_glb(path, old_name)
|
||||
print('Done.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,8 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
print("TYPE_VALUE: ", Animation.TYPE_VALUE)
|
||||
print("TYPE_POSITION_3D: ", Animation.TYPE_POSITION_3D)
|
||||
print("TYPE_ROTATION_3D: ", Animation.TYPE_ROTATION_3D)
|
||||
print("TYPE_SCALE_3D: ", Animation.TYPE_SCALE_3D)
|
||||
quit()
|
||||
@@ -1 +0,0 @@
|
||||
uid://dl8svqqgt6m3n
|
||||
@@ -1,33 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var lib = load("res://assets/characters/animations/dasher-pack.res") as AnimationLibrary
|
||||
if not lib:
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var anim_name = "dasher_hit"
|
||||
if lib.has_animation(anim_name):
|
||||
var anim = lib.get_animation(anim_name)
|
||||
print("Animation: ", anim_name)
|
||||
var has_pos = 0
|
||||
var has_rot = 0
|
||||
var has_scale = 0
|
||||
var pos_bones = []
|
||||
|
||||
for i in anim.get_track_count():
|
||||
var type = anim.track_get_type(i)
|
||||
var path = anim.track_get_path(i)
|
||||
if type == Animation.TYPE_POSITION_3D:
|
||||
has_pos += 1
|
||||
pos_bones.append(str(path))
|
||||
elif type == Animation.TYPE_ROTATION_3D:
|
||||
has_rot += 1
|
||||
elif type == Animation.TYPE_SCALE_3D:
|
||||
has_scale += 1
|
||||
|
||||
print(" Positions: ", has_pos, " (", pos_bones, ")")
|
||||
print(" Rotations: ", has_rot)
|
||||
print(" Scales: ", has_scale)
|
||||
|
||||
quit(0)
|
||||
@@ -1 +0,0 @@
|
||||
uid://bleaj3miugqrm
|
||||
@@ -1,13 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var lib = load("res://assets/characters/animations/dasher-pack.res") as AnimationLibrary
|
||||
for anim_name in lib.get_animation_list():
|
||||
var anim = lib.get_animation(anim_name)
|
||||
print("=== %s (%d tracks) ===" % [anim_name, anim.get_track_count()])
|
||||
for i in anim.get_track_count():
|
||||
var type = anim.track_get_type(i)
|
||||
var path = anim.track_get_path(i)
|
||||
var type_str = ["VALUE","POSITION","ROTATION","SCALE"][type]
|
||||
print(" [%d] type=%s path=%s" % [i, type_str, path])
|
||||
quit()
|
||||
@@ -1 +0,0 @@
|
||||
uid://yyhb801dk5ve
|
||||
@@ -1,16 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var lib = load("res://assets/characters/animations/dasher-pack.res") as AnimationLibrary
|
||||
if not lib:
|
||||
print("Failed to load dasher-pack.res")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var anim_name = lib.get_animation_list()[0]
|
||||
var anim = lib.get_animation(anim_name)
|
||||
print("First animation: ", anim_name)
|
||||
for i in min(anim.get_track_count(), 5):
|
||||
print(" Track ", i, " path: ", anim.track_get_path(i))
|
||||
|
||||
quit(0)
|
||||
@@ -1 +0,0 @@
|
||||
uid://ce58ow1re1l24
|
||||
@@ -1,19 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var doc = GLTFDocument.new()
|
||||
var state = GLTFState.new()
|
||||
var err = doc.append_from_file("res://assets/characters/dashers/dasher_hit.glb", state)
|
||||
var scene = doc.generate_scene(state)
|
||||
var ap: AnimationPlayer = scene.find_child("AnimationPlayer", true, false)
|
||||
|
||||
var lib = ap.get_animation_library("")
|
||||
var anim_name = "dasher_take|Hit|Anima_Layer"
|
||||
if lib.has_animation(anim_name):
|
||||
var anim = lib.get_animation(anim_name)
|
||||
print("=== RAW TRACKS ===")
|
||||
for i in anim.get_track_count():
|
||||
var type = anim.track_get_type(i)
|
||||
var path = anim.track_get_path(i)
|
||||
print(" Track ", i, " type: ", type, " path: ", path)
|
||||
quit()
|
||||
@@ -1 +0,0 @@
|
||||
uid://dcoduco766d6w
|
||||
@@ -1,34 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var lib = load("res://assets/characters/animations/animation-pack.res") as AnimationLibrary
|
||||
if not lib:
|
||||
print("Failed to load animation-pack.res")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var anim_name = "walk_forward"
|
||||
if lib.has_animation(anim_name):
|
||||
var anim = lib.get_animation(anim_name)
|
||||
print("Animation: ", anim_name)
|
||||
var has_pos = 0
|
||||
var has_rot = 0
|
||||
var has_scale = 0
|
||||
var pos_bones = []
|
||||
|
||||
for i in anim.get_track_count():
|
||||
var type = anim.track_get_type(i)
|
||||
var path = anim.track_get_path(i)
|
||||
if type == Animation.TYPE_POSITION_3D:
|
||||
has_pos += 1
|
||||
pos_bones.append(str(path))
|
||||
elif type == Animation.TYPE_ROTATION_3D:
|
||||
has_rot += 1
|
||||
elif type == Animation.TYPE_SCALE_3D:
|
||||
has_scale += 1
|
||||
|
||||
print(" Positions: ", has_pos, " (", pos_bones, ")")
|
||||
print(" Rotations: ", has_rot)
|
||||
print(" Scales: ", has_scale)
|
||||
|
||||
quit(0)
|
||||
@@ -1 +0,0 @@
|
||||
uid://c1boh2egfnnfu
|
||||
@@ -1,12 +0,0 @@
|
||||
extends SceneTree
|
||||
|
||||
func _init() -> void:
|
||||
var lib = load("res://assets/characters/animations/animation-pack.res") as AnimationLibrary
|
||||
var anim = lib.get_animation("walk_forward")
|
||||
print("=== walk_forward tracks ===")
|
||||
for i in anim.get_track_count():
|
||||
var type = anim.track_get_type(i)
|
||||
var path = anim.track_get_path(i)
|
||||
var type_str = ["VALUE","POSITION","ROTATION","SCALE"][type]
|
||||
print(" [%d] type=%s path=%s" % [i, type_str, path])
|
||||
quit()
|
||||
@@ -1 +0,0 @@
|
||||
uid://dajp15327baah
|
||||
Reference in New Issue
Block a user