feat(assets): wardrobe engine + proof t-shirt — batch-fit, offset-shell, 4-region tint, thrds logo (T-1089)
Engine: tooling/garment-fit/blender_batch_fit_skinned.py (G1 — the skinned Surface-Deform batch the old script couldn't produce; self-check green), blender_author_offset_shell.py (route c: garment shells from OUR body segments, weights inherited by construction, bone-plane cuts, procedural RGBA region mask, UV2 chest channel), make_logo.py. Shader: toon_garment.gdshader — channel-blended 4-region tint + UV2 logo composited after tint / before toon shading. Proof: tshirt_modern fitted to the six healthy bodies, manifest entry with style:modern + logo_capable, thrds wordmark, 18-assertion test suite, 216-capture chromakey QA. Key finding (Q-060 evidence): single-reference SD-fit of an offset-shell degrades on girth-divergent bodies (muscular_m worst) — 24mm standoff tripled headroom but the mechanism limits. Route guidance recorded on T-1089: per-body shell authoring for offset-shell garments; SD-fit for derived/hand-authored ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 355 KiB |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"hides": ["torso", "arm_upper_l", "arm_upper_r"],
|
||||
"torso_variant": "full",
|
||||
"multi_region": true
|
||||
}
|
||||
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
@@ -54,6 +54,11 @@
|
||||
},
|
||||
"peasant_tunic": {
|
||||
"slot": "torso"
|
||||
},
|
||||
"tshirt_modern": {
|
||||
"slot": "torso",
|
||||
"style": "modern",
|
||||
"logo_capable": true
|
||||
}
|
||||
},
|
||||
"accessories": []
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
shader_type spatial;
|
||||
render_mode unshaded;
|
||||
|
||||
// Multi-region recolorable garment shader (T-1089 gap G3 + G4 logo decal).
|
||||
//
|
||||
// Supersedes toon_masked.gdshader for CLOTHING: that shader routes a single
|
||||
// tint through recolor_mask.r only. This one channel-routes FOUR tints through
|
||||
// an RGBA region mask (R/G/B/A → tint_0..3), channel-BLENDED rather than
|
||||
// indexed-greyscale, so bilinear filtering cross-fades neighbouring regions
|
||||
// instead of smearing through wrong index levels at a boundary (feasibility G3).
|
||||
// A logo decal is then sampled on a dedicated UV2 (TEXCOORD_1) chest channel and
|
||||
// composited AFTER region tinting (brand colours stay faithful) but BEFORE the
|
||||
// toon shadow (the logo darkens with the fabric it sits on).
|
||||
//
|
||||
// Region-mask convention (authored by blender_author_offset_shell.py):
|
||||
// R = region 0 G = region 1 B = region 2 A = region 3
|
||||
// Per-texel channels partition to ~1.0; unmasked (all-zero) texels keep the
|
||||
// original fabric albedo untinted.
|
||||
|
||||
// Flat fabric base albedo (embedded in the garment GLB material).
|
||||
uniform sampler2D albedo_tex : source_color;
|
||||
// RGBA region mask, UV0-aligned. hint_default_black → a missing mask disables
|
||||
// tinting (falls back to the raw albedo) rather than tinting everything.
|
||||
uniform sampler2D region_mask : hint_default_black;
|
||||
// Logo decal, sampled on UV2. hint_default_transparent → an unbound logo
|
||||
// contributes nothing.
|
||||
uniform sampler2D logo_tex : source_color, hint_default_transparent;
|
||||
|
||||
// Per-region tints — fed from CharacterVisualDescriptor.clothing_tints[item][0..3].
|
||||
uniform vec4 tint_0 : source_color = vec4(0.80, 0.80, 0.80, 1.0);
|
||||
uniform vec4 tint_1 : source_color = vec4(0.80, 0.80, 0.80, 1.0);
|
||||
uniform vec4 tint_2 : source_color = vec4(0.80, 0.80, 0.80, 1.0);
|
||||
uniform vec4 tint_3 : source_color = vec4(0.80, 0.80, 0.80, 1.0);
|
||||
|
||||
// 0.0 = no logo (default; keeps every non-logo garment cheap), 1.0 = draw logo.
|
||||
uniform float logo_enabled : hint_range(0.0, 1.0) = 0.0;
|
||||
|
||||
// Toon shadow (same curve as toon_masked.gdshader for a consistent look).
|
||||
uniform float shadow_strength : hint_range(0.0, 1.0) = 0.2;
|
||||
uniform float shadow_threshold : hint_range(0.0, 1.0) = 0.3;
|
||||
|
||||
void fragment() {
|
||||
vec4 original = texture(albedo_tex, UV);
|
||||
vec4 m = texture(region_mask, UV);
|
||||
|
||||
// Luminance-preserving recolor: keep fabric shading detail, replace hue.
|
||||
float luma = dot(original.rgb, vec3(0.299, 0.587, 0.114));
|
||||
vec3 region_tint = m.r * tint_0.rgb
|
||||
+ m.g * tint_1.rgb
|
||||
+ m.b * tint_2.rgb
|
||||
+ m.a * tint_3.rgb;
|
||||
vec3 tinted = region_tint * (luma * 1.5 + 0.2); // scale luma to avoid too-dark
|
||||
// Blend toward the tint by total region weight; unmasked areas stay original.
|
||||
float region_weight = clamp(m.r + m.g + m.b + m.a, 0.0, 1.0);
|
||||
vec3 base = mix(original.rgb, tinted, region_weight);
|
||||
|
||||
// Logo decal on UV2, guarded to the authored [0,1] chest box.
|
||||
vec2 luv = UV2;
|
||||
float in_box = step(0.0, luv.x) * step(luv.x, 1.0)
|
||||
* step(0.0, luv.y) * step(luv.y, 1.0);
|
||||
vec4 logo = texture(logo_tex, luv);
|
||||
float logo_a = logo.a * in_box * logo_enabled;
|
||||
base = mix(base, logo.rgb, logo_a);
|
||||
|
||||
// Gentle toon shadow — subtle darkening on the shadow side.
|
||||
vec3 light_dir = normalize(vec3(0.3, -1.0, 0.5));
|
||||
float ndl = dot(NORMAL, -light_dir);
|
||||
float toon = smoothstep(shadow_threshold - 0.1, shadow_threshold + 0.1, ndl);
|
||||
ALBEDO = base * mix(1.0 - shadow_strength, 1.0, toon);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
## Tests for the T-1089 wardrobe engine: the toon_garment multi-region tint +
|
||||
## logo pipeline and the proof garment (tshirt_modern).
|
||||
##
|
||||
## Covers what is verifiable headless:
|
||||
## 1. The new toon_garment.gdshader loads/compiles.
|
||||
## 2. The region-mask CHANNEL MATH — pure GDScript replication of the shader's
|
||||
## RGBA→4-tint channel blend, asserting channel-blended (not indexed) routing.
|
||||
## 3. The manifest schema extension (style tag + logo_capable flag).
|
||||
## 4. coverage.json shape.
|
||||
## 5. Every fitted body variant loads as a skinned garment mesh.
|
||||
## 6. The logo + region-mask textures load.
|
||||
##
|
||||
## The shader itself runs on the GPU, so its arithmetic is re-implemented here in
|
||||
## GDScript and asserted against the same convention the .gdshader uses:
|
||||
## region_tint = m.r*t0 + m.g*t1 + m.b*t2 + m.a*t3
|
||||
## Ref: T-1089 gaps G3 (multi-region tint) + G4 (logo decal), D-251.
|
||||
class_name TestGarmentEngineT1089
|
||||
extends GdUnitTestSuite
|
||||
|
||||
const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader"
|
||||
const CLOTHING_DIR := "res://assets/characters/clothing/tshirt_modern/"
|
||||
const LOGO_PATH := "res://assets/characters/logos/thrds.png"
|
||||
const MANIFEST_PATH := "res://assets/characters/manifest.json"
|
||||
|
||||
const FITTED_BODIES := [
|
||||
"average_m", "average_f", "muscular_m", "muscular_f", "teen_m", "teen_f",
|
||||
]
|
||||
|
||||
|
||||
# --- 1. Shader loads -------------------------------------------------------
|
||||
|
||||
func test_toon_garment_shader_loads() -> void:
|
||||
assert_bool(ResourceLoader.exists(SHADER_PATH)).override_failure_message(
|
||||
"toon_garment.gdshader missing at %s" % SHADER_PATH
|
||||
).is_true()
|
||||
var shader := load(SHADER_PATH) as Shader
|
||||
assert_object(shader).is_not_null()
|
||||
assert_str(shader.code).contains("region_mask")
|
||||
assert_str(shader.code).contains("logo_tex")
|
||||
assert_str(shader.code).contains("UV2")
|
||||
|
||||
|
||||
func test_toon_garment_binds_to_material() -> void:
|
||||
# A ShaderMaterial with all four tints + logo params must accept them.
|
||||
var shader := load(SHADER_PATH) as Shader
|
||||
var mat := ShaderMaterial.new()
|
||||
mat.shader = shader
|
||||
mat.set_shader_parameter("tint_0", Color.RED)
|
||||
mat.set_shader_parameter("tint_1", Color.GREEN)
|
||||
mat.set_shader_parameter("tint_2", Color.BLUE)
|
||||
mat.set_shader_parameter("tint_3", Color.WHITE)
|
||||
mat.set_shader_parameter("logo_enabled", 1.0)
|
||||
assert_object(mat.get_shader_parameter("tint_2")).is_equal(Color.BLUE)
|
||||
|
||||
|
||||
# --- 2. Region-mask channel math (the testable shader logic) ---------------
|
||||
|
||||
## Pure-GDScript replication of the shader's per-texel region tint selection.
|
||||
func _region_tint(mask: Color, t0: Color, t1: Color, t2: Color, t3: Color) -> Color:
|
||||
var r := t0 * mask.r + t1 * mask.g + t2 * mask.b + t3 * mask.a
|
||||
return Color(r.r, r.g, r.b, 1.0)
|
||||
|
||||
|
||||
func test_pure_channel_routes_to_its_tint() -> void:
|
||||
var t0 := Color(0.1, 0.2, 0.3)
|
||||
var t1 := Color(0.4, 0.5, 0.6)
|
||||
var t2 := Color(0.7, 0.8, 0.9)
|
||||
var t3 := Color(0.0, 0.0, 0.0)
|
||||
# Pure collar (R) -> tint_0, body (G) -> tint_1, sleeve (B) -> tint_2
|
||||
assert_bool(_region_tint(Color(1, 0, 0, 0), t0, t1, t2, t3).is_equal_approx(t0)).is_true()
|
||||
assert_bool(_region_tint(Color(0, 1, 0, 0), t0, t1, t2, t3).is_equal_approx(t1)).is_true()
|
||||
assert_bool(_region_tint(Color(0, 0, 1, 0), t0, t1, t2, t3).is_equal_approx(t2)).is_true()
|
||||
|
||||
|
||||
func test_boundary_channel_blends_not_indexes() -> void:
|
||||
# The whole point of G3: a bilinear boundary texel (R=0.5,G=0.5) must produce
|
||||
# a MIDPOINT of the two tints — a crossfade — not snap to one index.
|
||||
var blended := _region_tint(
|
||||
Color(0.5, 0.5, 0.0, 0.0), Color(1, 0, 0), Color(0, 0, 1), Color.BLACK, Color.BLACK
|
||||
)
|
||||
assert_bool(blended.is_equal_approx(Color(0.5, 0.0, 0.5))).override_failure_message(
|
||||
"boundary texel must crossfade tints, got %s" % blended
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_zero_mask_contributes_no_tint() -> void:
|
||||
var out := _region_tint(Color(0, 0, 0, 0), Color.RED, Color.GREEN, Color.BLUE, Color.WHITE)
|
||||
assert_bool(out.is_equal_approx(Color.BLACK)).is_true()
|
||||
|
||||
|
||||
# --- 3. Manifest schema extension ------------------------------------------
|
||||
|
||||
func test_manifest_has_tshirt_with_style_and_logo_flag() -> void:
|
||||
var f := FileAccess.open(MANIFEST_PATH, FileAccess.READ)
|
||||
assert_object(f).is_not_null()
|
||||
var data: Variant = JSON.parse_string(f.get_as_text())
|
||||
f.close()
|
||||
assert_bool(data is Dictionary).is_true()
|
||||
var clothing: Dictionary = data["clothing"]
|
||||
assert_bool(clothing.has("tshirt_modern")).override_failure_message(
|
||||
"manifest.clothing missing tshirt_modern entry"
|
||||
).is_true()
|
||||
var entry: Dictionary = clothing["tshirt_modern"]
|
||||
assert_str(entry.get("slot", "")).is_equal("torso")
|
||||
assert_str(entry.get("style", "")).is_equal("modern")
|
||||
assert_bool(entry.get("logo_capable", false)).is_true()
|
||||
|
||||
|
||||
# --- 4. coverage.json ------------------------------------------------------
|
||||
|
||||
func test_coverage_json_shape() -> void:
|
||||
var path := CLOTHING_DIR + "coverage.json"
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
assert_object(f).is_not_null()
|
||||
var cov: Variant = JSON.parse_string(f.get_as_text())
|
||||
f.close()
|
||||
assert_bool(cov is Dictionary).is_true()
|
||||
assert_bool((cov as Dictionary).has("hides")).is_true()
|
||||
assert_array((cov as Dictionary)["hides"]).contains(["torso"])
|
||||
assert_str((cov as Dictionary).get("torso_variant", "")).is_equal("full")
|
||||
|
||||
|
||||
# --- 5. Fitted variants load as skinned garment meshes ---------------------
|
||||
|
||||
func test_all_fitted_bodies_load_skinned() -> void:
|
||||
for body in FITTED_BODIES:
|
||||
var glb := CLOTHING_DIR + "%s.glb" % body
|
||||
assert_bool(ResourceLoader.exists(glb)).override_failure_message(
|
||||
"fitted variant missing: %s" % glb
|
||||
).is_true()
|
||||
var scene := load(glb) as PackedScene
|
||||
assert_object(scene).override_failure_message(
|
||||
"variant %s failed to load as PackedScene" % body
|
||||
).is_not_null()
|
||||
var inst := scene.instantiate()
|
||||
var skinned := _has_skinned_mesh(inst)
|
||||
inst.queue_free()
|
||||
assert_bool(skinned).override_failure_message(
|
||||
"variant %s has no skinned MeshInstance3D (cannot animate)" % body
|
||||
).is_true()
|
||||
|
||||
|
||||
func _has_skinned_mesh(root: Node) -> bool:
|
||||
var stack: Array[Node] = [root]
|
||||
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:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# --- 6. Decal + mask textures load -----------------------------------------
|
||||
|
||||
func test_logo_and_mask_textures_load() -> void:
|
||||
assert_bool(ResourceLoader.exists(LOGO_PATH)).is_true()
|
||||
assert_bool(ResourceLoader.exists(CLOTHING_DIR + "reference_mask.png")).is_true()
|
||||
var logo := load(LOGO_PATH) as Texture2D
|
||||
assert_object(logo).is_not_null()
|
||||
var mask := load(CLOTHING_DIR + "reference_mask.png") as Texture2D
|
||||
assert_object(mask).is_not_null()
|
||||
@@ -0,0 +1,196 @@
|
||||
extends Node3D
|
||||
## Garment preview capture (T-1089 G3/G4 visual QA).
|
||||
##
|
||||
## Renders the fitted t-shirt worn on a body with the NEW toon_garment.gdshader:
|
||||
## RGBA-mask 4-region tints + a logo decal on the UV2 chest channel. This proves
|
||||
## the shader + region mask + logo pipeline end-to-end WITHOUT editing
|
||||
## character_visual.gd (which a parallel agent owns) — the garment meshes are
|
||||
## attached to the CharacterVisual skeleton and the shader is applied here as a
|
||||
## material_override, exactly the surface the lead's _apply_clothing_shader patch
|
||||
## will drive at runtime.
|
||||
##
|
||||
## Config via env vars (all optional):
|
||||
## GARMENT_PREVIEW_ITEM default "tshirt_modern"
|
||||
## GARMENT_PREVIEW_BODY default "average_m"
|
||||
## GARMENT_PREVIEW_OUT default "<repo>/.cache/garment-preview"
|
||||
##
|
||||
## Run: godot4 --path client --rendering-driver opengl3 \
|
||||
## res://tools/garment_preview/preview_scene.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,
|
||||
"teen_m": CharacterVisualDescriptor.BodyType.TEEN_M,
|
||||
"teen_f": CharacterVisualDescriptor.BodyType.TEEN_F,
|
||||
}
|
||||
|
||||
# Modern casual palette: light collar, teal body, charcoal sleeves.
|
||||
const TINT_COLLAR := Color(0.92, 0.93, 0.95) # R region
|
||||
const TINT_BODY := Color(0.15, 0.42, 0.48) # G region
|
||||
const TINT_SLEEVE := Color(0.20, 0.22, 0.26) # B region
|
||||
|
||||
var _out_dir := ""
|
||||
var _cam: Camera3D = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var item := _env("GARMENT_PREVIEW_ITEM", "tshirt_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 the chest logo + region tints 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
|
||||
var 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 on a clean rest-ish pose (idle) so the shirt reads without motion blur.
|
||||
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 + side + back so we can see the logo wherever the chest lands.
|
||||
for yaw in [0, 45, 180, 315]:
|
||||
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_COLLAR)
|
||||
mat.set_shader_parameter("tint_1", TINT_BODY)
|
||||
mat.set_shader_parameter("tint_2", TINT_SLEEVE)
|
||||
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 uid="uid://c8rtprev0garm1"]
|
||||
|
||||
[ext_resource type="Script" path="res://tools/garment_preview/preview_scene.gd" id="1_prev"]
|
||||
|
||||
[node name="GarmentPreview" type="Node3D"]
|
||||
script = ExtResource("1_prev")
|
||||