Ray Marching and Distance Fields: Crafting Beautiful Text Shadows in WebGL
Share this article
Ray Marching and Distance Fields: Crafting Beautiful Text Shadows in WebGL
In the world of computer graphics, creating realistic and visually appealing effects often comes at a computational cost. However, as demonstrated by a recent graphics project by Ryan Kaplan, clever algorithms can achieve stunning results with surprising efficiency. This article explores the technical wizardry behind rendering text with soft shadows using distance fields and ray marching in WebGL.
Understanding Distance Fields
At the heart of this technique lies the concept of a distance field. A distance field is essentially an image where each pixel's value represents the distance to the nearest edge of a shape. Light pixels indicate proximity to the shape's boundary, while dark pixels signify greater distance.
[Image: <figure class="embedded-image">
<img src="https://news.lavx.hu/api/uploads/ray-marching-and-distance-fields-crafting-beautiful-text-shadows-in-webgl_20251127_081723_ray-marching-and-distance-fields-crafting-beautiful-text-shadows-in-webgl_1.jpg"
alt="Article illustration 1"
loading="lazy">
</figure> - Distance field visualization]
When the demo initializes, it first renders text on a 2D canvas and then generates a distance field from this text. As Kaplan explains, "It uses a library I wrote that generates distance fields really quickly." This preprocessing step is crucial, as it transforms the text into a data structure that enables efficient ray-based calculations.
Ray Marching Fundamentals
The lighting scheme works by casting rays from each pixel toward the light source and determining whether these rays intersect with any glyphs. A naive approach would involve checking each pixel along the ray in small increments, but this would be computationally expensive.
Ray marching provides an elegant solution. Instead of moving in fixed increments, the algorithm uses the distance field to determine how far it can safely advance along the ray without intersecting any shapes. Kaplan illustrates this process:
- Start at the pixel being shaded and cast a ray toward the light.
- Query the distance field to determine the minimum distance to any shape.
- Advance along the ray by this distance.
- Repeat until either the light is reached or a shape is intersected.
This approach ensures we never skip over small shapes while still making efficient progress toward the light source.
Implementing Ray Marching
The GLSL implementation of this technique is remarkably concise:
vec2 rayOrigin = ...;
vec2 rayDirection = ...;
float rayProgress = 0;
while (true) {
if (rayProgress > distance(rayOrigin, lightPosition)) {
// We hit the light! This pixel is not in shadow.
return 1.;
}
float sceneDist = getDistance(
rayOrigin + rayProgress * rayDirection);
if (sceneDist <= 0.) {
// We hit a shape! This pixel is in shadow.
return 0.;
}
rayProgress += sceneDist;
}
In practice, Kaplan notes, a for-loop is used instead of a while-loop to prevent infinite iterations in edge cases. This implementation provides a foundation for determining whether a pixel is in shadow, but the resulting shadows are too sharp to look realistic.
Creating Soft Shadows with Three Rules
To achieve the soft, penumbral shadows seen in the demo, Kaplan employs three key rules:
Rule 1: Proximity-based Shadowing
The closer a ray gets to intersecting a shape, the more shadowed its corresponding pixel should be. This is efficiently calculated by tracking the minimum value of sceneDist across all ray marching steps.
Rule 2: Distance-based Shadow Spread
Pixels farther from the point where their ray almost intersects a shape should have more spread-out shadows. This is determined by the ratio of sceneDist to rayProgress.
Rule 3: Distance-based Light Attenuation
Light naturally diminishes with distance, so a distance factor is applied that decreases quadratically as we move away from the light source.
Combining these rules, the shadow calculation becomes:
vec2 rayOrigin = ...;
vec2 rayDirection = ...;
float rayProgress = 0.;
float stopAt = distance(samplePt, lightPosition);
float lightContribution = 1.;
for (int i = 0; i < 64; i++) {
if (rayProgress > stopAt) {
return lightContribution;
}
float sceneDist = getDistance(
rayOrigin + rayProgress * rayDirection);
if (sceneDist <= 0.) {
return 0.;
}
lightContribution = min(
lightContribution,
sceneDist / rayProgress
);
rayProgress += sceneDist;
}
return 0.;
Addressing Banding Artifacts
As Kaplan discovered, this approach can produce banding artifacts due to the discrete nature of ray marching steps. To mitigate this, he implemented two techniques:
- An improved approximation for the distance from a ray to the scene, as suggested by graphics expert Inigo Quilez.
- Random jittering of ray marching steps by multiplying
sceneDistby a random value between 0 and 1.
The jittering approach adds more steps to the ray march and ensures adjacent pixels don't fall into the same band, though it introduces a slight graininess that Kaplan is still working to improve.
The Beauty of Efficient Algorithms
What makes this technique particularly impressive is its balance between visual quality and computational efficiency. By leveraging the properties of distance fields and the cleverness of ray marching, the algorithm produces visually appealing soft shadows without resorting to more expensive global illumination techniques.
Kaplan's work demonstrates how understanding fundamental graphics concepts can lead to elegant solutions that push the boundaries of what's possible in real-time rendering. As he notes, "The ratio feels kind of magical to me because it doesn't correspond to any physical value," yet it produces visually convincing results.
The Future of Real-Time Graphics
This implementation is part of a broader trend in computer graphics where algorithms are being optimized to deliver increasingly sophisticated visual effects in real-time. Techniques like distance fields and ray marching are becoming essential tools in the graphics programmer's toolkit, enabling everything from dynamic text rendering to complex 3D scenes.
For developers interested in exploring these techniques further, Kaplan's blog post provides an excellent starting point. As he concludes, "Overall my demo has a few extra tweaks that I might write about in future but this is the core of it."
The intersection of mathematical elegance and practical implementation is what makes computer graphics such a fascinating field. By combining deep theoretical understanding with creative problem-solving, developers like Kaplan continue to push the boundaries of what's possible in real-time rendering.
This article is based on Ryan Kaplan's blog post "Distance Fields" available at https://www.rykap.com/2020/09/23/distance-fields/.