Ray Marching Soft Shadows in 2D (2020)
Recorded: Nov. 28, 2025, 1:02 a.m.
| Original | Summarized |
Ray Marching Soft Shadows in 2D – Ryan Kaplan Ryan Kaplan About Ray Marching Soft Shadows in 2D Sep 23, 2020 Disclaimer: the demos on this page use WebGL features that aren’t available on some mobile devices. Under the hood it uses something called a distance field. A distance field is an image like the one below that tells you how far each pixel is from your shape. Light grey pixels are close to the shape and dark grey pixels are far from it. When the demo starts up, it draws some text on a 2D canvas and generates a distance field of it. It uses a library I wrote that generates distance fields really quickly. If you’re curious how the library works, I wrote about that here. If the ray intersects a glyph, the pixel we’re shading must be in shadow because there’s something between it and the light. Now we’re a little closer to the light. We repeat the process until we hit the ascender of the b! If the b glyph weren’t there, we’d have kept going until we hit the light. Below is GLSL to implement this technique. It assumes you’ve defined a function getDistance that samples the distance field. float rayProgress = 0; float sceneDist = getDistance( rayProgress += sceneDist; It turns out that some pixels are really expensive to process. So in practice we use a for-loop instead of a while loop – that way we bail out if we’ve done too many steps. A common “slow case” in ray marching is when a ray is parallel to the edge of a shape in the scene… The approach I’ve described so far will get you a scene that looks like the one below. It’s cool, but the shadows are sharp which doesn’t look very good. The shadows in the demo look more like this… One big disclaimer is that they’re not physically realistic! Real shadows look like hard shadows where the edges have been fuzzed. This approach does something slightly different: all pixels that were previously in shadow are still fully in shadow. We’ve just added a penumbra of partially shaded pixels around them. This is cheap to compute because the variable sceneDist tells us how far we are from the closest shape at each ray marching step. So the smallest value of sceneDist across all steps is a good approximation for the yellow and green lines in the image above. Consider two pixels along the ray above. One is closer to the almost-intersection and is lighter (its distance is the green line). The other is farther and darker (its distance is the yellow line). In general: the further a pixel is from its almost intersection, the more “in shadow” we should make it. // `getDistance` samples our distance field texture. lightContribution = min( rayProgress += sceneDist; // Ray-marching took more than 64 steps! This ratio feels kind of magical to me because it doesn’t correspond to any physical value. So let’s build some intuition for it by thinking through why it might take on particular values… If sceneDist / rayProgress >= 1, then either sceneDist is big or rayProgress is small (relative to each other). In the former case we’re far from any shapes and we shouldn’t be in shadow, so a light value of 1 makes sense. In the latter case, the pixel we’re shadowing is really close to an object casting a shadow and the shadow isn’t fuzzy yet, so a light value of 1 makes sense. The ratio is 0 only when sceneDist is 0. This corresponds to rays that intersect an object and whose pixels are in shadow. And here’s a demo of what we have so far… Rule #3 is the most straightforward one: light gets weaker the further you get from it. // fadeRatio is 1.0 next to the light and 0. at // We'd like the light to fade off quadratically instead of // `getDistance` samples our distance field texture. lightContribution = min( rayProgress += sceneDist; // Ray-marching took more than 64 steps! I forget where I found this soft-shadow technique, but I definitely didn’t invent it. Inigo Quilez has a great post on it where he talks about using it in 3D. Please enable JavaScript to view the comments powered by Disqus. Back home |
Ryan Kaplan’s “Ray Marching Soft Shadows in 2D” details a technique for generating soft shadows in 2D graphics, utilizing a distance field and a core ray marching algorithm. The overarching goal, driven by Kaplan, is to create visually appealing shadows without the sharp, often unrealistic edges produced by traditional shadow casting methods. This approach leverages a distance field, an image that dictates the distance to the nearest shape, as the foundation for the process. The system begins by generating a distance field itself, typically using a library Kaplan developed. This distance field represents the scene, allowing the system to quickly determine the distance to the nearest glyph (shape). The lighting scheme then employs ray marching – a process where a ray originates from each pixel and moves along its direction until it hits either a glyph or the light source. If the ray intersects a glyph, the pixel is considered in shadow because it’s blocked by the shape. The key to creating soft shadows lies in the iterative ray marching process. Rather than a simple, linear movement, the ray advances by the distance field’s value, ‘sceneDist,’ at each step. This allows the algorithm to ‘march’ directly to the nearest shape, rather than jumping randomly. Kaplan highlights the importance of minimizing the number of steps taken for each ray, acknowledging that a significant portion of the code’s performance depends on ray marching optimization. To achieve more realistic shadows, Kaplan introduces three rules that govern how the shadow is applied to a given pixel: Rule 1 dictates that pixels closer to the closest intersection with a shape should be more shadowed, Rule 2 suggests that shadow spreads out further away from the closest intersection, and Rule 3 incorporates light fading as the ray progresses away from the light source. These rules, combined with the distance field, produce softer shadows that resemble real-world shadows with a degree of diffusion. However, Kaplan acknowledges a significant limitation – banding artifacts. This issue arises from the assumption that the smallest value from across all steps within the ray marching process serves as a precise approximation for the distance to the scene. Due to the relatively small number of steps taken, this approximation can be inaccurate, particularly when a ray’s path is parallel to the edge of a shape. To mitigate this, Kaplan employs several techniques: first, using a more sophisticated approximation as described by Inigo Quilez, and secondly, introducing a “random jitter” factor, where the ray advances by a value that varies slightly from step to step, reducing the potential for banding. This introduces a degree of granularity to the final image. He also considers the user's perspective, offering a discussion of how the technique was developed and how it can be improved, inviting feedback and suggestions for future revisions. |