// ───────────────────────────────────────────────────────────────────────────── // 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 }