Shared error-function implementations — the single source of truth for
every erf in the VIEWER, CPU and GPU. (The Python package carries its
own copy of the same A&S 7.1.26 form in luxar/gsplats/lift.py — the
two languages cannot share code, but the constants must stay in sync.)
Two implementations, each matched to its consumer. The TSL builder for
the second one lives next door in erf-tsl.ts so this file stays free
of three/tsl — see that module's header for why (issue #1679):
erfRef — the Abramowitz & Stegun 7.1.26 rational approximation
(max abs error 1.5e-7). The CPU-side reference: precomputing
normalization factors (computeRayIntegralFactor in
gsplat/math.ts), LUT generation, and the oracle the unit tests
measure erfPoly against. Costs one exp and one division per
call — fine on the CPU, the expensive form in a fragment shader.
erfPoly / GLSL_ERF_FUNCTIONS (+ erfPolyTSL in erf-tsl.ts) — a pure odd
polynomial on [-3, 3], clamped to ±1 outside. NO exp, NO division:
the fragment-shader form, built for the #1352 volumetric line
primitive (since deleted; no production shader consumes it today —
the parity fixture and codegen snapshot are its consumers, keeping
it verified for the next erf-hungry shader lane).
Degree-13 constrained least-squares fit with P(3) = 1 (to 1e-9 for
the printed coefficient set), so the clamp is continuous. Max abs
error 5.4e-4 against the exact erf; the worst error in the
difference QUOTIENT (erf(x1) − erf(x0)) / (x1 − x0) is 1.4e-3
absolute (0.13% of its 2/√π ≈ 1.128 peak) when the two arguments
are ≥ 0.5 apart (callers with closer arguments need a
midpoint/Taylor lane instead of the raw difference).
Two consequences of this being a least-squares fit rather than a
bounded approximation — both well inside that error bound, both easy
to trip over: it is NOT clamped to ±1 (it peaks at 1.00032 near
|x| ≈ 2.72) and it is NOT strictly monotone near saturation, so an
erf(x1) − erf(x0) window with x1 > x0 can come out slightly
NEGATIVE (worst −8.8e-4, at x0 ≈ 2.72 / x1 ≈ 2.94). A consumer that
needs a non-negative window — an optical depth, a coverage weight —
must clamp at zero rather than trust the sign.
The GLSL string and the TSL builder are BOTH generated from
ERF_POLY_COEFFS, so the two backends cannot drift in VALUE. The
guarantee is value-level, not textual: GLSL literals are serialized
here with toFixed(9), while the TSL path passes the same numbers to
float() and Three's code generator owns their formatting (numeric
parity is what the tsl-shader-parity harness verifies). Same
single-source pattern as falloff.ts / volumetric.ts.
Shared error-function implementations — the single source of truth for every erf in the VIEWER, CPU and GPU. (The Python package carries its own copy of the same A&S 7.1.26 form in
luxar/gsplats/lift.py— the two languages cannot share code, but the constants must stay in sync.)Two implementations, each matched to its consumer. The TSL builder for the second one lives next door in
erf-tsl.tsso this file stays free ofthree/tsl— see that module's header for why (issue #1679):erfRef— the Abramowitz & Stegun 7.1.26 rational approximation (max abs error 1.5e-7). The CPU-side reference: precomputing normalization factors (computeRayIntegralFactoringsplat/math.ts), LUT generation, and the oracle the unit tests measureerfPolyagainst. Costs oneexpand one division per call — fine on the CPU, the expensive form in a fragment shader.erfPoly/GLSL_ERF_FUNCTIONS(+erfPolyTSLinerf-tsl.ts) — a pure odd polynomial on [-3, 3], clamped to ±1 outside. NO exp, NO division: the fragment-shader form, built for the #1352 volumetric line primitive (since deleted; no production shader consumes it today — the parity fixture and codegen snapshot are its consumers, keeping it verified for the next erf-hungry shader lane). Degree-13 constrained least-squares fit with P(3) = 1 (to 1e-9 for the printed coefficient set), so the clamp is continuous. Max abs error 5.4e-4 against the exact erf; the worst error in the difference QUOTIENT(erf(x1) − erf(x0)) / (x1 − x0)is 1.4e-3 absolute (0.13% of its 2/√π ≈ 1.128 peak) when the two arguments are ≥ 0.5 apart (callers with closer arguments need a midpoint/Taylor lane instead of the raw difference).Two consequences of this being a least-squares fit rather than a bounded approximation — both well inside that error bound, both easy to trip over: it is NOT clamped to ±1 (it peaks at 1.00032 near |x| ≈ 2.72) and it is NOT strictly monotone near saturation, so an
erf(x1) − erf(x0)window with x1 > x0 can come out slightly NEGATIVE (worst −8.8e-4, at x0 ≈ 2.72 / x1 ≈ 2.94). A consumer that needs a non-negative window — an optical depth, a coverage weight — must clamp at zero rather than trust the sign.The GLSL string and the TSL builder are BOTH generated from
ERF_POLY_COEFFS, so the two backends cannot drift in VALUE. The guarantee is value-level, not textual: GLSL literals are serialized here withtoFixed(9), while the TSL path passes the same numbers tofloat()and Three's code generator owns their formatting (numeric parity is what the tsl-shader-parity harness verifies). Same single-source pattern asfalloff.ts/volumetric.ts.