CultLib/packages/cultmath/shaders/CultMath.Interval.hlsl
2026-10-04 09:47:49 +02:00

133 lines
5.4 KiB
HLSL

#ifndef CULTMATH_INTERVAL_HLSL
#define CULTMATH_INTERVAL_HLSL
// Interval arithmetic over float2(lo, hi), lo <= hi, both finite. Every cultmath_iv_* function returns
// an interval containing f(x) for every x in its input intervals, mirroring math.Interval.cs bit for
// bit; see that file and docs/design.md ("Intervals") for each op's enclosure and its ulp widening.
// Included by CultMath.hlsl after its own definitions of cultmath_smoothstep and cultmath_snoise.
// Lipschitz constant of cultmath_snoise(float3); provenance in math.Interval.cs (SNOISE_LIPSCHITZ).
static const float CULTMATH_SNOISE_LIPSCHITZ = 7.9640074;
// Lipschitz constant of cultmath_snoise(float2); provenance in math.Interval.cs (SNOISE2_LIPSCHITZ).
static const float CULTMATH_SNOISE2_LIPSCHITZ = 8.117002;
float2 cultmath_iv_point(float value) { return float2(value, value); }
float2 cultmath_iv_add(float2 a, float2 b) { return float2(a.x + b.x, a.y + b.y); }
float2 cultmath_iv_sub(float2 a, float2 b) { return float2(a.x - b.y, a.y - b.x); }
float2 cultmath_iv_neg(float2 a) { return float2(-a.y, -a.x); }
float2 cultmath_iv_mul(float2 a, float2 b)
{
float p0 = a.x * b.x;
float p1 = a.x * b.y;
float p2 = a.y * b.x;
float p3 = a.y * b.y;
return float2(min(min(p0, p1), min(p2, p3)), max(max(p0, p1), max(p2, p3)));
}
float2 cultmath_iv_scale(float2 a, float s) { return s >= 0.0 ? float2(a.x * s, a.y * s) : float2(a.y * s, a.x * s); }
float2 cultmath_iv_abs(float2 a)
{
if (a.x >= 0.0)
return a;
if (a.y <= 0.0)
return float2(-a.y, -a.x);
return float2(0.0, max(-a.x, a.y));
}
float2 cultmath_iv_min(float2 a, float2 b) { return float2(min(a.x, b.x), min(a.y, b.y)); }
float2 cultmath_iv_max(float2 a, float2 b) { return float2(max(a.x, b.x), max(a.y, b.y)); }
float2 cultmath_iv_sqr(float2 a)
{
float l = a.x * a.x;
float h = a.y * a.y;
if (a.x >= 0.0)
return float2(l, h);
if (a.y <= 0.0)
return float2(h, l);
return float2(0.0, max(l, h));
}
float2 cultmath_iv_sqrt(float2 a) { return float2(sqrt(max(a.x, 0.0)), sqrt(max(a.y, 0.0))); }
// Widened by one ulp on each side: exp is not required to be correctly rounded.
float2 cultmath_iv_exp(float2 a)
{
float lo = exp(a.x);
float hi = exp(a.y);
return float2(lo > 0.0 ? asfloat(asuint(lo) - 1u) : 0.0, asfloat(asuint(hi) + 1u));
}
float2 cultmath_iv_clamp(float2 a, float minimum, float maximum)
{
return float2(cultmath_clamp(a.x, minimum, maximum), cultmath_clamp(a.y, minimum, maximum));
}
float2 cultmath_iv_saturate(float2 a) { return float2(cultmath_saturate(a.x), cultmath_saturate(a.y)); }
// The endpoints, ordered, widened by one ulp inside [0, 1]: the float32 evaluation of smoothstep is
// monotone only to within one ulp.
float2 cultmath_iv_smoothstep(float minimum, float maximum, float2 a)
{
float s0 = cultmath_smoothstep(minimum, maximum, a.x);
float s1 = cultmath_smoothstep(minimum, maximum, a.y);
float lo = min(s0, s1);
float hi = max(s0, s1);
return float2(lo > 0.0 ? asfloat(asuint(lo) - 1u) : 0.0, hi < 1.0 ? asfloat(asuint(hi) + 1u) : 1.0);
}
// The natural interval extension of x + (y - x) * amount, which is how cultmath_lerp evaluates.
float2 cultmath_iv_lerp(float2 a, float2 b, float amount) { return cultmath_iv_add(a, cultmath_iv_scale(cultmath_iv_sub(b, a), amount)); }
// [n - L r, n + L r] intersected with [-1, 1], n = cultmath_snoise(centre). One snoise, value only.
float2 cultmath_iv_snoise_ball(float3 centre, float radius)
{
float n = cultmath_snoise(centre);
float e = CULTMATH_SNOISE_LIPSCHITZ * radius;
return float2(max(n - e, -1.0), min(n + e, 1.0));
}
// The 2D overload: [n - L2 r, n + L2 r] intersected with [-1, 1], n = cultmath_snoise(centre). One snoise, value only.
float2 cultmath_iv_snoise_ball(float2 centre, float radius)
{
float n = cultmath_snoise(centre);
float e = CULTMATH_SNOISE2_LIPSCHITZ * radius;
return float2(max(n - e, -1.0), min(n + e, 1.0));
}
// Octave sum of cultmath_iv_snoise_ball, compounding frequency and amplitude as the value-and-gradient fBm does.
float2 cultmath_iv_fbm_ball(float3 centre, float radius, int octaves, float lacunarity, float gain)
{
octaves = clamp(octaves, 0, 16);
float amplitude = 1.0;
float frequency = 1.0;
float2 sum = float2(0.0, 0.0);
for (int i = 0; i < octaves; i++)
{
sum = cultmath_iv_add(sum, cultmath_iv_scale(cultmath_iv_snoise_ball(centre * frequency, radius * abs(frequency)), amplitude));
frequency *= lacunarity;
amplitude *= gain;
}
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