第-2-章-光线追踪核心-几何交点与可见性计算
Course: I wrote a Ray Tracer from scratch… in a Year
🎮 第 2 章:光线追踪核心——几何交点与可见性计算
🏛 适用环境:自研渲染器 / C++17 / CPU 路径追踪器 🎯 目标:掌握光线与球体、平面、三角形等几何体的相交计算,理解“可见性”的核心判断逻辑。
🧭 引擎定位
本章属于光线追踪渲染器最底层的“几何求交模块”。 无论你使用的是 CPU、Vulkan、OptiX、或 SDL 软件渲染, 最终你都需要解决一个问题:
光线是否击中了物体?击中哪里?
🏷 关键词速记
Ray–Object Intersection|光线相交 Hit Record|命中记录 Discriminant|判别式 Normal|法线 Visibility|可见性 t_min / t_max|交点范围约束
🎮 30 秒玩法摘要
光线追踪的本质,就是“解方程”。 当光线射入世界时,我们只需问: 这条直线与哪些几何曲面相交?哪个相交点离我最近?
📚 核心原理讲解
🔹 1. 光线方程回顾
其中:
- [\mathbf{O}]:光线起点(相机位置)
- [\mathbf{D}]:方向单位向量
- [t]:光线行进距离,[t>0]
🔹 2. 球体相交方程
球体定义:
将光线方程代入:
展开为二次方程:
设:
- [a = \mathbf{D}\cdot\mathbf{D} = 1]
- [b = 2\mathbf{D}\cdot(\mathbf{O}-\mathbf{C})]
- [c = (\mathbf{O}-\mathbf{C})^2 - r^2]
判别式:
当 [\Delta < 0]:无交点 当 [\Delta = 0]:切线相交 当 [\Delta > 0]:两次相交,取较小正根
📦 C++ 实现:Sphere.hit()
bool Sphere::hit(const Ray& r, float t_min, float t_max, HitRecord& rec) const {
Vec3 oc = r.origin - center;
float a = dot(r.dir, r.dir);
float b = dot(oc, r.dir);
float c = dot(oc, oc) - radius * radius;
float discriminant = b*b - a*c;
if (discriminant < 0) return false;
float sqrtD = sqrt(discriminant);
float root = (-b - sqrtD) / a;
if (root < t_min || root > t_max) {
root = (-b + sqrtD) / a;
if (root < t_min || root > t_max)
return false;
}
rec.t = root;
rec.p = r.at(root);
rec.normal = (rec.p - center) / radius;
return true;
}🔹 3. 平面相交
平面方程:
光线代入:
解得:
若分母 ≈ 0,则光线与平面平行。
📦 伪代码
bool Plane::hit(const Ray& r, float t_min, float t_max, HitRecord& rec) {
float denom = dot(normal, r.dir);
if (fabs(denom) < 1e-6) return false;
float t = dot(point - r.origin, normal) / denom;
if (t < t_min || t > t_max) return false;
rec.t = t;
rec.p = r.at(t);
rec.normal = normal;
return true;
}🔹 4. 三角形相交(Möller–Trumbore 算法)
最常用算法:
📦 代码片段
bool Triangle::hit(const Ray& r, float t_min, float t_max, HitRecord& rec) {
const float EPS = 1e-8;
Vec3 e1 = v1 - v0;
Vec3 e2 = v2 - v0;
Vec3 h = cross(r.dir, e2);
float a = dot(e1, h);
if (fabs(a) < EPS) return false; // 平行
float f = 1.0f / a;
Vec3 s = r.origin - v0;
float u = f * dot(s, h);
if (u < 0.0 || u > 1.0) return false;
Vec3 q = cross(s, e1);
float v = f * dot(r.dir, q);
if (v < 0.0 || u + v > 1.0) return false;
float t = f * dot(e2, q);
if (t < t_min || t > t_max) return false;
rec.t = t;
rec.p = r.at(t);
rec.normal = normalize(cross(e1, e2));
return true;
}🔹 5. 可见性判断(Visibility)
若一条光线从相机出发, 命中物体前先被遮挡,则该像素处于阴影区。
📦 阴影检测逻辑
bool isInShadow(const Vec3& point, const Vec3& lightDir) {
Ray shadowRay(point + normal * 1e-3, lightDir);
return scene.hitAny(shadowRay, 0.001f, inf);
}🧪 实验与验证步骤
1️⃣ 验证球体交点可视化
- 用渐变颜色输出命中法线。
- 检查是否出现两次交点(前后球面)。
2️⃣ 验证平面反射
- 添加地板平面,设置法线朝上。
- 检查球体在地面上的投影与反射是否匹配。
3️⃣ 阴影检测
- 若光线被物体遮挡,则该像素亮度减半。
🪄 创作与实现建议
- 使用统一的
HitRecord结构体记录所有物体的命中信息(t、法线、材质指针)。 - 在
Scene层统一管理所有几何体,遍历求最近交点。 - 光线可见性函数
hitAny()是优化的关键,可以提前中断。 - 加入
t_min与t_max限制避免“自交”问题(反射光打到自己)。
🧠 记忆口诀
“射线方程解交点,判别平方决命运; 平面相交除点积,三角重心定阴影。”
✅ 实践题
Q1:为什么在相交测试中要加上 t_min / t_max? 🅰️ 为了限制光线范围,避免击中自身或穿透。
Q2:球体相交公式中的判别式 Δ 有什么几何意义? 🅰️ 表示光线与球体的相对位置关系(负:错过,零:相切,正:相交)。
Q3:在平面相交时分母接近 0 意味着什么? 🅰️ 光线与平面平行,不存在交点。
是否继续生成第 3 章 👉 《相机系统与投影模型》(解释视角、FOV、成像原理、光线生成函数、抗锯齿与景深实现)?