Hidden Surface Removal 隐蔽表面清除

We can now render any scene from any point of view, but the resulting image is visually simple: we’re rendering objects in wireframe, giving the impression that we’re looking at the blueprint of a set of objects, not at the objects themselves.
我们现在可以从任何角度渲染任何场景,但产生的图像在视觉上是简单的:我们是在用线框渲染物体,给人的印象是我们在看一组物体的蓝图,而不是看物体本身。

The remaining chapters of this book focus on improving the visual quality of the rendered scene. By the end of this chapter, we’ll be able to render objects that look solid (as opposed to wireframe). We already developed an algorithm to draw filled triangles, but as we will see, using that algorithm correctly in a 3D scene is not as simple as it might seem!
本书其余各章的重点是提高渲染场景的视觉质量。在本章结束时,我们将能够渲染看起来是实体的物体(相对于线框)。我们已经开发了一种绘制填充三角形的算法,但正如我们将看到的那样,在三维场景中正确使用该算法并不像看起来那么简单

Rendering Solid Objects 渲染实体物体

The first idea that comes to mind when we want to make solid objects look solid is to use the function DrawFilledTriangle that we developed in Chapter 7 (Filled Triangles) to draw each triangle of the objects using a random color (Figure 12-1).
当我们想让实心物体看起来是实心的时候,首先想到的是使用我们在第7章(填充三角形)中开发的函数 DrawFilledTriangle ,使用随机颜色绘制物体的每个三角形(图12-1)。

Figure 12-1: Using DrawFilledTriangle instead of DrawWireframeTriangle doesn’t produce the results we expect.

The shapes in Figure 12-1 don’t quite look like cubes, do they? If you look closely, you’ll see what the problem is: parts of the back faces of the cube are drawn on top of the front faces! This is because we’re blindly drawing 2D triangles on the canvas in a “random order” or, more precisely, in the order they happen to be defined in the Triangles list of the model, without taking into account the spatial relationships between them.
图12-1中的图形看起来不太像立方体,对吗?如果你仔细观察,就会发现问题所在:立方体的部分背面被画在了正面的上面!这是因为我们在画布上盲目地按照 "随机顺序 "或者更准确地说,按照它们发生的顺序来画二维三角形。这是因为我们在画布上盲目地按 "随机顺序 "绘制二维三角形,或者更准确地说,按它们恰好在模型的 Triangles 列表中定义的顺序绘制,而没有考虑到它们之间的空间关系。

You might be tempted to go back to the model definition and change the order of the triangles to fix this problem. However, if our scene includes another instance of the cube that is rotated 180, we’d go back to the original problem. In short, there’s no single “correct” triangle order that will work for every instance and camera orientation. What should we do?
你可能会想回到模型定义中去,改变三角形的顺序来解决这个问题。但是,如果我们的场景包括另一个旋转了180∘的立方体实例,我们又会回到原来的问题上。简而言之,没有一个 "正确 "的三角形顺序可以适用于每一个实例和摄像机方向。我们应该怎么做呢?

Painter’s Algorithm 画家的算法

A first solution to this problem is known as the painter’s algorithm. Real-life painters draw backgrounds first, and then cover parts of them with foreground objects. We could achieve the same effect by drawing all the triangles in the scene back to front. To do so, we’d apply the model and camera transforms and sort the triangles according to their distance to the camera.
这个问题的第一个解决方案被称为画家的算法。现实生活中的画家先画背景,然后用前景物体覆盖部分背景。我们可以通过从后往前绘制场景中的所有三角形来达到同样的效果。要做到这一点,我们要应用模型和摄像机的变换,并根据三角形与摄像机的距离对其进行排序。

This works around the “no single correct order” problem explained above, because now we’re looking for a correct ordering for a specific relative position of the objects and the camera.
这就解决了上面解释的 "没有单一的正确顺序 "的问题,因为现在我们要为物体和摄像机的特定相对位置寻找一个正确的顺序。

Although this would indeed draw the triangles in the correct order, it has some drawbacks that make it impractical.
虽然这确实会以正确的顺序画出三角形,但它有一些缺点,使之不切实际。

First, it doesn’t scale well. The most efficient sorting algorithm known to humans is O(nlog(n)), which means the runtime more than doubles if we double the number of triangles. (To illustrate, sorting 100 triangles would take approximately 200 operations; sorting 200 triangles would take 460, not 400; and sorting 800 triangles would take 2,322 operations, not 1,840!) In other words, this works for small scenes, but it quickly becomes a performance bottleneck as the complexity of the scene increases.
首先,它不能很好地扩展。人类已知的最有效的排序算法是 O(n⋅log(n)) 𝑂 ( 𝑛 ⋅ log ( 𝑛 )),这意味着如果我们把三角形的数量增加一倍,运行时间会增加一倍以上。(举例说明,对100个三角形进行排序需要大约200次操作;对200个三角形进行排序需要460次,而不是400次;对800个三角形进行排序需要2322次,而不是1840次!)换句话说,这对小场景是有效的,但随着场景的复杂性增加,它很快就会成为性能瓶颈。

Second, it requires us to know the whole list of triangles at once. This requires a lot of memory and stops us from using a stream-like approach to rendering. We want our renderer to be like a pipeline, where model triangles enter on one end and pixels come out the other end, but this algorithm doesn’t start drawing pixels until every triangle has been transformed and sorted.
第二,它要求我们一次就知道整个三角形列表。这需要大量的内存,并阻止我们使用类似于流的方法进行渲染。我们希望我们的渲染器就像一个流水线,模型三角形从一端进入,像素从另一端出来,但是这个算法在每个三角形被转换和排序后才开始绘制像素。

Third, even if we’d be willing to live with these limitations, there are cases where a correct ordering of triangles just doesn’t exist at all. Consider the case in Figure 12-2. We will never be able to sort these triangles in a way that produces the correct results.
第三,即使我们愿意接受这些限制,在有些情况下,三角形的正确排序根本不存在。考虑一下图12-2中的情况。我们永远无法以产生正确结果的方式对这些三角形进行排序。

Figure 12-2: There is no way to sort these triangles “back-to-front.”

Depth Buffering 深度缓冲

We can’t solve the ordering problem at the triangle level, so let’s try to solve it at the pixel level.
我们无法在三角形层面上解决排序问题,所以让我们尝试在像素层面上解决这个问题。

For each pixel on the canvas, we want to paint it with the “correct” color, where the “correct” color is the color of the object that is closest to the camera. In Figure 12-3, that’s P1.
对于画布上的每个像素,我们要给它涂上 "正确 "的颜色,其中 "正确 "的颜色是最靠近相机的物体的颜色。在图12-3中,这就是P 1 𝑃 1。

Figure 12-3: Both P1 and P2 project to the same P ’ on the canvas. Because P1 is closer to the camera than P2, we want to paint P ’ the color of P1.

At any time during rendering, each pixel on the canvas represents one point in the scene (before we draw anything, it represents a point infinitely far away). Suppose that for each pixel on the canvas, we kept the Z coordinate of the point it currently represents. When we need to decide whether to paint a pixel with the color of an object, we will do it only if the Z coordinate of the point we’re about to paint is smaller than the Z coordinate of the point that is already there. This guarantees that a pixel representing a point in the scene is never drawn over by a pixel representing a point that is farther away from the camera.
在渲染过程中的任何时候,画布上的每个像素都代表场景中的一个点(在我们绘制任何东西之前,它代表一个无限远的点)。假设对于画布上的每个像素,我们都保留了它当前代表的点的Z𝑍坐标。当我们需要决定是否给一个像素画上物体的颜色时,只有当我们要画的点的Z𝑍坐标小于已经存在的点的Z𝑍坐标时,我们才会这样做。这就保证了代表场景中某个点的像素永远不会被代表离相机更远的点的像素画过去。

Let’s go back to Figure 12-3. Suppose that because of the order of the triangles in a model, we want to paint P2 first and P1 second. When we paint P2, the pixel is painted red, and its associated Z value becomes ZP2. Then we want to paint P1, and since ZP2>ZP1, we paint the pixel blue and we get the correct result.
让我们回到图12-3。假设由于模型中三角形的顺序,我们想先画P 2 𝑃 2,后画P 1 𝑃 1。当我们涂抹P 2 𝑃 2时,该像素被涂成红色,其相关的Z 𝑍值变成Z P 2 𝑍 𝑃 2。然后我们想把P 1 𝑃 1画出来,由于Z P 2 > Z P 1 𝑍 𝑃 2 > 𝑍 𝑃 1,我们把这个像素画成蓝色,就得到了正确的结果。

In this particular case, we’d have gotten the correct result regardless of the values of Z, because the points happened to come in a convenient order. But what if we wanted to paint P1 first and P2 second? We first paint the pixel blue and store ZP1; but when we want to paint P2, we see that ZP2>ZP1, so we don’t paint it—because if we did, P1 would be covered by P2, which is farther away! We get a blue pixel again, which is the correct result.
在这种特殊情况下,无论Z𝑍的值是多少,我们都会得到正确的结果,因为这些点恰好以一种方便的顺序出现。但如果我们想先画P1𝑃1,后画P2𝑃2呢?我们先把像素涂成蓝色,并把Z P 1 𝑍 𝑃 1储存起来;但当我们想涂P 2 𝑃 2时,我们发现Z P 2 > Z P 1 𝑍 𝑃 2 > 𝑍 1,所以我们不涂它,因为如果我们这样做,P 1 𝑃 1会被更远的P 2 𝑃 2覆盖我们又得到了一个蓝色的像素,这就是正确的结果。

In terms of implementation, we need a buffer to store the Z coordinate of every pixel on the canvas; we call this the depth buffer. It has the same dimensions as the canvas, but its elements are real numbers representing depth values, not pixels.
在实施方面,我们需要一个缓冲区来存储画布上每个像素的Z𝑍坐标;我们称之为深度缓冲区。它的尺寸与画布相同,但其元素是代表深度值的实数,而不是像素。

But where do the Z values come from? These should be the Z values of the points after they’re transformed but before they’re perspective-projected. However, this only gives us Z values for vertices; we need a Z value for every pixel of every triangle.
但Z𝑍值是从哪里来的?这些应该是点在被变换后但在被透视投影前的Z𝑍值。然而,这只给了我们顶点的Z𝑍值;我们需要每个三角形的每个像素的Z𝑍值。

Here is yet another application of the attribute-mapping algorithm we developed in Chapter 8 (Shaded Triangles). Why not use Z as the attribute and interpolate it across the face of the triangle, just like we did before with color-intensity values? By now you know how to do it: take the values of z0, z1, and z2; compute z01, z02, and z012; combine them to get z_left and z_right; then, for each horizontal segment, compute z_segment. Finally, instead of blindly calling PutPixel(x, y, color), we do this:
这里是我们在第8章(阴影三角形)中开发的属性映射算法的另一个应用。为什么不使用Z𝑍作为属性,并在三角形的各个面上进行插值,就像我们之前对颜色强度值所做的那样?现在你已经知道怎么做了:取 z0z1z2 的值;计算 z01z02z012 ;结合它们得到 z_leftz_right ;然后,对于每个水平段,计算 z_segment 。最后,我们不盲目地调用 PutPixel(x, y, color) ,而是这样做。

z = z_segment[x - xl]
if (z < depth_buffer[x][y]) {
    canvas.PutPixel(x, y, color)
    depth_buffer[x][y] = z
}

For this to work correctly, every entry in depth_buffer should be initialized to + (or just “a very big value”). This guarantees that the first time we want to draw a pixel, the condition will be true, because any point in the scene is closer to the camera than a point infinitely far away.
为了使其正常工作, depth_buffer 中的每个条目都应该被初始化为+∞+∞(或者只是 "一个非常大的值")。这保证了我们第一次想画一个像素时,条件将是真的,因为场景中的任何一点都比无限远的一点更接近摄像机。

The results we get now are much better—check out Figure 12-4.
现在我们得到的结果要好得多--请看图12-4。

Figure 12-4: The cubes now look like cubes, regardless of the ordering of their triangles.

Source code and live demo >>
源代码和现场演示 >>

Using 1/Z instead of Z
用1/Z而不是Z

The results look much better, but what we’re doing is subtly wrong. The values of Z for the vertices are correct (they come from data, after all), but in most cases the linearly interpolated values of Z for the rest of the pixels are incorrect. This might not even result in a visible difference at this point, but it would become an issue later.
结果看起来要好得多,但我们所做的事情却有微妙的错误。顶点的Z𝑍值是正确的(毕竟它们来自数据),但在大多数情况下,其余像素的Z𝑍的线性插值是不正确的。这在此时可能甚至不会导致明显的差异,但以后就会成为一个问题。

To see how the values are wrong, consider the simple case of a line segment from A(1,0,2) to B(1,0,10), with its midpoint M at (0,0,6). Specifically, because M is the midpoint of AB, we know that Mz=(Az+Bz)/2=6. Figure 12-5 shows this line segment.
为了了解这些数值是如何错误的,请考虑一个简单的例子:从A(-1,0,2) 𝐴 ( - 1 , 0 , 2 ) 到B(1,0,10) 𝐵 ( 1 , 0 , 10 ) 的线段,其中点M 𝑀位于(0,0,6) ( 0 , 0 , 6 ) 。具体来说,因为M𝑀是AB𝐴𝐵的中点,我们知道M z =( A z + B z )/2=6 𝑀 𝑧 = ( 𝐴𝑧 + 𝐵 𝑧 ) / 2 = 6。图12-5显示了这条线段。

Figure 12-5: A line segment AB and its midpoint M

Let’s compute the projection of these points with d=1. Applying the perspective projection equations, we get Ax=Ax/Az=1/2=0.5. Similarly, Bx=0.1 and Mx=0. Figure 12-6 shows the projected points.
让我们计算一下这些点的投影,d=1𝑑=1。应用透视投影方程,我们得到A ′ x = A x / A z =-1/2=-0.5 𝐴 ′ = 𝐴 / 𝐴 𝑧 = - 1 / 2 = - 0.5。同样地,B ′ x =0.1 𝐵 𝑥 ′ =0.1,M ′ x =0 𝑀 𝑥 ′ =0。 图12-6显示了投影点。

Figure 12-6: The points A, B, and M projected onto the projection plane

AB is a horizontal segment on the viewport. We know the values of Az and Bz. Let’s see what happens if we try to compute the value of Mz using linear interpolation. The implied linear function looks like Figure 12-7.
A ′ B ′ 𝐴 ′ 𝐵 ′ 是视口上的一个水平段。我们知道A z 𝐴 𝑧和B z 𝐵 𝑧的值。让我们看看如果我们试图用线性插值计算M z 𝑀 𝑧的值会怎样。隐含的线性函数看起来像图12-7。

Figure 12-7: The values of Az and Bz for Ax’ and Bx’ define a linear function z = f(x’).

The slope of the function is constant, so we can write
函数的斜率是常数,所以我们可以写出

MzAzMxAx=BzAzBxAx
M z - A z M ′ x - A ′ x = B z - A z B ′ x - A ′ x 𝑀 𝑧 - 𝐴 𝑀 𝑥 ′ - 𝐴 ′ = 𝐵 𝑧 - 𝐴 𝐵 𝑥 ′ - 𝐴 ′

We can manipulate that expression to solve for Mz:
我们可以操作这个表达式来解决M z 𝑀 𝑧。

Mz=Az+(MxAx)(BzAzBxAx)
M z = A z +(M ′ x -A ′ x )( B z - A z B ′ x -A ′ x ) 𝑀 𝑧 = 𝐴 𝑧 + ( 𝑀 𝑥 ′ - 𝐴 ′ ) ( 𝐵 𝑧 - 𝐴 𝐵 𝑧 𝑥 ′ - 苺 ′ )

If we plug in the values we know and do some arithmetic, we get
如果我们插入我们知道的数值并做一些算术,我们会得到

Mz=2+(0(0.5))(1020.1(0.5))=2+(0.5)(80.6)=8.666
M z =2+(0-(-0.5))( 10-2 0.1-(-0.5)) =2+(0.5)( 8 0.6 ) =8.666 𝑀 𝑧 =2 + ( 0- ( - 0.5 ) ) ( 10 - 2 0.1 - ( - 0.5 ) ) = 2 + ( 0.5 ) ( 8 0.6 ) = 8.666

This says that the value of Mz is 8.666, but we know for a fact it’s actually 6!
这说明M z 𝑀 𝑧的值是8.666 8.666,但我们知道事实上它是6 6!

Where did we go wrong? We’re using linear interpolation, which we know works well, and we’re feeding it the correct values, which come from data, so why is the result wrong?
我们哪里出错了?我们使用的是线性插值,我们知道这很好用,而且我们给它提供了正确的数值,这些数值来自于数据,那么为什么结果是错误的呢?

Our mistake is hidden in the implicit assumption we make when we use linear interpolation: that the function we are interpolating is linear to begin with! In this case, it turns out it isn’t.
我们的错误隐藏在我们使用线性插值时的隐含假设中:我们所插值的函数一开始就是线性的!在这种情况下,事实证明不是。在这种情况下,事实证明它不是。

If Z=f(x,y) was a linear function of x and y, we could write it as Z=Ax+By+C for some values of A, B, and C. This kind of function has the property that the difference of its value between two points depends on the difference between the points but not on the points themselves:
如果Z=f( x ′ , y ′ ) 𝑍 = 𝑓 ( 𝑥 ′ , 𝑦 ′ ) 是x ′ 𝑥 ′ 和y ′ 𝑦 ′ 的线性函数,我们可以把它写成Z=A x ′ +B y ′ +C 𝑍 = 𝐴 ′ + 𝐵 𝑦 ′ + 𝐶的某些值A𝐴, B𝐵, 和C 瞐。这种函数的特性是它在两点之间的数值之差取决于两点之间的差异,而不取决于两点本身。

f(x+Δx,y+Δy)f(x,y)=[A(x+Δx)+B(y+Δy)+C][Ax+By+C]
f( x ′ +Δx, y ′ +Δy)-f( x ′ , y ′ )=[A( x ′ +Δx)+B( y ′ +Δy)+C]-[A⋅ x ′ +B⋅ y ′ +C] 𝑓 ( 𝑥 ′ + Δ 。𝑦 ′ + Δ 𝑦 ) - 𝑓 ( 𝑥 ′ , 𝑦 ′ ) = [ 𝐴 ( 𝑥 ′ + Δ 𝑥 ) + 𝐵 ( 𝑦 ′ + Δ 𝑦 ) + 𝐶 ] - [ 𝐴 ⋅ 𝑥 ′ + 𝐵 ⋅ Δ + 𝐶 ]
=A(x+Δxx)+B(y+Δyy)+CC
=A( x ′ +Δx- x ′ )+B( y ′ +Δy- y ′ )+C-C = 𝐴 ( 𝑥 ′ + Δ 𝑥 - ′ ) + 𝐵 ( 𝑦 ′ + Δ 𝑦 - 𝑦 ′ ) + 𝐶-𝐶
=AΔx+BΔy
=AΔx+BΔy = 𝐴 Δ 𝑥+𝐵 Δ 𝑦

That is, for a given difference in screen coordinates, the difference in Z would always be the same.
也就是说,对于屏幕坐标的特定差异,Z𝑍的差异将总是相同的。

More formally, the equation of the plane that contains the line segment we’re studying is
更正式地说,包含我们正在研究的线段的平面的方程是

Ax+By+Cz+D=0

On the other hand we have the perspective projection equations:
另一方面,我们有透视投影方程。

x=xdz

y=xdz

We can get x and y back from these:
我们可以通过这些得到x𝑥和y𝑦。

x=zxd
x= z⋅ x ′ d𝑥 = 𝑧 𝑑

y=zyd

If we replace x and y in the plane equation with these expressions, we get
如果我们用这些表达式替换平面方程中的x𝑥和y𝑦,我们可以得到

Axz+Byzd+Cz+D=0
A x ′ z+B y ′ z d +Cz+D=0 𝐴 ′ 𝑧 + 𝐵 𝑦 ′ 𝑧 𝑑 + 𝐶 𝑧+ 𝐷 = 0

Multiplying by d and then solving for z,
乘以d𝑑,然后求出z𝑧。

Axz+Byz+dCz+dD=0
A x ′ z+B y ′ z+dCz+dD=0 𝐴 ′ 𝑧 + 𝐵 𝑦 ′ 𝑧 + 𝑑 𝐶 𝑧 + 𝐷 = 0

(Ax+By+dC)z+dD=0
(A x ′ +B y ′ +dC)z+dD=0 ( 𝐴 ′ +𝐵 𝑦 ′ +𝑑 𝐶 ) 𝑧 +𝑑 𝐷 = 0

z=dDAx+By+dC
z= -dD A x ′ +B y ′ +dC 𝑧 = - 𝑑 𝐷 𝐴 ′ + 𝐵 𝑦 ′ + 𝑑 𝐶

This is clearly not a linear function of x and y, and this is why linearly interpolating values of z gave us an incorrect result.
这显然不是x ′ 𝑥 ′ 和y ′ 𝑦 ′ 的线性函数,这就是为什么线性插值z 𝑧 给我们的结果不正确。

However, if we compute 1/z instead of z, we get
然而,如果我们计算1/z 1 / 𝑧而不是z 𝑧,我们会得到

1/z=Ax+By+dCdD
1/z = A x ′ +B y ′ +dC -dD 1 / 𝑧 = 𝐴 ′ + 𝐵 𝑦 ′ + 𝑑 𝐶 - 𝑑 𝐷

This clearly is a linear function of x and y. This means we could linearly interpolate values of 1/z and get the correct results.
这显然是x ′ 𝑥 ′ 和y ′ 𝑦 ′ 的线性函数。这意味着我们可以对1/z 1 / 𝑧的值进行线性插值,并得到正确的结果。

In order to verify that this works, let’s calculate the interpolated value for Mz, but this time using the linear interpolation of 1/z:
为了验证这一点,让我们计算M z 𝑀 𝑧的内插值,但这次使用1/z 1 / 𝑧的线性内插。

M1zA1zMxAx=B1zA1zBxAx
M 1 z - A 1 z M ′ x - A ′ x = B 1 z - A 1 z B ′ x - A ′ x 𝑀 1 𝑧 - 𝐴 1 𝑧 𝑀 𝑥 ′ - 𝐴 ′ = 𝐵 1 𝑧 - 𝐴 1 𝐵 𝑥 ′ - 𝐴 ′

M1z=A1z+(MxAx)(B1zA1zBxAx)
M 1 z = A 1 z +( M ′ x - A ′ x )( B 1 z - A 1 z B ′ x - A ′ x ) 𝑀 1 𝑧 = 𝐴 1 𝑧 + ( 𝑀 𝑥 ′ - 𝐴 ′ ) ( 𝐵 1 𝑧 - 𝐴 1 𝑧 𝐵 𝑥 ′ - 𝐴 ′ )

M1z=12+(0(0.5))(110120.1(0.5))=0.166666
M 1 z = 1 2 + ( 0-(-0.5))( 1 10 - 1 2 0.1-(-0.5))=0.166666 𝑀 1 𝑧 = 1 2 + ( 0 - ( - 0.5 ) ) ( 1 10 - 1 2 0.1 - ( - 0.5 ) ) =0.166666

And therefore 因此

Mz=1M1z=10.166666=6
M z = 1 M 1 z = 1 0.166666 = 6 𝑀 𝑧 = 1 𝑀 1 𝑧 = 1 0.166666 = 6

This value is correct, in the sense that it matches our original calculation of Mz based on the geometry of the line segment.
这个值是正确的,因为它与我们最初根据线段的几何形状计算的M z 𝑀 𝑧相匹配。

All of this means we need to use values of 1/z instead of values of z for depth buffering. The only practical differences in the pseudocode are that every entry in the buffer should be initialized to 0 (which is conceptually 1/+), and that the comparison should be inverted (we keep the bigger value of 1/z, which corresponds to a smaller value of z).
所有这些意味着我们需要使用1/z 1 / 𝑧的值而不是z 𝑧的值来进行深度缓冲。伪代码中唯一的实际差别是,缓冲区中的每一个条目都应该被初始化为0 0(概念上是1/+∞ 1/+∞),而且比较应该是倒置的(我们保留较大的1/z 1 / 𝑧的值,这相当于较小的z 𝑧的值)。

Back Face Culling 背面删减

Depth buffering produces the desired results. But can we make things even faster?
深度缓冲产生了预期的结果。但我们能不能让事情变得更快?

Going back to the cube, even if each pixel ends up having the right color, many of them are painted over several times. For example, if a back face of the cube is rendered before a front face, many pixels will be painted twice. This can be costly. So far we’ve been computing 1/z for every pixel, but soon we’ll add more attributes, such as illumination. As the number of per-pixel operations we need to perform increases, computing pixels that will never be visible becomes more and more wasteful.
回到立方体上,即使每个像素最后都有正确的颜色,但其中许多像素会被涂抹几次。例如,如果立方体的一个背面在一个正面之前被渲染,许多像素将被涂抹两次。这可能是昂贵的。到目前为止,我们一直在为每个像素计算1/z 1 / 𝑧,但很快我们就会增加更多的属性,比如光照。随着我们需要执行的每个像素的操作数量的增加,计算那些永远不会出现的像素会变得越来越浪费。

Can we discard pixels earlier, before we go into all of this computation? It turns out we can discard entire triangles before we even start rendering!
我们能不能在进入所有这些计算之前,提前丢弃像素?事实证明,我们可以在开始渲染之前丢弃整个三角形

So far we’ve been talking informally about front faces and back faces. Imagine every triangle has two distinct sides; it’s impossible to see both sides of a triangle at the same time. In order to distinguish between the two sides, we’ll stick an imaginary arrow on each triangle, perpendicular to its surface. Then we’ll take the cube and make sure every arrow is pointing out. Figure 12-8 shows this idea.
到目前为止,我们一直在非正式地谈论正面和背面。想象一下,每个三角形都有两个不同的侧面;不可能同时看到三角形的两个侧面。为了区分两边,我们将在每个三角形上贴一个假想的箭头,垂直于它的表面。然后我们再拿起立方体,确保每个箭头都指向外面。图12-8显示了这个想法。

Figure 12-8: A cube viewed from above, with arrows on each triangle pointing out

These arrows let us classify each triangle as “front” or “back,” depending on whether they point toward the camera or away from the camera. More formally, if the view vector and this arrow (which is actually a normal vector of the triangle) form an angle of less than 90, the triangle is front-facing; otherwise, it’s back-facing (Figure 12-9).
这些箭头让我们把每个三角形划分为 "正面 "或 "背面",这取决于它们是指向摄像机还是远离摄像机。更正式地说,如果视图矢量和这个箭头(实际上是三角形的法线矢量)形成一个小于90∘的角度,那么这个三角形就是正面的;否则就是背面的(图12-9)。

Figure 12-9: The angle between the view vector and the normal vector of a triangle lets us classify it as front- facing or back-facing.

At this point, we need to impose a restriction on our 3D models: that they are closed. The exact definition of closed is pretty involved, but fortunately an intuitive understanding is enough. The cube we’ve been working with is closed; we can only see its exterior. If we removed one of its faces, it wouldn’t be closed because we could see inside it. This doesn’t mean we can’t have objects with holes or concavities; we would just model these with thin “walls.” See Figure 12-10 for some examples.
在这一点上,我们需要对我们的三维模型施加一个限制:它们是封闭的。封闭的确切定义是相当复杂的,但幸运的是,有一个直观的理解就足够了。我们一直在使用的立方体是封闭的;我们只能看到它的外部。如果我们去掉它的一个面,它就不会是封闭的,因为我们可以看到它的内部。这并不意味着我们不能有带孔或凹陷的物体;我们只是用薄薄的 "墙 "来模拟这些物体。一些例子见图12-10。

Figure 12-10: Some examples of open and closed objects

Why impose this restriction? Closed objects have the interesting property that the set of front faces completely covers the set of back faces, no matter the orientation of the model or the camera. This means we don’t need to draw the back faces at all, saving valuable computation time.
为什么要施加这个限制?封闭物体有一个有趣的特性,即无论模型或相机的方向如何,前脸的集合完全覆盖后脸的集合。这意味着我们根本不需要绘制背面的面孔,从而节省了宝贵的计算时间。

Since we can discard (cull) all the back faces, this algorithm is called back face culling. Its pseudocode is remarkably simple for an algorithm that can cut our rendering time by half!
由于我们可以丢弃(cull)所有的背面,这种算法被称为背面剔除(back face culling)。对于一个可以将我们的渲染时间减少一半的算法来说,它的伪代码是非常简单的。

CullBackFaces(object, camera) {
  for T in object.triangles {
    if T is back-facing {
      remove T from object.triangles
    }
  }
}
Listing 12-1: The back face culling algorithm
清单12-1:背脸剔除算法

Let’s take a more detailed look at how to determine whether a triangle is front-facing or back-facing.
让我们更详细地看看如何确定一个三角形是正面的还是背面的。

Classifying Triangles 三角形的分类

Suppose we have the normal vector N of a triangle and the vector V from a vertex of the triangle to the camera. Now suppose N points to the outside of the object. In order to classify the triangle as front-facing or back-facing, we compute the angle between N and V and check whether they’re within 90 of each other.
假设我们有一个三角形的法向量N ⃗ 𝑁 → 和从三角形的一个顶点到相机的向量V ⃗ 𝑉 →。现在假设N ⃗ 𝑁 → 指向物体的外部。为了把这个三角形划分为正面或背面,我们计算N ⃗ 𝑁 →和V ⃗ 𝑉 →之间的角度,并检查它们是否在90 ∘90 ∘之内。

We can again use the properties of the dot product to make this simpler. Remember that if α is the angle between N and V, then
我们可以再次利用点积的特性来使之更简单。记住,如果α𝛼是N⃗𝑁→与V⃗𝑉→之间的角度,那么

N,V|N||V|=cos(α)
⟨ N ⃗ , V ⃗ ⟩ | N ⃗ || V ⃗ | =cos(α) ⟨ 𝑁 → , 𝑉 → ⟩ | 𝑁 → | 𝑉 → =cos ( 𝛼 )

Because cos(α) is non-negative for |α|90, we only need to know the sign of this expression to classify a triangle as front-facing or back-facing. Note that |N| and |V| are always positive, so they don’t affect the sign of the expression. Therefore
因为cos(α)cos ( 𝛼 )对于|α|≤90 ∘ | 𝛼 | ≤90 ∘来说是非负的,所以我们只需要知道这个表达式的符号就可以将一个三角形分为正面和背面。请注意,| N ⃗ | 𝑁 → | 和| V ⃗ | 𝑉 → | 总是正数,所以它们不影响这个表达式的符号。因此

sign(N,V)=sign(cos(α))
sign(⟨ N ⃗ , V ⃗ ⟩)=sign(cos(α)) s i g n ( ⟨ 𝑁 → , 𝑉 → ⟩ ) = s i g n ( cos ( 𝛼 )

The classification criterion is simply this:
分类标准简单地说就是这样。

N,V 0
⟨ n ⃗ , v ⃗ ⟩≤ ⟨ 𝑁 → , 𝑉 → ⟩ ≤ 0
Back-facing 背向式
N,V> 0
⟨ n ⃗ , v ⃗ ⟩> 𝑁 → , 𝑉 → ⟩> 0
Front-facing 前置式

The edge case N,V=0 corresponds to the case where we’re looking at the edge of a triangle head on—that is, when the camera and the triangle are coplanar. We can classify this triangle either way without affecting the result much, so we choose to classify it as back-facing to avoid dealing with degenerate triangles.
边缘情况 ⟨ N ⃗ , V ⃗ ⟩=0 𝑁 → , 𝑉 → ⟩=0 对应于我们正面观察三角形边缘的情况,也就是说,当相机和三角形共面时。我们可以在不影响结果的情况下对这个三角形进行分类,所以我们选择把它归类为背对着的三角形,以避免处理退化的三角形。

Where do we get the normal vector from? It turns out there’s a vector operation, the cross product A×B, that takes two vectors A and B and produces a vector perpendicular to both (for a definition of this operation, see Appendix A (Linear Algebra)). In other words, the cross product of two vectors on the surface of a triangle is a normal vector of that triangle. We can easily get two vectors on the triangle by subtracting its vertices from each other. So computing the direction of the normal vector of the triangle ABC is straightforward:
我们从哪里得到法向量呢?原来有一个向量运算,即交叉积A ⃗ × B ⃗ 𝐴 → × 𝐵 →,它取两个向量A ⃗ 𝐴 → 和B ⃗ 𝐵 → 并产生一个与两者垂直的向量(关于这个运算的定义,见附录A(线性代数) )。换句话说,一个三角形表面上的两个向量的交积是该三角形的法向量。我们可以通过将三角形的顶点相互减去,很容易得到三角形上的两个向量。所以计算三角形ABC的法向量的方向𝐴𝐵 𝐶是很简单的。

V1=BA
v 1 → =b-a 𝑉 1 → = 𝐵 - 𝐴
V2=CA
v 2 → =c-a 𝑉 2 → = 𝐶 - 𝐴
N=V1×V2
n ⃗ = v 1 → × v 2 → 𝑁 → = 𝑉 1 → × 𝑉 2 →

Note that “the direction of the normal vector” is not the same as “the normal vector.” There are two reasons for this. The first one is that |N| isn’t necessarily equal to 1. This isn’t really important because normalizing N would be trivial and because we only care about the sign of N,V.
请注意,"法向量的方向 "与 "法向量 "不一样。这有两个原因。第一个原因是 | N ⃗ | | 𝑁 → | 不一定等于1 1。这其实并不重要,因为将N ⃗ 𝑁 →归一化是微不足道的,而且我们只关心⟨ N ⃗ 的符号,V ⃗ ⟩ 𝑁 → ,𝑉 → ⟩ 。

The second reason is that if N is a normal vector of ABC, so is N, and in this case we care deeply about the direction N points in, because this is exactly what lets us classify triangles as either front-facing or back-facing.
第二个原因是,如果N ⃗ 𝑁 →是ABC 𝐴 𝐵 𝐶的法向量,那么-N → - 𝑁 →也是,在这种情况下,我们非常关心N ⃗ 𝑁 →指向的方向,因为这正是让我们把三角形分为正面或背面的原因。

Moreover, the cross product of two vectors is not commutative: V1 × V2=(V2×V1). In other words, the order of the vectors in this operation matters. And since we defined V1 and V2 in terms of A, B, and C, this means the order of the vertices in a triangle matters. We can’t treat the triangles ABC and ACB as the same triangle anymore.
此外,两个向量的交叉积不是换算的。v 1 → × v 2 → =-( v 2 → × v 1 → 𝑉 1 → × 𝑉 2 → = - ( 𝑉 2 → × 𝑉 1 → ) 。换句话说,这个操作中的向量顺序很重要。由于我们用A𝐴、B𝐵和C𝐶来定义V 1𝑉 1和V 2𝑉 2,这意味着三角形中顶点的顺序很重要。我们不能再把三角形ABC 𝐴 𝐵 𝐶和ACB 𝐴 𝐵 视为同一个三角形。

Fortunately, none of this is random. Given the definition of the cross product operation, the way we defined V1 and V2, and the coordinate system we use (X to the right, Y up, Z forward), there is a very simple rule that determines the direction of the normal vector: if the vertices of the triangle ABC are in clockwise order when you look at them from the camera, the normal vector as calculated above will point toward the camera—that is, the camera is looking at the front face of the triangle.
幸运的是,这一切都不是随机的。鉴于交叉积运算的定义,我们定义V 1 𝑉 1和V 2 𝑉 2的方式,以及我们使用的坐标系(X向右,Y向上,Z向前),有一个非常简单的规则来决定法向量的方向。如果当你从相机上看三角形ABC𝐴 𝐵 𝐶的顶点是按顺时针顺序排列的,那么上面计算的法向量就会指向相机--也就是说,相机是在看三角形的正面。

We just need to keep this rule in mind when designing 3D models manually and list the vertices of each triangle in clockwise order when looking at its front face, so that their normals point “out” when we compute them this way. Of course, the example cube model we’ve been using so far follows this rule.
我们只需要在手动设计3D模型时牢记这一规则,在观察每个三角形的正面时,按顺时针顺序列出每个三角形的顶点,这样,当我们这样计算时,它们的法线就会指向 "外面"。当然,到目前为止,我们一直在使用的立方体模型的例子就遵循了这个规则。

Summary

In this chapter, we made our renderer, which could previously only render wireframe objects, capable of rendering solid-looking objects. This is more involved than just using DrawFilledTriangle instead of DrawWireframeTriangle, because we need triangles close to the camera to obscure triangles further away from the camera.
在本章中,我们让以前只能渲染线框物体的渲染器能够渲染实体物体。这比仅仅使用 DrawFilledTriangle 而不是 DrawWireframeTriangle 要复杂得多,因为我们需要靠近摄像机的三角形来遮挡远离摄像机的三角形。

The first idea we explored was to draw the triangles from back to front, but this had a few drawbacks that we discussed. A better idea is to work at the pixel level; this idea led us to a technique called depth buffering, which produces correct results regardless of the order in which we draw the triangles.
我们探索的第一个想法是从后往前画三角形,但这有一些缺点,我们讨论过。一个更好的想法是在像素层面上工作;这个想法让我们找到了一种叫做深度缓冲的技术,无论我们以何种顺序绘制三角形,它都能产生正确的结果。

We finally explored an optional but valuable technique that doesn’t change the correctness of the results, but can save us from rendering approximately half of the triangles of the scene: back face culling. Since all the back-facing triangles of a closed object are covered by all its front-facing triangles, there’s no need to draw the back-facing triangles at all. We presented a simple algebraic way to determine whether a triangle is front- or back-facing.
我们最终探索了一个可选的但有价值的技术,它不会改变结果的正确性,但可以使我们免于渲染场景中大约一半的三角形:背脸剔除。由于一个封闭物体的所有背向三角形都被其所有的正面三角形所覆盖,所以根本不需要绘制背向三角形。我们提出了一个简单的代数方法来确定一个三角形是正面还是背面的。

Now that we can render solid-looking objects, we’ll devote the rest of this book to making these objects look more realistic.
现在我们已经可以渲染实体物体了,我们将在本书的其余部分专门讨论如何使这些物体看起来更逼真。