Files
settled-reach/spikes/synty-intake/scripts/qa_bodies.gd
T
jpmschweitzerandClaude Fable 5 4bcd778e16 chore(assets): Synty intake spike — scripts, evidence logs, verdict data (T-1089)
Bridge-C evidence spike: unitypackage extraction, rest-pose recon, garment
transplant onto the Quaternius rig (Data Transfer POLYINTERP_NEAREST), 11-body
batch fit, Godot QA scenes. Technical PASS; route nonetheless REJECTED by
product call (cost, baked body-segment modularity, style) — full trail on
T-1089. Salvage: source-agnostic transplant/batch-fit scripts (the G1-family
pipeline for any donor mesh) and the T-1090 discovery (5 body types misrender
bare). Extracted vendor payload (spikes/**/raw/) now gitignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:01:24 +02:00

194 lines
6.0 KiB
GDScript

extends Node3D
## Spike T-1089 Synty intake — 11-body batch QA scene (THROWAWAY, lives in
## client/spike_synty_qa/ only for the res:// path; source of truth is
## spikes/synty-intake/scripts/qa_bodies.gd).
##
## For each body type on disk: builds a CharacterVisual, reparents that
## body's refitted Synty torso garment onto its skeleton (mirrors
## character_visual.gd:517-535), screenshots idle front, then plays Walk
## and screenshots two stride phases (front + side). Peasant pants/shoes
## are added only for bodies that have them on disk. Saves to
## spikes/synty-intake/out/qa_bodies/, then quits.
const BODIES_GLB_DIR := "res://spike_synty_qa/bodies/"
const SHOT_DIR := "/var/mnt/data/projects/settled-reach/spikes/synty-intake/out/qa_bodies/"
const CLOTHING_DIR := "res://assets/characters/clothing/"
const BODY_KEYS: Array[String] = [
"average_m", "average_f", "muscular_m", "muscular_f",
"thin_m", "thin_f", "heavy_m", "heavy_f",
"teen_m", "teen_f", "child",
]
const KEY_TO_ENUM := {
"average_m": CharacterVisualDescriptor.BodyType.AVERAGE_M,
"average_f": CharacterVisualDescriptor.BodyType.AVERAGE_F,
"muscular_m": CharacterVisualDescriptor.BodyType.MUSCULAR_M,
"muscular_f": CharacterVisualDescriptor.BodyType.MUSCULAR_F,
"thin_m": CharacterVisualDescriptor.BodyType.THIN_M,
"thin_f": CharacterVisualDescriptor.BodyType.THIN_F,
"heavy_m": CharacterVisualDescriptor.BodyType.HEAVY_M,
"heavy_f": CharacterVisualDescriptor.BodyType.HEAVY_F,
"teen_m": CharacterVisualDescriptor.BodyType.TEEN_M,
"teen_f": CharacterVisualDescriptor.BodyType.TEEN_F,
"child": CharacterVisualDescriptor.BodyType.CHILD,
}
var _cam: Camera3D = null
func _ready() -> void:
_setup_stage()
_run_qa.call_deferred()
func _setup_stage() -> void:
var light := DirectionalLight3D.new()
light.rotation_degrees = Vector3(-45, 30, 0)
light.light_energy = 1.2
add_child(light)
var env := Environment.new()
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.ambient_light_color = Color(0.8, 0.8, 0.85)
env.ambient_light_energy = 0.9
env.background_mode = Environment.BG_COLOR
env.background_color = Color(0.18, 0.19, 0.22)
var we := WorldEnvironment.new()
we.environment = env
add_child(we)
_cam = Camera3D.new()
add_child(_cam)
func _cam_front(h: float) -> void:
_cam.position = Vector3(0, h * 0.72, h * 0.85)
_cam.look_at_from_position(_cam.position, Vector3(0, h * 0.62, 0), Vector3.UP)
func _cam_side(h: float) -> void:
_cam.position = Vector3(h * 0.85, h * 0.72, 0)
_cam.look_at_from_position(_cam.position, Vector3(0, h * 0.62, 0), Vector3.UP)
func _attach_garment(skel: Skeleton3D, glb_path: String) -> Array[MeshInstance3D]:
var out: Array[MeshInstance3D] = []
var scene := load(glb_path) as PackedScene
if scene == null:
push_error("QA: garment GLB failed to load: " + glb_path)
return out
var inst := scene.instantiate()
var stack: Array[Node] = [inst]
while not stack.is_empty():
var n: Node = stack.pop_back()
for c in n.get_children():
stack.append(c)
if n is MeshInstance3D and (n as MeshInstance3D).skin != null:
var mi := n as MeshInstance3D
mi.get_parent().remove_child(mi)
mi.owner = null
skel.add_child(mi)
out.append(mi)
inst.queue_free()
return out
func _shot(fname: String) -> void:
await RenderingServer.frame_post_draw
var img := get_viewport().get_texture().get_image()
img.save_png(SHOT_DIR + fname)
print("QA: saved ", fname)
func _wait_frames(n: int) -> void:
for i in n:
await get_tree().process_frame
func _run_qa() -> void:
DirAccess.make_dir_recursive_absolute(SHOT_DIR)
for body_key: String in BODY_KEYS:
var glb := BODIES_GLB_DIR + "SK_SCFI_CIVL_09_10TORS_HU01_%s.glb" % body_key
if not ResourceLoader.exists(glb):
print("QA: MISSING garment GLB for ", body_key, " — skipped")
continue
var cv := CharacterVisual.new()
add_child(cv)
var desc := CharacterVisualDescriptor.new()
desc.body_type = KEY_TO_ENUM[body_key]
desc.head_id = "head_001"
desc.hair_id = "buzzed"
desc.eyebrow_id = "regular"
desc.skin_tone = 3
var slots := {}
if ResourceLoader.exists(CLOTHING_DIR + "peasant_pants/%s.glb" % body_key):
slots["legs"] = "peasant_pants"
if ResourceLoader.exists(CLOTHING_DIR + "peasant_shoes/%s.glb" % body_key):
slots["feet"] = "peasant_shoes"
desc.clothing_slots = slots
cv.load_descriptor(desc)
var skel := cv.get_skeleton()
if skel == null:
push_error("QA: no skeleton for " + body_key)
cv.queue_free()
continue
print("QA[", body_key, "]: skeleton bones = ", skel.get_bone_count(),
" pants = ", slots.has("legs"))
# rough height for camera framing (child/teen are shorter)
var h0 := 1.75
if body_key == "child" or body_key.begins_with("teen"):
h0 = 1.45
# body-only control shot BEFORE the garment — separates body-asset
# rendering defects from garment fit defects
await _wait_frames(5)
_cam_front(h0)
await _shot("%s_0_body_only.png" % body_key)
var meshes := _attach_garment(skel, glb)
for mi in meshes:
print("QA[", body_key, "]: garment '", mi.name, "' skin binds = ",
mi.skin.get_bind_count(), " aabb = ", mi.get_aabb())
var h := h0
await _wait_frames(5)
_cam_front(h)
await _shot("%s_1_idle_front.png" % body_key)
var ap := cv.get_animation_player()
var walk := ""
if ap:
for lib_name in ap.get_animation_library_list():
var lib := ap.get_animation_library(lib_name)
for a in lib.get_animation_list():
var full := str(lib_name) + "/" + str(a)
if full.to_lower().contains("walk"):
walk = str(a)
if full.to_lower().ends_with("/walk"):
break
if walk != "":
cv.play_animation(walk)
await _wait_frames(24)
_cam_front(h)
await _shot("%s_2_walk_front.png" % body_key)
_cam_side(h)
await _wait_frames(8)
await _shot("%s_3_walk_side.png" % body_key)
else:
push_warning("QA: no walk animation for " + body_key)
for mi in meshes:
print("QA[", body_key, "]: post-walk garment aabb = ", mi.get_aabb())
cv.queue_free()
await _wait_frames(2)
print("QA: DONE")
get_tree().quit(0)