GLSL SDF Normals With the Tetrahedron Technique
This article provides an overview of the tetrahedron technique for calculating surface normals of Signed Distance Fields (SDF) in GLSL. Computing normals via standard central differences requires six evaluations of the scene function per pixel, which creates a substantial performance bottleneck in raymarching shaders. The tetrahedron method reduces this workload to just four evaluations while preserving geometric symmetry, offering an optimal balance between performance and visual fidelity.
The Gradient of a Signed Distance Field
In signed distance field rendering, the surface normal \(\mathbf{n}\) at a point \(\mathbf{p}\) corresponds to the normalized gradient of the distance function \(f(\mathbf{p})\):
\[\mathbf{n} = \frac{\nabla f(\mathbf{p})}{\|\nabla f(\mathbf{p})\|}\]
Because most distance functions do not have analytically derived derivatives, the gradient must be approximated numerically.
The standard approach uses central finite differences along the Cartesian axes:
\[\nabla f(\mathbf{p}) \approx \begin{pmatrix} f(\mathbf{p} + \epsilon \mathbf{\hat{x}}) - f(\mathbf{p} - \epsilon \mathbf{\hat{x}}) \\ f(\mathbf{p} + \epsilon \mathbf{\hat{y}}) - f(\mathbf{p} - \epsilon \mathbf{\hat{y}}) \\ f(\mathbf{p} + \epsilon \mathbf{\hat{z}}) - f(\mathbf{p} - \epsilon \mathbf{\hat{z}}) \end{pmatrix}\]
This calculation requires sampling the scene function \(f\) six times. In complex scenes containing fractals, boolean operations, or domain repetitions, evaluating \(f\) six times per ray hit significantly degrades frame rates.
How the Tetrahedron Method Works
The tetrahedron technique approximates the gradient by sampling the distance field at the four vertices of a regular tetrahedron centered around the evaluation point \(\mathbf{p}\).
A regular tetrahedron can be inscribed in a cube using four alternating vertices whose coordinates use signs from alternating permutations:
- \(\mathbf{v}_1 = (+1, -1, -1)\)
- \(\mathbf{v}_2 = (-1, -1, +1)\)
- \(\mathbf{v}_3 = (-1, +1, -1)\)
- \(\mathbf{v}_4 = (+1, +1, +1)\)
To calculate the gradient vector, each vertex direction \(\mathbf{v}_i\) is scaled by the distance field value sampled at that offset:
\[\nabla f(\mathbf{p}) \approx \sum_{i=1}^{4} \mathbf{v}_i f(\mathbf{p} + \epsilon \mathbf{v}_i)\]
Summing these weighted directional vectors reconstructs the true directional gradient. Dividing the final vector by its length produces the unit surface normal.
GLSL Implementation
The four vertices can be generated efficiently using GLSL vector
swizzling from a single vec2(1.0, -1.0).
// User-defined SDF scene evaluation function
float map(vec3 p);
vec3 calcNormalTetrahedron(vec3 p) {
const float eps = 0.0005; // Sampling step size
const vec2 k = vec2(1.0, -1.0);
return normalize(
k.xyy * map(p + k.xyy * eps) +
k.yyx * map(p + k.yyx * eps) +
k.yxy * map(p + k.yxy * eps) +
k.xxx * map(p + k.xxx * eps)
);
}Breakdown of the Code:
k.xyyresolves tovec3(1.0, -1.0, -1.0)k.yyxresolves tovec3(-1.0, -1.0, 1.0)k.yxyresolves tovec3(-1.0, 1.0, -1.0)k.xxxresolves tovec3(1.0, 1.0, 1.0)
Each vector represents one vertex of the tetrahedron. The scene
function map is evaluated at the displaced positions,
multiplied by the respective direction vectors, accumulated, and finally
normalized.
Performance and Accuracy Considerations
- Computational Savings: Reducing scene evaluations from six to four yields an immediate 33% reduction in normal estimation cost.
- Step Size (\(\epsilon\)): Because the sampling
vectors \(\mathbf{v}_i\) have a length
of \(\sqrt{3} \approx 1.732\), the
effective sample distance from \(\mathbf{p}\) is \(\epsilon\sqrt{3}\). If sharp edges show
slight smoothing or artifacts, lower the
epsconstant slightly compared to a Cartesian difference step. - Rotational Invariance: Unlike forward differences (which use only 4 taps along standard axes but produce asymmetric artifacts), the tetrahedron method distributes error symmetrically across three dimensions, preventing axis-aligned lighting bias.