1. 反射方向的计算
反射方向的计算是光线追踪中一个重要的数学过程,用于模拟光线在物体表面反射的行为。通过计算反射方向,可以生成新的光线来追踪反射光的传播路径。
1.1 反射方向公式:
反射光的方向基于光的反射定律:入射角等于反射角,且反射光方向与表面法线关于法线对称。
数学公式为:
R
=
I
−
2
⋅
(
I
⋅
N
)
⋅
N
\mathbf{R} = \mathbf{I} - 2 \cdot (\mathbf{I} \cdot \mathbf{N}) \cdot \mathbf{N}
R=I−2⋅(I⋅N)⋅N
其中:
- R \mathbf{R} R 是反射光线的方向(单位向量)。
- I \mathbf{I} I 是入射光线的方向(单位向量)。
- N \mathbf{N} N 是物体表面的法线向量(单位向量),垂直于物体表面。
- I ⋅ N \mathbf{I} \cdot \mathbf{N} I⋅N 是入射光线与法线的点积,即光线入射方向与法线方向之间的夹角的余弦值。它的物理意义是光线在法线方向上的投影长度。
通常默认 R \mathbf{R} R, I \mathbf{I} I, N \mathbf{N} N为单位向量(长度为1),以确保反射方向的计算正确,如果不是单位向量,需要先归一化。
1.2 代码实现
Vector3f reflect(const Vector3f &I, const Vector3f &N)
{
return I - 2 * dotProduct(I, N) * N;
}
I
: 入射光线的方向。N
: 法线向量。
代码解释:
dotProduct(I, N)
: 计算入射光线与法线之间的点积,即入射光线在法线方向上的投影长度。2 * dotProduct(I, N) * N
: 计算入射光线在法线方向上的镜像部分。I - 2 * dotProduct(I, N) * N
: 从入射光线 ( I ) 中减去镜像部分,得到反射方向 ( R )。
2. 折射方向计算
折射方向计算是基于 斯涅尔定律(Snell’s Law),用于模拟光线从一个介质进入另一个介质时的偏折现象。在计算折射方向时,我们需要根据折射率的不同和入射光线与表面法线的角度,来确定光线在折射时的变化。
2.1 折射定律(Snell’s Law 斯涅尔定律)
折射定律(Snell’s Law 斯涅尔定律)描述了光线从一种介质进入另一种介质时,折射角和入射角之间的关系。其数学表达式为:
n
1
sin
(
θ
1
)
=
n
2
sin
(
θ
2
)
n_1 \sin(\theta_1) = n_2 \sin(\theta_2)
n1sin(θ1)=n2sin(θ2)
其中:
- n 1 n_1 n1 和 n 2 n_2 n2 分别是介质 1 和介质 2 的折射率。
- θ 1 \theta_1 θ1是入射角(光线与法线之间的角度)。
- θ 2 \theta_2 θ2是折射角(折射光线与法线之间的角度)。
折射率(Refraction Index):是光线在介质中的传播速度与真空中传播速度的比值。空气的折射率通常为 1,其他材料如玻璃、液体等的折射率大于 1。
2.2 折射的方向计算
折射方向的计算公式通过斯涅尔定律来推导。具体公式如下:
T
=
η
⋅
I
+
(
η
⋅
cos
(
θ
1
)
−
cos
(
θ
2
)
)
⋅
N
\mathbf{T} = \eta \cdot \mathbf{I} + (\eta \cdot \cos(\theta_1) - \cos(\theta_2)) \cdot \mathbf{N}
T=η⋅I+(η⋅cos(θ1)−cos(θ2))⋅N
其中:
- T \mathbf{T} T 是折射光线的方向(单位向量)。
- I \mathbf{I} I 是入射光线的方向(单位向量)。
- N \mathbf{N} N 是物体表面的法线向量(单位向量)。
- η \eta η 是折射率比值,通常定义为 η = n 1 n 2 \eta = \frac{n_1}{n_2} η=n2n1。
为了计算
θ
2
\theta_2
θ2(折射角),我们可以使用三角恒等式:
sin
2
(
θ
2
)
=
1
−
1
η
2
⋅
(
1
−
cos
2
(
θ
1
)
)
\sin^2(\theta_2) = 1 - \frac{1}{\eta^2} \cdot (1 - \cos^2(\theta_1))
sin2(θ2)=1−η21⋅(1−cos2(θ1))
这里的
cos
(
θ
2
)
\cos(\theta_2)
cos(θ2) 是通过折射率比
η
\eta
η 和入射角
θ
1
\theta_1
θ1 计算出来的。
2.3 代码实现
在代码中,refract
函数实现了折射光线的计算,代码如下:
Vector3f refract(const Vector3f &I, const Vector3f &N, const float &ior)
{
float cosi = clamp(-1, 1, dotProduct(I, N)); // 入射角的余弦值
float etai = 1, etat = ior; // etai 是空气的折射率,etat 是目标材料的折射率
Vector3f n = N;
// 处理光线从物体内部出来的情况
if (cosi < 0) {
cosi = -cosi;
} else {
std::swap(etai, etat);
n = -N; // 如果光线在物体内部,则法线方向需要取反
}
// 计算折射率比值 eta
float eta = etai / etat;
// 计算判定是否发生全反射的参数 k
float k = 1 - eta * eta * (1 - cosi * cosi);
// 如果 k 小于 0,表示发生了全反射,返回零向量
return k < 0 ? 0 : eta * I + (eta * cosi - sqrtf(k)) * n;
}
代码解释:
-
cosi = clamp(-1, 1, dotProduct(I, N));
作用:计算入射光线与表面法线之间的夹角余弦值(入射角的余弦)。dotProduct(I, N)
计算的是光线与法线的点积,结果是夹角余弦值。clamp(-1, 1, ...)
保证结果在 -1 到 1 的范围内。
背景知识:点积是计算两个向量之间夹角余弦的一种方式,公式为 I ⋅ N = cos ( θ ) \mathbf{I} \cdot \mathbf{N} = \cos(\theta) I⋅N=cos(θ)。 -
if (cosi < 0)
作用:如果入射角大于 90 度(即光线从物体内部传播出来),则取反法线方向,更新折射率比值(交换etai
和etat
)。
背景知识:当光线从物体内部向外传播时,折射率的方向应该反转,因为从物体内到外的折射方向和从外到内的折射方向是相反的。 -
float eta = etai / etat;
作用:计算折射率比值 η \eta η,决定光线如何折射。根据斯涅尔定律,折射率比值影响折射角的大小。 -
float k = 1 - eta * eta * (1 - cosi * cosi);
作用:计算一个判定值 k k k,用于判断是否发生全反射。当 k < 0 k < 0 k<0 时,光线会完全反射,没有折射。
背景知识:当入射角过大时,折射角的正弦值可能大于 1,导致没有折射光线发生,此时计算的 k k k 为负值,表示发生了全反射。 -
return k < 0 ? 0 : eta * I + (eta * cosi - sqrtf(k)) * n;
作用:如果 k < 0 k < 0 k<0,则表示发生全反射,返回零向量。否则,计算折射光线的方向 T \mathbf{T} T,即折射方向向量。
公式解释:- η ⋅ I \eta \cdot \mathbf{I} η⋅I:光线在折射介质中的传播方向。
- ( η ⋅ cos ( θ 1 ) − k ) ⋅ N (\eta \cdot \cos(\theta_1) - \sqrt{k}) \cdot \mathbf{N} (η⋅cos(θ1)−k )⋅N:这部分是调整的法线方向部分,它决定了折射光线的具体偏折。
2.4 全反射与折射
当光线从一个折射率较高的介质(如水或玻璃)进入折射率较低的介质(如空气)时,如果入射角过大,可能会发生全反射现象。此时,光线不会折射,而是完全反射回去。通过判断 k k k 的值,可以决定是计算折射光线还是返回全反射。
3. 菲涅耳反射系数
菲涅耳反射系数是描述光线在两种介质交界面发生反射时,反射光强度与入射角之间关系的数学模型。在计算机图形学中,菲涅耳反射系数是光线追踪中反射和折射比例的重要参数。它通过几何和光学属性计算反射光的比例,来模拟物体表面的光学特性,从而增强视觉真实感。代码实现基于菲涅耳方程,准确判断反射与折射的关系,适用于透明材质(如玻璃、水)和高光材质(如金属)的模拟。
3.1 菲涅耳反射的基本原理
当光线从一种介质射向另一种介质时,部分光线被反射,部分光线被折射。反射与折射的比例取决于:
- 两种介质的折射率 n 1 n_1 n1 和 n 2 n_2 n2。
- 入射光线与表面法线的夹角(入射角)。
菲涅耳定律
根据菲涅耳定律,反射光强度由偏振方向决定,分为两部分:
- 垂直偏振光( R s R_s Rs):
R s = ( n 2 cos θ i − n 1 cos θ t n 2 cos θ i + n 1 cos θ t ) 2 R_s = \left( \frac{n_2 \cos\theta_i - n_1 \cos\theta_t}{n_2 \cos\theta_i + n_1 \cos\theta_t} \right)^2 Rs=(n2cosθi+n1cosθtn2cosθi−n1cosθt)2
- θ i \theta_i θi:入射角,入射光线与法线之间的夹角。
- θ t \theta_t θt:折射角,折射光线与法线之间的夹角。
- 平行偏振光( R p R_p Rp):
R p = ( n 1 cos θ i − n 2 cos θ t n 1 cos θ i + n 2 cos θ t ) 2 R_p = \left( \frac{n_1 \cos\theta_i - n_2 \cos\theta_t}{n_1 \cos\theta_i + n_2 \cos\theta_t} \right)^2 Rp=(n1cosθi+n2cosθtn1cosθi−n2cosθt)2
- 未偏振光的菲涅耳反射系数:
对于未偏振光(即自然光),总的反射系数是两种偏振光反射系数的平均值:
R = R s + R p 2 R = \frac{R_s + R_p}{2} R=2Rs+Rp
3.2 菲涅耳效应
菲涅耳效应描述了反射光的强度随着观察角度(即入射角)的变化而变化:
- 小入射角(垂直观察):反射光较弱,更多光线进入另一介质(折射)。
- 大入射角(平行观察):反射光较强,表面反射更明显。
在视觉上,这种效应表现为:
- 水表面:正上方观察时反射较弱,远处反射天空的效果更明显。
- 金属表面:高光区域的反射随角度增强。
3.3 菲涅耳反射系数在光线追踪中的应用
在光线追踪中,菲涅耳反射系数用于决定反射光和折射光的比例。例如,对于玻璃材质,部分光线反射,部分光线折射,通过菲涅耳系数来控制两种光线的权重。
实现流程:
- 计算菲涅耳反射系数:
根据光线方向、表面法线和材质的折射率,计算反射光所占比例 R R R。 - 混合反射光和折射光:
- 反射光:使用光线追踪递归计算。
- 折射光:递归计算折射方向上的颜色。
最终颜色 = R ⋅ 反射光颜色 + ( 1 − R ) ⋅ 折射光颜色 \text{最终颜色} = R \cdot \text{反射光颜色} + (1 - R) \cdot \text{折射光颜色} 最终颜色=R⋅反射光颜色+(1−R)⋅折射光颜色
3.4 代码实现
float fresnel(const Vector3f &I, const Vector3f &N, const float &ior)
{
float cosi = clamp(-1, 1, dotProduct(I, N)); // 入射角余弦值
float etai = 1, etat = ior; // etai:外部折射率,etat:内部折射率
if (cosi > 0) { std::swap(etai, etat); } // 光线从内部射出时交换折射率
// 计算折射角的正弦值
float sint = etai / etat * sqrtf(std::max(0.f, 1 - cosi * cosi));
// 判断是否发生全反射
if (sint >= 1) {
return 1; // 完全反射,反射系数为 1
}
else {
float cost = sqrtf(std::max(0.f, 1 - sint * sint)); // 折射角余弦值
cosi = fabsf(cosi); // 确保 cosi 为正
// 垂直偏振光的菲涅耳反射系数
float Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost));
// 平行偏振光的菲涅耳反射系数
float Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost));
// 返回平均反射系数
return (Rs * Rs + Rp * Rp) / 2;
}
}
代码解析:
clamp(-1, 1, dotProduct(I, N))
:
确保入射角余弦值 cos θ \cos\theta cosθ 在有效范围内。std::swap(etai, etat)
:
如果光线从物体内部射出,交换介质折射率 n 1 n_1 n1 和 n 2 n_2 n2。- 全反射判断:
如果折射角正弦值 sin θ t ≥ 1 \sin\theta_t \geq 1 sinθt≥1,说明光线无法折射,发生全反射,返回反射系数 1。 - 菲涅耳系数计算:
分别计算垂直偏振光( R s R_s Rs)和平行偏振光( R p R_p Rp)的反射系数。
最终返回两者的平均值。
4. 光线追踪中的相交检测
相交检测是光线追踪算法的核心步骤之一,用于确定场景中光线与物体是否相交,并找到最近的交点。它决定了光线在场景中的行为,例如光线是否反射、折射或终止。
4.1 相交检测的目的
相交检测的目标是:
- 判断光线是否与场景中的物体相交。
- 找到光线与物体的最近交点。
- 返回交点的详细信息,例如交点的位置、法线、材质属性等。
这些信息用于后续的光照计算和递归处理(反射、折射)。
4.2 光线方程
光线在三维空间中的数学表示是:
P ( t ) = o r i g + t ⋅ d i r \mathbf{P}(t) = \mathbf{orig} + t \cdot \mathbf{dir} P(t)=orig+t⋅dir
其中:
- P ( t ) \mathbf{P}(t) P(t) 是光线上某点的坐标。
- o r i g \mathbf{orig} orig 是光线的起点(通常是相机位置或反射光线的起点)。
- d i r \mathbf{dir} dir 是光线的方向向量(单位向量)。
- t t t 是参数,表示从起点到光线上的某点的距离。
相交检测的任务是找到一个 t > 0 t > 0 t>0,使得光线上的点 P ( t ) \mathbf{P}(t) P(t) 满足物体的几何方程。
4.3 几何体的相交检测
在场景中,物体可以是不同的几何形状(如球体、平面、三角形网格等)。每种几何体都有其特定的相交检测算法。
(1) 光线与球体的相交检测
球体的方程为:
∥ P − C ∥ 2 = R 2 \| \mathbf{P} - \mathbf{C} \|^2 = R^2 ∥P−C∥2=R2
其中:
- P \mathbf{P} P 是球面上的点。
- C \mathbf{C} C 是球心。
- R R R 是球的半径。
将光线方程代入球的方程,得到:
∥ ( o r i g + t ⋅ d i r ) − C ∥ 2 = R 2 \| (\mathbf{orig} + t \cdot \mathbf{dir}) - \mathbf{C} \|^2 = R^2 ∥(orig+t⋅dir)−C∥2=R2
展开后化为二次方程:
a ⋅ t 2 + b ⋅ t + c = 0 a \cdot t^2 + b \cdot t + c = 0 a⋅t2+b⋅t+c=0
其中:
- a = d i r ⋅ d i r a = \mathbf{dir} \cdot \mathbf{dir} a=dir⋅dir。
- b = 2 ⋅ d i r ⋅ ( o r i g − C ) b = 2 \cdot \mathbf{dir} \cdot (\mathbf{orig} - \mathbf{C}) b=2⋅dir⋅(orig−C)。
- c = ∥ o r i g − C ∥ 2 − R 2 c = \| \mathbf{orig} - \mathbf{C} \|^2 - R^2 c=∥orig−C∥2−R2。
通过求解二次方程的根,可以找到交点参数 t t t:
t = − b ± b 2 − 4 a c 2 a t = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} t=2a−b±b2−4ac
代码实现:
// 检测光线是否与球体相交
// 参数:
// orig: 光线的起点(通常是相机位置或反射光线的起点)
// dir: 光线的方向(单位向量)
// tnear: 输出参数,用于存储最近的交点距离
// uint32_t&: (未使用的参数)保留接口一致性
// Vector2f&: (未使用的参数)保留接口一致性
// 返回值:如果光线与球体相交,返回 true,并更新 tnear,否则返回 false
bool intersect(const Vector3f& orig, const Vector3f& dir, float& tnear, uint32_t&, Vector2f&) const override
{
// 解析法解二次方程
// L 是从光线起点到球心的向量
Vector3f L = orig - center;
// 二次方程的系数 a、b 和 c
float a = dotProduct(dir, dir); // 光线方向向量的平方(通常为 1,因为 dir 是单位向量)
float b = 2 * dotProduct(dir, L); // 光线方向与 L 的点积
float c = dotProduct(L, L) - radius2; // L 的平方减去球的半径平方
float t0, t1; // 解的两个根
// 检测二次方程是否有解
if (!solveQuadratic(a, b, c, t0, t1))
return false; // 如果没有实数解,光线没有与球体相交
// 检测最近的根 t0 是否为正(光线方向上的交点)
if (t0 < 0)
t0 = t1; // 如果 t0 为负,则检查 t1
if (t0 < 0)
return false; // 如果 t1 也为负,则光线与球体相交的点在光线反方向上
// 更新最近交点的距离
tnear = t0;
return true; // 光线与球体相交
}
二次方程求解过程
-
solveQuadratic(a, b, c, t0, t1)
:- 检测二次方程是否有实数解。
- 如果判别式 ( b^2 - 4ac < 0 ),则无解,光线未与球体相交。
- 如果有解,则计算两个根 ( t0 ) 和 ( t1 )。
-
选择最近的交点:
- 如果 ( t0 ) 为负,说明交点在光线的反方向,取 ( t1 )。
- 如果 ( t1 ) 也为负,说明光线没有与球体相交(交点在反方向上)。
-
更新最近交点:
- 将最近的交点距离存储到
tnear
,以便后续计算光线与场景的相交信息。
- 将最近的交点距离存储到
//解析二次方程 ax^2 + bx + c = 0 的解
inline bool solveQuadratic(const float& a, const float& b, const float& c, float& x0, float& x1)
{
float discr = b * b - 4 * a * c;
if (discr < 0)
return false;
else if (discr == 0)
x0 = x1 = -0.5 * b / a;
else
{
float q = (b > 0) ? -0.5 * (b + sqrt(discr)) : -0.5 * (b - sqrt(discr));
x0 = q / a;
x1 = c / q;
}
// 确保 x0 是较小的解,x1 是较大的解
if (x0 > x1)
std::swap(x0, x1);
return true;
}
(2) 光线与平面的相交检测
平面的方程为:
N ⋅ ( P − Q ) = 0 \mathbf{N} \cdot (\mathbf{P} - \mathbf{Q}) = 0 N⋅(P−Q)=0
其中:
- N \mathbf{N} N 是平面的法线。
- Q \mathbf{Q} Q 是平面上的一个点。
将光线方程代入平面方程,得到:
t = N ⋅ ( Q − o r i g ) N ⋅ d i r t = \frac{\mathbf{N} \cdot (\mathbf{Q} - \mathbf{orig})}{\mathbf{N} \cdot \mathbf{dir}} t=N⋅dirN⋅(Q−orig)
如果 N ⋅ d i r = 0 \mathbf{N} \cdot \mathbf{dir} = 0 N⋅dir=0,说明光线平行于平面,没有交点。
(3) 光线与三角形的相交检测★★★
三角形的检测通常使用 Möller-Trumbore 算法,通过重心坐标判断光线是否穿过三角形。
Möller-Trumbore 算法
是一种高效的光线与三角形相交检测方法,主要用于判断光线是否与三角形相交,并计算交点的具体位置。
1. 关键概念
-
三角形表示:
一个三角形由三个顶点 v 0 \mathbf{v}_0 v0、 v 1 \mathbf{v}_1 v1、 v 2 \mathbf{v}_2 v2 定义。 -
光线表示:
光线由起点 O \mathbf{O} O 和方向向量 D \mathbf{D} D 定义:
R ( t ) = O + t D , t ≥ 0 \mathbf{R}(t) = \mathbf{O} + t\mathbf{D}, \quad t \geq 0 R(t)=O+tD,t≥0
- 重心坐标:
三角形内任意点 P \mathbf{P} P 可以通过重心坐标 ( u , v ) (u, v) (u,v) 表示:
P
=
v
0
+
u
(
v
1
−
v
0
)
+
v
(
v
2
−
v
0
)
,
u
≥
0
,
v
≥
0
,
u
+
v
≤
1
\mathbf{P} = \mathbf{v}_0 + u(\mathbf{v}_1 - \mathbf{v}_0) + v(\mathbf{v}_2 - \mathbf{v}_0), \quad u \geq 0, \, v \geq 0, \, u + v \leq 1
P=v0+u(v1−v0)+v(v2−v0),u≥0,v≥0,u+v≤1
交点
P
\mathbf{P}
P 必须同时满足这两个方程。
2. 算法公式推导
为了快速求解,定义如下向量:
- E 1 = V 1 − V 0 \mathbf{E}_1 = \mathbf{V}_1 - \mathbf{V}_0 E1=V1−V0
- E 2 = V 2 − V 0 \mathbf{E}_2 = \mathbf{V}_2 - \mathbf{V}_0 E2=V2−V0
- S = O − V 0 \mathbf{S} = \mathbf{O} - \mathbf{V}_0 S=O−V0
- S 1 = D × E 2 \mathbf{S}_1 = \mathbf{D} \times\mathbf{E}_2 S1=D×E2
- S 2 = S × E 1 \mathbf{S}_2 = \mathbf{S} \times\mathbf{E}_1 S2=S×E1
然后,利用射线与三角形重心坐标的方程,转化为矩阵形式:
详细推导过程可以参考:计算机图形学——射线与三角形相交检测_Möller-Trumbore算法及推导过程(涉及标量三重积、克莱姆法则)
3. 判断交点是否在三角形内
解得 t , b 1 , b 2 t, b_1, b_2 t,b1,b2 后:
- 如果 t < 0 t < 0 t<0,说明交点在射线起点的反方向。
- 如果 b 1 , b 2 ≥ 0 b_1, b_2 \geq 0 b1,b2≥0 且 1 − b 1 − b 2 ≥ 0 1 - b_1 - b_2 \geq 0 1−b1−b2≥0,说明交点位于三角形内部。
4. Möller-Trumbore算法的应用场景
- 光线追踪:判断光线是否击中物体表面。
- 碰撞检测:快速检测点是否落在多边形表面。
- 三角网格渲染:确定屏幕上像素是否位于三角形内。
光线与三角形的相交__代码实现:
// 检测光线是否与三角形相交
// 参数:
// orig: 光线的起点(通常是相机位置或反射光线的起点)
// dir: 光线的方向(单位向量)
// v0, v1, v2: 三角形的三个顶点
// tnear: 输出参数,光线起点到交点的距离
// u, v: 输出参数,重心坐标中的两个参数,用于判断交点是否在三角形内部
// 返回值:如果光线与三角形相交,返回 true,并更新 tnear, u, v,否则返回 false
bool rayTriangleIntersect(const Vector3f &orig, const Vector3f &dir,
const Vector3f &v0, const Vector3f &v1, const Vector3f &v2,
float &tnear, float &u, float &v)
{
// 计算三角形的两条边
Vector3f E1 = v1 - v0;
Vector3f E2 = v2 - v0;
Vector3f S = orig - v0;
Vector3f S1 = crossProduct(dir, E2);
Vector3f S2 = crossProduct(S, E1);
// 计算行列式(determinant),用于判断光线是否平行于三角形
float det = dotProduct(S1, E1);
if (det <= 0) return false; // 如果行列式为 0 或小于 0,光线与三角形平行或背向三角形
u = dotProduct(S1, S) / det;
if (u < 0 || u > 1) return false; // 如果 u 不在 [0, 1] 范围内,交点在三角形外部
v = dotProduct(S2, dir) / det;
if (v < 0 || u + v > 1) return false; // 如果 v 不在 [0, 1 - u] 范围内,交点在三角形外部
// 计算光线参数 tnear
tnear = dotProduct(S2, E2) / det;
if (tnear < 0) return false; // 如果 tnear 小于 0,交点在光线的反方向
return true; // 光线与三角形相交,返回 true
}
4.4 相交检测的优化
在复杂场景中,场景可能包含成千上万个几何体。直接对每条光线遍历所有物体进行相交检测效率非常低。因此,需要使用一些加速结构来优化相交检测。
(1) 加速结构
- BVH (Bounding Volume Hierarchy):
- 使用层级包围盒分割场景。
- 光线首先与包围盒进行相交检测,如果光线不穿过包围盒,则无需检测其中的物体。
- KD-Tree:
- 将空间递归分割成若干子区域,适用于静态场景的快速相交检测。
(2) 早期退出
在某些情况下,例如阴影检测,只需要判断光线是否与物体相交,而不需要找到最近交点。此时可以在检测到第一个相交物体后立即退出。
4.5 相交检测的基础代码实现
// 光线与物体的相交检测
// orig 是光线的起点,dir 是光线方向
// objects 是场景中的物体集合
// 返回最近的交点信息
std::optional<hit_payload> trace(
const Vector3f &orig, const Vector3f &dir,
const std::vector<std::unique_ptr<Object>> &objects)
{
float tNear = kInfinity; // 初始化最近交点距离为无穷大
std::optional<hit_payload> payload; // 用于存储最近的交点信息
// 遍历所有物体
for (const auto & object : objects)
{
float tNearK = kInfinity; // 当前物体的交点距离
uint32_t indexK; // 如果是网格,记录交点的三角形索引
Vector2f uvK; // 交点的纹理坐标
// 检测光线是否与物体相交
if (object->intersect(orig, dir, tNearK, indexK, uvK) && tNearK < tNear)
{
payload.emplace(); // 更新最近交点信息
payload->hit_obj = object.get(); // 保存相交的物体
payload->tNear = tNearK; // 更新最近交点距离
payload->index = indexK; // 保存三角形索引
payload->uv = uvK; // 保存纹理坐标
tNear = tNearK; // 更新最近交点距离
}
}
// 如果没有相交,返回空
return payload;
}
5. Whitted 风格的光线追踪算法
Whitted 光线追踪算法由 Turner Whitted 于 1980 年提出,是一种通过递归地模拟光线的反射、折射和光照的算法,能够生成真实感的图像,被广泛用于模拟物体表面的光学现象。虽然计算量较大,但它为现代的全局光照和路径追踪算法提供了重要的理论和实现基础。通过结合加速结构(如 BVH)和现代 GPU 渲染技术,可以显著提升其效率。
5.1 算法的核心思想
Whitted 光线追踪算法的核心是递归地模拟光线在场景中的传播过程,包括:
- 直接光照:光线从光源直接照射到物体的表面,产生漫反射和镜面高光。
- 反射光:光线从表面反射,模拟镜面反射的效果。
- 折射光:光线穿透透明物体,模拟玻璃、水等材质的折射效果。
- 阴影检测:判断表面点是否被遮挡,从而计算阴影区域。
5.2 Whitted 光线追踪的步骤
- 生成初始光线:
- 从视点出发,穿过每个像素生成一条光线(即视线光线)。
- 检测光线与场景中的物体是否相交:
- 如果光线没有与任何物体相交,返回背景色。
- 如果光线与物体相交,计算交点位置和相关属性(如法线、材质等)。
- 计算光照:
- 根据光线的交点计算直接光照(漫反射和镜面高光)。
- 检测交点是否在阴影中(通过射向光源的光线判断)。
- 递归计算反射和折射光:
- 如果物体具有反射特性,计算反射光线的颜色。
- 如果物体具有折射特性,计算折射光线的颜色。
- 通过递归调用,计算反射光和折射光的贡献,直到达到最大递归深度。
- 颜色混合:
- 根据材质属性和菲涅耳系数,将直接光照、反射光和折射光的贡献混合,生成最终颜色。
5.3 步骤解析
1. 直接光照
计算光源对交点的直接光照,包括:
- 漫反射:根据 Lambert 定律,漫反射强度与光线入射角的余弦值成正比。
- 镜面反射:使用 Phong 模型计算高光区域的强度。
2. 反射光线
- 使用反射公式计算反射光线方向:
R = I − 2 ( I ⋅ N ) N \mathbf{R} = \mathbf{I} - 2 (\mathbf{I} \cdot \mathbf{N}) \mathbf{N} R=I−2(I⋅N)N
- 调用递归函数计算反射光线的颜色。
3. 折射光线
- 使用 Snell’s Law (斯涅尔定律)计算折射方向:
η sin θ i = sin θ t \eta \sin \theta_i = \sin \theta_t ηsinθi=sinθt
- 调用递归函数计算折射光线的颜色。
- 使用菲涅耳系数混合反射光和折射光。
4. 阴影检测
从交点射向光源,如果光线被其他物体阻挡,则交点处于阴影中。
5. 递归深度控制
- 通过限制递归深度,防止光线追踪进入无限递归。
- 递归深度通常为 5-10,以平衡渲染效果和性能。
5.4 代码实现
// 实现 Whitted 光线追踪算法
// 参数:
// orig: 光线的起点(如相机位置或反射光线的起点)
// dir: 光线的方向(单位向量,表示光线传播的方向)
// scene: 场景对象,包含物体、光源和背景信息
// depth: 当前递归深度,用于控制光线追踪的递归次数
Vector3f castRay(const Vector3f &orig, const Vector3f &dir, const Scene& scene,int depth)
{
// 如果递归深度超过最大值,返回黑色,表示没有进一步的光线传播
if (depth > scene.maxDepth) {
return Vector3f(0.0,0.0,0.0);
}
Vector3f hitColor = scene.backgroundColor; // 初始化颜色为背景色
// 检测光线与场景中的物体是否相交
if (auto payload = trace(orig, dir, scene.get_objects()); payload)
{
// 计算光线与物体的交点位置
Vector3f hitPoint = orig + dir * payload->tNear;
Vector3f N; // 法线,用于后续的光照计算
Vector2f st; // 纹理坐标
// 获取物体表面属性,包括法线和纹理坐标
payload->hit_obj->getSurfaceProperties(hitPoint, dir, payload->index, payload->uv, N, st);
// 根据物体的材质类型处理不同的光线行为
switch (payload->hit_obj->materialType) {
// 如果物体同时具有反射和折射特性
case REFLECTION_AND_REFRACTION:
{
// 计算反射方向
Vector3f reflectionDirection = normalize(reflect(dir, N));
// 计算折射方向
Vector3f refractionDirection = normalize(refract(dir, N, payload->hit_obj->ior));
// 调整反射光线的起点,避免数值误差导致的“自相交”
Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
hitPoint - N * scene.epsilon :
hitPoint + N * scene.epsilon;
// 调整折射光线的起点
Vector3f refractionRayOrig = (dotProduct(refractionDirection, N) < 0) ?
hitPoint - N * scene.epsilon :
hitPoint + N * scene.epsilon;
// 递归计算反射光线的颜色
Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1);
// 递归计算折射光线的颜色
Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth + 1);
// 使用菲涅耳系数混合反射和折射颜色
float kr = fresnel(dir, N, payload->hit_obj->ior);
hitColor = reflectionColor * kr + refractionColor * (1 - kr);
break;
}
// 如果物体只有反射特性
case REFLECTION:
{
// 计算菲涅耳系数
float kr = fresnel(dir, N, payload->hit_obj->ior);
// 计算反射方向
Vector3f reflectionDirection = reflect(dir, N);
// 调整反射光线的起点
Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
hitPoint + N * scene.epsilon :
hitPoint - N * scene.epsilon;
// 递归计算反射光线的颜色
hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1) * kr;
break;
}
// 默认情况,处理漫反射和镜面高光
default:
{
Vector3f lightAmt = 0, specularColor = 0; // 漫反射和高光颜色
// 调整阴影光线的起点,避免自相交
Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?
hitPoint + N * scene.epsilon :
hitPoint - N * scene.epsilon;
// 遍历场景中的每个光源
for (auto& light : scene.get_lights()) {
Vector3f lightDir = light->position - hitPoint; // 从交点指向光源的方向
float lightDistance2 = dotProduct(lightDir, lightDir); // 光源距离的平方
lightDir = normalize(lightDir); // 归一化光源方向
// 计算光线与法线的点积,用于漫反射计算
float LdotN = std::max(0.f, dotProduct(lightDir, N));
// 检测交点是否被其他物体遮挡(阴影检测)
auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
bool inShadow = shadow_res && (shadow_res->tNear * shadow_res->tNear < lightDistance2);
// 如果没有被遮挡,累加漫反射光强
lightAmt += inShadow ? 0 : light->intensity * LdotN;
// 计算镜面反射的高光部分
Vector3f reflectionDirection = reflect(-lightDir, N);
specularColor += powf(std::max(0.f, -dotProduct(reflectionDirection, dir)),
payload->hit_obj->specularExponent) * light->intensity;
}
// 漫反射和高光颜色的最终组合
hitColor = lightAmt * payload->hit_obj->evalDiffuseColor(st) * payload->hit_obj->Kd + specularColor * payload->hit_obj->Ks;
break;
}
}
}
// 返回最终计算的颜色值
return hitColor;
}
5.5 应用场景
1. 透明材质
模拟玻璃、水等材质的透明效果,通过递归计算折射光线的颜色。
2. 金属材质
模拟镜面反射的效果,通过递归计算反射光线的颜色。
3. 光影效果
通过阴影检测模拟光线被遮挡的场景,例如投影和柔和阴影。
6.6 优缺点
优点:
- 能够生成高质量的真实感图像。
- 支持反射、折射、阴影等光学现象的模拟。
缺点:
- 计算量大:递归光线追踪需要检测大量光线与物体的相交,计算量非常大。
- 性能问题:在复杂场景中,渲染时间较长。
标签:cos,mathbf,光线,Vector3f,float,图形学,dir,追踪 From: https://blog.csdn.net/llxllx6969/article/details/144695537