diff --git a/docs/research/2026-10-14-curl-flow-probe/README.md b/docs/research/2026-10-14-curl-flow-probe/README.md index e53a4ef60..d18fe3b35 100644 --- a/docs/research/2026-10-14-curl-flow-probe/README.md +++ b/docs/research/2026-10-14-curl-flow-probe/README.md @@ -28,6 +28,9 @@ the speeds below compare shape at equal energy. | 3 | `3-curl-warped` | `fbm_grad` with 2 octaves at a warped position `q = p + .55 * (n1, n2)`. n1 and n2 are two offset `snoise_grad` at frequency .6. The chain rule through the warp's Jacobian is exact, so the flow stays divergence-free. k = .76. | | 4 | `4-curl-fbm3-slope` | #2 plus `FlowField.Slope` with swirl 6 and slide 1.5 (B25g), as the flow-slope-terms ruling turns it on. | | 4b | `4b-curl-warped-slope` | #3 plus the same slope terms. | +| 5 | `5-lab-stretch-fixed` | The Nebula Span Lab's construction as a stream function (see below and `labs-noise-facts.md`): `fbm_grad` (3 octaves, lacunarity 2, gain .5) of the stretched argument `Mop(v) = v + (1/5 - 1) dot(v, d) d`, with d fixed at azimuth 25 (the lab's "Streaming filaments" preset). The flow is exact analytically, since Mop is symmetric. k = 1.09. | +| 5b | `5b-lab-stretch-contour` | #5 with d set to the local contour tangent (the swirl direction around wells, bilinear on the gravity grid). The analytic chain rule would need dd/dp, which is the height Hessian, so psi is differenced instead (4 evaluations). k = 1.25. | +| 5c | `5c-lab-stretch-contour-slope` | #5b plus the slope terms. | Time is the noise's third axis: `tau = fmod(.025 * zoneTime, 867)`, folded in double as `FlowField.Scroll` folds `frac`. This works because `snoise(float3)` has a period of exactly 867 along each axis: a shift of 867 @@ -48,6 +51,9 @@ central differences with a 1 u step. "Still" is the fraction of points with spee | 3 | 9.1e-4 | .585 | 1.5e-3 | 0.8 / 2.7 / 8.9 / 25.3 / **125.7** | 8.2 % | 4 | | 4 | .175 | .334 | .52 | 3.7 / 7.3 / 12.9 / 21.4 / 34.7 | 0.4 % | 3 + slope term | | 4b | .152 | .634 | .24 | 4.2 / 6.7 / 11.2 / 26.5 / 107.7 | 0.4 % | 4 + slope term | +| 5 | 5.6e-5 | .273 | 2.0e-4 | 0.9 / 3.2 / 10.7 / 22.9 / 36.1 | 6.4 % | 3 | +| 5b | 1.98 | 3.33 | .59 | 0.6 / 1.9 / 6.1 / 22.8 / **174.8** | 10.4 % | 12 as probed; 3 plus a height Hessian analytically | +| 5c | 2.02 | 3.35 | .60 | 4.3 / 6.4 / 9.6 / 24.0 / 212.0 | 0.2 % | as 5b plus the slope term | The curl candidates' divergence is finite-difference error. Tri3D's is real: today's flow is more compressive than rotational (1.35). That compression is why its long-exposure drift collapses into kinked convergence lines. @@ -65,6 +71,8 @@ The table gives the correlation between the velocity field at t = 0 and at t, on | 1 | .57 | -.09 | .09 | | 2 | .29 | -.04 | .12 | | 3 | .10 | -.01 | .01 | +| 5 | .22 | -.03 | .15 | +| 5b | .31 | -.01 | -.03 | Tri3D is quasi-periodic: octave periods of 40 s and 24.7 s make the field recur, with a correlation of .8 at 120 s. The curl fields evolve without recurring for `867 / .025 = 34,680 s` (9.6 h). The warped field turns over @@ -76,7 +84,7 @@ steps). The fold is required, as it is today. ## Images (option order) -- `lic_contact_sheet.png`: every LIC side by side, in the order 0, 1, 2, 3, 4, 4b. +- `lic_contact_sheet.png`: every LIC side by side, in the order 0, 1, 2, 3, 4, 4b, 5, 5b, 5c. - `lic_.png`: a line-integral convolution of the flow over the whole zone (512 px, t = 0). Brightness and colour rise with speed relative to Tri3D's RMS. Red rings mark the wells. - `drift_.png`, left panel: the Stardust law for one cycle (Period 2 s from `ARPG.unity`; @@ -96,6 +104,31 @@ In the one-cycle Stardust panels, every candidate looks like short straight dash back-projects linearly at the spawn point over 2 s (up to 30 u). The noise's prettiness shows on screen through the dashes' direction coherence and through the nebula's flow-advected noise, not through particle paths. +## The Nebula Span Lab candidate (5, 5b, 5c) + +Self's input: the operator said "The last nebula playground I saw had some pretty sophisticated noise". +`labs-noise-facts.md` holds the Eyes extraction of that page, copied here unchanged. Its noise is a density warp, +not a flow. The page builds a two-level warp chain of fbm, ridged `1 - 2|n|` or curl fields over CultMath +simplex, with up to 4 octaves and a stretch along one fixed direction. It moves only by adding a constant offset +to the noise argument. Candidates 5 to 5c turn the lab's stretched fbm into a stream function and take its curl, +so the result is a velocity field. It scrolls on the same folded tau as the other curl candidates. + +- **5, fixed axis**: long parallel jets, all leaning along azimuth 25. It reads as a prevailing wind across the + whole zone. It is exactly divergence-free and costs the same as #2 (3 `snoise_grad`). A fixed axis is a global + heading the zone has no physical reason to own. +- **5b and 5c, axis following the slope: the probe found this does not work as built.** The lab's stretch is a + global linear map of the position (`dot(v, d) d`). With a varying d, the argument changes by about + `|p| * delta(d)`, so it shears fastest far from the origin and where d turns quickly. At the well floors and + saddles the contour tangent is undefined or flips sign, and the bilinear axis passes through zero there. The + LIC shows dipole fans on every well and seam lines through the saddles. The numbers show it: a max speed 28x + RMS, and a measured div/vort of .59. The flow is a discrete curl, but its structure is finer than the + measurement step. Following the slope coherently needs the stretch in a local frame rather than in world + position. One route is to sample the noise in coordinates of the gravity potential, such as height and arc + along the contour. The other is to build the anisotropy from the height Hessian. Neither is probed here. +- **Following the Skein instead**: this is the same construction with d set to the nearest strand's tangent. It + hits the same fault (the stretch is global in position, and the axis is undefined at strand junctions and + between strands), so it needs the same local-frame fix. It is not rendered. + ## CultMath gaps, by name - **No curl node.** A 2D curl is a swizzle of `snoise_grad`/`fbm_grad` (`(g.y, -g.x)` of the xz-plane @@ -109,6 +142,15 @@ the dashes' direction coherence and through the nebula's flow-advected noise, no - **No 4D simplex** (`snoise(float4)`/`snoise_grad(float4)`). A true 3D curl (Bridson's vector potential: three potentials, so 3 `snoise_grad` per octave) that also evolves in time needs a fourth axis. Without one, a 3D flow must scroll by translating instead of evolving. This matters only if the flow keeps a y component. +- **No Hessian of simplex.** The lab generates `cultmath_snoise_gh` locally by splicing CultMath's gradient + kernel (`makeGH`). CultMath has only the bound constant `CULTMATH_SNOISE_HESSIAN`. A slope-following stretch + done analytically also needs the height field's Hessian, which is a separate gap on the Aetheria side. +- **Lab-local pieces that CultMath lacks** (from `labs-noise-facts.md` section 2): the curl mode (`dispVec` + mode 2, `curlK`, a 3-component vector-potential curl); the ridged form `1 - 2|n|` with per-octave offset + (CultMath's `ridged_grad` is `1 - |n|`); the stretch operator `Mop` with per-component offsets + (`fieldComp`); and the warp chain (`fieldS`, `dispVec`, `dispY`, `brushPoint`, `ampG`). The facts also record + a constant mismatch: the lab pins `SN_L = 10.099261` and calls it CultMath's, but CultMath's + `SNOISE_LIPSCHITZ` is now 7.9640074. - **No period constant.** The 867 period is a fact of `snoise(float3)` that nothing in CultMath states or pins. A named `snoise_period` and a test pinning it would let FlowField fold tau in double without relying on this probe. diff --git a/docs/research/2026-10-14-curl-flow-probe/curl_probe.py b/docs/research/2026-10-14-curl-flow-probe/curl_probe.py index d5b94b95e..ee9d46859 100644 --- a/docs/research/2026-10-14-curl-flow-probe/curl_probe.py +++ b/docs/research/2026-10-14-curl-flow-probe/curl_probe.py @@ -118,6 +118,24 @@ def stream_grad(kind, x, z, t): J11 = 1 + W * fw * n1[..., 0]; J12 = W * fw * n1[..., 1] J21 = W * fw * n2[..., 0]; J22 = 1 + W * fw * n2[..., 1] return g[..., 0] * J11 + g[..., 1] * J21, g[..., 0] * J12 + g[..., 1] * J22 + if kind in ("lab", "labslope"): + # Nebula Span Lab construction as a stream function (labs-noise-facts.md 1a): fbm of the stretched argument + # Mop(v) = v + (1/st - 1) dot(v, d) d (SPAN 396), st 5, 3 octaves, gain .5 (lab defaults; lacunarity 2, not + # the preset's 2.1, so the 867 fold holds). + st = 5.0; a = 1.0 / st - 1.0 + def psi(xx, zz, dx_, dz_): + px, pz = xx / S, zz / S; dp = px * dx_ + pz * dz_ + return fbm_grad(np.stack([px + a * dp * dx_, pz + a * dp * dz_, np.full_like(xx, tau)], -1), 3, 2.0, 0.5) + if kind == "lab": # d fixed at azimuth 25 (the preset's fAz); analytic: Mop is symmetric, grad_p = Mop grad_q + dx_ = np.full_like(x, LAB_DIR[0]); dz_ = np.full_like(x, LAB_DIR[1]); g = psi(x, z, dx_, dz_) + gd = g[..., 0] * dx_ + g[..., 1] * dz_ + return g[..., 0] + a * gd * dx_, g[..., 1] + a * gd * dz_ + # "labslope": d = the local contour tangent (bilinear on the gravity grid). The analytic chain rule would need + # dd/dp, i.e. the height Hessian; instead psi is differenced (4 evaluations, 12 snoise_grad), which keeps the + # flow an exact discrete curl. + e = 2.0 + def ps(xx, zz): d = DIRFN(xx, zz); return psi(xx, zz, d[0], d[1])[..., 3] + return ((ps(x + e, z) - ps(x - e, z)) / (2 * e) * S, (ps(x, z + e) - ps(x, z - e)) / (2 * e) * S) raise KeyError(kind) GAIN = {} # per-candidate k, set so RMS speed matches tri3d's RMS over the zone at t = 0 @@ -132,6 +150,9 @@ def slope_terms(x, z, t): ox[k] = dz * SWIRL + dx * SLIDE; oz[k] = -dx * SWIRL + dz * SLIDE return ox.reshape(x.shape), oz.reshape(x.shape) +DIRFN = None # set in main: (x, z) -> unit contour tangent (the swirl direction), nearest cell of the grid +LAB_DIR = (math.cos(math.radians(25)), math.sin(math.radians(25))) # Span Lab "Streaming filaments" fAz 25 + def velocity(kind, x, z, t, slope=None): if kind == "tri3d": return tri3d(x, z, t) base = kind.replace("+slope", "") @@ -143,13 +164,16 @@ def velocity(kind, x, z, t, slope=None): ux, uz = ux + sx, uz + sz return ux, uz -CANDS = ["tri3d", "curl1", "curlfbm", "warp", "curlfbm+slope", "warp+slope"] +CANDS = ["tri3d", "curl1", "curlfbm", "warp", "curlfbm+slope", "warp+slope", "lab", "labslope", "labslope+slope"] NAMES = {"tri3d": "0-tri3d", "curl1": "1-curl-simplex", "curlfbm": "2-curl-fbm3", - "warp": "3-curl-warped", "curlfbm+slope": "4-curl-fbm3-slope", "warp+slope": "4b-curl-warped-slope"} + "warp": "3-curl-warped", "curlfbm+slope": "4-curl-fbm3-slope", "warp+slope": "4b-curl-warped-slope", + "lab": "5-lab-stretch-fixed", "labslope": "5b-lab-stretch-contour", "labslope+slope": "5c-lab-stretch-contour-slope"} COST = {"tri3d": "0 snoise_grad (2 normalize(tri3) + cross)", "curl1": "1 snoise_grad", "curlfbm": "3 snoise_grad (fbm_grad, 3 octaves)", "warp": "4 snoise_grad (2 warp + fbm_grad 2 octaves)", "curlfbm+slope": "3 snoise_grad + the slope term (gravity gradient: 1 texel gradient in the shader)", - "warp+slope": "4 snoise_grad + the slope term"} + "warp+slope": "4 snoise_grad + the slope term", "lab": "3 snoise_grad (fbm_grad of a stretched argument)", + "labslope": "12 snoise_grad as probed (psi differenced); 3 + a height Hessian analytically", + "labslope+slope": "as 5b + the slope term (axis and slope term share the gravity gradient)"} # ---------------- images ---------------- STOPS = np.array([[0, 10, 14, 30], [.45, 20, 120, 130], [.8, 200, 210, 120], [1, 255, 245, 210]], float) @@ -272,11 +296,21 @@ def main(): print("tri3d numpy vs habitat_probe max err", float(np.abs(np.stack(a, -1) - b).max())) c = (np.arange(N) + .5) / N * 2 * R - R; X, Z = np.meshgrid(c, c) tx, tz = tri3d(X, Z, t0); vref = float(np.sqrt((tx ** 2 + tz ** 2).mean())) - for kind in ("curl1", "curlfbm", "warp"): - gx, gz = stream_grad(kind, X, Z, t0); GAIN[kind] = vref / float(np.sqrt((gx ** 2 + gz ** 2).mean())) - print("tri3d RMS speed", vref, "gains k", GAIN) + def dir_fn(x, z): # bilinear, renormalised: a continuous axis field + fi = np.clip((x + R) / (2 * R) * N - .5, 0, N - 1.001); fj = np.clip((z + R) / (2 * R) * N - .5, 0, N - 1.001) + i = fi.astype(int); j = fj.astype(int); u = fi - i; v = fj - j + bl = lambda A: (A[j, i] * (1 - u) + A[j, i + 1] * u) * (1 - v) + (A[j + 1, i] * (1 - u) + A[j + 1, i + 1] * u) * v + tx, tz = bl(TX), bl(TZ); l = np.hypot(tx, tz) + 1e-12 + return tx / l, tz / l + global DIRFN; DIRFN = dir_fn print("slope grid ...", flush=True) SX, SZ = slope_terms(X, Z, t0) + # contour tangent from the slope terms: S = SWIRL t + SLIDE d with t = (d.z, -d.x), so t = (SWIRL S + SLIDE rot) / |..| + TX = (SWIRL * SX - SLIDE * (-SZ)) ; TZ = (SWIRL * SZ - SLIDE * SX) + TL = np.hypot(TX, TZ) + 1e-12; TX, TZ = TX / TL, TZ / TL + for kind in ("curl1", "curlfbm", "warp", "lab", "labslope"): + gx, gz = stream_grad(kind, X, Z, t0); GAIN[kind] = vref / float(np.sqrt((gx ** 2 + gz ** 2).mean())) + print("tri3d RMS speed", vref, "gains k", GAIN) # slope sampler for scattered points: nearest cell of the grid (the shader samples a gravity texture too) def slope_fn(x, z): i = np.clip(((x + R) / (2 * R) * N).astype(int), 0, N - 1); j = np.clip(((z + R) / (2 * R) * N).astype(int), 0, N - 1) @@ -288,7 +322,7 @@ def main(): nums = numbers(kind, t0, slope_fn) print(f"== {name}: cost {COST[kind]}\n {nums}\n {p1}\n {p2}", flush=True) # scroll: how far the field decorrelates over one minute of zone time, per candidate - for kind in CANDS[:4]: + for kind in CANDS[:4] + ["lab", "labslope"]: u0 = np.stack(velocity(kind, X[::8, ::8], Z[::8, ::8], 0.0), -1).reshape(-1, 2) for dt in (10.0, 40.0, 120.0): u1 = np.stack(velocity(kind, X[::8, ::8], Z[::8, ::8], dt), -1).reshape(-1, 2) diff --git a/docs/research/2026-10-14-curl-flow-probe/drift_5-lab-stretch-fixed.png b/docs/research/2026-10-14-curl-flow-probe/drift_5-lab-stretch-fixed.png new file mode 100644 index 000000000..39234ccd3 --- /dev/null +++ b/docs/research/2026-10-14-curl-flow-probe/drift_5-lab-stretch-fixed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff20d599fae5752e50010ab196b27b2dd73a9d6010a23b91074d26c83b426e08 +size 107264 diff --git a/docs/research/2026-10-14-curl-flow-probe/drift_5b-lab-stretch-contour.png b/docs/research/2026-10-14-curl-flow-probe/drift_5b-lab-stretch-contour.png new file mode 100644 index 000000000..bb97b64d7 --- /dev/null +++ b/docs/research/2026-10-14-curl-flow-probe/drift_5b-lab-stretch-contour.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:898e3011e1b68342493630a8c9f2a4a6e79da8d673feb4e1ebbe2b92a93cc5cb +size 120833 diff --git a/docs/research/2026-10-14-curl-flow-probe/drift_5c-lab-stretch-contour-slope.png b/docs/research/2026-10-14-curl-flow-probe/drift_5c-lab-stretch-contour-slope.png new file mode 100644 index 000000000..2d4ace992 --- /dev/null +++ b/docs/research/2026-10-14-curl-flow-probe/drift_5c-lab-stretch-contour-slope.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dddc37fb0ab89c05eced6ae67b8e37a59c767ba0d91d7ca80fc67e52df532317 +size 139894 diff --git a/docs/research/2026-10-14-curl-flow-probe/labs-noise-facts.md b/docs/research/2026-10-14-curl-flow-probe/labs-noise-facts.md new file mode 100644 index 000000000..eb57b2789 --- /dev/null +++ b/docs/research/2026-10-14-curl-flow-probe/labs-noise-facts.md @@ -0,0 +1,136 @@ +# Nebula labs: noise facts (eyes-nebula-labs, 2026-10-14) + +Sources +- SPAN = "Nebula Span Lab" (artifact be6a9e31, v1791074878-d07e). Saved HTML: C:\Users\Meta\.claude\projects\F--Projects-CultLib\5ea0b87f-66d0-4b24-8d36-45015e4cedc1\tool-results\artifact-be6a9e31-1791074878-d07e.html (1166 lines). Line numbers below are that file's. +- KERN = "Kernel Nebula Lab" (artifact c09d2762, v1791066352-e6f2). Returned inline, no saved file; pointers are by function/constant name (page is ~560 lines). +- Caveat: the SPAN tool output was truncated near HTML line 830. I read lines 940-1000 of the saved file and grepped the rest. I did NOT read every line of SPAN (bench/audit/test JS harness at ~840-940 and ~1000-1166 only grepped). + +## 1. Noise constructions + +### Common finding +Neither lab uses fbm of value noise, Worley, Phacelle, or hash noise. The ONLY noise primitive in SPAN is CultMath's 3D simplex `cultmath_snoise` (Ashima/McEwan, SPAN 232-286) and its analytic gradient `cultmath_snoise_grad` (SPAN 293-352), embedded verbatim in a JS template string `CULTMATH` (SPAN 232). KERN uses NO noise function at all: its density is a procedurally generated cascade of anisotropic compact-support kernels (1b). + +### 1a. SPAN: density = ramp pushed down by brushes, domain-warped twice +Density is not a noise sum; `fieldS` (SPAN 434-441) returns a height-like scalar s and density = ext * clamp((F - s)/B, 0, cap): +```glsl +float fieldS(vec3 p){ + float H; vec2 g; brushes(brushPoint(p), H, g); + float gm = ampG(H); + vec3 q1 = p + vec3(0.0, H, 0.0); + vec3 q2 = q1 + (gm*uWA[0])*dispVec(0, q1); + return q2.y + (gm*uWA[1])*dispY(1, q2); +} +``` +(reference march: `rho = uExt*clamp((uF - s)/uB, 0.0, uCap)` in traceRef.) Brush bowl H shifts q1.y; warp 1 displaces q1 by a 3-vector field (amplitude a1); warp 2 displaces q2 but only its Y component reaches s (amplitude a2). Two warp levels, hard-coded. Warp amplitude is modulated by the bowl: `ampG(H)` = ampRim + (ampDeep-ampRim)*H/Hsum (SPAN 391). + +Base noise: `cultmath_snoise_grad(...)` returns vec4(grad, value) (SPAN 293). A Hessian variant `cultmath_snoise_gh` is page-generated by `makeGH(src)` (SPAN 838-845) by string-splicing the CultMath grad kernel. + +Fractal sum per component, `fieldComp` (SPAN 398-409): plain or ridged, per-octave offset OCT, lacunarity uWLac[st], gain uWGain[st]: +```glsl +float fieldComp(int st, int c, vec3 q, bool ridge, out vec3 g){ + vec3 base = Mop(st, q) + uWSh[st]; float lac = 1.0, gn = 1.0, sum = 0.0; g = vec3(0.0); + for (int o = 0; o < uWOct[st]; o++){ + vec4 t = cultmath_snoise_grad(lac*base + CO[st*3 + c] + float(o)*OCT); + float fac = 1.0, val = t.w; + if (ridge){ fac = t.w >= 0.0 ? -2.0 : 2.0; val = 1.0 - 2.0*abs(t.w); } + sum += gn*val; g += (gn*fac*lac)*Mop(st, t.xyz); + lac *= uWLac[st]; gn *= uWGain[st]; + } + return sum; +} +``` +Ridged form here is `1 - 2|n|` (range -1..1), not CultMath's `1 - |n|`. Displacement components x/y/z use different offsets from `CO[6] = {O0,O1,O2,O4,O3,O5}` (SPAN 376-380; OCT = (17.3,-9.1,23.9)). Anisotropic stretch (SPAN 396): +`vec3 Mop(int st, vec3 v){ return uWF[st]*(v + (1.0/uWStr[st] - 1.0)*dot(v, uDir)*uDir); }` - frequency F times a stretch along the fixed flow axis uDir. + +Three noise modes per warp stage (uWMode: 0 plain fbm, 1 ridged, 2 curl), `dispVec` (SPAN 411-417): +```glsl +vec3 dispVec(int st, vec3 q){ + int md = uWMode[st], nc = uWOct[st] > 0 ? 3 : 0; + vec3 w = vec3(0.0), gs[3]; + for (int c = 0; c < nc; c++){ vec3 g; float v = fieldComp(st, c, q, md == 1, g); w[c] = v; gs[c] = g; } + if (md == 2) w = curlK(st)*vec3(gs[2].y - gs[1].z, gs[0].z - gs[2].x, gs[1].x - gs[0].y); + return w; +} +``` +Curl: curl of a 3-component vector potential (three fbm fields at three offsets) from analytic gradients, scaled `curlK(st) = 0.25/uWF[st]` (SPAN 410). Divergence-free. `dispY` (SPAN 418-427) is warp 2's Y-only version (curl: `curlK*(gx.z - gz.x)`). + +Brush warp, `brushPoint` (SPAN 427-433): the xz position fed to the brushes is displaced by two snoise values: `pb[j] += uBwA*cultmath_snoise_grad(xb + (j == 0 ? O6 : O7)).w`, `xb = vec3(p.x,p.z,0)*uBwF + uBwSh` (ragged bowl edges). + +Brushes (SPAN 383-390): H = sum D_i (1-r^2)^e_i, comment "Aetheria PowerBrush powerPulse"; three brushes from JS `brushList()` (not read line by line). + +Time evolution (JS `setUniforms`, SPAN 976-978): pure domain scroll of the noise argument by constant vectors: +```js +const sp = P.speed*0.03; +gl.uniform3fv(U.uWSh, new Float32Array([simT*sp*0.9, simT*sp*0.35, -simT*sp*0.6, -simT*sp*0.5, simT*sp*0.8, simT*sp*0.4])); +u3("uBwSh", [simT*sp*0.4, -simT*sp*0.7, 0.3]); +``` +`uWSh[st]` is added to the noise argument (`base = Mop(st,q) + uWSh[st]`). `anim` toggle default off (SPAN 118); `simT += dt` when on (SPAN 1037). No crossfade, no velocity field, no advection. + +Composition: s = brush bowl (itself warped by 2 snoise in xz) -> + a1*gm*W1(q1) (vec3 plain/ridged/curl fbm, <=4 octaves, anisotropic) -> + a2*gm*W2y(q2) -> density = ext*clamp((F-s)/B,0,cap) -> single-scatter with HG phase and analytic sun optical depth. Most of the page is the span integrator (`compField` SPAN 448-483, `evalSpan` 484-531, curvature bounds); the noise definition itself is ~50 lines (394-441). Pinned constants `SN_H=56.050385, SN_L=10.099261, SN_T=540.0` (SPAN ~374). + +### 1b. KERN: kernel cascade, no noise function +Density is a sum of compact-support kernels rho = peak * w * (1-u)^k, u = |A^-1 (x - mu)|^2 (`pointDens`, shader), each an anisotropic ellipsoid in a multiplicative cascade built in JS `build(p)`: +- roots on a jittered lattice (spacing H0), hashed per cell (`hash32`, `rngSeed`), kept by the fog envelope `Pm(s)=(F-s)/gradB` times radial edge fade `ef`; +- root axes: e1 = flow tangent rotated by random phi, e2 = n x e1, c2 = surface normal * flat; alpha = asp*(0.8+0.4r); +- children: up to `nc` per node, offsets from a 256-entry random table TXi, linear map table TL (rotation about random axis by rot*S(), diag scale cs*{alc,1,1/alc}), mass share q = u^qp * Pc/Pp (intermittency); parent keeps `keep`; `lvl` <= 6; budgets KBUDGET 420000 kernels, IDXCAP 4096*3072 bin entries; +- kernel orders `kr` (roots), `kc` (children) in (1-u)^k; closed-form line integral `segInt(k,R,al,s0,s1)`; mass normaliser `kMass(k)`; +- LOD: `uLod*foot` culls kernels below pixel footprint and swaps child peak (`t2.w` vs `t3.w`). +KERN flow (`flowAt`, in build): `fx = -z/r*1.0 + gx*30 + 0.5*sin(z*0.011+1.3)`, `fz = x/r*1.0 + gz*30 + 0.5*cos(x*0.013+0.7)`, normalised, scaled by `sp = 0.6+0.4*sin(x*0.011+z*0.007)`; gx,gz = bowl-height gradient (central diff e=3). Stored per root (cr[24], cr[25]), passed to the shader as t4.x, t4.y. +KERN time (`draw()` + shader): two-phase crossfade, PERIOD=8: `ph0=(simT/PERIOD)%1, ph1=(simT/PERIOD+0.5)%1, w0 = 1-|2*ph0-1|`; each kernel evaluated twice with weights uW=(w0,1-w0), centre `mu = t0.xyz + vec3(t4.x,0,t4.y)*(uWarp*sh)`, sh=(ph-0.5)*2; `uWarp` = "Flow warp distance". Same two-copy cycle structure as the game shader (section 4), with a per-root baked, time-constant 2D flow vector. +Tint: per-kernel hash `t4.w` plus depth `(uF-s)/uTintDepth`. + +## 2. Which exist in CultMath (F:\Projects\CultLib\packages\cultmath) + +| Lab construct | In CultMath? | Where | +|---|---|---| +| `cultmath_snoise(vec3)` | yes | shaders/CultMath.glsl:85 (3D), :134 (2D); C# `math.snoise(float3)` src/CultMath/math.cs:608, float2 :661; HLSL CultMath.hlsl | +| `cultmath_snoise_grad` -> vec4(grad,val) | yes | CultMath.glsl:171; C# math.cs:714 | +| fbm with grad | yes `cultmath_fbm_grad(p,octaves,lacunarity,gain)` | CultMath.glsl:233; C# math.cs:784 | +| ridged | yes `cultmath_ridged_grad`, sum amp*(1-abs(n)) | CultMath.glsl:254; C# math.cs:811. Differs from lab: lab uses 1-2|n|, per-octave offset OCT, per-component offsets, and does not call CultMath's fbm_grad/ridged_grad | +| interval / affine snoise, fbm | yes | cultmath_iv_snoise_ball :595, cultmath_iv_fbm_ball :611, cultmath_af_snoise :721, cultmath_af_fbm :738 (C# math.Interval.cs:146,176; math.Affine.cs:159,183) | +| value noise | yes (unused by labs) | cultmath_value_noise :778, _bicubic :792 | +| hash | yes (unused) | cultmath_hash :392-394 | +| Phacelle | yes (unused) | `CultPhasor cultmath_phacelle(vec3 p, vec3 side, float offset, float normalization)` CultMath.Phacelle.glsl:25; math.Phacelle.cs | +| Cellular/Voronoi | src/CultMath/CultCellular.cs and Voronoi.cs exist (Voronoi.cs public API is tone sampling); no GLSL cellular function found by my grep (unused) | | +| Cycles port | CultMath.Cycles.glsl has math/mix/map-range/hsv node ports; my grep found no noise-texture/Musgrave/Voronoi node (unused) | | +| `cultmath_snoise_gh` (Hessian function) | NO, page-local, generated by `makeGH` (SPAN 838). CultMath has only the constant `CULTMATH_SNOISE_HESSIAN = 56.050385` (CultMath.glsl:656; C# math.Affine.cs:35) | | +| curl noise (`dispVec` mode 2, `curlK`) | NO (grep "curl" in CultMath.glsl and math*.cs: no hit); page-local | SPAN 410-417 | +| `fieldComp` (offsets, anisotropic `Mop`, ridged 1-2|n|) | NO, page-local | SPAN 394-409 | +| warp chain `fieldS`, `brushPoint`, `ampG`, `brushes` | NO, page-local | SPAN 383-391, 427-441 | +| `compField`/`evalSpan` bound evaluators | NO, page-local | SPAN 448-531 | +| KERN hash32/rngSeed/cascade/segInt | NO, page-local | KERN build(), shader | + +Constant mismatch: SPAN pins `SN_L = 10.099261` and its page text says "SNOISE_LIPSCHITZ 10.10 from CultMath"; CultMath currently has `SNOISE_LIPSCHITZ = 7.9640074` (math.Interval.cs:21, CultMath.glsl:519, CultMath.Interval.hlsl:10; derivation at packages/cultmath/docs/design.md:593). `SNOISE_HESSIAN` 56.050385 matches. + +## 3. Authoring parameters + +SPAN `SPEC` (SPAN 106-168; id, label, min..max, default): +- Field: F -100..100 (-20); gradB 2..60 (12); ext 0.002..0.4 (0.035); cap 0.5..10 (4) +- Brush: bDepth 0..260 (120); bWidth 400..3000 (1600); bExp 2..8 (2) +- Chaos: a1 0..80 (24); f1 0.1..3 per 100u (0.8); a2 0..40 (6); f2 0.3..8 (3); anim check (off); speed 0.1..4 (1) +- Warp 1 shape: m1 select {Plain fbm, Ridged, Curl} (0); o1 1..4 (1); lac1 1.2..3.5 (2); gain1 0.2..0.8 (0.5); st1 stretch 0.2..8 (1) +- Warp 2 shape: m2, o2 1..4 (1), lac2 1.2..3.5 (2), gain2 0.2..0.8 (0.5), st2 0.2..8 (1) +- Flow direction: fAz 0..360 (30); fEl -90..90 (0) +- Brush warp and regions: bwA 0..150 (0); bwF 0.1..4 (0.6); ampRim 0..4 (1); ampDeep 0..4 (1) +- Integrator: tol 0.01..1.5 (0.15); shadeLim 0.1..2 (0.5); maxSpans 8..1000 (400); hScale 0.05..1 (1); refSteps 32..2048 (256); lightN 0..4 (0); lightStep 4..80 (20); v2 checks satOn(1) rangeOn(1) localOn(0) ctrlOn(1) +- Sun: az 0..360 (40); el 5..90 (80); sunI 0..20 (3); sunCol #cfeaff. Medium: g -0.9..0.95 (0.4); albedo 0..1 (0.85); emit 0..4 (0.8); amb 0..1.5 (0.15). Tint colors c0 #8ff3e8, c1 #20b4c8, c2 #0b5f86 +- View: mode select (4 modes), split, exposure 0.05..6 (0.5), fov 30..110 (70), yaw, pitch, camx/camz +-900, camh -60..200, maxDist 300..2400, scale 0.2..1 +Presets (SPAN 172-): teal (defaults), view1, ramp (a1=a2=bDepth=0), chaos (a1 60,f1 .6,a2 14,f2 4), "Streaming filaments" fil (o1 3, lac1 2.1, st1 5, a2 12, f2 2.2, o2 2, st2 4, fAz 25, fEl 8, ampRim 1.3, ampDeep 0.5), curl (m1=m2=2, a1 16, f1 .55, o1 2, a2 5, f2 2.4), ridge (m1=m2=1, a1 26, o1 3, a2 8, bwA 35, bwF .7, ampRim 1.4, ampDeep .6). Test buttons: Benchmark, Noise self-test, Span vs reference, Matched-quality bench, Bound audit. + +KERN `SPEC` (min..max, probe/aeth default; Cascade items trigger rebuild; presets "Aetheria-ish" and "Probe defaults"): +- Cascade: lvl 1..6 (4/4); nc 1..8 (5/6); keep .05..0.9 (.25/.15); cs .2..0.7 (.40/.45); asp 1..3.5 (1.8/1.35); alc 1..3 (1.6/1.3); rot 0..1.57 (.6/.9); off 0..1.2 (.6/.75); qp .2..4 (2/1.2); flat .2..1 (.5/.6); spacing 30..120 (56); rootR .3..1 (.55/.6); radius 300..1200 (700); warp 0..80 (30); seed 1..99 (7) +- Kernel: kr 0..4 (2/2), kc 0..4 (1/2). Envelope: depth 0..300 (120); width 800..3000 (1600); gradB 4..40 (12) +- Medium: ext .005..0.6 (.10/.035); albedo 0..1 (1/.85); g -.9..0.95 (.6/.4); emit 0..4 (0/1.1); amb 0..1.5 (0/.15); lightN 0..4; lightStep 4..60 (20); lod 0..2 (.5) +- Sun (sunI 0..20, az, el, sunCol); Nebula tint (c0..c2, tintVar 0..1 (.3/.35), tintDepth 5..120 (40)); View (exposure, fov 30..110, yaw, pitch, camx +-650, camh 5..200, scale .2..1, speed .1..3, anim check off) + +## 4. Is the labs' motion a velocity field Aetheria's global flow could adopt? + +Aetheria flow facts (FlowField.cs is not on fauna-curl-probe HEAD; read from commit 5f5a1e4d on branch eureka/aetheria-fauna-flow-field via `git -C C:\aeth-wt\fauna-curl-probe show 5f5a1e4d:Assets/Scripts/ServerShared/FlowField.cs`; HLSL twin `git show HEAD:Assets/Shaders/Volumetric.cginc` lines 46-74, 130-161, 175-205): +- `FlowField.Global(settings, position, scroll)` = `Tri3D(field - (0,scroll.x,0), field*Golden - (0,scroll.y,0)) * GlobalAmplitude`, y *= .5, field = position/GlobalScale. `Tri3D` = cross(normalize(tri3(p)), normalize(tri3(p*1.618))), `tri(x)=|frac(x)-.5|` (piecewise linear). `Scroll(settings,t)` = phase mod 1 of GlobalScrollSpeed*t (second octave times Golden). It is a velocity field (position -> vec3), with a scrolled phase as the time term; `Slope` adds swirl+slide from the gravity gradient. +- The game's nebula `density()` (Volumetric.cginc 175-205) uses that flow to ADVECT `triNoise3d` samples: `noise = pow(triNoise3d((pos + fl*(lerpK-.5)*_FlowPeriod)/_NebulaNoiseScale), exponent) * tri2(lerpK)`, two copies at phases frac(t/P) and frac(t/P+.5), then a second pair at doubled cycle rate and 8x spatial frequency; sum displaces pos.y. Base noise is `triNoise3d` (Volumetric.cginc 51-67, 2 octaves of tri3 folds), not simplex. + +Lab motion: +- SPAN: not a velocity field. Time is a constant-vector domain scroll added to the noise argument (`uWSh`, `uBwSh`; SPAN 977-978). The "flow" is `uDir`, a fixed azimuth/elevation axis along which the warp noise is stretched (SPAN 396); page text states it is not derived from the brush gradient. Curl mode (SPAN 410-417) is the curl of a displacement potential, a divergence-free displacement evaluated per point at one instant; it is not advected and defines no time derivative beyond the scroll shift. Density warp only. +- KERN: motion is a two-phase crossfade (period 8) displacing kernel centres by a per-root baked, time-constant 2D flow vector times `uWarp`*(phase-.5)*2. Same cycle/crossfade structure as the game's `density()`, but with its own hand-written 2D flow (`flowAt`: swirl about origin + 30*bowl-gradient + sinusoid wobble, speed-modulated), not FlowField. + +Neither lab references FlowField, Tri3D, or any advection of noise by a time-varying velocity field. diff --git a/docs/research/2026-10-14-curl-flow-probe/lic_5-lab-stretch-fixed.png b/docs/research/2026-10-14-curl-flow-probe/lic_5-lab-stretch-fixed.png new file mode 100644 index 000000000..8861010a6 --- /dev/null +++ b/docs/research/2026-10-14-curl-flow-probe/lic_5-lab-stretch-fixed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24fc828a78dec54976002a1bc48f39566037a2bf6af903a678cfa572dacd4a1f +size 628274 diff --git a/docs/research/2026-10-14-curl-flow-probe/lic_5b-lab-stretch-contour.png b/docs/research/2026-10-14-curl-flow-probe/lic_5b-lab-stretch-contour.png new file mode 100644 index 000000000..101b10faa --- /dev/null +++ b/docs/research/2026-10-14-curl-flow-probe/lic_5b-lab-stretch-contour.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a33b40b3bb8a21ce3b955e07aaf08e45eafc0da49d4bcee706655116d872672 +size 597591 diff --git a/docs/research/2026-10-14-curl-flow-probe/lic_5c-lab-stretch-contour-slope.png b/docs/research/2026-10-14-curl-flow-probe/lic_5c-lab-stretch-contour-slope.png new file mode 100644 index 000000000..6070c7093 --- /dev/null +++ b/docs/research/2026-10-14-curl-flow-probe/lic_5c-lab-stretch-contour-slope.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaf4948ce3519650f98114238851e00bd246bd36aacbb955342a631333e7714a +size 707958 diff --git a/docs/research/2026-10-14-curl-flow-probe/lic_contact_sheet.png b/docs/research/2026-10-14-curl-flow-probe/lic_contact_sheet.png index 8dbe8a6bf..5737609f1 100644 --- a/docs/research/2026-10-14-curl-flow-probe/lic_contact_sheet.png +++ b/docs/research/2026-10-14-curl-flow-probe/lic_contact_sheet.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1677296be35eb7ab2158af3fd1f21adf02eeedbd4b27e08d3f785fce86011b4 -size 2161404 +oid sha256:af6ee21f52e0572de912862b71c7cac5a21d621ecf920a3b5e459c12615cfe83 +size 3211216 diff --git a/docs/research/2026-10-14-curl-flow-probe/run.log b/docs/research/2026-10-14-curl-flow-probe/run.log index 0b632d446..bf69579e0 100644 --- a/docs/research/2026-10-14-curl-flow-probe/run.log +++ b/docs/research/2026-10-14-curl-flow-probe/run.log @@ -1,8 +1,8 @@ snoise_grad analytic vs FD max err 2.1925998794358748e-08 period / precision {'axis0': 1.0206946399193839e-11, 'axis1': 1.2218337452907235e-11, 'axis2': 1.0844658504538529e-11, 'fbm3_z': 2.8636648607971438e-11, 'ulp32_unfolded_1d': 0.000244140625, 'ulp32_folded_1d': 3.0517578125e-05, 'ulp32_unfolded_10d': 0.001953125, 'ulp32_folded_10d': 6.103515625e-05, 'ulp32_unfolded_100d': 0.015625, 'ulp32_folded_100d': 7.62939453125e-06} tri3d numpy vs habitat_probe max err 0.0 -tri3d RMS speed 6.603448949711046 gains k {'curl1': 1.8716047508573805, 'curlfbm': 1.126907845785225, 'warp': 0.7637324999861898} slope grid ... +tri3d RMS speed 6.603448949711046 gains k {'curl1': 1.8716047508573805, 'curlfbm': 1.126907845785225, 'warp': 0.7637324999861898, 'lab': 1.0915955246130895, 'labslope': 0.3915687026127663} == 0-tri3d: cost 0 snoise_grad (2 normalize(tri3) + cross) {'rms_div': 0.20651205778187315, 'rms_vort': 0.15264839371242175, 'div_over_vort': 1.3528609948619998, 'speed': [1.7371031863703295, 5.901654179436111, 9.62248760054106, 11.586184082696114, 12.223586819574475], 'rms': 6.503824936533222, 'still_frac': 0.0319} C:\aeth-wt\fauna-curl-probe\docs\research\2026-10-14-curl-flow-probe\lic_0-tri3d.png @@ -27,6 +27,18 @@ slope grid ... {'rms_div': 0.15167226920301866, 'rms_vort': 0.6337080265131629, 'div_over_vort': 0.23934093124488495, 'speed': [4.152942742162839, 6.721649628568566, 11.157884115284288, 26.547283825576866, 107.69405595071426], 'rms': 8.93130732391741, 'still_frac': 0.0035} C:\aeth-wt\fauna-curl-probe\docs\research\2026-10-14-curl-flow-probe\lic_4b-curl-warped-slope.png C:\aeth-wt\fauna-curl-probe\docs\research\2026-10-14-curl-flow-probe\drift_4b-curl-warped-slope.png +== 5-lab-stretch-fixed: cost 3 snoise_grad (fbm_grad of a stretched argument) + {'rms_div': 5.558963705645817e-05, 'rms_vort': 0.2730793919618278, 'div_over_vort': 0.0002035658445593314, 'speed': [0.8667381407585574, 3.172091295533267, 10.728892348644765, 22.920368457618324, 36.08635765573939], 'rms': 6.658199538461918, 'still_frac': 0.06445} + C:\aeth-wt\fauna-curl-probe\docs\research\2026-10-14-curl-flow-probe\lic_5-lab-stretch-fixed.png + C:\aeth-wt\fauna-curl-probe\docs\research\2026-10-14-curl-flow-probe\drift_5-lab-stretch-fixed.png +== 5b-lab-stretch-contour: cost 12 snoise_grad as probed (psi differenced); 3 + a height Hessian analytically + {'rms_div': 1.9801024466641561, 'rms_vort': 3.3310309410856718, 'div_over_vort': 0.5944413251288467, 'speed': [0.6113167724180104, 1.9093933242420025, 6.116526381276066, 22.779618958983708, 174.75200171199305], 'rms': 6.253181637281358, 'still_frac': 0.1038} + C:\aeth-wt\fauna-curl-probe\docs\research\2026-10-14-curl-flow-probe\lic_5b-lab-stretch-contour.png + C:\aeth-wt\fauna-curl-probe\docs\research\2026-10-14-curl-flow-probe\drift_5b-lab-stretch-contour.png +== 5c-lab-stretch-contour-slope: cost as 5b + the slope term (axis and slope term share the gravity gradient) + {'rms_div': 2.021078137274867, 'rms_vort': 3.3530115055974576, 'div_over_vort': 0.6027650468544219, 'speed': [4.309379229574169, 6.448502702031059, 9.634888532119085, 23.996604067476206, 211.9540577466552], 'rms': 8.764542223350555, 'still_frac': 0.0019} + C:\aeth-wt\fauna-curl-probe\docs\research\2026-10-14-curl-flow-probe\lic_5c-lab-stretch-contour-slope.png + C:\aeth-wt\fauna-curl-probe\docs\research\2026-10-14-curl-flow-probe\drift_5c-lab-stretch-contour-slope.png scroll 0-tri3d corr(t=0, t=10s) = 0.316 scroll 0-tri3d corr(t=0, t=40s) = 0.452 scroll 0-tri3d corr(t=0, t=120s) = 0.799 @@ -39,4 +51,10 @@ slope grid ... scroll 3-curl-warped corr(t=0, t=10s) = 0.100 scroll 3-curl-warped corr(t=0, t=40s) = -0.007 scroll 3-curl-warped corr(t=0, t=120s) = 0.010 -done 45.1 s + scroll 5-lab-stretch-fixed corr(t=0, t=10s) = 0.221 + scroll 5-lab-stretch-fixed corr(t=0, t=40s) = -0.033 + scroll 5-lab-stretch-fixed corr(t=0, t=120s) = 0.145 + scroll 5b-lab-stretch-contour corr(t=0, t=10s) = 0.312 + scroll 5b-lab-stretch-contour corr(t=0, t=40s) = -0.010 + scroll 5b-lab-stretch-contour corr(t=0, t=120s) = -0.026 +done 90.9 s