Windows平台CPU路径追踪器输出过暗,疑似缺失全局光照
路径追踪器画面过暗、缺失全局光照的问题分析
问题描述
在Windows平台开发CPU光线追踪器,场景为带两扇窗户的室内空间,仅设置一个方向光,光线反弹次数设为8次。直接光照表现正常,但输出画面过暗,疑似缺失全局光照:
我的路径追踪器输出:
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
相关产品推荐
相关产品推荐

