Project_WL/Assets/WL/Look/Farm/WLCloudShadow.cs

523 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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..top1 · 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));
}
}
}