Notes on transitioning from RT in One Weeknd to PBRT
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 | class ONB { |
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.
1 | namespace pbrt{ |
- 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
rayandscatteredsuggests the other way around (i.e. global coordinates,raypoints 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’ffunction, you’ll need to divide bycos(theta)and later it cancels out in the estimator. In RT in one weeknd, you don’t need that divide bycos(theta), it’s directly handled withif (srec.is_specular)branch.
- The example below illustrates the idea
1 | //PBRT, Conductor BRDF |
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_pdfin RT in one weeknd is just out of convenience to my understanding. Since we already pass in thepdf_ptrto thescatter record, we can simply call the pointer in the integrator. Alternatively, you can implement an interface like this.
1 | float scatter_pdf(const ScatterRecord &srec, const Ray &wi) const { |
- The integrator from Ray tracing in one weeknd doesn’t requires an
ffunction (BxDF evaluation) for each material, since theattenuationvalue is stored inscatter_record, there’s no need to recalculate theattenuationwithmaterial_ptr->f(). However, thef()andscatter_record.attenuationare bit different.
It’s also mentioned in RT in one weeknd.

1 | //RT in one weeknd, Lambertian example |
f()is required for direct light sampling with NEE, thesrec.attenutaionstores thefvalue for the material sampling path, not the light sampling path. When we sample the light, we cannot reusesrec.attenutationas ourf, we must perform another BxDF evaluation on light path.
1 | generate light direction: |
- Now let’s get to the
Materialinterface 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.
1 | // these two functions are equivalent, both do one sampling in the material's lobe, and then returns necessary information for this sample. |