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>
194 lines
6.0 KiB
GDScript
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)
|