You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ray Tracing In One Weekend Rust实现电介质折射异常问题

Rust光线追踪电介质材质折射异常修复

问题现象

跟随《Ray Tracing In One Weekend》教程编写Rust光线追踪器时,电介质(玻璃)材质出现异常:

  • 折射没有出现预期的上下倒像效果
  • 回退到100%折射配置时渲染球体为纯黑色,手动对折射方向z轴取反才能看到渲染内容
  • 手动对折射方向x、y分量取反后效果勉强可运行,但视觉表现错误

相关代码

Vec3折射方法实现

pub fn refract(&self, normal: &Vec3, etai_over_etat: f64) -> Vec3 {
    let dot = (-(*self)).dot(normal);
    let cos_theta = dot.min(1.0);
    let out_perp = etai_over_etat * ((*self) + cos_theta * (*normal));
    let inner =  1.0 - out_perp.length_squared();
    let abs = inner.abs();
    let r = -(abs.sqrt());
    let out_parallel = r * (*normal);
    out_perp + out_paralle
}

电介质材质scatter实现

fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option<Ray>, Color)> {
        let refraction_ratio = if hit_record.front_face {
            1.0/self.index_of_refraction
        } else {
            self.index_of_refraction
        };
        let unit_direction = ray.direction().unit_vector();
        let cos_theta = ((-unit_direction).dot(&hit_record.normal)).min(1.0);
        let sin_theta = (1.0 - cos_theta*cos_theta).sqrt();
        let cannot_refract = refraction_ratio * sin_theta > 1.0;
        let reflectance = Dielectric::reflectance(cos_theta, refraction_ratio);
        let mut rng = rand::thread_rng();
        let color = Color::new(1.0, 1.0, 1.0);
        if cannot_refract || reflectance > rng.gen::<f64>() {
            let reflected = unit_direction.reflected(&hit_record.normal);
            let scattered = Ray::new(hit_record.point, reflected);
            Some((Some(scattered), color))
        } else {
            let direction = unit_direction.refract(&hit_record.normal, refraction_ratio);
            let scattered = Ray::new(hit_record.point, direction);
            Some((Some(scattered), color))
        }
    }

问题根因

  1. 代码笔误:refract方法最后返回时变量名拼写错误,定义的平行分量变量为out_parallel,返回时写为out_paralle(末尾缺失字母l),直接导致编译失败或逻辑异常。
  2. 核心逻辑错误:法线与front_face标记不匹配
    反射计算对法线方向不敏感——将法线整体取反时,反射公式输出的反射方向完全不变,因此金属反射材质表现正常,但折射计算同时依赖法线方向和入射/出射介质的折射率比值,一旦front_face判断错误、法线方向翻转,对应的折射率比也会取反,直接导致折射方向完全错误:光线朝向相机后方传播,无法击中场景物体返回纯黑;折射分量符号错误,无法形成玻璃球凸透镜应有的倒像效果。
  3. 浮点数精度问题
    所有余弦值计算仅做了min(1.0)的上限截断,未处理浮点数累计误差导致余弦值略大于1或小于-1的情况,会触发平方根对负数求值返回NaN,直接导致像素出现无规律黑块。
  4. 潜在隐患:几何法线未单位化
    若球体等几何体的几何法线(交点减球心得到的向量)未除以半径做单位化,所有依赖法线点乘的计算都会出现量级错误。

修复方案

  1. 对齐教程实现set_face_normal逻辑,保证法线方向和front_face标记正确:
    // outward_normal为几何体表面的几何法线,必须提前单位化,方向指向几何体外侧
    fn set_face_normal(&mut self, ray: &Ray, outward_normal: &Vec3) {
        // 光线方向与外法线点乘小于0,说明光线从几何体外部入射,为正面
        self.front_face = ray.direction().dot(outward_normal) < 0.0;
        self.normal = if self.front_face {
            *outward_normal
        } else {
            -(*outward_normal)
        };
    }
    
  2. 修正refract方法,补全浮点安全处理,修正拼写错误:
    pub fn refract(&self, normal: &Vec3, etai_over_etat: f64) -> Vec3 {
        // 余弦值夹紧到[-1,1],避免浮点误差导致非法值
        let cos_theta = (-(*self)).dot(normal).clamp(-1.0, 1.0);
        let out_perp = etai_over_etat * ((*self) + cos_theta * (*normal));
        let out_parallel_sq = 1.0 - out_perp.length_squared();
        // 临界角附近兜底,避免浮点误差导致负数开平方
        let out_parallel = if out_parallel_sq > 0.0 {
            -out_parallel_sq.sqrt() * (*normal)
        } else {
            Vec3::new(0.0, 0.0, 0.0)
        };
        out_perp + out_parallel
    }
    
  3. 修正scatter函数中的余弦计算,增加浮点夹紧:
    let cos_theta = (-unit_direction).dot(&hit_record.normal).clamp(-1.0, 1.0);
    let sin_theta = (1.0 - cos_theta*cos_theta).max(0.0).sqrt(); // 兜底避免负数
    
  4. 确认所有几何体的几何法线传入set_face_normal前已经完成单位化,例如球体法线计算为(hit_point - center) / radius。

内容的提问来源于stack exchange,提问作者äymm

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 20:57:21