// ───────────────────────────────────────────────────────────────────────────── // WLEquipPreview.cs — 장비 화면 3D 캐릭터 미리보기 (2026-10-12 · 레퍼런스 「캐릭터 중심 장비 화면」) // // 모델 = 코스튬 프리팹을 `WLCharacterSwapSettings.Resolve` 로 바꾼 이름(실제 PC 생성 PCInfo.cs:16 과 같은 규칙 = 붉은 기사 LH_M05). // 힘민지 원본 `UI3DModel`(EquipmentUI 의 PC 모델 로더 · Addressables PC 프리팹 + Stop + Set_UIPC + 물리 제거)을 그대로 상속하고, // 원본은 화면 카메라(depth 6 · 직교)로 바로 그리지만 WL 장비 GUI 는 ScreenSpaceOverlay 캔버스라 **RenderTexture 카메라**로 그려 RawImage 에 띄운다. // 리그는 월드 밖(hub.rigY · 기본 −2000)에 두고 카메라는 "UI" 레이어만 본다(UI3DModel 이 모델 전체를 UI 레이어로 바꾼다). // 장착 무기 → 직업(WLWeaponAttackTable.ClassIdFor)으로 무기 모델을 붙인다. 섬(로비 모드)은 Set_Obj 가 무기를 안 붙이므로 // WLWeaponClassApplier 와 같이 호출 동안만 게임 모드를 Stage 로 바꾼다. 치수·카메라 값은 WLEquipGui.json `hub` (C45). // ───────────────────────────────────────────────────────────────────────────── using System.Reflection; using UnityEngine; namespace WL.Equip { public class WLEquipPreview : UI3DModel { public static WLEquipPreview Instance { get; private set; } public static string LastLog = ""; public static int Loads, ClassApplies; public RenderTexture Texture { get { return _rt; } } public int ShownClassId { get; private set; } public string ShownPrefab { get; private set; } public bool HasModel { get { return tf_model != null && tf_model.childCount > 1; } } public bool Loading { get { return _loading; } } Camera _cam; RenderTexture _rt; Light _light; WLGuiHub _h; PCActor _pc; bool _loading; int _pendingClass; float _orientUntil = -1f; public static WLEquipPreview Ensure(WLGuiHub h) { if (Instance != null) return Instance; var go = new GameObject("WL_EquipPreviewRig"); DontDestroyOnLoad(go); go.transform.position = new Vector3(0f, h.rigY, 0f); var p = go.AddComponent(); p._h = h; p.Build(); Instance = p; return p; } void Build() { var h = _h; _rt = new RenderTexture(Mathf.Max(64, h.previewRtW), Mathf.Max(64, h.previewRtH), 24, RenderTextureFormat.ARGB32); _rt.name = "WL_EquipPreviewRT"; _rt.Create(); var camGo = new GameObject("Camera"); camGo.transform.SetParent(transform, false); camGo.transform.localPosition = new Vector3(0f, h.camY, -h.camDist); _cam = camGo.AddComponent(); _cam.orthographic = true; _cam.orthographicSize = h.camOrtho; _cam.nearClipPlane = 0.05f; _cam.farClipPlane = h.camDist * 2f + 5f; _cam.clearFlags = CameraClearFlags.SolidColor; _cam.backgroundColor = new Color(0f, 0f, 0f, 0f); _cam.cullingMask = 1 << 5; _cam.targetTexture = _rt; _cam.depth = -50f; _cam.allowHDR = false; _cam.allowMSAA = false; _cam.useOcclusionCulling = false; _cam.enabled = false; var model = new GameObject("tf_model"); model.transform.SetParent(transform, false); tf_model = model.transform; var lightGo = new GameObject("Light"); lightGo.transform.SetParent(tf_model, false); // 0 번 자식 = 빛(UI3DModel 규약 · Set_3DModel 이 1 번부터 지운다) lightGo.transform.localRotation = Quaternion.Euler(h.lightPitch, h.lightYaw, 0f); _light = lightGo.AddComponent(); _light.type = LightType.Directional; _light.cullingMask = 1 << 5; _light.shadows = LightShadows.None; _light.intensity = h.lightIntensity; _light.enabled = h.lightIntensity > 0f; // 기본 0 = 장면 태양광(무한 방향광)만 — URP 메인 라이트 선정을 건드리지 않는다 } public void SetVisible(bool on) { if (_cam != null) _cam.enabled = on; } /// 현재 코스튬·장착 무기(직업)로 모델을 맞춘다. 바뀐 것이 없으면 아무 일도 없다. public void Refresh() { string prefab = null; int classId = 0; try { var sd = ServerInfo.Ins != null ? ServerInfo.Ins.m_ServerData : null; if (sd == null) { LastLog = "서버 데이터 없음(타이틀)"; return; } var cd = table_costumelist.Ins.Get_Data_orNull(sd.Get_EqiupmentEquip(eItem.PCCostume)); // 실제 PC 와 같은 교체 규칙(PCInfo.cs:16 · PD #814 「붉은 기사 M05」) — 빠뜨리면 원본 코스튬 Ai01 이 나온다(2026-10-12 PD 지적) prefab = cd != null ? WL.Character.WLCharacterSwapSettings.Resolve(cd.s_Prefab) : null; var w = WLInventory.EquippedInSlot(eFREquipmentType.Weapon); if (w != null && w.itemJson != null) classId = WL.Combat.Weapon.WLWeaponAttackTable.ClassIdFor(w.itemJson.GroupNo); if (classId <= 0 && MyValue.bMyPC && MyValue.MyPC != null) classId = MyValue.MyPC.Get_ID(); if (classId <= 0) classId = sd.Get_EqiupmentEquip(eItem.PC); } catch (System.Exception ex) { LastLog = "미리보기 정보 실패 — " + ex.Message; return; } if (string.IsNullOrEmpty(prefab)) { LastLog = "코스튬 프리팹 없음"; return; } if (_loading) { _pendingClass = classId; return; } if (prefab == ShownPrefab && HasModel && _pc != null) { if (classId != ShownClassId) ApplyClass(_pc, classId); return; } ShownPrefab = prefab; _pendingClass = classId; _pc = null; _loading = true; Loads++; try { Set_3DModel(true, prefab, tr => { _loading = false; tr.localPosition = Vector3.zero; tr.localRotation = Quaternion.Euler(0f, _h.modelYaw, 0f); _pc = tr.GetComponent(); if (_pc != null) ApplyClass(_pc, _pendingClass); LastLog = "모델 " + prefab + " · 직업 " + ShownClassId; }); } catch (System.Exception ex) { _loading = false; LastLog = "모델 로드 실패 — " + ex.Message; } } static FieldInfo FindField(System.Type t, string name) { while (t != null) { var f = t.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (f != null) return f; t = t.BaseType; } return null; } /// 직업(무기 모델·애니메이터)을 UI 모델에 적용 — Change_Class 는 m_Stat 이 없는 UI 클론에서 예외를 내므로 필드 주입 + Set_Obj. void ApplyClass(PCActor pc, int classId) { ShownClassId = classId; ClassConfigTableData cd = null; try { cd = table_classconfig.Ins.Get_Data_orNull(classId); } catch (System.Exception) { cd = null; } if (cd == null) { LastLog = "ClassConfig 없음 " + classId; return; } try { WL.Combat.Weapon.WLWeaponClasses.Ensure(); } catch (System.Exception) { } try { var fClass = FindField(pc.GetType(), "m_ClassData"); if (fClass != null) fClass.SetValue(pc, cd); var fId = FindField(pc.GetType(), "m_ID"); if (fId != null) fId.SetValue(pc, classId); var fSd = FindField(pc.GetType(), "m_ServerData"); if (fSd != null && fSd.GetValue(pc) == null && ServerInfo.Ins != null) fSd.SetValue(pc, ServerInfo.Ins.m_ServerData); } catch (System.Exception ex) { LastLog = "필드 주입 실패 — " + ex.Message; } var info = InGameInfo.Ins; var gf = info != null ? typeof(InGameInfo).GetField("m_GameMode", BindingFlags.Instance | BindingFlags.NonPublic) : null; object prev = gf != null ? gf.GetValue(info) : null; bool flipped = false; try { if (gf != null && prev is eGameMode && (eGameMode)prev == eGameMode.Lobby) { gf.SetValue(info, eGameMode.Stage); flipped = true; } pc.Set_Obj(); ClassApplies++; } catch (System.Exception ex) { LastLog = "Set_Obj 실패 — " + ex.Message; } finally { if (flipped) gf.SetValue(info, prev); } try { DSUtil.ChangeLayersRecursively(pc.transform, "UI"); } catch (System.Exception) { } _orientUntil = Time.unscaledTime + 3f; // 무기 인스턴스는 Addressables 로 늦게 붙는다 → 3 s 동안 레이어·방향 보정을 다시 건다 } void Update() { if (_pc == null || _orientUntil < 0f || Time.unscaledTime > _orientUntil) return; try { WL.Combat.Weapon.WLWeaponClasses.EnsureWeaponOrient(_pc); DSUtil.ChangeLayersRecursively(_pc.transform, "UI"); } catch (System.Exception) { } } void OnDestroy() { if (Instance == this) Instance = null; if (_rt != null) { _rt.Release(); Destroy(_rt); _rt = null; } } } }