Wednesday, November 21, 2012

Game-off: Day 18

You're not seeing things; I actually did skip a day of my daily blog! Oh, I'm so ashamed. :(

My only excuse is that I was preparing for a flight, while studying some vector mathematics so that I could do simple 2D shadow-casting. So I slept only 4 hours, then I started coding on the plane. Welp, it just started working about an hour ago!

It works pretty well, and the performance is even great on iPhone 4S.

It's important to note this is not dynamic lighting; it's faked using canvas clipping regions to draw only outside of the "shadow" area. There are a couple of known bugs: First, the shadow will be cast from an incorrect vector when the lightsource is very near to a solid shape. And second, when two shadows overlap, it will display as a beam of light instead.

The first bug seems like it will be kind of easy to solve; The shadow projection is done from the body's center of mass. It should really be projected from the vector nearest to the lightsource. The second bug may be very difficult to fix. I'll have to investigate some options for that*.

The difficult part of implementing this was getting the math right. It essentially just casts a bunch of rays at the solid objects within the light radius (using Chipmunk-js vector math functions). To calculate the width of the shadow, I used the vector normal (perpendicular to) the axis between the lightsource and the solid shape. A dot product on each of the vectors within the shape creates a projection on that vector normal, and we can use these projections to find which vectors project the farthest distance apart. (This is very similar to the projection step used in the Separating Axis Theorem for collision detection with convex polygons.) Then just cast some rays away from the lightsource, starting at the two vectors at the edges of the shadow.

* The "beam of light bug" is caused by using the clipping region to "render" the shadows. There's a default region which covers the canvas, and then each shadow is created as a polygon with reverse winding, so that the areas covered by shadow won't be drawn when I fill the radial gradient. Any reverse-winding sub-paths that overlap will alternate between hiding and showing the pixels within those regions.

How to fix it? With a single lightsource, I can just use point queries to detect whether a vector on a shadow appears within another shadow. But with multiple lightsources, things will get too complex to solve, I'm afraid. Maybe there's a way to fix it by fiddling with different windings?

A better option may be using globalCompositeOperation instead of clip. ;)

In any case, it's a good start, no?

No comments: