271 lines
8.4 KiB
GDScript
271 lines
8.4 KiB
GDScript
|
|
# 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)
|