# Animation test: the tattooed QuatSkin Lena driven by UAL1 clips. # Controls: # ←/→ or A/D previous / next animation # Space pause / resume # U lighting ON / FLAT (kills the sun+fill lights, boosts ambient) # left-drag orbit camera, scroll wheel zooms # Saves a few mid-animation shots to shots-anim-lena/ first, then goes interactive. extends Node3D const GLB_PATH := "C:/Users/CAN/tinqs-ltd/tattoo-test/lena_clean_uv.glb" const BAKED_PATH := "C:/Users/CAN/tinqs-ltd/tattoo-test/baked_lena_body.png" const UAL1 := "C:/Users/CAN/tinqs-ltd/ariki-game/assets/quaternius/anim/UAL1.glb" const SHOT_DIR := "C:/Users/CAN/tinqs-ltd/tattoo-test/shots-anim-lena/" const TAKE_SHOTS := false # gallery order: locomotion first, then actions const CLIPS := [ "Idle_Loop", "Walk_Loop", "Jog_Fwd_Loop", "Sprint_Loop", "Crouch_Idle_Loop", "Crouch_Fwd_Loop", "Jump_Loop", "Swim_Idle_Loop", "Swim_Fwd_Loop", "Dance_Loop", "Punch_Jab", "Punch_Cross", "Roll", "Interact", "Fixing_Kneeling", "Sitting_Idle_Loop", "Spell_Simple_Shoot", "Death01", ] var model: Node3D var skel: Skeleton3D var cam: Camera3D var player: AnimationPlayer var hud: Label 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.4 var orbit_target := Vector3(0, 1.0, 0) func _ready() -> void: DirAccess.make_dir_recursive_absolute(SHOT_DIR) 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] # ground plane so locomotion reads against something 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) var doc := GLTFDocument.new() var state := GLTFState.new() if doc.append_from_file(GLB_PATH, state) != OK: push_error("GLB load failed") return model = doc.generate_scene(state) add_child(model) var baked := ImageTexture.create_from_image(Image.load_from_file(BAKED_PATH)) var stack: Array = [model] while not stack.is_empty(): var n: Node = stack.pop_back() if n is Skeleton3D and skel == null: skel = n if n is MeshInstance3D and String(n.name).to_lower().contains("lena"): var mat := StandardMaterial3D.new() mat.roughness = 0.85 mat.albedo_texture = baked mat.cull_mode = BaseMaterial3D.CULL_BACK for s in n.mesh.get_surface_count(): n.set_surface_override_material(s, mat) for c in n.get_children(): stack.push_back(c) _load_ual_clips() 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() if TAKE_SHOTS: await _take_shots() print("SHOTS_DONE") _play_clip(0) interactive = true # pulls the wanted UAL1 clips onto a local AnimationPlayer, remapping the # "Armature/Skeleton3D" track prefix to this scene's skeleton (game approach, # see PlayerController.LoadUalLibrary) func _load_ual_clips() -> void: var doc := GLTFDocument.new() var state := GLTFState.new() if doc.append_from_file(UAL1, state) != OK: push_error("UAL1 load failed") return var ual := doc.generate_scene(state) var src: AnimationPlayer = null var stack: Array = [ual] while not stack.is_empty(): var n: Node = stack.pop_back() if n is AnimationPlayer: src = n break for c in n.get_children(): stack.push_back(c) if src == null: push_error("no AnimationPlayer in UAL1") return player = AnimationPlayer.new() add_child(player) var skel_path := String(get_path_to(skel)) var lib := AnimationLibrary.new() for clip_name in CLIPS: if not src.has_animation(clip_name): push_warning("UAL1 missing clip: " + clip_name) continue 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)) if path.contains("Armature/Skeleton3D"): anim.track_set_path(i, path.replace("Armature/Skeleton3D", skel_path)) lib.add_animation(clip_name, anim) player.add_animation_library("", lib) print("loaded %d clips" % lib.get_animation_list().size()) ual.queue_free() func _play_clip(idx: int) -> void: clip_idx = wrapi(idx, 0, CLIPS.size()) var clip: String = CLIPS[clip_idx] if player and player.has_animation(clip): player.play(clip) _update_hud() func _set_lights(on: bool) -> void: lights_on = on for l in lights: l.visible = on # flat mode: strong even ambient so the texture reads unshaded env.ambient_light_energy = 0.7 if on else 1.6 _update_hud() func _update_hud() -> void: if hud: hud.text = "[%d/%d] %s light: %s\n←/→ switch Space pause U light drag rotate scroll zoom" \ % [clip_idx + 1, CLIPS.size(), CLIPS[clip_idx], "SUN" if lights_on else "FLAT"] 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() 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, 8.0) _update_cam() elif event.pressed and event.button_index == MOUSE_BUTTON_WHEEL_DOWN: orbit_dist = clampf(orbit_dist / 0.90, 0.35, 8.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) func _snap(shot_name: String) -> void: await RenderingServer.frame_post_draw var img := get_viewport().get_texture().get_image() img.save_png(SHOT_DIR + shot_name + ".png") print("shot saved: ", SHOT_DIR + shot_name + ".png") func _take_shots() -> void: for spec in [["Jog_Fwd_Loop", 0.4], ["Dance_Loop", 1.2], ["Punch_Jab", 0.5], ["Interact", 0.6]]: var clip: String = spec[0] if not player.has_animation(clip): continue player.play(clip) await get_tree().create_timer(spec[1]).timeout await _snap("anim_" + clip.to_lower()) # verify the U-key light toggle path: one flat-lit shot, then restore _set_lights(false) await _snap("anim_light_flat") _set_lights(true)