Files
tattoo-test/anim_male.gd
T

249 lines
7.6 KiB
GDScript
Raw Normal View History

# Animation control test: Meshy-rigged male Lena (Tripo mesh from V15).
# Merges the walk/run clips from the separate Meshy GLBs onto one player.
# Controls:
# ←/→ or A/D previous / next animation
# Space pause / resume
# ,/. step one frame back / forward (while paused)
# +/- playback speed up / down R reset speed
# U lighting ON / FLAT
# left-drag orbit camera, scroll wheel zooms
extends Node3D
const DIR := "C:/Users/CAN/tinqs-ltd/tattoo-test/male/"
const BASE_GLB := DIR + "male-lena-v15-walking.glb"
# extra clips pulled from sibling GLBs (same Meshy rig): file -> clip name
const EXTRA_CLIPS := {
"male-lena-v15-running.glb": "Run",
"male-lena-v15-rigged.glb": "TPose",
}
const BASE_CLIP_NAME := "Walk"
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(BASE_GLB)
if model == null:
return
add_child(model)
var src_player: AnimationPlayer = null
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 AnimationPlayer and src_player == null:
src_player = n
for c in n.get_children():
stack.push_back(c)
if skel == null or src_player == null:
push_error("base GLB missing skeleton or AnimationPlayer")
return
# one flat library on a fresh player: base clip + clips lifted from siblings
player = AnimationPlayer.new()
add_child(player)
var lib := AnimationLibrary.new()
var base_anim := src_player.get_animation(src_player.get_animation_list()[0]).duplicate() as Animation
_adopt_clip(base_anim, lib, BASE_CLIP_NAME)
src_player.queue_free()
for file in EXTRA_CLIPS:
var extra := _load_glb(DIR + file)
if extra == null:
continue
var p := _find_player(extra)
if p != null and p.get_animation_list().size() > 0:
var anim := p.get_animation(p.get_animation_list()[0]).duplicate() as Animation
_adopt_clip(anim, lib, EXTRA_CLIPS[file])
extra.queue_free()
player.add_animation_library("", lib)
clips.assign(Array(lib.get_animation_list()))
clips.sort_custom(func(a, b): return _clip_rank(a) < _clip_rank(b))
print("clips: ", ",".join(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()
interactive = true
func _clip_rank(n: String) -> int:
return {"TPose": 0, "Walk": 1, "Run": 2}.get(n, 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
# retarget a clip's skeleton tracks onto this scene's skeleton and add it.
# All Meshy exports share the same rig, so only the node-path prefix differs.
func _adopt_clip(anim: Animation, lib: AnimationLibrary, clip_name: String) -> void:
var skel_path := String(get_path_to(skel))
anim.loop_mode = Animation.LOOP_LINEAR
for i in anim.get_track_count():
var path := String(anim.track_get_path(i))
if path.contains("Skeleton3D"):
var bone := path.get_slice(":", 1)
anim.track_set_path(i, skel_path + ":" + bone)
lib.add_animation(clip_name, anim)
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 = "[%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)