extends SceneTree ## Fixed simulation step — see _lock_timestep. const STEP := 1.0 / 60.0 ## Dev tool: the front of the skirt through a whole run cycle, as a player sees it. ## ## godot --path . --windowed --resolution 1280x960 \ ## -s res://debug/skirt_run_view.gd -- [skin] ## ## Every other check in debug/ looks at this from 0.9 m with flat colours or from ## a solver's point of view. Those answer "is a bone inside a capsule" and "did ## the mesh tear", and both can read clean while the render is obviously wrong — ## a material that does not occlude, an outline shell drawn over the cloth, or ## simply a pose none of the sampled frames happened to catch. ## ## So: the shipped materials, a straight-on front camera at normal viewing ## distance, and EVERY frame of the run cycle rather than eight samples of it. const FIRST := 40 # let the chains settle before recording const FRAMES := 48 # a full stride at 60 fps and then some var _frames := 0 var _out := "." var _model: SkinnedPlayerModel = null var _cam: Camera3D = null var _shots := 0 func _initialize() -> void: var args := OS.get_cmdline_user_args() _out = args[0] if args.size() > 0 else "." var path := "res://assets/characters/skins/taila.glb" if args.size() > 1: path = "res://assets/characters/skins/%s.glb" % args[1] var scene := Node3D.new() root.add_child(scene) current_scene = scene var env := WorldEnvironment.new() var e := Environment.new() e.background_mode = Environment.BG_COLOR e.background_color = Color(0.16, 0.16, 0.2) e.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR e.ambient_light_color = Color(1, 1, 1) e.ambient_light_energy = 1.5 env.environment = e scene.add_child(env) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-35, 25, 0) sun.light_energy = 1.3 scene.add_child(sun) _model = SkinnedPlayerModel.new() _model.model_path = path scene.add_child(_model) _cam = Camera3D.new() _cam.fov = 38.0 scene.add_child(_cam) _cam.current = true ## Drive the animation and the solver on a FIXED timestep. ## ## Both advance on the real frame delta otherwise, so the pose at a given frame ## drifts between runs and the same build measured 146k and 398k offending ## pixels. Every A/B comparison made without this was noise, and several tuning ## decisions were taken on the strength of it. ## ## The modifier stack needs pinning too: on PHYSICS it runs a variable number ## of times per rendered frame, so with a fixed step the amount of simulated ## time per frame still wandered. func _lock_timestep() -> void: var skel: Skeleton3D = _model.skeleton if skel: skel.modifier_callback_mode_process = \ Skeleton3D.MODIFIER_CALLBACK_MODE_PROCESS_IDLE var spring := skel.get_node_or_null("SpringBones") if spring: spring.fixed_delta = STEP for n in _model.find_children("*", "AnimationTree", true, false): n.callback_mode_process = AnimationMixer.ANIMATION_CALLBACK_MODE_PROCESS_MANUAL ## One fixed step of the animation. Call once per rendered frame. func _step_anim() -> void: for n in _model.find_children("*", "AnimationTree", true, false): n.advance(STEP) func _process(_delta: float) -> bool: _frames += 1 if _frames < 10 or not _model.loaded: return false if _frames == 10: _lock_timestep() # `nospring` renders the same cycle with the cloth solver removed, so a # change can be told from no change at all. var a := OS.get_cmdline_user_args() if a.size() > 2 and String(a[2]) == "nospring": var sp := _model.skeleton.get_node_or_null("SpringBones") if sp: sp.queue_free() print("spring solver REMOVED") _model.update_state("ground", 9.0, false) _model.set_locomotion(0.0, 1.0, 0.0) _step_anim() var hips := 0.95 if _model.skeleton: var h := _model.skeleton.find_bone("DEF-spine") if h >= 0: hips = _model.skeleton.get_bone_global_pose(h).origin.y # Waist to knee, dead in front, from about where a third-person camera sits. # # NEGATIVE Z. SkinnedPlayerModel spins the imported scene 180 degrees # (`facing_flip`: glTF forward is +Z, players face -Z), so a camera on +Z is # looking at the character's BACK. Every earlier tool in here sat on +Z and # every "front" render judged from them was the back of the skirt. _cam.position = Vector3(0.0, hips - 0.05, -1.5) _cam.look_at(Vector3(0, hips - 0.20, 0), Vector3.UP) if _frames >= FIRST and _shots < FRAMES: root.get_texture().get_image().save_png( "%s/run_%02d.png" % [_out, _shots]) _shots += 1 if _shots == FRAMES: print("saved %d run frames" % FRAMES) return true return _frames > FIRST + FRAMES * 3