第-4-章-材质与反射模型-漫反射-金属与玻璃
Course: I wrote a Ray Tracer from scratch… in a Year
🎮 第 4 章:材质与反射模型 —— 漫反射、金属与玻璃
🏛 适用环境:CPU / GPU 光线追踪器
🎯 目标:理解材质的物理意义、BRDF 模型、反射与折射的数学公式,并实现三类核心材质:漫反射 (Lambertian)、金属 (Metal)、玻璃 (Dielectric)。
🧭 引擎定位
本章属于“光线反弹模块”。
当光线第一次击中物体后,我们不再结束,而是根据物体材质决定光线接下来的“命运”:
吸收、反射,还是折射。
🏷 关键词速记
BRDF|双向反射分布函数
Diffuse|漫反射
Specular|镜面反射
Metal|金属反射
Dielectric|透明介质
Snell’s Law|斯涅尔定律
Fresnel|菲涅尔反射
🎮 30 秒玩法摘要
光线打在不同材质上,就像声音打在不同表面:
有的吸收(漫反射),有的反弹(金属),有的穿透(玻璃)。
📚 核心原理讲解
🔹 1. 材质的数学模型
在光线追踪中,每种材质定义一个函数:
作用:决定入射光线 [r_{in}] 如何产生散射光线 [r_{out}],以及颜色衰减 [attenuation]。
不同材质只需实现不同的散射规则即可。
🔹 2. 漫反射(Diffuse, Lambertian)
物理意义:光线被粗糙表面打乱方向,平均反射。
反射方向随机化:
\[ \mathbf{scatter} = \mathbf{N} + \text{random_unit_vector}() \]📦 C++ 实现
class Lambertian : public Material {
public:
Vec3 albedo; // 反射率
Lambertian(const Vec3& color) : albedo(color) {}
virtual bool scatter(const Ray& inRay, const HitRecord& rec,
Vec3& attenuation, Ray& scattered) const override {
Vec3 scatterDir = rec.normal + randomUnitVector();
if (scatterDir.nearZero()) scatterDir = rec.normal;
scattered = Ray(rec.p, scatterDir);
attenuation = albedo;
return true;
}
};效果:柔和漫反射表面,适合岩石、地面、皮肤等。
🔹 3. 金属反射(Metal)
物理意义:光线镜面反射,角度等于入射角。
反射公式:
📦 C++ 实现
class Metal : public Material {
public:
Vec3 albedo;
float fuzz; // 模糊程度
Metal(const Vec3& color, float f) : albedo(color), fuzz(f < 1 ? f : 1) {}
virtual bool scatter(const Ray& inRay, const HitRecord& rec,
Vec3& attenuation, Ray& scattered) const override {
Vec3 reflected = reflect(normalize(inRay.dir), rec.normal);
scattered = Ray(rec.p, reflected + fuzz * randomInUnitSphere());
attenuation = albedo;
return dot(scattered.dir, rec.normal) > 0;
}
};fuzz决定金属表面粗糙度(0 = 镜面,1 = 模糊)。- 若反射方向进入物体内部,则丢弃该光线。
🔹 4. 透明介质(玻璃, Dielectric)
物理意义:光线通过表面时发生折射。
📐 Snell’s Law(斯涅尔定律)
折射方向:
其中 [\eta = n_i / n_t]。
⚡ 菲涅尔反射(Fresnel Approximation)
概率上决定是"反射"还是"折射":
📦 C++ 实现
class Dielectric : public Material {
public:
float ref_idx; // 折射率
Dielectric(float ri) : ref_idx(ri) {}
virtual bool scatter(const Ray& inRay, const HitRecord& rec,
Vec3& attenuation, Ray& scattered) const override {
attenuation = Vec3(1.0, 1.0, 1.0); // 玻璃不吸收光
float refractionRatio = rec.frontFace ? (1.0 / ref_idx) : ref_idx;
Vec3 unitDir = normalize(inRay.dir);
float cosTheta = fmin(dot(-unitDir, rec.normal), 1.0);
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
bool cannotRefract = refractionRatio * sinTheta > 1.0;
Vec3 direction;
if (cannotRefract || reflectance(cosTheta, refractionRatio) > randomFloat())
direction = reflect(unitDir, rec.normal);
else
direction = refract(unitDir, rec.normal, refractionRatio);
scattered = Ray(rec.p, direction);
return true;
}
private:
static float reflectance(float cosine, float ref_idx) {
float r0 = (1 - ref_idx) / (1 + ref_idx);
r0 = r0 * r0;
return r0 + (1 - r0) * pow((1 - cosine), 5);
}
};结果:玻璃表面会同时产生反射与折射——高光边缘闪亮,内部折射真实。
🔹 5. 合成世界:多材质场景
渲染循环中为每个物体指定不同材质:
world.add(make_shared<Sphere>(Vec3(0,-100.5,-1), 100, make_shared<Lambertian>(Vec3(0.8,0.8,0.0))));
world.add(make_shared<Sphere>(Vec3(0,0,-1), 0.5, make_shared<Dielectric>(1.5)));
world.add(make_shared<Sphere>(Vec3(-1,0,-1), 0.5, make_shared<Metal>(Vec3(0.8,0.8,0.8), 0.0)));
world.add(make_shared<Sphere>(Vec3(1,0,-1), 0.5, make_shared<Lambertian>(Vec3(0.7,0.3,0.3))));效果:
- 左侧金属球反光
- 中间玻璃球折射
- 右侧漫反射球柔光
- 下方地面黄棕色漫反射
🧪 实验与验证步骤
1️⃣ 基础验证
- 渲染三个球(漫反射、金属、玻璃)。
2️⃣ 模糊参数调试
- 观察
fuzz从 0 → 1 的变化。
3️⃣ 玻璃折射验证
- 设置
ref_idx = 1.5,查看内部反射。
4️⃣ 复合反射
- 多层球(内层玻璃、外层金属),验证菲涅尔混合效果。
🪄 创作与实现建议
- 将每种材质抽象为
Material基类接口,后续可扩展 Plastic、Subsurface 等类型。 - 在调试时输出法线与散射方向向量,用颜色区分。
- 若递归深度过大,设定最大反弹次数(如 50)。
- 玻璃折射计算容易出现“NaN”错误,注意向量归一化。
🧠 记忆口诀
“漫反射,乱中柔;金属光,镜中流;玻璃透,折中忧;菲涅尔,界上秀。”
✅ 实践题
Q1:金属反射公式中的 fuzz 有什么作用?
🅰️ 控制反射方向扰动,使金属表面从镜面变为拉丝或模糊反光。
Q2:菲涅尔效应描述了什么?
🅰️ 光线在入射角大时反射增强、折射减少的现象。
Q3:漫反射模型中为什么要随机方向?
🅰️ 因为粗糙表面会让入射光以均匀分布散射出去。
是否继续生成第 5 章
👉 《加速结构:BVH 与空间划分》(讲解 AABB 包围盒、层次包围体构建、递归相交算法与性能优化)?