When we estimate the rendering equation by monte carlo integration, we typically integrate over the Solid Angle domain, but with direct light sampling, we use the (surface) area domain of the light sources, and need to convert between the 2.
An integral for direct lighting can be written as so:
We can instead integrate over area domain like this:
This uses the fact that:
As we tilt the light source away from the shading point, the projected area also grows. The factor with which it grows is determined by the cosine of the angle between the surface normal on the light source and the negated incoming light direction. As the angle grows, the cosine shrinks. Since the area should grow and not shrink when this happens, we have the
vectors are normalized, that dot product is exactly the cosine of the angle. See https://arxiv.org/abs/1205.4447 for more details.
We can write out an estimator for our integral over area domain, assuming uniform sampling over the surface of the light source:
We can further simplify to:
Note what this tell us about direct light sampling - to evaluate direct lighting, for a given bounce, we choose a direction towards a light source, and multiply the incoming radiance (light source emission) by the BRDF, by the regular cosine term (often folded into the BRDF), and by the weighting factor described just above - there are 2 cosine at terms at play!
This explanation doesn't include visibility checks or weight for multiple different sources, though, so let me briefly describe. When we don't pass a visibility check (ie. the chosen light point is occluded), we simply don't add the contribution, since the probability of hitting that point is 0. When we have multiple light sources, we simply pick one at random and divide the contribution by the probability of picking the given light source. This is just splitting the estimator into multiple addends.
Code:
let cos_theta = light_normal.dot(-light_direction);
if cos_theta <= 0.0 {
return 0.0;
}
return light_distance.powi(2) / (light_area * cos_theta);