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

Windows平台CPU路径追踪器输出过暗,疑似缺失全局光照

路径追踪器画面过暗、缺失全局光照的问题分析

问题描述

在Windows平台开发CPU光线追踪器,场景为带两扇窗户的室内空间,仅设置一个方向光,光线反弹次数设为8次。直接光照表现正常,但输出画面过暗,疑似缺失全局光照:

我的路径追踪器输出:
我的路径追踪器输出画面

Blender Cycles输出:
Blender Cycles输出画面

核心代码分析

路径追踪主函数

RGBColor pathTracing(const Math::Ray& ray, nbUint32 depth)
{
    // Find intersection using intel's Embree
    Intersector::IntersectionInfo isectInfo;
    m_intersector->intersect(ray, isectInfo);

    // Check if an intersection occured
    if (isectInfo.object == nullptr)
    {
        return BlackRGBColor;
    }

    // Get intersection properties
    const auto isectProps = buildIntersectionProperties(ray, isectInfo, m_currentScene);

    // The scene model
    const auto& model = m_currentScene->getModel();

    // Read the material
    const DatabaseMaterialPtr material = model->getMaterialPtr_FromEntity(isectInfo.object->getMaterialId());

    // Compute direct lighning
    RGBColor directCont = BlackRGBColor;
    {
        for (const EntityIdentifier& lightId : m_currentScene->getLights())
        {
            // Generate light sample
            const auto light = Entity::EntityDatabaseSingleton::instance()->getEntity<Light::BaseLight>(lightId);
            const auto sampleToLight = light->generateSampleToLight(isectProps.P);

            const nbFloat32 NoL = glm::dot(isectProps.BsdfProps.N, sampleToLight.L);
            if (NoL <= 0.0f)
                continue;

            // Compute occlusion
            Math::Ray shadowRay(isectProps.deltaP, sampleToLight.L, sampleToLight.length);
            const nbFloat32 occlusionStrength = m_intersector->occlusion(shadowRay);

            if (occlusionStrength != 1.0f)
            {
                // Build the bsdf sample struture
                Material::BaseMaterial::SampleBsdfData bsdfSample;
                bsdfSample.L = sampleToLight.L;
                bsdfSample.V = isectProps.V;
                bsdfSample.NoL = NoL;
                bsdfSample.geoProps = &isectProps.BsdfProps;

                // Sample the brdf to get the scale
                auto sampledBrdf = material.sampleBsdf(bsdfSample);

                // Multiply by light radiance
                sampledBrdf *= light->getColor();

                // Multiply by visibility
                sampledBrdf *= (1.0f - occlusionStrength);

                // Finally add light contribution
                directCont += sampledBrdf;
            }
        }
    }


    // Compute indirect lighning
    RGBColor indirectCont;
    if (++depth <= m_settings.m_maxDepth)
    {
        BaseMaterial::SpawnRayResult newRayData;
        newRayData = material->spawnRay(/* some parameters*/);

        // Build the bsdf sample struture
        Material::BaseMaterial::SampleBsdfData bsdfSample;
        bsdfSample.L = newRayData.ray.getDirection();
        bsdfSample.V = isectProps.V;
        bsdfSample.NoL = glm::dot(newRayData.ray.getDirection(), isectProps.BsdfProps.N);
        bsdfSample.geoProps = &isectProps.BsdfProps;

        // Indirect raytracing
        const Spectrum sampledBrdf = material->sampleBsdf(bsdfSample);
        const Spectrum sampledRadiance = sampledBrdf * pathTracing(newRayData.ray, depth);//recursion

        static const float normalizationFactor = 2.0f * Math::Pi.getValue();
        indirectCont = sampledRadiance * normalizationFactor;
    }

    return directCont + indirectCont;
}

SampleBsdf方法

RGBColor BaseMaterial::sampleBsdf(const SampleBsdfData& data) const
{
    DistributionFunction::SampleInput distSampleInput;
    distSampleInput.L = data.L;
    distSampleInput.V = data.V;
    distSampleInput.N = data.geoProps->N;
    distSampleInput.T = data.geoProps->T;
    distSampleInput.B = data.geoProps->B;
    distSampleInput.H = glm::normalize(data.L + data.V);

    distSampleInput.HoN = std::abs(glm::dot(data.geoProps->N, distSampleInput.H));
    distSampleInput.NoV = std::abs(glm::dot(data.geoProps->N, data.V));
    distSampleInput.NoL = data.NoL;


    return sampleBrdf(distSampleInput) * distSampleInput.NoL;
}

SampleBrdf方法

inline nbFloat32 lambert(const SampleInput& input)
{
    return Math::InvPi.getValue();
}

nbFloat32 blinnPhong(const SampleInput& input, nbFloat32 shininess)
{
    const nbFloat32 HoN = std::min(input.HoN, 0.999f);
    nbFloat32 normalizationFactor = (shininess + 8.0f) * Math::InvPi8.getValue();
    normalizationFactor = glm::clamp(normalizationFactor, 0.0f, 1.0f);

    return std::pow(HoN, shininess) * normalizationFactor;
}

#define NO_FRESNEL_VALUE 0.4f

nbFloat32 DefaultDielectric::sampleFresnel(nbFloat32 HoN) const
{
    if (m_fresnelEnabled)
    {
        // Schlick approximation.
        return glm::clamp(m_fresnel0 + (1.0f - m_fresnel0) * pow(1.0f - HoN, 5.0f), 0.0f, 1.0f);
    }

    return NO_FRESNEL_VALUE;
}

RGBColor DefaultDielectric::sampleBrdf(DistributionFunction::SampleInput& distSampleInput) const
{
    nbFloat32 fresnelFactor = sampleFresnel(distSampleInput.HoN);

    // Diffuse
    nbFloat32 diffuseFactor = lambert(distSampleInput) * (1.0f - fresnelFactor);
    auto diffuseLightning = m_diffuse * diffuseFactor;

    // Specular
    nbFloat32 specularFactor = blinnPhong(distSampleInput, m_glossiness) * fresnelFactor;
    auto specLightning = m_specular * specularFactor;

    return m_opacity * (diffuseLightning + specLightning);
}

问题根源及修复方案

1. 间接光照变量未初始化

RGBColor indirectCont;未初始化为BlackRGBColor,当光线深度超过最大值时,变量会携带垃圾值,导致画面异常。
修复:初始化时改为RGBColor indirectCont = BlackRGBColor;,或在深度不满足条件的分支显式赋值。

2. 未过滤背面的间接光线

采样的间接光线方向如果与法线夹角为负(光线射向物体内部),此时NoL <= 0,应直接舍弃该采样,避免引入负光照贡献。
修复:在计算间接光照前添加判断:

bsdfSample.NoL = glm::dot(newRayData.ray.getDirection(), isectProps.BsdfProps.N);
if (bsdfSample.NoL <= 0.0f)
{
    indirectCont = BlackRGBColor;
    return directCont + indirectCont;
}

3. 路径追踪权重计算错误(核心问题)

路径追踪中,递归的光照贡献公式应为(BSDF值 / 采样PDF) * 递归返回的辐射度,原代码用固定的2π做归一化,完全不符合物理规律,这是全局光照缺失的主因。
修复:

  • 给SpawnRayResult添加nbFloat32 pdf字段,存储采样对应的概率密度值
  • 修改间接光照计算逻辑:
if (newRayData.pdf <= 0.0f)
{
    indirectCont = BlackRGBColor;
    return directCont + indirectCont;
}
const Spectrum sampledBrdf = material->sampleBsdf(bsdfSample);
const Spectrum sampledRadiance = (sampledBrdf / newRayData.pdf) * pathTracing(newRayData.ray, depth);
indirectCont = sampledRadiance;

注:如果spawnRay采用余弦加权半球分布(Lambert材质标准采样),其PDF为NoL / π,此时sampledBrdf / pdf的结果会自动抵消冗余系数,符合物理渲染逻辑。

4. 方向光阴影射线长度不合理

方向光是平行光,理论上无终点,原代码中sampleToLight.length如果是有限值,会导致阴影射线提前终止,无法正确判断远处遮挡。
修复:将方向光的阴影射线长度设为极大值(如1e6),确保覆盖场景内所有物体。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 00:31:02