271 lines
9.5 KiB
GDScript
271 lines
9.5 KiB
GDScript
extends SceneTree
|
|
|
|
## Dev tool: screenshot every animation state on the active GLB skin.
|
|
## Run:
|
|
## godot --path . --windowed --resolution 1280x720 -s res://debug/anim_capture.gd -- <out_dir> [skin_id] [unarmed] [tag,tag,...]
|
|
## Saves anim_<state>.png per state plus mid-action shots for reload/throw.
|
|
|
|
var _frames := 0
|
|
var _out_dir := "."
|
|
var _skin := "miku"
|
|
var _unarmed := false
|
|
var _player: Node = null
|
|
var _model: Node = null
|
|
var _cam: Camera3D = null
|
|
|
|
# [tag, state, speed, crouch, hold_frames, strafe, forward, emote_index]
|
|
var _states := [
|
|
["idle", "idle", 0.0, false, 50, 0.0, 0.0, -1],
|
|
["walk", "ground", 0.975, false, 50, 0.0, 1.0, -1],
|
|
["run", "ground", 5.26148, false, 50, 0.0, 1.0, -1],
|
|
["sprint", "ground", 8.25, false, 50, 0.0, 1.0, -1],
|
|
["sprint_game_speed", "ground", 12.0, false, 50, 0.0, 1.0, -1],
|
|
["strafe_walk_back", "ground", 3.0, false, 50, 0.0, -1.0, -1],
|
|
["strafe_walk_left", "ground", 3.0, false, 50, -1.0, 0.0, -1],
|
|
["strafe_walk_right", "ground", 3.0, false, 50, 1.0, 0.0, -1],
|
|
["run_back", "ground", 8.25, false, 50, 0.0, -1.0, -1],
|
|
["run_left", "ground", 8.25, false, 50, -1.0, 0.0, -1],
|
|
["run_right", "ground", 8.25, false, 50, 1.0, 0.0, -1],
|
|
["run_left_game_speed", "ground", 11.0, false, 50, -1.0, 0.0, -1],
|
|
["run_right_game_speed", "ground", 11.0, false, 50, 1.0, 0.0, -1],
|
|
["crouch", "ground", 0.0, true, 50, 0.0, 0.0, -1],
|
|
["crouchwalk", "ground", 0.75, true, 50, 0.0, 1.0, -1],
|
|
["fall", "air", 4.0, false, 50, 0.0, 1.0, -1],
|
|
["slide", "slide", 10.0, true, 50, 0.0, 1.0, -1],
|
|
["dash", "dash", 14.0, false, 30, 0.0, 1.0, -1],
|
|
["wallrun", "wall_run", 13.0, false, 50, 0.0, 1.0, -1],
|
|
["wallcling", "wall_cling", 0.0, false, 50, 0.0, 0.0, -1],
|
|
["wallclimb", "wall_climb", 2.0, false, 50, 0.0, 1.0, -1],
|
|
["grapple", "grapple", 10.0, false, 50, 0.0, 1.0, -1],
|
|
["emote_dance", "idle", 0.0, false, 60, 0.0, 0.0, 0],
|
|
["emote_stretch", "idle", 0.0, false, 60, 0.0, 0.0, 1],
|
|
["emote_call", "idle", 0.0, false, 60, 0.0, 0.0, 2],
|
|
["emote_yes", "idle", 0.0, false, 60, 0.0, 0.0, 3],
|
|
["emote_no", "idle", 0.0, false, 60, 0.0, 0.0, 4],
|
|
]
|
|
var _phase := 0 # index into _states, then actions after
|
|
var _phase_frame := 0
|
|
var _actions := [["reload", 30], ["throw", 18]]
|
|
var _action_i := 0
|
|
var _mode := "states" # states -> actions -> ads -> done
|
|
|
|
|
|
func _initialize() -> void:
|
|
var args := OS.get_cmdline_user_args()
|
|
if args.size() > 0:
|
|
_out_dir = args[0]
|
|
if args.size() > 1:
|
|
_skin = args[1]
|
|
if args.size() > 2:
|
|
_unarmed = args[2].to_lower() == "unarmed"
|
|
if args.size() > 3 and args[3] != "":
|
|
var wanted := args[3].split(",", false)
|
|
_states = _states.filter(func(state: Array) -> bool:
|
|
return String(state[0]) in wanted)
|
|
if not _out_dir.is_absolute_path():
|
|
_out_dir = ProjectSettings.globalize_path(_out_dir)
|
|
var mkdir_error := DirAccess.make_dir_recursive_absolute(_out_dir)
|
|
if mkdir_error != OK:
|
|
printerr("anim_capture: could not create '%s' (error %d)" % [
|
|
_out_dir, mkdir_error])
|
|
quit(mkdir_error)
|
|
return
|
|
change_scene_to_file("res://ui/main_menu/main_menu.tscn")
|
|
|
|
|
|
func _process(_delta: float) -> bool:
|
|
_frames += 1
|
|
if _frames == 40:
|
|
var sm = root.get_node_or_null("SkinManager")
|
|
if sm:
|
|
sm.set_active_skin(_skin)
|
|
var nm = root.get_node_or_null("NetworkManager")
|
|
if nm and nm.has_method("start_singleplayer_match"):
|
|
nm.start_singleplayer_match(GameMode.DEATHMATCH)
|
|
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
|
|
return false
|
|
if _frames < 160:
|
|
return false
|
|
if _frames == 160:
|
|
for p in root.find_children("*", "CharacterBody3D", true, false):
|
|
if p.has_method("get_visual_model") and p.is_multiplayer_authority():
|
|
_player = p
|
|
break
|
|
if not _player:
|
|
printerr("anim_capture: no player found")
|
|
return true
|
|
_model = _player.get_node_or_null("SkinnedModel")
|
|
if not _model:
|
|
printerr("anim_capture: no SkinnedModel (skin '%s' active?)" % _skin)
|
|
return true
|
|
# Freeze gameplay driving so we control the animation state directly,
|
|
# and teleport to open ground so walls never block the shot.
|
|
_player.set_physics_process(false)
|
|
_player.set_process(false)
|
|
_player.global_position = Vector3(0, 1.2, 14)
|
|
_player.rotation = Vector3.ZERO
|
|
_model.set_owner_visible(true)
|
|
# Optional unarmed pass exposes the authored arm silhouettes used by
|
|
# idle/dance/emotes instead of covering them with weapon IK.
|
|
if _model.has_method("set_weapon"):
|
|
_model.set_weapon("" if _unarmed else "res://weapons/ak47.gd")
|
|
# Camera: repositioned per shot (front + side). Model faces -Z.
|
|
_cam = Camera3D.new()
|
|
_cam.cull_mask &= ~(1 << 19) # don't render the FP viewmodel layer
|
|
current_scene.add_child(_cam)
|
|
_cam_to(Vector3(0.4, 0.9, -2.4))
|
|
_cam.current = true
|
|
return false
|
|
|
|
# Drive the forced state every frame so blends settle. Front shot two
|
|
# frames before the hold ends, side shot on the last frame (the camera
|
|
# hop needs a real rendered frame in between).
|
|
if _mode == "states":
|
|
var s: Array = _states[_phase]
|
|
if _model.has_method("set_locomotion"):
|
|
_model.set_locomotion(float(s[5]), float(s[6]), 0.0)
|
|
if s[0] == "wallrun" and _model.has_method("set_wall_side"):
|
|
_model.set_wall_side(-1.0)
|
|
if _model.has_method("set_wall_run_motion"):
|
|
_model.set_wall_run_motion(Vector3(13.0, 0.0, 0.0))
|
|
if int(s[7]) >= 0 and _model.has_method("set_dancing"):
|
|
_model.set_dancing(true, int(s[7]))
|
|
_model.update_state(s[1], s[2], s[3])
|
|
_phase_frame += 1
|
|
var hold: int = int(s[4])
|
|
if _phase_frame == hold - 2:
|
|
_snap("anim_" + String(s[0]) + "_f")
|
|
_cam_to(Vector3(2.4, 0.9, -0.4))
|
|
elif _phase_frame >= hold:
|
|
_snap("anim_" + String(s[0]) + "_s")
|
|
_cam_to(Vector3(0.4, 0.9, -2.4))
|
|
if int(s[7]) >= 0 and _model.has_method("set_dancing"):
|
|
_model.set_dancing(false)
|
|
if s[0] == "idle":
|
|
_debug_gun()
|
|
_phase_frame = 0
|
|
_phase += 1
|
|
if _phase >= _states.size():
|
|
_mode = "actions"
|
|
return false
|
|
if _mode == "actions":
|
|
var a: Array = _actions[_action_i]
|
|
if _phase_frame == 0:
|
|
_model.play_action(a[0])
|
|
_model.update_state("idle", 0.0, false)
|
|
_phase_frame += 1
|
|
var hold_a: int = int(a[1])
|
|
if _phase_frame == hold_a - 2:
|
|
_snap("anim_" + String(a[0]) + "_f")
|
|
_cam_to(Vector3(2.4, 0.9, -0.4))
|
|
elif _phase_frame >= hold_a:
|
|
_snap("anim_" + String(a[0]) + "_s")
|
|
_cam_to(Vector3(0.4, 0.9, -2.4))
|
|
_phase_frame = 0
|
|
_action_i += 1
|
|
if _action_i >= _actions.size():
|
|
_mode = "ads"
|
|
return false
|
|
if _mode == "ads":
|
|
_model.update_state("idle", 0.0, false)
|
|
if _model.has_method("set_locomotion"):
|
|
_model.set_locomotion(0.0, 0.0, 1.0)
|
|
_phase_frame += 1
|
|
if _phase_frame == 108:
|
|
_snap("anim_ads_f")
|
|
_cam_to(Vector3(2.4, 0.9, -0.4))
|
|
elif _phase_frame >= 110:
|
|
_snap("anim_ads_s")
|
|
_debug_gun()
|
|
_phase_frame = 0
|
|
_mode = "recoil"
|
|
return false
|
|
# Sustained fire: the shot kick must visibly move the third-person model.
|
|
if _mode == "recoil":
|
|
_model.update_state("idle", 0.0, false)
|
|
if _model.has_method("set_locomotion"):
|
|
_model.set_locomotion(0.0, 0.0, 0.0)
|
|
_phase_frame += 1
|
|
if _phase_frame == 40:
|
|
_snap("anim_recoil_before_f")
|
|
elif _phase_frame == 41:
|
|
_model.add_gun_recoil() # a single shot
|
|
elif _phase_frame == 42:
|
|
_snap("anim_recoil_peak_f")
|
|
_cam_to(Vector3(2.4, 0.9, -0.4))
|
|
elif _phase_frame == 44:
|
|
_model.add_gun_recoil()
|
|
_snap("anim_recoil_peak_s")
|
|
_cam_to(Vector3(0.4, 0.9, -2.4))
|
|
_phase_frame = 0
|
|
_mode = "combo"
|
|
return false
|
|
# Mechanic-fidelity combos: reload WHILE sliding (legs must keep sliding),
|
|
# then the grapple zip pose toward an actual anchor point.
|
|
if _mode == "combo":
|
|
if _model.has_method("set_locomotion"):
|
|
_model.set_locomotion(0.0, 0.0, 0.0)
|
|
_model.update_state("slide", 10.0, true)
|
|
if _phase_frame == 10:
|
|
_model.play_action("reload")
|
|
_phase_frame += 1
|
|
if _phase_frame == 40:
|
|
_snap("anim_slide_reload_f")
|
|
_cam_to(Vector3(2.4, 0.9, -0.4))
|
|
elif _phase_frame >= 42:
|
|
_snap("anim_slide_reload_s")
|
|
_phase_frame = 0
|
|
_mode = "grapple_zip"
|
|
return false
|
|
if _mode == "grapple_zip":
|
|
if _phase_frame == 0 and _model._anim_tree:
|
|
# End the reload one-shot so the zip pose is unpolluted.
|
|
_model._anim_tree.set("parameters/upper/request",
|
|
AnimationNodeOneShot.ONE_SHOT_REQUEST_ABORT)
|
|
_model.update_state("grapple", 12.0, false)
|
|
if _model.has_method("set_grapple_target"):
|
|
# Anchor up and ahead of the player (player faces -Z world).
|
|
_model.set_grapple_target(
|
|
_player.global_position + Vector3(1.5, 7.0, -7.0),
|
|
Vector3(14.0, 2.0, -4.0),
|
|
)
|
|
_phase_frame += 1
|
|
if _phase_frame == 48:
|
|
_snap("anim_grapple_zip_f")
|
|
_cam_to(Vector3(2.4, 0.9, -0.4))
|
|
elif _phase_frame >= 50:
|
|
_snap("anim_grapple_zip_s")
|
|
return true
|
|
return false
|
|
return true
|
|
|
|
|
|
func _debug_gun() -> void:
|
|
var pm = _model.get("_pose_mod")
|
|
if not pm:
|
|
return
|
|
var skel: Skeleton3D = _model.skeleton
|
|
var hand := skel.find_bone("DEF-hand.R")
|
|
var achieved := Vector3.ZERO
|
|
if hand >= 0 and pm.gun_fwd_hand.length_squared() > 0.01:
|
|
achieved = skel.get_bone_global_pose(hand).basis * pm.gun_fwd_hand
|
|
print("GUN DEBUG: fwd_hand=", pm.gun_fwd_hand, " ads=", pm.ads,
|
|
" hold_r=", pm._hold_r, " achieved_world_fwd=", achieved)
|
|
|
|
|
|
func _cam_to(offset: Vector3) -> void:
|
|
var base: Vector3 = _player.global_position
|
|
_cam.global_position = base + offset
|
|
_cam.look_at(base + Vector3(0, 0.25, 0), Vector3.UP)
|
|
|
|
|
|
func _snap(tag: String) -> void:
|
|
var img := root.get_viewport().get_texture().get_image()
|
|
var path := _out_dir + "/" + tag + ".png"
|
|
var save_error := img.save_png(path)
|
|
if save_error == OK:
|
|
print("anim_capture: saved ", path)
|
|
else:
|
|
printerr("anim_capture: could not save '%s' (error %d)" % [
|
|
path, save_error])
|