引言

《Ray Tracing: The Next Week》（《一周搞定光线追踪》）， 由 Peter Shirley（就是那本图形学虎书的作者）所编写的的软渲光追三部曲第二本，是一本非常好的入门级书籍，当前版本 v3.0。

概述

In Ray Tracing in One Weekend, you built a simple brute force path tracer. In this installment we’ll add textures, volumes (like fog), rectangles, instances, lights, and support for lots of objects using a BVH. When done, you’ll have a “real” ray tracer.

Ray Tracing in One Weekend中, 你实现了一个暴力的光线路径追踪器。在本部分中, 我们将加入纹理, 体积体(例如烟雾), 矩形, 实例, 光源, 并用BVH来包裹我们的物体。当你完成这些后, 你将拥有一个“真正的”光线追踪器。

A heuristic in ray tracing that many people—including me—believe, is that most optimizations complicate the code without delivering much speedup. What I will do in this mini-book is go with the simplest approach in each design decision I make. Check https://in1weekend.blogspot.com/ for readings and references to a more sophisticated approach. However, I strongly encourage you to do no premature optimization; if it doesn’t show up high in the execution time profile, it doesn’t need optimization until all the features are supported!

The two hardest parts of this book are the BVH and the Perlin textures. This is why the title suggests you take a week rather than a weekend for this endeavor. But you can save those for last if you want a weekend project. Order is not very important for the concepts presented in this book, and without BVH and Perlin texture you will still get a Cornell Box!

动态模糊

When you decided to ray trace, you decided that visual quality was worth more than run-time. In your fuzzy reflection and defocus blur you needed multiple samples per pixel. Once you have taken a step down that road, the good news is that almost all effects can be brute-forced. Motion blur is certainly one of those. In a real camera, the shutter opens and stays open for a time interval, and the camera and objects may move during that time. Its really an average of what the camera sees over that interval that we want.

We can get a random estimate by sending each ray at some random time when the shutter is open. As long as the objects are where they should be at that time, we can get the right average answer with a ray that is at exactly a single time. This is fundamentally why random ray tracing tends to be simple.

Introduction of SpaceTime Ray Tracing

The basic idea is to generate rays at random times while the shutter is open and intersect the model at that one time. The way it is usually done is to have the camera move and the objects move, but have each ray exist at exactly one time. This way the “engine” of the ray tracer can just make sure the objects are where they need to be for the ray, and the intersection guts don’t change much.

For this we will first need to have a ray store the time it exists at:

Updating the Camera to Simulate Motion Blur

Now we need to modify the camera to generate rays at a random time between time1 and time2. Should the camera keep track of time1 and time2 or should that be up to the user of camera when a ray is created? When in doubt, I like to make constructors complicated if it makes calls simple, so I will make the camera keep track, but that’s a personal preference. Not many changes are needed to camera because for now it is not allowed to move; it just sends out rays over a time period.

We also need a moving object. I’ll create a sphere class that has its center move linearly from center0 at time0 to center1 at time1. Outside that time interval it continues on, so those times need not match up with the camera aperture open and close.

An alternative to making a new moving sphere class is to just make them all move, while stationary spheres have the same begin and end position. I’m on the fence about that trade-off between fewer classes and more efficient stationary spheres, so let your design taste guide you.

The moving_sphere::hit() function is almost identical to the sphere::hit() function: center just needs to become a function center(time):

Tracking the Time of Ray Intersection

Now that rays have a time property, we need to update the material::scatter() methods to account for the time of intersection:

Putting Everything Together

The code below takes the example diffuse spheres from the scene at the end of the last book, and makes them move during the image render. (Think of a camera with shutter opening at time 0 and closing at time 1.) Each sphere moves from its center 𝐂 at time 𝑡=0 to 𝐂+(0,𝑟/2,0) at time 𝑡=1, where 𝑟 is a random number in [0,1):

层次包围盒

This part is by far the most difficult and involved part of the ray tracer we are working on. I am sticking it in this chapter so the code can run faster, and because it refactors hittable a little, and when I add rectangles and boxes we won’t have to go back and refactor them.

The ray-object intersection is the main time-bottleneck in a ray tracer, and the time is linear with the number of objects. But it’s a repeated search on the same model, so we ought to be able to make it a logarithmic search in the spirit of binary search. Because we are sending millions to billions of rays on the same model, we can do an analog of sorting the model, and then each ray intersection can be a sublinear search. The two most common families of sorting are to 1) divide the space, and 2) divide the objects. The latter is usually much easier to code up and just as fast to run for most models.

The Key Idea

The key idea of a bounding volume over a set of primitives is to find a volume that fully encloses (bounds) all the objects. For example, suppose you computed a bounding sphere of 10 objects. Any ray that misses the bounding sphere definitely misses all ten objects. If the ray hits the bounding sphere, then it might hit one of the ten objects. So the bounding code is always of the form:

A key thing is we are dividing objects into subsets. We are not dividing the screen or the volume. Any object is in just one bounding volume, but bounding volumes can overlap.

Hierarchies of Bounding Volumes

To make things sub-linear we need to make the bounding volumes hierarchical. For example, if we divided a set of objects into two groups, red and blue, and used rectangular bounding volumes, we’d have:

Note that the blue and red bounding volumes are contained in the purple one, but they might overlap, and they are not ordered — they are just both inside. So the tree shown on the right has no concept of ordering in the left and right children; they are simply inside. The code would be:

Axis-Aligned Bounding Boxes (AABBs)

To get that all to work we need a way to make good divisions, rather than bad ones, and a way to intersect a ray with a bounding volume. A ray bounding volume intersection needs to be fast, and bounding volumes need to be pretty compact. In practice for most models, axis-aligned boxes work better than the alternatives, but this design choice is always something to keep in mind if you encounter unusual types of models.

From now on we will call axis-aligned bounding rectangular parallelepiped (really, that is what they need to be called if precise) axis-aligned bounding boxes, or AABBs. Any method you want to use to intersect a ray with an AABB is fine. And all we need to know is whether or not we hit it; we don’t need hit points or normals or any of that stuff that we need for an object we want to display.

Most people use the “slab” method. This is based on the observation that an n-dimensional AABB is just the intersection of n axis-aligned intervals, often called “slabs” An interval is just the points between two endpoints, e.g., 𝑥 such that 3<𝑥<5, or more succinctly 𝑥x in (3,5). In 2D, two intervals overlapping makes a 2D AABB (a rectangle):

For a ray to hit one interval we first need to figure out whether the ray hits the boundaries. For example, again in 2D, this is the ray parameters 𝑡0 and 𝑡1. (If the ray is parallel to the plane those will be undefined.)

In 3D, those boundaries are planes. The equations for the planes are $𝑥=𝑥_0$​, and $𝑥=𝑥_1$. Where does the ray hit that plane? Recall that the ray can be thought of as just a function that given a 𝑡t returns a location 𝐏(𝑡):

$P(t) = A + tb$

$x(t)=A_x+t_0⋅B_x$​

$x_0=A_x+t_0⋅B_x$

$t_0=\frac{x0−A_x}{B_x}$​

$t_1=\frac{x_1−A_x}{B_x}$

The key observation to turn that 1D math into a hit test is that for a hit, the 𝑡t-intervals need to overlap. For example, in 2D the green and blue overlapping only happens if there is a hit:

【译注: 这张图挺好的, 上面的那条射线, 蓝色与绿色部分没有重叠, 很自然的就没有穿过这个AABB矩形, 下面那条射线发生了重叠, 说明射线同时传过了蓝色区域和绿色区域, 即穿过了AABB矩形。注意对每一个维度来说, 这里我们解出来的t0, t1都表示直线上一个固定的点的位置, 所以我们可以自然地按照维度拆分计算, 然后在通过t这个统一的标识进行求交运算】

Ray Intersection with an AABB

The following pseudocode determines whether the 𝑡t intervals in the slab overlap:

That is awesomely simple, and the fact that the 3D version also works is why people love the slab method:

There are some caveats that make this less pretty than it first appears. First, suppose the ray is travelling in the negative 𝑥 direction. The interval $(𝑡{𝑥0},𝑡{𝑥1})$ as computed above might be reversed, e.g. something like (7,3). Second, the divide in there could give us infinities. And if the ray origin is on one of the slab boundaries, we can get a NaN. There are many ways these issues are dealt with in various ray tracers’ AABB. (There are also vectorization issues like SIMD which we will not discuss here. Ingo Wald’s papers are a great place to start if you want to go the extra mile in vectorization for speed.) For our purposes, this is unlikely to be a major bottleneck as long as we make it reasonably fast, so let’s go for simplest, which is often fastest anyway! First let’s look at computing the intervals:

$t_{x0} = \frac{x_0 - A_x}{B_x}$

$t_{x1}=\frac{x_1-A_x}{b_x}$

One troublesome thing is that perfectly valid rays will have $𝑏𝑥=0$, causing division by zero. Some of those rays are inside the slab, and some are not. Also, the zero will have a ± sign under IEEE floating point. The good news for $𝑏𝑥=0$ is that $𝑡{𝑥0}$ and $𝑡{𝑥1}$ will both be +∞ or both be -∞ if not between $𝑥_0$ and $𝑥_1$. So, using min and max should get us the right answers:

$t_{x0}=\min{(\frac{x_0-A_x}{B_x}, \frac{x_1- A_x}{B_x})}$

$t_{x1} = \max{(\frac{x_0-A_x}{B_x}, \frac{x_1-A_x}{B_x})}$

The remaining troublesome case if we do that is if $𝑏_𝑥=0$​ and either $𝑥_0−𝐴_𝑥=0$​ or $𝑥_1−𝐴_𝑥=0$ so we get a NaN. In that case we can probably accept either hit or no hit answer, but we’ll revisit that later.

Now, let’s look at that overlap function. Suppose we can assume the intervals are not reversed (so the first value is less than the second value in the interval) and we want to return true in that case. The boolean overlap that also computes the overlap interval (𝑓,𝐹) of intervals (𝑑,𝐷) and (𝑒,𝐸) would be:

If there are any NaNs running around there, the compare will return false so we need to be sure our bounding boxes have a little padding if we care about grazing cases (and we probably should because in a ray tracer all cases come up eventually). With all three dimensions in a loop, and passing in the interval $[𝑡{𝑚𝑖𝑛}, 𝑡{𝑚𝑎𝑥}]$, we get:

An Optimized AABB Hit Method

In reviewing this intersection method, Andrew Kensler at Pixar tried some experiments and proposed the following version of the code. It works extremely well on many compilers, and I have adopted it as my go-to method:

Constructing Bounding Boxes for Hittables

We now need to add a function to compute the bounding boxes of all the hittables. Then we will make a hierarchy of boxes over all the primitives, and the individual primitives—like spheres—will live at the leaves. That function returns a bool because not all primitives have bounding boxes (e.g., infinite planes). In addition, moving objects will have a bounding box that encloses the object for the entire time interval [time0,time1].

Creating Bounding Boxes of Lists of Objects

For a sphere, that bounding_box function is easy:

For moving sphere, we can take the box of the sphere at $𝑡_0$​, and the box of the sphere at $𝑡_1$​, and compute the box of those two boxes:

For lists you can store the bounding box at construction, or compute it on the fly. I like doing it the fly because it is only usually called at BVH construction.

This requires the surrounding_box function for aabb which computes the bounding box of two boxes:

The BVH Node Class

A BVH is also going to be a hittable — just like lists of hittables. It’s really a container, but it can respond to the query “does this ray hit you?”. One design question is whether we have two classes, one for the tree, and one for the nodes in the tree; or do we have just one class and have the root just be a node we point to. I am a fan of the one class design when feasible. Here is such a class:

BVH也应该是hittable的一员, 就像hittable_list类那样。BVH虽然是个容器, 但也能对于问题“这条光线射中你了么?”做出回答。一个设计上的问题是, 我们是为树和树的节点设计两个不同的类呢, 还是用一个类加上指针来搞定。我是一个类搞定派, 所以这个 bvh.h 类会是这样:

Note that the children pointers are to generic hittables. They can be other bvh_nodes, or spheres, or any other hittable.

The hit function is pretty straightforward: check whether the box for the node is hit, and if so, check the children and sort out any details:

hit 函数也是十分的直接明了: 检查这个节点的box是否被击中, 如果是的话, 那就对这个节点的子节点进行判断。【译注: 对于二叉树来说, 这样的递归结构相信大家并不陌生】

Splitting BVH Volumes

The most complicated part of any efficiency structure, including the BVH, is building it. We do this in the constructor. A cool thing about BVHs is that as long as the list of objects in a bvh_node gets divided into two sub-lists, the hit function will work. It will work best if the division is done well, so that the two children have smaller bounding boxes than their parent’s bounding box, but that is for speed not correctness. I’ll choose the middle ground, and at each node split the list along one axis. I’ll go for simplicity:

1. randomly choose an axis
2. sort the primitives (using std::sort)
3. put half in each subtree

When the list coming in is two elements, I put one in each subtree and end the recursion. The traversal algorithm should be smooth and not have to check for null pointers, so if I just have one element I duplicate it in each subtree. Checking explicitly for three elements and just following one recursion would probably help a little, but I figure the whole method will get optimized later. This yields:

1.随机选取一个轴来分割
2.使用库函数sort()对图元进行排序
3.对半分, 每个子树分一半的物体

This uses a new function: random_int():

rtweekend.h 中添加函数 random_int()

The check for whether there is a bounding box at all is in case you sent in something like an infinite plane that doesn’t have a bounding box. We don’t have any of those primitives, so it shouldn’t happen until you add such a thing.

The Box Comparison Functions

Now we need to implement the box comparison functions, used by std::sort(). To do this, create a generic comparator returns true if the first argument is less than the second, given an additional axis index argument. Then define axis-specific comparison functions that use the generic comparison function.

【译注: 使用方法：在 random_scene()函数最后return static_cast<hittable_list>(make_shared<bvh_node>(world,0,1));

固体贴图

A texture in graphics usually means a function that makes the colors on a surface procedural. This procedure can be synthesis code, or it could be an image lookup, or a combination of both. We will first make all colors a texture. Most programs keep constant rgb colors and textures in different classes, so feel free to do something different, but I am a big believer in this architecture because being able to make any color a texture is great.

The First Texture Class: Constant Texture

We’ll need to update the hit_record structure to store the U,V surface coordinates of the ray-object hit point.

Now we can make textured materials by replacing the const color& a with a texture pointer:

A Checker Texture

We can create a checker texture by noting that the sign of sine and cosine just alternates in a regular way, and if we multiply trig functions in all three dimensions, the sign of that product forms a 3D checker pattern.

Those checker odd/even pointers can be to a constant texture or to some other procedural texture. This is in the spirit of shader networks introduced by Pat Hanrahan back in the 1980s.

If we add this to our random_scene() function’s base sphere:

We get:

Rendering a Scene with a Checkered Texture

We’re going to add a second scene to our program, and will add more scenes after that as we progress through this book. To help with this, we’ll set up a hard-coded switch statement to select the desired scene for a given run. Clearly, this is a crude approach, but we’re trying to keep things dead simple and focus on the raytracing. You may want to use a different approach in your own raytracer.

Here’s the scene construction function:

We get this result:

柏林噪音

To get cool looking solid textures most people use some form of Perlin noise. These are named after their inventor Ken Perlin. Perlin texture doesn’t return white noise like this:

Instead it returns something similar to blurred white noise:

A key part of Perlin noise is that it is repeatable: it takes a 3D point as input and always returns the same randomish number. Nearby points return similar numbers. Another important part of Perlin noise is that it be simple and fast, so it’s usually done as a hack. I’ll build that hack up incrementally based on Andrew Kensler’s description.

Using Blocks of Random Numbers

We could just tile all of space with a 3D array of random numbers and use them in blocks. You get something blocky where the repeating is clear:

Let’s just use some sort of hashing to scramble this, instead of tiling. This has a bit of support code to make it all happen:

Now if we create an actual texture that takes these floats between 0 and 1 and creates grey colors:

We can use that texture on some spheres:

Add the hashing does scramble as hoped:

Smoothing out the Result

To make it smooth, we can linearly interpolate:

And we get:

Improvement with Hermitian Smoothing

Smoothing yields an improved result, but there are obvious grid features in there. Some of it is Mach bands, a known perceptual artifact of linear interpolation of color. A standard trick is to use a Hermite cubic to round off the interpolation:

This gives a smoother looking image:

Tweaking The Frequency

It is also a bit low frequency. We can scale the input point to make it vary more quickly:

We then add that scale to the two_perlin_spheres() scene description:

Using Random Vectors on the Lattice Points

This is still a bit blocky looking, probably because the min and max of the pattern always lands exactly on the integer x/y/z. Ken Perlin’s very clever trick was to instead put random unit vectors (instead of just floats) on the lattice points, and use a dot product to move the min and max off the lattice. So, first we need to change the random floats to random vectors. These vectors are any reasonable set of irregular directions, and I won’t bother to make them exactly uniform:

And the interpolation becomes a bit more complicated:

The output of the perlin interpretation can return negative values. These negative values will be passed to the sqrt() function of our gamma function and get turned into NaNs. We will cast the perlin output back to between 0 and 1.

This finally gives something more reasonable looking:

Introducing Turbulence

Very often, a composite noise that has multiple summed frequencies is used. This is usually called turbulence, and is a sum of repeated calls to noise:

Here fabs() is the absolute value function defined in <cmath>.

Used directly, turbulence gives a sort of camouflage netting appearance:

However, usually turbulence is used indirectly. For example, the “hello world” of procedural solid textures is a simple marble-like texture. The basic idea is to make color proportional to something like a sine function, and use turbulence to adjust the phase (so it shifts 𝑥 in sin(𝑥)) which makes the stripes undulate. Commenting out straight noise and turbulence, and giving a marble-like effect is:

Which yields:

纹理映射

From the hitpoint 𝐏, we compute the surface coordinates (𝑢,𝑣). We then use these to index into our procedural solid texture (like marble). We can also read in an image and use the 2D (𝑢,𝑣) texture coordinate to index into the image.

A direct way to use scaled (𝑢,𝑣) in an image is to round the 𝑢 and 𝑣 to integers, and use that as (𝑖,𝑗) pixels. This is awkward, because we don’t want to have to change the code when we change image resolution. So instead, one of the the most universal unofficial standards in graphics is to use texture coordinates instead of image pixel coordinates. These are just some form of fractional position in the image. For example, for pixel (𝑖,𝑗) in an $𝑁_𝑥$ by $𝑁_𝑦$​ image, the image texture position is:

$u = \frac{i}{N_x-1}$

$v = \frac{j}{N_y - 1}$

$u = \frac{\phi}{2\pi}$

$v = \frac{\phi}{\pi}$

$x = \cos(\phi)\cos(\theta)$

$y=\sin(\phi)\cos(\theta)$

$z = \sin(\theta)$

We need to invert these equations to solve for 𝜃 and 𝜙. Because of the lovely <cmath> function atan2(), which takes any pair of numbers proportional to sine and cosine and returns the angle, we can pass in 𝑥 and 𝑧 (the sin(𝜃) cancel) to solve for 𝜙:

$\phi = \atan2(y, x)$

atan2() returns values in the range −𝜋 to 𝜋, but they go from 0 to 𝜋, then flip to −𝜋 and proceed back to zero. While this is mathematically correct, we want 𝑢 to range from 0 to 1, not from 0 to 1/2 and then from −1/2 to 0. Fortunately,

$\atan2$ 函数的返回值范围为 −π 到 π 【译注:即返回弧度(radius)】所以我们这里还要小心一下。相对的, 求角 θ 更为简单直接:

$\theta = \asin(z)$

So for a sphere, the (𝑢,𝑣) coord computation is accomplished by a utility function that takes points on the unit sphere centered at the origin, and computes 𝑢 and 𝑣:

Update the sphere::hit() function to use this function to update the hit record UV coordinates.

Storing Texture Image Data

The representation of a packed array in that order is pretty standard. Thankfully, the stb_image package makes that super simple — just write a header called rtw_stb_image.h that also deals with some compiler warnings:

Using an Image Texture

I just grabbed a random earth map from the web — any standard projection will do for our purposes.

Here’s the code to read an image from a file and then assign it to a diffuse material:

We start to see some of the power of all colors being textures — we can assign any kind of texture to the lambertian material, and lambertian doesn’t need to be aware of it.

To test this, throw it into main:

矩阵和光源

Lighting is a key component of raytracing. Early simple raytracers used abstract light sources, like points in space, or directions. Modern approaches have more physically based lights, which have position and size. To create such light sources, we need to be able to take any regular object and turn it into something that emits light into our scene.

Emissive Materials

First, let’s make a light emitting material. We need to add an emitted function (we could also add it to hit_record instead — that’s a matter of design taste). Like the background, it just tells the ray what color it is and performs no reflection. It’s very simple:

So that I don’t have to make all the non-emitting materials implement emitted(), I have the base class return black:

Adding Background Color to the Ray Color Function

Next, we want a pure black background so the only light in the scene is coming from the emitters. To do this, we’ll add a background color parameter to our ray_color function, and pay attention to the new emitted value.

Creating Rectangle Objects

Now, let’s make some rectangles. Rectangles are often convenient for modeling man-made environments. I’m a fan of doing axis-aligned rectangles because they are easy. (We’ll get to instancing so we can rotate them later.)

First, here is a rectangle in an xy plane. Such a plane is defined by its z value. For example, 𝑧=𝑘. An axis-aligned rectangle is defined by the lines 𝑥=𝑥0, 𝑥=𝑥1, 𝑦=𝑦0, and 𝑦=𝑦1.

To determine whether a ray hits such a rectangle, we first determine where the ray hits the plane. Recall that a ray 𝐏(𝑡)=𝐀+𝑡𝐛 has its z component defined by $𝑃_𝑧(𝑡)=𝐴_𝑧+𝑡𝑏_z$. Rearranging those terms we can solve for what the t is where 𝑧=𝑘.

$t = \frac{k - a_z}{\vec{b_z}}$

Once we have 𝑡, we can plug that into the equations for 𝑥 and 𝑦:

$x = a_x + t \cdot \vec{b_x}$

$y = a_y + t \cdot \vec{b_y}$

It is a hit if $𝑥_0<𝑥<𝑥_1$ and $𝑦_0<𝑦<𝑦_1$.

Because our rectangles are axis-aligned, their bounding boxes will have an infinitely-thin side. This can be a problem when dividing them up with our axis-aligned bounding volume hierarchy. To counter this, all hittable objects should get a bounding box that has finite width along every dimension. For our rectangles, we’ll just pad the box a bit on the infnitely-thin side.

The actual xy_rect class is thus:

And the hit function is:

hit函数是这样的:

Turning Objects into Lights

If we set up a rectangle as a light:

More Axis-Aligned Rectangles

Now let’s add the other two axes and the famous Cornell Box.

This is xz and yz:

xz 和 yz平面是这样的:【实话说这样写代码有些冗余了】

With unsurprising hit functions:

Creating an Empty “Cornell Box”

The “Cornell Box” was introduced in 1984 to model the interaction of light between diffuse surfaces. Let’s make the 5 walls and the light of the box:

Add the view and scene info:

实例

The Cornell Box usually has two blocks in it. These are rotated relative to the walls. First, let’s make an axis-aligned block primitive that holds 6 rectangles:

Cornell Box里面一般都有两个相对墙面有些角度的长方体。首先我们先把轴对齐的长方体图元做出来。每个长方体是由6个平面构成的:

Now we can add two blocks (but not rotated)

Now that we have boxes, we need to rotate them a bit to have them match the real Cornell box. In ray tracing, this is usually done with an instance. An instance is a geometric primitive that has been moved or rotated somehow. This is especially easy in ray tracing because we don’t move anything; instead we move the rays in the opposite direction. For example, consider a translation (often called a move). We could take the pink box at the origin and add 2 to all its x components, or (as we almost always do in ray tracing) leave the box where it is, but in its hit routine subtract 2 off the x-component of the ray origin.

Instance Translation

Whether you think of this as a move or a change of coordinates is up to you. The code for this, to move any underlying hittable is a translate instance.

Instance Rotation

Rotation isn’t quite as easy to understand or generate the formulas for. A common graphics tactic is to apply all rotations about the x, y, and z axes. These rotations are in some sense axis-aligned. First, let’s rotate by theta about the z-axis. That will be changing only x and y, and in ways that don’t depend on z.

This involves some basic trigonometry that uses formulas that I will not cover here. That gives you the correct impression it’s a little involved, but it is straightforward, and you can find it in any graphics text and in many lecture notes. The result for rotating counter-clockwise about z is:

$x’ = \cos(\theta) \cdot x - \sin(\theta) \cdot y$

$y’ = \sin(\theta) \cdot x + \cos(\theta) \cdot y$

The great thing is that it works for any 𝜃 and doesn’t need any cases for quadrants or anything like that. The inverse transform is the opposite geometric operation: rotate by −𝜃. Here, recall that $\cos(𝜃)=\cos(−𝜃)$ and $\sin(−𝜃)=−\sin(𝜃)$, so the formulas are very simple.

Similarly, for rotating about y (as we want to do for the blocks in the box) the formulas are:

$x’ = \cos(\theta) \cdot x + \sin(\theta) \cdot z$

$z’ = -\sin(\theta) \cdot x + \cos(\theta) \cdot z$

$y’ = \cos(\theta) \cdot y - \sin(\theta) \cdot z$

$z’ = \sin(\theta) \cdot y + \cos(\theta) \cdot z$​

Unlike the situation with translations, the surface normal vector also changes, so we need to transform directions too if we get a hit. Fortunately for rotations, the same formulas apply. If you add scales, things get more complicated. See the web page https://in1weekend.blogspot.com/ for links to that.

For a y-rotation class we have:

With constructor:

And the hit function:

And the changes to Cornell are:

Which yields:

体积体

One thing it’s nice to add to a ray tracer is smoke/fog/mist. These are sometimes called volumes or participating media. Another feature that is nice to add is subsurface scattering, which is sort of like dense fog inside an object. This usually adds software architectural mayhem because volumes are a different animal than surfaces, but a cute technique is to make a volume a random surface. A bunch of smoke can be replaced with a surface that probabilistically might or might not be there at every point in the volume. This will make more sense when you see the code.

Constant Density Mediums

First, let’s start with a volume of constant density. A ray going through there can either scatter inside the volume, or it can make it all the way through like the middle ray in the figure. More thin transparent volumes, like a light fog, are more likely to have rays like the middle one. How far the ray has to travel through the volume also determines how likely it is for the ray to make it through.

As the ray passes through the volume, it may scatter at any point. The denser the volume, the more likely that is. The probability that the ray scatters in any small distance Δ𝐿 is:

$probability = C \cdot \Delta L$

where 𝐶 is proportional to the optical density of the volume. If you go through all the differential equations, for a random number you get a distance where the scattering occurs. If that distance is outside the volume, then there is no “hit”. For a constant volume we just need the density 𝐶 and the boundary. I’ll use another hittable for the boundary. The resulting class is:

The scattering function of isotropic picks a uniform random direction:

And the hit function is:

hit函数如下:

The reason we have to be so careful about the logic around the boundary is we need to make sure this works for ray origins inside the volume. In clouds, things bounce around a lot so that is a common case.

In addition, the above code assumes that once a ray exits the constant medium boundary, it will continue forever outside the boundary. Put another way, it assumes that the boundary shape is convex. So this particular implementation will work for boundaries like boxes or spheres, but will not work with toruses or shapes that contain voids. It’s possible to write an implementation that handles arbitrary shapes, but we’ll leave that as an exercise for the reader.

Rendering a Cornell Box with Smoke and Fog Boxes

If we replace the two blocks with smoke and fog (dark and light particles), and make the light bigger (and dimmer so it doesn’t blow out the scene) for faster convergence:

We get:

测试场景

Let’s put it all together, with a big thin mist covering everything, and a blue subsurface reflection sphere (we didn’t implement that explicitly, but a volume inside a dielectric is what a subsurface material is). The biggest limitation left in the renderer is no shadow rays, but that is why we get caustics and subsurface for free. It’s a double-edged design decision.

Running it with 10,000 rays per pixel yields:

Now go off and make a really cool image of your own! See https://in1weekend.blogspot.com/ for pointers to further reading and features, and feel free to email questions, comments, and cool images to me at ptrshrl@gmail.com.