So recently I was thinking reading PBRT for volumetric path-tracing stuff, as well as bunch of other detailed BxDFs. I did write a MIS path-trace based on Ray tracing in One Weeknd series before, and extended some features. However, some design choices / conventions are different. While I was going to implement some materials from PBRT to my path-tracer, I took quite some time to adapt the PBRT’s code to my codebase.
I don’t really want to rewrite everything from scratch though, I would rather do adaption and refactor. So here are some of my findings on the differences between these two resources and their correspondence (Covering Ch. 9 in PBRT Material for now ). Hope this helps if anyone want to do some similar transition.
PBRT uses local coordinates for their f (BxDF) interface, i.e. z-axis up, RT in one weeknd uses a global coordinates. This can be easily address using a Orthonormal Basis.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
classONB { ... // local to world gl::vec3 toWorld(gl::vec3 v)const{ returnat(v); };
//in scatter function, you would do something like this if you want to use PBRT's f function directly. ONB basis(rec.normal); vec3 wo = basis.toLocal(wo_world.normalize()); vec3 wi = basis.toLocal(wi_world.normalize()); auto f = f(wo, wi);
And note that PBRT defines many trigonometric functions / geometric with above convention, the corresponding functions are give below. It’s just the difference of calculation in local coordinates and global coordinates, but you have to be aware of that. since PBRT uses these functions heavily, I would recommend to just convert to local coordinates before passing in directions, and uses the math functions that PBRT defines.
PBRT assumes $w_i, w_o$ are both starting from the surface, pointing outwards. And $w_o$ is the camera ray in local coordinates (since we trace backwards). Whereas in RT in one weeknd, the variable name ray and scattered suggests the other way around (i.e. global coordinates, ray points into the surface).
The correspondence works like this,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
//suppose the interface in RT in one weeknd is like this color ray_color(const ray& ray_in, int depth, const hittable& world)const{
// If the ray hits nothing, return the background color. if (!world.hit(ray_in, interval(0.001, infinity), rec)) return background;
ray scattered;
}
// then the correspondence is like this, w_o points outwards the surface whereas ray_in points into the surface. w_o_world = -ray_in.direction().normalize(); w_i_world = scattered.direction.normalize();
//convert to local coordinate ...
PBRT’s specular rays are handled with delta function and BxDFFlags. For specular cases’ f function, you’ll need to divide by cos(theta) and later it cancels out in the estimator. In RT in one weeknd, you don’t need that divide by cos(theta), it’s directly handled with if (srec.is_specular) branch.
//PBRT, Conductor BRDF if (mfDistrib.EffectivelySmooth()) { //Sample perfect specular conductor BRDF, delta-branch Vector3f wi(-wo.x, -wo.y, wo.z); //note the division by cos(theta) SampledSpectrum f = FrComplex(AbsCosTheta(wi), eta, k) / AbsCosTheta(wi); returnBSDFSample(f, wi, 1, BxDFFlags::SpecularReflection); }
//RT in one weeknd, conductor BRDF if (mfDistribution.effectivelySmooth()) {
gl::vec3 wi = gl::pbrt::reflect(wo, rec.normal).normalize(); if (dot(rec.normal, wi) <= 0) returnfalse;
srec.sampled_ray = Ray(rec.position, wi); // Fresnel reflectance at this angle: float absCosThetaI = fabs(dot(rec.normal, wi)); vec3 f = fresnelComplex(absCosThetaI, eta, k); // Note that no divide by absCosThetaI! srec.attenuation = f; ... }
If you accidentally divide by absCosTheta in your specular srec.attenutaion, you would get something like this. The left surface of the box is very bright (which is wrong).
The design of scattering_pdf in RT in one weeknd is just out of convenience to my understanding. Since we already pass in the pdf_ptr to the scatter record, we can simply call the pointer in the integrator. Alternatively, you can implement an interface like this.
1 2 3 4 5
floatscatter_pdf(const ScatterRecord &srec, const Ray &wi)const{ if (srec.pdf_ptr != nullptr) return0; return srec.pdf_ptr->at(wi.getDirection().normalize()); }
The integrator from Ray tracing in one weeknd doesn’t requires an f function (BxDF evaluation) for each material, since the attenuation value is stored in scatter_record, there’s no need to recalculate the attenuation with material_ptr->f(). However, the f() and scatter_record.attenuation are bit different.
It’s also mentioned in RT in one weeknd.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
//RT in one weeknd, Lambertian example srec.attenuation = tex->value(rec.u, rec.v, rec.p); color sample_color = ray_color(scattered, depth-1, world, lights); color color_from_scatter = (srec.attenuation * scattering_pdf * sample_color) / pdf_value;
auto f = mat->f(wo, wi, hit_record); auto pdf_val = mix_pdf.at(wi.toWorld()); float cos_theta = std::max(dot(hit_record.normal, wi.toWorld()), 0.0f); color sample_color = getRayColor(wi.toWorld(), depth-1, world, lights); color color_from_scatter = f * sample_color * cos_theta / pdf_Val
f() is required for direct light sampling with NEE, the srec.attenutaion stores the f value for the material sampling path, not the light sampling path. When we sample the light, we cannot reuse srec.attenutation as our f, we must perform another BxDF evaluation on light path.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
generate light direction: f(wi) * Li * G * V / p_light. //in your material interface, you can do something like to avoid code redundancy // --- rough (microfacet) case --- OrthoBasis basis(rec.normal); auto pdf_ptr = std::make_shared<MicrofacetPDF>(mfDistribution, basis, wo);
// 2) sample an incoming direction `wi` vec3 wi = pdf_ptr->get().normalize(); // 3) reject if it’s below the geometric normal if (dot(rec.normal, wi) <= 0) returnfalse;
//directly calling the bxdf function srec.attenuation = f(wo, wi, rec);
Now let’s get to the Material interface difference between these two books. The correspondence is rather straightforward, and you’ll probably need some small tweaks to your RT in one weeknd interface.
//This is missing in Rt 1wk, required for NEE SampledSpectrum f(Vector3f wo, Vector3f wi, TransportMode mode)const{ if (!SameHemisphere(wo, wi)) returnSampledSpectrum(0.f); return R * InvPi; }
//Both does pdf evaluation //PBRT Float PDF(Vector3f wo, Vector3f wi, TransportMode mode, BxDFReflTransFlags sampleFlags = BxDFReflTransFlags::All)const //Rt 1wk floatscatter_pdf(const Ray &ray_in, const HitRecord &rec, const Ray &scattered)constoverride