diff --git a/client/scripts/rendering/character_visual.gd b/client/scripts/rendering/character_visual.gd index c59ee0cac..26cc19dc0 100644 --- a/client/scripts/rendering/character_visual.gd +++ b/client/scripts/rendering/character_visual.gd @@ -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) diff --git a/client/scripts/rendering/character_visual_descriptor.gd b/client/scripts/rendering/character_visual_descriptor.gd index d248fc525..4ede3663b 100644 --- a/client/scripts/rendering/character_visual_descriptor.gd +++ b/client/scripts/rendering/character_visual_descriptor.gd @@ -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/.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, }