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 -- [skin_id] [unarmed] [tag,tag,...] ## Saves anim_.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])