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:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user