第-2-章-光线追踪核心-几何交点与可见性计算

Course: I wrote a Ray Tracer from scratch… in a Year


🏛 适用环境:自研渲染器 / C++17 / CPU 路径追踪器 🎯 目标:掌握光线与球体、平面、三角形等几何体的相交计算,理解“可见性”的核心判断逻辑。


本章属于光线追踪渲染器最底层的“几何求交模块”。 无论你使用的是 CPU、Vulkan、OptiX、或 SDL 软件渲染, 最终你都需要解决一个问题:

光线是否击中了物体?击中哪里?


Ray–Object Intersection|光线相交 Hit Record|命中记录 Discriminant|判别式 Normal|法线 Visibility|可见性 t_min / t_max|交点范围约束


光线追踪的本质,就是“解方程”。 当光线射入世界时,我们只需问: 这条直线与哪些几何曲面相交?哪个相交点离我最近?


P(t)=O+tD \mathbf{P}(t) = \mathbf{O} + t\mathbf{D}

其中:

  • [\mathbf{O}]:光线起点(相机位置)
  • [\mathbf{D}]:方向单位向量
  • [t]:光线行进距离,[t>0]

球体定义:

(PC)2=r2 (\mathbf{P} - \mathbf{C})^2 = r^2

将光线方程代入:

(O+tDC)2=r2 (\mathbf{O} + t\mathbf{D} - \mathbf{C})^2 = r^2

展开为二次方程:

t2(DD)+2t(D(OC))+(OC)2r2=0 t^2 (\mathbf{D}\cdot\mathbf{D}) + 2t (\mathbf{D}\cdot(\mathbf{O}-\mathbf{C})) + (\mathbf{O}-\mathbf{C})^2 - r^2 = 0

设:

  • [a = \mathbf{D}\cdot\mathbf{D} = 1]
  • [b = 2\mathbf{D}\cdot(\mathbf{O}-\mathbf{C})]
  • [c = (\mathbf{O}-\mathbf{C})^2 - r^2]

判别式:

Δ=b24ac \Delta = b^2 - 4ac

当 [\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;
}

平面方程:

(PP0)N=0 (\mathbf{P} - \mathbf{P_0}) \cdot \mathbf{N} = 0

光线代入:

(O+tDP0)N=0 (\mathbf{O} + t\mathbf{D} - \mathbf{P_0}) \cdot \mathbf{N} = 0

解得:

t=(P0O)NDN t = \frac{(\mathbf{P_0} - \mathbf{O}) \cdot \mathbf{N}}{\mathbf{D}\cdot\mathbf{N}}

若分母 ≈ 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;
}

最常用算法:

t=(V1O)NDN t = \frac{(V_1 - O) \cdot N}{D \cdot N}

📦 代码片段

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;
}

若一条光线从相机出发, 命中物体前先被遮挡,则该像素处于阴影区。

📦 阴影检测逻辑

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_mint_max 限制避免“自交”问题(反射光打到自己)。

“射线方程解交点,判别平方决命运; 平面相交除点积,三角重心定阴影。”


Q1:为什么在相交测试中要加上 t_min / t_max? 🅰️ 为了限制光线范围,避免击中自身或穿透。

Q2:球体相交公式中的判别式 Δ 有什么几何意义? 🅰️ 表示光线与球体的相对位置关系(负:错过,零:相切,正:相交)。

Q3:在平面相交时分母接近 0 意味着什么? 🅰️ 光线与平面平行,不存在交点。


是否继续生成第 3 章 👉 《相机系统与投影模型》(解释视角、FOV、成像原理、光线生成函数、抗锯齿与景深实现)?