# Animation control test: V15 male (Tripo mesh, Meshy rig renamed to Quaternius # bones) driven by REAL UAL1 clips baked for this skeleton via # ariki-game/tools/mixamo_retarget.py --map ual (world-rotation-delta retarget). # Controls: same as anim_male.gd (see HUD). extends Node3D const DIR := "C:/Users/CAN/tinqs-ltd/tattoo-test/male/" const BODY_GLB := DIR + "male-lena-v15-quatnamed.glb" const CLIPS_GLB := DIR + "male-ual-clips.glb" const PREFERRED_ORDER := ["Idle_Loop", "Walk_Loop", "Jog_Fwd_Loop", "Sprint_Loop", "Dance_Loop", "Punch_Jab"] var model: Node3D var skel: Skeleton3D var cam: Camera3D var player: AnimationPlayer var hud: Label var clips: Array[String] = [] var clip_idx := 0 var lights: Array[Light3D] = [] var env: Environment var lights_on := true var interactive := false var dragging := false var orbit_yaw := 0.0 var orbit_pitch := 0.08 var orbit_dist := 3.6 var orbit_target := Vector3(0, 0.95, 0) func _ready() -> void: env = Environment.new() env.background_mode = Environment.BG_COLOR env.background_color = Color(0.12, 0.13, 0.16) env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(0.75, 0.78, 0.85) env.ambient_light_energy = 0.7 env.tonemap_mode = Environment.TONE_MAPPER_FILMIC var we := WorldEnvironment.new() we.environment = env add_child(we) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-40, -35, 0) sun.light_energy = 1.5 add_child(sun) var fill := DirectionalLight3D.new() fill.rotation_degrees = Vector3(-15, 140, 0) fill.light_energy = 0.6 add_child(fill) var under := DirectionalLight3D.new() under.rotation_degrees = Vector3(35, 0, 0) under.light_energy = 0.35 add_child(under) lights = [sun, fill, under] var ground := MeshInstance3D.new() var plane := PlaneMesh.new() plane.size = Vector2(12, 12) ground.mesh = plane var gmat := StandardMaterial3D.new() gmat.albedo_color = Color(0.16, 0.17, 0.21) gmat.roughness = 1.0 ground.set_surface_override_material(0, gmat) add_child(ground) model = _load_glb(BODY_GLB) if model == null: return add_child(model) var stack: Array = [model] while not stack.is_empty(): var n: Node = stack.pop_back() if n is Skeleton3D and skel == null: skel = n for c in n.get_children(): stack.push_back(c) if skel == null: push_error("no skeleton in body GLB") return print("INFO bones=", skel.get_bone_count()) # lift baked UAL clips onto a local player bound to this skeleton var clip_scene := _load_glb(CLIPS_GLB) if clip_scene == null: return var src := _find_player(clip_scene) if src == null: push_error("no AnimationPlayer in clips GLB") return player = AnimationPlayer.new() add_child(player) var skel_path := String(get_path_to(skel)) # The Meshy body skeleton is authored in cm (armature node carries a 0.01 # scale); the baked clips are in meters. Rotations are unit-free but # position keys must be rescaled into the body's bone space. var pos_scale := 1.0 var cskel: Skeleton3D = null var cstack: Array = [clip_scene] while not cstack.is_empty(): var cn: Node = cstack.pop_back() if cn is Skeleton3D: cskel = cn break for cc in cn.get_children(): cstack.push_back(cc) if cskel != null: var body_y := skel.get_bone_global_rest(skel.find_bone("pelvis")).origin.y var clip_y := cskel.get_bone_global_rest(cskel.find_bone("pelvis")).origin.y if clip_y > 0.001: pos_scale = body_y / clip_y print("INFO pos_scale=", pos_scale) var lib := AnimationLibrary.new() for clip_name in src.get_animation_list(): var anim := src.get_animation(clip_name).duplicate() as Animation anim.loop_mode = Animation.LOOP_LINEAR for i in anim.get_track_count(): var path := String(anim.track_get_path(i)) var bone := path.get_slice(":", 1) anim.track_set_path(i, skel_path + ":" + bone) if anim.track_get_type(i) == Animation.TYPE_POSITION_3D: for k in anim.track_get_key_count(i): anim.track_set_key_value(i, k, anim.track_get_key_value(i, k) * pos_scale) lib.add_animation(clip_name, anim) player.add_animation_library("", lib) clip_scene.queue_free() clips.assign(Array(lib.get_animation_list())) clips.sort_custom(func(a, b): return _clip_rank(a) < _clip_rank(b)) print("clips: ", ",".join(clips)) if clips.is_empty(): push_error("no clips loaded") return cam = Camera3D.new() cam.fov = 50 add_child(cam) cam.current = true hud = Label.new() hud.position = Vector2(16, 12) hud.add_theme_font_size_override("font_size", 22) hud.add_theme_color_override("font_color", Color(0.95, 0.95, 0.9)) add_child(hud) _play_clip(0) _update_cam() # UAL_SHOTS=1: save mid-clip verification frames before going interactive if OS.get_environment("UAL_SHOTS") == "1": DirAccess.make_dir_recursive_absolute(DIR + "shots/") for shot_clip in ["Idle_Loop", "Walk_Loop", "Jog_Fwd_Loop", "Dance_Loop"]: var i := clips.find(shot_clip) if i < 0: continue _play_clip(i) await get_tree().create_timer(0.6).timeout await RenderingServer.frame_post_draw var img := get_viewport().get_texture().get_image() img.save_png(DIR + "shots/ual_" + shot_clip + ".png") print("shot saved: ", DIR + "shots/ual_" + shot_clip + ".png") print("SHOTS_DONE") _play_clip(0) interactive = true func _clip_rank(n: String) -> int: var i := PREFERRED_ORDER.find(n) return i if i >= 0 else 99 func _load_glb(path: String) -> Node3D: var doc := GLTFDocument.new() var state := GLTFState.new() if doc.append_from_file(path, state) != OK: push_error("GLB load failed: " + path) return null return doc.generate_scene(state) func _find_player(root: Node) -> AnimationPlayer: var stack: Array = [root] while not stack.is_empty(): var n: Node = stack.pop_back() if n is AnimationPlayer: return n for c in n.get_children(): stack.push_back(c) return null func _play_clip(idx: int) -> void: if clips.is_empty(): return clip_idx = wrapi(idx, 0, clips.size()) player.play(clips[clip_idx]) _update_hud() func _set_lights(on: bool) -> void: lights_on = on for l in lights: l.visible = on env.ambient_light_energy = 0.7 if on else 1.6 _update_hud() func _update_hud() -> void: if hud == null or clips.is_empty(): return var clip: String = clips[clip_idx] var anim := player.get_animation(clip) hud.text = "UAL on Quat-named Meshy rig [%d/%d] %s %.2fs / %.2fs speed %.2fx light: %s %s\n←/→ clip Space pause ,/. step +/- speed R reset U light drag rotate scroll zoom" \ % [clip_idx + 1, clips.size(), clip, player.current_animation_position, anim.length, player.speed_scale, "SUN" if lights_on else "FLAT", "PLAYING" if player.is_playing() else "PAUSED"] func _process(_dt: float) -> void: if interactive and player.is_playing(): _update_hud() func _step_frame(dir: int) -> void: if player.is_playing(): player.pause() var anim := player.get_animation(clips[clip_idx]) var t := fposmod(player.current_animation_position + dir / 30.0, anim.length) player.seek(t, true) _update_hud() func _unhandled_input(event: InputEvent) -> void: if not interactive: return if event is InputEventKey and event.pressed and not event.echo: match event.keycode: KEY_RIGHT, KEY_D: _play_clip(clip_idx + 1) KEY_LEFT, KEY_A: _play_clip(clip_idx - 1) KEY_SPACE: if player.is_playing(): player.pause() else: player.play() _update_hud() KEY_COMMA: _step_frame(-1) KEY_PERIOD: _step_frame(1) KEY_EQUAL, KEY_KP_ADD: player.speed_scale = clampf(player.speed_scale * 1.25, 0.1, 4.0) _update_hud() KEY_MINUS, KEY_KP_SUBTRACT: player.speed_scale = clampf(player.speed_scale / 1.25, 0.1, 4.0) _update_hud() KEY_R: player.speed_scale = 1.0 _update_hud() KEY_U: _set_lights(not lights_on) elif event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT: dragging = event.pressed elif event.pressed and event.button_index == MOUSE_BUTTON_WHEEL_UP: orbit_dist = clampf(orbit_dist * 0.90, 0.35, 10.0) _update_cam() elif event.pressed and event.button_index == MOUSE_BUTTON_WHEEL_DOWN: orbit_dist = clampf(orbit_dist / 0.90, 0.35, 10.0) _update_cam() elif event is InputEventMouseMotion and dragging: orbit_yaw -= event.relative.x * 0.008 orbit_pitch = clampf(orbit_pitch + event.relative.y * 0.008, -1.3, 1.3) _update_cam() func _update_cam() -> void: var dir := Vector3( sin(orbit_yaw) * cos(orbit_pitch), sin(orbit_pitch), cos(orbit_yaw) * cos(orbit_pitch)) cam.look_at_from_position(orbit_target + dir * orbit_dist, orbit_target)