SFSS

Super Fast Soft Shadows

History

I've been developing and improving the SFSS algorithm for well over a decade already. My first taste of 2D shadows came from the game Gish. One of the many clever things about the game was it's use of dynamic lighting and shadows in a 2D game. I figured out how to duplicate the effect using a perspective matrix that would push the far edges of the shadows off to infinity, and then storing the mask in the stencil buffer. Cheap and effective hard shadows. When the iPhone SDK first came out I used this technique to do realtime shadows in a little game called Twilight Golf. We sort of joked that we were the first iPhone game to do realtime shadows (2D or otherwise), but I think it was actually true.

Gish Screenshot Twilight Golf Screenshot

Left: Gish, Right Twilight Golf

There were other similar algorithms at the time that did soft shadows by drawing penumbra "fins" around hard shadows, but I didn't like visual artifacts as the fins popped from one vertex to the next. There were algorithms that used post process blurring to soften the shadows at a very high fillrate cost. Other algorithms Another algorithm I've seen operated in polar space which is clever. Raymarching based algorithms allow for pretty accurate shadows. The downside of a lot of these algorithms is that they are very slow, with a very high per pixel or per light cost.

What I really wanted was to extend the classic hard shadowing algorithm. It seemed like it should be possible to use shaders to change the coverage of the projected quad to cover the whole penumbra and then calculate the amount of occlusion per pixel.

Goals

I came up with the current SFSS algorithm while working on the mobile focused Cocos2D-SpriteBuilder engine. Expensive fragment shaders and lots of texture samples were simply not possible. With the target hardware in mind, my goals were pretty clear cut:

On the other hand, there were several non-goals that might be non ideal for some projects:

With that in mind, lets dig into the details.

Algorithm

The first step is to project geometry that covers the entire shadow including the penumbra. In the classic hard shadow algorithm, you project a second set of vertexes away from the light's center. By joining the regular vertexes with the projected vertexes you extrude each of the shadow outline segments into a quad. To cover the whole penumbra, you need to project the vertexes away from the edges of the light instead of the center.

Vertex Geometry

You need to find a tangent line on the surface of the light that goes through the segment's vertex. A simpler and more robust solution is to pick a vector perpendicular to the vector going from the center to the vertex. If you use a consistent winding for your shadow polygons, you can easily pick the correct direction to go. The following snippet calculates the offset for both segment endpoints.

vec2 lightOffsetA = vec2(-radius,  radius)*normalize(segmentA).yx;
vec2 lightOffsetB = vec2( radius, -radius)*normalize(segmentB).yx;

Given these two offsets, you know what point to project the segment endpoints away from.

Segment Geometry

I've seen several articles on 2D shadows that project the far edges of the shadow away a specific distance or maybe a multiplier. Yet another did the work to figure out how far to project the vertexes to ensure the quad would fill the screen. There is a trick that makes this way easier though. Project them to infinity! If you are familiar with homogenous coordinates, you've probably heard that a point is represented as (x, y, z, 1) and a vector as (x, y, z, 0). Did you know that if you pass a vector like that as a vertex position that the GPU will treat it as if it's infinitely far away but in the direction of the vector? That's exactly what you want to do here. Simply subtract the light offset from the vertex position and output 0 for the w-coordinate when returning the vertex position in the vertex shader.

Now that we have a quad that covers the entire shadow of the segment, we can calculate the geometry of the penumbra. I render the penumbra as a simple

Penumbra Geometry

TODO