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

497 lines
28 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// 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);
}
// ─────────────────────────────────────────────────────────────
// ④ 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));
}
}
}