For a recent project, we needed to “simulate” or “fake” the look of pieces of paper in a physical environment. We didn’t need fancy physical modeling or curves and curls and folds, just perspective and shadowing during an animation. With OpenGL, perspective and shadowing are dead simple, but animation is hard. With Core Animation, perspective and animation are dead simple, but shadowing is hard. Being loathe to work with a lower-level framework when a high-level one will do, we put together a little CALayer subclass that knows how to render sufficiently accurate shadows on its surface. Note that it doesn’t do inter-object shadows, just shading based on its angle in space. Since the math is essentially the same, we decided to throw in specular highlights free of charge.
How Does It Work?
The math more or less uses the Lambert illumination model. The key concept is that the illumination of a surface is related to the angle between the incoming light ray and the normal of the surface at that point. (Since layers are by definition planar, this is the normal of the layer.) All we do is calculate the dot product of the incoming light with the normal of the layer, and scale the shadow’s strength by that value. (For simplicity, we model the light as a directional source, shooting into the screen.) This means that the closer the layer is to facing you full-on, the less visible the shadow is. The specular highlight works the same way, except it has a sharper falloff: exponential rather than linear.
I say “more or less” because in the true illumination model, this math would change the color of the surface. We can’t do that for a number of reasons — primarily because you can set an image or other content in a CALayer, and we don’t want to wipe it out — so instead we put a shadow layer and a highlight layer on top of the base layer, and change their opacity.
To get the shadow and highlight to do the right thing during an implicit animation, we had to find a way to recalculate their opacities during each frame. If we had followed the naive approach, we would have missed changes that occur when, for instance, the layer rotates from facing left to facing right by going through facing center. In that case the shadow should go from dark to light to dark, but an implicit animation would simply go from dark to dark. Instead, we define our own internal animatable property, tell the layer it needs to re-display when that property changes, and then recalculate the shadow math rather than drawing anything in the
We also put in a method to allow users to bypass this extra math, if they know their transform animation won’t cause this kind of nonlinear change in the shading.
Enough Talk, Let’s See It
(Why is there a slight stutter in that video? Because I did not take the time to do this.)