Merge interval-ops r3 (3ac7250a) into the GLSL lowering
README conflict resolved by keeping r3's Intervals bullet, then the GLSL bullet. design.md and HlslSourceCompatibilityTests.cs merged clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
8be32a2e27
7 changed files with 1300 additions and 116 deletions
|
|
@ -38,9 +38,15 @@ shader semantics.
|
|||
- interval arithmetic for culling empty space with a proof: `iv_*` functions
|
||||
over `float2(lo, hi)` (`iv_add`, `iv_mul`, `iv_exp`, `iv_smoothstep`, ...)
|
||||
and `iv_snoise_ball`/`iv_fbm_ball`, which bound `snoise` and `fbm_grad` over a
|
||||
ball. Every one returns an interval containing the function's value at every
|
||||
point of its input, so a raymarch can skip a segment whose `hi` is below its
|
||||
cutoff; `cultmath_iv_*` in HLSL. See `docs/design.md`, "Intervals".
|
||||
ball, and `iv_frustum_ball`, the ball around a screen tile's rays over a depth
|
||||
segment, so one probe serves the whole tile (its radius carries its own
|
||||
float32 rounding bound). Every one returns an interval
|
||||
containing the function's value at every point of its input, so a raymarch
|
||||
can skip a segment whose `hi` is below its cutoff; `cultmath_iv_*` in HLSL.
|
||||
The bounds compose with a consumer's own analytic envelope (a height fog, a
|
||||
carved sphere): its interval over the segment proves empty space in one probe,
|
||||
and the noise ball is consulted only near the surface. See `docs/design.md`,
|
||||
"Intervals".
|
||||
- `shaders/CultMath.glsl`, the same `cultmath_*` library in GLSL ES 3.00 for
|
||||
WebGL2, generated from `CultMath.hlsl` (never edited by hand). GLSL has no
|
||||
`#include`: a WebGL consumer concatenates the file's text after its own
|
||||
|
|
|
|||
|
|
@ -458,6 +458,58 @@ warp that moves a point by at most `D` is enclosed by the radius `r + D`
|
|||
(`NoiseBoundTests.WarpedPointsStayEnclosed`); this is what the gamecult.org
|
||||
ground's march rests on.
|
||||
|
||||
`iv_frustum_ball(m_c, z0, z1, footprintPerDepth, warp)` is the ball one probe
|
||||
serves a whole screen tile with. In a pinhole camera frame looking down `+z`,
|
||||
where the ray of slope `m` is the points `(m z, z)`, it encloses every point of
|
||||
every ray whose slope lies within `footprintPerDepth` of `m_c` (`N / (sqrt(2) f)`
|
||||
for an `N x N` tile at focal length `f` pixels: the full pixel footprint, so
|
||||
sub-pixel jitter is covered), over depths `[z0, z1]`, each point moved by a warp
|
||||
of length at most `warp`. The centre is `(m_c z_m, z_m)`, `z_m` the mid-depth;
|
||||
the radius is `((z1 - z0) / 2) |(m_c, 1)| + z1 footprintPerDepth + warp`. A point
|
||||
of slope `m_c + e` at depth `z` is the centre plus `(z - z_m)(m_c, 1)` plus
|
||||
`z (e, 0)`. The returned ball is float32, and on a degenerate (`z0 == z1`),
|
||||
thin or far segment the rounding of the centre alone exceeds the gap the
|
||||
triangle inequality leaves, so the radius is widened by
|
||||
`(|c|_1 + radius) 2^-20`. With `u = 2^-24`, each centre component carries at most
|
||||
two roundings (`z_m`, then the product), so the float centre is within
|
||||
`2u |c|_1` of the exact one; the radius carries at most seven (`z1 - z0`, the
|
||||
axis length at `3u`, the product, `z1 footprintPerDepth`, the sum, `+ warp`), so
|
||||
the exact radius is at most `(1 + 7u)` times the float one; the widening rounds
|
||||
once more. `2^-20 = 16u` covers `8u (|c|_1 + radius)` twice over, and fma
|
||||
contraction only removes roundings, so HLSL and GLSL compilers are covered by
|
||||
the same bound. At the shipped scales (`|c|_1` near 600, `r` near 5) it adds
|
||||
`6e-4`. The caller rotates and translates the centre into world space and scales
|
||||
centre and radius by its noise frequency; the rounding of that transform is the
|
||||
caller's. `NoiseBoundTests.TileBallEnclosesEveryRaySegment` checks 2,000 seeded
|
||||
tiles x 64 points of the r2 domain and 2,000 tiles x 8 corner points of each of
|
||||
seven extreme families (`z0 == z1`, `z1 - z0 = 1e-6 z1`, `|m_c|` to 1000, `z` to
|
||||
`1e7`, `z0 = 0` with `z1` to `1e-3`, `f` down to 1, warp to `1e6`), corner flows
|
||||
full-length and outward, distances in double, with no tolerance. It pins the ball
|
||||
to the derivation both ways (`r <= ball.w <= r + 2^-19 (r + |c|_1)`), so a ball
|
||||
that forgets the widening fails and one grown past it fails too. A moving camera needs nothing
|
||||
more: each frame's probes use that frame's camera, and no ball, mask or probe
|
||||
result is carried from one frame to the next.
|
||||
|
||||
The noise ball alone proves little: at `L` near 10 it bounds `snoise` to a width
|
||||
of `2 L r`, which covers all of `[-1, 1]` once `L r` reaches 1. Empty space
|
||||
becomes provable through an analytic envelope. The consumer's density is a
|
||||
pre-distortion SDF (a height fog, a well map, a carved sphere) that noise
|
||||
displaces only near its surface, and the consumer composes the bound from `iv_*`
|
||||
ops. The envelope is taken over the slice's exact box (`iv_mul` of the depth
|
||||
range by each slope range, then the SDF's own monotone pieces), together with its
|
||||
fade. The noise is `iv_snoise_ball` over `iv_frustum_ball`, scaled by the noise
|
||||
frequency. The warp moves only the noise argument, so it enlarges the ball and
|
||||
the envelope's box takes none of it. Where the fade's upper end is 0 the noise
|
||||
term is identically 0 and no `snoise` is evaluated. Where the envelope already
|
||||
proves the slice empty with the noise at its full range, the ball is not
|
||||
evaluated either. A probe is worth attempting only when the best result any
|
||||
centre value could give would prove the slice, and a ball with `L r >= 2` is
|
||||
`[-1, 1]` whatever `snoise(c)` is, so it costs nothing. CultMath owns none of the
|
||||
envelope: the interval files have no envelope function, and each consumer composes
|
||||
its own. `NoiseBoundTests.EnvelopeBoundEnclosesDensity` checks that composition,
|
||||
the one the site and Aetheria use, over 2,000 seeded slices x 64 warped points
|
||||
with no tolerance.
|
||||
|
||||
`L` has provenance, not a proof. `NoiseBoundTests.MeasureLipschitz` (slow,
|
||||
explicit) takes the largest `|snoise_grad|` over 1e6 seeded points in
|
||||
[-256, 256]^3 (7.1933103), refines the 1e4 largest by gradient ascent
|
||||
|
|
@ -478,21 +530,43 @@ tracking or affine forms; those are later steps of
|
|||
sampled range: 5.08 for `iv_snoise_ball` at `r = 0.05`, 1.96 at 0.25 and 1.28
|
||||
at 1.0; 6.64 and 2.82 for 4-octave `iv_fbm_ball` at 0.05 and 0.25.
|
||||
|
||||
The saving is a number too. `NoiseBoundTests.IntervalSkipHalvesEvaluations`
|
||||
marches 1,000 seeded rays through the map's warped coverage field (`F0 = 0.35`,
|
||||
cutoff 0.25, a slab of depth 12) once at the dense step and once with the
|
||||
adaptive interval march, and counts `snoise` evaluations (three per dense
|
||||
sample, one per probe; the flow is not counted, because the site reads it from
|
||||
a texture). Both marches integrate the same cells, so their transmittance
|
||||
agrees exactly. At the test's parameters (first step 0.25, dense step 0.125,
|
||||
warp bound 0.05) the dense march spends 287,940 evaluations and the interval
|
||||
march 309,550: 0.93x, not the 2x saving the proving-ground contract asks for.
|
||||
Over the grid the test also prints, the best is 1.83x (first step 0.0625, no
|
||||
warp), and at a first step of 1 it is 0.74x. With `L` near 10 a probe proves a
|
||||
segment empty only where `snoise` is far below the cutoff, and a probe that
|
||||
fails costs one evaluation on top of the three the dense sample costs. The test
|
||||
skips with that open fork named instead of passing; whether the march, the
|
||||
field or the bound changes is the campaign's decision.
|
||||
The saving is measured where there is empty space to find.
|
||||
`NoiseBoundTests.IntervalSkipHalvesEvaluations` draws screen tiles, each with
|
||||
its own camera, and runs one pre-pass per tile over the dense grid's cell ranges:
|
||||
it starts with the whole grid, skips and doubles a range it proves empty, halves
|
||||
one it cannot, and runs to the grid's end. Each of the tile's `N^2` rays is then
|
||||
marched twice with the same integrator and the same early-out at transmittance
|
||||
0.02: once over every cell, once over the tile's unproven cells only. The two
|
||||
integrate the same cells, so their transmittance agrees exactly in every
|
||||
configuration. Cost is counted in `snoise` evaluations, with an envelope
|
||||
evaluation or probe weighted 0.2. That weight is an estimate, and both counts are
|
||||
printed. The flow is not counted. At `N = 8` with the warp on, in Aetheria's units
|
||||
on its 256-cell quadratic grid:
|
||||
|
||||
- (a) Height fog, camera above the safety band, rays level and up: the whole
|
||||
grid is one envelope probe per tile, against 256 envelope evaluations per ray,
|
||||
16384x.
|
||||
- (b) Inside the fog: 0.99x. Every cell is fog, and the pre-pass probes each one.
|
||||
- (c) The zone bowl and four wells, the camera gazing across: 1.41x, and 3.32x
|
||||
with no warp. The pre-pass cuts envelope evaluations from 212.93 to 37.12 per
|
||||
ray. Almost all of the dense march's 78.67 `snoise` per ray fall in the noise
|
||||
band, where the rays need them (ceiling 1.42x). The warp bound `D = 60` is what
|
||||
leaves the noise ball unable to prove anything at this noise scale.
|
||||
- (d) r1's uniform slab, which has no envelope: 1.23x, and 1.48x with no warp.
|
||||
- (e) The void at the shipped point (`Rh = 198`, `S = 50`, the low camera,
|
||||
200 tiles), with three marches. The fixed-step reference takes 36.34 steps per
|
||||
pixel. The same cells masked by the pre-pass take 20.13, with identical
|
||||
transmittance. The footprint-aware march takes 16.45 inside the unmasked
|
||||
cells, within 0.008339431 of the reference's transmittance: 2.21x fewer steps.
|
||||
`snoise` per pixel goes from 32.98 to 26.56, and combined cost improves 1.34x.
|
||||
Over the grid of hollow radius, camera offset and ramp width the steps ratio
|
||||
runs from 1.57x (`Rh = 100`, low, `S = 150`) to 12.40x (`Rh = 1000`, centred,
|
||||
`S = 10`), and the cost ratio from 1.12x to 3.47x.
|
||||
|
||||
The contract is 2x on combined cost for (a) and (c) and 2x fewer steps per pixel
|
||||
for (e). (a) and (e) meet it. (c) does not, so the test skips naming
|
||||
`saving-2x-scenarios`. The fields, the grids and the 0.2 weight are not tuned
|
||||
toward the contract.
|
||||
|
||||
Consumers: the gamecult.org ground shader (through the GLSL lowering) and
|
||||
Aetheria's nebula raymarch at its CultMath pin bump.
|
||||
|
|
|
|||
|
|
@ -104,4 +104,19 @@ float2 cultmath_iv_fbm_ball(float3 centre, float radius, int octaves, float lacu
|
|||
return sum;
|
||||
}
|
||||
|
||||
// A ball (xyz centre, w radius) enclosing every point of every ray of a screen tile over the depth segment
|
||||
// [z0, z1], warped by at most warp. Camera frame: pinhole at the origin looking down +z, ray (m z, z).
|
||||
// footprintPerDepth = N / (sqrt(2) f) for an N x N tile at focal length f. The radius is widened by
|
||||
// (|c|_1 + radius) 2^-20, which covers the ball's own float32 rounding. Derivation in math.Interval.cs.
|
||||
float4 cultmath_iv_frustum_ball(float2 centreSlope, float z0, float z1, float footprintPerDepth, float warp)
|
||||
{
|
||||
float zm = (z0 + z1) * 0.5;
|
||||
float axis = sqrt(centreSlope.x * centreSlope.x + centreSlope.y * centreSlope.y + 1.0);
|
||||
float cx = centreSlope.x * zm;
|
||||
float cy = centreSlope.y * zm;
|
||||
float radius = (z1 - z0) * 0.5 * axis + z1 * footprintPerDepth + warp;
|
||||
radius += (abs(cx) + abs(cy) + abs(zm) + radius) * 9.5367431640625e-7;
|
||||
return float4(cx, cy, zm, radius);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -164,4 +164,36 @@ public static partial class math
|
|||
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A ball (xyz centre, w radius) enclosing every point of every ray of a screen tile over the depth
|
||||
/// segment [z0, z1], each point then moved by a warp of length at most warp: the ball to hand
|
||||
/// iv_snoise_ball (scaled by the noise frequency) when one probe serves the whole tile. Camera frame:
|
||||
/// a pinhole at the origin looking down +z, where the ray of slope m is the points (m z, z).
|
||||
/// centreSlope is the tile's central slope m_c; footprintPerDepth is the largest distance of any
|
||||
/// ray's slope from m_c, N / (sqrt(2) f) for an N x N pixel tile at focal length f pixels (the full
|
||||
/// pixel footprint, so sub-pixel jitter is covered). The centre is (m_c z_m, z_m), z_m = (z0 + z1) / 2;
|
||||
/// the radius is ((z1 - z0) / 2) |(m_c, 1)| + z1 footprintPerDepth + warp. Derivation: a point of slope
|
||||
/// m_c + e (|e| <= footprintPerDepth) at depth z is c + (z - z_m)(m_c, 1) + z (e, 0); the first term is
|
||||
/// at most the first radius term, the second at most the second, and the warp adds at most warp.
|
||||
/// The ball also carries its own float32 rounding: the radius is widened by (|c|_1 + radius) 2^-20.
|
||||
/// With u = 2^-24, each centre component carries at most two roundings (z_m, then the product), so the
|
||||
/// float centre is within 2u |c|_1 of the exact one; the radius carries at most seven, so the exact
|
||||
/// radius is at most (1 + 7u) times the float one; the widening rounds once more. 2^-20 = 16u covers
|
||||
/// 8u (|c|_1 + radius) twice over, and fma contraction only removes roundings. So every exact point of
|
||||
/// every ray, at any depth and slope, lies in the returned float ball
|
||||
/// (NoiseBoundTests.TileBallEnclosesEveryRaySegment, degenerate and far segments included). The caller
|
||||
/// rotates and translates the centre into world space (the radius is unchanged) and scales both by its
|
||||
/// noise frequency; the rounding of that transform is the caller's.
|
||||
/// </summary>
|
||||
public static float4 iv_frustum_ball(float2 centreSlope, float z0, float z1, float footprintPerDepth, float warp)
|
||||
{
|
||||
var zm = (z0 + z1) * 0.5f;
|
||||
var axis = sqrt(centreSlope.x * centreSlope.x + centreSlope.y * centreSlope.y + 1.0f);
|
||||
var cx = centreSlope.x * zm;
|
||||
var cy = centreSlope.y * zm;
|
||||
var radius = (z1 - z0) * 0.5f * axis + z1 * footprintPerDepth + warp;
|
||||
radius += (abs(cx) + abs(cy) + abs(zm) + radius) * 9.5367431640625e-7f;
|
||||
return new float4(cx, cy, zm, radius);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ public sealed class HlslSourceCompatibilityTests
|
|||
}
|
||||
|
||||
Assert.True(mismatches.Count == 0, $"{mismatches.Count} mismatches:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}");
|
||||
Assert.Equal(52, compared.Count);
|
||||
Assert.Equal(53, compared.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -104,4 +104,19 @@ float2 cultmath_iv_fbm_ball(float3 centre, float radius, int octaves, float lacu
|
|||
return sum;
|
||||
}
|
||||
|
||||
// A ball (xyz centre, w radius) enclosing every point of every ray of a screen tile over the depth segment
|
||||
// [z0, z1], warped by at most warp. Camera frame: pinhole at the origin looking down +z, ray (m z, z).
|
||||
// footprintPerDepth = N / (sqrt(2) f) for an N x N tile at focal length f. The radius is widened by
|
||||
// (|c|_1 + radius) 2^-20, which covers the ball's own float32 rounding. Derivation in math.Interval.cs.
|
||||
float4 cultmath_iv_frustum_ball(float2 centreSlope, float z0, float z1, float footprintPerDepth, float warp)
|
||||
{
|
||||
float zm = (z0 + z1) * 0.5;
|
||||
float axis = sqrt(centreSlope.x * centreSlope.x + centreSlope.y * centreSlope.y + 1.0);
|
||||
float cx = centreSlope.x * zm;
|
||||
float cy = centreSlope.y * zm;
|
||||
float radius = (z1 - z0) * 0.5 * axis + z1 * footprintPerDepth + warp;
|
||||
radius += (abs(cx) + abs(cy) + abs(zm) + radius) * 9.5367431640625e-7;
|
||||
return float4(cx, cy, zm, radius);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Reference in a new issue