From 9b8f7e968b48c02430ac9221688219b25b91d3d8 Mon Sep 17 00:00:00 2001 From: swrring Date: Sat, 12 Sep 2026 23:51:28 +0900 Subject: [PATCH] =?UTF-8?q?[WL-814t]=20=EB=A0=88=ED=8D=BC=EB=9F=B0?= =?UTF-8?q?=EC=8A=A4=20=EB=8A=90=EB=82=8C(=EC=99=B8=EA=B3=BD=EC=84=A0?= =?UTF-8?q?=C2=B7=EB=8C=80=EB=B9=84=C2=B7=EC=83=89=EC=A1=B0=C2=B7=EB=A6=BC?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=8A=B8)=20=EC=98=B5=EC=85=98=ED=99=94=20+?= =?UTF-8?q?=20=EC=A2=85=ED=95=A9=EC=95=88=20(#814)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- AgentScripts/WL814t_Capture.cs | 192 ++++++++++ Assets/WL/Look/Arena/Post.meta | 8 + Assets/WL/Look/Arena/Post/WL_ArenaMood.asset | 179 +++++++++ .../Look/Arena/Post/WL_ArenaMood.asset.meta | 8 + Assets/WL/Look/Arena/Resources.meta | 8 + Assets/WL/Look/Arena/Resources/WL.meta | 8 + .../WL/WLReferenceLookSettings.asset | 44 +++ .../WL/WLReferenceLookSettings.asset.meta | 8 + .../WL/Look/Arena/Scenes/WL_ArenaProto.unity | 47 +++ Assets/WL/Look/Arena/WLReferenceLook.cs | 343 ++++++++++++++++++ Assets/WL/Look/Arena/WLReferenceLook.cs.meta | 2 + .../WL/Look/Arena/WLReferenceLookSettings.cs | 130 +++++++ .../Arena/WLReferenceLookSettings.cs.meta | 2 + Assets/WL/Look/Character/Shaders.meta | 8 + .../Character/Shaders/WLHullOutline.shader | 120 ++++++ .../Shaders/WLHullOutline.shader.meta | 9 + .../WL/Look/Character/Shaders/WLRimAdd.shader | 102 ++++++ .../Character/Shaders/WLRimAdd.shader.meta | 9 + 18 files changed, 1227 insertions(+) create mode 100644 AgentScripts/WL814t_Capture.cs create mode 100644 Assets/WL/Look/Arena/Post.meta create mode 100644 Assets/WL/Look/Arena/Post/WL_ArenaMood.asset create mode 100644 Assets/WL/Look/Arena/Post/WL_ArenaMood.asset.meta create mode 100644 Assets/WL/Look/Arena/Resources.meta create mode 100644 Assets/WL/Look/Arena/Resources/WL.meta create mode 100644 Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset create mode 100644 Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset.meta create mode 100644 Assets/WL/Look/Arena/WLReferenceLook.cs create mode 100644 Assets/WL/Look/Arena/WLReferenceLook.cs.meta create mode 100644 Assets/WL/Look/Arena/WLReferenceLookSettings.cs create mode 100644 Assets/WL/Look/Arena/WLReferenceLookSettings.cs.meta create mode 100644 Assets/WL/Look/Character/Shaders.meta create mode 100644 Assets/WL/Look/Character/Shaders/WLHullOutline.shader create mode 100644 Assets/WL/Look/Character/Shaders/WLHullOutline.shader.meta create mode 100644 Assets/WL/Look/Character/Shaders/WLRimAdd.shader create mode 100644 Assets/WL/Look/Character/Shaders/WLRimAdd.shader.meta diff --git a/AgentScripts/WL814t_Capture.cs b/AgentScripts/WL814t_Capture.cs new file mode 100644 index 000000000..e51491d91 --- /dev/null +++ b/AgentScripts/WL814t_Capture.cs @@ -0,0 +1,192 @@ +// WL-814t 캡처 — 레퍼런스 느낌 4요소 단독/단계/종합 (Play 중 한 프레임 안에서 전부 렌더) +// 한 프레임 안에서 찍는 이유: 구름 그림자(_Time)·포즈가 프레임마다 움직여 세션 간 비교가 노이즈투성이가 된다(814s 미확인⑤). +// run_script --file AgentScripts/WL814t_Capture.cs --entry WL814t_Capture.Run + +using System.Collections.Generic; +using UnityEngine; +using WL.Look.Arena; + +public static class WL814t_Capture +{ + const string Dir = @"E:\NerdNavis\WL_wt\gameplay\Screenshots_WL\WL814t"; + const int W = 1080, H = 1920; + const float Close = 3.4f; // 도트A 프리셋과 같은 직교 반높이 = 비교 기준 + const float Wide = 10f; // 아레나 씬의 실제 카메라 + + static Camera s_cam; + static Vector3 s_camPos, s_charCenter; + static float s_camSize; + static System.Text.StringBuilder s_log; + + public static string Run() + { + s_log = new System.Text.StringBuilder(); + System.IO.Directory.CreateDirectory(Dir); + + s_cam = Camera.main; + if (s_cam == null) { var cs = Object.FindObjectsByType(FindObjectsSortMode.None); if (cs.Length > 0) s_cam = cs[0]; } + if (s_cam == null) return "no camera"; + s_camPos = s_cam.transform.position; s_camSize = s_cam.orthographicSize; + + var pc = GameObject.Find("PC_LH_M05"); + var rs = pc != null ? pc.GetComponentsInChildren(false) : new Renderer[0]; + Bounds b = new Bounds(pc != null ? pc.transform.position : Vector3.zero, Vector3.zero); + bool first = true; + foreach (var r in rs) { if (r == null || !r.enabled) continue; if (first) { b = r.bounds; first = false; } else b.Encapsulate(r.bounds); } + s_charCenter = b.center; + s_log.AppendLine("charCenter=" + s_charCenter.ToString("F3") + " bodyH=" + b.size.y.ToString("F3") + + " · 화면 캐릭터 키 @ortho3.4 = " + (b.size.y / (2f * Close) * H).ToString("F0") + " px · @ortho10 = " + (b.size.y / (2f * Wide) * H).ToString("F0") + " px"); + + var cfg = WLReferenceLookSettings.Instance; + if (cfg == null) return "no settings asset"; + + // ── ⓐ 지금(814s) — 아무것도 안 얹은 상태 + Shot2("a_now", Nothing(cfg), Close, 0); + Shot2("a_now_wide", Nothing(cfg), Wide, 0); + Shot2("a_now_pixel", Nothing(cfg), Close, 240); + + // ── 4요소 단독 + Shot2("b_outline_only", Only(cfg, true, false, false, false), Close, 0); + Shot2("c_contrast_only", Only(cfg, false, true, false, false), Close, 0); + Shot2("d_mood_only", Only(cfg, false, false, true, false), Close, 0); + Shot2("e_rim_only", Only(cfg, false, false, false, true), Close, 0); + + // ── Ⓐ 외곽선 4단 (PD: 굵게 · 상한을 올린 2/4/6/8 px) + float[] ws = { 2f, 4f, 6f, 8f }; + for (int i = 0; i < ws.Length; i++) + { + var o = Only(cfg, true, false, false, false); + o.outlineWidth = ws[i]; o.outlineWidthBackground = Mathf.Max(1f, ws[i] * 0.45f); + Shot2("o" + (i + 1) + "_outline_" + ws[i].ToString("F0") + "px", o, Close, 0); + } + // 외곽선 색 — 순검정 vs 아주 어두운 보라 + { + var o = Only(cfg, true, false, false, false); o.outlineWidth = 6f; o.outlineWidthBackground = 2.7f; + o.outlineColor = Color.black; Shot2("o5_color_pureblack", o, Close, 0); + o.outlineColor = new Color(0.043f, 0.031f, 0.067f, 1f); Shot2("o6_color_darkviolet", o, Close, 0); + } + + // ── Ⓑ 대비 3단 + float[] mds = { 0.10f, 0.05f, 0.0f }; + for (int i = 0; i < mds.Length; i++) + { + var o = Only(cfg, false, true, false, false); o.minimumDarkness = mds[i]; o.charShades = 0; + Shot2("c" + (i + 1) + "_darkness_" + mds[i].ToString("F2"), o, Close, 0); + } + + // ── Ⓑ2 캐릭터 색단계 7 / 4 / 3 (레퍼런스2 = 부위당 2~3단) + int[] shades = { 0, 4, 3 }; + for (int i = 0; i < shades.Length; i++) + { + var o = Only(cfg, false, true, false, false); o.charShades = shades[i]; + Shot2("s" + (i + 1) + "_shades_" + (shades[i] == 0 ? 7 : shades[i]), o, Close, 0); + } + + // ── Ⓒ 분위기 3단 + float[] ms = { 0.35f, 0.65f, 1.0f }; + for (int i = 0; i < ms.Length; i++) + { + var o = Only(cfg, false, false, true, false); o.moodStrength = ms[i]; + Shot2("m" + (i + 1) + "_mood_" + ms[i].ToString("F2"), o, Close, 0); + } + + // ── Ⓓ 림 3단 + float[] rss = { 0.6f, 1.25f, 2.2f }; + for (int i = 0; i < rss.Length; i++) + { + var o = Only(cfg, false, false, false, true); o.rimStrength = rss[i]; + Shot2("r" + (i + 1) + "_rim_" + rss[i].ToString("F2"), o, Close, 0); + } + + // ── ⓕ 종합안 (비픽셀) + var fin = WLReferenceLook.Opt.FromSettings(cfg); + Shot2("f_final", fin, Close, 0); + Shot2("f_final_wide", fin, Wide, 0); + + // ── ⓕ2 종합안 + 도트 — 도트A 프리셋(orthoSize 3.4 · pixelHeight 240) + var fp = fin; fp.outlineWidth = cfg.outlineWidthPixelMode; fp.outlineWidthBackground = Mathf.Max(0.5f, cfg.outlineWidthPixelMode * 0.5f); + Shot2("f_final_pixel", fp, Close, 240); + var fp2 = fp; fp2.outlineWidth = cfg.outlineWidthPixelMode * 2f; fp2.outlineWidthBackground = cfg.outlineWidthPixelMode; + Shot2("f_final_pixel_w2", fp2, Close, 240); + Shot2("f_final_pixel_thick", fin, Close, 240); // 비픽셀 두께 그대로 → 저해상도에서 뭉개지는지 + + // ── 원복 확인 + Shot2("z_restored_check", Nothing(cfg), Close, 0); + s_cam.transform.position = s_camPos; s_cam.orthographicSize = s_camSize; + + s_log.AppendLine("DONE"); + return s_log.ToString(); + } + + static WLReferenceLook.Opt Nothing(WLReferenceLookSettings cfg) + { + var o = WLReferenceLook.Opt.FromSettings(cfg); + o.outline = false; o.contrast = false; o.mood = false; o.rim = false; + return o; + } + + static WLReferenceLook.Opt Only(WLReferenceLookSettings cfg, bool a, bool b, bool c, bool d) + { + var o = WLReferenceLook.Opt.FromSettings(cfg); + o.outline = a; o.contrast = b; o.mood = c; o.rim = d; + return o; + } + + /// 카메라를 먼저 맞춘 뒤(줌 비례 두께가 카메라를 읽는다) 적용 → 렌더. + static void Shot2(string name, WLReferenceLook.Opt o, float orthoSize, int pixelHeight) + { + s_cam.orthographicSize = orthoSize; + s_cam.transform.position = s_charCenter - s_cam.transform.forward * 30f; + WLReferenceLook.Apply(o, WLReferenceLookSettings.Instance); + Shot(name, pixelHeight); + } + + /// 한 장 렌더. pixelHeight 0 = 1080×1920 그대로, >0 = 저해상도 RT 로 찍어 점보간 확대(도트). + static void Shot(string name, int pixelHeight) + { + int rw = W, rh = H; + if (pixelHeight > 0) { rh = pixelHeight; rw = Mathf.RoundToInt(pixelHeight * (float)W / H); } + + var rt = new RenderTexture(rw, rh, 24, RenderTextureFormat.ARGB32); + rt.antiAliasing = 1; + rt.filterMode = pixelHeight > 0 ? FilterMode.Point : FilterMode.Bilinear; + rt.Create(); + + var prevTarget = s_cam.targetTexture; + s_cam.targetTexture = rt; + s_cam.Render(); + s_cam.targetTexture = prevTarget; + + var prevActive = RenderTexture.active; + RenderTexture.active = rt; + var src = new Texture2D(rw, rh, TextureFormat.RGB24, false); + src.ReadPixels(new Rect(0, 0, rw, rh), 0, 0); + src.Apply(); + RenderTexture.active = prevActive; + + Texture2D outTex = src; + if (pixelHeight > 0) + { + outTex = new Texture2D(W, H, TextureFormat.RGB24, false); + var sp = src.GetPixels32(); + var dp = new Color32[W * H]; + for (int y = 0; y < H; y++) + { + int sy = Mathf.Min(rh - 1, y * rh / H); + for (int x = 0; x < W; x++) + { + int sx = Mathf.Min(rw - 1, x * rw / W); + dp[y * W + x] = sp[sy * rw + sx]; + } + } + outTex.SetPixels32(dp); outTex.Apply(); + } + + System.IO.File.WriteAllBytes(System.IO.Path.Combine(Dir, name + ".png"), outTex.EncodeToPNG()); + s_log.AppendLine("shot " + name + " " + rw + "x" + rh + " · ortho " + s_cam.orthographicSize.ToString("F2") + " · " + WLReferenceLook.LastLog); + + if (outTex != src) Object.Destroy(outTex); + Object.Destroy(src); + rt.Release(); Object.Destroy(rt); + } +} diff --git a/Assets/WL/Look/Arena/Post.meta b/Assets/WL/Look/Arena/Post.meta new file mode 100644 index 000000000..6f42ad859 --- /dev/null +++ b/Assets/WL/Look/Arena/Post.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 269ec43cd1eb2794e9f12763ccdb9af5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look/Arena/Post/WL_ArenaMood.asset b/Assets/WL/Look/Arena/Post/WL_ArenaMood.asset new file mode 100644 index 000000000..4d7ddc8ba --- /dev/null +++ b/Assets/WL/Look/Arena/Post/WL_ArenaMood.asset @@ -0,0 +1,179 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-7716237022740010770 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 558a8e2b6826cf840aae193990ba9f2e, type: 3} + m_Name: ShadowsMidtonesHighlights + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.ShadowsMidtonesHighlights + active: 1 + shadows: + m_OverrideState: 1 + m_Value: {x: 0.78, y: 0.58, z: 1.25, w: -0.06} + midtones: + m_OverrideState: 1 + m_Value: {x: 1, y: 0.96, z: 1.05, w: 0} + highlights: + m_OverrideState: 1 + m_Value: {x: 1.14, y: 1.02, z: 0.74, w: 0.03} + shadowsStart: + m_OverrideState: 0 + m_Value: 0 + shadowsEnd: + m_OverrideState: 0 + m_Value: 0.3 + highlightsStart: + m_OverrideState: 0 + m_Value: 0.55 + highlightsEnd: + m_OverrideState: 0 + m_Value: 1 +--- !u!114 &-6414152611911942192 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0b2db86121404754db890f4c8dfe81b2, type: 3} + m_Name: Bloom + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.Bloom + active: 1 + skipIterations: + m_OverrideState: 0 + m_Value: 1 + threshold: + m_OverrideState: 1 + m_Value: 1.05 + intensity: + m_OverrideState: 1 + m_Value: 0.55 + scatter: + m_OverrideState: 1 + m_Value: 0.6 + clamp: + m_OverrideState: 0 + m_Value: 65472 + tint: + m_OverrideState: 1 + m_Value: {r: 1, g: 0.86, b: 0.6, a: 1} + highQualityFiltering: + m_OverrideState: 0 + m_Value: 0 + filter: + m_OverrideState: 0 + m_Value: 0 + downscale: + m_OverrideState: 0 + m_Value: 0 + maxIterations: + m_OverrideState: 0 + m_Value: 6 + dirtTexture: + m_OverrideState: 0 + m_Value: {fileID: 0} + dimension: 1 + dirtIntensity: + m_OverrideState: 0 + m_Value: 0 +--- !u!114 &-2512526436937386102 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 221518ef91623a7438a71fef23660601, type: 3} + m_Name: WhiteBalance + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.WhiteBalance + active: 1 + temperature: + m_OverrideState: 1 + m_Value: -22 + tint: + m_OverrideState: 1 + m_Value: 26 +--- !u!114 &-875615482179838212 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 899c54efeace73346a0a16faa3afe726, type: 3} + m_Name: Vignette + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.Vignette + active: 1 + color: + m_OverrideState: 1 + m_Value: {r: 0.09, g: 0.03, b: 0.14, a: 1} + center: + m_OverrideState: 0 + m_Value: {x: 0.5, y: 0.5} + intensity: + m_OverrideState: 1 + m_Value: 0.28 + smoothness: + m_OverrideState: 1 + m_Value: 0.6 + rounded: + m_OverrideState: 0 + m_Value: 0 +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3} + m_Name: WL_ArenaMood + m_EditorClassIdentifier: Unity.RenderPipelines.Core.Runtime::UnityEngine.Rendering.VolumeProfile + components: + - {fileID: 1813646129596036978} + - {fileID: -2512526436937386102} + - {fileID: -7716237022740010770} + - {fileID: -6414152611911942192} + - {fileID: -875615482179838212} +--- !u!114 &1813646129596036978 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 66f335fb1ffd8684294ad653bf1c7564, type: 3} + m_Name: ColorAdjustments + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.ColorAdjustments + active: 1 + postExposure: + m_OverrideState: 1 + m_Value: 0.05 + contrast: + m_OverrideState: 1 + m_Value: 30 + colorFilter: + m_OverrideState: 1 + m_Value: {r: 0.95, g: 0.8, b: 1.08, a: 1} + hueShift: + m_OverrideState: 0 + m_Value: 0 + saturation: + m_OverrideState: 1 + m_Value: 12 diff --git a/Assets/WL/Look/Arena/Post/WL_ArenaMood.asset.meta b/Assets/WL/Look/Arena/Post/WL_ArenaMood.asset.meta new file mode 100644 index 000000000..9a17f25c0 --- /dev/null +++ b/Assets/WL/Look/Arena/Post/WL_ArenaMood.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 534beeaf638a948459a24ec36906d7f6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look/Arena/Resources.meta b/Assets/WL/Look/Arena/Resources.meta new file mode 100644 index 000000000..0e6fae7a8 --- /dev/null +++ b/Assets/WL/Look/Arena/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7b6cfdda832d05f478e86a49991bde8f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look/Arena/Resources/WL.meta b/Assets/WL/Look/Arena/Resources/WL.meta new file mode 100644 index 000000000..941246661 --- /dev/null +++ b/Assets/WL/Look/Arena/Resources/WL.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5e200478fef3667448d80f66592afd9a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset b/Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset new file mode 100644 index 000000000..b33593832 --- /dev/null +++ b/Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset @@ -0,0 +1,44 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: af081c9d37b39dd40aa4c58f0397f23c, type: 3} + m_Name: WLReferenceLookSettings + m_EditorClassIdentifier: Assembly-CSharp::WL.Look.Arena.WLReferenceLookSettings + enabled_: 1 + verboseLog: 0 + outlineOn: 1 + outlineWidth: 6 + outlineWidthPixelMode: 1 + outlineColor: {r: 0, g: 0, b: 0, a: 1} + outlineOnBackground: 1 + outlineWidthBackground: 2.5 + outlineScaleWithZoom: 1 + outlineRefOrthoSize: 3.4 + outlineDepthBias: 0.06 + contrastOn: 1 + minimumDarkness: 0.12 + shadowColorScale: 0.85 + shadowTintAmount: 0.3 + shadowTint: {r: 0.243, g: 0.106, b: 0.353, a: 1} + charShades: 0 + moodOn: 1 + moodStrength: 0.55 + profile: + asset: {fileID: 11400000, guid: 534beeaf638a948459a24ec36906d7f6, type: 2} + keyLightColor: {r: 1, g: 0.78, b: 0.46, a: 1} + keyLightIntensity: 1.45 + ambientColor: {r: 0.28, g: 0.17, b: 0.4, a: 1} + rimOn: 1 + rimStrength: 1.6 + rimPower: 3 + rimUpBias: 0.35 + rimColor: {r: 1, g: 0.82, b: 0.5, a: 1} + rimOnBackground: 0 diff --git a/Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset.meta b/Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset.meta new file mode 100644 index 000000000..dd5471509 --- /dev/null +++ b/Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f662bae23dfde7b42a3b5966d68fbcf0 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity b/Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity index 28ef318d7..b3a5ad869 100644 --- a/Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity +++ b/Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity @@ -1098,6 +1098,52 @@ Transform: type: 3} m_PrefabInstance: {fileID: 206788312} m_PrefabAsset: {fileID: 0} +--- !u!1 &259214373 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 259214375} + - component: {fileID: 259214374} + m_Layer: 0 + m_Name: WL_ReferenceLook + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &259214374 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 259214373} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4fd112583fc74eb40bbc4ba98ea96331, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::WL.Look.Arena.WLReferenceLook + characterRoot: {fileID: 711662063} + characterRootName: PC_LH_M05 +--- !u!4 &259214375 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 259214373} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1001 &266873449 PrefabInstance: m_ObjectHideFlags: 0 @@ -14773,3 +14819,4 @@ SceneRoots: - {fileID: 918041734} - {fileID: 1166334010} - {fileID: 2042308456} + - {fileID: 259214375} diff --git a/Assets/WL/Look/Arena/WLReferenceLook.cs b/Assets/WL/Look/Arena/WLReferenceLook.cs new file mode 100644 index 000000000..c1bf41ba3 --- /dev/null +++ b/Assets/WL/Look/Arena/WLReferenceLook.cs @@ -0,0 +1,343 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLReferenceLook.cs — 레퍼런스 느낌 4요소를 씬에 얹는다 (WL-814t · #814) +// +// Ⓐ 외곽선(inverted hull · 화면 픽셀 두께) Ⓑ 명암 대비(Toon 램프) Ⓒ 색조·분위기(Volume+라이트) +// Ⓓ 림라이트(가산 패스) +// +// ■ 원칙 +// · 에셋(머티리얼·그래프·프리팹·원본 텍스처)을 **한 글자도 안 바꾼다**. +// 전부 런타임 인스턴스(머티리얼 복제 · 머티리얼 배열 뒤에 덧붙이기 · 임시 Volume)로만 한다. +// · `enabled_ = 0` 이면 아무 것도 하지 않는다 = WL-814s 상태 100 %(C8). +// · 되돌릴 수 있게 바꾸기 전 값을 전부 저장한다(OnDisable 에서 복구). +// +// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙). +// ───────────────────────────────────────────────────────────────────────────── + +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Rendering; + +namespace WL.Look.Arena +{ + [DisallowMultipleComponent] + public class WLReferenceLook : MonoBehaviour + { + // ── 진단(프로브가 읽는다) + public static int OutlinedRenderers, RimRenderers, ContrastMaterials; + public static string LastLog = ""; + public static bool IsApplied { get { return s_applied; } } + + [Tooltip("캐릭터 루트(비우면 이름으로 찾는다).")] + public GameObject characterRoot; + + [Tooltip("캐릭터 루트를 못 찾을 때 쓰는 이름.")] + public string characterRootName = "PC_LH_M05"; + + /// 요소별 on/off + 세기. 캡처 스크립트가 단독 요소 비교에 쓴다. + public struct Opt + { + public bool outline, contrast, mood, rim; + public float outlineWidth, outlineWidthBackground; + public Color outlineColor; + public bool outlineOnBackground; + public bool outlineScaleWithZoom; + public float outlineRefOrthoSize, outlineDepthBias; + public float minimumDarkness, shadowColorScale, shadowTintAmount; + public Color shadowTint; + public int charShades; + public float moodStrength; + public float rimStrength, rimPower, rimUpBias; + public Color rimColor; + public bool rimOnBackground; + + public static Opt FromSettings(WLReferenceLookSettings c) + { + var o = new Opt(); + if (c == null) return o; + o.outline = c.outlineOn; o.contrast = c.contrastOn; o.mood = c.moodOn; o.rim = c.rimOn; + o.outlineWidth = c.outlineWidth; o.outlineWidthBackground = c.outlineWidthBackground; + o.outlineColor = c.outlineColor; o.outlineOnBackground = c.outlineOnBackground; + o.outlineScaleWithZoom = c.outlineScaleWithZoom; o.outlineRefOrthoSize = c.outlineRefOrthoSize; + o.outlineDepthBias = c.outlineDepthBias; + o.minimumDarkness = c.minimumDarkness; o.shadowColorScale = c.shadowColorScale; + o.shadowTintAmount = c.shadowTintAmount; o.shadowTint = c.shadowTint; + o.charShades = c.charShades; + o.moodStrength = c.moodStrength; + o.rimStrength = c.rimStrength; o.rimPower = c.rimPower; o.rimUpBias = c.rimUpBias; + o.rimColor = c.rimColor; o.rimOnBackground = c.rimOnBackground; + return o; + } + } + + // ── 저장된 원래 상태 ──────────────────────────────────────────────── + struct RendState { public Renderer r; public Material[] mats; } + struct LightState { public Light l; public Color c; public float i; } + + static readonly List s_rends = new List(200); + static readonly List s_lights = new List(8); + static readonly List s_created = new List(32); + static GameObject s_volumeGo; + static bool s_applied; + static Color s_ambient; static AmbientMode s_ambMode; static float s_ambIntensity; + static Terrain s_terrain; static Material s_terrainMat; + static Camera s_cam; static bool s_camPost; static bool s_camPostSaved; + static readonly Dictionary s_clones = new Dictionary(); + + const string kToon = "Shader Graphs/Toon"; + const string kToonTerrain = "Shader Graphs/ToonTerrain"; + + void OnEnable() + { + var cfg = WLReferenceLookSettings.Instance; + if (cfg == null || cfg.enabled_ == 0) { LastLog = "enabled_=0 → 적용 0(814s 상태)"; return; } + Apply(Opt.FromSettings(cfg), cfg); + } + + void OnDisable() { Restore(); } + + // ───────────────────────────────────────────────────────────────── + /// 4요소를 지정한 세기로 얹는다. 이미 얹혀 있으면 먼저 되돌린다(멱등). + public static void Apply(Opt o, WLReferenceLookSettings cfg) + { + Restore(); + if (cfg == null) cfg = WLReferenceLookSettings.Instance; + + var charRoot = FindCharacterRoot(); + var rends = Object.FindObjectsByType(FindObjectsSortMode.None); + + OutlinedRenderers = 0; RimRenderers = 0; ContrastMaterials = 0; + + Material outlineChar = null, outlineBg = null, rimMat = null; + if (o.outline) + { + var sh = Shader.Find("WL/HullOutline"); + if (sh != null) + { + // 줌 비례 — 직교 크기가 커지면(멀리서 보면) 화면 픽셀 두께를 같은 비율로 줄인다. + // 그래야 「캐릭터 키 대비 굵기」가 프레이밍과 무관하게 일정하다(2D 스프라이트처럼). + float zoom = 1f; + var c0 = Camera.main; + if (c0 == null) { var cs0 = Object.FindObjectsByType(FindObjectsSortMode.None); if (cs0.Length > 0) c0 = cs0[0]; } + if (o.outlineScaleWithZoom && c0 != null && c0.orthographic && c0.orthographicSize > 0.001f && o.outlineRefOrthoSize > 0.001f) + zoom = o.outlineRefOrthoSize / c0.orthographicSize; + + outlineChar = Make(sh); outlineChar.SetColor("_OutlineColor", o.outlineColor); + outlineChar.SetFloat("_OutlineWidth", o.outlineWidth * zoom); + outlineChar.SetFloat("_OutlineDepthBias", o.outlineDepthBias); + outlineBg = Make(sh); outlineBg.SetColor("_OutlineColor", o.outlineColor); + outlineBg.SetFloat("_OutlineWidth", o.outlineWidthBackground * zoom); + outlineBg.SetFloat("_OutlineDepthBias", o.outlineDepthBias); + } + } + if (o.rim) + { + var sh = Shader.Find("WL/RimAdd"); + if (sh != null) + { + rimMat = Make(sh); + rimMat.SetColor("_RimColor", o.rimColor); + rimMat.SetFloat("_RimPower", o.rimPower); + rimMat.SetFloat("_RimStrength", o.rimStrength); + rimMat.SetFloat("_RimUpBias", o.rimUpBias); + } + } + + for (int i = 0; i < rends.Length; i++) + { + var r = rends[i]; + if (r == null || r is ParticleSystemRenderer) continue; + var mats = r.sharedMaterials; + if (mats == null || mats.Length == 0 || mats[0] == null) continue; + bool isChar = charRoot != null && r.transform.IsChildOf(charRoot.transform); + + // Toon 계열 불투명만 대상(얼굴 Unlit/Transparent · 물은 제외) + bool toon = false; + for (int m = 0; m < mats.Length; m++) + if (mats[m] != null && mats[m].shader != null && mats[m].shader.name == kToon) toon = true; + if (!toon) continue; + + var newMats = mats; + bool touched = false; + + // Ⓑ 대비 — 머티리얼 복제본으로 교체 + if (o.contrast) + { + newMats = (Material[])mats.Clone(); + for (int m = 0; m < newMats.Length; m++) + { + if (newMats[m] == null || newMats[m].shader == null || newMats[m].shader.name != kToon) continue; + newMats[m] = CloneForContrast(newMats[m], o, isChar); + touched = true; + } + } + + // Ⓐ 외곽선 · Ⓓ 림 — 머티리얼 배열 뒤에 덧붙인다(서브메시 1개면 한 번 더 그린다) + var extra = new List(2); + if (o.outline && (isChar || o.outlineOnBackground)) extra.Add(isChar ? outlineChar : outlineBg); + if (o.rim && rimMat != null && (isChar || o.rimOnBackground)) extra.Add(rimMat); + if (extra.Count > 0 && extra[0] != null) + { + if (!touched) newMats = (Material[])mats.Clone(); + var list = new List(newMats); + for (int e = 0; e < extra.Count; e++) if (extra[e] != null) list.Add(extra[e]); + newMats = list.ToArray(); + touched = true; + if (o.outline && (isChar || o.outlineOnBackground)) OutlinedRenderers++; + if (o.rim && rimMat != null && (isChar || o.rimOnBackground)) RimRenderers++; + } + + if (touched) + { + s_rends.Add(new RendState { r = r, mats = mats }); + r.sharedMaterials = newMats; + } + } + + // Ⓑ — 터레인(화면 면적의 대부분) + if (o.contrast) + { + var t = Terrain.activeTerrain; + if (t != null && t.materialTemplate != null && t.materialTemplate.shader != null + && t.materialTemplate.shader.name == kToonTerrain) + { + s_terrain = t; s_terrainMat = t.materialTemplate; + t.materialTemplate = CloneForContrast(t.materialTemplate, o, false); + } + } + + // Ⓒ — 라이트 · 앰비언트 · Volume + if (o.mood && o.moodStrength > 0f) + { + float k = Mathf.Clamp01(o.moodStrength); + s_ambMode = RenderSettings.ambientMode; + s_ambient = RenderSettings.ambientLight; + s_ambIntensity = RenderSettings.ambientIntensity; + + var lights = Object.FindObjectsByType(FindObjectsSortMode.None); + for (int i = 0; i < lights.Length; i++) + { + var l = lights[i]; + if (l == null) continue; + s_lights.Add(new LightState { l = l, c = l.color, i = l.intensity }); + if (l.type == LightType.Directional) + { + l.color = Color.Lerp(l.color, cfg != null ? cfg.keyLightColor : new Color(1f, .76f, .42f), k); + l.intensity = Mathf.Lerp(l.intensity, cfg != null ? cfg.keyLightIntensity : 1.15f, k); + } + } + + RenderSettings.ambientMode = AmbientMode.Flat; + RenderSettings.ambientLight = Color.Lerp( + s_ambMode == AmbientMode.Flat ? s_ambient : RenderSettings.ambientSkyColor, + cfg != null ? cfg.ambientColor : new Color(.259f, .165f, .373f), k); + + if (cfg != null && cfg.profile != null && cfg.profile.asset != null) + { + s_volumeGo = new GameObject("WL814t_ReferenceLook_Volume"); + s_volumeGo.hideFlags = HideFlags.DontSave; + var v = s_volumeGo.AddComponent(); + v.isGlobal = true; v.priority = 100f; + v.sharedProfile = cfg.profile.asset; + v.weight = k; + + s_cam = Camera.main; + if (s_cam == null) { var cs = Object.FindObjectsByType(FindObjectsSortMode.None); if (cs.Length > 0) s_cam = cs[0]; } + if (s_cam != null) + { + var ac = s_cam.GetComponent(); + if (ac != null) { s_camPost = ac.renderPostProcessing; s_camPostSaved = true; ac.renderPostProcessing = true; } + } + } + } + + s_applied = true; + LastLog = "Ⓐ" + (o.outline ? OutlinedRenderers.ToString() : "off") + + " Ⓑ" + (o.contrast ? ContrastMaterials + "mat/md" + o.minimumDarkness.ToString("F2") : "off") + + " Ⓒ" + (o.mood ? o.moodStrength.ToString("F2") : "off") + + " Ⓓ" + (o.rim ? RimRenderers.ToString() : "off"); + if (cfg != null && cfg.verboseLog != 0) Debug.Log("[WL814t ReferenceLook] " + LastLog); + } + + /// 바꾼 것을 전부 원래대로. 몇 번 불러도 안전. + public static void Restore() + { + for (int i = 0; i < s_rends.Count; i++) + if (s_rends[i].r != null) s_rends[i].r.sharedMaterials = s_rends[i].mats; + s_rends.Clear(); + + if (s_terrain != null && s_terrainMat != null) s_terrain.materialTemplate = s_terrainMat; + s_terrain = null; s_terrainMat = null; + + for (int i = 0; i < s_lights.Count; i++) + if (s_lights[i].l != null) { s_lights[i].l.color = s_lights[i].c; s_lights[i].l.intensity = s_lights[i].i; } + if (s_lights.Count > 0) + { + RenderSettings.ambientMode = s_ambMode; + RenderSettings.ambientLight = s_ambient; + RenderSettings.ambientIntensity = s_ambIntensity; + } + s_lights.Clear(); + + if (s_volumeGo != null) { DestroyAny(s_volumeGo); s_volumeGo = null; } + if (s_camPostSaved && s_cam != null) + { + var ac = s_cam.GetComponent(); + if (ac != null) ac.renderPostProcessing = s_camPost; + } + s_camPostSaved = false; s_cam = null; + + for (int i = 0; i < s_created.Count; i++) if (s_created[i] != null) DestroyAny(s_created[i]); + s_created.Clear(); + s_clones.Clear(); + OutlinedRenderers = 0; RimRenderers = 0; ContrastMaterials = 0; + if (s_applied) LastLog = "restored(814s 상태)"; + s_applied = false; + } + + // ── helpers ────────────────────────────────────────────────────── + static GameObject FindCharacterRoot() + { + var go = GameObject.Find("PC_LH_M05"); + if (go != null) return go; + var anim = Object.FindFirstObjectByType(); + return anim != null ? anim.gameObject : null; + } + + static Material Make(Shader sh) + { + var m = new Material(sh); + m.hideFlags = HideFlags.HideAndDontSave; + s_created.Add(m); + return m; + } + + static Material CloneForContrast(Material src, Opt o, bool isChar) + { + Material c; + // 캐릭터/배경이 같은 머티리얼을 공유하는 경우가 없어 키 하나로 충분하다 + if (s_clones.TryGetValue(src, out c) && c != null) return c; + c = new Material(src); + c.hideFlags = HideFlags.HideAndDontSave; + c.name = src.name + "_814t"; + if (c.HasProperty("_MinimumDarkness")) c.SetFloat("_MinimumDarkness", o.minimumDarkness); + if (c.HasProperty("_ShadowDiffuseColor")) + { + var s = c.GetColor("_ShadowDiffuseColor"); + s = new Color(s.r * o.shadowColorScale, s.g * o.shadowColorScale, s.b * o.shadowColorScale, s.a); + if (o.shadowTintAmount > 0f) s = Color.Lerp(s, new Color(s.grayscale, s.grayscale, s.grayscale, s.a) * o.shadowTint, o.shadowTintAmount); + c.SetColor("_ShadowDiffuseColor", s); + } + if (isChar && o.charShades > 0 && c.HasProperty("_Shades")) c.SetFloat("_Shades", o.charShades); + s_clones[src] = c; + s_created.Add(c); + ContrastMaterials++; + return c; + } + + static void DestroyAny(Object o) + { + if (o == null) return; + if (Application.isPlaying) Destroy(o); else DestroyImmediate(o); + } + } +} diff --git a/Assets/WL/Look/Arena/WLReferenceLook.cs.meta b/Assets/WL/Look/Arena/WLReferenceLook.cs.meta new file mode 100644 index 000000000..ab6e177ee --- /dev/null +++ b/Assets/WL/Look/Arena/WLReferenceLook.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4fd112583fc74eb40bbc4ba98ea96331 \ No newline at end of file diff --git a/Assets/WL/Look/Arena/WLReferenceLookSettings.cs b/Assets/WL/Look/Arena/WLReferenceLookSettings.cs new file mode 100644 index 000000000..6ba8071b4 --- /dev/null +++ b/Assets/WL/Look/Arena/WLReferenceLookSettings.cs @@ -0,0 +1,130 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLReferenceLookSettings.cs — 「레퍼런스 느낌」 4요소의 값 단일 출처 (WL-814t · #814) +// +// PD 지시(2026-09-12) 「레퍼런스 게임 같은 느낌을 살릴 수 없을까?」 · 발주서 WL-814t §2-3. +// 레퍼런스 특징 = ⓐ 굵은 검은 외곽선 ⓑ 매우 어두운 그림자(강한 대비) +// ⓒ 보라~자홍 배경 + 금색/주황 조명 ⓓ 윤곽의 밝은 테(림라이트) +// +// ■ C45 — 값은 코드 상수가 아니라 이 에셋(`Resources/WL/WLReferenceLookSettings.asset`) +// ■ C8 — `enabled_ = 0` 이면 `WLReferenceLook` 이 **아무것도 하지 않는다** = 814s 상태 100 % +// +// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일). +// ───────────────────────────────────────────────────────────────────────────── + +using UnityEngine; + +namespace WL.Look.Arena +{ + [CreateAssetMenu(fileName = "WLReferenceLookSettings", menuName = "WL/Reference Look Settings", order = 37)] + public sealed class WLReferenceLookSettings : ScriptableObject + { + public const string ResourcesPath = "WL/WLReferenceLookSettings"; + + private static WLReferenceLookSettings s_instance; + private static bool s_tried; + + /// 에셋이 없으면 null — 호출부는 전부 건너뛴다(C8 롤백). + public static WLReferenceLookSettings Instance + { + get + { + if (s_instance == null && !s_tried) + { + s_tried = true; + s_instance = Resources.Load(ResourcesPath); + } + return s_instance; + } + } + + [Header("마스터 (C8)")] + [Tooltip("0 이면 이 기능 전체가 꺼진다 = WL-814s 상태 100 %.")] + public int enabled_ = 1; + + [Tooltip("1 이면 적용 결과를 콘솔에 1줄 남긴다.")] + public int verboseLog = 0; + + // ── Ⓐ 외곽선 ──────────────────────────────────────────────────────── + [Header("Ⓐ 외곽선 (inverted hull · 두께 단위 = 화면 픽셀 @1080×1920)")] + public bool outlineOn = true; + + [Tooltip("캐릭터 외곽선 두께(px @직교 3.4). PD 지시 = 굵게. 레퍼런스2 비율 = 캐릭터 키의 1/80~1/60 ≈ 4.3~5.7 px.")] + public float outlineWidth = 6.0f; + + [Tooltip("도트(픽셀) 모드에서 쓰는 캐릭터 외곽선 두께(px · 저해상도 RT 기준).")] + public float outlineWidthPixelMode = 1.0f; + + [Tooltip("외곽선 색. 순검정보다 아주 어두운 중성/보라가 레퍼런스에 가깝다.")] + public Color outlineColor = new Color(0.043f, 0.031f, 0.067f, 1f); + + [Tooltip("배경(바위·나무·건물)에도 외곽선을 넣을지.")] + public bool outlineOnBackground = true; + + [Tooltip("배경 외곽선 두께(px). 저폴리 하드에지라 캐릭터보다 얇게 잡는다.")] + public float outlineWidthBackground = 2.5f; + + [Tooltip("켜면 두께를 카메라 줌(직교 크기)에 맞춰 비례시킨다 = 캐릭터 대비 굵기가 항상 같다(2D 스프라이트처럼).")] + public bool outlineScaleWithZoom = true; + + [Tooltip("위 비례의 기준 직교 반높이. 이 크기일 때 두께가 위 px 값 그대로다.")] + public float outlineRefOrthoSize = 3.4f; + + [Tooltip("껍데기를 카메라에서 뒤로 미는 양(m). 저폴리 하드에지의 실루엣 안쪽 얼룩을 없앤다.")] + public float outlineDepthBias = 0.06f; + + // ── Ⓑ 명암 대비 ───────────────────────────────────────────────────── + [Header("Ⓑ 명암 대비 (Toon 램프)")] + public bool contrastOn = true; + + [Tooltip("`_MinimumDarkness` — 그림자 바닥 밝기. 기본 0.2 · 낮을수록 어둡다.")] + [Range(0f, 0.5f)] public float minimumDarkness = 0.08f; + + [Tooltip("`_ShadowDiffuseColor` 에 곱하는 계수. 1 미만이면 그림자가 더 진해진다.")] + [Range(0.2f, 1.5f)] public float shadowColorScale = 0.72f; + + [Tooltip("그림자에 섞을 색조(보라). 0 이면 안 섞는다.")] + [Range(0f, 1f)] public float shadowTintAmount = 0.35f; + public Color shadowTint = new Color(0.243f, 0.106f, 0.353f, 1f); + + [Tooltip("캐릭터 `_Shades`(색 단계 수). 0 이면 원래 값(배경과 동일 7) 유지. 레퍼런스2 = 부위당 2~3단.")] + public int charShades = 0; + + // ── Ⓒ 색조·분위기 ─────────────────────────────────────────────────── + [Header("Ⓒ 색조·분위기 (Volume + 라이트)")] + public bool moodOn = true; + + [Tooltip("0=끔 · 0.5=은은 · 1=강함. Volume weight 와 라이트 보간에 함께 쓴다.")] + [Range(0f, 1f)] public float moodStrength = 0.75f; + + [Tooltip("씬에 붙일 포스트 프로파일(없으면 Ⓒ 는 라이트만 바꾼다).")] + public VolumeProfileRef profile = new VolumeProfileRef(); + + [Tooltip("키라이트(Directional) 목표 색 — 레퍼런스의 금색/주황.")] + public Color keyLightColor = new Color(1f, 0.76f, 0.42f, 1f); + + [Tooltip("키라이트 목표 세기.")] + public float keyLightIntensity = 1.15f; + + [Tooltip("앰비언트 목표 색 — 레퍼런스의 보라~자홍.")] + public Color ambientColor = new Color(0.259f, 0.165f, 0.373f, 1f); + + // ── Ⓓ 림라이트 ────────────────────────────────────────────────────── + [Header("Ⓓ 림라이트 (캐릭터 윤곽의 밝은 테)")] + public bool rimOn = true; + + [Range(0f, 4f)] public float rimStrength = 1.25f; + [Range(0.5f, 8f)] public float rimPower = 3.0f; + [Range(0f, 1f)] public float rimUpBias = 0.35f; + public Color rimColor = new Color(1f, 0.82f, 0.5f, 1f); + + [Tooltip("배경에도 림을 넣을지(레퍼런스는 캐릭터만).")] + public bool rimOnBackground = false; + + /// 인스펙터에서 프로파일을 물릴 수 있게 하는 얇은 래퍼(Volume 패키지 타입 직접 노출). + [System.Serializable] + public sealed class VolumeProfileRef + { + public UnityEngine.Rendering.VolumeProfile asset; + } + } +} diff --git a/Assets/WL/Look/Arena/WLReferenceLookSettings.cs.meta b/Assets/WL/Look/Arena/WLReferenceLookSettings.cs.meta new file mode 100644 index 000000000..1529651e3 --- /dev/null +++ b/Assets/WL/Look/Arena/WLReferenceLookSettings.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: af081c9d37b39dd40aa4c58f0397f23c \ No newline at end of file diff --git a/Assets/WL/Look/Character/Shaders.meta b/Assets/WL/Look/Character/Shaders.meta new file mode 100644 index 000000000..8c5104926 --- /dev/null +++ b/Assets/WL/Look/Character/Shaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5cded1f5a214b5e46aec36dd921a61a6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look/Character/Shaders/WLHullOutline.shader b/Assets/WL/Look/Character/Shaders/WLHullOutline.shader new file mode 100644 index 000000000..a93894fd5 --- /dev/null +++ b/Assets/WL/Look/Character/Shaders/WLHullOutline.shader @@ -0,0 +1,120 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLHullOutline.shader — 화면 픽셀 단위 두께의 「굵은 검은 외곽선」 (WL-814t · #814) +// +// ■ 왜 새로 만드나 (§1 실측 결론) +// 원본 `Shader Graphs/Toon` 의 `_Outline`/`_OUTLINESENABLED` 는 **머티리얼 안에 박힌 +// 화면공간 에지검출 후처리**다(Depth+Normals 1텍셀 비교 → Overlay 블렌드). +// · 두께 = `_CameraNormalsTexture` **1 텍셀 고정** — 머티리얼로 못 굵게 한다. +// · `_Outline` 의 **알파는 무시**된다(Blend 노드가 float3 로 해석) — RGB 만 쓴다. +// · `PixelOutlineSetup` 렌더러 피처가 꺼져 있으면(현재 `m_Active: 0`) 아무것도 안 나온다. +// · 켜도 나오는 것은 「면 경계의 얇은 실선」이지 레퍼런스의 굵은 실루엣이 아니다. +// → 레퍼런스(2D 도트)의 굵은 외곽선은 **메시 노멀 확장(inverted hull)** 으로만 나온다. +// +// ■ 무엇을 하나 +// 앞면을 버리고(Cull Front) 뒷면을 화면 XY 로 `_OutlineWidth` **픽셀**만큼 밀어 그린다. +// 두께가 화면 픽셀 단위라 거리·직교크기와 무관하게 일정하다(= 2D 도트 느낌). +// +// ■ 어떻게 쓰나 +// 원본 머티리얼을 **건드리지 않는다**. 렌더러의 머티리얼 배열 뒤에 이 머티리얼을 한 장 +// 더 붙이면(서브메시가 1개면 마지막 서브메시를 한 번 더 그린다) 외곽선이 된다. +// 붙이고 떼는 것은 `WLReferenceLook` 이 런타임에만 한다(에셋·씬 무변경). +// +// ■ 모바일 비용 +// 대상 렌더러당 드로우콜 +1(정점만 한 번 더). 픽셀 부하는 실루엣 띠 면적뿐. +// ───────────────────────────────────────────────────────────────────────────── +Shader "WL/HullOutline" +{ + Properties + { + [HDR] _OutlineColor ("Outline Color", Color) = (0, 0, 0, 1) + _OutlineWidth ("Outline Width (screen px)", Float) = 2.0 + _OutlineDepthBias ("Depth Bias (m · 하드에지 저폴리의 내부 얼룩 제거)", Float) = 0.06 + } + + SubShader + { + Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Opaque" "Queue" = "Geometry-1" } + + Pass + { + Name "WLHullOutline" + Tags { "LightMode" = "SRPDefaultUnlit" } + + Cull Front + ZWrite On + ZTest LEqual + ColorMask RGB + + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma multi_compile_instancing + #pragma target 3.0 + + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + + CBUFFER_START(UnityPerMaterial) + float4 _OutlineColor; + float _OutlineWidth; + float _OutlineDepthBias; + CBUFFER_END + + struct Attributes + { + float4 positionOS : POSITION; + float3 normalOS : NORMAL; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct Varyings + { + float4 positionCS : SV_POSITION; + UNITY_VERTEX_INPUT_INSTANCE_ID + UNITY_VERTEX_OUTPUT_STEREO + }; + + Varyings vert(Attributes input) + { + Varyings o = (Varyings)0; + UNITY_SETUP_INSTANCE_ID(input); + UNITY_TRANSFER_INSTANCE_ID(input, o); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); + + // 🔴 깊이 바이어스 — 껍데기를 카메라에서 조금 더 멀리 밀어 둔다. + // 저폴리 하드에지 메시(머리카락·옷)는 같은 위치에서 노멀이 쪼개져 있어 + // 그냥 밀면 실루엣 **안쪽**에 뒷면이 튀어나와 얼룩이 된다(실측). + // 본체 면보다 항상 뒤에 있게 만들면 안쪽 얼룩만 깔끔히 사라진다. + float3 posWS = TransformObjectToWorld(input.positionOS.xyz); + float3 posVS = TransformWorldToView(posWS); + posVS.z -= _OutlineDepthBias; // 뷰공간은 -Z 가 앞 → 빼면 멀어진다 + float4 posCS = TransformWViewToHClip(posVS); + + float3 nWS = TransformObjectToWorldNormal(input.normalOS); + float3 nVS = TransformWorldToViewDir(nWS, true); + // 투영까지 태워야 직교/원근 모두에서 화면 방향이 맞는다 + float2 nCS = mul((float3x3)UNITY_MATRIX_P, nVS).xy; + + // 화면(픽셀) 공간에서 정규화 → 방향이 대각이어도 두께가 일정하다 + float2 dirPx = nCS * float2(_ScreenParams.x, _ScreenParams.y); + float len = length(dirPx); + dirPx = (len > 1e-5) ? dirPx / len : float2(0.0, 0.0); + + // 픽셀 → NDC(가로 2.0 이 화면 폭) → 클립(× w) + float2 ndcPerPx = float2(2.0 / _ScreenParams.x, 2.0 / _ScreenParams.y); + posCS.xy += dirPx * ndcPerPx * _OutlineWidth * posCS.w; + + o.positionCS = posCS; + return o; + } + + half4 frag(Varyings input) : SV_Target + { + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); + return half4(_OutlineColor.rgb, 1.0); + } + ENDHLSL + } + } + + Fallback Off +} diff --git a/Assets/WL/Look/Character/Shaders/WLHullOutline.shader.meta b/Assets/WL/Look/Character/Shaders/WLHullOutline.shader.meta new file mode 100644 index 000000000..d5bce4b60 --- /dev/null +++ b/Assets/WL/Look/Character/Shaders/WLHullOutline.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: c1901a3913628b24590d96fb77efd166 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look/Character/Shaders/WLRimAdd.shader b/Assets/WL/Look/Character/Shaders/WLRimAdd.shader new file mode 100644 index 000000000..5a53102c3 --- /dev/null +++ b/Assets/WL/Look/Character/Shaders/WLRimAdd.shader @@ -0,0 +1,102 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLRimAdd.shader — 캐릭터 윤곽의 밝은 테(림라이트) (WL-814t · #814 · 요소 Ⓓ) +// +// ■ 왜 별도 패스인가 +// 원본 `Toon.shadergraph` 에는 Fresnel 입력이 없고, 원본(`Assets/3DPixelArtEnvironment/**`) +// 수정은 금지다. 그래프 **복사본**을 만들어 Fresnel 을 더하는 대신, 같은 메시를 한 번 더 +// 가산(Blend One One)으로 덧그리는 패스를 택했다 — 머티리얼/그래프를 하나도 안 건드리고 +// 런타임에 켜고 끌 수 있고, 세기를 슬라이더 하나로 준다. +// +// ■ 무엇을 하나 +// rim = pow(1 - saturate(dot(N, V)), _RimPower) — 시선과 수직인 가장자리에서 1. +// `_RimUpBias` 로 위쪽(레퍼런스의 무대조명) 윤곽만 더 밝게 줄 수 있다. +// +// ■ 주의 +// Transparent 큐 · ZWrite Off · ZTest LEqual + Offset 으로 원본 면 위에 얹는다. +// ───────────────────────────────────────────────────────────────────────────── +Shader "WL/RimAdd" +{ + Properties + { + [HDR] _RimColor ("Rim Color", Color) = (1, 0.85, 0.55, 1) + _RimPower ("Rim Power", Float) = 3.0 + _RimStrength ("Rim Strength", Float) = 1.0 + _RimUpBias ("Rim Up Bias (0=균일, 1=위쪽만)", Range(0, 1)) = 0.35 + } + + SubShader + { + Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Transparent" "Queue" = "Transparent" } + + Pass + { + Name "WLRimAdd" + Tags { "LightMode" = "SRPDefaultUnlit" } + + Cull Back + ZWrite Off + ZTest LEqual + Offset -1, -1 + Blend One One + + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma multi_compile_instancing + #pragma target 3.0 + + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + + CBUFFER_START(UnityPerMaterial) + float4 _RimColor; + float _RimPower; + float _RimStrength; + float _RimUpBias; + CBUFFER_END + + struct Attributes + { + float4 positionOS : POSITION; + float3 normalOS : NORMAL; + UNITY_VERTEX_INPUT_INSTANCE_ID + }; + + struct Varyings + { + float4 positionCS : SV_POSITION; + float3 normalWS : TEXCOORD0; + float3 positionWS : TEXCOORD1; + UNITY_VERTEX_INPUT_INSTANCE_ID + UNITY_VERTEX_OUTPUT_STEREO + }; + + Varyings vert(Attributes input) + { + Varyings o = (Varyings)0; + UNITY_SETUP_INSTANCE_ID(input); + UNITY_TRANSFER_INSTANCE_ID(input, o); + UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); + o.positionWS = TransformObjectToWorld(input.positionOS.xyz); + o.positionCS = TransformWorldToHClip(o.positionWS); + o.normalWS = TransformObjectToWorldNormal(input.normalOS); + return o; + } + + half4 frag(Varyings input) : SV_Target + { + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); + float3 n = normalize(input.normalWS); + float3 v = normalize(GetWorldSpaceViewDir(input.positionWS)); + float rim = 1.0 - saturate(dot(n, v)); + rim = pow(saturate(rim), max(_RimPower, 0.01)); + // 위쪽 윤곽 가중(무대 조명 느낌) + float up = saturate(n.y * 0.5 + 0.5); + rim *= lerp(1.0, up, saturate(_RimUpBias)); + return half4(_RimColor.rgb * (rim * _RimStrength), 0.0); + } + ENDHLSL + } + } + + Fallback Off +} diff --git a/Assets/WL/Look/Character/Shaders/WLRimAdd.shader.meta b/Assets/WL/Look/Character/Shaders/WLRimAdd.shader.meta new file mode 100644 index 000000000..76793e8d7 --- /dev/null +++ b/Assets/WL/Look/Character/Shaders/WLRimAdd.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: e22defdb5476d4b4f93db440d73c4233 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: