← Projects

2D raytracer

January 2025 · C++, SFML, Computer graphics

Code

How do you draw a shadow? The surprising answer is that you don’t have to. If you draw the light instead, the shadow is what stays dark.

This small C++ program does exactly that. A sun sits in an 800 × 600 window with two circular obstacles. The sun sends out rays in all directions, and every ray stops at the first obstacle it touches. There is no line of code about shadows. You can drag all three circles with the mouse, and the rays are recomputed every frame, so the shadows follow right away.

The sun, two obstacles and the shadows behind them Fig. 1: The program running. Rays leave the orange sun and stop at the blue and green obstacles. The black areas behind them are simply places no ray reaches.

How it works

The sun sends out 360 rays, one per degree. Ray ii starts at the edge of the sun, which has centre cc and radius R=40R = 40, and points in the direction

di=(cos⁡θi, sin⁡θi),θi=i⋅1∘d_i = (\cos\theta_i,\ \sin\theta_i), \qquad \theta_i = i \cdot 1^\circ

Because did_i has length 1, adding it to a point moves that point exactly one pixel along the ray. That gives the whole algorithm: start at the edge of the sun, take one step, and check whether you are inside an obstacle. After kk steps the ray has reached

pk=c+(R+k) dip_k = c + (R + k)\, d_i

A point is inside a circle with centre oo and radius rr if its distance to the centre is less than rr. Comparing the squared distances does the same job without a square root:

(px−ox)2+(py−oy)2<r2(p_x - o_x)^2 + (p_y - o_y)^2 < r^2

The first step that passes this test ends the ray. If no step passes it, the ray runs out after 800 steps. Strictly speaking, this walking is called ray marching. A classic raytracer would compute the exact intersection point of ray and circle instead.

Here is that loop from updateRays(). stepIncrement works out to the direction vector again, so each step has length 1:

sf::Vector2f direction(std::cos(radian), std::sin(radian));
sf::Vector2f rayEnd = circleCenter + direction * RAY_LENGTH;

float xStep = (rayEnd.x - circleCenter.x) / RAY_LENGTH;
float yStep = (rayEnd.y - circleCenter.y) / RAY_LENGTH;

sf::Vector2f stepIncrement = {xStep, yStep};
sf::Vector2f stepPosition = circleCenter + direction * m_sun.getRadius();

for (int j = 0; j < RAY_LENGTH; j++) {
  stepPosition += stepIncrement;

  if (isPointInCircle(stepPosition, m_obstacle.getPosition(), m_obstacle.getRadius()) ||
      isPointInCircle(stepPosition, m_obstacle2.getPosition(), m_obstacle2.getRadius())) {
    rayEnd = stepPosition;
    break;
  }
}

The same point-in-circle test also decides which circle the mouse has grabbed:

bool isPointInCircle(sf::Vector2f point, sf::Vector2f circlePosition, float radius) {
  float dx = point.x - circlePosition.x;
  float dy = point.y - circlePosition.y;
  return (dx * dx + dy * dy) < (radius * radius);
}

The approach is simple, and that has a price. In the worst case, when no ray hits anything, one frame takes 360×800=288,000360 \times 800 = 288{,}000 steps, each with two circle tests.

Try it

This demo runs the same algorithm in your browser, with the same ray count, step size and circle sizes. It opens with a scene like the one in Fig. 1.