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

