大致流程
这次的作业挺难的。首先我们来对照公式解释一下大致的代码流程:
- 如果点 $p$ 是光源,直接返回其自发光项 $L_e$.
- 随机采样一个光源方向 $w_s$,根据公式计算 $L_\text{direct}$.
随机采样一个方向 $w_i$,按轮盘赌决定是否结束弹射。
如果不结束且 $w_i$ 没打到光源,根据公式计算 $L_\text{indirect}$.
返回 $L_\text{direct}+L_\text{indirect}$
然后放代码:
Vector3f Scene::castRay(const Ray &ray, int depth) const
{
Intersection shade_point_inter = intersect(ray);
if (!shade_point_inter.happened) {
return Vector3f();
}
// ----------------Contribution from emission----------------
if (shade_point_inter.m->hasEmission()) {
// Light source doesn't reflect light, so we return here
return shade_point_inter.m->getEmission();
}
// ----------------Contribution from the light source----------------
Vector3f wo = -normalize(ray.direction);
Vector3f normal = normalize(shade_point_inter.normal);
Vector3f l_direct = Vector3f();
Intersection light_sample;
float pdf_light;
sampleLight(light_sample, pdf_light);
// If the ray is not blocked in the middle, compute its contribution
Vector3f light_normal = normalize(light_sample.normal);
Vector3f shade_point = shade_point_inter.coords;
Vector3f ws = normalize(light_sample.coords - shade_point);
Vector3f direct_ray_origin = shade_point + ws * EPSILON;
Ray ray_to_light(direct_ray_origin, ws);
Intersection inter_between_light = intersect(ray_to_light);
if ((!inter_between_light.happened) || inter_between_light.distance > (light_sample.coords - direct_ray_origin).norm() - EPSILON) {
l_direct = light_sample.emit * shade_point_inter.m->eval(ws, wo, normal) \
* std::max(dotProduct(ws, normal), 0.0f) * std::max(dotProduct(-ws, light_normal), 0.0f) \
/ std::pow((light_sample.coords - direct_ray_origin).norm(), 2) / pdf_light;
}
// ----------------Contribution from other reflectors----------------
Vector3f l_indirect = Vector3f();
// Russian Roulette test
if (get_random_float() < RussianRoulette) {
Vector3f wi = normalize(shade_point_inter.m->sample(wo, normal));
Vector3f indirect_ray_origin = shade_point + wi * EPSILON;
Ray reflected_ray(indirect_ray_origin, wi);
Intersection reflected_inter = intersect(reflected_ray);
// If reflected_ray hitting a non-emitting object, compute its contribution
if (reflected_inter.happened && reflected_inter.m->hasEmission()) {
l_indirect = Vector3f();
}
else {
l_indirect = castRay(reflected_ray, depth + 1) * shade_point_inter.m->eval(wi, wo, normal) \
* std::max(dotProduct(wi, normal), 0.0f) \
/ (shade_point_inter.m->pdf(wi, wo, normal)) / RussianRoulette;
}
}
return l_direct + l_indirect;
}
再来看看图
代码细节
接下来我们再聊聊具体的代码细节。自发光项比较简单,我们略过。
$L_\text{direct}$
直接光部分。首先要注意的是,各个射线 $w_i, w_o, w_s$ 都是从着色点指向外的单位方向向量。这是一个图形学里常用的约定,所以不要问为什么不是指向内了()
然后在发出射线时,最好做一个微小的偏移以避免发出的射线和着色点平面相交。这就是我们 direct_ray_origin = shade_point + ws * EPSILON
做了加法的原因。
Vector3f wo = -normalize(ray.direction);
Vector3f normal = normalize(shade_point_inter.normal);
Vector3f l_direct = Vector3f();
Intersection light_sample;
float pdf_light;
sampleLight(light_sample, pdf_light);
// If the ray is not blocked in the middle, compute its contribution
Vector3f light_normal = normalize(light_sample.normal);
Vector3f shade_point = shade_point_inter.coords;
Vector3f ws = normalize(light_sample.coords - shade_point);
Vector3f direct_ray_origin = shade_point + ws * EPSILON;
Ray ray_to_light(direct_ray_origin, ws);
Intersection inter_between_light = intersect(ray_to_light);
if ((!inter_between_light.happened) || inter_between_light.distance > (light_sample.coords - direct_ray_origin).norm() - EPSILON) {
l_direct = light_sample.emit * shade_point_inter.m->eval(ws, wo, normal) \
* std::max(dotProduct(ws, normal), 0.0f) * std::max(dotProduct(-ws, light_normal), 0.0f) \
/ std::pow((light_sample.coords - direct_ray_origin).norm(), 2) / pdf_light;
}
另外,我们有从光源采样计算直接光的公式:
用蒙特卡罗近似就是代码里写的东西了。
$L_\text{indirect}$
间接部分。首先要做俄罗斯轮盘赌,然后要检查打到的是否是光源。
如果通过了轮盘赌,而且打到的不是光源,就要计算间接光。
与直接光一样,我们要做微小偏移,这就是 indirect_ray_origin = shade_point + wi * EPSILON
.
然后套公式就行。
Vector3f l_indirect = Vector3f();
// Russian Roulette test
if (get_random_float() < RussianRoulette) {
Vector3f wi = normalize(shade_point_inter.m->sample(wo, normal));
Vector3f indirect_ray_origin = shade_point + wi * EPSILON;
Ray reflected_ray(indirect_ray_origin, wi);
Intersection reflected_inter = intersect(reflected_ray);
// If reflected_ray hitting a non-emitting object, compute its contribution
if (reflected_inter.happened && reflected_inter.m->hasEmission()) {
l_indirect = Vector3f();
}
else {
l_indirect = castRay(reflected_ray, depth + 1) * shade_point_inter.m->eval(wi, wo, normal) \
* std::max(dotProduct(wi, normal), 0.0f) \
/ (shade_point_inter.m->pdf(wi, wo, normal)) / RussianRoulette;
}
}
结果对 EPSILON 挺敏感的,可以把 EPSILON 适当调大点,我用的是 0.00020
.