// ───────────────────────────────────────────────────────────────────────────── // WLWeaponPalette.cs — 무기를 팔레트 아틀라스 UV(도트)로 갈아끼운다 (WL-816b) // // PD 지시 #816 「이 화풍으로 무기와 코스튬으로 넓히자」 · 발주서 WL-816b // // ■ 왜 필요한가 (인게임 Play 실측 · 게임 크기 1080×1920 · ortho 10) // 무기는 캐릭터(2,959 px)보다 훨씬 작다 — `Elven_Sword_01` 304 px 뿐인데 **252 색**이 보였다 // (서로 다른 색 82.9 % = 캐릭터가 814x 이전에 앓던 「흐린 사진」 그대로다). // PurePoly 9종은 이미 팔레트 텍스처(`PP_Color_Palette`)를 쓰지만 **Bilinear + 밉 + ASTC 압축**이라 // 화면에서 칸이 섞여 27~44 %가 나왔다. 그래서 캐릭터와 같은 처방이 그대로 듣는다. // // ■ 무엇을 하나 — 814x 와 동일 // 메시 복사본의 UV 를 팔레트 텍스처의 **칸 한가운데**로 보낸다(면이 영구 단색) + // 머티리얼을 캐릭터 팔레트 머티리얼(`M05_ToonP`)과 같은 셰이더·같은 값으로 바꾼다. // 🔴 팔레트 텍스처는 32×32 · 칸 8 px · Point · **밉 OFF** · 무압축 — 밉을 끄는 게 핵심이다. // // ■ 언제 부르나 — `WLWeaponFitter.Scan` (LateUpdate 소켓 스캔) // 무기는 `PCActor.Make_Weapon` 의 **Addressables 비동기 콜백**으로 붙어서 생성 프레임을 알 수 없다. // 814p 가 같은 이유로 만든 소켓 스캔에 얹는다 = 새 훅·새 컴포넌트 0. // 🔴 크기 보정(`WLWeaponFitSettings`)과 **독립**이다 — 그쪽을 꺼도 팔레트는 돈다(반대도 같다). // 메시 복사본은 정점 좌표가 원본과 같아(UV 만 바뀐다) 바운즈가 같으므로 배수 계산에 영향이 없다. // // ■ 되돌리기 // `WLCharacterPaletteSettings.weaponEnabled_ = 0` → 무기만 원본 100 %. // `enabled_ = 0` → 캐릭터·무기 전부 off (= 814s + 814t 상태 100 %). // // ■ 성능 / GC // 무기 인스턴스 1개당 1회. 표에 없는 무기는 이름 비교 몇 번으로 끝난다(대입 0). // // 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙). // ───────────────────────────────────────────────────────────────────────────── using System.Collections.Generic; using UnityEngine; namespace WL.Look.Character { public static class WLWeaponPalette { // 진단(프로브가 읽는다) public static int AppliedWeapons, AppliedMeshes, AppliedRenderers; public static string LastLog = ""; static readonly List s_mf = new List(8); static readonly List s_rd = new List(8); /// 소켓에 새로 붙은 무기 하나를 팔레트 메시·머티리얼로 갈아끼운다. 표에 없으면 무동작. public static bool Apply(Transform weaponRoot) { if (weaponRoot == null) return false; var cfg = WLCharacterPaletteSettings.Instance; if (cfg == null || cfg.enabled_ == 0 || cfg.weaponEnabled_ == 0) return false; var names = cfg.weaponNames; if (names == null || names.Length == 0) return false; int idx = IndexOf(names, Strip(weaponRoot.name)); if (idx < 0) { // 이름이 바뀐 인스턴스 대비 — 메시 이름으로 한 번 더 찾는다(원본 FBX/obj 메시 이름). var mf0 = weaponRoot.GetComponentInChildren(true); if (mf0 != null && mf0.sharedMesh != null) idx = IndexOf(names, Strip(mf0.sharedMesh.name)); if (idx < 0) return false; } Mesh mesh = cfg.weaponMeshes != null && idx < cfg.weaponMeshes.Length ? cfg.weaponMeshes[idx] : null; Material mat = cfg.weaponMaterials != null && idx < cfg.weaponMaterials.Length ? cfg.weaponMaterials[idx] : null; if (mesh == null && mat == null) return false; int meshes = 0, rends = 0; if (mesh != null) { s_mf.Clear(); weaponRoot.GetComponentsInChildren(true, s_mf); for (int i = 0; i < s_mf.Count; i++) { var f = s_mf[i]; if (f == null || f.sharedMesh == mesh) continue; f.sharedMesh = mesh; meshes++; } } if (mat != null) { s_rd.Clear(); weaponRoot.GetComponentsInChildren(true, s_rd); for (int i = 0; i < s_rd.Count; i++) { var r = s_rd[i]; if (r == null || r is ParticleSystemRenderer || r is TrailRenderer || r is LineRenderer) continue; var ms = r.sharedMaterials; bool changed = false; for (int s = 0; s < ms.Length; s++) if (ms[s] != mat) { ms[s] = mat; changed = true; } if (changed) { r.sharedMaterials = ms; rends++; } } } AppliedWeapons++; AppliedMeshes += meshes; AppliedRenderers += rends; LastLog = names[idx] + " · 메시 " + meshes + " · 렌더러 " + rends; if (cfg.verboseLog != 0) Debug.Log("[WL816b WeaponPalette] " + LastLog); return meshes + rends > 0; } /// 인스턴스 접미사 "(Clone)" 을 뗀다. static string Strip(string n) { if (string.IsNullOrEmpty(n)) return n; int i = n.IndexOf("(Clone)"); return i >= 0 ? n.Substring(0, i).TrimEnd() : n; } static int IndexOf(string[] names, string n) { for (int i = 0; i < names.Length; i++) if (names[i] == n) return i; return -1; } /// 진단 카운터 초기화(프로브 전용). public static void ResetCounters() { AppliedWeapons = 0; AppliedMeshes = 0; AppliedRenderers = 0; LastLog = ""; } } }