From a0634ad7356a4fb2cf6818540241d8c48273b56f Mon Sep 17 00:00:00 2001 From: swrring Date: Thu, 17 Sep 2026 22:45:45 +0900 Subject: [PATCH] =?UTF-8?q?[WL]=20=EB=8D=98=EC=A0=84=20=ED=92=80=EB=B0=AD?= =?UTF-8?q?=20=3D=20=EC=84=AC=20=EA=B0=92=20=C2=B7=20=EB=AC=BC=EA=B0=80=20?= =?UTF-8?q?=EB=AC=BC=EB=B3=B4=EB=9D=BC(=EC=A0=88=EC=B0=A8=EC=A0=81=20?= =?UTF-8?q?=EA=B1=B0=ED=92=88=20=EC=85=B0=EC=9D=B4=EB=8D=94)=20=C2=B7=20?= =?UTF-8?q?=EB=93=B1=EA=B8=89=20=EC=97=B0=EC=B6=9C(PC=201=20=C2=B7=20?= =?UTF-8?q?=EB=AA=AC=EC=8A=A4=ED=84=B0/NPC=202=20=C2=B7=20=ED=94=84?= =?UTF-8?q?=EB=9E=8D=203=20=E2=80=94=20=ED=86=A4=20=EA=B3=B5=EC=9C=A0?= =?UTF-8?q?=C2=B7=EC=84=B8=EA=B8=B0=EB=A7=8C=20=EC=B0=A8=EB=93=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WLDungeonGrassLook: 던전 데모 인스턴서/터레인 재질/태양을 섬 설정 값으로(풀 0.22·꽃 0.26·구름 0.5/0.025·밝기 0.12·그림자 0.45) + ssaoOffScenes 에 던전 2개 - WLShoreSpray.shader + WLShoreFoam: uv2 = 물가선 거리(m) · 또렷한 물가 거품(밀려왔다 빠짐) + 밀려오는 포말 줄 · 텍스처 0장 · shoreSprayEnabled 0 = 예전 띠 - WLCharacterBacklight: 2등급(Actor 역할 스캔 1.5 s · 림/채움빛 약하게 · MK Toon 그늘색·Toon 그늘 값은 프로퍼티 블록) · 3등급(Toon 프랍 공유 재질 복사본 · 5 s 스캔 · 복원 가능) · 디졸브 픽셀 버림 · ForceScan - 실측: 같은 프레임 끔/켬 쌍 렌더(섬·던전) · trace 0 · 콘솔 에러 0 Co-Authored-By: Claude Fable 5.1 --- Assets/WL/Island/WLDungeonSceneSetup.cs | 2 + .../WL/WLCharacterBacklightSettings.asset | 26 +++ .../Character/Shaders/WLCharBacklight.shader | 11 + .../WL/Look/Character/WLCharacterBacklight.cs | 215 +++++++++++++++++- .../Character/WLCharacterBacklightSettings.cs | 28 +++ .../Resources/WL/WLIslandLookSettings.asset | 17 ++ .../WL/Look/Farm/Shaders/WLShoreSpray.shader | 158 +++++++++++++ .../Farm/Shaders/WLShoreSpray.shader.meta | 9 + Assets/WL/Look/Farm/WLDungeonGrassLook.cs | 131 +++++++++++ .../WL/Look/Farm/WLDungeonGrassLook.cs.meta | 2 + Assets/WL/Look/Farm/WLIslandLookSettings.cs | 40 +++- Assets/WL/Look/Farm/WLShoreFoam.cs | 53 ++++- 12 files changed, 679 insertions(+), 13 deletions(-) create mode 100644 Assets/WL/Look/Farm/Shaders/WLShoreSpray.shader create mode 100644 Assets/WL/Look/Farm/Shaders/WLShoreSpray.shader.meta create mode 100644 Assets/WL/Look/Farm/WLDungeonGrassLook.cs create mode 100644 Assets/WL/Look/Farm/WLDungeonGrassLook.cs.meta diff --git a/Assets/WL/Island/WLDungeonSceneSetup.cs b/Assets/WL/Island/WLDungeonSceneSetup.cs index 12c22d1f1..c181fa6d3 100644 --- a/Assets/WL/Island/WLDungeonSceneSetup.cs +++ b/Assets/WL/Island/WLDungeonSceneSetup.cs @@ -47,6 +47,8 @@ namespace WL.Island WLIslandSceneSetup.EnsureDungeonMapNodes(cfg, scene, d); FixDuplicates(cfg, scene); + // 2026-09-17 PD 「인게임의 풀밭의 느낌도 섬과 동일하게」 — 던전 씬의 풀 인스턴서·터레인 재질·태양을 섬 값으로(로딩 화면 중 · 씬/원본 재질 0줄) + WL.Look.Farm.WLDungeonGrassLook.Apply(scene); // 🔴 WL-816i — 던전 카메라를 **섬 구도**(각 45° · 거리 22 m · FOV 60)로 맞춘다. // 씬이 내려가면 이 오브젝트도 사라지고 OnDestroy 가 원래 값을 돌려놓는다(C8). diff --git a/Assets/WL/Look/Character/Resources/WL/WLCharacterBacklightSettings.asset b/Assets/WL/Look/Character/Resources/WL/WLCharacterBacklightSettings.asset index 557dd38ee..c310e0185 100644 --- a/Assets/WL/Look/Character/Resources/WL/WLCharacterBacklightSettings.asset +++ b/Assets/WL/Look/Character/Resources/WL/WLCharacterBacklightSettings.asset @@ -53,6 +53,32 @@ MonoBehaviour: faceRendererHints: - Face - Head + tiersEnabled: 1 + tier2RimStrength: 0.7 + tier2RimOpacity: 0.35 + tier2FillStrength: 0.07 + tier2MinimumDarknessAdd: 0.1 + tier2ShadowBlend: 0.6 + tier2ShadowColor: {r: 0.36, g: 0.31, b: 0.4, a: 0} + tier2MkShadowColor: {r: 0.3, g: 0.25, b: 0.34, a: 1} + tier2Roles: + - Mob + - Pet + - Worker + - Fairy + - EnemyPC + - Farmer + - Summon + - BattleNPC + - EnemyPet + - EnemySummon + tier2ScanSeconds: 1.5 + tier3MinimumDarknessAdd: 0.06 + tier3ShadowBlend: 0.35 + tier3ShadowColor: {r: 0.29, g: 0.27, b: 0.39, a: 0} + tier3ShaderNames: + - Shader Graphs/Toon + tier3ScanSeconds: 5 bloomEnabled: 1 bloomScenes: - Level01 diff --git a/Assets/WL/Look/Character/Shaders/WLCharBacklight.shader b/Assets/WL/Look/Character/Shaders/WLCharBacklight.shader index 106ea4595..3bd423077 100644 --- a/Assets/WL/Look/Character/Shaders/WLCharBacklight.shader +++ b/Assets/WL/Look/Character/Shaders/WLCharBacklight.shader @@ -53,6 +53,7 @@ Shader "WL/CharBacklight" [HDR] _FaceColor ("Face Light Color", Color) = (1.0, 0.93, 0.85, 1) _FaceStrength ("Face Light Strength (0 = 끔)", Float) = 0.3 _FaceWrap ("Face Light Wrap (0 = 또렷 · 1 = 둥글게 감싸는 빛)", Range(0, 1)) = 0.5 + _FillStrength ("Body Fill Strength (위치 없는 채움빛 · 2등급용)", Float) = 0 } SubShader @@ -105,6 +106,7 @@ Shader "WL/CharBacklight" float4 _FaceColor; float _FaceStrength; float _FaceWrap; + float _FillStrength; CBUFFER_END // xyz = 표면에서 가상 역광 쪽을 가리키는 월드 방향(정규화) · w = 1 이면 유효(0 = 아직 안 들어옴 → 위쪽) @@ -164,6 +166,9 @@ Shader "WL/CharBacklight" if (_UseDepthRim > 0.5) { float2 uv = GetNormalizedScreenSpaceUV(input.positionCS); + // 본체가 이 픽셀을 안 그렸으면(디졸브·알파 클립으로 잘려 나간 자리) 덧그리지 않는다 — 몬스터 사망 디졸브에 빛 자국이 남지 않게 + float dBase = LinearEyeDepth(SampleSceneDepth(uv), _ZBufferParams); + clip(0.06 - (dBase - LinearEyeDepth(input.positionCS.z, _ZBufferParams))); float3 bVS = TransformWorldToViewDir(b); float2 dirSS = dot(bVS.xy, bVS.xy) > 1e-5 ? normalize(bVS.xy) : float2(0.7071, 0.7071); float minSide = min(_ScaledScreenParams.x, _ScaledScreenParams.y); @@ -193,6 +198,12 @@ Shader "WL/CharBacklight" float ndl = saturate(dot(n, fl) * (1.0 - 0.5 * _FaceWrap) + 0.5 * _FaceWrap); face = fall * ndl * _FaceStrength * (1.0 - rim); } + // ④-b 위치 없는 채움빛(2등급 = 몬스터·NPC) — 같은 빛 방향으로 몸 전체를 옅게 + if (_FillStrength > 0.0) + { + float3 fl2 = dot(_WLFaceLightDir.xyz, _WLFaceLightDir.xyz) > 1e-5 ? normalize(_WLFaceLightDir.xyz) : v; + face += saturate(dot(n, fl2) * (1.0 - 0.5 * _FaceWrap) + 0.5 * _FaceWrap) * _FillStrength * (1.0 - rim); + } float3 col = _RimColor.rgb * (rim * _RimStrength) + _WrapColor.rgb * wrap + _SssColor.rgb * sss + _FaceColor.rgb * face; return half4(col, rim * saturate(_RimOpacity)); diff --git a/Assets/WL/Look/Character/WLCharacterBacklight.cs b/Assets/WL/Look/Character/WLCharacterBacklight.cs index 7906a5dc1..18218c066 100644 --- a/Assets/WL/Look/Character/WLCharacterBacklight.cs +++ b/Assets/WL/Look/Character/WLCharacterBacklight.cs @@ -31,11 +31,11 @@ namespace WL.Look.Character public static int AppliedRenderers, SkinRenderers, MultiSubmeshRenderers; public static string LastLog = ""; - static Material s_rim, s_rimSkin; + static Material s_rim, s_rimSkin, s_rimT2; static readonly List s_buf = new List(16); static readonly List s_tmp = new List(4); - public static bool IsOurs(Material m) { return m != null && (m == s_rim || m == s_rimSkin); } + public static bool IsOurs(Material m) { return m != null && (m == s_rim || m == s_rimSkin || m == s_rimT2); } /// root 아래 캐릭터 렌더러에 역광 패스를 덧붙인다(설정이 꺼져 있으면 떼어낸다). public static void Apply(GameObject root) @@ -107,6 +107,7 @@ namespace WL.Look.Character { r.sharedMaterials = s_tmp.ToArray(); BrightenBase(null, r, s_tmp.Count > 0 ? s_tmp[0] : null, false); // 우리가 붙였던 렌더러만 본체 값도 되돌린다 + RestoreMkTint(r, s_tmp.Count > 0 ? s_tmp[0] : null); } } LastLog = "역광 패스 제거 @" + root.name; @@ -145,27 +146,178 @@ namespace WL.Look.Character r.SetPropertyBlock(s_mpb); } - /// 설정 값을 런타임 재질 2장에 다시 밀어 넣는다(프로브에서 값을 바꾼 뒤 호출). + /// 2등급 본체 색(MK Toon 그늘 색)을 재질 원래 값으로 되쓴다. + static void RestoreMkTint(Renderer r, Material baseMat) + { + if (r == null || baseMat == null || !baseMat.HasProperty(kGoochDark)) return; + if (s_mpb == null) s_mpb = new MaterialPropertyBlock(); + r.GetPropertyBlock(s_mpb); + s_mpb.SetColor(kGoochDark, baseMat.GetColor(kGoochDark)); + r.SetPropertyBlock(s_mpb); + } + + // ───────────────────────────────────────────────────────────────── + // 등급 — 2026-09-17 PD 「톤앤매너는 전체가 공유 · 플레이어 1등급 · 몬스터와 NPC 2등급 · 배경(프랍) 3등급」 + // 공유하는 것 = 빛 방향(가상 역광·채움빛) · 림 색 · 그늘 색 계열(따뜻한 보랏빛). 등급은 **세기**만 다르다. + // 2등급 = 같은 덧그리기 패스(림 약하게 + 몸 전체 채움빛 약하게) + 본체 그늘을 조금 밝게(Toon = 그늘 최소 밝기/색 · MK Toon = _GoochDarkColor) + // 3등급 = 덧그리기 없음(드로우콜 0 추가) · 프랍이 공유하는 Toon 재질의 **런타임 복사본**에 그늘 값만 살짝(재질당 1장 → 배칭 유지) + // ───────────────────────────────────────────────────────────────── + public static int Tier2Roots, Tier2Renderers, Tier3Renderers, Tier3Materials; + static readonly int kGoochDark = Shader.PropertyToID("_GoochDarkColor"); + + public static void ApplyTier2(GameObject root) + { + if (root == null) return; + var cfg = WLCharacterBacklightSettings.Instance; + if (cfg == null) return; + if (cfg.enabled_ == 0 || cfg.tiersEnabled == 0) { Remove(root); return; } + if (!EnsureMaterials(cfg)) return; + + int n = 0; + s_buf.Clear(); + root.GetComponentsInChildren(false, s_buf); + for (int i = 0; i < s_buf.Count; i++) + { + var r = s_buf[i]; + if (r == null || !(r is SkinnedMeshRenderer || r is MeshRenderer)) continue; + var mats = r.sharedMaterials; + if (mats == null || mats.Length == 0) continue; + s_tmp.Clear(); + Material baseMat = null; bool hasT2 = false; + for (int m = 0; m < mats.Length; m++) + { + if (IsOurs(mats[m])) { if (mats[m] == s_rimT2) hasT2 = true; continue; } + s_tmp.Add(mats[m]); + if (baseMat == null && mats[m] != null) baseMat = mats[m]; + } + if (baseMat == null || baseMat.renderQueue >= 2500) continue; // 불투명 본체만(반투명 이펙트 메시 제외) + if (!(hasT2 && mats.Length == s_tmp.Count + 1)) + { + s_tmp.Add(s_rimT2); + r.sharedMaterials = s_tmp.ToArray(); + } + TintTier2Base(cfg, r, baseMat); + n++; + } + Tier2Renderers += n; + } + + static void TintTier2Base(WLCharacterBacklightSettings cfg, Renderer r, Material baseMat) + { + if (s_mpb == null) s_mpb = new MaterialPropertyBlock(); + bool mk = baseMat.HasProperty(kGoochDark); + bool toon = baseMat.HasProperty(kMinDark) && baseMat.HasProperty(kShadowCol); + if (!mk && !toon) return; + r.GetPropertyBlock(s_mpb); + if (mk) s_mpb.SetColor(kGoochDark, cfg.tier2MkShadowColor); + if (toon) + { + s_mpb.SetFloat(kMinDark, Mathf.Clamp01(baseMat.GetFloat(kMinDark) + cfg.tier2MinimumDarknessAdd)); + s_mpb.SetColor(kShadowCol, Color.Lerp(baseMat.GetColor(kShadowCol), cfg.tier2ShadowColor, cfg.tier2ShadowBlend)); + } + r.SetPropertyBlock(s_mpb); + } + + // ── 3등급(프랍) — 공유 재질 복사본으로 갈아끼운다 + static readonly Dictionary s_t3Copy = new Dictionary(); + static readonly HashSet s_t3Made = new HashSet(); + static readonly HashSet s_t3Seen = new HashSet(); + struct T3State { public Renderer r; public Material[] mats; } + static readonly List s_t3Swapped = new List(256); + + /// 3등급으로 갈아끼운 프랍 재질을 전부 원래대로(설정을 끄면 다음 검색 때 자동으로 불린다). + public static void RestoreTier3() + { + for (int i = 0; i < s_t3Swapped.Count; i++) if (s_t3Swapped[i].r != null) s_t3Swapped[i].r.sharedMaterials = s_t3Swapped[i].mats; + s_t3Swapped.Clear(); s_t3Seen.Clear(); + } + + public static void ScanTier3() + { + var cfg = WLCharacterBacklightSettings.Instance; + if (cfg == null) return; + if (cfg.enabled_ == 0 || cfg.tiersEnabled == 0 || cfg.tier3ShaderNames == null || cfg.tier3ShaderNames.Length == 0) + { if (s_t3Swapped.Count > 0) RestoreTier3(); return; } + if (s_t3Seen.Count > 20000) s_t3Seen.Clear(); + for (int i = s_t3Swapped.Count - 1; i >= 0; i--) if (s_t3Swapped[i].r == null) s_t3Swapped.RemoveAt(i); // 씬이 내려가 사라진 것 + var rends = Object.FindObjectsByType(FindObjectsSortMode.None); + int swapped = 0; + for (int i = 0; i < rends.Length; i++) + { + var r = rends[i]; + if (r == null || !s_t3Seen.Add(r.GetInstanceID())) continue; + var mats = r.sharedMaterials; + if (mats == null || mats.Length == 0) continue; + bool any = false; + for (int m = 0; m < mats.Length; m++) if (IsTier3Source(cfg, mats[m])) { any = true; break; } + if (!any) continue; + // 캐릭터·몬스터·동물에 달린 것은 1·2등급이 맡는다 + if (r.GetComponentInParent() != null || r.GetComponentInParent() != null + || r.GetComponentInParent() != null + || r.GetComponentInParent() != null) continue; + s_t3Swapped.Add(new T3State { r = r, mats = (Material[])mats.Clone() }); + for (int m = 0; m < mats.Length; m++) if (IsTier3Source(cfg, mats[m])) mats[m] = Tier3Copy(cfg, mats[m]); + r.sharedMaterials = mats; + swapped++; + } + Tier3Renderers += swapped; Tier3Materials = s_t3Copy.Count; + } + + static bool IsTier3Source(WLCharacterBacklightSettings cfg, Material m) + { + if (m == null || m.shader == null || s_t3Made.Contains(m) || IsOurs(m)) return false; + if (!m.HasProperty(kMinDark) || !m.HasProperty(kShadowCol)) return false; + var n = m.shader.name; + for (int i = 0; i < cfg.tier3ShaderNames.Length; i++) if (cfg.tier3ShaderNames[i] == n) return true; + return false; + } + + static Material Tier3Copy(WLCharacterBacklightSettings cfg, Material src) + { + Material c; + if (s_t3Copy.TryGetValue(src, out c) && c != null) return c; + c = new Material(src) { name = src.name + " (WL tier3)" }; + c.SetFloat(kMinDark, Mathf.Clamp01(src.GetFloat(kMinDark) + cfg.tier3MinimumDarknessAdd)); + c.SetColor(kShadowCol, Color.Lerp(src.GetColor(kShadowCol), cfg.tier3ShadowColor, cfg.tier3ShadowBlend)); + s_t3Copy[src] = c; s_t3Made.Add(c); + return c; + } + + /// 설정 값을 런타임 재질에 다시 밀어 넣는다(프로브에서 값을 바꾼 뒤 호출). public static void RefreshValues() { var cfg = WLCharacterBacklightSettings.Instance; if (cfg == null) return; if (s_rim != null) Push(cfg, s_rim, false); if (s_rimSkin != null) Push(cfg, s_rimSkin, true); + if (s_rimT2 != null) PushTier2(cfg, s_rimT2); } static bool EnsureMaterials(WLCharacterBacklightSettings cfg) { - if (s_rim != null && s_rimSkin != null) return true; + if (s_rim != null && s_rimSkin != null && s_rimT2 != null) return true; var sh = Shader.Find("WL/CharBacklight"); if (sh == null) return false; if (s_rim == null) s_rim = new Material(sh) { name = "WL_CharBacklight(runtime)" }; if (s_rimSkin == null) s_rimSkin = new Material(sh) { name = "WL_CharBacklight_Skin(runtime)" }; Push(cfg, s_rim, false); Push(cfg, s_rimSkin, true); + if (s_rimT2 == null) s_rimT2 = new Material(sh) { name = "WL_CharBacklight_Tier2(runtime)" }; + PushTier2(cfg, s_rimT2); return true; } + /// 2등급(몬스터·NPC) 재질 = 1등급과 같은 색·방향(톤 공유)에 세기만 낮춘다 · 얼굴빛/피부 투과 없음 · 몸 전체 채움빛 약하게. + static void PushTier2(WLCharacterBacklightSettings c, Material m) + { + Push(c, m, false); + m.SetFloat("_RimStrength", c.tier2RimStrength); + m.SetFloat("_RimOpacity", c.tier2RimOpacity); + m.SetFloat("_WrapStrength", c.wrapStrength * 0.5f); + m.SetFloat("_FaceStrength", 0f); + m.SetFloat("_FillStrength", c.tier2FillStrength); + } + static void Push(WLCharacterBacklightSettings c, Material m, bool skin) { m.SetColor("_RimColor", c.rimColor); @@ -277,6 +429,20 @@ namespace WL.Look.Character UpdateBloomScope(cfg, cam); } + if (cfg != null) + { + if (Time.unscaledTime >= _nextTier2) + { + _nextTier2 = Time.unscaledTime + Mathf.Max(0.5f, cfg.tier2ScanSeconds); + ScanTier2(cfg); + } + if (Time.unscaledTime >= _nextTier3) + { + _nextTier3 = Time.unscaledTime + Mathf.Max(1f, cfg.tier3ScanSeconds); + WLCharacterBacklight.ScanTier3(); + } + } + if (Time.unscaledTime >= _nextRescan) { _nextRescan = Time.unscaledTime + 2f; @@ -291,6 +457,47 @@ namespace WL.Look.Character } } + // ── 등급 검색 — 원본 Actor 를 역할로 나눠 2등급을 붙인다(PC 는 1등급 · 원본 0줄: 읽기만 한다) + float _nextTier2, _nextTier3; + static HashSet s_t2Roles; static string[] s_t2RolesSrc; + public static int Tier2Scans, LastTier2Actors; + + /// 지금 바로 1·2·3등급을 다시 맞춘다(프로브의 끔/켬 비교용). + public static void ForceScan() + { + var cfg = WLCharacterBacklightSettings.Instance; if (cfg == null) return; + for (int i = s_roots.Count - 1; i >= 0; i--) if (s_roots[i] != null && s_roots[i].activeInHierarchy) WLCharacterBacklight.Apply(s_roots[i]); + ScanTier2(cfg); + WLCharacterBacklight.ScanTier3(); + } + + static void ScanTier2(WLCharacterBacklightSettings cfg) + { + if (s_t2Roles == null || s_t2RolesSrc != cfg.tier2Roles) + { + s_t2RolesSrc = cfg.tier2Roles; s_t2Roles = new HashSet(); + if (cfg.tier2Roles != null) for (int i = 0; i < cfg.tier2Roles.Length; i++) if (!string.IsNullOrEmpty(cfg.tier2Roles[i])) s_t2Roles.Add(cfg.tier2Roles[i]); + } + int n = 0; + var actors = Object.FindObjectsByType(FindObjectsSortMode.None); + for (int i = 0; i < actors.Length; i++) + { + var a = actors[i]; + if (a == null || !a.gameObject.activeInHierarchy) continue; + if (a.m_Role == eRole.PC) + { + // 룩 컴포넌트 없는 PC 프리팹도 1등급 + if (!s_roots.Contains(a.gameObject) && a.GetComponentInChildren() == null) WLCharacterBacklight.Apply(a.gameObject); + continue; + } + if (!s_t2Roles.Contains(a.m_Role.ToString())) continue; + WLCharacterBacklight.ApplyTier2(a.gameObject); n++; + } + var animals = Object.FindObjectsByType(FindObjectsSortMode.None); + for (int i = 0; i < animals.Length; i++) if (animals[i] != null) { WLCharacterBacklight.ApplyTier2(animals[i].gameObject); n++; } + LastTier2Actors = n; WLCharacterBacklight.Tier2Roots = n; Tier2Scans++; + } + // ── 얼굴 포인트 라이트 — 카메라에 가장 가까운 등록 캐릭터의 얼굴(머리) 렌더러 경계 중심을 전역 벡터로 넘긴다 static readonly int kFaceCenterId = Shader.PropertyToID("_WLFaceCenter"); static readonly int kFaceDirId = Shader.PropertyToID("_WLFaceLightDir"); diff --git a/Assets/WL/Look/Character/WLCharacterBacklightSettings.cs b/Assets/WL/Look/Character/WLCharacterBacklightSettings.cs index d3519a4b4..b8f08cfe1 100644 --- a/Assets/WL/Look/Character/WLCharacterBacklightSettings.cs +++ b/Assets/WL/Look/Character/WLCharacterBacklightSettings.cs @@ -99,6 +99,34 @@ namespace WL.Look.Character [Tooltip("얼굴 중심으로 쓸 렌더러 이름 힌트(앞에서부터 찾는다 · 못 찾으면 얼굴 빛 없음).")] public string[] faceRendererHints = { "Face", "Head" }; + [Header("등급 — 2026-09-17 PD 「톤은 전체 공유 · 플레이어 1등급 · 몬스터와 NPC 2등급 · 배경(프랍) 3등급」")] + [Tooltip("1 = 2·3등급도 적용한다. 0 = 플레이어(1등급)만.")] + public int tiersEnabled = 1; + [Tooltip("2등급(몬스터·NPC·동료) — 림 세기/불투명도(1등급보다 약하게).")] + public float tier2RimStrength = 0.7f; + [Range(0f, 1f)] public float tier2RimOpacity = 0.35f; + [Tooltip("2등급 — 몸 전체 채움빛 세기(얼굴 중심 없음).")] + public float tier2FillStrength = 0.07f; + [Tooltip("2등급 — 본체가 Toon 그래프일 때: 그늘 최소 밝기에 더하는 양 · 그늘 색을 공유 그늘 색 쪽으로 섞는 비율(재질마다 원래 값이 달라 상대값으로).")] + [Range(0f, 0.5f)] public float tier2MinimumDarknessAdd = 0.10f; + [Range(0f, 1f)] public float tier2ShadowBlend = 0.6f; + public Color tier2ShadowColor = new Color(0.36f, 0.31f, 0.40f, 0f); + [Tooltip("2등급 — 본체가 MK Toon(힘민지 몬스터)일 때 그늘 색 `_GoochDarkColor`(원본 검정). 전체가 공유하는 따뜻한 보랏빛 그늘 계열.")] + public Color tier2MkShadowColor = new Color(0.30f, 0.25f, 0.34f, 1f); + [Tooltip("2등급 대상 역할(원본 eRole 이름). PC 는 1등급이라 제외.")] + public string[] tier2Roles = { "Mob", "Pet", "Worker", "Fairy", "EnemyPC", "Farmer", "Summon", "BattleNPC", "EnemyPet", "EnemySummon" }; + [Tooltip("2등급 재검색 주기(s) — 새로 나온 몬스터에 붙인다.")] + public float tier2ScanSeconds = 1.5f; + + [Tooltip("3등급(배경 프랍) — 덧그리기 없음(비용 0) · 공유 재질의 런타임 복사본에 그늘 값만 살짝: 그늘 최소 밝기에 더하는 양 · 공유 그늘 색 쪽으로 섞는 비율.")] + [Range(0f, 0.5f)] public float tier3MinimumDarknessAdd = 0.06f; + [Range(0f, 1f)] public float tier3ShadowBlend = 0.35f; + public Color tier3ShadowColor = new Color(0.29f, 0.27f, 0.39f, 0f); + [Tooltip("3등급 대상 셰이더 이름(이 셰이더를 쓰는 MeshRenderer · 캐릭터/몬스터/지형/풀 제외).")] + public string[] tier3ShaderNames = { "Shader Graphs/Toon" }; + [Tooltip("3등급 재검색 주기(s) — 섬 확장·설치물처럼 나중에 생기는 프랍.")] + public float tier3ScanSeconds = 5f; + [Header("⑤ 번짐(Bloom) — 역광이 화사하게 번지게(전체 화면 패스 · 모바일 비용 있음)")] [Tooltip("0 = 카메라·볼륨을 건드리지 않는다.")] public int bloomEnabled = 1; diff --git a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset index f64066e9f..b6a0bffe1 100644 --- a/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset +++ b/Assets/WL/Look/Farm/Resources/WL/WLIslandLookSettings.asset @@ -58,6 +58,8 @@ MonoBehaviour: ssaoOffScenes: - Level01 - WL_FarmLook + - WL_Dungeon01 + - WL_Dungeon02 fogOff: 1 disableFiGraphicsManager: 1 rescanAtSeconds: @@ -77,6 +79,21 @@ MonoBehaviour: shoreFoamEnabled: 1 shoreFoamUp: 0.3 shoreFoamDown: 2 + shoreSprayEnabled: 1 + shoreSprayColor: {r: 1, g: 1, b: 1, a: 1} + shoreSprayOpacity: 0.92 + shoreSprayNoiseScale: 2.4 + shoreSprayEdgeMin: 0.1 + shoreSprayEdgeMax: 0.36 + shoreSpraySandReach: 0.16 + shoreSprayWaveSpeed: 0.9 + shoreSprayLineSpacing: 0.62 + shoreSprayLineSpeed: 0.22 + shoreSprayLineWidth: 0.16 + shoreSprayLineReach: 1.7 + shoreSprayLineBreak: 0.6 + dungeonGrassMatchIsland: 1 + dungeonLightMatchIsland: 1 shoreFoamMaterial: {fileID: 2100000, guid: d24a4c5ce16646c409631c112b57f2cf, type: 2} shoreFoamWidth: 1.6 shoreFoamInner: 0.5 diff --git a/Assets/WL/Look/Farm/Shaders/WLShoreSpray.shader b/Assets/WL/Look/Farm/Shaders/WLShoreSpray.shader new file mode 100644 index 000000000..cd353b761 --- /dev/null +++ b/Assets/WL/Look/Farm/Shaders/WLShoreSpray.shader @@ -0,0 +1,158 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLShoreSpray.shader — 섬 물가의 물보라(절차적 거품) · 언릿 · 알파 블렌드 +// +// 2026-09-17 PD 「섬 가장자리의 물거품이 안개처럼 보이고 부자연스럽다 → 좀 더 자연스러운 물보라처럼」. +// 예전 띠 = 옅은 알파 그라데이션 텍스처(폭 2 m) → 흰 안개처럼 번져 보였다. +// 이 셰이더 = 경계가 **또렷한 조각**만 그린다(그라데이션 0): +// ① 물가 거품 띠: 물가선에 붙어 있고, 바다 쪽 가장자리가 노이즈로 끊긴다. 해안 구간마다 다른 박자로 밀려왔다(_EdgeMax) 빠진다(_EdgeMin). +// ② 포말 줄: 그 바깥에서 해안으로 밀려오는 가는 줄 2~3가닥. 멀수록 가늘고 듬성듬성(노이즈 임계가 올라가 조각으로 흩어진다). +// 입력: uv2.x = 물가선까지의 수평 거리(m · + 바다 / − 모래) — `WLShoreFoam.BuildFromTerrain` 이 채운다. 무늬 좌표 = 월드 xz(이음매 없음). +// 모바일: 텍스처 0장 · 그라디언트 노이즈 3회(좁은 띠 위에서만) · 조명 없음. +// ───────────────────────────────────────────────────────────────────────────── +Shader "WL/ShoreSpray" +{ + Properties + { + _Color ("Color", Color) = (1, 1, 1, 1) + _Opacity ("Opacity", Range(0, 1)) = 0.92 + _NoiseScale ("Noise Scale (1/m)", Float) = 2.4 + _EdgeMin ("Contact Foam Width — 빠졌을 때 (m)", Float) = 0.10 + _EdgeMax ("Contact Foam Width — 밀려왔을 때 (m)", Float) = 0.42 + _SandReach ("Sand Reach (m)", Float) = 0.16 + _WaveSpeed ("Wave Speed (rad/s)", Float) = 0.9 + _LineSpacing ("Wash Line Spacing (m)", Float) = 0.62 + _LineSpeed ("Wash Line Speed (spacing/s)", Float) = 0.22 + _LineWidth ("Wash Line Width (0~1)", Range(0.02, 0.6)) = 0.2 + _LineReach ("Wash Line Reach (m)", Float) = 1.7 + _LineBreak ("Wash Line Break (0~1)", Range(0, 1)) = 0.45 + } + + SubShader + { + Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Transparent" "Queue" = "Transparent+50" "IgnoreProjector" = "True" } + + Pass + { + Name "WLShoreSpray" + Tags { "LightMode" = "SRPDefaultUnlit" } + + Cull Off + ZWrite Off + ZTest LEqual + Blend SrcAlpha OneMinusSrcAlpha + + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #pragma target 3.0 + + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + + CBUFFER_START(UnityPerMaterial) + float4 _Color; + float _Opacity; + float _NoiseScale; + float _EdgeMin; + float _EdgeMax; + float _SandReach; + float _WaveSpeed; + float _LineSpacing; + float _LineSpeed; + float _LineWidth; + float _LineReach; + float _LineBreak; + CBUFFER_END + + struct Attributes + { + float4 positionOS : POSITION; + float2 uv2 : TEXCOORD1; + }; + + struct Varyings + { + float4 positionCS : SV_POSITION; + float3 positionWS : TEXCOORD0; + float dist : TEXCOORD1; // 물가선까지 수평 거리(m · + 바다 / − 모래) + }; + + Varyings vert(Attributes input) + { + Varyings o = (Varyings)0; + o.positionWS = TransformObjectToWorld(input.positionOS.xyz); + o.positionCS = TransformWorldToHClip(o.positionWS); + o.dist = input.uv2.x; + return o; + } + + float hash21(float2 p) + { + p = frac(p * float2(123.34, 456.21)); + p += dot(p, p + 45.32); + return frac(p.x * p.y); + } + + float2 grad2(float2 i) + { + float a = hash21(i) * 6.2831853; + return float2(cos(a), sin(a)); + } + + // 그라디언트(펄린) 노이즈 0..1 — 값 노이즈는 격자 모양(둥근 네모)이 임계 경계에 그대로 드러난다(2026-09-16 지형 실측과 같은 이유) + float gnoise(float2 p) + { + float2 i = floor(p), f = frac(p); + float2 u = f * f * (3.0 - 2.0 * f); + float a = dot(grad2(i), f); + float b = dot(grad2(i + float2(1, 0)), f - float2(1, 0)); + float c = dot(grad2(i + float2(0, 1)), f - float2(0, 1)); + float d = dot(grad2(i + float2(1, 1)), f - float2(1, 1)); + return saturate(0.5 + 0.72 * lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y)); + } + + // 화면 미분으로 1 px 만 부드럽게 한 계단(또렷하되 계단 현상 없음) + float aastep(float x, float edge) + { + float fw = max(fwidth(x), 1e-4); + return smoothstep(edge - fw, edge + fw, x); + } + + half4 frag(Varyings input) : SV_Target + { + float d = input.dist; + float2 w = input.positionWS.xz; + float t = _Time.y; + + float n1 = gnoise(w * _NoiseScale + float2(t * 0.13, t * 0.09)); + float n2 = gnoise(w * _NoiseScale * 2.6 + float2(-t * 0.21, t * 0.05)); + float n = n1 * 0.62 + n2 * 0.38; + + // 해안 구간마다 다른 박자로 밀려왔다 빠진다 + float ph = gnoise(w * 0.11) * 6.2831853; + float breath = 0.5 + 0.5 * sin(t * _WaveSpeed + ph); + float edgeW = lerp(_EdgeMin, _EdgeMax, breath); + + // ① 물가 거품 띠 — 모래 쪽 끝(−) ~ 바다 쪽 끝(+ · 노이즈로 끊김) + float seaEdge = edgeW * (0.45 + 1.1 * n); + float sandEdge = -_SandReach * (0.35 + 0.65 * n2) * (0.4 + 0.6 * breath); + float contact = aastep(d, sandEdge) * (1.0 - aastep(d, seaEdge)); + + // ② 포말 줄 — 해안으로 밀려온다(시간이 흐르면 같은 줄의 d 가 줄어든다) + float k = d / max(_LineSpacing, 0.05) + t * _LineSpeed; + float core = 1.0 - abs(frac(k) - 0.5) * 2.0; // 줄 중심 1 + float distT = saturate(d / max(_LineReach, 0.05)); + float width = _LineWidth * (1.0 - 0.6 * distT); // 멀수록 가늘다 + float washLine = aastep(core, 1.0 - width); + float keepTh = lerp(_LineBreak * 0.6, 0.98, distT * distT); // 멀수록 듬성듬성 + washLine *= aastep(n, keepTh); + washLine *= step(seaEdge, d) * step(d, _LineReach); + + float a = saturate(max(contact, washLine)) * _Opacity * _Color.a; + return half4(_Color.rgb, a); + } + ENDHLSL + } + } + + Fallback Off +} diff --git a/Assets/WL/Look/Farm/Shaders/WLShoreSpray.shader.meta b/Assets/WL/Look/Farm/Shaders/WLShoreSpray.shader.meta new file mode 100644 index 000000000..d9ab07a75 --- /dev/null +++ b/Assets/WL/Look/Farm/Shaders/WLShoreSpray.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 572f88bfeac39f241831f44603fa78a9 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Look/Farm/WLDungeonGrassLook.cs b/Assets/WL/Look/Farm/WLDungeonGrassLook.cs new file mode 100644 index 000000000..a382b1ea0 --- /dev/null +++ b/Assets/WL/Look/Farm/WLDungeonGrassLook.cs @@ -0,0 +1,131 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLDungeonGrassLook.cs — 던전(인게임) 풀밭을 섬과 같은 느낌으로 맞춘다 +// +// 2026-09-17 PD 「인게임의 풀밭의 느낌도 섬과 동일하게 맞춰줘」. +// +// ■ 실측(WL_Dungeon01 · 데모 씬 복사본)으로 본 차이 +// 항목 섬 던전(원래) +// 풀/꽃 크기 0.22 / 0.26 0.35 / 0.35 +// 구름 그늘 세기 0.5 1.0 +// 구름 그늘 크기 0.025 0.010 +// 토온 밝기 단계 0.12 0.25 +// 태양 그림자 0.45 1.0 +// SSAO 끔 켬(풀잎마다 발밑 그늘 = 얼룩) +// ■ 무엇을 하나(씬 파일·원본 재질 0줄 — 전부 런타임) +// 던전 씬의 데모 인스턴서(`TerrainInstancesBehaviour`) 항목을 섬 값으로 바꾸고(크기 · 재질은 구름/밝기만 덮은 런타임 복사본) +// 인스턴서를 껐다 켜 다시 심는다. 터레인 재질도 같은 복사본으로, 디렉셔널 라이트는 섬의 색·세기·그림자 진하기로 맞춘다. +// SSAO 는 `WLIslandLookSettings.ssaoOffScenes` 에 던전 씬을 넣어 `WLSkyScope` 가 끈다(나갈 때 복원). +// ■ 값 = 전부 `WLIslandLookSettings`(섬과 같은 칸을 읽는다 → 섬을 고치면 던전도 따라온다) · `dungeonGrassMatchIsland 0` = 아무 것도 안 한다. +// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙). +// ───────────────────────────────────────────────────────────────────────────── +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; +using DemoInstancer = global::Environment.Instancing.TerrainInstancesBehaviour; +using DemoLayer = global::Environment.Instancing.TerrainInstancesBehaviour.TerrainInstancingInput; + +namespace WL.Look.Farm +{ + public static class WLDungeonGrassLook + { + // ── 진단(프로브가 읽는다) + public static int Instancers, EntriesScaled, MaterialsTuned, TerrainsTuned, LightsMatched; + public static string LastLog = ""; + + static readonly Dictionary s_tuned = new Dictionary(); + + public static void Apply(Scene scene) + { + var cfg = WLIslandLookSettings.Instance; + if (cfg == null || cfg.enabled_ == 0 || cfg.dungeonGrassMatchIsland == 0 || !scene.IsValid() || !scene.isLoaded) return; + + Mesh grassMesh = ScatterMesh(cfg, cfg.demoGrass), flowerMesh = ScatterMesh(cfg, cfg.demoFlower); + int insts = 0, scaled = 0, terrains = 0, lights = 0; int tunedBefore = s_tuned.Count; + + var roots = scene.GetRootGameObjects(); + for (int r = 0; r < roots.Length; r++) + { + var list = roots[r].GetComponentsInChildren(true); + for (int i = 0; i < list.Length; i++) + { + var inst = list[i]; if (inst == null) continue; + bool was = inst.enabled; + if (was) inst.enabled = false; // OnDisable → 버퍼 해제 + scaled += TuneLayer(cfg, inst.FirstLayer, grassMesh, flowerMesh); + scaled += TuneLayer(cfg, inst.SecondLayer, grassMesh, flowerMesh); + scaled += TuneLayer(cfg, inst.ThirdLayer, grassMesh, flowerMesh); + scaled += TuneLayer(cfg, inst.FourthLayer, grassMesh, flowerMesh); + if (was) inst.enabled = true; // OnEnable → 새 값으로 다시 심는다 + insts++; + } + + var ts = roots[r].GetComponentsInChildren(true); + for (int i = 0; i < ts.Length; i++) + { + var t = ts[i]; if (t == null || t.materialTemplate == null) continue; + var m = Tuned(cfg, t.materialTemplate); + if (m != t.materialTemplate) { t.materialTemplate = m; terrains++; } + } + + if (cfg.dungeonLightMatchIsland != 0) + { + var ls = roots[r].GetComponentsInChildren(true); + for (int i = 0; i < ls.Length; i++) + { + var l = ls[i]; if (l == null || l.type != LightType.Directional) continue; + l.color = cfg.dirLightColor; + l.intensity = cfg.dirLightIntensity; + if (cfg.dirLightShadowStrength >= 0f) l.shadowStrength = Mathf.Clamp01(cfg.dirLightShadowStrength); + if (cfg.dirLightSoftShadows != 0 && l.shadows != LightShadows.None) l.shadows = LightShadows.Soft; + lights++; + } + } + } + + Instancers = insts; EntriesScaled = scaled; TerrainsTuned = terrains; LightsMatched = lights; MaterialsTuned = s_tuned.Count; + LastLog = "던전 풀밭 = 섬 값 — 인스턴서 " + insts + " · 크기 바꾼 항목 " + scaled + " · 재질 복사본 " + s_tuned.Count + "(새로 " + (s_tuned.Count - tunedBefore) + ")" + + " · 터레인 " + terrains + " · 태양 " + lights + " @" + scene.name; + cfg.Log(LastLog); + } + + static int TuneLayer(WLIslandLookSettings cfg, DemoLayer layer, Mesh grassMesh, Mesh flowerMesh) + { + if (layer == null || layer.Settings == null) return 0; + int n = 0; + for (int i = 0; i < layer.Settings.Length; i++) + { + var e = layer.Settings[i]; if (e == null) continue; + if (e.Material != null) e.Material = Tuned(cfg, e.Material); + if (grassMesh != null && e.Mesh == grassMesh && cfg.demoGrass != null && cfg.demoGrass.scale > 0f && !Mathf.Approximately(e.Scale, cfg.demoGrass.scale)) { e.Scale = cfg.demoGrass.scale; n++; } + else if (flowerMesh != null && e.Mesh == flowerMesh && cfg.demoFlower != null && cfg.demoFlower.scale > 0f && !Mathf.Approximately(e.Scale, cfg.demoFlower.scale)) { e.Scale = cfg.demoFlower.scale; n++; } + } + return n; + } + + static Mesh ScatterMesh(WLIslandLookSettings cfg, WLDemoScatter d) + { + var sc = cfg.scatter; + if (d == null || sc == null || d.scatterIndex < 0 || d.scatterIndex >= sc.Length || sc[d.scatterIndex] == null) return null; + return sc[d.scatterIndex].mesh; + } + + /// 섬의 `WLDemoTerrain.Tuned` 와 같은 규칙 — 구름 세기·크기·밝기 단계만 덮은 런타임 복사본(원본 재질 0줄). + static Material Tuned(WLIslandLookSettings cfg, Material src) + { + if (src == null) return null; + Material m; + if (s_tuned.TryGetValue(src, out m) && m != null) return m; + foreach (var kv in s_tuned) if (kv.Value == src) return src; // 이미 우리 복사본 + bool strength = cfg.demoCloudStrength > 0f && src.HasProperty("_Cloud_Strength"); + bool density = cfg.demoCloudDensity > 0f && src.HasProperty("_Cloud_Density"); + bool bright = cfg.demoTerrainBrightness > 0f && src.HasProperty("_Brightness") && src.GetFloat("_Brightness") < 0.99f; // 자갈(1.0)은 그대로 + if (!strength && !density && !bright) return src; + m = new Material(src) { name = src.name + " (WL dungeon=island)" }; + if (strength) m.SetFloat("_Cloud_Strength", cfg.demoCloudStrength); + if (density) m.SetFloat("_Cloud_Density", cfg.demoCloudDensity); + if (bright) m.SetFloat("_Brightness", cfg.demoTerrainBrightness); + s_tuned[src] = m; + return m; + } + } +} diff --git a/Assets/WL/Look/Farm/WLDungeonGrassLook.cs.meta b/Assets/WL/Look/Farm/WLDungeonGrassLook.cs.meta new file mode 100644 index 000000000..45fa9aaa8 --- /dev/null +++ b/Assets/WL/Look/Farm/WLDungeonGrassLook.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2debbf9cab3929a4aa60dcac2624d8b9 \ No newline at end of file diff --git a/Assets/WL/Look/Farm/WLIslandLookSettings.cs b/Assets/WL/Look/Farm/WLIslandLookSettings.cs index 67ebeb222..173ba4b0a 100644 --- a/Assets/WL/Look/Farm/WLIslandLookSettings.cs +++ b/Assets/WL/Look/Farm/WLIslandLookSettings.cs @@ -204,8 +204,8 @@ namespace WL.Look.Farm [Header("§0-e — 섬에서는 SSAO 를 끈다 (렌더러 에셋 0줄 · 나갈 때 복원)")] [Tooltip("1 = 아래 씬이 떠 있는 동안 URP 렌더러의 SSAO 피처를 끈다(전투 맵은 그대로). 0 = 손대지 않음.")] public int islandSsaoOff = 1; - [Tooltip("SSAO 를 끌 씬 이름(섬만 · 던전은 아레나 룩 그대로).")] - public string[] ssaoOffScenes = new[] { "Level01", "WL_FarmLook" }; + [Tooltip("SSAO 를 끌 씬 이름. 2026-09-17 PD 「인게임 풀밭도 섬과 동일하게」 → 던전 2개 추가(풀잎마다 발밑 그늘 = 얼룩의 원인).")] + public string[] ssaoOffScenes = new[] { "Level01", "WL_FarmLook", "WL_Dungeon01", "WL_Dungeon02" }; [Tooltip("1 이면 안개를 끈다(데모 = off).")] public int fogOff = 1; @@ -281,6 +281,42 @@ namespace WL.Look.Farm "2026-09-16 실측: 1.2 m 에 알파가 끝까지 남아 격자 칸 경계가 네모 계단으로 보였다 → 2 m.")] public float shoreFoamDown = 2f; + // ── 2026-09-17 PD 「섬 가장자리의 물거품이 안개처럼 보이고 부자연스럽다 → 좀 더 자연스러운 물보라처럼」 + // 옅은 그라데이션 띠(텍스처) 대신 셰이더 `WL/ShoreSpray` 가 절차적으로 그린다: ① 물가선에 붙은 **또렷한 거품 띠**(노이즈로 가장자리가 끊기고, 구간마다 다른 박자로 밀려왔다 빠진다) + // ② 그 바깥에서 해안으로 **밀려오는 가는 포말 줄** 2~3가닥(멀수록 듬성듬성). 전부 경계가 또렷한 조각이라 안개처럼 번지지 않는다. + [Header("§816zg — 물보라(절차적 거품) · 2026-09-17 PD 「안개 같다 → 자연스러운 물보라」")] + [Tooltip("1 = 물가 띠를 `WL/ShoreSpray` 셰이더로 그린다(데모 터레인 물가선 모드에서만). 0 = 예전 텍스처 띠(옅은 그라데이션).")] + public int shoreSprayEnabled = 1; + public Color shoreSprayColor = new Color(1f, 1f, 1f, 1f); + [Tooltip("거품 조각의 불투명도(0~1).")] + [Range(0f, 1f)] public float shoreSprayOpacity = 0.92f; + [Tooltip("거품 무늬 노이즈 크기(1/m). 클수록 잘게 끊긴다.")] + public float shoreSprayNoiseScale = 2.4f; + [Tooltip("물가 거품 띠가 바다 쪽으로 뻗는 폭(m) — 빠졌을 때 / 밀려왔을 때.")] + public float shoreSprayEdgeMin = 0.10f; + public float shoreSprayEdgeMax = 0.36f; + [Tooltip("물가 거품이 모래 위로 올라오는 길이(m).")] + public float shoreSpraySandReach = 0.16f; + [Tooltip("밀려왔다 빠지는 박자(rad/s).")] + public float shoreSprayWaveSpeed = 0.9f; + [Tooltip("포말 줄 사이 간격(m).")] + public float shoreSprayLineSpacing = 0.62f; + [Tooltip("포말 줄이 해안으로 밀려오는 속도(줄 간격/초).")] + public float shoreSprayLineSpeed = 0.22f; + [Tooltip("포말 줄 굵기(0~1 · 간격 대비).")] + [Range(0.02f, 0.6f)] public float shoreSprayLineWidth = 0.16f; + [Tooltip("포말 줄이 보이는 최대 거리(m · shoreFoamDown 이하).")] + public float shoreSprayLineReach = 1.7f; + [Tooltip("포말 줄이 끊기는 정도(0 = 이어진 줄 · 1 = 거의 점).")] + [Range(0f, 1f)] public float shoreSprayLineBreak = 0.6f; + + // ── 2026-09-17 PD 「인게임의 풀밭의 느낌도 섬과 동일하게」 + [Header("§816zh — 던전(인게임) 풀밭을 섬 값으로 맞춘다")] + [Tooltip("1 = 던전 씬의 데모 인스턴서·터레인 재질을 섬과 같은 값(풀/꽃 크기 · 구름 세기/크기 · 밝기 단계)으로 런타임에 맞춘다(씬·원본 재질 0줄). 0 = 던전 원래대로.")] + public int dungeonGrassMatchIsland = 1; + [Tooltip("1 = 던전의 디렉셔널 라이트도 섬의 색·세기·그림자 진하기(`dirLight*`)로 맞춘다.")] + public int dungeonLightMatchIsland = 1; + // ───────────────────────────────── §816zf 부드러운 흙 데칼 [Header("§816zf — 흙 자리를 데모처럼 「둥글게 번지게」 (PD 2026-09-15)")] [Tooltip("0 = 지금(각진 1×1 흙 타일 · 100 % 복귀) · 1 = 밭 구역을 덮는 **부드러운 흙 데칼 1장**" + diff --git a/Assets/WL/Look/Farm/WLShoreFoam.cs b/Assets/WL/Look/Farm/WLShoreFoam.cs index 5d39085cc..fab46847f 100644 --- a/Assets/WL/Look/Farm/WLShoreFoam.cs +++ b/Assets/WL/Look/Farm/WLShoreFoam.cs @@ -73,6 +73,7 @@ namespace WL.Look.Farm void Update() { if (cfg == null || _mr == null || _inst == null) return; + if (_spray) return; // 물보라 셰이더는 _Time 으로 스스로 움직인다(텍스처 오프셋 없음) float sp = cfg.shoreFoamScroll; if (sp == 0f) return; _scroll += Time.deltaTime * sp; @@ -83,6 +84,8 @@ namespace WL.Look.Farm _inst.SetTextureOffset(IdBase, new Vector2(_scroll, waterline ? 0f : _scroll * 0.37f)); } Material _inst; + bool _spray; + public static bool SprayActive; public static string SprayLog = ""; static readonly int IdBase = Shader.PropertyToID("_BaseMap"); // ───────────────────────────────────────────────────────────────── @@ -93,11 +96,41 @@ namespace WL.Look.Farm if (cfg == null || cfg.enabled_ == 0 || cfg.shoreFoamEnabled == 0) { _mr.enabled = false; LastLog = "꺼짐"; return; } - if (cfg.shoreFoamMaterial == null) { _mr.enabled = false; LastLog = "🔴 머티리얼 없음"; return; } - if (_inst == null || _inst.shader != cfg.shoreFoamMaterial.shader) + // 🔴 2026-09-17 PD 「물거품이 안개처럼 보인다 → 자연스러운 물보라」 — 데모 터레인 물가선 모드에서는 절차적 거품 셰이더 `WL/ShoreSpray` + // (또렷한 물가 거품 + 밀려오는 포말 줄)로 그린다. 셰이더가 없거나 `shoreSprayEnabled 0` 이면 예전 텍스처 띠. + Shader sprayShader = cfg.shoreSprayEnabled != 0 && cfg.demoTerrainEnabled != 0 ? Shader.Find("WL/ShoreSpray") : null; + _spray = sprayShader != null; + SprayActive = _spray; + if (_spray) { - if (_inst != null) Destroy(_inst); - _inst = new Material(cfg.shoreFoamMaterial) { name = "WL_ShoreFoam(runtime)", hideFlags = HideFlags.DontSave }; + if (_inst == null || _inst.shader != sprayShader) + { + if (_inst != null) Destroy(_inst); + _inst = new Material(sprayShader) { name = "WL_ShoreSpray(runtime)" }; + } + _inst.SetColor("_Color", cfg.shoreSprayColor); + _inst.SetFloat("_Opacity", cfg.shoreSprayOpacity); + _inst.SetFloat("_NoiseScale", cfg.shoreSprayNoiseScale); + _inst.SetFloat("_EdgeMin", cfg.shoreSprayEdgeMin); + _inst.SetFloat("_EdgeMax", cfg.shoreSprayEdgeMax); + _inst.SetFloat("_SandReach", cfg.shoreSpraySandReach); + _inst.SetFloat("_WaveSpeed", cfg.shoreSprayWaveSpeed); + _inst.SetFloat("_LineSpacing", cfg.shoreSprayLineSpacing); + _inst.SetFloat("_LineSpeed", cfg.shoreSprayLineSpeed); + _inst.SetFloat("_LineWidth", cfg.shoreSprayLineWidth); + _inst.SetFloat("_LineReach", Mathf.Min(cfg.shoreSprayLineReach, cfg.shoreFoamDown)); + _inst.SetFloat("_LineBreak", cfg.shoreSprayLineBreak); + SprayLog = "물보라 셰이더 · 물가 띠 " + cfg.shoreSprayEdgeMin.ToString("F2") + "~" + cfg.shoreSprayEdgeMax.ToString("F2") + " m · 포말 줄 간격 " + cfg.shoreSprayLineSpacing.ToString("F2") + " m"; + } + else + { + if (cfg.shoreFoamMaterial == null) { _mr.enabled = false; LastLog = "🔴 머티리얼 없음"; return; } + if (_inst == null || _inst.shader != cfg.shoreFoamMaterial.shader) + { + if (_inst != null) Destroy(_inst); + _inst = new Material(cfg.shoreFoamMaterial) { name = "WL_ShoreFoam(runtime)", hideFlags = HideFlags.DontSave }; + } + SprayLog = "텍스처 띠(예전 방식)"; } _mr.sharedMaterial = _inst; @@ -217,9 +250,12 @@ namespace WL.Look.Farm var tp = terrain.transform.position; var sz = terrain.terrainData.size; // 띠 텍스처 = 런타임 생성(물가선에서 진하고 모래 쪽은 짧게 · 바다 쪽은 길게 들쭉날쭉 사라진다) — 에셋 텍스처는 네모 띠(816j2)용이라 가장자리가 딱 끊긴다 - var tex = FoamTexture(up / (up + down)); - _inst.SetTexture(IdBase, tex); - if (_inst.HasProperty("_MainTex")) _inst.SetTexture("_MainTex", tex); + if (!_spray) + { + var tex = FoamTexture(up / (up + down)); + _inst.SetTexture(IdBase, tex); + if (_inst.HasProperty("_MainTex")) _inst.SetTexture("_MainTex", tex); + } int nx = Mathf.Clamp(Mathf.CeilToInt(sz.x / cell) + 1, 2, 1024); int nz = Mathf.Clamp(Mathf.CeilToInt(sz.z / cell) + 1, 2, 1024); float stepX = sz.x / (nx - 1), stepZ = sz.z / (nz - 1); @@ -280,6 +316,7 @@ namespace WL.Look.Farm for (int k = 0; k < vmap.Length; k++) vmap[k] = -1; var verts = new List(2048); var uvs = new List(2048); + var uvs2 = new List(2048); // x = 물가선까지 수평 거리(m · + 바다 / − 모래) — 물보라 셰이더 입력 var tris = new List(4096); for (int j = 0; j < nz - 1; j++) @@ -302,6 +339,7 @@ namespace WL.Look.Farm verts.Add(new Vector3(x, Mathf.Max(hgt[k], waterY) + lift, z)); float v = Mathf.Clamp01((f[k] + up) / (down + up)); uvs.Add(new Vector2((x * 0.7071f + z * 0.7071f) / tileLen, v)); + uvs2.Add(new Vector2(Mathf.Clamp(f[k], -up - 1f, down + 1f), 0f)); vmap[k] = verts.Count - 1; return vmap[k]; } @@ -312,6 +350,7 @@ namespace WL.Look.Farm ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16; _mesh.SetVertices(verts); _mesh.SetUVs(0, uvs); + _mesh.SetUVs(1, uvs2); _mesh.SetTriangles(tris, 0); _mesh.RecalculateBounds(); _mr.enabled = true;