[WL] 던전 풀밭 = 섬 값 · 물가 물보라(절차적 거품 셰이더) · 등급 연출(PC 1 · 몬스터/NPC 2 · 프랍 3 — 톤 공유·세기만 차등)
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
b2d08cf739
commit
a0634ad735
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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<Renderer> s_buf = new List<Renderer>(16);
|
||||
static readonly List<Material> s_tmp = new List<Material>(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); }
|
||||
|
||||
/// <summary>root 아래 캐릭터 렌더러에 역광 패스를 덧붙인다(설정이 꺼져 있으면 떼어낸다).</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>설정 값을 런타임 재질 2장에 다시 밀어 넣는다(프로브에서 값을 바꾼 뒤 호출).</summary>
|
||||
/// <summary>2등급 본체 색(MK Toon 그늘 색)을 재질 원래 값으로 되쓴다.</summary>
|
||||
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<Renderer>(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<Material, Material> s_t3Copy = new Dictionary<Material, Material>();
|
||||
static readonly HashSet<Material> s_t3Made = new HashSet<Material>();
|
||||
static readonly HashSet<int> s_t3Seen = new HashSet<int>();
|
||||
struct T3State { public Renderer r; public Material[] mats; }
|
||||
static readonly List<T3State> s_t3Swapped = new List<T3State>(256);
|
||||
|
||||
/// <summary>3등급으로 갈아끼운 프랍 재질을 전부 원래대로(설정을 끄면 다음 검색 때 자동으로 불린다).</summary>
|
||||
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<MeshRenderer>(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<Actor>() != null || r.GetComponentInParent<WLCharacterLook>() != null
|
||||
|| r.GetComponentInParent<CryingSnow.FarmingIsland.PlayerController>() != null
|
||||
|| r.GetComponentInParent<CryingSnow.FarmingIsland.AnimalController>() != 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;
|
||||
}
|
||||
|
||||
/// <summary>설정 값을 런타임 재질에 다시 밀어 넣는다(프로브에서 값을 바꾼 뒤 호출).</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>2등급(몬스터·NPC) 재질 = 1등급과 같은 색·방향(톤 공유)에 세기만 낮춘다 · 얼굴빛/피부 투과 없음 · 몸 전체 채움빛 약하게.</summary>
|
||||
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<string> s_t2Roles; static string[] s_t2RolesSrc;
|
||||
public static int Tier2Scans, LastTier2Actors;
|
||||
|
||||
/// <summary>지금 바로 1·2·3등급을 다시 맞춘다(프로브의 끔/켬 비교용).</summary>
|
||||
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<string>();
|
||||
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<Actor>(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<WLCharacterLook>() == null) WLCharacterBacklight.Apply(a.gameObject);
|
||||
continue;
|
||||
}
|
||||
if (!s_t2Roles.Contains(a.m_Role.ToString())) continue;
|
||||
WLCharacterBacklight.ApplyTier2(a.gameObject); n++;
|
||||
}
|
||||
var animals = Object.FindObjectsByType<CryingSnow.FarmingIsland.AnimalController>(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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 572f88bfeac39f241831f44603fa78a9
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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<Material, Material> s_tuned = new Dictionary<Material, Material>();
|
||||
|
||||
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<DemoInstancer>(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<Terrain>(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<Light>(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;
|
||||
}
|
||||
|
||||
/// <summary>섬의 `WLDemoTerrain.Tuned` 와 같은 규칙 — 구름 세기·크기·밝기 단계만 덮은 런타임 복사본(원본 재질 0줄).</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2debbf9cab3929a4aa60dcac2624d8b9
|
||||
|
|
@ -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장**" +
|
||||
|
|
|
|||
|
|
@ -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<Vector3>(2048);
|
||||
var uvs = new List<Vector2>(2048);
|
||||
var uvs2 = new List<Vector2>(2048); // x = 물가선까지 수평 거리(m · + 바다 / − 모래) — 물보라 셰이더 입력
|
||||
var tris = new List<int>(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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue