Computer Graphics From Scratch - Chapter 2
系列文章目录
简介: Computer Graphics From Scratch-《从零开始的计算机图形学》简介
第一章: Computer Graphics From Scratch - Chapter 1 介绍性概念
Chapter 2
- 文章目录
-
基于光线追踪的概述
-
一、渲染风景
-
二、基本假设条件
-
三、画面与视口转换
-
四章 光线追踪技术
- 第四章第一节 射线方程的建立
- 第四章第二节 球体表面方程的推导
- 第四章第三节 球体与射线的交点计算
- 第四章第三节附录:详细计算过程:
-
五、渲染我们的第一个球体
-
六、概括
-
Basic Raytracing - 基本光线追踪
在本章中,我们将介绍光线追踪 ,这是我们将介绍的第一个主要算法。
首先启动算法并编写基本的伪代码。随后我们探讨如何在场景中表示光线和物体。最终我们推导出一种方法用于计算哪些光线构成了场景中每个对象的可见图像同时探讨我们在画布上表示它们的方式。
一、渲染瑞士风景
假设您正在游览一些异国情调的地方,并遇到了令人叹为观止的风景——如此美不胜收的情形下, 您只需绘制一幅图画即可. 图 2-1展示了这种情形.

图 2-1:令人叹为观止的瑞士风景
你有画布和画笔,但你绝对缺乏艺术天赋。 所有的希望都失去了吗?
类似于像素网格一样
类似于像素网格一样
不必多言。 由于不具备艺术天赋却表现得非常专注与细致, 因此你选择了最为直接的方式: 获得了一个类似于像素网格一样的A网络. 你裁取一块精确的长方形并将其围栏式框架稳固地固定在一木棒上. 现如今您可以利用网状视窗俯瞰景色. 随后您将种植另一根木棒以精确地标记您的双眼应位于的位置.
在开始创作之前,您已获得了一个恒定的观察角度和一个明确的构图基础。从这一视角出发,请从这一特定位置进行观察并记录下来。将此构图单元划分成均匀的小方格,在这些小方格中进行详细描绘。在画布上绘制了一个网格结构,并给每个单元格赋予相同的色调。观察左上方区域时所呈现的主要色调是什么?这种色调会自然地映射到该区域的视觉感受中。这种色调会持续在整个画面中产生和谐的整体效果,并呈现出天蓝色调。

图 2-2:粗略的近似景观
从本质上讲, 计算机是一台高度有序的机器, 并不具备艺术天赋. 我们可以通过阐述的方式详细说明绘画创作的具体步骤.
For each little square on the canvas
Paint it the right color
对于画布上的每个小方块
涂上合适的颜色
虽然简单, 但这一方法存在局限性, 即该数学模型过于抽象化, 因此难以直接应用于计算机领域. 我们可以通过进一步分析来更好地理解这一概念并将其转化为具体的计算工具.
Place the eye and the frame as desired
For each square on the canvas
Determine which square on the grid corresponds to this square on the canvas
Determine the color seen through that grid square
Paint the square with that color
根据需要放置眼睛和框架
对于画布上的每个正方形
确定网格上的哪个正方形对应于画布上的这个正方形
确定通过那个方格看到的颜色
用那种颜色画正方形
This method still seems overly abstract. However, it now appears to take on a preliminary algorithmic form. What is surprising is that this detailed overview of the full ray tracing algorithm is presented in such an advanced manner. In simple terms, that's what it is.
二、基本假设
计算机图形学的独特之处在于能够呈现图像。 为了尽快地实现这一目标, 我们做了一些简化的假设。
显然这些前提条件对我们的任务施加了一定的约束但我们将在后续章节中取消这种限制
首先提出一种固定的观察视角设定。 在瑞士风景类比中这一特定的位置通常被称作相机位置; 我们将其标记为_O_ 。为了简化起见我们将相机视为位于坐标系的原点处的一个固定点 因此_O_ 的坐标可表示为_O = (0, 0, 0) 。这种设定保证了观察者始终处于相同的几何位置从而便于后续计算和分析
其次,在本研究中我们假定相机具有固定的方向设定。这种设置决定了其指向的具体位置。为方便起见我们假定该相机面向Z轴正向(用\vec{Z_+}表示)。其中Y轴的正向向上而X轴的正向则向右(如图2-3所示)。

图 2-3. 相机的位置和方向
相机的方位和位置目前已被固定。 在类比过程中仍缺乏构建观察场景所需的基础框架。 我们假定该框架的宽度和高度分别为V_w和V_h,且其正对相机的方向即与\vec{Z_+}垂直。此外,我们假定该框架位于距离d的位置,并其边与X、Y轴平行,在\vec{Z}轴上对称中心处。 看起来有些复杂其实很简单,请参考图2-4以获取更多细节信息。
如同连接外界的重要窗口_视口_ _矩形_一般被用作摄影机成像的基础结构,在画布上呈现所见即所得的图像内容。值得注意的是,在摄影机参数设置中,视野 FOV 的大小主要由视口的比例以及相机到观察者的距离决定;人类通常具有约180°的水平视野范围(尽管其中大部分区域仅有模糊边缘感知而缺乏深度信息)。为了简化计算过程,在本研究中将假设视口宽度与高度相等且等于观察距离,并将其值设定为1单位;这将导致生成大约53°的视角范围以保证图像效果既具合理性又不失真实感

图 2-4 视口的位置和方向
请我们回顾之前所讲述的部分关于算法的内容,并采用专业术语来描述这些概念。在示例2-1中详细列出各个步骤,并为各个步骤赋予编号。
In a manner suitable for the application, place both the camera and the viewport.
For each pixel on the canvas, identify which square of the viewport corresponds to it.
Calculate its color by determining what is seen through each corresponding square.
Render each pixel in its calculated color.
【根据需求配置相机和视口
示例2-1:我们的光线追踪算法的高级描述
我们刚刚完成了步骤①(亦即暂时将其搁置 aside)。 步骤⑨非常简单:只需调用 canvas.PutPixel(x, y, color)。 为了节省时间,请迅速执行步骤②,在后续章节中请重点关注逐步实施更为复杂的操作。
作为补充说明,在字母中使用特定符号来表示向量时会遇到以下几种情况:通过在字符上方添加横线来表示梯度操作符;在字符下方加上横线用于标记散度运算;用一个向上的箭头指示场的方向;并在字符上方叠加波浪线代表拉普拉斯算子;而一阶导数和二阶导数则分别对应于函数的一次变化率与二次变化率。
$\vec{a}$ 向量
$\overline{a}$ 平均值
$\underline{a}$下横线
$\widehat{a}$ (线性回归,直线方程) y尖
$\widetilde{a}$ 颚化符号 等价无穷小
$\dot{a}$ 一阶导数
$\ddot{a}$ 二阶导数
$\frac{分子}{分母}$:
矢量 \vec{\mathbf{\mathit{\alpha}} },
平均数 \overline{\mathbf{\mathit{\alpha}} },
下划线 \underline{\mathbf{\mathit{\alpha}} },
y-hat (線性回歸, 直線方程) 是波浪符號,
等價於無限小的DecorationSign is波浪符號,
一次導數,
二次Derivative,
Formula: ...
三、画布到视口
在示例2-1中所采用的算法中,第②步要求我们确定画布上的哪个正方形对应于该像素。我们知道每个像素都有其对应的画布坐标值——通常用 C_x 和 C_y 表示。值得注意的是,在设置视口时我们会将其轴线与画布的方向保持一致,并使其中心对齐。由于视口使用的是世界单位来度量尺寸,而画布则是基于像素来计算坐标的,在这种情况下从一个坐标系转换到另一个仅仅是一种比例关系的变化!
还存在一个额外的细节点。尽管视窗在二维空间中但它却被嵌入到三维空间中。我们定义该平面与摄像机之间的距离为d基于此定义该平面上的所有点(称为投影平面)均满足z坐标等于d因此
我们实现了这一阶段的任务。 在图像中的每一个像素位置(Cx,Cy)上,在视图空间中都计算出对应的投影坐标(Vx,Vy,Vz)
四、追踪光线
下一步是确定从相机的角度(Ox,Oy,Oz)看到的通过 (Vx,Vy,Vz) 的光是什么颜色。
在现实环境中,光线来源于光源(如太阳、灯泡等),随后被多个物体反射并最终抵达人类的眼睛。为了更好地理解这一过程,在计算机图形学中我们通常会模拟单个光线离开虚拟光源的路径;然而由于计算资源的限制,在实际应用中这种方法往往难以实施。这是因为即使是一个功率为100瓦特的普通白炽灯 bulb 每秒钟也会释放超过一千个光线粒子!经过镜头后仅有极少数光线粒子能够准确地抵达观察点 (Ox, Oy, Oz)。为此我们需要引入一种更为高效的方法——光照追踪法或光线追踪技术 。然而遗憾的是这种方法超出了本书讨论的范围。
相反,在逆向工程中考虑光线;从相机的角度出发(即从来自相机的光线)开始分析;该物体则是相机通过视窗那个点所感知的对象;因此,在初步估算中我们可以假设:该物体的颜色即为通过该点光的颜色(如图2-5所示)。

图 2-5. 视口中的一个小方块,代表画布中的单个像素,涂有相机通过它看到的对象的颜色
现在我们只需要一些方程。
4.1. 射线方程
就我们的目标而言,在描述射线时最为便捷的方式是采用参数方程的形式。 已知射线经过点 O 并且其方向由向量 (V - O) 给出,则该射线上任意一点的位置矢量 P 可以表示为
P = O + t(V - O)
其中 t 为实数参数。 当参数 t 遍历所有实数值时,则可得到该射线上每一个位置矢量对应的点 P 的位置坐标。
我们称 (V – O),射线的方向,\vec{D}。 方程变为
P = O + t \vec{D}
直观上掌握这个方程的一种方式是从原点O出发画出一条射线。
接着沿着射线方向(向量D)移动特定距离t。
可以看出所有位于这条射线上方的点。
对于进一步了解这些向量运算,请参阅附录中的相关内容。
图2-6展示了我们的方程。

图 2-6:对于不同的 t 值,射线 O + t \vec{D} 的一些点。
图2-6呈现了对应于t=0.5和t=1.0的射线上的点;每一个t值都会在射线上产生独特的坐标位置。
4.2. 球面方程
在某个场景中,我们需要引入某种对象以使光线能够照射到特定目标。我们可以采用任何几何形状作为构建基础;对于光线追踪技术而言,我们选择球体 ,因为它们具有便于用数学方程描述的特点。
请定义为一个几何体内所有与固定点等距的所有位置集合。它由一组与固定点等距的所有位置组成,并被称为球面。这个特定的距离被称为半径,并且决定了球体的大小。这个固定的中心位置被称为中心,并且对称地影响整个形状。

图 2-7:一个球体,由其中心和半径定义
让我们深入探讨这一方程的意义。如果您发现其中涉及的知识体系有疑问,请参考附录中的线性代数部分以获得必要的背景知识支持。\
进一步地,
]
|\vec{v}| 表示向量 \(\vec{v}\) 的模长,
而这里的距离计算可表示为:
\[
|\vec{PC}| = \sqrt{\langle P - C, P - C\rangle} = r
- 内乘:内乘、点乘、标量积(内乘)。其运算结果是一个标量(实数)。
- 外乘:外乘、向量积

为了避免涉及平方根的问题,在对方程两边进行平方运算后
4.3. 射线遇到球体
我们有两个方程需要解决:第一个方程用于确定球体表面的点的位置;第二个方程则用于表示射线上的点的位置。⟨P – C, P – C⟩ = r^2 该式表示球面上任一点P到中心C的距离为半径r;而P = O + t \vec{D} 则表示了射线上任意一点P都可以由起点O出发沿着方向向量D延伸t倍长度到达的位置。那么这条射线是否会与球体相交呢? 如果相交的话,请指出交点位置的信息。
我们假设射线与球体在点 P 处相交。这个点既位于射线上又位于球体表面,因此该点必须同时满足这两个方程组。请注意,在这些方程组中唯一变化的量是参数 t。
因为变量 P 在两个方程中代表同一个几何位置,在解决涉及这两个方程的问题时
如果我们能够求解出符合该方程的 t 值,则可以通过将这些值代入射线方程来计算得到射线与球体相交的位置。
目前的形式下,这个方程显得有些繁琐。 请接受一些代数操作,并探索潜在的价值。
首先,让 \vec{CO} = O – C。然后我们可以将方程写为
⟨\vec{CO} + t\vec{D} , \vec{CO} +t\vec{D} ⟩ = r^2
然后我们将点积展开为分量形式,并基于其分布特性进行计算(同时,请参考线性代数附录)
展开上式后可得:
⟨( \textbf{x}_0, x_1, x_2 ), (y_0, y_1, y_2 )⟩ = x_0 y_0 + x_1 y_1 + x_2 y_2
类似的运算表明:
a_{ij} 的具体形式取决于变量之间的关系
分离参数t于点积之外,并将r^2转移至方程另一侧。从而得到了以下方程:t^2 ⟨\vec{D}, \vec{D}⟩ + t(2⟨\vec{CO}, \vec{D}⟩) + ⟨\vec{CO}, \vec{CO}⟩ - r^2 = 0
请注意,在向量间的内积运算结果是一个实数值 ,因此尖括号内的所有计算项均属于实数范畴:
定义以下变量:
a = \langle\vec{D}, \vec{D}\rangle
b = 2\langle\vec{CO}, \vec{D}\rangle
c = \langle\vec{CO}, \vec{CO}\rangle - r^2
将上述结果代入方程后得到:
at^2 + bt + c = 0
这是一个非常有用的二次方程式![类似于我们在几何学中所学的一元二次方程式] 其解即为光线与球体相交所对应的参数t值:
\{t_1, t_2\} = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
4.3.1 附:上面计算细节:
- 射线方程:
射线从O到V,
O是原点(相机的角度,坐标为(O_x, O_y, O_z );
V是视口,也就是相机Z轴正向前方的一个平面,坐标为(V_x, V_y, V_z );
那么射线上任意点P可以表示为:
P = O + t (V - O)
其中t是任何实数,是一个变量;
将(V - O),射线的方向,设为\vec{D} ;
则方程为
P = O + t \vec{D}
\vec{OP} = \vec{O} + t \vec{D}
综上,得:
\vec{OP} = t \vec{D}
球面方程:
圆心标记为C(记作O),半径用r表示;设P为球面上任意一点。
那么有:\left|\overrightarrow{OP} - \overrightarrow{OC}\right| = r
同样地,
\left|\overrightarrow{OC} - \overrightarrow{OP}\right| = r
由于向量运算的性质,
\overrightarrow{OP} - \overrightarrow{OC}等于向量\overrightarrow{CP},
而\overrightarrow{OC} - \overrightarrow{OP}则等于向量\overrightarrow{PC}。
为了简化表达,在后续讨论中统一采用向量\overrightarrow{CP}的形式。
由此可得 |\vec{x}|=r. 由于 \overrightarrow{x} 的长度即为其与自身的点积之平方根, 即
\overrightarrow{x}\cdot\overrightarrow{x}= |\overrightarrow{x}|^2.
因此有
\sqrt{\overrightarrow{x}\cdot\overrightarrow{x}}= |\overrightarrow{x}|.
由此可见,
\sqrt{\overrightarrow{x}\cdot\overrightarrow{x}}=r,
或等价地,
\overrightarrow{x}\cdot\overrightarrow{x}=r^2.
射线方程结合球面方程:
\begin{cases} \vec{OP} = t \vec{D} ---① \\ \vec{CP} \cdot \vec{CP} = r^2 ---② \\ \vec{OP} - \vec{OC} = \vec{CP} ---③ \end{cases}
将式①与式③代入式②,则有:
( t \vec{D} - \vec{OC} ) \cdot ( t \vec{D} - \vec{OC} ) = r^2
通过加法运算得:
( t \vec{D} + \vec{CO} ) \cdot ( t \vec{D} + \vec{CO} ) = r^2
根据向量的交换律性质得:
(\vec{CO} + t\vec D)⋅(\vec CO +t\ve D)=r²
附:
设向量\vec{a}表示为(x_1, y_1),
向量\vec{b}表示为(x_2, y_2),
则内积结果等于x_1乘以x_2加上y_1乘以y_2,
同时该内积也等于两向量模长之积乘以夹角余弦值,
此外该运算满足分配律关系,
即向量与标量乘法之间保持分配律性质,
而且满足交换律性质,
即点积运算对向量具有交换律,
进一步地该运算满足分配律关系。
将该点积展开为分量形式,并利用其分配律特性进行计算:
(\vec{CO} + t \vec{D}) \cdot \vec{CO} + (\vec{CO} + t \vec{D}) \cdot (t \vec{D}) = r^2
同时应用点积的交换律性质:
\vec{CO} \cdot \vec{CO} + t\vec{D} \cdot \vec{CO} + \vec{CO} \cdot t\vec{D} + t\vec{D} \cdot t\vec{D} = r^2
进一步展开并整理:
t\vec{D} \cdot t\vec{D} + 2 (\vec{CO} \cdot t\vec{D}) + \vec{CO} \cdot \vec{CO} = r^2
再结合标量乘法的结合律:
t^2 (\vec{D} \cdot \vec{D}) + t (2 (\vec{CO} \cdot \vec{D})) + (\vec{CO} \cdot \vec{CO}) = r^2
最终得到二次方程形式:
t^2 (\vec{D} \cdot \vec D) + t (2 (\overrightarrow{\mathrm CO}\,.\,\overrightarrow{\mathrm D})) - |\overrightarrow{\mathrm CO}|^2 - r^2 = 0
任意两个向量的内积属于实数域
另解:
\begin{cases} a = \vec{D}\cdot\vec{D}\quad---① \\ b = 2(\vec{CO}\cdot\vec D)\quad---② \\ c=\vec {CO}\cdot\vec {CO}-r^2\quad---③\end {cases}
通过整体代入法得到:
a t^2 + b t + c=0
其解即为光线与球体相交时所涉及的关键参数t:
\{t_1, t_2\} = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
值得庆幸的是,这一现象具有几何意义。
或许还记得,在判别式 b^2 - 4 a c 的影响下决定着二次方程的解的情况:当判别式小于零时方程无解;当其等于零时方程有一个双重根;而当其大于零时方程则有两个不同的实数根。
这些解的情况分别对应于光线不与球体相交的情形、光线与球体相切的情况以及光线依次进入并随后离开球体的情形(如图 2-8所示)。

图 2-8:二次方程解的几何解释:无解、一个解或两个解。
当我们在确定t值后将其代入射线方程时,则能计算出与该t值对应的交点P。
五、渲染我们的第一个球体
回顾一下画布上的每个像素,在画面区域中对应的点是可以计算出来的。
根据相机位置,在画面区域中某一点发出的光线方程是可以表示的。
对于一个球体来说,在其周围空间中穿过它的光线位置是可以被计算出来的。
因此我们需要确定光线与每一个球体的相交位置,并确保这些相交位置距离相机最近。同时我们需要以恰当的颜色在画布上绘制这些像素。目前我们已经非常接近完成阶段,并即将开始渲染我们的第一个完整球体!
值得注意的是参数t值得特别关注让我们重新审视射线方程如式(\ref{eq:ray})所示
| Table2-1: | 参数空间的细分 |
|---|---|
| t < 0 | 摄像头后面[相机后面] |
| 0 ≤ t ≤ 1 | 在相机和投影平面/视口之间 |
| t > 1 | 在投影平面/视口前面 |

图 2-9:参数空间中的几个点
请特别注意,在相交方程中并未提供关于球体必须位于相机前方的信息。然而此方程自然地提供了处理位于相机后方交叉点的方法。显然,并非我们需要的结果。因此,在这种情况下我们应当舍弃所有满足t<0条件的解。为了避免可能产生的额外复杂性我们的解仅限于t>1的情况即实现对超出投影区域物体的处理。
另一方面,我们不设定 t 的上限值;
为了观察到位于相机前方的所有物体,无论其距离多么遥远;
在后续阶段中,我们需要缩短射线;
因此,我们现在将引入这种方法,并设定 t 的上限为无穷大
(如果目标语言无法直接表示“无穷大”这一概念,则可使用一个非常大的数值来替代)。
我们现在可以用一种正式的方式概括迄今为止所做的所有工作,并将其以一种系统化的步骤呈现出来。通常情况下,在编写相关程序时会假设该程序能够访问所需的所有数据,并且这些数据都是预先定义好的或可寻址的内存区域中的对象引用或索引值。因此,在编写相关程序时没有必要特意传递这些参数;相反地,则只需要关注那些真正必要的那些参数即可。
main 方法现在如示例2-2 所示。
O = (0, 0, 0)
for x = -Cw/2 to Cw/2 {
for y = -Ch/2 to Ch/2 {
D = CanvasToViewport(x, y)
color = TraceRay(O, D, 1, inf)
canvas.PutPixel(x, y, color)
}
}
示例 2-2:main 方法
该函数的实现极为简便,请参考图解2-3以获得详细说明。 其中常数 d 代表相机与投影平面之间的距离。
CanvasToViewport(x, y) {
return (x*Vw/Cw, y*Vh/Ch, d)
}
示例 2-3:CanvasToViewport 函数
TraceRay 算法(示例 2-4)通过光线与各个球体的相交位置进行计算,并输出请求范围内最早相交位置处物体的颜色信息。
TraceRay(O, D, t_min, t_max) {
closest_t = inf
closest_sphere = NULL
for sphere in scene.spheres {
t1, t2 = IntersectRaySphere(O, D, sphere)
if t1 in [t_min, t_max] and t1 < closest_t {
closest_t = t1
closest_sphere = sphere
}
if t2 in [t_min, t_max] and t2 < closest_t {
closest_t = t2
closest_sphere = sphere
}
}
if closest_sphere == NULL {
❶ return BACKGROUND_COLOR
}
return closest_sphere.color
}
示例 2-4:TraceRay 方法
在示例 2-4 中, 符号 O 表示射线的起始点; 尽管我们在追踪源于位于起点 O 的光线时, 在后续阶段可能会有所变化, 因此它必须作为一个参数来处理; 同样地, 在后续步骤中我们也需要考虑 $t_{min} 和 $t_{max}$ 的值。
注释:此处解释了为什么使用特定的颜色值
特别注意,在光线与任何球体无交点的情况下(即光线穿行于两个球体之间),系统仍需返回一种颜色值——通常我们采用背景色以确保渲染的一致性)。return BACKGROUND_COLOR
最后,IntersectRaySphere(示例2-5)只是求解二次方程;
IntersectRaySphere(O, D, sphere) {
r = sphere.radius
CO = O - sphere.center
a = dot(D, D)
b = 2*dot(CO, D)
c = dot(CO, CO) - r*r
discriminant = b*b - 4*a*c
if discriminant < 0 {
return inf, inf
}
t1 = (-b + sqrt(discriminant)) / (2*a)
t2 = (-b - sqrt(discriminant)) / (2*a)
return t1, t2
}
示例 2-5:IntersectRaySphere 方法
为了将所有这些付诸实践,让我们定义一个非常简单的场景,如图 2-10 所示。

图 2-10:一个非常简单的场景,从上(左)和从右(右)看
在伪场景语言中,它是这样的:
viewport_size = 1 x 1
projection_plane_d = 1
sphere {
center = (0, -1, 3)
radius = 1
color = (255, 0, 0) # Red
}
sphere {
center = (2, 0, 4)
radius = 1
color = (0, 0, 255) # Blue
}
sphere {
center = (-2, 0, 4)
radius = 1
color = (0, 255, 0) # Green
}
在该场景下启动我们的算法后,在线生成了一个令人赞誉的光线追踪效果展示(图 2-11所示)。

图 2-11:令人难以置信的光线追踪场景
您可以在指定位置访问该算法的在线演示:https://gabrielgambetta.com/cgfs/basic-rays-demo
说实话,在这一点上让我感到有些沮丧。
我想知道反射、阴影和抛光的具体位置。
无需担心——我们即将抵达那里。
这是个不错的起点。
相比之下,在视觉效果上球体的表现更胜一筹。
实际上,在视觉感知方面存在一个关键缺失:物体形状是由光线与其表面的互动所决定的特性。换句话说,在没有这一关键因素的情况下(即无法观察到光线与物体表面的交互),我们无法准确判断物体的真实形状。
我们将在下一章中介绍。
六、概括
在本章中, 我们确立了光线追踪器的基础. 选择固定参数配置(相机与视图的位置及朝向等参数)。采用球体和平面束的表示方法. 研究确定球体和平面束交互作用机制所需的数学理论. 整合以上内容并在画布上以纯色绘制球体.
下一章基于光线与场景中物体相互作用的方式进行建模,并对这一过程进行深入分析。
