feat(assets): wardrobe wave 1 — per-body shells, 8 garments, try-on UI (T-1089)
Infra: offset-shell gains --per-body mode (each body's own segments, cut/mask thresholds derived from that body's bone landmarks — reproduces the hand-calibrated reference constants exactly on average_m); compositor prefers <body>_mask.png with reference_mask.png fallback; tshirt re-authored per-body on all 11 (the Q-060 torso poke-through class is GONE — residual flags are a sleeve-hem epsilon artifact on thick arms, offset-insensitive, documented). Garments (all per-body x 11, chromakey-gated <=150px worst, previewed): hoodie (hood-down roll, kangaroo pocket, logo), button-down (collar/placket), shorts, jeans (analytic denim field driving albedo+mask together; boundary weld + open-rim flattening — real segment-splitter findings), formal pants, jacket (over-shirt standoff, zip), suit_jacket_black (lapel region, tintable shirt triangle — the hand-author proof), uniform_utility (11-segment coverall, gap-free waist join by construction, 4-zone showcase, logo patch). Try-on UI: creation screen shows per-region tint pickers (multi_region garments) + logo picker (logo_capable + logos/*.png scan), data-driven off manifest+coverage. Manifest merged by the lead: 12 clothing entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
extends Node3D
|
||||
## Buttondown preview capture (T-1089 buttondown_modern) — per-garment copy of
|
||||
## preview_scene.gd (same convention as preview_shorts_modern.gd): oxford
|
||||
## palette (white collar/cuffs/placket over light-blue body), body-aware mask
|
||||
## loading (<body>_mask.png first, mirroring runtime), no logo (logo_capable
|
||||
## OFF), front + 3/4 walk yaws only.
|
||||
##
|
||||
## Config via env vars (all optional):
|
||||
## GARMENT_PREVIEW_ITEM default "buttondown_modern"
|
||||
## GARMENT_PREVIEW_BODY default "average_m"
|
||||
## GARMENT_PREVIEW_OUT default "<repo>/.cache/garment-preview"
|
||||
|
||||
const CLOTHING_DIR := "res://assets/characters/clothing/"
|
||||
const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader"
|
||||
const VIEWPORT := Vector2i(768, 1024)
|
||||
|
||||
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,
|
||||
"teen_m": CharacterVisualDescriptor.BodyType.TEEN_M,
|
||||
"teen_f": CharacterVisualDescriptor.BodyType.TEEN_F,
|
||||
}
|
||||
|
||||
# Winchester oxford: white collar + white cuffs/placket over light-blue body.
|
||||
const TINT_COLLAR := Color(0.93, 0.94, 0.96) # R region
|
||||
const TINT_BODY := Color(0.55, 0.68, 0.82) # G region
|
||||
const TINT_CUFF := Color(0.90, 0.91, 0.94) # B region (cuffs + placket)
|
||||
|
||||
var _out_dir := ""
|
||||
var _cam: Camera3D = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var item := _env("GARMENT_PREVIEW_ITEM", "buttondown_modern")
|
||||
var body := _env("GARMENT_PREVIEW_BODY", "average_m")
|
||||
_out_dir = _env("GARMENT_PREVIEW_OUT",
|
||||
"/var/mnt/data/projects/settled-reach/.cache/garment-preview")
|
||||
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 = 45.0
|
||||
add_child(_cam)
|
||||
# Frame the upper body so placket/buttons/collar/cuffs read clearly.
|
||||
_cam.position = Vector3(0.0, 1.25, 1.7)
|
||||
_cam.look_at_from_position(_cam.position, Vector3(0.0, 1.15, 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-authored garment: prefer <body>_mask.png (runtime behaviour).
|
||||
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 walk pose so the shirt reads in 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 spec: 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_COLLAR)
|
||||
mat.set_shader_parameter("tint_1", TINT_BODY)
|
||||
mat.set_shader_parameter("tint_2", TINT_CUFF)
|
||||
mat.set_shader_parameter("tint_3", Color.WHITE)
|
||||
mat.set_shader_parameter("logo_enabled", 0.0)
|
||||
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 ""
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tools/garment_preview/preview_buttondown_modern.gd" id="1_bdprev"]
|
||||
|
||||
[node name="ButtondownPreview" type="Node3D"]
|
||||
script = ExtResource("1_bdprev")
|
||||
@@ -0,0 +1,185 @@
|
||||
extends Node3D
|
||||
## shorts_modern preview capture (T-1089 basic set) — parameterized copy of
|
||||
## preview_scene.gd for a LOWER-BODY 2-region garment: per-body region mask
|
||||
## (reference fallback), no logo, camera framed on hips + legs, front + 3/4
|
||||
## Walk renders. Temporary tool scene — safe to delete after captures.
|
||||
##
|
||||
## Config via env vars (all optional):
|
||||
## GARMENT_PREVIEW_ITEM default "shorts_modern"
|
||||
## GARMENT_PREVIEW_BODY default "average_m"
|
||||
## GARMENT_PREVIEW_OUT default "<repo>/.cache/garment-preview/shorts_modern"
|
||||
|
||||
const CLOTHING_DIR := "res://assets/characters/clothing/"
|
||||
const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader"
|
||||
const VIEWPORT := Vector2i(768, 1024)
|
||||
|
||||
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,
|
||||
"teen_m": CharacterVisualDescriptor.BodyType.TEEN_M,
|
||||
"teen_f": CharacterVisualDescriptor.BodyType.TEEN_F,
|
||||
}
|
||||
|
||||
# Modern casual palette: charcoal waistband, muted olive body. tint_2/3 unused.
|
||||
const TINT_WAISTBAND := Color(0.13, 0.14, 0.16) # R region
|
||||
const TINT_BODY := Color(0.42, 0.47, 0.38) # G region
|
||||
|
||||
var _out_dir := ""
|
||||
var _cam: Camera3D = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var item := _env("GARMENT_PREVIEW_ITEM", "shorts_modern")
|
||||
var body := _env("GARMENT_PREVIEW_BODY", "average_m")
|
||||
_out_dir = _env("GARMENT_PREVIEW_OUT",
|
||||
"/var/mnt/data/projects/settled-reach/.cache/garment-preview/shorts_modern")
|
||||
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 = 45.0
|
||||
add_child(_cam)
|
||||
# Frame hips + legs so the waistband and hem read clearly.
|
||||
_cam.position = Vector3(0.0, 0.82, 1.7)
|
||||
_cam.look_at_from_position(_cam.position, Vector3(0.0, 0.72, 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 hem + waistband 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_WAISTBAND)
|
||||
mat.set_shader_parameter("tint_1", TINT_BODY)
|
||||
mat.set_shader_parameter("tint_2", Color.WHITE)
|
||||
mat.set_shader_parameter("tint_3", Color.WHITE)
|
||||
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 ""
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tools/garment_preview/preview_shorts_modern.gd" id="1_prevs"]
|
||||
|
||||
[node name="GarmentPreviewShorts" type="Node3D"]
|
||||
script = ExtResource("1_prevs")
|
||||
@@ -0,0 +1,208 @@
|
||||
extends Node3D
|
||||
## uniform_utility preview capture (T-1089 route (c) colorable-uniform proof).
|
||||
##
|
||||
## Full-body variant of preview_scene.gd for the utility coverall: whole-figure
|
||||
## camera framing (the garment runs collar to ankle), per-body region mask
|
||||
## preference (<body>_mask.png, the per-body offset-shell convention) and the
|
||||
## default utility palette on the four recolor zones:
|
||||
## R trim (collar + cuffs) = dark slate
|
||||
## G body fabric = utility teal-grey
|
||||
## B belt + patches = safety amber
|
||||
## A shoulder marks = pale high-vis
|
||||
## The chest patch carries the thrds logo (logo_capable proof).
|
||||
##
|
||||
## Config via env vars (all optional):
|
||||
## GARMENT_PREVIEW_ITEM default "uniform_utility"
|
||||
## GARMENT_PREVIEW_BODY default "average_m"
|
||||
## GARMENT_PREVIEW_OUT default "<repo>/.cache/garment-preview/uniform_utility"
|
||||
## GARMENT_PREVIEW_LOGO default thrds
|
||||
##
|
||||
## Run: godot4 --path client --rendering-driver opengl3 \
|
||||
## res://tools/garment_preview/preview_uniform_utility.tscn
|
||||
|
||||
const CLOTHING_DIR := "res://assets/characters/clothing/"
|
||||
const LOGO_PATH := "res://assets/characters/logos/thrds.png"
|
||||
const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader"
|
||||
const VIEWPORT := Vector2i(768, 1024)
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
# Default utility palette — four cleanly recolorable zones.
|
||||
const TINT_TRIM := Color(0.23, 0.28, 0.31) # R: collar + cuffs, dark slate
|
||||
const TINT_BODY := Color(0.42, 0.51, 0.50) # G: main fabric, teal-grey
|
||||
const TINT_BELT := Color(0.82, 0.55, 0.18) # B: belt + patches, safety amber
|
||||
const TINT_MARKS := Color(0.85, 0.87, 0.88) # A: shoulder marks, pale high-vis
|
||||
|
||||
var _out_dir := ""
|
||||
var _cam: Camera3D = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var item := _env("GARMENT_PREVIEW_ITEM", "uniform_utility")
|
||||
var body := _env("GARMENT_PREVIEW_BODY", "average_m")
|
||||
_out_dir = _env("GARMENT_PREVIEW_OUT",
|
||||
"/var/mnt/data/projects/settled-reach/.cache/garment-preview/uniform_utility")
|
||||
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 = 45.0
|
||||
add_child(_cam)
|
||||
# Frame the WHOLE figure — the coverall runs collar to ankle.
|
||||
_cam.position = Vector3(0.0, 1.05, 3.1)
|
||||
_cam.look_at_from_position(_cam.position, Vector3(0.0, 0.92, 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 mask first (offset-shell per-body convention), then fallback.
|
||||
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)
|
||||
var logo_path := _env("GARMENT_PREVIEW_LOGO", LOGO_PATH)
|
||||
var logo := _try_load(logo_path)
|
||||
for mi in meshes:
|
||||
_apply_garment_shader(mi, shader, mask, logo)
|
||||
|
||||
# Freeze mid-walk so the coverall reads under a real pose.
|
||||
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 — the two requested preview angles.
|
||||
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, logo: 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)
|
||||
if logo:
|
||||
mat.set_shader_parameter("logo_tex", logo)
|
||||
mat.set_shader_parameter("logo_enabled", 1.0)
|
||||
mat.set_shader_parameter("tint_0", TINT_TRIM)
|
||||
mat.set_shader_parameter("tint_1", TINT_BODY)
|
||||
mat.set_shader_parameter("tint_2", TINT_BELT)
|
||||
mat.set_shader_parameter("tint_3", TINT_MARKS)
|
||||
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 ""
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://c8rtprevunifrm1"]
|
||||
|
||||
[ext_resource type="Script" path="res://tools/garment_preview/preview_uniform_utility.gd" id="1_prevu"]
|
||||
|
||||
[node name="UniformUtilityPreview" type="Node3D"]
|
||||
script = ExtResource("1_prevu")
|
||||
Reference in New Issue
Block a user