523 lines
29 KiB
C#
523 lines
29 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLCloudShadow.cs — 데모 Toon 셰이더의 「구름 그림자 띠」를 CPU 에서 **같은 식으로** 재현한다
|
||
// (WL-816o · #816)
|
||
//
|
||
// ■ 왜 필요한가 (PD 지시 2026-09-15)
|
||
// 「데모와 같은 비주얼. 하지만 잔디를 많이 심어서 퍼포먼스에 영향을 주지 않는 방향으로
|
||
// 최적화된 로직(예: 구름 그림자 기준으로만 배치)」
|
||
// 816n 실측 — 데모 풀은 바닥과 거의 같은 색(밝기비 0.933)이라 **구름 그림자 경계에서만**
|
||
// 눈에 띈다(보이는 풀의 88 % 가 경계 띠 15.7 % 면적에 몰림). 즉 평평한 색 면 안쪽의 풀은
|
||
// 삼각형만 먹고 화면에는 거의 기여하지 않는다 → **경계 띠에만 심으면 그림은 같고 비용은 준다**.
|
||
//
|
||
// ■ 무엇을 그대로 옮겼나 (원본 0줄 — 읽기만 했다)
|
||
// `Assets/3DPixelArtEnvironment/Shaders/Subgraphs/CloudShadows.shadersubgraph`
|
||
// worldPos ─▶ ShadowProjection(SunDir) ─▶ TilingAndOffset(offset = _Time.y × _Cloud_Movement)
|
||
// ─▶ CloudNoise(Scale=_Cloud_Density, VerticalSpeed=_Cloud_Change,
|
||
// Step=_Cloud_Step, Coverage=_Cloud_Cover, Time=_Time.y)
|
||
// ─▶ × _Cloud_Strength = Cloudiness
|
||
// `Subgraphs/ToonLighting.shadersubgraph` Lighting = step(ShadowAtten) − Cloudiness
|
||
// `Subgraphs/ToonRamp.shadersubgraph` t = saturate(ceil(Lighting × _Shades + _Brightness) / _Shades)
|
||
// → 섬 윗면은 **평면**이라 ShadowAtten·법선이 상수다. 그래서 화면에 보이는 색 띠의 경계는
|
||
// 오로지 `ceil((1 − Cloudiness) × _Shades + _Brightness)` 가 바뀌는 지점이다.
|
||
// `Includes/CloudNoise.hlsl`(6 옥타브 FBM) · `Includes/SimplexNoise3D.hlsl`(Ashima snoise) 를
|
||
// C# 으로 1:1 이식했다.
|
||
//
|
||
// ■ C45 — _Cloud_*, _Shades, _Brightness 는 전부 **머티리얼에서 읽는다**(코드 상수 0).
|
||
// 셰이더 값이 바뀌면 이 판정도 자동으로 따라간다.
|
||
//
|
||
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
using UnityEngine;
|
||
|
||
namespace WL.Look.Farm
|
||
{
|
||
/// <summary>
|
||
/// 구름 그림자의 「색 띠 경계」를 XZ 격자에 한 번 구워 두고, 점 하나가 경계 근처인지 O(1) 로 답한다.
|
||
/// </summary>
|
||
public sealed class WLCloudShadowField
|
||
{
|
||
// ── 머티리얼에서 읽은 값 ────────────────────────────────────────
|
||
public bool Ready;
|
||
public bool CloudsOn;
|
||
public string Why = "";
|
||
|
||
float _scale, _cover, _strength, _change, _shades, _brightness;
|
||
float _movX, _movY, _stepX, _stepY;
|
||
float _sunX, _sunY, _sunZ;
|
||
float _time;
|
||
|
||
// ── 구운 격자 ──────────────────────────────────────────────────
|
||
int[] _band;
|
||
byte[] _edge, _tmp;
|
||
float[] _smp;
|
||
int _nx, _nz;
|
||
float _minX, _minZ, _cell, _inv;
|
||
|
||
public int Cells { get { return _nx * _nz; } }
|
||
public float CellSize { get { return _cell; } }
|
||
public int Samples; // 노이즈를 실제로 계산한 점 수(비용은 여기서만 난다)
|
||
public float SampleCellSize;
|
||
public float EdgeFraction; // 격자에서 경계로 잡힌 칸의 비율(예산 추정에 쓴다)
|
||
|
||
/// <summary>구름 값이 1초에 월드 몇 m 를 흐르는가 — 갱신 주기 ↔ 띠 폭을 맞추는 데 쓴다.</summary>
|
||
public float DriftPerSecond { get { return Mathf.Sqrt(_movX * _movX + _movY * _movY); } }
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// ① 셰이더 값 읽기
|
||
// ─────────────────────────────────────────────────────────────
|
||
public bool Configure(Material groundMat)
|
||
{
|
||
Ready = false; Why = "";
|
||
if (groundMat == null) { Why = "바닥 머티리얼이 없다"; return false; }
|
||
if (!groundMat.HasProperty("_Cloud_Density")) { Why = "머티리얼에 _Cloud_* 가 없다(Toon 계열이 아니다)"; return false; }
|
||
|
||
_scale = groundMat.GetFloat("_Cloud_Density");
|
||
_cover = groundMat.HasProperty("_Cloud_Cover") ? groundMat.GetFloat("_Cloud_Cover") : 0.5f;
|
||
_strength = groundMat.HasProperty("_Cloud_Strength") ? groundMat.GetFloat("_Cloud_Strength") : 1f;
|
||
_change = groundMat.HasProperty("_Cloud_Change") ? groundMat.GetFloat("_Cloud_Change") : 0f;
|
||
|
||
var mv = groundMat.HasProperty("_Cloud_Movement") ? groundMat.GetVector("_Cloud_Movement") : Vector4.zero;
|
||
_movX = mv.x; _movY = mv.y;
|
||
var st = groundMat.HasProperty("_Cloud_Step") ? groundMat.GetVector("_Cloud_Step") : Vector4.zero;
|
||
_stepX = st.x; _stepY = st.y;
|
||
|
||
_shades = groundMat.HasProperty("_Shades") ? groundMat.GetFloat("_Shades") : 1f;
|
||
_brightness = groundMat.HasProperty("_Brightness") ? groundMat.GetFloat("_Brightness") : 0f;
|
||
if (_shades < 1f) _shades = 1f;
|
||
|
||
// 🔴 실측(816o) — `_CLOUDSENABLED` 는 **전역 키워드**라 `Material.IsKeywordEnabled`(로컬 공간)로는
|
||
// 항상 false 가 나온다. 머티리얼에 실제로 켜져 있는지는 `shaderKeywords` 배열로 봐야 한다.
|
||
CloudsOn = false;
|
||
var kws = groundMat.shaderKeywords;
|
||
for (int i = 0; i < kws.Length; i++) if (kws[i] == "_CLOUDSENABLED") { CloudsOn = true; break; }
|
||
if (!CloudsOn)
|
||
CloudsOn = groundMat.IsKeywordEnabled("_CLOUDSENABLED")
|
||
|| (groundMat.HasProperty("_CLOUDSENABLED") && groundMat.GetFloat("_CLOUDSENABLED") > 0.5f);
|
||
if (!CloudsOn) { Why = "이 머티리얼은 구름이 꺼져 있다"; return false; }
|
||
if (_strength <= 0f || _scale <= 0f) { Why = "_Cloud_Strength/_Cloud_Density 가 0"; return false; }
|
||
|
||
ResolveSun();
|
||
ResolveTime();
|
||
|
||
Ready = true;
|
||
return true;
|
||
}
|
||
|
||
void ResolveSun()
|
||
{
|
||
// URP `Main Light` = RenderSettings.sun(지정돼 있으면) · Direction = 빛으로 향하는 방향 = −forward
|
||
Light sun = RenderSettings.sun;
|
||
if (sun == null || !sun.isActiveAndEnabled || sun.type != LightType.Directional)
|
||
{
|
||
sun = null;
|
||
float best = -1f;
|
||
var lights = Object.FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||
for (int i = 0; i < lights.Length; i++)
|
||
{
|
||
var l = lights[i];
|
||
if (l == null || l.type != LightType.Directional || !l.isActiveAndEnabled) continue;
|
||
if (l.intensity > best) { best = l.intensity; sun = l; }
|
||
}
|
||
}
|
||
Vector3 d = sun != null ? -sun.transform.forward : Vector3.up;
|
||
_sunX = d.x; _sunY = d.y; _sunZ = d.z;
|
||
}
|
||
|
||
void ResolveTime()
|
||
{
|
||
// 셰이더가 실제로 쓰는 _Time.y 를 우선 읽는다. 못 읽으면(0) 레벨 로드 후 경과 시간.
|
||
var t = Shader.GetGlobalVector("_Time");
|
||
_time = (t.y != 0f) ? t.y : Time.timeSinceLevelLoad;
|
||
}
|
||
|
||
/// <summary>시간만 다시 읽는다(주기 갱신용 — 머티리얼·태양은 그대로).</summary>
|
||
public void Tick() { ResolveSun(); ResolveTime(); }
|
||
|
||
/// <summary>시간만 다시 읽는다(816zh 띠 갱신용 — 태양·머티리얼은 그대로 · FindObjectsByType 0회).</summary>
|
||
public void TickTime() { ResolveTime(); }
|
||
|
||
/// <summary>머티리얼 `_Cloud_Cover` — 816zh 띠의 중심선(구름 높이가 이 값을 지나는 곳 = 그림자 얼룩의 가장자리).</summary>
|
||
public float Cover { get { return _cover; } }
|
||
|
||
/// <summary>셰이더가 쓰는 `_Time.y`. 진단·검증에서 직접 넣어 볼 수 있다.</summary>
|
||
public float TimeValue { get { return _time; } set { _time = value; } }
|
||
|
||
public Vector3 SunDir { get { return new Vector3(_sunX, _sunY, _sunZ); } }
|
||
public string Dump()
|
||
{
|
||
return "scale=" + _scale + " cover=" + _cover + " strength=" + _strength + " change=" + _change
|
||
+ " mov=(" + _movX + "," + _movY + ") step=(" + _stepX + "," + _stepY + ")"
|
||
+ " shades=" + _shades + " brightness=" + _brightness
|
||
+ " sun=(" + _sunX.ToString("F3") + "," + _sunY.ToString("F3") + "," + _sunZ.ToString("F3") + ")"
|
||
+ " time=" + _time.ToString("F3");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// ② 셰이더와 같은 식
|
||
// ─────────────────────────────────────────────────────────────
|
||
/// <summary>CloudShadows 서브그래프의 최종 출력 `Cloudiness`.</summary>
|
||
public float Cloudiness(float wx, float wy, float wz)
|
||
{
|
||
// ShadowProjection.hlsl — 그림자가 바닥에 떨어지는 지점
|
||
float m = (Mathf.Abs(_sunY) > 1e-4f) ? (wy / -_sunY) : 0f;
|
||
float px = wx + m * _sunX;
|
||
float pz = wz + m * _sunZ;
|
||
|
||
// TilingAndOffset (Tiling = 1) — offset = _Time.y × _Cloud_Movement
|
||
float ux = px + _time * _movX;
|
||
float uy = pz + _time * _movY;
|
||
|
||
// CloudNoise.hlsl — 6 옥타브 FBM
|
||
float S = _scale;
|
||
float tz = _time * _change;
|
||
float n = Snoise(ux * S, uy * S, tz);
|
||
n += 0.5f * Snoise((ux * 2f - _stepX) * S, (uy * 2f - _stepY) * S, tz);
|
||
n += 0.25f * Snoise((ux * 4f - 2f * _stepX) * S, (uy * 4f - 2f * _stepY) * S, tz);
|
||
n += 0.125f * Snoise((ux * 8f - 3f * _stepX) * S, (uy * 8f - 3f * _stepY) * S, tz);
|
||
n += 0.0625f * Snoise((ux * 16f - 4f * _stepX) * S, (uy * 16f - 4f * _stepY) * S, tz);
|
||
n += 0.03125f * Snoise((ux * 32f - 5f * _stepX) * S, (uy * 32f - 5f * _stepY) * S, tz);
|
||
|
||
return (_cover + 0.5f * n) * _strength;
|
||
}
|
||
|
||
/// <summary>ToonRamp 가 만드는 색 띠 번호. 이 값이 바뀌는 곳이 화면에서 보이는 경계다.</summary>
|
||
public int Band(float wx, float wy, float wz) { return BandOf(Cloudiness(wx, wy, wz)); }
|
||
|
||
/// <summary>구름 값 하나를 띠 번호로. (보간한 값에도 쓴다)</summary>
|
||
public int BandOf(float cloudiness)
|
||
{
|
||
float lighting = 1f - cloudiness; // step(ShadowAtten)=1 (평면·그림자 없음)
|
||
int k = Mathf.CeilToInt(lighting * _shades + _brightness);
|
||
// ToonRamp 의 saturate — 위아래로 넘친 칸은 한 색으로 뭉친다(=경계가 없다)
|
||
int top = Mathf.CeilToInt(_shades);
|
||
if (k < 0) k = 0;
|
||
if (k > top) k = top;
|
||
return k;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// ③ 격자에 굽기
|
||
// ─────────────────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 주어진 범위의 XZ 평면(높이 y)에 대해 띠 번호를 구우고, 번호가 바뀌는 칸을 경계로 표시한다.
|
||
/// </summary>
|
||
/// <param name="cell">격자 칸(m). 작을수록 띠가 얇고 정확하다.</param>
|
||
/// <param name="maxCells">칸 수 상한 — 섬이 커지면 칸을 자동으로 키운다(모바일 예산).</param>
|
||
/// <param name="dilate">경계에서 몇 칸 더 넓힐 것인가(구름이 흐르는 만큼 미리 덮어 둔다).</param>
|
||
public void Bake(Bounds b, float y, float cell, int maxCells, int dilate,
|
||
float sampleCell, int maxSamples)
|
||
{
|
||
EdgeFraction = 0f;
|
||
if (!Ready) return;
|
||
if (cell <= 0.01f) cell = 0.01f;
|
||
if (dilate < 0) dilate = 0;
|
||
if (sampleCell < cell) sampleCell = cell;
|
||
|
||
int pad = dilate + 2;
|
||
float w = Mathf.Max(b.size.x, 0.1f);
|
||
float h = Mathf.Max(b.size.z, 0.1f);
|
||
|
||
// 칸 수 상한 — 넘으면 칸을 키운다
|
||
for (int guard = 0; guard < 24; guard++)
|
||
{
|
||
int tx = Mathf.CeilToInt(w / cell) + 1 + pad * 2;
|
||
int tz = Mathf.CeilToInt(h / cell) + 1 + pad * 2;
|
||
if (maxCells <= 0 || (long)tx * tz <= maxCells) { _nx = tx; _nz = tz; break; }
|
||
cell *= 1.25f;
|
||
_nx = tx; _nz = tz;
|
||
}
|
||
_cell = cell;
|
||
_inv = 1f / cell;
|
||
_minX = b.min.x - pad * cell;
|
||
_minZ = b.min.z - pad * cell;
|
||
|
||
int n = _nx * _nz;
|
||
if (_band == null || _band.Length < n) { _band = new int[n]; _edge = new byte[n]; _tmp = new byte[n]; }
|
||
|
||
// ── ① 구름 값은 **성긴 격자**에서만 계산한다 (6 옥타브 노이즈 = 유일한 비싼 부분) ──
|
||
// 구름의 가장 작은 무늬가 1/(32×_Cloud_Density) ≈ 3 m 라 1 m 간격이면 충분히 따라간다.
|
||
float spanX = (_nx - 1) * cell, spanZ = (_nz - 1) * cell;
|
||
int sx = 0, sz = 0;
|
||
for (int guard = 0; guard < 24; guard++)
|
||
{
|
||
sx = Mathf.CeilToInt(spanX / sampleCell) + 2;
|
||
sz = Mathf.CeilToInt(spanZ / sampleCell) + 2;
|
||
if (maxSamples <= 0 || (long)sx * sz <= maxSamples) break;
|
||
sampleCell *= 1.25f;
|
||
}
|
||
SampleCellSize = sampleCell; Samples = sx * sz;
|
||
if (_smp == null || _smp.Length < sx * sz) _smp = new float[sx * sz];
|
||
float s0x = _minX + 0.5f * cell - sampleCell; // 가장자리 보간을 위해 한 칸 바깥에서 시작
|
||
float s0z = _minZ + 0.5f * cell - sampleCell;
|
||
for (int j = 0; j < sz; j++)
|
||
{
|
||
float wz = s0z + j * sampleCell;
|
||
int row = j * sx;
|
||
for (int i2 = 0; i2 < sx; i2++) _smp[row + i2] = Cloudiness(s0x + i2 * sampleCell, y, wz);
|
||
}
|
||
|
||
// ── ② 촘촘한 격자에서는 **보간한 값**만 띠 번호로 바꾼다(노이즈 재계산 없음) ──
|
||
float invS = 1f / sampleCell;
|
||
for (int iz = 0; iz < _nz; iz++)
|
||
{
|
||
float wz = _minZ + (iz + 0.5f) * cell;
|
||
float fz = (wz - s0z) * invS;
|
||
int jz = (int)fz; if (jz < 0) jz = 0; if (jz > sz - 2) jz = sz - 2;
|
||
float tz = fz - jz;
|
||
int row = iz * _nx, r0 = jz * sx, r1 = r0 + sx;
|
||
for (int ix = 0; ix < _nx; ix++)
|
||
{
|
||
float wx = _minX + (ix + 0.5f) * cell;
|
||
float fx = (wx - s0x) * invS;
|
||
int jx = (int)fx; if (jx < 0) jx = 0; if (jx > sx - 2) jx = sx - 2;
|
||
float tx = fx - jx;
|
||
float a = _smp[r0 + jx] + (_smp[r0 + jx + 1] - _smp[r0 + jx]) * tx;
|
||
float c = _smp[r1 + jx] + (_smp[r1 + jx + 1] - _smp[r1 + jx]) * tx;
|
||
_band[row + ix] = BandOf(a + (c - a) * tz);
|
||
}
|
||
}
|
||
|
||
for (int i = 0; i < n; i++) _edge[i] = 0;
|
||
for (int iz = 1; iz < _nz - 1; iz++)
|
||
{
|
||
int row = iz * _nx;
|
||
for (int ix = 1; ix < _nx - 1; ix++)
|
||
{
|
||
int i = row + ix;
|
||
int c = _band[i];
|
||
if (_band[i - 1] != c || _band[i + 1] != c || _band[i - _nx] != c || _band[i + _nx] != c)
|
||
_edge[i] = 1;
|
||
}
|
||
}
|
||
|
||
for (int p = 0; p < dilate; p++)
|
||
{
|
||
System.Array.Copy(_edge, _tmp, n);
|
||
for (int iz = 1; iz < _nz - 1; iz++)
|
||
{
|
||
int row = iz * _nx;
|
||
for (int ix = 1; ix < _nx - 1; ix++)
|
||
{
|
||
int i = row + ix;
|
||
if (_tmp[i] != 0) continue;
|
||
if (_tmp[i - 1] != 0 || _tmp[i + 1] != 0 || _tmp[i - _nx] != 0 || _tmp[i + _nx] != 0)
|
||
_edge[i] = 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 패딩을 뺀 안쪽에서만 비율을 센다(예산 추정용)
|
||
int cnt = 0, tot = 0;
|
||
for (int iz = pad; iz < _nz - pad; iz++)
|
||
{
|
||
int row = iz * _nx;
|
||
for (int ix = pad; ix < _nx - pad; ix++) { tot++; if (_edge[row + ix] != 0) cnt++; }
|
||
}
|
||
EdgeFraction = tot > 0 ? (float)cnt / tot : 0f;
|
||
}
|
||
|
||
/// <summary>이 점이 색 띠 경계 근처인가. 격자 밖이면 자르지 않는다(안전 쪽).</summary>
|
||
public bool IsEdge(float wx, float wz)
|
||
{
|
||
if (_edge == null) return true;
|
||
int cx = (int)((wx - _minX) * _inv);
|
||
int cz = (int)((wz - _minZ) * _inv);
|
||
if (cx < 0 || cz < 0 || cx >= _nx || cz >= _nz) return true;
|
||
return _edge[cz * _nx + cx] != 0;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// ③-b 816zh — 구름 「높이」 표본 격자 (강도를 걷어 낸 값 · 경계 = cover 를 지나는 선)
|
||
//
|
||
// Cloudiness = (cover + 0.5·noise) × strength. 그림자 얼룩의 「가장자리」는 노이즈가 0 을 지나는 선
|
||
// = 강도를 걷어 낸 값이 cover 와 같아지는 선이다. 🔴 강도(0.5)를 곱한 값에 |c − cover| 를 재면
|
||
// c 가 cover 에 닿지 못해(최대 ≈ 0.43) 띠가 아예 없어진다 — 그래서 판정은 강도를 걷어 낸 값으로 한다.
|
||
// 비싼 6 옥타브 노이즈는 성긴 격자에서만 계산하고(816o 와 같은 원칙) 사이는 겹선형 보간.
|
||
// ─────────────────────────────────────────────────────────────
|
||
float[] _lv;
|
||
int _lx, _lz;
|
||
float _l0x, _l0z, _linv;
|
||
public int LevelSamples; // 노이즈를 실제로 계산한 점 수(비용은 여기서만 난다)
|
||
public float LevelCell;
|
||
|
||
/// <summary>범위의 XZ 평면(높이 y)에 구름 높이를 성긴 격자로 굽는다. 칸 수가 상한을 넘으면 간격을 1.25 배씩 키운다.</summary>
|
||
public void BakeLevels(Bounds b, float y, float sampleCell, int maxSamples)
|
||
{
|
||
LevelSamples = 0;
|
||
if (!Ready) { _lv = null; return; }
|
||
if (sampleCell < 0.05f) sampleCell = 0.05f;
|
||
float w = Mathf.Max(b.size.x, 0.1f), h = Mathf.Max(b.size.z, 0.1f);
|
||
int sx = 2, sz = 2;
|
||
for (int guard = 0; guard < 24; guard++)
|
||
{
|
||
sx = Mathf.CeilToInt(w / sampleCell) + 3; // 한 칸 바깥에서 시작 + 끝 보간 여유
|
||
sz = Mathf.CeilToInt(h / sampleCell) + 3;
|
||
if (maxSamples <= 0 || (long)sx * sz <= maxSamples) break;
|
||
sampleCell *= 1.25f;
|
||
}
|
||
_lx = sx; _lz = sz; _linv = 1f / sampleCell; LevelCell = sampleCell;
|
||
_l0x = b.min.x - sampleCell; _l0z = b.min.z - sampleCell;
|
||
if (_lv == null || _lv.Length < sx * sz) _lv = new float[sx * sz];
|
||
float inv = _strength > 1e-6f ? 1f / _strength : 1f;
|
||
for (int j = 0; j < sz; j++)
|
||
{
|
||
float wz = _l0z + j * sampleCell;
|
||
int row = j * sx;
|
||
for (int i = 0; i < sx; i++) _lv[row + i] = Cloudiness(_l0x + i * sampleCell, y, wz) * inv;
|
||
}
|
||
LevelSamples = sx * sz;
|
||
}
|
||
|
||
/// <summary>표본 격자를 겹선형으로 읽은 구름 높이(= cover + 0.5·noise). 격자 밖은 가장자리 값으로 고정.</summary>
|
||
public float LevelAt(float wx, float wz)
|
||
{
|
||
if (_lv == null || LevelSamples == 0) return _cover;
|
||
float fx = (wx - _l0x) * _linv, fz = (wz - _l0z) * _linv;
|
||
int jx = (int)fx; if (jx < 0) jx = 0; else if (jx > _lx - 2) jx = _lx - 2;
|
||
int jz = (int)fz; if (jz < 0) jz = 0; else if (jz > _lz - 2) jz = _lz - 2;
|
||
float tx = fx - jx; if (tx < 0f) tx = 0f; else if (tx > 1f) tx = 1f;
|
||
float tz = fz - jz; if (tz < 0f) tz = 0f; else if (tz > 1f) tz = 1f;
|
||
int r0 = jz * _lx + jx, r1 = r0 + _lx;
|
||
float a = _lv[r0] + (_lv[r0 + 1] - _lv[r0]) * tx;
|
||
float c = _lv[r1] + (_lv[r1 + 1] - _lv[r1]) * tx;
|
||
return a + (c - a) * tz;
|
||
}
|
||
|
||
/// <summary>띠 가중치 w = smoothstep(clamp01(1 − |높이 − cover| / width)) — 그림자 경계에서 1, 안쪽·바깥쪽은 0.</summary>
|
||
public float BandWeight(float wx, float wz, float width)
|
||
{
|
||
float t = 1f - Mathf.Abs(LevelAt(wx, wz) - _cover) / (width > 1e-4f ? width : 1e-4f);
|
||
if (t <= 0f) return 0f;
|
||
if (t >= 1f) return 1f;
|
||
return t * t * (3f - 2f * t);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 2026-09-16 PD 「데모는 그림자 그라데이션 **경계 지점**에 풀이 보인다 — 지금은 그림자와의 거리만으로 보인다」.
|
||
/// 화면에 실제로 보이는 경계는 ToonRamp 색 띠가 바뀌는 **계단선**(`Band` 가 바뀌는 곳)이다. 노이즈 0선(`BandWeight`)은
|
||
/// 계단 사이 그라데이션의 중심이라 그림자 가장자리에서 떨어진 곳에 풀이 생겼다.
|
||
/// 계단선의 높이 문턱 T_m = (1 − (m − brightness)/shades)/strength (m = 1..top−1 · top 과 0 으로 뭉치는 칸은 경계가 없다)
|
||
/// 까지의 거리로 가중치를 준다: 가장 가까운 계단선 위 1 → width 만큼 떨어지면 0(smoothstep).
|
||
/// </summary>
|
||
public float ContourWeight(float wx, float wz, float width)
|
||
{
|
||
if (_strength <= 1e-6f) return 0f;
|
||
float lvl = LevelAt(wx, wz);
|
||
int top = Mathf.CeilToInt(_shades);
|
||
float best = float.MaxValue;
|
||
for (int m = 1; m < top; m++)
|
||
{
|
||
float T = (1f - (m - _brightness) / _shades) / _strength;
|
||
float d = Mathf.Abs(lvl - T);
|
||
if (d < best) best = d;
|
||
}
|
||
if (best == float.MaxValue) return 0f;
|
||
float t = 1f - best / (width > 1e-4f ? width : 1e-4f);
|
||
if (t <= 0f) return 0f;
|
||
if (t >= 1f) return 1f;
|
||
return t * t * (3f - 2f * t);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// ④ SimplexNoise3D.hlsl `snoise` 이식 (Ashima Arts · MIT · 원본 파일 0줄 수정)
|
||
// ─────────────────────────────────────────────────────────────
|
||
static float Mod289(float x) { return x - Mathf.Floor(x * (1f / 289f)) * 289f; }
|
||
static float Permute(float x) { return Mod289(((x * 34f) + 10f) * x); }
|
||
static float TaylorInvSqrt(float r) { return 1.79284291400159f - 0.85373472095314f * r; }
|
||
|
||
public static float Snoise(float vx, float vy, float vz)
|
||
{
|
||
const float Cx = 1f / 6f, Cy = 1f / 3f;
|
||
|
||
// First corner
|
||
float dv = (vx + vy + vz) * Cy;
|
||
float ix = Mathf.Floor(vx + dv), iy = Mathf.Floor(vy + dv), iz = Mathf.Floor(vz + dv);
|
||
float di = (ix + iy + iz) * Cx;
|
||
float x0x = vx - ix + di, x0y = vy - iy + di, x0z = vz - iz + di;
|
||
|
||
// Other corners — g = step(x0.yzx, x0.xyz)
|
||
float gx = x0x >= x0y ? 1f : 0f;
|
||
float gy = x0y >= x0z ? 1f : 0f;
|
||
float gz = x0z >= x0x ? 1f : 0f;
|
||
float lx = 1f - gx, ly = 1f - gy, lz = 1f - gz;
|
||
float i1x = gx < lz ? gx : lz, i1y = gy < lx ? gy : lx, i1z = gz < ly ? gz : ly;
|
||
float i2x = gx > lz ? gx : lz, i2y = gy > lx ? gy : lx, i2z = gz > ly ? gz : ly;
|
||
|
||
float x1x = x0x - i1x + Cx, x1y = x0y - i1y + Cx, x1z = x0z - i1z + Cx;
|
||
float x2x = x0x - i2x + Cy, x2y = x0y - i2y + Cy, x2z = x0z - i2z + Cy;
|
||
float x3x = x0x - 0.5f, x3y = x0y - 0.5f, x3z = x0z - 0.5f;
|
||
|
||
// Permutations
|
||
ix = Mod289(ix); iy = Mod289(iy); iz = Mod289(iz);
|
||
float p0 = Permute(iz + 0f);
|
||
float p1 = Permute(iz + i1z);
|
||
float p2 = Permute(iz + i2z);
|
||
float p3 = Permute(iz + 1f);
|
||
p0 = Permute(p0 + iy + 0f);
|
||
p1 = Permute(p1 + iy + i1y);
|
||
p2 = Permute(p2 + iy + i2y);
|
||
p3 = Permute(p3 + iy + 1f);
|
||
p0 = Permute(p0 + ix + 0f);
|
||
p1 = Permute(p1 + ix + i1x);
|
||
p2 = Permute(p2 + ix + i2x);
|
||
p3 = Permute(p3 + ix + 1f);
|
||
|
||
// Gradients — ns = n_ * D.wyz − D.xzx, D = (0, 0.5, 1, 2)
|
||
const float n_ = 0.142857142857f;
|
||
const float nsx = n_ * 2f, nsy = n_ * 0.5f - 1f, nsz = n_;
|
||
const float nszz = nsz * nsz;
|
||
|
||
float j0 = p0 - 49f * Mathf.Floor(p0 * nszz);
|
||
float j1 = p1 - 49f * Mathf.Floor(p1 * nszz);
|
||
float j2 = p2 - 49f * Mathf.Floor(p2 * nszz);
|
||
float j3 = p3 - 49f * Mathf.Floor(p3 * nszz);
|
||
|
||
float xa = Mathf.Floor(j0 * nsz), xb = Mathf.Floor(j1 * nsz), xc = Mathf.Floor(j2 * nsz), xd = Mathf.Floor(j3 * nsz);
|
||
float ya = Mathf.Floor(j0 - 7f * xa), yb = Mathf.Floor(j1 - 7f * xb), yc = Mathf.Floor(j2 - 7f * xc), yd = Mathf.Floor(j3 - 7f * xd);
|
||
|
||
float X0 = xa * nsx + nsy, X1 = xb * nsx + nsy, X2 = xc * nsx + nsy, X3 = xd * nsx + nsy;
|
||
float Y0 = ya * nsx + nsy, Y1 = yb * nsx + nsy, Y2 = yc * nsx + nsy, Y3 = yd * nsx + nsy;
|
||
float h0 = 1f - Mathf.Abs(X0) - Mathf.Abs(Y0);
|
||
float h1 = 1f - Mathf.Abs(X1) - Mathf.Abs(Y1);
|
||
float h2 = 1f - Mathf.Abs(X2) - Mathf.Abs(Y2);
|
||
float h3 = 1f - Mathf.Abs(X3) - Mathf.Abs(Y3);
|
||
|
||
// b0 = (X0, X1, Y0, Y1) · s0 = floor(b0) * 2 + 1 · sh = −step(h, 0)
|
||
float sX0 = Mathf.Floor(X0) * 2f + 1f, sX1 = Mathf.Floor(X1) * 2f + 1f;
|
||
float sY0 = Mathf.Floor(Y0) * 2f + 1f, sY1 = Mathf.Floor(Y1) * 2f + 1f;
|
||
float sX2 = Mathf.Floor(X2) * 2f + 1f, sX3 = Mathf.Floor(X3) * 2f + 1f;
|
||
float sY2 = Mathf.Floor(Y2) * 2f + 1f, sY3 = Mathf.Floor(Y3) * 2f + 1f;
|
||
float sh0 = h0 <= 0f ? -1f : 0f, sh1 = h1 <= 0f ? -1f : 0f;
|
||
float sh2 = h2 <= 0f ? -1f : 0f, sh3 = h3 <= 0f ? -1f : 0f;
|
||
|
||
// a0 = b0.xzyw + s0.xzyw * sh.xxyy → (X0, Y0, X1, Y1) + (sX0, sY0, sX1, sY1) * (sh0, sh0, sh1, sh1)
|
||
float g0x = X0 + sX0 * sh0, g0y = Y0 + sY0 * sh0, g0z = h0;
|
||
float g1x = X1 + sX1 * sh1, g1y = Y1 + sY1 * sh1, g1z = h1;
|
||
float g2x = X2 + sX2 * sh2, g2y = Y2 + sY2 * sh2, g2z = h2;
|
||
float g3x = X3 + sX3 * sh3, g3y = Y3 + sY3 * sh3, g3z = h3;
|
||
|
||
float n0 = TaylorInvSqrt(g0x * g0x + g0y * g0y + g0z * g0z);
|
||
float n1 = TaylorInvSqrt(g1x * g1x + g1y * g1y + g1z * g1z);
|
||
float n2 = TaylorInvSqrt(g2x * g2x + g2y * g2y + g2z * g2z);
|
||
float n3 = TaylorInvSqrt(g3x * g3x + g3y * g3y + g3z * g3z);
|
||
g0x *= n0; g0y *= n0; g0z *= n0;
|
||
g1x *= n1; g1y *= n1; g1z *= n1;
|
||
g2x *= n2; g2y *= n2; g2z *= n2;
|
||
g3x *= n3; g3y *= n3; g3z *= n3;
|
||
|
||
float m0 = 0.5f - (x0x * x0x + x0y * x0y + x0z * x0z); if (m0 < 0f) m0 = 0f;
|
||
float m1 = 0.5f - (x1x * x1x + x1y * x1y + x1z * x1z); if (m1 < 0f) m1 = 0f;
|
||
float m2 = 0.5f - (x2x * x2x + x2y * x2y + x2z * x2z); if (m2 < 0f) m2 = 0f;
|
||
float m3 = 0.5f - (x3x * x3x + x3y * x3y + x3z * x3z); if (m3 < 0f) m3 = 0f;
|
||
m0 *= m0; m1 *= m1; m2 *= m2; m3 *= m3;
|
||
m0 *= m0; m1 *= m1; m2 *= m2; m3 *= m3;
|
||
|
||
return 105f * (m0 * (g0x * x0x + g0y * x0y + g0z * x0z)
|
||
+ m1 * (g1x * x1x + g1y * x1y + g1z * x1z)
|
||
+ m2 * (g2x * x2x + g2y * x2y + g2z * x2z)
|
||
+ m3 * (g3x * x3x + g3y * x3y + g3z * x3z));
|
||
}
|
||
}
|
||
}
|