Why Glancing Rays Are Expensive in Sphere Tracing

EN NLESPT-BR


Sphere tracing over a signed distance field is efficient because the field tells each ray exactly how far it can jump without missing a surface. But that efficiency is not uniform. A ray aimed directly at a surface converges in a handful of steps, while a ray that skims nearly parallel to the same surface can chew through the entire step budget before it makes a hit or escapes. This article explains the geometry behind that disparity, derives the relationship between ray angle and step count, and covers practical ways to keep performance under control.

If you are new to the technique, start with the ray marching overview for a high-level interactive introduction to signed distance fields, sphere tracing, and SDF shading.

The Geometry of Step Efficiency

Consider a ray approaching an infinite flat plane. The plane sits at y=0y = 0 and the ray starts at height hh above it, pointing downward at an angle θ\theta measured from the plane. A ray pointed straight down has θ=90\theta = 90^\circ (perpendicular). A ray that barely grazes the surface has θ\theta close to 00^\circ.

The signed distance from any point (x,y)(x, y) to the plane is simply yy. At the starting point, the distance is hh. If the ray points straight down, the first step moves the full distance hh and the ray lands exactly on the surface. One step, done.

Now tilt the ray so it makes a shallow angle θ\theta with the plane. The first step still moves a distance hh, but it moves along the ray direction, not straight down. The vertical component of that step is hsinθh \sin\theta. So after step one, the ray is at height:

y1=hhsinθ=h(1sinθ)y_1 = h - h \sin\theta = h(1 - \sin\theta)

The SDF at the new position returns y1y_1, so step two moves a distance y1y_1, and the vertical component of that step is y1sinθy_1 \sin\theta. The height after step two is:

y2=y1y1sinθ=h(1sinθ)2y_2 = y_1 - y_1 \sin\theta = h(1 - \sin\theta)^2

After nn steps the height is:

yn=h(1sinθ)ny_n = h (1 - \sin\theta)^n

The ray has hit the surface when yny_n drops below the hit threshold ε\varepsilon. Solving h(1sinθ)nεh (1 - \sin\theta)^n \leq \varepsilon for nn:

nln(ε/h)ln(1sinθ)n \geq \frac{\ln(\varepsilon/h)}{\ln(1 - \sin\theta)}

This is the key formula. When θ\theta is small, sinθ\sin\theta is small, so (1sinθ)(1 - \sin\theta) is close to 1, and the denominator ln(1sinθ)\ln(1 - \sin\theta) is a small negative number. The step count blows up.

Head-On vs Glancing: A Concrete Comparison

Set h=10h = 10, ε=0.001\varepsilon = 0.001, and compute the step count for several angles.

For a perpendicular ray, θ=90\theta = 90^\circ, sin(90)=1\sin(90^\circ) = 1, and (1sinθ)=0(1 - \sin\theta) = 0. The formula gives n=1n = 1 step: the ray lands on the surface immediately.

For θ=30\theta = 30^\circ, sin(30)=0.5\sin(30^\circ) = 0.5, and (10.5)=0.5(1 - 0.5) = 0.5. The step count is:

n=ln(0.001/10)ln(0.5)=ln(0.0001)ln(0.5)=9.210.69313n = \frac{\ln(0.001/10)}{\ln(0.5)} = \frac{\ln(0.0001)}{\ln(0.5)} = \frac{-9.21}{-0.693} \approx 13

Thirteen steps is still reasonable, but already an order of magnitude more than the perpendicular case.

For θ=5\theta = 5^\circ, sin(5)0.0872\sin(5^\circ) \approx 0.0872, and (10.0872)=0.9128(1 - 0.0872) = 0.9128:

n=ln(0.0001)ln(0.9128)=9.210.0912101n = \frac{\ln(0.0001)}{\ln(0.9128)} = \frac{-9.21}{-0.0912} \approx 101

Over a hundred steps to reach a flat plane, simply because the ray is coming in at a shallow angle.

For θ=1\theta = 1^\circ, sin(1)0.01745\sin(1^\circ) \approx 0.01745, and (10.01745)=0.98255(1 - 0.01745) = 0.98255:

n=9.21ln(0.98255)=9.210.0176523n = \frac{-9.21}{\ln(0.98255)} = \frac{-9.21}{-0.0176} \approx 523

Five hundred steps for a single ray. In a shader that budgets 64 or 128 steps per ray, this ray never reaches the surface before the loop exits. The result is a visible artifact: a hard cutoff where glancing surfaces go black or show the background instead of the expected surface color.

The table below summarizes the step count across the full range, assuming h=10h = 10 and ε=0.001\varepsilon = 0.001:

Angle θ\thetasinθ\sin\thetaSteps to hitNotes
90°1.01Perpendicular, optimal
60°0.8664Still fast
45°0.7077Manageable
30°0.513Tolerable
15°0.25931Warning zone
10°0.17448Getting expensive
0.0872101Already heavy
0.0349261Likely exceeds budget
0.01745523Almost certainly misses

The growth is not linear. As θ\theta approaches zero, the step count grows roughly like 1/θ1/\theta. That is a divergence, not a gradual increase.

Why It Happens: Shrinking Safe Steps

The underlying mechanism is simple, but worth stating explicitly. The SDF at each sample point returns the perpendicular distance to the nearest surface. When the ray is nearly parallel to the surface, that perpendicular distance changes slowly as the ray advances. Each step shrinks the distance only by the sine of the angle, so the step size decays geometrically instead of collapsing in one jump.

You can see this directly in the recurrence. Each step reduces the remaining distance by the factor (1sinθ)(1 - \sin\theta). When that factor is close to 1, each step makes almost no progress toward the surface. The ray is essentially crawling along.

This is why the maximum step count in a shader loop matters so much. It is not there just to bound work for rays that miss the scene entirely. It is there to limit the damage from glancing rays that do eventually hit, but take an unreasonable number of steps to do so.

Curved Surfaces Make It Worse

A plane was the simplest case. Curved surfaces can produce even worse step behavior because the SDF surface is not flat and the nearest-surface distance can stay small over a long arc.

Take a large circle of radius RR centered at the origin, and a ray that passes close to the circle without entering it. The ray starts far from the circle, so the initial steps are large. But as the ray approaches the closest point on the circle, the distance to the surface shrinks, and the ray direction becomes nearly tangent to the circle. The distance stays small over a long travel arc, forcing many small steps before the ray either hits or clears the curvature and the distance grows again.

The same geometry appears with any convex surface. The worst offender in practice is a ray that nearly grazes the “silhouette” of a sphere or cylinder in 3D. Viewed from the camera, the ray is aimed at a point on the sphere where the surface normal is almost perpendicular to the view direction. At that grazing angle, the sphere tracing step size becomes tiny over many consecutive steps, burning through the step budget.

Concave corners create a related but distinct problem. When a ray enters a narrow crevice between two surfaces, the SDF returns a small value from both sides. The ray direction may not point strongly toward either surface, so the step size stays clamped to roughly the crevice width. The ray inches through, step by step, until it either bottoms out or exits. The step count here is proportional to the crevice depth divided by the crevice width, which can be large for deep, narrow features.

The Fixed-Step Baseline

It is instructive to compare glancing-ray behavior in sphere tracing with naive fixed-step marching. A fixed-step marcher with step size Δs\Delta s needs exactly:

nfixed=hΔssinθn_{\text{fixed}} = \frac{h}{\Delta s \sin\theta}

steps to reach a plane at angle θ\theta. This also diverges as θ0\theta \to 0, but the cause is different. In fixed-step marching, every step is equally small regardless of how far the surface is, so the problem is pure geometry: a shallow angle means the vertical progress per step is Δssinθ\Delta s \sin\theta, which is tiny. Sphere tracing shares that geometric factor, but it adds a compounding effect: the step size itself shrinks as the ray gets closer to the surface, because the SDF returns smaller values.

So sphere tracing is actually worse than fixed-step marching for glancing rays, once the ray is close to the surface. A fixed-step marcher keeps plodding along at constant speed. A sphere tracer decelerates as it approaches, compounding the geometric slowdown with a feedback loop.

This is the essential tradeoff of sphere tracing. It wins dramatically when the ray is far from surfaces and heading roughly toward them, because large safe jumps clear empty space fast. But it loses when the ray is close to a surface and heading along it, because the SDF forces small steps that a fixed-step marcher would not need.

The Step-Budget Tradeoff

Every sphere-tracing implementation chooses a maximum step count. Common values in real-time shaders range from 64 to 256. The choice affects both performance and correctness:

A low budget (32 to 64 steps) keeps each pixel cheap but causes visible artifacts. Glancing surfaces disappear at their edges, producing hard geometric cutoffs where the surface should smoothly continue. Concave details vanish. The image looks clipped.

A high budget (256 to 512 steps) captures more glancing geometry correctly but costs more per pixel, and the worst-case pixels still may not converge. The law of diminishing returns applies: doubling the budget from 128 to 256 buys you a few more degrees of glancing angle, not double the coverage.

No finite budget captures all angles. As θ0\theta \to 0, the step count goes to infinity for any nonzero hh and ε\varepsilon. Every implementation implicitly draws a line in angular space beyond which surfaces are invisible, whether the author intended to or not.

Practical Mitigations

Several techniques reduce the cost of glancing rays without simply raising the step budget.

Relax the Hit Threshold for Shallow Angles

The standard hit threshold ε\varepsilon is typically a fixed small number like 0.0010.001. But the required precision depends on what the pixel sees. When a surface is viewed head-on, a tight threshold matters because the intersection point affects texture coordinates and the surface normal estimation. When the same surface is viewed at a glancing angle, the intersection point matters less because the surface projects to a tiny region on screen. Relaxing ε\varepsilon to, say, 0.010.01 or 0.050.05 for rays with a shallow incidence angle can cut the step count significantly without visible degradation.

A simple version checks the dot product between the ray direction and the estimated surface normal at the current sample point. If the absolute dot product is small, the ray is nearly parallel to the surface and the threshold can be loosened.

Early Termination with a Larger Epsilon

A cruder version of the same idea: use a larger ε\varepsilon globally and accept slightly less precise intersections. This is common in demo-scene shaders where speed matters more than pixel-perfect accuracy. The tradeoff is that thin features and sharp edges soften, and self-shadowing artifacts may appear when the larger threshold causes the shadow ray to intersect the surface it is supposed to start from.

Adaptive Step Count Per Ray

Instead of a uniform step budget for all pixels, allocate more steps to rays that need them and fewer to rays that do not. One approach tracks the step count of neighboring pixels and adjusts per-tile. Another uses a two-pass strategy: a fast pass with a low budget identifies pixels that did not converge, and a second pass with a higher budget re-marches only those pixels.

The downside is implementation complexity and coherence issues on GPU architectures where divergent step counts across a warp or wavefront cause idle lanes.

Distance Estimator Improvements

The standard SDF for a plane is exact. But for many procedural scenes, the distance function is only a lower bound or an estimator. Improving the tightness of that estimator reduces the step count for all rays, but it helps glancing rays disproportionately because they spend more steps in the region where the estimator matters.

If the estimator returns a value that is, say, half the true distance, then every step takes half the optimal jump and the step count doubles. For a glancing ray that already needs 500 steps, that pushes it to 1000. Tightening the estimator cuts that back down.

Scene-Level Design

The most practical mitigation is often scene design. Avoid large flat surfaces viewed at shallow angles when possible. Use slight curvature, bevels, or surface detail that breaks up the grazing condition. A perfectly flat ground plane extending to the horizon is the worst-case input for a sphere tracer. Adding gentle undulation, a slight tilt, or fog that fades distant geometry before the grazing rays run out of steps all help.

Ray Marching with Over-Relaxation

A more exotic approach is to multiply the SDF distance by a factor slightly larger than 1.0, say 1.2 or 1.5. This “over-relaxation” violates the conservative-step guarantee but can dramatically reduce the step count for glancing rays. The risk is overshooting thin surfaces. In practice, a small over-relaxation factor combined with a slightly larger hit threshold often works well for scenes without very thin features, and the speedup on glancing rays can be substantial.

Summary

Glancing rays are expensive in sphere tracing for a clear geometric reason. When a ray runs nearly parallel to a surface, the perpendicular distance that the SDF returns changes slowly as the ray advances. Each step shrinks that distance by only sinθ\sin\theta, producing a geometric decay that requires on the order of 1/θ1/\theta steps to converge. Curved surfaces and concave corners compound the problem by keeping the distance small over long travel arcs.

The step count formula for a flat plane captures the core behavior:

nln(ε/h)ln(1sinθ)n \geq \frac{\ln(\varepsilon/h)}{\ln(1 - \sin\theta)}

A perpendicular ray takes 1 step. A ray at 55^\circ takes over 100. A ray at 11^\circ takes over 500. The divergence is real, not an edge case.

Sphere tracing is still the right algorithm for SDF rendering because it wins so decisively in the common case: large empty regions traversed in a few jumps, with step sizes that adapt automatically to scene detail. But understanding the glancing-ray cost explains why every sphere-tracing shader needs a maximum step count, why silhouettes can look clipped even when the geometry should be visible, and what options exist to push the performance boundary further. If you are building a sphere-tracing renderer, keep the ln(1sinθ)\ln(1 - \sin\theta) relationship in mind. It tells you exactly where your step budget is going.