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)