// ───────────────────────────────────────────────────────────────────────────── // 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 { /// /// 구름 그림자의 「색 띠 경계」를 XZ 격자에 한 번 구워 두고, 점 하나가 경계 근처인지 O(1) 로 답한다. /// 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; // 격자에서 경계로 잡힌 칸의 비율(예산 추정에 쓴다) /// 구름 값이 1초에 월드 몇 m 를 흐르는가 — 갱신 주기 ↔ 띠 폭을 맞추는 데 쓴다. 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(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; } /// 시간만 다시 읽는다(주기 갱신용 — 머티리얼·태양은 그대로). public void Tick() { ResolveSun(); ResolveTime(); } /// 시간만 다시 읽는다(816zh 띠 갱신용 — 태양·머티리얼은 그대로 · FindObjectsByType 0회). public void TickTime() { ResolveTime(); } /// 머티리얼 `_Cloud_Cover` — 816zh 띠의 중심선(구름 높이가 이 값을 지나는 곳 = 그림자 얼룩의 가장자리). public float Cover { get { return _cover; } } /// 셰이더가 쓰는 `_Time.y`. 진단·검증에서 직접 넣어 볼 수 있다. 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"); } // ───────────────────────────────────────────────────────────── // ② 셰이더와 같은 식 // ───────────────────────────────────────────────────────────── /// CloudShadows 서브그래프의 최종 출력 `Cloudiness`. 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; } /// ToonRamp 가 만드는 색 띠 번호. 이 값이 바뀌는 곳이 화면에서 보이는 경계다. public int Band(float wx, float wy, float wz) { return BandOf(Cloudiness(wx, wy, wz)); } /// 구름 값 하나를 띠 번호로. (보간한 값에도 쓴다) 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; } // ───────────────────────────────────────────────────────────── // ③ 격자에 굽기 // ───────────────────────────────────────────────────────────── /// /// 주어진 범위의 XZ 평면(높이 y)에 대해 띠 번호를 구우고, 번호가 바뀌는 칸을 경계로 표시한다. /// /// 격자 칸(m). 작을수록 띠가 얇고 정확하다. /// 칸 수 상한 — 섬이 커지면 칸을 자동으로 키운다(모바일 예산). /// 경계에서 몇 칸 더 넓힐 것인가(구름이 흐르는 만큼 미리 덮어 둔다). 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; } /// 이 점이 색 띠 경계 근처인가. 격자 밖이면 자르지 않는다(안전 쪽). 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; /// 범위의 XZ 평면(높이 y)에 구름 높이를 성긴 격자로 굽는다. 칸 수가 상한을 넘으면 간격을 1.25 배씩 키운다. 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; } /// 표본 격자를 겹선형으로 읽은 구름 높이(= cover + 0.5·noise). 격자 밖은 가장자리 값으로 고정. 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; } /// 띠 가중치 w = smoothstep(clamp01(1 − |높이 − cover| / width)) — 그림자 경계에서 1, 안쪽·바깥쪽은 0. 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); } /// /// 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). /// 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)); } } }