Rendering equation's value can be estimated with Monte Carlo (Physically Based Rendering: Light Transport I: Surface Reflection):
$\begin{equation} \begin{split} L_o(p, \omega_o) &= \int_{S^2}f(p, \omega_o, \omega_i) L_i(p, \omega_i) |\cos\theta_i| \mathrm{d}\omega_i\\ & = \frac{1}{N} \sum_{j=1}^{N} \frac{f(p, \omega_o, \omega_i) L_i(p, \omega_j) |\cos\theta_j|}{p(\omega_j)} \end{split} \end{equation}$
So if the hit surface is a mirror and $\omega_j$ is the correct direction (only 1 sample is needed and $p(\omega_j)=1$) then this equation becomes:
$\begin{equation} \begin{split} L_o(p, \omega_o) & = f(p, \omega_o, \omega_i) L_i(p, \omega_j) |\cos\theta_j| \end{split} \end{equation}$
For a ideal mirror that reflect without any energe loss, $f(p, \omega_o, \omega_i)$ is 1.
Yet I couldn't find term $\cos\theta_j$ when reading code from Ray Tracing Weekend and smallpt.
In Ray Tracing Weekend, the ray tracing function is written as
color ray_color(
const ray& r,
const color& background,
const hittable& world,
shared_ptr<hittable> lights,
int depth
) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
// If the ray hits nothing, return the background color.
if (!world.hit(r, 0.001, infinity, rec))
return background;
scatter_record srec;
color emitted = rec.mat_ptr->emitted(r, rec, rec.u, rec.v, rec.p);
if (!rec.mat_ptr->scatter(r, rec, srec))
return emitted;
if (srec.is_specular) {
return srec.attenuation
* ray_color(srec.specular_ray, background, world, lights, depth-1);
}
...
The last part
if (srec.is_specular) {
return srec.attenuation
* ray_color(srec.specular_ray, background, world, lights, depth-1);
}
implies that when hitting a specular object, the result (color) got attenuated by srec.attenuation
but the cosine term is gone.
Similarly, neither smallpt (line 61, 62) include cosine term when handling scatterring for specular object:
else if (obj.refl == SPEC) // Ideal SPECULAR reflection
return obj.e + f.mult(radiance(Ray(x,r.d-n*2*n.dot(r.d)),depth,Xi));
it shoots a ray to a new direction without having $\cos(\theta)$ after reflection.
So, how does $\cos(\theta)$ got cancelled for both renderer? Or did I misunderstand the equation?