《Fundamentals of Computer Graphics》第四章 光线追踪
<h1 id="开篇">开篇</h1><p> 计算机图形学的基础任务之一是<strong>渲染</strong>(<strong>Rendering</strong>)三维物体,即利用在三维空间中排布的物体所构成的场景,计算出场景在某一<strong>视点</strong>(<strong>Viewpoint</strong>)观察得到的二维图像,这实际上和数世纪以来一些建筑师和工程师使用绘图与其他人交流他们的设计一样。<br>
从根本上来说,渲染是一个接受一系列物体的输入,然后产生像素数组输出的过程。不管怎么样,渲染涉及到考虑每个物体对每个像素的贡献,这可以被分为两种一般方法。第一种是<strong>物体顺序渲染</strong>(<strong>Object-Order Rendering</strong>),这种渲染方法以每个物体为中心,为每个物体找到它能影响的像素并且更新像素值。第二种是<strong>图像顺序渲染</strong>(<strong>Image-Order Rendering</strong>),这种方式以每个像素为中心,为每个像素找到能影响到当前像素的物体并且计算像素值。你可以用嵌套循环来思考这两种方法的区别,在图像顺序渲染中,<code>for each pixel</code>在外面,然而在物体顺序渲染中,<code>for each object</code>在外面。<br>
使用这两种方法实际上能计算出完全一样的图像,但是它们的区别导致了不同的计算效果,有截然不同的性能特性。在我们讨论完这两个方法后,我们将在第九章探索这两种方法的比较优势。但是广地来说,使用图像顺序渲染更容易得到能用的结果而且在能产生的效果上更加灵活,不过通常要花费更多的执行时间来得到相似的图像。<br>
<strong>光线追踪</strong>就是一个用于渲染三维场景的图像顺序算法,我们首先考虑这个,因为开发一个能用的光线追踪器相比于物体顺序渲染而言,不需要开发额外的数学工具。</p>
<h1 id="基础的光线追踪算法the-basic-ray-tracing-algorithm">基础的光线追踪算法(The Basic Ray-Tracing Algorithm)</h1>
<p> 一个光线追踪器通过为每个像素计算而工作,对于每个像素来说,基础的任务就是找到图像中当前像素位置能看到的物体。每个像素会往不同方向“看”,任何被像素看到物体必定会和<strong>视线</strong>(<strong>Viewing Ray</strong>)相交。而且看到的应该是与视线相交并且距离相机最近的物体,正是因为被看到的物体遮蔽了它后面的物体。一旦找到了目标物体,接着就要利用位置、表面法线等交汇点的信息进行<strong>着色</strong>(<strong>Shading</strong>),从而计算出像素的颜色。下图是个比较形象的交汇检测的例子<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250522173647373-413586946.png"></p>
<p>因此基础的光线追踪器应该包括三个部分:</p>
<ol>
<li><strong>光线生成</strong>:它基于相机几何为每个像素计算视线的<strong>起点</strong>(<strong>Origin</strong>)和<strong>方向</strong>(<strong>Direction</strong>)。</li>
<li><strong>光线交汇</strong>:它为视线找到最近的物体。</li>
<li><strong>着色</strong>:它基于光线交汇的的结果为像素计算颜色。</li>
</ol>
<p>基础的光线追踪程序对每个像素的计算流程应该如下:</p>
<ol>
<li>计算视线。</li>
<li>找到视线击中的最近物体和表面法线。</li>
<li>基于击中点、光源、法线计算并设置像素的颜色。</li>
</ol>
<h1 id="透视perspective">透视(Perspective)</h1>
<p> 在计算机之前的数百年间,一些艺术家一直在研究使用二维绘画来表示三维物体或场景这一问题,照片也通过二维图像表示三维场景。对于创造图像来说,其实有许多不一般的方式,从立体派绘画到鱼眼镜头再到外围摄像机。对于艺术和摄影还有计算机图形学来说,一般途径就是<strong>线性透视</strong>(<strong>Linear Perspective</strong>),在这个途径中,三维物体通过在场景中的直线在图像中依旧是直线这种投影方式被投影到<strong>图像平面</strong>(<strong>Image Plane</strong>)上。<br>
最简单的一种投影类型就是<strong>平行投影</strong>(<strong>Planar Projection</strong>),如下图所示<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250522212432380-283794385.png"><br>
三维点通过跟随<strong>投影方向</strong>(<strong>Projection Direction</strong>)移动,从而被映射到图像平面上。视图是由投影方向和图像平面的选择决定的,如果图像平面和观察方向垂直,那么这种平行投影叫<strong>正投影</strong>(<strong>Orthographic</strong>),否则就是<strong>斜投影</strong>(<strong>Oblique</strong>)。<br>
平行投影通常用于机械以及建筑绘图,因为这个方法可以让相互平行的直线投影后依然平行,当平面物体平行于图像平面时,还能保留大小以及形状。但是它的优势同时也是它的劣势,在我们的日常生活体验中,远离我们的物体看起来会更小,因此在空间中平行的直线由我们观察可能不会平行。这是因为人眼和相机不会从一个方向收集光照信息,而是从某个固定位置观察,收集四面八方汇集来的光线。早在文艺复新时期,艺术家们就已经认可使用<strong>透视投影</strong>(<strong>Perspective Projection</strong>)来生成看起来自然的视图,使用这种投影方法只需要让三维点跟随过<strong>视点</strong>的直线移动,如下图所示<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250522221628324-79591053.png"><br>
这样,离视点越远的物体在投影后会自然地变小。一个透视视图是通过视点和图像平面的选择决定的,和平行视图一样有斜角透视视图和非斜角透视视图,它们之间的区别就在于图像中心处的投影方向。<br>
你可能已经了解了艺术中使用的<strong>三点透视</strong>(<strong>Three-point Perspective</strong>)规则,不过我们只需要遵循这之下的数学规则,就能实现透视投影效果。</p>
<h1 id="计算观察光线computing-viewing-rays">计算观察光线(Computing Viewing Rays)</h1>
<p> 从上个部分告诉我们,生成光线基本的工具是视点和图像平面。有许多方法能得到相机几何的细节,在这个部分我们将解释一个方法,它基于规范正交基,支持正平行、斜平行、正交投影视图。<br>
为了生成光线,我们首先需要光线的数学表达。一条光线仅仅由一个起始点和一个传播方向构成。我们可以使用三维参数直线来描述光线,即</p>
<p></p><div class="math display">\[\mathbf{p}(t)=\mathbf{e}+t(\mathbf{s}-\mathbf{e})
\]</div><p></p><p>这可以理解为光线从<span class="math inline">\(\mathbf{e}\)</span>点出发,沿<span class="math inline">\(\mathbf{s}-\mathbf{e}\)</span>方向传播,从而击中表面上的点<span class="math inline">\(\mathbf{p}\)</span>。对于给定的<span class="math inline">\(t\)</span>,我们可以计算出<span class="math inline">\(\mathbf{p}\)</span>点。在这个参数方程中点<span class="math inline">\(\mathbf{e}\)</span>为光线<strong>起点</strong>,<span class="math inline">\(\mathbf{s}-\mathbf{e}\)</span>为光线<strong>方向</strong>,下图为一张相关的示意图。<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250522233946555-771917088.png"><br>
这里要注意的是<span class="math inline">\(\mathbf{p}(0)=\mathbf{e}\)</span>还有<span class="math inline">\(\mathbf{p}(1)=\mathbf{s}\)</span>,如果计算出两个交汇参数<span class="math inline">\(t\)</span>且满足<span class="math inline">\(0<t_1<t_2\)</span>,那么可知<span class="math inline">\(\mathbf{p}(t_1)\)</span>相比<span class="math inline">\(\mathbf{p}(t_2)\)</span>距离光线起始点更近。如果<span class="math inline">\(t<0\)</span>,那么交点在眼睛后面。当要寻找离光线起始点最近的物体并且满足不在眼睛后方的条件时,这个事实会很有用。<br>
光线在一些结构或者对象中是不变的,一般都存储位置和方向。以面向对象的程序为例,我们可能写如下的代码:</p>
<pre><code class="language-cpp">class Ray
Vec3 o | 光线起点
Vec3 d | 光线方向
Vec3 evaluate(real t)
return o+td
</code></pre>
<p>上述代码假设有个<code>Vec3</code>类代表着三维向量并且支持基本的算术操作。<br>
为了计算一条观察光线,我们需要知道<span class="math inline">\(\mathbf{e}\)</span>点和<span class="math inline">\(\mathbf{s}\)</span>点,找到<span class="math inline">\(\mathbf{s}\)</span>点可能有点困难,但是当我们在右手坐标系中看待这一问题时,答案还是挺直接的。<br>
我们所有的光线生成方法都是从一个叫<strong>相机帧</strong>(<strong>Camera Frame</strong>)的规范正交基开始的,通过<span class="math inline">\(\mathbf{e}\)</span>作为视点还有<span class="math inline">\(\mathbf{u}\)</span>、<span class="math inline">\(\mathbf{v}\)</span>、<span class="math inline">\(\mathbf{w}\)</span>作为三个基向量确定。其中<span class="math inline">\(\mathbf{u}\)</span>指向右方,<span class="math inline">\(\mathbf{v}\)</span>指向上方,<span class="math inline">\(\mathbf{w}\)</span>指向后方,因此<span class="math inline">\(\{\mathbf{u},\mathbf{v},\mathbf{w}\}\)</span>构成了一个右手坐标系。使用最通常的方法来构造相机帧得首先确定视点也就是<span class="math inline">\(\mathbf{e}\)</span>,接着确定观察方向也就是-<span class="math inline">\(\mathbf{w}\)</span>,然后确定<strong>上向量</strong>(<strong>Up Vector</strong>)<span class="math inline">\(\mathbf{v}\)</span>,最后通过第二章中的方法确定右向量<span class="math inline">\(\mathbf{u}\)</span>。<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250523223021007-871226685.png"></p>
<h2 id="正交视图orthographic-views">正交视图(Orthographic Views)</h2>
<p> 对于正交视图来说,所有的光线都有方向<span class="math inline">\(-\mathbf{w}\)</span>。尽管平行视图没有视点,但是我们可以使用相机帧的原点来定义光线出发的平面。<br>
视线应该从以点<span class="math inline">\(\mathbf{e}\)</span>、向量<span class="math inline">\(\mathbf{u}\)</span>和向量<span class="math inline">\(\mathbf{v}\)</span>确定的平面出发,最后一个要确定的信息就是图像平面应该<strong>在哪</strong>。我们直接使用四个数来定义图像维度也就是图像的四边,其中<span class="math inline">\(l\)</span>和<span class="math inline">\(r\)</span>分别为图像的左边和右边位置,长度通过从<span class="math inline">\(\mathbf{e}\)</span>点出发沿着<span class="math inline">\(\mathbf{u}\)</span>方向测量。<span class="math inline">\(b\)</span>和<span class="math inline">\(t\)</span>分别为图像的底边和顶边位置,长度通过从<span class="math inline">\(\mathbf{e}\)</span>点出发沿着<span class="math inline">\(\mathbf{v}\)</span>方向测量。通常来说这四个量满足<span class="math inline">\(l<0<r\)</span>且<span class="math inline">\(b<0<t\)</span>。<br>
假设图像维度为<span class="math inline">\(n_x \times n_y\)</span>,为了把这个图像填充到<span class="math inline">\((r-l)\times(t-b)\)</span>的矩形空间中,像素中心处之间的水平间距和竖直间距分别为<span class="math inline">\((r-l)/n_x\)</span>和<span class="math inline">\((t-b)/n_y\)</span>。像素位置<span class="math inline">\((i,j)\)</span>可以用如下公式把它映射到图像平面的位置<span class="math inline">\((u,v)\)</span></p>
<p></p><div class="math display">\[u=l+(r-l)(i+0.5)/n_x
\]</div><p></p><p></p><div class="math display">\[v=b+(t-b)(j+0.5)/n_y
\]</div><p></p><p>有了<span class="math inline">\((u,v)\)</span>后接下来我们可以为正交视图计算观察光线</p>
<p></p><div class="math display">\[\mathrm{ray.o} \leftarrow \mathbf{e}+u\cdot\mathbf{u}+v\cdot\mathbf{v}
\]</div><p></p><p></p><div class="math display">\[\mathrm{ray.d} \leftarrow -\mathbf{w}
\]</div><p></p><p>创建斜平行视图很简单,直接改变<code>ray.d</code>即可,下图是一个比较形象的正交视图演示<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250523232044912-828166145.png"></p>
<h2 id="透视视图perspective-views">透视视图(Perspective Views)</h2>
<p> 对于透视视图来说,所有的光线都有相同的起点也就是视点,但是方向是不同的。<span class="math inline">\(\mathbf{e}\)</span>点不在图像平面上,距离图像平面的距离为<span class="math inline">\(d\)</span>,这段距离是图像平面距离也被称为<strong>焦距</strong>(<strong>Focal Length</strong>),因为<span class="math inline">\(d\)</span>在真实的摄像机中是焦距。光线方向是通过视点以及像素在图像平面的位置定义的,为透视视图计算光线的流程如下</p>
<ol>
<li>利用之前提到的方法计算<span class="math inline">\((u,v)\)</span></li>
<li><span class="math inline">\(\mathrm{ray.o} \leftarrow \mathbf{e}\)</span></li>
<li><span class="math inline">\(\mathrm{ray.d} \leftarrow -d\cdot\mathbf{w} + u\cdot\mathbf{u} + v\cdot\mathbf{v}\)</span></li>
</ol>
<p>斜角透视视图可以通过分别声明图像平面的法线和投影方向来得到。下图是一个比较形象的透视视图演示<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250524000533245-1058668324.png"></p>
<h1 id="光线物体交汇ray-object-intersection">光线物体交汇(Ray-Object Intersection)</h1>
<p> 一旦我们生成了光线<span class="math inline">\(\mathbf{e}+t\mathbf{d}\)</span>,接下来要做的事就是找到第一个交点且<span class="math inline">\(t>0\)</span>。在实践中变成了找到光线在区间<span class="math inline">\(\)</span>与表面的交点。基础的光线交汇情况是当<span class="math inline">\(t_0=0\)</span>且<span class="math inline">\(t_1=+\infty\)</span>的时候。接下来的部分将为球面和三角形解决这个问题。</p>
<h2 id="光线球面交汇ray-sphere-intersection">光线球面交汇(Ray-Sphere Intersection)</h2>
<p>现在已知光线<span class="math inline">\(\mathbf{p}(t)=\mathbf{e}+t\mathbf{d}\)</span>以及球面的隐式函数</p>
<p></p><div class="math display">\[(x-x_c)^2+(y-y_c)^2+(z-z_c)^2-R^2=0
\]</div><p></p><p>把<span class="math inline">\(\mathbf{p}\)</span>带入隐式函数可得</p>
<p></p><div class="math display">\[(\mathbf{p}-\mathbf{c})\cdot(\mathbf{p}-\mathbf{c})-R^2=0
\]</div><p></p><p>因此可得</p>
<p></p><div class="math display">\[(\mathbf{d}\cdot\mathbf{d})t^2+2\mathbf{d}\cdot(\mathbf{e}-\mathbf{c})t+(\mathbf{e}-\mathbf{c})\cdot(\mathbf{e}-\mathbf{c})-R^2=0
\]</div><p></p><p>这个等式实际上是一个一元二次方程<span class="math inline">\(At^2+Bt+C=0\)</span>,不过首先得通过<strong>判别式</strong>(<strong>Discriminant</strong>)也就是<span class="math inline">\(B^2-4AC\)</span>来获得根的情况,如果是负数那么没有根也就是没有交点,相反如果是正的那么有两个根也就是两个交点,如果等于<span class="math inline">\(0\)</span>那么只有一个根也就是一个交点。接着使用求根公式可以得到</p>
<p></p><div class="math display">\[t=\frac{-\mathbf{d}\cdot(\mathbf{e}-\mathbf{c})\pm \sqrt{(\mathbf{d}\cdot(\mathbf{e}-\mathbf{c}))^2-(\mathbf{d}\cdot\mathbf{d})((\mathbf{e}-\mathbf{c})\cdot(\mathbf{e}-\mathbf{c})-R^2)}}{(\mathbf{d}\cdot\mathbf{d})}
\]</div><p></p><p>在实践中应该先检测判别式,在区间<span class="math inline">\(\)</span>中找到的交点中,如果更小的根在这个区间中那么取更小的根,相反取更大的根,以上两种情况都不符合就认为光线没有击中表面。求得交点后下一步就是求法线<span class="math inline">\(\mathbf{n}\)</span>,对于球面来说其实很简单,<span class="math inline">\(\mathbf{n}=(\mathbf{p}-\mathbf{c})/R\)</span>。</p>
<h2 id="光线三角形交汇ray-triangle-intersection">光线三角形交汇(Ray-Triangle Intersection)</h2>
<p> 有许多算法可以计算光线和三角形的交点,这里介绍一种求重心坐标的方法。</p>
<p>现在已知光线<span class="math inline">\(\mathbf{p}(t)=\mathbf{e}+t\mathbf{d}\)</span>以及三角形三点<span class="math inline">\(\mathbf{a}\)</span>、<span class="math inline">\(\mathbf{b}\)</span>、<span class="math inline">\(\mathbf{c}\)</span>,可知</p>
<p></p><div class="math display">\[\mathbf{e}+t\mathbf{d}=\mathbf{a}+\beta(\mathbf{b}-\mathbf{a})+\gamma(\mathbf{c}-\mathbf{a})
\]</div><p></p><p>其中<span class="math inline">\(\beta>0\)</span>、<span class="math inline">\(\gamma>0\)</span>、<span class="math inline">\(\beta+\gamma<1\)</span>,为了求解<span class="math inline">\(t\)</span>、<span class="math inline">\(\beta\)</span>、<span class="math inline">\(\gamma\)</span>我们展开上方的等式为</p>
<p></p><div class="math display">\[x_e+tx_d=x_a+\beta(x_b-x_a)+\gamma(x_c-x_a)
\]</div><p></p><p></p><div class="math display">\[y_e+ty_d=y_a+\beta(y_b-y_a)+\gamma(y_c-y_a)
\]</div><p></p><p></p><div class="math display">\[z_e+tz_d=z_a+\beta(z_b-z_a)+\gamma(z_c-z_a)
\]</div><p></p><p>可以重写为向量矩阵相乘形式</p>
<p></p><div class="math display">\[\begin{bmatrix}
x_a-x_b & x_a-x_c & x_d \\
y_a-y_b & y_a-y_c & y_d \\
z_a-z_b & z_a-z_c & z_d
\end{bmatrix}
\begin{bmatrix} \beta \\ \gamma \\ t \end{bmatrix} =
\begin{bmatrix} x_a-x_e \\ y_a-y_e \\ z_a-z_e \end{bmatrix}
\]</div><p></p><p>因此可以使用<strong>克拉默法则</strong>求解</p>
<p></p><div class="math display">\[\beta = \frac{\begin{vmatrix} x_a-x_e & x_a-x_c & x_d \\ y_a-y_e & y_a-y_c & y_d \\ z_a-z_e & z_a-z_c & z_d \end{vmatrix} }{|\mathbf{A}|}
\]</div><p></p><p></p><div class="math display">\[\gamma = \frac{\begin{vmatrix} x_a-x_b & x_a-x_e & x_d \\ y_a-y_b & y_a-y_e & y_d \\ z_a-z_b & z_a-z_e & z_d\end{vmatrix}}{|\mathbf{A}|}
\]</div><p></p><p></p><div class="math display">\[t = \frac{\begin{vmatrix} x_a-x_b & x_a-x_c & x_a-x_e \\ y_a-y_b & y_a-y_c & y_a-y_e \\ z_a-z_b & z_a-z_c & z_a-z_e \end{vmatrix}}{|\mathbf{A}|}
\]</div><p></p><p></p><div class="math display">\[\mathbf{A} = \begin{bmatrix} x_a-x_b & x_a-x_c & x_d \\ y_a-y_b & y_a-y_c & y_d \\ z_a-z_b & z_a-z_c & z_d \end{bmatrix}
\]</div><p></p><p> 对于光线三角形的交汇算法来说,可以使用一些<code>if</code>语句来更早地结束检测,因此函数可以像下面这样写。<br>
<img src="https://img2024.cnblogs.com/blog/2774734/202509/2774734-20250910160343595-626439226.png"></p>
<h2 id="在软件中的光线交汇ray-intersection-in-software">在软件中的光线交汇(Ray intersection in software)</h2>
<p> 在光线追踪程序中,一个好的想法是使用面向对象的设计,例如有<strong>表面</strong>(<strong>Surface</strong>)类以及它的派生类<strong>三角形</strong>(<strong>Triangle</strong>)、<strong>球</strong>(<strong>Sphere</strong>)等等,任何能与光线交汇的都应该是表面的子类。表面类应该有的关键接口就是和光线交汇的方法。</p>
<pre><code class="language-cpp">class Surface
HitRecord hit(Ray r,real t0,real t1)
</code></pre>
<p>这里的<span class="math inline">\(t_0\)</span>和<span class="math inline">\(t_1\)</span>被用来指定方法返回区间<span class="math inline">\(\)</span>内的击中情况,而返回的<code>HitRecord</code>包含了表面交汇点的数据</p>
<pre><code class="language-cpp">class HitRecord
Surface s | 被击中的表面
real t | 击中点的参数值
Vec3 n | 击中点的表面法线
.
.
.
</code></pre>
<p>被击中的表面、<span class="math inline">\(t\)</span>值还有表面法线是<code>HitRecord</code>的最小需求,其它的数据例如纹理坐标或者切向向量可能也会被存储。根据语言或者实际写的代码,<code>HitRecord</code>可能不会直接被返回,而是使用引用传参<code>HitRecord</code>,接着让<code>hit</code>方法写入成员。未击中可以通过<span class="math inline">\(t=\infty\)</span>来指定。</p>
<h2 id="与一组物体交汇intersecting-a-group-of-objects">与一组物体交汇(Intersecting a Group of Objects)</h2>
<p> 一个有趣的场景应该是由多个物体构成的,通常我们会检测光线与场景的交汇,我们必须找到在光线上且离相机最近的交点。最简单的方法就是与所有物体检测交汇,挑选出最小的参数<span class="math inline">\(t\)</span>。</p>
<pre><code class="language-cpp">class Group,subclass of Surface
list-of-Surface surfaces | 组中的所有表面
HitRecord hit(Ray ray,real t0,real t1)
HitRecord closest-hit(inf) | 使用未命中情况初始化最近的交点
for surf in surfaces do
rec = surf.hit(ray,t0,t1)
if rec.t < inf then
closest-hit = rec
t1 = t
return closest-hit
</code></pre>
<h1 id="着色shading">着色(Shading)</h1>
<p> 一旦找到像素能看到的表面,接着就能通过<strong>着色模型</strong>(<strong>Shading Model</strong>)来计算像素值。着色模型有简单的启发式的还有基于物理的,要怎么做完全取决于你。下面的部分介绍如何利用基础的信息来进行着色。</p>
<h2 id="光源light-sources">光源(Light Sources)</h2>
<p> 为了支持着色,一个光线追踪程序通常有一系列的光源。我们需要三种基础光源,分别为点光源、定向光、环境光。其中点光源向四周均匀辐射能量,定向光从某个固定的方向点亮场景,环境光提供固定的光照。在更加绚丽的系统中通常有其它类型的光源,例如局部光源。<br>
利用点光源或者定向光计算着色需要某些几何信息,在光线追踪器中,当光线击中表面时我们就能得到以下必要的信息</p>
<ul>
<li>着色点<span class="math inline">\(\mathbf{x}\)</span>:通过参数值<span class="math inline">\(t\)</span>计算。</li>
<li>表面法线<span class="math inline">\(\mathbf{n}\)</span>:取决于表面的类型,每个表面都要能计算击中点的法线。</li>
<li>光照方向<span class="math inline">\(\mathbf{l}\)</span>:通过光源的信息得到,如位置或方向。</li>
<li>观察方向<span class="math inline">\(\mathbf{v}\)</span>:仅仅是归一化的光线反向向量 <span class="math inline">\((\mathbf{v} = -\mathbf{d}/||\mathbf{d}||)\)</span>。</li>
</ul>
<p>对于环境光来说就更简单,没有光照方向这种概念,因为在这种情况下光来自四面八方,而且光照着色也不依赖于<span class="math inline">\(\mathbf{v}\)</span>,对于更加简单的环境光来说,甚至不会依赖于<span class="math inline">\(\mathbf{x}\)</span>或<span class="math inline">\(\mathbf{n}\)</span>。<br>
计算一个包含多个光源的场景的光照其实很简单,只需要把各个光源的着色结果累加起来。在基础的光线追踪器中,可以直接遍历所有光源积累光照结果。</p>
<h2 id="在软件中的着色shading-in-software">在软件中的着色(Shading in software)</h2>
<p> 一个光线追踪程序通常包含代表着光源和材质的类,光源可以是光源类的子类实例,它们必须包含足够的信息来描述光源。着色通常也需要参数来描述表面的材质,因此另一个有用的类就是<strong>材质</strong>(<strong>Material</strong>),它囊括了着色模型进行计算所需要的一切。<br>
不同的系统采取不同的措施来分开光源和材质之间的着色计算,一种和上述目标一致的方法是让光源负责总的光照计算,让材质负责<strong>双向反射分布函数</strong>(<strong>BRDF</strong>)值。有了以上这些,一些类以及所属的接口应该长这样</p>
<pre><code class="language-cpp">class Light
Color illuminate(Ray ray,HitRecord hrec)
</code></pre>
<pre><code class="language-cpp">class Material
Color evaluate(Vec3 l,Vec3 v,Vec3 n)
</code></pre>
<p>每个表面都会存储一份对它的材质的引用。点光源光照可能会以如下方式实现</p>
<pre><code class="language-cpp">class PointLight, subclass of Light
Color I
Vec3 p
Color illuminate(Ray ray,HitRecord hrec)
Vec3 x = ray.evaluate(hrec.t)
real r = distance(p,x)
Vec3 l = (p-x)/r
Vec3 n = hrec.normal
Color E = max(0,dot(n,l))*I/pow(r,2)
Color k = hrec.surface.material.evaluate(l,v,n)
return kE
</code></pre>
<p>常强度的环境光源可能会以如下方式实现</p>
<pre><code class="language-cpp">class AmbientLight, subclass of Light
Color Ia
Color illuminate(Ray ray,HitRecord hrec)
Color ka=hrec.surface.material.ka
return ka*Ia;
</code></pre>
<p>光线击中并且进行着色的代码可能像下面这样</p>
<pre><code class="language-cpp">function shade-ray(Ray ray,real t0,real t1)
HitRecord rec = scene.hit(ray,t0,t1)
if rec.t < inf then
Color c = 0
for light in scene.lights do
c += light.illuminate(ray,rec)
return c
else
return background-color
</code></pre>
<p>下方为着色结果的一张示例图<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250525205911682-289709548.png"></p>
<h2 id="阴影shadows">阴影(Shadows)</h2>
<p> 一旦光线追踪器中有了基础的着色,对于点光源以及定向光的阴影可以被简单地添加。我们只需发出<strong>阴影光线</strong>(<strong>Shadow Ray</strong>)来检测击中点是否能被光源“看到”。如果能被“看到”,那么就得计算着色结果,否则跳过这个光源的着色计算。阴影光线的例子如下图所示。<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250525210351163-1203756369.png"><img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250525210429631-300943885.png"><br>
理想点光源的阴影光线如左图所示,是以击中点为起点到点光源处为方向的光线,从<span class="math inline">\(\mathbf{x}\)</span>位置发出的阴影光线击中了其它物体因此无法被点光源“看到”,所以无法被点光源影响。相反<span class="math inline">\(\mathbf{x}^\prime\)</span>可以被点光源“看到”,从而能被点光源影响。理想定向光的阴影光线如右图所示,是以击中点为起点且方向固定的光线,检测逻辑与点光源一样。<br>
不过在实际情况下,由于数值精度的问题,一般会检测阴影光线在区间<span class="math inline">\([\epsilon,a]\)</span>之内是否有交点,其中<span class="math inline">\(\epsilon\)</span>是小的正常量来用来避免自遮蔽的情况,如右图所示。加入阴影后的着色结果如下图所示。<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250525224638138-758372770.png"></p>
<h2 id="镜像反射mirror-reflection">镜像反射(Mirror Reflection)</h2>
<p> 加入理想情况下的<strong>镜面</strong>(<strong>Specular</strong>)反射也很直接,我们直接使用公式<span class="math inline">\(\mathbf{r}=\mathbf{d}-2(\mathbf{d}\cdot\mathbf{n})\mathbf{n}\)</span>计算击中点处的反射光线即可。不过在现实世界中有些能量会随着在表面上的反射而损失,例如相对于蓝色来说,黄金反射黄色更多,我们可以使用<span class="math inline">\(k_m\)</span>镜面反射颜色来描述这一现象。在进行实践时要注意反射光线应该同样使用<span class="math inline">\(\epsilon\)</span>检测区间<span class="math inline">\([\epsilon,a]\)</span>内的交点,就像阴影检测那样,因此着色代码变成了</p>
<pre><code>color c = c + k_m*shade-ray(Ray(p,r),epsilon,inf)
</code></pre>
<p>加入镜面反射的着色结果如下图所示<br>
<img src="https://img2023.cnblogs.com/blog/2774734/202505/2774734-20250525231312641-731968898.png"></p>
</div>
<div id="MySignature" role="contentinfo">
<p>本文来自博客园,作者:TiredInkRaven,转载请注明原文链接:https://www.cnblogs.com/TiredInkRaven/p/18889728</p><br><br>
来源:https://www.cnblogs.com/TiredInkRaven/p/18889728
頁:
[1]