系列文章目录
简介: Computer Graphics From Scratch-《从零开始的计算机图形学》简介
第一章: Computer Graphics From Scratch - Chapter 1 介绍性概念
第二章:Computer Graphics From Scratch - Chapter 2 基本光线追踪
Chapter 3
Light - 光照
We’ll start adding “realism” to our rendering of the scene by introducing light. Light is a vast and complex topic, so we’ll present a simplified model that is good enough for our purposes. This model is, for the most part, inspired by how light works in the real world, but it also takes some liberties with the aim of making the rendered scenes look good.
我们将开始通过引入光来为我们的场景渲染添加“真实感”。 光是一个庞大而复杂的主题,因此我们将展示一个足以满足我们目的的简化模型。 这个模型在很大程度上受到了光在现实世界中的工作方式的启发,但它也需要一些自由,以使渲染的场景看起来更好。
We’ll start with some simplifying assumptions that will make our lives easier, then we’ll introduce three types of light sources: point lights, directional lights, and ambient light. We’ll end the chapter by discussing how these lights affect the appearance of surfaces, including diffuse and specular reflection.
我们将从一些简化的假设开始,这将使我们的生活更轻松,然后我们将介绍三种类型的光源:点光源、定向光源和环境光源。 我们将通过讨论这些光如何影响表面的外观来结束本章,包括漫反射和镜面反射。
一、简化假设
让我们做一些假设以使事情变得更简单。 首先,我们声明所有的光都是白色的。 这让我们可以使用一个实数 i i i 来表征任何光,称为光的强度。 模拟彩色灯光并不复杂(我们只需使用三个强度值,每个颜色通道一个,并逐个计算所有颜色和照明通道),但我们将坚持使用白光以保持简单。
其次,我们将忽略气氛。 在现实生活中,灯光越远越暗; 这是因为漂浮在空气中的颗粒会在光线穿过它们时吸收部分光线。 虽然这在光线追踪器中并不是特别复杂,但我们将保持简单并忽略此效果; 在我们的场景中,距离并没有使灯光变得不那么明亮。
二、光源
光必须来自某个地方。 在本节中,我们将定义三种不同类型的光源。
2.1. Point Lights - 点光源
点光源从 3D 空间中的固定点发出光,称为它们的位置。 它们向各个方向均匀地发光; 这就是为什么它们也被称为全向灯的原因。 因此,点光源完全由它的位置和强度来描述。
灯泡是点光源在现实生活中的一个很好的近似值。 虽然现实生活中的灯泡不会从单点发光,也不是完全 全向 的,但它是一个非常准确的近似值。
让我们将向量 L ⃗ \vec{L} L定义为从场景中的一个点 P P P 到光 Q Q Q 的方向。我们可以计算这个向量,称为光向量,为 Q − P Q - P Q−P。请注意,由于 Q Q Q 是固定的,但 P P P 可以是任何场景中的点, L ⃗ \vec{L} L对应场景中的每个点都是不同的,正如你所看到的图3-1所示。
图 3-1: Q Q Q 处的点光源。每个点 P P P 的 L ⃗ \vec{L} L 矢量都不同.
2.2. Directional Lights - 定向光源
如果点光源很好地近似于灯泡,它是否也可以近似于太阳?
这是一个棘手的问题,答案取决于我们要渲染的内容。 在太阳系比例上,太阳可以近似为点光源。 毕竟是从一点发光,而且是向四面八方发光的,所以看起来还算合格。
然而,如果我们的场景代表地球上正在发生的事情,那么它就不是一个很好的近似值。 太阳是如此遥远,以至于到达我们的每一束光线几乎都具有完全相同的方向。 我们可以用一个离场景中的物体非常、非常、非常远的点光源来近似这一点。 然而,光和物体之间的距离会比物体之间的距离大几个数量级,所以我们会开始遇到数值精度错误。
为了更好地处理这些情况,我们定义了定向光源。 与点光源一样,定向光源也有强度,但与它们不同的是,它们没有位置; 相反,它们有一个固定的方向。 您可以将它们视为位于指定方向的有限距离点光源。
在点光源的情况下,我们需要为场景中的每个点
P
P
P 计算不同的光向量
L
⃗
\vec{L}
L,在这种情况下
L
⃗
\vec{L}
L是给定的。 在太阳到地球的场景示例中
L
⃗
\vec{L}
L将是 (太阳中心)-(地球中心)。 图 3-2 显示了它的外观。
图 3-2:定向光源。 L ⃗ \vec{L} L向量对于每个点 P P P 都是相同的。
正如我们在这里看到的,定向光的光 矢量 对于场景中的每个点 都是相同的。 将此与图 3-1 进行比较,其中点光源的 光矢量 对于场景中的每个点都是不同的。
2.3. Ambient Light - 环境光源
每个现实生活中的光都可以建模为点光或定向光吗? 差不多。
这两种类型的光足以照亮一个场景吗? 不幸的是没有。
想想月球会发生什么。 附近唯一重要的光源是太阳。
所以月球相对于太阳的“前半部分”得到了所有的光,而它的“后半部分”则完全黑暗。 我们从地球的不同角度看到这一点,创造了我们所谓的月球“阴影”。
然而,地球上的情况有点不同。
即使不直接从光源接收光的点也不是完全黑暗的(看看你椅子下面的地板)。 如果他们对光源的“视野”被其他东西阻挡,光线如何到达这些点?
正如第 1 章“颜色模型”中提到的,当光线照射到一个物体上时,它的一部分被吸收,而其余的又被散射回场景中。 这意味着光不仅可以来自光源,还可以来自从光源获得光并将部分光散射回场景的物体。
但是为什么要停在那里呢? 散射光会依次照射到其他物体上,一部分会被吸收,一部分会被散射回场景中。 以此类推,直到原始光的所有能量都被场景中的表面吸收。
这意味着我们应该将每个物体都视为光源。 可以想象,这会给我们的模型增加很多复杂性,所以我们不会在本书中探讨这种机制。 如果您好奇,可以搜索全局照明并惊叹于漂亮的图片。
但是我们仍然不希望每个物体都被直接照亮或完全黑暗(除非我们实际上是在渲染太阳系的模型)。 为了克服这个限制,我们将定义第三种类型的光源,称为环境光,其特征仅在于其强度。 我们将声明环境光会为场景中的每个点贡献一些光,无论环境光在哪里。 这是对场景中光源和表面之间非常复杂的交互的过度简化,但它运行得很好。
一般来说,一个场景将有一个环境光(因为环境光只有一个强度值,所以它们中的任意数量都可以简单地组合成一个环境光)和任意数量的点光源和定向光源。
2.4. 光源类型
定向光源Directional
定向光源是一个能够发射出某个方向光照的光源类型。它类似于太阳光,从无限远处等量射出不会衰减的平行光线。
Directional Lights(定向光源) 主要用作为基本的室外光源,或者用作为需要呈现出是从极远处或者接近于无限远处发出的光的任何光源。
Directional Lights
属性 | 描述 |
---|---|
Intensity(强度) | 光源所散发的总能量 |
Light Color(光源颜色) | 光源所散发的颜色 |
Used As Atmosphere Sun Light(用作大气阳光) | 使用此 定向光源 来定义太阳在天空中的位置 |
Affects World(影响场景) | 完全禁用光源。无法在运行时设置。如要在运行时禁用光源的效果,须修改其 可视性 属性。 |
Casts Shadows(投射阴影) | 光源是否投射阴影。 |
Indirect Lighting Intensity(间接光照强度) | 缩放光源发出的间接光照贡献。 |
Min Roughness(最小粗糙度) | 对此光照产生作用的最小粗糙度。用于柔化反射高光。 |
Shadow Bias(阴影偏差) | 控制此光源所投射阴影的精确度。 |
Shadow Filter Sharpen(阴影过滤锐化) | 此光源投射阴影过滤的锐化程度。 |
Cast Translucent Shadows(投射半透明阴影) | 该光源是否可从半透明物体处投射动态阴影。 |
Affect Dynamic Indirect Lighting(影响动态间接光照) | 光源是否应被注入 光照传播体积。 |
Cast Static Shadows(投射静态阴影) | 此光源是否投射静态阴影。 |
Cast Dynamic Shadows(投射动态阴影) | 此光源是否投射动态阴影。 |
Affect Translucent Lighting(影响半透明光照) | 光源是否影响半透明物体。 |
点光源Point
点光源是一个能够同时向四面八方发射光线的光源类型。它与电灯泡类似,可以从一点向四周发射能量逐渐衰减的光线。
Point Lights(点光源) 是像传统的"灯泡"一样的光源,从一个单独的点处向各个方向发光。
聚光源Spot
聚光源就类似于我们平时见到的手电筒或者聚光灯,不同在于,开发人员可以使用内圆锥角和外圆锥角来修改聚光源的形状。如下图所示,内圆锥角控制橙色圈(1号)的大小,外圆锥角控制蓝色圈(2号)的大小。
Spot Lights(聚光源) 也是从一个单独的点处向外发光,但是其光线会受到一组锥体的限制。
天光Sky
天光是给整个场景添加光亮的光源,可以简单的想象为,使用天光后,场景中的每个物体都具有了微弱的自发光,自发光的内容,就是天光的内容。
Sky Lights(天空光照) 则获取场景的背景并将它用于场景网格物体的光照效果。
矩形光源
这个就类似于壁灯、显示器、矩形吊顶灯等这些矩形自发光的物体。没啥好说的,就类似于特殊的聚光灯。
Rect Lights(矩形光源) 从一个矩形平面向场景发出光线。可以用它来模拟电视或显示器屏幕、吊顶灯具或壁灯。
三、单点照明
现在我们知道了如何定义场景中的灯光,我们需要弄清楚灯光如何与场景中的物体表面相互作用。
为了计算单个点的光照,我们将计算每个光源贡献的光量,并将它们加在一起,得到一个表示该点接收的总光量的数字。
然后,我们可以将该点的表面颜色乘以这个数量,以获得表示它接收到多少光的颜色阴影。
那么,当一束光线(无论是来自定向光还是点光)照射到场景中某个对象上的某个点时会发生什么?
我们可以直观地将物体分为两大类,具体取决于它们反射光的方式:“哑光”和“闪亮”物体。 由于我们周围的大多数物体都可以归类为哑光,因此我们将首先关注这一组。
四、漫反射
当一束光线照射到无光物体上时,光线会在各个方向均匀地散射回场景中,这一过程称为漫反射; 这就是使哑光对象看起来哑光的原因。
要验证这一点,请查看您周围的一些无光泽物体,例如墙壁。 如果你相对于墙壁移动,它的颜色不会改变。 也就是说,无论你从哪里看,你看到的从物体反射的光都是一样的。
另一方面,反射的光量确实取决于光线与表面之间的角度。 直观地说,这是因为光线携带的能量必须根据角度分布在更小或更大的区域上,因此每单位面积反射到场景的能量分别更高或更低,如图 3-3 所示:
图 3-3:一束光线的能量分布在不同大小的区域上,这取决于它与表面的角度。
在图 3-3 中,我们可以看到两条强度相同(表示为具有相同宽度)的光线以一定角度迎面撞击表面。
光线携带的能量均匀地分布在它们所击中的区域。
右侧射线的能量比左侧射线的能量传播的区域更大,因此其区域中的每个点接收的能量都比左侧的少。
为了在数学上探索这一点,让我们通过其法线向量来表征表面的方向。
一个表面在 P P P 点的法线向量,或者简单地说“法线”,是一个垂直于 P P P 点的表面的向量。它也是一个单位向量,意味着它的长度是 1 1 1。我们称这个向量为 N ⃗ \vec{N} N 。
4.1. 漫反射建模
方向为 L ⃗ \vec{L} L , 强度为 I I I 的光线照射到具有法线 N ⃗ \vec{N} N 的表面。
作为 I I I、 N ⃗ \vec{N} N 和 L ⃗ \vec{L} L 的函数,有多少 I I I 被反射回场景?
作为几何类比,让我们将光的强度表示为光线的“宽度”。 它的能量分布在大小为 A A A 的表面上。当 N ⃗ \vec{N} N 和 L ⃗ \vec{L} L 具有相同方向时——也就是说,当光线垂直于表面时——则 I = A I = A I=A,这意味着每单位面积反射的能量与每单位面积的入射能量相同。
用公式表示为: I A = 1 \dfrac {I} {A} = 1 AI=1。另一方面, L ⃗ \vec{L} L 和 N ⃗ \vec{N} N 的夹角接近 90 ° 90° 90°时, A A A接近 ∞ ∞ ∞,所以单位面积的能量接近 0 0 0; l i m A → ∞ I A = 0 lim_{A→∞ } \dfrac{I}{A} = 0 limA→∞AI=0。但是介于两者之间会发生什么?
这种情况如图 3-4 所示。 我们知道 N ⃗ \vec{N} N , L ⃗ \vec{L} L 和 P P P; 我添加了角度 α α α 和 β β β,以及点 Q Q Q、 R R R 和 S S S,以便更轻松地编写图表。
图 3-4:漫反射计算中涉及的矢量和角度.
由于光线在技术上没有宽度,我们可以假设每件事都发生在一个平坦的、无限小的表面上。
即使是球体的表面,我们所考虑的区域也非常小,与球体的大小相比几乎是平的,就像地球在小尺度下看起来是平的一样。
宽度为 I I I 的光线,以角度 β β β 在 P P P 处撞击表面。 P P P 处的法线为 N ⃗ \vec{N} N,射线携带的能量在 A A A 上传播。我们需要计算 I A \dfrac{I}{A} AI。
假定 R S RS RS为,射线的“宽度”。 根据定义,它垂直 L ⃗ \vec{L} L,也是 P Q PQ PQ 的方向。 因此, P Q PQ PQ 和 Q R QR QR 形成一个直角,使 P Q R PQR PQR 成为一个直角三角形。
P Q R PQR PQR 的一个角度是 90 ° 90° 90°,另一个是 β β β。 因此剩余的角度为 90 ° − β 90° - β 90°−β。 但请注意, N ⃗ \vec{N} N和 P R PR PR 也形成直角,这意味着 α + β α + β α+β 也必须是 90 ° 90° 90°。 因此, ∠ Q R P = α \angle{QRP} = α ∠QRP=α。
让我们关注三角形 △ \triangle △PQR(图 3-5)。 它的角度是 α α α、 β β β 和 90 ° 90° 90°。边 Q R QR QR 测量值为 I 2 \dfrac{I}{2} 2I ,边 PR 测量值为 A 2 \dfrac{A}{2} 2A 。
图 3-5:上下文中的 PQR 三角形
现在,三角函数来拯救!
根据定义,
c
o
s
(
α
)
=
Q
R
P
R
cos(α) = \dfrac{QR}{PR}
cos(α)=PRQR ; 用
I
2
\dfrac{I}{2}
2I代替
Q
R
QR
QR , 用
A
2
\dfrac{A}{2}
2A代替
P
R
PR
PR,我们得到:
c
o
s
(
α
)
=
I
2
A
2
cos(α) = \dfrac{\frac{I}{2}}{\frac{A}{2}}
cos(α)=2A2I
∴
\therefore
∴
c
o
s
(
α
)
=
I
A
cos(α) = \dfrac{I}{A}
cos(α)=AI
我们快得到了。 α α α 是 N ⃗ \vec{N} N 和 L ⃗ \vec{L} L 之间的角度。
我们可以使用点积的性质 (
a
⃗
⋅
b
⃗
=
∣
a
⃗
∣
∗
∣
b
⃗
∣
∗
cos
(
θ
)
\vec{a}\cdot\vec{b} = |\vec{a}| * |\vec{b}| * \cos(\theta)
a⋅b=∣a∣∗∣b∣∗cos(θ))(请随时查阅线性代数附录)将 cos(α) 表示为:
c
o
s
(
α
)
=
N
⃗
⋅
L
⃗
∣
N
⃗
∣
∣
L
⃗
∣
cos(α) = \dfrac{\vec{N}\cdot\vec{L}}{|\vec{N}| |\vec{L}|}
cos(α)=∣N∣∣L∣N⋅L
整理,得:
I
A
=
N
⃗
⋅
L
⃗
∣
N
⃗
∣
∣
L
⃗
∣
\dfrac{I}{A} = \dfrac{\vec{N}\cdot\vec{L}}{|\vec{N}| |\vec{L}|}
AI=∣N∣∣L∣N⋅L
我们已经得出了一个简单的方程,它给出了反射的光的分数,它是表面法线和光的方向之间的角度的函数。
请注意,对于超过 90 ° 90° 90° 的角度, c o s ( α ) cos(α) cos(α) 的值变为负值。 如果我们盲目地使用这个值,我们最终会得到一个使表面更暗的光源! 这没有任何物理意义。
超过 90 ° 90° 90° 的角度仅表示光线实际上照亮了表面的背面,因此它不会对我们正在照亮的点贡献任何光线。 因此,如果 cos(α) 变为负数,我们需要将其视为 0。
4.2. 漫反射方程
我们现在可以制定一个方程来
计算在具有强度为
I
A
I_A
IA 的环境光源和
n
n
n 个点 或 强度为
I
n
I_n
In 定向光源 和光矢量
L
n
⃗
\vec{L_n}
Ln 的定向光源 的场景中,具有法线
N
⃗
\vec{N}
N 的一个点
P
P
P 接收到的全部光量 ,或者 为
P
P
P 计算(对于点光源):
I
P
=
I
A
+
∑
i
=
1
n
I
i
N
⃗
⋅
L
i
⃗
∣
N
⃗
∣
∣
L
i
⃗
∣
I_P = I_A + \sum_{i=1}^n I_i \dfrac{\vec{N}\cdot\vec{L_i}} {|\vec{N}| |\vec{L_i}|}
IP=IA+i=1∑nIi∣N∣∣Li∣N⋅Li
值得重复的是,不应该将 N ⃗ ⋅ L i ⃗ < 0 \vec{N}\cdot\vec{L_i} < 0 N⋅Li<0 的项添加到点的光照中。
4.3. 球体法线
只缺少一个小细节:法线从何而来?
正如我们将在本书的第二部分中看到的那样,这个一般性问题的答案比看起来要复杂得多。
幸运的是,此时我们只处理球体,并且有一个非常简单的答案:球体任意点的法向量位于穿过球体中心的直线上。 如图 3-6 所示,如果球心为
C
C
C,则点
P
P
P 的法线方向为
P
–
C
P – C
P–C。
图 3-6:球体在 P 处的法线与 CP 方向相同。
为什么是“法线的方向”而不是“法线”?
法线向量需要垂直于表面,但它的长度也必须为 1。为了规范化这个向量并将其变成真正的法线,我们需要将它除以它自己的长度,从而保证结果的长度为 1:
N
⃗
=
P
−
C
∣
P
−
C
∣
\vec{N} = \dfrac{P-C}{|P - C|}
N=∣P−C∣P−C
相当于坐标系中,
O
O
O为坐标原点,
N
⃗
=
O
P
−
O
C
∣
O
P
−
O
C
∣
\vec{N} = \dfrac{OP-OC}{|OP - OC|}
N=∣OP−OC∣OP−OC
4.4. 使用漫反射渲染
让我们将所有这些转换为伪代码。 首先,让我们在场景中添加几个灯光:
light {
type = ambient
intensity = 0.2
}
light {
type = point
intensity = 0.6
position = (2, 1, 0)
}
light {
type = directional
intensity = 0.2
direction = (1, 4, 4)
}
请注意,强度加起来很方便,为 1.0 1.0 1.0; 由于光照方程的作用方式,这确保了没有任何点的光照强度大于该值。 这意味着我们不会有任何“过度曝光”的点。
将光照方程转换为伪代码非常简单(示例 3-1)。
ComputeLighting(P, N)
{
i = 0.0
for light in scene.Lights
{
if light.type == ambient
{
❶ i += light.intensity
}
else
{
if light.type == point
{
❷ L = light.position - P
}
else
{
❸ L = light.direction
}
n_dot_l = dot(N, L)
❹ if n_dot_l > 0
{
❺ i += light.intensity * n_dot_l/(length(N) * length(L))
}
}
}
return i
}
示例 3-1:计算漫反射光照的函数
在示例 3-1 中,我们以稍微不同的方式处理这三种类型的光。
环境光是最简单的,直接处理❶。
点光源和方向光共享大部分代码,尤其是强度计算❺,但方向向量的计算方式不同(❷和❸),具体取决于它们的类型。 ❹ 中的条件确保我们不添加负值,如前所述,负值表示照亮表面背面的光。
剩下要做的就是在 TraceRay
中使用 ComputeLighting
。 我们替换返回球体颜色的这一行:
return closest_sphere.color
用这个片段:
P = O + closest_t * D // Compute intersection
N = P - closest_sphere.center // Compute sphere normal at intersection
N = N / length(N)
return closest_sphere.color * ComputeLighting(P, N)
只是为了好玩,让我们添加一个黄色的大球体:
sphere {
color = (255, 255, 0) # Yellow
center = (0, -5001, 0)
radius = 5000
}
我们运行渲染器,你瞧,球体现在开始看起来像球体(图 3-7)!
图 3-7:漫反射为场景增加了深度和体积感。
您可以在以下位置找到该算法的实时实现:https://gabrielgambetta.com/computer-graphics-from-scratch/demos/raytracer-02.html
但是等等,大黄色球体是如何变成平坦的黄色地板的? 它没有; 与其他三个球体相比,它是如此之大,而且相机离它如此之近,以至于它看起来很平坦——就像我们站在地球上时地球表面看起来是平坦的一样。
五、镜面反射
让我们把注意力转向闪亮的物体。 与哑光物体不同,闪亮的物体看起来略有不同,具体取决于您从哪里看。
想象一下刚洗完车的台球或汽车。 这些类型的物体表现出非常特殊的光模式,通常是亮点,当你在它们周围移动时,它们似乎在移动。 与哑光对象不同,您感知这些对象表面的方式实际上取决于您的观点【也就是观察的位置】。
请注意,如果您在红色台球周围走动,它会保持红色,但使它具有闪亮外观的亮白点会随着您的移动而移动。 这表明我们想要建模的新效果并没有取代漫反射,而是对其进行了补充。
为了理解为什么会发生这种情况,让我们仔细看看表面是如何反射光的。 正如我们在上一节中看到的,当一束光线照射到无光泽物体的表面时,它会在各个方向均匀地散射回场景中。 发生这种情况是因为物体的表面是不规则的,因此在微观层面上,它的行为就像一组指向随机方向的微小表面(图 3-8)。
图 3-8:无光泽物体的粗糙表面在显微镜下可能看起来像什么。 入射光线沿随机方向反射。
但是如果表面不是那么不规则呢?
让我们走向另一个极端:完美抛光的镜子。 当一束光线照射到镜子上时,它会向一个方向反射。 如果我们将反射光的方向称为
R
⃗
\vec{R}
R,并且我们坚持
L
⃗
\vec{L}
L **指向光源(入射光线)**的约定,图 3-9 说明了这种情况。
图 3-9:镜子反射的光线
根据表面的“抛光”程度,它或多或少像一面镜子。 这就是为什么它被称为镜面反射,来自 speculum,拉丁语中的镜子。
对于完美抛光的镜子,入射光线 L ⃗ \vec{L} L 沿单一方向 R ⃗ \vec{R} R 反射。 这就是为什么您可以非常清楚地看到反射物体的原因:
对于每条入射光 L ⃗ \vec{L} L,都有一条反射光线 R ⃗ \vec{R} R。 但并非每件物品都经过完美打磨;
虽然大部分光在 R ⃗ \vec{R} R 的方向上反射,但其中一些光在靠近 R ⃗ \vec{R} R 的方向上反射。
越接近 R ⃗ \vec{R} R ,在该方向反射的光就越多,如图 3-10 所示。
物体的“光泽度”决定了当您远离 R ⃗ \vec{R} R 时反射光减少的速度。
图 3-10:对于未完美抛光的表面,方向越接 R ⃗ \vec{R} R,在该方向反射的光线越多。
我们想知道有多少来自
L
⃗
\vec{L}
L 的光被反射回我们的视点方向。
如
V
⃗
\vec{V}
V是从
P
P
P指向相机的“视图向量”,
α
α
α是
R
⃗
\vec{R}
R和
V
⃗
\vec{V}
V 之间的角度,我们得到图3-11。
图 3-11:镜面反射计算中涉及的向量和角度
对于
α
=
0
°
α = 0°
α=0°,所有光都以
V
⃗
\vec{V}
V 方向反射。
对于
α
=
90
°
α = 90°
α=90° ,没有光被反射。
与漫反射一样,我们需要一个数学表达式来确定
α
α
α 的中间值会发生什么。
5.1. 镜面反射建模
在本章的开头,我提到了一些模型不是基于物理模型的。 这是其中之一。
以下模型是任意的,但使用它是因为它易于计算且看起来不错。
思考
c
o
s
(
α
)
cos(α)
cos(α)。 它具有
c
o
s
(
0
)
=
1
cos(0) = 1
cos(0)=1 和
c
o
s
(
±
90
)
=
0
cos(±90) =0
cos(±90)=0 的优点,就像我们需要的那样;
并且数值从
0
0
0 到
90
90
90 逐渐变小,曲线非常令人满意(图 3-12)。
图 3-12:cos(α) 的曲线图
这意味着 c o s ( α ) cos(α) cos(α) 符合我们对镜面反射函数的所有要求,那么为什么不使用它呢?
还有一个细节。 如果我们直接使用这个公式,每个物体都会同样闪亮。 我们如何调整方程来表示不同程度的光泽度?
请记住,光泽度是反射函数随着
α
α
α 增加而减小的速度的量度。
获得不同光泽度曲线的一种简单方法是计算
c
o
s
(
α
)
cos(α)
cos(α) 对某个正指数
s
s
s 的幂。
由于
0
≤
c
o
s
(
α
)
≤
1
0 ≤ cos(α) ≤ 1
0≤cos(α)≤1,我们保证
0
≤
c
o
s
(
α
)
s
≤
1
0 ≤ cos(α)^s ≤ 1
0≤cos(α)s≤1;
所以
c
o
s
(
α
)
s
cos(α)^s
cos(α)s 就像
c
o
s
(
α
)
cos(α)
cos(α)曲线 一样,只是“更窄”。 图 3-13 显示了不同
s
s
s 值下
c
o
s
(
α
)
s
cos(α)^s
cos(α)s 的图表。
图 3-13: c o s ( α ) s cos(α)^s cos(α)s 图
s
s
s 的值越大,函数越“窄”到
0
0
0 附近,物体看起来越亮。
s
s
s 称为镜面反射指数,它是表面的属性。
由于该模型不是基于物理现实,
s
s
s 的值只能通过反复试验来确定——本质上是调整值直到它们看起来“正确”。 对于基于物理的模型,您可以查看双向反射函数 (BDRF)。
让我们把所有这些放在一起。 一束光线从方向 L ⃗ \vec{L} L 在点 P P P 处以镜面反射指数 s s s 撞击表面,其法线为 N ⃗ \vec{N} N。 有多少光反射到观察方向 V ⃗ \vec{V} V?
根据我们的模型,这个值是 c o s ( α ) s cos(α)^s cos(α)s,其中 α α α 是 V ⃗ \vec{V} V 和 R ⃗ \vec{R} R之间的夹角; R ⃗ \vec{R} R又是 L ⃗ \vec{L} L 相对于 N ⃗ \vec{N} N的反射(对称)。 所以第一步是从 N ⃗ \vec{N} N 和 L ⃗ \vec{L} L 计算 R ⃗ \vec{R} R。
我们可以
L
⃗
\vec{L}
L分解为两个向量
L
p
⃗
\vec{L_p}
Lp和
L
n
⃗
\vec{L_n}
Ln,使
L
⃗
=
L
n
⃗
+
L
p
⃗
\vec{L}=\vec{L_n}+\vec{L_p}
L=Ln+Lp,其中
L
n
⃗
\vec{L_n}
Ln平行于
N
⃗
\vec{N}
N,
L
p
⃗
\vec{L_p}
Lp垂直于
N
⃗
\vec{N}
N(图3-14)。
L
n
⃗
∥
N
⃗
,
L
p
⃗
⊥
N
⃗
\vec{L_n} \parallel \vec{N} , \vec{L_p}\perp\vec{N}
Ln∥N,Lp⊥N
图 3-14:将 L ⃗ \vec{L} L分解为其分向量 L p ⃗ \vec{L_p} Lp 和 L n ⃗ \vec{L_n} Ln
L
n
⃗
\vec{L_n}
Ln 是
L
⃗
\vec{L}
L在
N
⃗
\vec{N}
N 上的投影;
通过点积的性质和
N
⃗
\vec{N}
N= 1,这个投影的长度是
N
⃗
⋅
L
⃗
\vec{N}\cdot\vec{L}
N⋅L。
N
⃗
⋅
L
⃗
=
∣
N
⃗
∣
∣
L
⃗
∣
cos
∠
N
⃗
L
⃗
=
∣
L
⃗
∣
cos
∠
N
⃗
L
⃗
\vec{N}\cdot\vec{L} = |\vec{N}| |\vec{L}| \cos\angle{\vec{N}\vec{L}} = |\vec{L}| \cos\angle{\vec{N}\vec{L}}
N⋅L=∣N∣∣L∣cos∠NL=∣L∣cos∠NL
我们定义
L
n
⃗
\vec{L_n}
Ln平行于
N
⃗
\vec{N}
N,
所以
L
n
⃗
=
N
⃗
(
N
⃗
⋅
L
⃗
)
\vec{L_n} = \vec{N}( \vec{N}\cdot\vec{L})
Ln=N(N⋅L)。
因为
L
⃗
=
L
n
⃗
+
L
p
⃗
\vec{L}=\vec{L_n}+\vec{L_p}
L=Ln+Lp ,我们可以立即得到:
L
p
⃗
=
L
⃗
−
L
n
⃗
=
L
⃗
−
N
⃗
(
N
⃗
⋅
L
⃗
)
\vec{L_p} = \vec{L} - \vec{L_n} = \vec{L} - \vec{N}( \vec{N}\cdot\vec{L})
Lp=L−Ln=L−N(N⋅L)
现在让我们看看
R
⃗
\vec{R}
R 。 由于它与
L
⃗
\vec{L}
L关于
N
⃗
\vec{N}
N对称,它投影于
N
⃗
\vec{N}
N的分量与
L
⃗
\vec{L}
L的分量相同,其垂直分量
L
⃗
\vec{L}
L的相反;
也就是说,
R
⃗
=
L
n
⃗
−
L
p
⃗
\vec{R}=\vec{L_n} - \vec{L_p}
R=Ln−Lp。 您可以在图 3-15 中看到这一点。
图 3-15:计算 L ⃗ \vec{L} L R ⃗ \vec{R} R
用我们上面找到的表达式替换,我们得到:
R
⃗
=
N
⃗
(
N
⃗
⋅
L
⃗
)
−
L
⃗
+
N
⃗
(
N
⃗
⋅
L
⃗
)
\vec{R}=\vec{N}( \vec{N}\cdot\vec{L}) - \vec{L} + \vec{N}( \vec{N}\cdot\vec{L})
R=N(N⋅L)−L+N(N⋅L)
简化得:
R
⃗
=
2
N
⃗
(
N
⃗
⋅
L
⃗
)
−
L
⃗
\vec{R}= 2 \vec{N}( \vec{N}\cdot\vec{L}) - \vec{L}
R=2N(N⋅L)−L
5.2. 镜面反射项
我们现在准备写一个镜面反射方程:
{
R
⃗
=
2
N
⃗
(
N
⃗
⋅
L
⃗
)
−
L
⃗
I
S
=
I
L
(
R
⃗
⋅
V
⃗
∣
R
∣
⃗
∣
V
∣
⃗
)
s
\begin{cases} \vec{R}= 2 \vec{N}( \vec{N}\cdot\vec{L}) - \vec{L} \\ I_S = I_L (\dfrac {\vec{R}\cdot\vec{V}} {\vec{|R|} \vec{|V|}})^s \end{cases}
⎩⎪⎨⎪⎧R=2N(N⋅L)−LIS=IL(∣R∣∣V∣R⋅V)s
与漫反射照明一样, c o s ( α ) cos(α) cos(α) 可能为负数,出于与之前相同的原因,我们应该忽略它。
此外,并非每个物体都必须是闪亮的。 对于遮罩对象,根本不应该计算镜面反射项。
我们将通过将它们的镜面反射指数设置为
–
1
–1
–1, 并相应地处理它们来 在场景中注意到这一点。
5.3. 全光照方程
我们可以将镜面反射项添加到我们一直在开发的照明方程中,并得到一个描述某个点的照明的表达式:
I P = I A + ∑ i = 1 n I i ⋅ [ { N ⃗ ⋅ L i ⃗ ∣ N ⃗ ∣ ∣ L i ⃗ ∣ + ( R i ⃗ ⋅ V ⃗ ∣ R i ⃗ ∣ ∣ V ⃗ ∣ ) s ] I_P = I_A + \sum_{i=1}^n I_i · \left[ \begin{cases} \dfrac {\vec{N}\cdot\vec{L_i}} {|\vec{N}| |\vec{L_i}|} + (\dfrac {\vec{R_i}\cdot\vec{V}} {|\vec{R_i}| |\vec{V}|})^s \end{cases} \right] IP=IA+i=1∑nIi⋅[{∣N∣∣Li∣N⋅Li+(∣Ri∣∣V∣Ri⋅V)s]
其中 I P I_P IP 是 P P P 点的总照度, I A I_A IA 是环境光的强度, N N N 是表面在 P P P 处的法线, V V V 是从 P P P 到相机的矢量, s s s 是表面的镜面反射指数, I i I_i Ii 是光 i i i 的强度, L i L_i Li 是从 P P P 到光 i i i 的矢量, R i R_i Ri 是光 i i i 在 P P P 处的反射矢量。
sphere {
center = (0, -1, 3)
radius = 1
color = (255, 0, 0) # Red
specular = 500 # Shiny
}
sphere {
center = (2, 0, 4)
radius = 1
color = (0, 0, 255) # Blue
specular = 500 # Shiny
}
sphere {
center = (-2, 0, 4)
radius = 1
color = (0, 255, 0) # Green
specular = 10 # Somewhat shiny
}
sphere {
center = (0, -5001, 0)
radius = 5000
color = (255, 255, 0) # Yellow
specular = 1000 # Very shiny
}
这是与之前相同的场景,在球体定义中添加了镜面反射指数。
在代码级别,我们需要更改 ComputeLighting
以在必要时计算镜面反射项,并将其添加到整体光照中。 请注意,该函数现在需要
V
⃗
\vec{V}
V 和
s
s
s,如示例 3-2 所示。
ComputeLighting(P, N, V, s) {
i = 0.0
for light in scene.Lights
{
if light.type == ambient
{
i += light.intensity
}
else
{
if light.type == point
{
L = light.position - P
}
else {
L = light.direction
}
// Diffuse
n_dot_l = dot(N, L)
if n_dot_l > 0
{
i += light.intensity * n_dot_l/(length(N) * length(L))
}
// Specular
❶ if s != -1
{
R = 2 * N * dot(N, L) - L
r_dot_v = dot(R, V)
❷ if r_dot_v > 0
{
i += light.intensity * pow(r_dot_v/(length(R) * length(V)), s)
}
}
}
}
return i
}
清单 3-2:同时支持漫反射和镜面反射的
ComputeLighting
大部分代码保持不变,但我们添加了一个片段来处理镜面反射。 我们确保它只适用于闪亮的物体❶,并确保我们不会像漫反射那样添加负光强度❷。
最后,我们需要修改 TraceRay
以将新参数传递给 Compute Lighting
。
s
s
s 很简单:它直接来自场景定义。 但是
V
⃗
\vec{V}
V从何而来?
V
⃗
\vec{V}
V 是从物体指向相机的向量。 幸运的是,我们已经有了一个从相机指向 TraceRay
对象的向量——这就是
D
⃗
\vec{D}
D ,我们正在追踪的光线的方向! 所以
V
⃗
\vec{V}
V 就是
–
D
⃗
–\vec{D}
–D 。
示例3-3 给出了带有镜面反射的新 TraceRay。
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
}
P = O + closest_t * D // Compute intersection
N = P - closest_sphere.center // Compute sphere normal at intersection
N = N / length(N)
❶ return closest_sphere.color * ComputeLighting(P, N, -D, closest_sphere.specular)
}
颜色计算❶比看起来要复杂一些。 请记住,颜色必须按通道相乘,并且结果必须限制在通道的范围内(在我们的例子中, [ 0 − 255 ] [0-255] [0−255])。 尽管在示例场景中光强度加起来为 1.0 1.0 1.0,但现在我们添加了镜面反射的贡献,这些值可能会超出该范围。
你可以在图 3-16 中看到所有这些向量推算之后的效果。
图 3-16:使用环境反射、漫反射和镜面反射渲染的场景。 我们不仅可以获得深度和体积感,而且每个表面的外观也略有不同。
您可以在以下位置找到该算法的实时实现: https://gabrielgambetta.com/computer-graphics-from-scratch/demos/raytracer-03.html
请注意,在图 3-16 中,镜面反射指数为 500 500 500 的红色球体比镜面反射指数为 10 10 10 的绿色球体具有更集中的亮点,正如预期的那样。 蓝色球体的镜面反射指数也为 500 500 500,但没有可见的亮点。 这只是图像如何裁剪以及灯光如何放置在场景中的结果; 事实上,红色球体的左半部分也没有表现出任何镜面反射。
六、概括
在本章中,我们采用了上一章开发的非常简单的光线追踪器,并赋予它建模灯光以及它们与场景中对象交互方式的能力。
我们将灯光分为三种类型:点光源、定向光源和环境光源。 我们探索了它们中的每一个如何代表您在现实生活中可以找到的不同类型的光,以及如何在我们的场景定义中描述它们。
然后我们将注意力转向场景中物体的表面,将它们分为两种类型:哑光和闪亮。 我们讨论了光线如何与它们相互作用,并开发了两种模型——漫反射和镜面反射——来计算它们向相机反射的光量。
最终结果是场景的渲染更加逼真:我们现在不仅可以看到物体的轮廓,还可以真正感受到深度和体积感,以及对物体材质的感觉。
然而,我们缺少了灯光的一个基本方面:阴影。 这是下一章的重点。