第-4-章-材质与反射模型-漫反射-金属与玻璃

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


🏛 适用环境:CPU / GPU 光线追踪器

🎯 目标:理解材质的物理意义、BRDF 模型、反射与折射的数学公式,并实现三类核心材质:漫反射 (Lambertian)、金属 (Metal)、玻璃 (Dielectric)。


本章属于“光线反弹模块”。

当光线第一次击中物体后,我们不再结束,而是根据物体材质决定光线接下来的“命运”:

吸收、反射,还是折射。


BRDF|双向反射分布函数

Diffuse|漫反射

Specular|镜面反射

Metal|金属反射

Dielectric|透明介质

Snell’s Law|斯涅尔定律

Fresnel|菲涅尔反射


光线打在不同材质上,就像声音打在不同表面:

有的吸收(漫反射),有的反弹(金属),有的穿透(玻璃)。


在光线追踪中,每种材质定义一个函数:

scatter(rin,hit,attenuation,rout) \text{scatter}(r_{in}, hit, \text{attenuation}, r_{out})

作用:决定入射光线 [r_{in}] 如何产生散射光线 [r_{out}],以及颜色衰减 [attenuation]。

不同材质只需实现不同的散射规则即可。


物理意义:光线被粗糙表面打乱方向,平均反射。

反射方向随机化:

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

效果:柔和漫反射表面,适合岩石、地面、皮肤等。


物理意义:光线镜面反射,角度等于入射角。

反射公式:

R=V2(VN)N \mathbf{R} = \mathbf{V} - 2(\mathbf{V}\cdot\mathbf{N})\mathbf{N}

📦 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 = 模糊)。
  • 若反射方向进入物体内部,则丢弃该光线。

物理意义:光线通过表面时发生折射。

nisinθi=ntsinθt n_i \sin\theta_i = n_t \sin\theta_t

折射方向:

rout=ηI+(ηcosθi1η2(1cos2θi))N \mathbf{r_{out}} = \eta \mathbf{I} + (\eta \cos\theta_i - \sqrt{1 - \eta^2(1 - \cos^2\theta_i)})\mathbf{N}

其中 [\eta = n_i / n_t]。

概率上决定是"反射"还是"折射":

R(cosθ)=R0+(1R0)(1cosθ)5,R0=(1n1+n)2 R(\cos\theta) = R_0 + (1 - R_0)(1 - \cos\theta)^5,\quad R_0 = \left(\frac{1-n}{1+n}\right)^2

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

结果:玻璃表面会同时产生反射与折射——高光边缘闪亮,内部折射真实。


渲染循环中为每个物体指定不同材质:

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 包围盒、层次包围体构建、递归相交算法与性能优化)?