光线追踪算法详解:从原理到实现
光线追踪算法详解:从原理到实现,一篇就够了!
本文基于Scratchapixel经典教程,深入浅出地讲解光线追踪算法,包含完整代码实现和优化技巧。
📖 前言
光线追踪(Ray Tracing)是计算机图形学中最重要的渲染技术之一,能够生成接近真实世界的光照效果。从《玩具总动员》到《阿凡达》,从游戏RTX技术到电影特效,光线追踪无处不在。
本文将带你从零开始,深入理解光线追踪的原理,并实现一个完整的光线追踪器。
🎯 文章目录
基本原理
算法设计
核心实现
高级特性
优化技巧
完整代码
🔍 基本原理
什么是光线追踪?
光线追踪的核心思想是"反向追踪":不是从光源发射光线,而是从相机向场景中的每个像素发射光线,然后追踪这些光线与物体的交互。
相机 → 像素 → 物体 → 光源
为什么选择反向追踪?
想象一下,如果从光源发射光线:
光源发射无数光子
只有极少数光子能到达相机
计算效率极低
而反向追踪:
从相机发射光线
每条光线都有明确目标
计算效率大大提高
图像是如何创建的?
要创建一张图像,我们需要一个二维表面作为投影介质。从概念上讲,这可以想象为在一个金字塔上切一刀,金字塔的顶点位于观察者的眼睛位置,高度平行于视线方向。这个概念性的切片被称为图像平面,类似于艺术家的画布。
图1:我们可以将图像想象为通过一个金字塔的切片,金字塔的顶点位于我们眼睛的中心,高度平行于我们的视线。
透视投影
透视投影是一种将三维对象转换到二维平面的技术,在平面表面上创造深度和空间的幻觉。
图2:将立方体前面四个角投影到画布上。
光线与颜色
场景中对象的颜色和亮度主要由光线如何与对象材料相互作用决定。光线由光子组成,光子是体现电和磁特性的电磁粒子。
图3:al-Haytham的光感知模型。
🧮 算法设计
前向追踪 vs 后向追踪
前向追踪的问题
图1:光源发射的无数光子击中绿色球体,但只有一个会到达眼睛表面。
前向追踪的主要问题是效率极低,因为只有极少数的光子能到达相机。
后向追踪的解决方案
图2:后向光线追踪。我们从眼睛向球体上的一个点追踪光线,然后从该点向光源追踪光线。
后向追踪通过从相机向场景发射光线,大大提高了计算效率。
主渲染循环
void render(const Scene& scene, Image& image) {
for (int y = 0; y < image.height; y++) {
for (int x = 0; x < image.width; x++) {
// 1. 计算像素坐标
float u = float(x) / float(image.width - 1);
float v = float(y) / float(image.height - 1);
// 2. 生成光线
Ray ray = camera.getRay(u, v);
// 3. 追踪光线
Vec3 color = traceRay(ray, scene, 0);
// 4. 存储结果
image.setPixel(x, y, color);
}
}
}
光线追踪核心函数
Vec3 traceRay(const Ray& ray, const Scene& scene, int depth) {
// 递归深度限制
if (depth > MAX_DEPTH) return Vec3(0, 0, 0);
// 寻找最近的相交点
Intersection hit;
if (!scene.intersect(ray, hit)) {
return scene.background; // 没有相交,返回背景色
}
// 根据材质类型进行着色
switch (hit.material.type) {
case MaterialType::DIFFUSE:
return shadeDiffuse(hit, scene);
case MaterialType::MIRROR:
return shadeMirror(hit, ray, scene, depth);
case MaterialType::GLASS:
return shadeGlass(hit, ray, scene, depth);
}
}
光照和阴影
图1:通过像素中心投射主光线以检测对象相交。找到后,发送阴影光线以确定点的照明状态。
图2:较大的球体被较小的球体投射阴影,因为阴影光线在到达光源之前遇到较小的球体。
图3:渲染帧涉及向帧缓冲区内的每个像素发送主光线。
💻 核心实现
1. 基础数据结构
向量类
class Vec3 {
public:
float x, y, z;
Vec3(float x = 0, float y = 0, float z = 0) : x(x), y(y), z(z) {
}
Vec3 operator+(const Vec3& other) const {
return Vec3(x + other.x, y + other.y, z + other.z);
}
Vec3 operator-(const Vec3& other) const {
return Vec3(x - other.x, y - other.y, z - other.z);
}
Vec3 operator*(float scalar) const {
return Vec3(x * scalar, y * scalar, z * scalar);
}
float dot(const Vec3& other) const {
return x * other.x + y * other.y + z * other.z;
}
float length() const {
return sqrt(x*x + y*y + z*z);
}
Vec3 normalize() const {
float len = length();
return Vec3(x/len, y/len, z/len);
}
};
光线类
class Ray {
public:
Vec3 origin;
Vec3 direction;
Ray(const Vec3& origin, const Vec3& direction)
: origin(origin), direction(direction.normalize()) {
}
Vec3 pointAt(float t) const {
return origin + direction * t;
}
};
2. 相交检测
球体相交检测
bool intersectSphere(const Ray& ray, const Sphere& sphere, Intersection& hit) {
Vec3 oc = ray.origin - sphere.center;
float a = ray.direction.dot(ray.direction);
float b = 2.0f * oc.dot(ray.direction);
float c = oc.dot(oc) - sphere.radius * sphere.radius;
float discriminant = b*b - 4*a*c;
if (discriminant < 0) return false;
float sqrtDisc = sqrt(discriminant);
float t1 = (-b - sqrtDisc) / (2.0f * a);
float t2 = (-b + sqrtDisc) / (2.0f * a);
if (t1 > 0.001f) {
hit.distance = t1;
hit.point = ray.pointAt(t1);
hit.normal = (hit.point - sphere.center).normalize();
return true;
}
if (t2