Perspective Projection 透视投影
So far, we have learned to draw 2D triangles on the canvas, given the 2D coordinates of their vertices. However, the goal of this book is to render 3D scenes. So in this chapter, we’ll take a break from 2D triangles and focus on how to turn 3D scene coordinates into 2D canvas coordinates. We’ll then use this to draw 3D triangles on the 2D canvas.
到目前为止,我们已经学会了在画布上绘制2D三角形,给定其顶点的2D坐标。然而,本书的目标是渲染3D场景。因此,在本章中,我们将从二维三角形中抽身出来,重点讨论如何将三维场景坐标转化为二维画布坐标。然后,我们将用它来在二维画布上绘制三维三角形。
Basic Assumptions 基本假设
Just like we did at the beginning of Chapter 2 (Basic Raytracing), we’ll start by defining a camera. We’ll use the same conventions as before: the camera is at , looking in the direction of , and its “up” vector is . We’ll also define a rectangular viewport of size and whose edges are parallel to and , at a distance from the camera. The goal is to draw on the canvas whatever the camera sees through the viewport. If you need a refresher on these concepts, refer to Chapter 2 (Basic Raytracing).
就像我们在第二章(基本光线追踪)的开头所做的那样,我们将从定义一个摄像机开始。我们将使用与之前相同的约定:摄像机位于O=(0,0,0) 𝑂 = ( 0 , 0 , 0 ) ,看向Z + → 𝑍 + → 的方向,其 "向上 "的矢量是Y + → 𝑌 + → 。我们还将定义一个尺寸为V w 𝑉 𝑤和V h 𝑉 ℎ的矩形视口,其边缘与X ⃗ 𝑋 →和Y ⃗ 𝑌 →平行,距离相机有d 𝑑。我们的目标是在画布上画出相机通过视口看到的任何东西。如果你需要复习这些概念,请参考第二章(基本光线追踪)。
Consider a point somewhere in front of the camera. We’re interested in finding , the point on the viewport through which the camera sees , as shown in Figure 9-1.
考虑在摄像机前面的某个地方有一个点P𝑃。我们感兴趣的是找到P′ 𝑃 ′,即相机通过它看到P 𝑃 的视口上的点,如图9-1所示。
This is the opposite of what we did with raytracing. Our raytracer started with a point in the canvas, and determined what it could see through that point; here, we start from a point in the scene and want to determine where it is seen on the viewport.
这与我们在光线追踪中的做法相反。我们的光线追踪器从画布中的一个点开始,并确定它可以通过这个点看到什么;在这里,我们从场景中的一个点开始,并想确定它在视口中的位置。
Finding P’ 寻找P'。
To find , let’s look at the setup shown in Figure 9-1 from a different angle, literally. Figure 9-2 shows a diagram of the setup viewed from the “right,” as if we were standing on the axis: points up, points to the right, and points at us.
为了找到P′𝑃 ′,让我们从不同的角度来看图9-1所示的设置,从字面上看。图9-2显示了从 "右边 "观察的设置图,就像我们站在X⃗ 𝑋 →轴上一样。Y + → 𝑌 + →指向上方,Z + → 𝑍 + →指向右边,X + → 𝑋 + →指向我们。
In addition to , , and , this diagram also shows the points and , which help us reason about it.
除了O𝑂、P𝑃和P′𝑃′之外,这个图还显示了A𝐴和B𝐵等点,这有助于我们进行推理。
We know that because we defined to be a point on the viewport, and we know the viewport is embedded in the plane .
我们知道P ′ z =d 𝑃 𝑧 ′ = 𝑑,因为我们定义P ′ 𝑃 ′是视口上的一个点,而且我们知道视口是嵌入在平面Z=d 𝑍 = 𝑑。
We can also see that the triangles and are similar, because their corresponding sides ( and , and , and and ) are parallel. This implies that the proportions of their sides are the same; for example:
我们还可以看到,三角形OP ′A 𝑂 𝑃 ′ 𝐴和OPB 𝑂 𝑃 𝐵是相似的,因为它们对应的边(P ′A 𝑃 ′ 𝐴和PB 𝑃 𝐵,OP 𝑂 𝑃 和 OP ′ 𝑂 𝑃 ′,以及OA 𝑂 𝐴和OB 𝐵)是平行的。这意味着它们的边的比例是相同的;例如。
From that, we get 由此,我们可以得到
The (signed) length of each segment in that equation is a coordinate of a point we know or we’re interested in: , , , and . If we substitute these in the equation we get
该方程中每段的(有符号)长度是我们知道的或我们感兴趣的一个点的坐标。|P ′ A|=P ′ y | 𝑃 ′ 𝐴 | = 𝑃 𝑦 ′, |PB|=P y | 𝑃 𝐵 | = 𝑃 𝑦, |OA|=P ′ z =d | 𝑂 𝐴 | = 𝑃 𝑧 ′ = 𝑑, 以及 |OB|=P z | 𝑂 𝐵 = 𝑃𝑧。如果我们把这些代入方程,就可以得到
We can draw a similar diagram, this time viewing the setup from above: points up, points to the right, and points at us (Figure 9-3).
我们可以画一个类似的图,这次是从上面看这个设置。Z + → 𝑍 + →指向上方,X + → 𝑋 + →指向右边,Y + → 𝑌 + →指向我们(图9-3)。
Using similar triangles again in the same way, we can deduce that
再次以同样的方式使用相似三角形,我们可以推导出
We now have all three coordinates of .
现在我们有P的所有三个坐标′𝑃 ′。
The Projection Equation 投影方程
Let’s put all this together. Given a point in the scene and a standard camera and viewport setup, we can compute the projection of on the viewport, which we call , as follows:
让我们把所有这些放在一起。给定场景中的一个点P𝑃和一个标准的相机和视口设置,我们可以计算P𝑃在视口上的投影,我们称之为P ′ 𝑃 ′,如下所示。
is on the viewport, but it’s still a point in 3D space. How do we get the corresponding point in the canvas?
P ′ 𝑃 ′ 在视口上,但它仍然是三维空间中的一个点。我们怎样才能得到画布上的对应点呢?
We can immediately drop , because every projected point is on the viewport plane. Next we need to convert and to canvas coordinates and . is still a point in the scene, so its coordinates are expressed in scene units. We can divide them by the width and height of the viewport. These are also expressed in scene units, so we obtain temporarily unit-less values. Finally, we multiply them by the width and height of the canvas, expressed in pixels:
我们可以立即放弃P ′ z 𝑃 𝑧 ′,因为每个投影点都在视口平面上。接下来我们需要将P ′ x 𝑃 𝑥 ′和P ′ y 𝑃 𝑦 ′转换成画布坐标C x 𝐶 𝑥 和C y 𝐶 𝑦。P′𝑃 ′仍然是场景中的一个点,所以它的坐标以场景单位表示。我们可以把它们除以视口的宽度和高度。这些也是用场景单位表示的,所以我们暂时得到无单位的值。最后,我们用它们乘以画布的宽度和高度,以像素表示。
This viewport-to-canvas transform is the exact inverse of the canvas-to-viewport transform we used in the raytracing part of this book. And with this, we can finally go from a point in the scene to a pixel on the screen!
这个视口到画布的变换正是我们在本书光线追踪部分所使用的画布到视口变换的逆运算。有了这个,我们终于可以从场景中的一个点到屏幕上的一个像素了
Properties of the Projection Equation
投影方程的属性
Before we move on, there are some interesting properties of the projection equation that are worth discussing.
在我们继续前行之前,投影方程有一些有趣的特性值得讨论。
The equations above should be compatible with our day-to-day experience of looking at things in the real world. For example, the farther away an object is, the smaller it looks; and indeed, if we increase , we get smaller values of and .
上述方程应该与我们在现实世界中观察事物的日常经验相一致。例如,一个物体离我们越远,它看起来就越小;事实上,如果我们增加P z 𝑃 𝑧,我们得到的P ′ x 𝑃 𝑥 ′和P ′ y 𝑃 𝑦 ′的值就越小。
However, things stop being so intuitive when we decrease the value of too much; for negative values of , that is, when an object is behind the camera, the object is still projected, but upside down! And, of course, when we’d divide by zero and the universe would implode. We’ll need to find a way to avoid these unpleasant situations; for now, we’ll assume that every point is in front of the camera and deal with this in a later chapter.
然而,当我们把P z 𝑃 𝑧的值降低得太多时,事情就不再那么直观了;对于P z 𝑃 𝑧的负值,也就是说,当一个物体在摄像机后面时,该物体仍然被投射,但却是颠倒的!所以,当P z 𝑃 𝑧=0时,我们就会除以0。当然,当P z =0 𝑃 𝑧 = 0时,我们会除以0,宇宙会内爆。我们需要找到一种方法来避免这些不愉快的情况;现在,我们将假设每一个点都在摄像机的前面,并在后面的章节中处理这个问题。
Another fundamental property of the perspective projection is that it preserves point alignment: if three points are aligned in space, their projections will be aligned on the viewport. In other words, a straight line is always projected as a straight line. This might sound too obvious to be worth mentioning, but note, for example, that the angle between two lines isn’t preserved: in real life, we see parallel lines “converge” at the horizon, such as when driving on a highway.
透视投影的另一个基本属性是它保留了点的对齐:如果三个点在空间中对齐,它们在视口上的投影也将对齐。换句话说,一条直线总是被投影成一条直线。这可能听起来太明显了,不值得一提,但请注意,例如,两条线之间的角度没有被保留:在现实生活中,我们看到平行线在地平线上 "汇合",例如在高速公路上行驶时。
The fact that a straight line is always projected as a straight line is extremely convenient for us: so far we have talked about projecting a point, but how about projecting a line segment, or even a triangle? Because of this property, the projection of a line segment between two points is the line segment between the projection of two points; and the projection of a triangle is the triangle formed by the projections of its vertices.
直线总是被投影成一条直线,这对我们来说是非常方便的:到目前为止,我们已经谈到了投影一个点,但投影一条线段,甚至是一个三角形呢?由于这一特性,两点之间的线段的投影就是两点的投影之间的线段;而一个三角形的投影就是其顶点的投影所形成的三角形。
Projecting Our First 3D Object
投射我们的第一个三维物体
This means we can go ahead and draw our first 3D object: a cube. We define the coordinates of its 8 vertices, and we draw line segments between the projections of the 12 pairs of vertices that make the edges of the cube, as seen in Listing 9-1:
这意味着我们可以继续绘制我们的第一个三维对象:一个立方体。我们定义其8个顶点的坐标,并在构成立方体边缘的12对顶点的投影之间绘制线段,如清单9-1所示。
ViewportToCanvas(x, y) {
return (x * Cw/Vw, y * Ch/Vh);
}
ProjectVertex(v) {
return ViewportToCanvas(v.x * d / v.z, v.y * d / v.z)
}
// The four "front" vertices
vAf = [-2, -0.5, 5]
vBf = [-2, 0.5, 5]
vCf = [-1, 0.5, 5]
vDf = [-1, -0.5, 5]
// The four "back" vertices
vAb = [-2, -0.5, 6]
vBb = [-2, 0.5, 6]
vCb = [-1, 0.5, 6]
vDb = [-1, -0.5, 6]
// The front face
DrawLine(ProjectVertex(vAf), ProjectVertex(vBf), BLUE);
DrawLine(ProjectVertex(vBf), ProjectVertex(vCf), BLUE);
DrawLine(ProjectVertex(vCf), ProjectVertex(vDf), BLUE);
DrawLine(ProjectVertex(vDf), ProjectVertex(vAf), BLUE);
// The back face
DrawLine(ProjectVertex(vAb), ProjectVertex(vBb), RED);
DrawLine(ProjectVertex(vBb), ProjectVertex(vCb), RED);
DrawLine(ProjectVertex(vCb), ProjectVertex(vDb), RED);
DrawLine(ProjectVertex(vDb), ProjectVertex(vAb), RED);
// The front-to-back edges
DrawLine(ProjectVertex(vAf), ProjectVertex(vAb), GREEN);
DrawLine(ProjectVertex(vBf), ProjectVertex(vBb), GREEN);
DrawLine(ProjectVertex(vCf), ProjectVertex(vCb), GREEN);
DrawLine(ProjectVertex(vDf), ProjectVertex(vDb), GREEN);We get something like Figure 9-4.
我们得到类似图9-4的东西。
Source code and live demo >>
源代码和现场演示 >>
Success! We’ve managed to go from the geometrical 3D representation of an object to its 2D representation as seen from our synthetic camera!
成功了!我们已经成功地从一个物体的几何三维表示转为从我们的合成摄像机所看到的二维表示!
Our approach is very artisanal, though. It has many limitations. What if we want to render two cubes? Would we have to duplicate most of the code? What if we want to render something other than a cube? What if we want to let the user load a 3D model from a file? We clearly need a more data-driven approach to representing 3D geometry.
不过,我们的方法是非常艺术化的。它有很多限制。如果我们想渲染两个立方体怎么办?我们是否必须重复大部分的代码?如果我们想渲染一个立方体以外的东西呢?如果我们想让用户从一个文件中加载一个三维模型呢?我们显然需要一种更多的数据驱动的方法来表示三维几何体。
Summary
In this chapter, we’ve developed the math to go from a 3D point in the scene to a 2D point on the canvas. Because of the properties of the perspective projection, we can immediately extend this to projecting line segments and then to 3D objects.
在本章中,我们已经开发了从场景中的三维点到画布上的二维点的数学方法。由于透视投影的特性,我们可以立即将其扩展到投射线段,然后再扩展到三维物体。
但是我们留下了两个重要的问题没有解决。首先,清单9-1将透视投影逻辑与立方体的几何形状混合在一起;这种方法显然无法扩展。第二,由于透视投影方程的限制,它不能处理摄像机后面的物体。我们将在接下来的两章里解决这些问题。