← Back to blog

The Mathematics of 3D Rendering: The Rendering Equation

Modern video games and CGI in movies look incredibly realistic due to our ability to mathematically simulate how light behaves in the real world. At the heart of this simulation lies The Rendering Equation, introduced by James Kajiya in 1986.

What does it look like?

The Rendering Equation

The fundamental equation that all physical light-transport simulators attempt to solve is:

Lo(x,ωo,λ,t)=Le(x,ωo,λ,t) +Ωfr(x,ωi,ωo,λ,t)Li(x,ωi,λ,t)(ωin)dωiL_o(\mathbf{x}, \omega_o, \lambda, t) = L_e(\mathbf{x}, \omega_o, \lambda, t) \ + \int_{\Omega} f_r(\mathbf{x}, \omega_i, \omega_o, \lambda, t) \, L_i(\mathbf{x}, \omega_i, \lambda, t) \, (\omega_i \cdot \mathbf{n}) \, \operatorname{d} \omega_i

Breaking it down

To a layman, this looks like inscrutable calculus. But mathematically, it’s just an energy balance equation expressing how light bounces off a specific point x\mathbf{x}.

  • Lo(x,ωo,λ,t)L_o(\mathbf{x}, \omega_o, \lambda, t): The total outgoing radiance from point x\mathbf{x} in direction ωo\omega_o. This is essentially what the camera sees.
  • Le(x,ωo,λ,t)L_e(\mathbf{x}, \omega_o, \lambda, t): The emitted radiance. Is the object glowing? E.g., a lightbulb or the sun.
  • Ωdωi\int_{\Omega} \dots \operatorname{d} \omega_i: The integral over the hemisphere Ω\Omega of incoming directions ωi\omega_i. This means we must check every possible angle light could be hitting the point from.
  • fr(x,ωi,ωo,λ,t)f_r(\mathbf{x}, \omega_i, \omega_o, \lambda, t): The Bidirectional Reflectance Distribution Function (BRDF). This defines the material properties. Is it a matte wall or a shiny mirror? It decides what proportion of incoming light from ωi\omega_i bounces toward ωo\omega_o.
  • Li(x,ωi,λ,t)L_i(\mathbf{x}, \omega_i, \lambda, t): The incoming radiance. How much light is actually arriving from direction ωi\omega_i?
  • (ωin)(\omega_i \cdot \mathbf{n}): The attenuation factor (Lambert’s Cosine Law). Light hitting a surface at a steep angle transfers less energy than light hitting straight on. It is the dot product of the incoming light vector and the surface normal n\mathbf{n}.

Why is it so hard to compute?

Look closely at the incoming light term: Li(x,ωi)L_i(\mathbf{x}, \omega_i). Where does that incoming light come from? It comes from other surfaces in the scene.

To know LiL_i for our current point, we must evaluate LoL_o for the surface the light bounced from!

This means the rendering equation is recursive.

L(x)=E(x)+frL(x)cosθdωL(\mathbf{x}) = E(\mathbf{x}) + \int f_r \, L(\mathbf{x}') \, \cos \theta \operatorname{d}\omega

Evaluating an infinite-dimensional integral analytically is impossible for anything but a perfectly empty glowing sphere. Therefore, computer graphics engineers use Monte Carlo integration.

Monte Carlo Path Tracing

Instead of integrating perfectly, we shoot random rays to estimate the integral. The expected value function looks like this:

E[FN]=E[1Ni=1Nf(Xi)p(Xi)]=Ωf(x)dxE[F_N] = E\left[ \frac{1}{N} \sum_{i=1}^N \frac{f(X_i)}{p(X_i)} \right] = \int_{\Omega} f(x) \,dx

By randomly bouncing light rays (Paths) around the scene thousands of times per pixel (NN), the image converges to the correct solution.

// A highly simplified C++ pseudocode for Path Tracing
Vec3 trace_ray(Ray ray, int depth) {
    if (depth > MAX_BOUNCES) return Vec3(0, 0, 0);

    HitRecord rec;
    if (world.hit(ray, 0.001, infinity, rec)) {
        Ray scattered;
        Vec3 attenuation;
        Vec3 emitted = rec.mat->emitted(rec.u, rec.v, rec.p);
        
        // Material decides how light scatters (the BRDF probability)
        if (rec.mat->scatter(ray, rec, attenuation, scattered)) {
            return emitted + attenuation * trace_ray(scattered, depth + 1);
        }
        return emitted;
    }
    return background_color(ray);
}

The next time you see a beautiful photorealistic game, remember that your GPU is evaluating this integral millions of times per second!