feat(client): compositor plumb for multi-region garments + per-character logos (T-1089)

_apply_clothing_shader routes coverage.json multi_region garments to
toon_garment (4 tints, cascading fallback) with the brand logo resolved from
the new descriptor field clothing_logos (item_id -> brand id -> logos/<id>.png
— a logo is a per-CHARACTER wardrobe choice, so it rides the descriptor, not
the garment). Legacy garments keep the single-tint path untouched. Server-side
descriptor mirror stays parked with T-1060/Q-114 (appearance is client-local).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 22:21:52 +02:00
co-authored by Claude Fable 5
parent e22ea0fa4a
commit 278e8b7c21
2 changed files with 57 additions and 12 deletions
+50 -12
View File
@@ -45,6 +45,7 @@ const SKIN_TONE_DIR := BASE_PATH + "skin_tones/"
const EYE_IRIS_MASK_PATH := BASE_PATH + "bodies/eye_iris_mask.png"
const TOON_SHADER_PATH := BASE_PATH + "shaders/toon.gdshader"
const TOON_MASKED_SHADER_PATH := BASE_PATH + "shaders/toon_masked.gdshader"
const TOON_GARMENT_SHADER_PATH := BASE_PATH + "shaders/toon_garment.gdshader"
const OUTLINE_SHADER_PATH := BASE_PATH + "shaders/outline.gdshader"
## All body segment names in assembly order. D-160: 14 base + 2 swappable torso + 2 face = 18.
@@ -135,6 +136,7 @@ var _loaded_slots: Array[String] = [] # "hair", "facial_hair", etc.
var _toon_shader: Shader = null
var _toon_masked_shader: Shader = null
var _toon_garment_shader: Shader = null # T-1089: 4-region tint + UV2 logo
var _outline_shader: Shader = null
var _white_mask: ImageTexture = null # 1x1 white pixel — forces full-body tinting
var _iris_mask: Texture2D = null # iris-only mask from T_Eye_Split.png
@@ -147,6 +149,7 @@ func _ready() -> void:
func _load_shaders() -> void:
_toon_shader = load(TOON_SHADER_PATH) as Shader
_toon_masked_shader = load(TOON_MASKED_SHADER_PATH) as Shader
_toon_garment_shader = load(TOON_GARMENT_SHADER_PATH) as Shader
_outline_shader = load(OUTLINE_SHADER_PATH) as Shader
if not _toon_shader or not _toon_masked_shader or not _outline_shader:
push_error("CharacterVisual: shader load failed — check %s" % BASE_PATH)
@@ -589,6 +592,16 @@ func _load_clothing(desc: CharacterVisualDescriptor) -> void:
if ResourceLoader.exists(mask_path):
mask_tex = load(mask_path) as Texture2D
# T-1089 wardrobe engine: multi_region garments route to the 4-tint +
# logo shader; legacy garments (no flag) keep the single-tint path.
var multi_region: bool = coverage.get("multi_region", false)
var logo_tex: Texture2D = null
var brand_id: String = str(desc.clothing_logos.get(item_id, ""))
if not brand_id.is_empty():
var logo_path := BASE_PATH + "logos/%s.png" % brand_id
if ResourceLoader.exists(logo_path):
logo_tex = load(logo_path) as Texture2D
# Load the body-type-fitted clothing GLB
if ResourceLoader.exists(glb_path):
var scene := load(glb_path) as PackedScene
@@ -598,7 +611,7 @@ func _load_clothing(desc: CharacterVisualDescriptor) -> void:
for mi in _collect_meshes(inst):
mi.get_parent().remove_child(mi)
mi.owner = null
_apply_clothing_shader(mi, tints, mask_tex)
_apply_clothing_shader(mi, tints, mask_tex, multi_region, logo_tex)
_skeleton.add_child(mi)
_clothing_meshes.append(mi)
inst.queue_free()
@@ -621,26 +634,51 @@ func _read_coverage(path: String) -> Dictionary:
return {}
func _apply_clothing_shader(mi: MeshInstance3D, tints: Array, mask_tex: Texture2D = null) -> void:
func _apply_clothing_shader(
mi: MeshInstance3D,
tints: Array,
mask_tex: Texture2D = null,
multi_region: bool = false,
logo_tex: Texture2D = null,
) -> void:
if mi.mesh == null:
return
var tint: Color = tints[0] if not tints.is_empty() else Color(0.7, 0.65, 0.6)
for surf in range(mi.mesh.get_surface_count()):
var orig: Material = mi.mesh.surface_get_material(surf)
var tex := _get_albedo_texture(orig)
var mat := ShaderMaterial.new()
if tex and _toon_masked_shader:
mat.shader = _toon_masked_shader
if multi_region and tex and _toon_garment_shader:
# T-1089: RGBA-mask 4-region tint + UV2 logo decal (toon_garment).
mat.shader = _toon_garment_shader
mat.set_shader_parameter("albedo_tex", tex)
mat.set_shader_parameter("tint_color", tint)
var t0: Color = tints[0] if tints.size() > 0 else Color(0.7, 0.65, 0.6)
var t1: Color = tints[1] if tints.size() > 1 else t0
var t2: Color = tints[2] if tints.size() > 2 else t1
var t3: Color = tints[3] if tints.size() > 3 else t2
mat.set_shader_parameter("tint_0", t0)
mat.set_shader_parameter("tint_1", t1)
mat.set_shader_parameter("tint_2", t2)
mat.set_shader_parameter("tint_3", t3)
mat.set_shader_parameter("shadow_strength", 0.2)
if mask_tex:
mat.set_shader_parameter("recolor_mask", mask_tex)
elif _toon_shader:
mat.shader = _toon_shader
mat.set_shader_parameter("base_color", tint)
mat.set_shader_parameter("shadow_color", tint.darkened(0.4))
mat.set_shader_parameter("shadow_threshold", 0.4)
mat.set_shader_parameter("region_mask", mask_tex)
if logo_tex:
mat.set_shader_parameter("logo_tex", logo_tex)
mat.set_shader_parameter("logo_enabled", 1.0)
else:
var tint: Color = tints[0] if not tints.is_empty() else Color(0.7, 0.65, 0.6)
if tex and _toon_masked_shader:
mat.shader = _toon_masked_shader
mat.set_shader_parameter("albedo_tex", tex)
mat.set_shader_parameter("tint_color", tint)
mat.set_shader_parameter("shadow_strength", 0.2)
if mask_tex:
mat.set_shader_parameter("recolor_mask", mask_tex)
elif _toon_shader:
mat.shader = _toon_shader
mat.set_shader_parameter("base_color", tint)
mat.set_shader_parameter("shadow_color", tint.darkened(0.4))
mat.set_shader_parameter("shadow_threshold", 0.4)
mi.set_surface_override_material(surf, mat)
@@ -104,6 +104,11 @@ var accessory_slots: Dictionary = {}
## Changed from single Color per Tyre architecture review (Task #6).
var accessory_tints: Dictionary = {}
## item_id → brand id, resolved to assets/characters/logos/<id>.png (T-1089).
## A logo is a per-CHARACTER wardrobe choice (which brand THIS shirt wears),
## so it rides the descriptor, not the garment's coverage.json.
var clothing_logos: Dictionary = {}
## Get the file key for the current body type (e.g. "average_m").
func body_type_key() -> String:
@@ -139,6 +144,7 @@ static func from_dict(data: Dictionary) -> CharacterVisualDescriptor:
desc.clothing_tints = _decode_tint_map(data.get("clothing_tints", {}))
desc.accessory_slots = data.get("accessory_slots", {})
desc.accessory_tints = _decode_tint_map(data.get("accessory_tints", {}))
desc.clothing_logos = data.get("clothing_logos", {})
return desc
@@ -160,6 +166,7 @@ func to_dict() -> Dictionary:
"clothing_tints": _encode_tint_map(clothing_tints),
"accessory_slots": accessory_slots,
"accessory_tints": _encode_tint_map(accessory_tints),
"clothing_logos": clothing_logos,
}