Casting Rays

A friend of mine was playing with some shadow casting. He requested help with a bit of the math to cast a line from the center of a light to a point to the edge.

In short, here’s a picture:

Shadow cast from a point light to a point.

Shadow cast from a point light to a point.

There’s no need to use trigonometry to solve this problem. All we need to do is solve some simple vector math problems. I’ll try and run through step by step, then give the code at the end. For those with programming inclination, when I say, foo.xy /= 3, that means foo.x /= 3 and foo.y /= 3. When I say foo.xy + bar.xy, it means foo.x + bar.x and foo.y + bar.y.

Let’s begin.

The distance between the light and the point forms a line. The line can be represented as a vector. We can take the vector, found by point.xy – light.xy, and stretch it from the point to the edge of the light.

point.xy – light.xy translates the point into a zero-centered coordinate system. That is, instead of having a light at <123, 456> and a point at <124, 457>, we’re assuming the light is at zero and moving the light appropriately. That is, the light would be at <0,0> and the point at <1,1>. It means we can use a few neat math tricks like ‘normalization’.

So why not just take that vector (found with e.xy – l.xy) and apply it again? “Because it doesn’t reach the edge then! Or it stretches past!”

shadow_unnormalized

Ah! So we normalize it so that the slope is the same (e.xy – l.xy)/magnitude(e.xy-l.xy) and then multiply by the magnitude of the distance between the edge of the circle and the point. c.xy = magnitude(l.radius – magnitude(e.xy-l.xy))*(e.xy-l.xy)/magnitude(e.xy-l.xy);

“Whoa. Wait. How’s that again?”

Intuitively, imagine a line from the center of the light to a point that’s exactly one unit away. Assume the center is at (0,0).

L.........P
0.........1.........2.........3

If the ‘light radius’ is 3, then all you have to do is multiply the ‘unit length’ by three to reach the edge. Since our point is already one unit away from the center, we just have to multiply by (3 – 1) = 2!

L-------->P------------------>|
0.........1.........2.........3

If we treat the distance from the light to the point as ‘normalized’ (as though it were 1), then we can multiply it by the distance from the point to the edge of the circle and it will fill exactly that space.

That’s all there is to it. Apply these steps and you can cast a line from a point to the edge of a circle.

shadow_renormalized

Here’s the full source:

import javax.swing.*;
import java.awt.*;

/**
 * Created by jcatrambone on 2013/12/20.
 */
public class MainApp extends JFrame {
	public Vector2f lightCenter;
	public float lightRadius;
	public float lightSize = 3.0f; // This is just for display purposes.  How 'big' the light appears.
	public Vector2f entityCenter;
	public float entitySize = 1.0f;

	public static void main(String[] args) {
		MainApp app = new MainApp();

		app.lightCenter = new Vector2f(200f, 200f);
		app.lightRadius = 200f;
		app.entityCenter = new Vector2f(280f, 280f);

		app.setSize(600, 600);
		app.setVisible(true);
	}

	@Override
	public void paint(Graphics g1d) {
		Graphics2D g = (Graphics2D)g1d;
		g.setColor(Color.black);
		g.fillRect(0, 0, 600, 600);

		// Draw the outside boundary of the light.
		g.setPaint(Color.yellow);
		g.drawOval((int)(lightCenter.x-lightRadius), (int)(lightCenter.y-lightRadius), (int)(lightRadius*2), (int)(lightRadius*2));

		// Draw the center of the light.
		g.setPaint(Color.yellow);
		g.drawOval((int) (lightCenter.x - lightSize), (int) (lightCenter.y - lightSize), (int) (lightSize * 2), (int) (lightSize * 2));

		// Draw the entity edge.
		g.setPaint(Color.green);
		g.drawOval((int)(entityCenter.x - entitySize), (int)(entityCenter.y - entitySize), (int)(entitySize*2), (int)(entitySize*2));

		// Draw the important shadow!
		// Shadow starts at the entity, so we only need to calculate the line from the entity to the edge.
		// Translate the entity into the -1,1 range, centered at zero.
		Vector2f entityLocalSpace = entityCenter.minus(lightCenter);
		// Normalize the edge to preserve the angle.
		Vector2f normalizedEdge = entityLocalSpace.getNormalized();
		// Now stretch this to the edge of the circle.  Since the edge is normalized, it has length 1,
		// so all we have to do is multiply by the length of a line from the entity to the edge of the light.
		Vector2f shadowLine = normalizedEdge.multiply(lightRadius - entityLocalSpace.getMagnitude());
		// c.xy = magnitude(l.xy-e.xy)*(e.xy-l.xy)/magnitude(e.xy-l.xy);
		// Whoops.  That first magnitude should be magnitude(l.radius - magnitude(e.xy-l.xy))
		g.setPaint(Color.green);
		g.drawLine((int)entityCenter.x, (int)entityCenter.y, (int)(entityCenter.x+shadowLine.x), (int)(entityCenter.y+shadowLine.y));
	}
}

class Vector2f {
	public float x, y;

	public Vector2f() {
		x=0; y=0;
	}

	public Vector2f(float x, float y) {
		this.x = x;
		this.y = y;
	}

	public Vector2f getNormalized() {
		float mag = this.getMagnitude();
		return new Vector2f(this.x/mag, this.y/mag);
	}

	public float getMagnitude() {
		return (float)Math.sqrt(x*x + y*y);
	}

	public Vector2f add(Vector2f v) {
		return new Vector2f(this.x + v.x, this.y + v.y);
	}

	public Vector2f multiply(float scalar) {
		return new Vector2f(this.x * scalar, this.y * scalar);
	}

	public Vector2f minus(Vector2f v) {
		// foo.minus(bar) = foo - bar
		return new Vector2f(this.x - v.x, this.y - v.y);
	}
}

Comments are closed.