Twelve garments, per-body on all 11 bodies, chromakey-gated (worst clips: parka 3px, boots 54, sweater/tank 83, sneakers 100 final-geometry, slides 123, swim trunks 132, cargo 138 — all under the 150px gate), previewed: tank top, sweater (crew-neck via per-body ring-valley probe — first draft read mock-neck, fixed by measured rim circularization), track jacket (recolorable sleeve-stripe region), joggers (side-stripe region), cargo pants, swim trunks, one-piece swimsuit, sneakers (prism-sole), formal shoes, ankle boots (calf shaft), slides (open strap + sole), and the hip-length thrds parka — the first canon-branded garment (Braemar cold-weather cooperative), quilted, logo-capable. Tops now hem into real hip geometry (the natural seg_torso bottom is a 9-14cm tooth ring — the sweater established the hem-into-hips practice). Manifest merged by the lead: 24 clothing entries with region/default-tint metadata. Full modern catalogue: 21 garments across tops/bottoms/feet/ full-body x casual/formal/sport/swim/outerwear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
195 lines
6.6 KiB
GDScript
195 lines
6.6 KiB
GDScript
extends Node3D
|
|
## slides preview capture (T-1089 wave 2, swim set) — parameterized copy of
|
|
## preview_swim_trunks.gd for the FOOTWEAR slot, 2-region garment: per-body
|
|
## region mask (reference fallback), no logo, camera framed low on the feet so
|
|
## the sole slab and midfoot strap band read. Front + 3/4 Walk renders.
|
|
## Temporary tool scene — safe to delete after captures.
|
|
##
|
|
## Config via env vars (all optional):
|
|
## GARMENT_PREVIEW_ITEM default "slides"
|
|
## GARMENT_PREVIEW_BODY default "average_m"
|
|
## GARMENT_PREVIEW_OUT default "<repo>/.cache/garment-preview/slides"
|
|
|
|
const CLOTHING_DIR := "res://assets/characters/clothing/"
|
|
const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader"
|
|
const VIEWPORT := Vector2i(1024, 768)
|
|
|
|
const BODY_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,
|
|
}
|
|
|
|
# Bright swim palette matching the painted albedo (sole=R off-white foam,
|
|
# strap=G coral) so tinted and untinted renders agree.
|
|
const TINT_SOLE := Color(0.82, 0.83, 0.85) # R region, off-white foam
|
|
const TINT_STRAP := Color(0.93, 0.35, 0.20) # G region, bright coral
|
|
const TINT_UNUSED := Color.WHITE
|
|
|
|
var _out_dir := ""
|
|
var _cam: Camera3D = null
|
|
|
|
|
|
func _ready() -> void:
|
|
var item := _env("GARMENT_PREVIEW_ITEM", "slides")
|
|
var body := _env("GARMENT_PREVIEW_BODY", "average_m")
|
|
_out_dir = _env("GARMENT_PREVIEW_OUT",
|
|
"/var/mnt/data/projects/settled-reach/.cache/garment-preview/slides")
|
|
get_window().size = VIEWPORT
|
|
_setup_stage()
|
|
_run.call_deferred(item, body)
|
|
|
|
|
|
func _env(key: String, fallback: String) -> String:
|
|
var v := OS.get_environment(key)
|
|
return v if not v.is_empty() else fallback
|
|
|
|
|
|
func _setup_stage() -> void:
|
|
var light := DirectionalLight3D.new()
|
|
light.rotation_degrees = Vector3(-38, 28, 0)
|
|
light.light_energy = 1.3
|
|
add_child(light)
|
|
var env := Environment.new()
|
|
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
|
env.ambient_light_color = Color(0.82, 0.82, 0.86)
|
|
env.ambient_light_energy = 0.9
|
|
env.background_mode = Environment.BG_COLOR
|
|
env.background_color = Color(0.16, 0.17, 0.20)
|
|
var we := WorldEnvironment.new()
|
|
we.environment = env
|
|
add_child(we)
|
|
_cam = Camera3D.new()
|
|
_cam.fov = 40.0
|
|
add_child(_cam)
|
|
# Low, close framing on the feet: the sole slab, the strap band and the
|
|
# open toe/heel must all read. Slight downward angle shows the footbed.
|
|
_cam.position = Vector3(0.0, 0.42, 1.15)
|
|
_cam.look_at_from_position(_cam.position, Vector3(0.0, 0.10, 0.0), Vector3.UP)
|
|
|
|
|
|
func _run(item: String, body: String) -> void:
|
|
DirAccess.make_dir_recursive_absolute(_out_dir)
|
|
if not BODY_KEY_TO_ENUM.has(body):
|
|
push_error("preview: unknown body %s" % body)
|
|
get_tree().quit(1)
|
|
return
|
|
|
|
var cv := CharacterVisual.new()
|
|
add_child(cv)
|
|
var desc := CharacterVisualDescriptor.new()
|
|
desc.body_type = BODY_KEY_TO_ENUM[body]
|
|
desc.head_id = "head_001"
|
|
desc.hair_id = "buzzed"
|
|
desc.eyebrow_id = "regular"
|
|
desc.skin_tone = 3
|
|
cv.load_descriptor(desc)
|
|
|
|
var skel := cv.get_skeleton()
|
|
if skel == null:
|
|
push_error("preview: no skeleton")
|
|
get_tree().quit(1)
|
|
return
|
|
|
|
var meshes := _attach_garment(skel, item, body)
|
|
print("preview: attached %d garment meshes" % meshes.size())
|
|
var shader := load(SHADER_PATH) as Shader
|
|
# Per-body region mask (offset-shell route); reference fallback mirrors
|
|
# the runtime compositor.
|
|
var mask := _try_load(CLOTHING_DIR + "%s/%s_mask.png" % [item, body])
|
|
if mask == null:
|
|
mask = _try_load(CLOTHING_DIR + "%s/reference_mask.png" % item)
|
|
for mi in meshes:
|
|
_apply_garment_shader(mi, shader, mask)
|
|
|
|
# Freeze on a mid-stride Walk pose so the strap and sole read under motion.
|
|
var ap := cv.get_animation_player()
|
|
if ap != null:
|
|
var walk := _resolve_clip(ap, "Walk")
|
|
if not walk.is_empty():
|
|
cv.play_animation(walk)
|
|
ap.pause()
|
|
ap.seek(ap.current_animation_length * 0.25, true)
|
|
await get_tree().process_frame
|
|
|
|
# Front + 3/4 (task deliverable: two preview renders).
|
|
for yaw in [0, 45]:
|
|
cv.rotation_degrees.y = float(yaw)
|
|
await RenderingServer.frame_post_draw
|
|
await RenderingServer.frame_post_draw
|
|
var fname := "%s__%s__yaw%03d.png" % [item, body, yaw]
|
|
get_viewport().get_texture().get_image().save_png(_out_dir.path_join(fname))
|
|
print("preview: wrote ", fname)
|
|
|
|
print("preview: DONE")
|
|
get_tree().quit(0)
|
|
|
|
|
|
func _attach_garment(skel: Skeleton3D, item: String, body: String) -> Array:
|
|
var out: Array = []
|
|
var glb := CLOTHING_DIR + "%s/%s.glb" % [item, body]
|
|
if not ResourceLoader.exists(glb):
|
|
push_error("preview: garment GLB missing %s" % glb)
|
|
return out
|
|
var inst := (load(glb) as PackedScene).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 _apply_garment_shader(mi: MeshInstance3D, shader: Shader, mask: Texture2D) -> void:
|
|
if mi.mesh == null or shader == null:
|
|
return
|
|
var albedo := _albedo_of(mi)
|
|
var mat := ShaderMaterial.new()
|
|
mat.shader = shader
|
|
if albedo:
|
|
mat.set_shader_parameter("albedo_tex", albedo)
|
|
if mask:
|
|
mat.set_shader_parameter("region_mask", mask)
|
|
mat.set_shader_parameter("tint_0", TINT_SOLE)
|
|
mat.set_shader_parameter("tint_1", TINT_STRAP)
|
|
mat.set_shader_parameter("tint_2", TINT_UNUSED)
|
|
mat.set_shader_parameter("tint_3", TINT_UNUSED)
|
|
mat.set_shader_parameter("shadow_strength", 0.2)
|
|
mi.material_override = mat
|
|
|
|
|
|
func _albedo_of(mi: MeshInstance3D) -> Texture2D:
|
|
for surf in range(mi.mesh.get_surface_count()):
|
|
var m := mi.mesh.surface_get_material(surf)
|
|
if m is BaseMaterial3D:
|
|
return (m as BaseMaterial3D).albedo_texture
|
|
return null
|
|
|
|
|
|
func _try_load(path: String) -> Texture2D:
|
|
return load(path) as Texture2D if ResourceLoader.exists(path) else null
|
|
|
|
|
|
func _resolve_clip(ap: AnimationPlayer, requested: String) -> String:
|
|
for lib_name in ap.get_animation_library_list():
|
|
var lib := ap.get_animation_library(lib_name)
|
|
for a in lib.get_animation_list():
|
|
if str(a).to_lower().contains(requested.to_lower()):
|
|
return str(a)
|
|
return ""
|