《Ray Tracing in One Weekend》全内反射致球体黑边问题求助
光线追踪器电介质材质全内反射黑边问题解决
问题现象
- 基于《Ray Tracing in One Weekend》实现光线追踪器,电介质材质未开启全内反射时折射效果正常;开启后球体边缘出现黑边。
- 调试确认:球体边缘发生全内反射时,光线在球体内无限反弹直至超过反射次数限制,最终返回
(0.f, 0.f, 0.f)导致黑边。
核心代码回顾
Dielectric::scatter 方法
bool Dilectric::scatter(const Ray& input, const HitRecord& rec, glm::vec3& attenuation, Ray& scatterRay) const { glm::vec3 dir; if (rec.frontFace) { // 光线从空气进入介质,仅折射 float ratio = 1.f / m_refractIndex; dir = GfxLib::refract(glm::normalize(input.direction()), rec.n, ratio); } else { // 光线从介质射向空气,需处理全内反射 float ratio = m_refractIndex; float cosTheta = std::fmin(glm::dot(-input.direction(), rec.n), 1.f); float sinTheta = std::sqrt(1.f - cosTheta * cosTheta); bool internalReflection = (ratio * sinTheta) > 1.f; if (internalReflection) { dir = GfxLib::reflect(glm::normalize(input.direction()), rec.n); } else { dir = GfxLib::refract(glm::normalize(input.direction()), rec.n, ratio); } } scatterRay.setOrigin(rec.pt); scatterRay.setDirection(dir); // m_albedo 设为 vec3(1.f) attenuation = m_albedo; return true; }
rayColor 方法
glm::vec3 RrtTest::rayColor(const Ray& ray, const HittableList& objList, int reflectDepth) { if (reflectDepth <= 0) return glm::vec3(0.f); HitRecord rec; // 用0.001替代0.f解决阴影 acne if (objList.hit(ray, 0.001f, FLT_MAX, rec) && rec.hitInd >= 0) { Ray scatterRay; glm::vec3 attenu{ 1.f }; std::shared_ptr<Matl> mat = objList.at(rec.hitInd)->getMatl(); if (!mat->scatter(ray, rec, attenu, scatterRay)) return glm::vec3(0.f); glm::vec3 retColor = rayColor(scatterRay, objList, --reflectDepth); return attenu * retColor; } else { glm::vec3 startColor{ 1.f }, endColor{ 0.5f, 0.7f, 1.f }; float t = (ray.direction().y + 1.f) * 0.5f; return GfxLib::blend(startColor, endColor, t); } }
GfxLib::reflect 方法
glm::vec3 GfxLib::reflect(const glm::vec3& directionIn, const glm::vec3& n) { float b = glm::dot(directionIn, n); glm::vec3 refDir = directionIn - 2 * b * n; return glm::normalize(refDir); }
问题根源
- 反射方向计算错误:全内反射场景下,入射光线从球内射向表面,法线
rec.n指向球外,此时传入reflect的directionIn是从入射点指向外部的光线方向,与法线的点积为正,导致反射方向错误,光线无法逃出球体,反复内部反射直至耗尽反射次数返回黑色。 - 反射次数递减逻辑隐患:
--reflectDepth是前置递减,可能导致反射次数提前消耗,虽非核心问题,但易引发逻辑混淆。
解决方案
1. 修复反射方向计算
两种可行方案:
方案A:修正reflect函数参数
在全内反射分支中,传入入射光线的反方向(指向入射点的方向)到reflect:
// 在Dilectric::scatter的internalReflection分支修改 dir = GfxLib::reflect(-glm::normalize(input.direction()), rec.n);
方案B:修正reflect函数实现
确保反射公式使用入射光线的反方向(指向入射点)计算:
glm::vec3 GfxLib::reflect(const glm::vec3& directionIn, const glm::vec3& n) { // directionIn应为指向入射点的方向(即入射光线的反方向) float dotProduct = glm::dot(directionIn, n); glm::vec3 refDir = directionIn - 2 * dotProduct * n; return refDir; // 入射方向已归一化,反射后无需再次归一化 }
2. 优化反射次数递减逻辑
使用reflectDepth - 1替代--reflectDepth,避免变量提前修改的隐患:
glm::vec3 retColor = rayColor(scatterRay, objList, reflectDepth - 1);
3. 加入Schlick近似(可选,提升真实感)
书中电介质材质使用Schlick近似模拟菲涅尔效应,可避免极端黑边,同时让材质更真实:
// 新增Schlick近似函数 float schlick(float cosine, float refIdx) { float r0 = (1 - refIdx) / (1 + refIdx); r0 = r0 * r0; return r0 + (1 - r0) * pow((1 - cosine), 5); } // 修改Dielectric::scatter的全内反射分支逻辑 else { float ratio = m_refractIndex; float cosTheta = std::fmin(glm::dot(-input.direction(), rec.n), 1.f); float sinTheta = std::sqrt(1.f - cosTheta * cosTheta); bool internalReflection = (ratio * sinTheta) > 1.f; glm::vec3 dir; if (internalReflection) { dir = GfxLib::reflect(-glm::normalize(input.direction()), rec.n); } else { // 加入菲涅尔反射概率 float reflectProb = schlick(cosTheta, m_refractIndex); if (randomFloat() < reflectProb) { dir = GfxLib::reflect(-glm::normalize(input.direction()), rec.n); } else { dir = GfxLib::refract(glm::normalize(input.direction()), rec.n, ratio); } } // ... 剩余代码不变 }
注:randomFloat()需实现返回[0,1)区间的随机数。
内容的提问来源于stack exchange,提问作者lyzirving
相关产品推荐
相关产品推荐

