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

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

问题根源

  1. 反射方向计算错误:全内反射场景下,入射光线从球内射向表面,法线rec.n指向球外,此时传入reflect的directionIn是从入射点指向外部的光线方向,与法线的点积为正,导致反射方向错误,光线无法逃出球体,反复内部反射直至耗尽反射次数返回黑色。
  2. 反射次数递减逻辑隐患:--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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 13:31:02