male(v15): Tripo mesh + Meshy rig, UAL retarget test beds
- male/: raw + z-facing + quat-named bodies, Meshy rigged GLB/FBX, stock walk/run clips, male-ual-clips.glb (16 UAL1 clips baked via ariki-game mixamo_retarget.py --map ual) - anim_male.tscn: Meshy stock clips; anim_male_ual.tscn: UAL clips (scales position keys x100 — Meshy skeleton is cm, clips are m) - view_tripo.tscn: env-var GLB turntable/anim shots + interactive orbit - rotate_glb.gd: headless face-+Z/height fixup (Meshy pose estimation rejects Tripo's +X-facing normalized output) - Meshy spine chain is reverse-named: Spine02 = lowest vertebra Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+248
@@ -0,0 +1,248 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://anim_male_scene"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://anim_male.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="AnimMale" type="Node3D"]
|
||||||
|
script = ExtResource("1")
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://anim_male_ual_scene"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://anim_male_ual.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="AnimMaleUal" type="Node3D"]
|
||||||
|
script = ExtResource("1")
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,50 @@
|
|||||||
|
# Headless probe: compare the male body's pelvis rest height with the pelvis
|
||||||
|
# position keys in the baked UAL clips GLB.
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const DIR := "C:/Users/CAN/tinqs-ltd/tattoo-test/male/"
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
var body := _load(DIR + "male-lena-v15-quatnamed.glb")
|
||||||
|
var skel: Skeleton3D = _find(body, "Skeleton3D")
|
||||||
|
print("body bones=", skel.get_bone_count())
|
||||||
|
for i in [0, 1]:
|
||||||
|
print("bone ", i, " '", skel.get_bone_name(i), "' rest=", skel.get_bone_rest(i).origin,
|
||||||
|
" parent=", skel.get_bone_parent(i))
|
||||||
|
var pelvis := skel.find_bone("pelvis")
|
||||||
|
print("pelvis idx=", pelvis, " global_rest=", skel.get_bone_global_rest(pelvis).origin)
|
||||||
|
|
||||||
|
var clips := _load(DIR + "male-ual-clips.glb")
|
||||||
|
var ap: AnimationPlayer = _find(clips, "AnimationPlayer")
|
||||||
|
var anim := ap.get_animation("Idle_Loop")
|
||||||
|
for t in anim.get_track_count():
|
||||||
|
var path := String(anim.track_get_path(t))
|
||||||
|
if path.ends_with(":pelvis"):
|
||||||
|
var kind := anim.track_get_type(t)
|
||||||
|
if kind == Animation.TYPE_POSITION_3D:
|
||||||
|
print("POS track ", path, " key0=", anim.track_get_key_value(t, 0),
|
||||||
|
" keyN=", anim.track_get_key_value(t, anim.track_get_key_count(t) - 1))
|
||||||
|
elif kind == Animation.TYPE_ROTATION_3D:
|
||||||
|
print("ROT track ", path, " key0=", anim.track_get_key_value(t, 0))
|
||||||
|
# also inspect the clips GLB's own skeleton rest (exported armature)
|
||||||
|
var cskel: Skeleton3D = _find(clips, "Skeleton3D")
|
||||||
|
if cskel:
|
||||||
|
var cp := cskel.find_bone("pelvis")
|
||||||
|
print("clipsGLB pelvis rest=", cskel.get_bone_rest(cp).origin, " bones=", cskel.get_bone_count())
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
func _load(path: String) -> Node3D:
|
||||||
|
var doc := GLTFDocument.new()
|
||||||
|
var state := GLTFState.new()
|
||||||
|
doc.append_from_file(path, state)
|
||||||
|
return doc.generate_scene(state)
|
||||||
|
|
||||||
|
func _find(root: Node, klass: String) -> Node:
|
||||||
|
var stack: Array = [root]
|
||||||
|
while not stack.is_empty():
|
||||||
|
var n: Node = stack.pop_back()
|
||||||
|
if n.get_class() == klass:
|
||||||
|
return n
|
||||||
|
for c in n.get_children():
|
||||||
|
stack.push_back(c)
|
||||||
|
return null
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Headless GLB fixup: rotate a model to face +Z and scale to a target height,
|
||||||
|
# baking the transform into vertices via the root node transform on export.
|
||||||
|
# Env: ROT_IN (glb path), ROT_OUT (glb path), ROT_YAW_DEG, ROT_HEIGHT_M
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
var in_path := OS.get_environment("ROT_IN")
|
||||||
|
var out_path := OS.get_environment("ROT_OUT")
|
||||||
|
var yaw_deg := float(OS.get_environment("ROT_YAW_DEG"))
|
||||||
|
var target_h := float(OS.get_environment("ROT_HEIGHT_M"))
|
||||||
|
|
||||||
|
var doc := GLTFDocument.new()
|
||||||
|
var state := GLTFState.new()
|
||||||
|
if doc.append_from_file(in_path, state) != OK:
|
||||||
|
push_error("load failed: " + in_path)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var scene := doc.generate_scene(state)
|
||||||
|
|
||||||
|
# measure current height from merged mesh AABB
|
||||||
|
var merged := AABB()
|
||||||
|
var first := true
|
||||||
|
var stack: Array = [scene]
|
||||||
|
while not stack.is_empty():
|
||||||
|
var n: Node = stack.pop_back()
|
||||||
|
if n is MeshInstance3D:
|
||||||
|
var box: AABB = (n as MeshInstance3D).get_aabb()
|
||||||
|
if first:
|
||||||
|
merged = box
|
||||||
|
first = false
|
||||||
|
else:
|
||||||
|
merged = merged.merge(box)
|
||||||
|
for c in n.get_children():
|
||||||
|
stack.push_back(c)
|
||||||
|
var h := merged.size.y
|
||||||
|
var s := target_h / h if h > 0.001 else 1.0
|
||||||
|
print("height=", h, " scale=", s)
|
||||||
|
|
||||||
|
var wrapper := Node3D.new()
|
||||||
|
wrapper.name = "Root"
|
||||||
|
wrapper.rotate_y(deg_to_rad(yaw_deg))
|
||||||
|
wrapper.scale = Vector3(s, s, s)
|
||||||
|
# move model so feet sit at y=0 after scaling
|
||||||
|
scene.position.y = -merged.position.y
|
||||||
|
var parent := scene.get_parent()
|
||||||
|
if parent != null:
|
||||||
|
parent.remove_child(scene)
|
||||||
|
wrapper.add_child(scene)
|
||||||
|
_own_recursive(scene, wrapper)
|
||||||
|
|
||||||
|
var out_doc := GLTFDocument.new()
|
||||||
|
var out_state := GLTFState.new()
|
||||||
|
if out_doc.append_from_scene(wrapper, out_state) != OK:
|
||||||
|
push_error("export append failed")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
if out_doc.write_to_filesystem(out_state, out_path) != OK:
|
||||||
|
push_error("write failed: " + out_path)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print("WROTE ", out_path)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
func _own_recursive(n: Node, owner: Node) -> void:
|
||||||
|
n.owner = owner
|
||||||
|
for c in n.get_children():
|
||||||
|
_own_recursive(c, owner)
|
||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
# Generic GLB inspection viewer for AI-generated meshes (Tripo / Meshy rigged).
|
||||||
|
# Reads env vars:
|
||||||
|
# VIEW_GLB - absolute path to the .glb to load
|
||||||
|
# VIEW_SHOTS - absolute dir for screenshots
|
||||||
|
# VIEW_PREFIX - filename prefix for shots
|
||||||
|
# Takes turntable shots (auto-framed from the mesh AABB); if the GLB carries
|
||||||
|
# animations, also captures frames across the first clip. Quits when done.
|
||||||
|
# Set VIEW_INTERACTIVE=1 to skip shots and orbit live instead
|
||||||
|
# (left-drag orbits, wheel zooms; animation loops if present).
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
var model: Node3D
|
||||||
|
var cam: Camera3D
|
||||||
|
var shot_dir: String
|
||||||
|
var prefix: String
|
||||||
|
|
||||||
|
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:
|
||||||
|
var glb_path := OS.get_environment("VIEW_GLB")
|
||||||
|
shot_dir = OS.get_environment("VIEW_SHOTS")
|
||||||
|
prefix = OS.get_environment("VIEW_PREFIX")
|
||||||
|
interactive = OS.get_environment("VIEW_INTERACTIVE") == "1"
|
||||||
|
if interactive and shot_dir == "":
|
||||||
|
shot_dir = "C:/Users/CAN/AppData/Local/Temp/"
|
||||||
|
if glb_path == "" or shot_dir == "":
|
||||||
|
push_error("VIEW_GLB / VIEW_SHOTS not set")
|
||||||
|
get_tree().quit(1)
|
||||||
|
return
|
||||||
|
if not shot_dir.ends_with("/"):
|
||||||
|
shot_dir += "/"
|
||||||
|
DirAccess.make_dir_recursive_absolute(shot_dir)
|
||||||
|
|
||||||
|
var 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 doc := GLTFDocument.new()
|
||||||
|
var state := GLTFState.new()
|
||||||
|
if doc.append_from_file(glb_path, state) != OK:
|
||||||
|
push_error("GLB load failed: " + glb_path)
|
||||||
|
get_tree().quit(1)
|
||||||
|
return
|
||||||
|
model = doc.generate_scene(state)
|
||||||
|
add_child(model)
|
||||||
|
|
||||||
|
# inventory: meshes, verts, skeleton bones, animations
|
||||||
|
var anim_player: AnimationPlayer = null
|
||||||
|
var skel: Skeleton3D = null
|
||||||
|
var tri_count := 0
|
||||||
|
var stack: Array = [model]
|
||||||
|
while not stack.is_empty():
|
||||||
|
var n: Node = stack.pop_back()
|
||||||
|
if n is AnimationPlayer and anim_player == null:
|
||||||
|
anim_player = n
|
||||||
|
if n is Skeleton3D and skel == null:
|
||||||
|
skel = n
|
||||||
|
if n is MeshInstance3D and n.mesh != null:
|
||||||
|
for s in n.mesh.get_surface_count():
|
||||||
|
tri_count += n.mesh.surface_get_arrays(s)[Mesh.ARRAY_INDEX].size() / 3
|
||||||
|
for c in n.get_children():
|
||||||
|
stack.push_back(c)
|
||||||
|
print("INFO tris=", tri_count)
|
||||||
|
if skel != null:
|
||||||
|
var names := PackedStringArray()
|
||||||
|
for i in skel.get_bone_count():
|
||||||
|
names.append(skel.get_bone_name(i))
|
||||||
|
print("INFO bones=", skel.get_bone_count(), " names=", ",".join(names))
|
||||||
|
else:
|
||||||
|
print("INFO bones=0")
|
||||||
|
if anim_player != null:
|
||||||
|
print("INFO anims=", ",".join(anim_player.get_animation_list()))
|
||||||
|
|
||||||
|
# auto-frame from merged AABB (skinned meshes report bogus tiny AABBs —
|
||||||
|
# fall back to skeleton bone rest positions)
|
||||||
|
await get_tree().process_frame
|
||||||
|
var aabb := _merged_aabb()
|
||||||
|
if skel != null and max(aabb.size.x, max(aabb.size.y, aabb.size.z)) < 0.5:
|
||||||
|
aabb = _skeleton_aabb(skel)
|
||||||
|
print("INFO aabb pos=", aabb.position, " size=", aabb.size)
|
||||||
|
var center := aabb.get_center()
|
||||||
|
var radius: float = max(aabb.size.x, max(aabb.size.y, aabb.size.z)) * 0.5
|
||||||
|
var dist := radius * 2.6
|
||||||
|
|
||||||
|
cam = Camera3D.new()
|
||||||
|
cam.fov = 50
|
||||||
|
add_child(cam)
|
||||||
|
cam.current = true
|
||||||
|
|
||||||
|
if interactive:
|
||||||
|
orbit_target = center
|
||||||
|
orbit_dist = dist
|
||||||
|
_update_cam()
|
||||||
|
if anim_player != null and anim_player.get_animation_list().size() > 0:
|
||||||
|
var loop_anim: String = anim_player.get_animation_list()[0]
|
||||||
|
anim_player.get_animation(loop_anim).loop_mode = Animation.LOOP_LINEAR
|
||||||
|
anim_player.play(loop_anim)
|
||||||
|
print("INTERACTIVE — left-drag orbit, wheel zoom")
|
||||||
|
return
|
||||||
|
|
||||||
|
# turntable
|
||||||
|
var angles := {"01_front": 0.0, "02_three_quarter": PI * 0.25, "03_side": PI * 0.5, "04_back": PI}
|
||||||
|
for shot_name in angles:
|
||||||
|
var a: float = angles[shot_name]
|
||||||
|
cam.look_at_from_position(center + Vector3(sin(a) * dist, radius * 0.35, cos(a) * dist), center)
|
||||||
|
await _snap(shot_name)
|
||||||
|
|
||||||
|
# head closeup (top of AABB)
|
||||||
|
var head := center + Vector3(0, aabb.size.y * 0.38, 0)
|
||||||
|
cam.look_at_from_position(head + Vector3(0, 0.05 * radius, dist * 0.35), head)
|
||||||
|
await _snap("05_head")
|
||||||
|
|
||||||
|
# animation frames
|
||||||
|
if anim_player != null and anim_player.get_animation_list().size() > 0:
|
||||||
|
var anim_name: String = anim_player.get_animation_list()[0]
|
||||||
|
var anim := anim_player.get_animation(anim_name)
|
||||||
|
var frames := 6
|
||||||
|
for i in frames:
|
||||||
|
var t := anim.length * float(i) / float(frames)
|
||||||
|
anim_player.play(anim_name)
|
||||||
|
anim_player.seek(t, true)
|
||||||
|
anim_player.pause()
|
||||||
|
cam.look_at_from_position(center + Vector3(sin(PI * 0.2) * dist, radius * 0.35, cos(PI * 0.2) * dist), center)
|
||||||
|
await _snap("anim_f%d" % i)
|
||||||
|
print("SHOTS_DONE")
|
||||||
|
get_tree().quit(0)
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not interactive:
|
||||||
|
return
|
||||||
|
if 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, 12.0)
|
||||||
|
_update_cam()
|
||||||
|
elif event.pressed and event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||||
|
orbit_dist = clampf(orbit_dist / 0.90, 0.35, 12.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 _skeleton_aabb(skel: Skeleton3D) -> AABB:
|
||||||
|
var box := AABB(skel.global_transform * skel.get_bone_global_rest(0).origin, Vector3.ZERO)
|
||||||
|
for i in skel.get_bone_count():
|
||||||
|
box = box.expand(skel.global_transform * skel.get_bone_global_rest(i).origin)
|
||||||
|
box = box.grow(box.size.y * 0.12)
|
||||||
|
return box
|
||||||
|
|
||||||
|
func _merged_aabb() -> AABB:
|
||||||
|
var merged := AABB()
|
||||||
|
var first := true
|
||||||
|
var stack: Array = [model]
|
||||||
|
while not stack.is_empty():
|
||||||
|
var n: Node = stack.pop_back()
|
||||||
|
if n is MeshInstance3D:
|
||||||
|
var box: AABB = n.global_transform * n.get_aabb()
|
||||||
|
if first:
|
||||||
|
merged = box
|
||||||
|
first = false
|
||||||
|
else:
|
||||||
|
merged = merged.merge(box)
|
||||||
|
for c in n.get_children():
|
||||||
|
stack.push_back(c)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
func _snap(shot_name: String) -> void:
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var img := get_viewport().get_texture().get_image()
|
||||||
|
img.save_png(shot_dir + prefix + shot_name + ".png")
|
||||||
|
print("shot saved: ", shot_dir + prefix + shot_name + ".png")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://view_tripo_scene"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://view_tripo.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="ViewTripo" type="Node3D"]
|
||||||
|
script = ExtResource("1")
|
||||||
Reference in New Issue
Block a user