如何改进单向路径追踪器的收敛效率?附实现代码
路径追踪器收敛性优化方案
不需要立刻切换到BDPT或MLT,通过以下单向路径追踪的优化手段,可以显著提升收敛速度、减少噪声:
1. 替换为余弦加权半球采样(核心优化)
当前代码使用均匀半球采样,这种方式对漫反射场景的方差较大——漫反射BRDF的贡献与入射方向和法线的夹角余弦成正比,大部分有效贡献集中在法线附近。改用余弦加权采样可以让采样方向更集中于高贡献区域,直接降低方差。
修改采样函数:
// 余弦加权半球采样,PDF为 cosθ/π inline Math::Vec3 cosineWeightedHemisphereSample(const float &r1, const float &r2) { float phi = Math::Pi2.getValue() * r2; float cosTheta = sqrtf(1.0f - r1); // 对应余弦加权的分布变换 float sinTheta = sqrtf(r1); float x = sinTheta * cosf(phi); float z = sinTheta * sinf(phi); return Math::Vec3(x, cosTheta, z); }
调整间接光照计算:
余弦加权采样的PDF刚好抵消漫反射BRDF的相关项,间接光照部分可简化,去掉多余的归一化计算:
// 替换原采样逻辑 const Math::Vec3 sample = cosineWeightedHemisphereSample(randCosLatitude, randomAzimut); const Math::Vec3 rotatedSample = Math::Vec3( sample.x * axisB.x + sample.y * axis.x + sample.z * axisT.x, sample.x * axisB.y + sample.y * axis.y + sample.z * axisT.y, sample.x * axisB.z + sample.y * axis.z + sample.z * axisT.z); const Math::Ray reflectedRay(isectProps.deltaP, rotatedSample); indirectCont = gatherRadiance(reflectedRay, depth); // 原代码中以下三行可删除: // indirectCont *= Math::InvPi.getValue(); // indirectCont *= NoL; // indirectCont *= HEMISPHERICAL_NORMALIZATION;
2. 添加俄罗斯轮盘赌(减少无效计算)
当前代码固定追踪到32层深度,但随着路径深度增加,间接光照贡献会快速衰减,继续追踪大量低贡献路径是无效的。添加俄罗斯轮盘赌可在深度超过阈值后,随机终止贡献极低的路径,节省计算资源:
#define MAX_DEPTH 8 // 配合轮盘赌适当降低最大深度 #define ROULETTE_DEPTH_THRESHOLD 3 // 从第3层开始启用轮盘赌 #define SURVIVAL_PROBABILITY 0.5f // 50%概率继续追踪 if (++depth < MAX_DEPTH) { // ... 采样逻辑 ... indirectCont = gatherRadiance(reflectedRay, depth); // 启用俄罗斯轮盘赌 if (depth >= ROULETTE_DEPTH_THRESHOLD) { if (m_nbGenerator.generateUnsignedNormalized() > SURVIVAL_PROBABILITY) { indirectCont = RGBFColor(0.0f); } else { indirectCont /= SURVIVAL_PROBABILITY; // 补偿概率,保证期望值不变 } } // ... 其他逻辑 ... }
3. 优化直接光照的阴影计算
针对太阳方向光,可做以下优化:
- 预计算太阳方向的包围盒,缩小阴影ray的遍历范围(Embree支持加速结构优化)
- 启用Embree的遮挡测试批量处理(若渲染器支持并行发射阴影ray),提升阴影计算效率
4. 提升随机数生成质量
确保m_nbGenerator使用高质量随机数生成器(如PCG、XorShift128+),避免使用标准库rand()——低质量随机数会导致噪声分布不均匀,延长收敛时间。
5. 渐进式采样与图像过滤
在累积样本过程中,每新增一定数量样本(如200样本/像素),对当前图像应用一次轻微高斯过滤,或使用Mitchell-Netravali重建滤波器,虽不会改变数学收敛速度,但能让图像视觉上更快达到“干净”状态,提升体验。
是否需要切换到BDPT或MLT?
如果场景只有漫反射表面,上述优化后的单向路径追踪完全可以满足需求,收敛速度会比原实现提升数倍。只有当场景中出现镜面反射、焦散、复杂间接光照路径时,BDPT或MLT这类双向/马尔可夫链积分器才会体现明显优势。当前场景是单一方向光+漫反射,优先优化单向路径追踪即可。
原实现代码(供参考)
// The intersection properties. struct IntersectionProperties { Math::Vec3 P; Math::Vec3 deltaP; Math::Vec3 N; }; //@See: https://www.scratchapixel.com/lessons/3d-basic-rendering/global-illumination-path-tracing/global-illumination-path-tracing-practical-implementation.html inline Math::Vec3 uniformSphericalSample(const float &r1, const float &r2) { float sinTheta = sqrtf(1 - r1 * r1); float phi = Math::Pi2.getValue() * r2; float x = sinTheta * cosf(phi); float z = sinTheta * sinf(phi); return Math::Vec3(x, r1, z); } //@See : https://www.scratchapixel.com/lessons/3d-basic-rendering/global-illumination-path-tracing/global-illumination-path-tracing-practical-implementation.html inline void createCoordinateSystem(const Math::Vec3 &N, Math::Vec3 &Nt, Math::Vec3 &Nb) { if (std::fabs(N.x) > std::fabs(N.y)) Nt = Math::Vec3(N.z, 0, -N.x) / sqrtf(N.x * N.x + N.z * N.z); else Nt = Math::Vec3(0, -N.z, N.y) / sqrtf(N.y * N.y + N.z * N.z); Nb = glm::cross(N, Nt); } inline IntersectionProperties buildIntersectionProperties(const Math::Ray& ray, const Intersector::IntersectionInfo& info) { // Compute the intersection properties const auto mesh = info.object; const auto P = ray.getPoint(info.meshIntersectData.t); #define PRIMITIVE_NB_VTX 3 const unsigned int triStartIdx = info.meshIntersectData.primId * PRIMITIVE_NB_VTX; const auto v1 = mesh->buildTransformedVertexFromIndex(triStartIdx); const auto v2 = mesh->buildTransformedVertexFromIndex(triStartIdx + 1); const auto v3 = mesh->buildTransformedVertexFromIndex(triStartIdx + 2); const float invArea = mesh->getPrecomputedInvArea()[info.meshIntersectData.primId]; const Math::Vec3 coefs = Math::interpolate(v1.position, v2.position, v3.position, P, invArea); // Eye vector const Math::Vec3 V = -ray.getDirection(); IntersectionProperties props; // The position. props.P = P; // The normal. props.N = glm::normalize( (v1.normal * coefs.x) + (v2.normal * coefs.y) + (v3.normal * coefs.z) ); if (glm::dot(props.N, V) < 0.0f) props.N *= -1; // Offseted position to avoid self intersections. props.deltaP = P + (props.N * 0.01f); return props; } // THE MAIN FUNCTION ---------------------------------------------- Integrator::Spectrum OfflineRenderer::gatherRadiance(const Math::Ray& ray, unsigned int depth) { Intersector::IntersectionInfo intersectInfo; m_intersector->intersect(ray, intersectInfo); if (intersectInfo.object == nullptr) return RGBFColor(0.0f, 0.0f, 0.0f); // Get intersection properties const IntersectionProperties isectProps = buildIntersectionProperties(ray, intersectInfo); // Compute sun direct lighting contribution. RGBFColor directCont = RGBFColor(0.0f, 0.0f, 0.0f); { // Sun properties. static const glm::vec3 sunDirection = glm::normalize(glm::vec3(0.980282f, -0.190809, -0.0513746)); static const RGBFColor sunColor = RGBFColor(1.0f, 1.0f, 1.0f); static const float sunIntensity = 11.0f; const float NoL = glm::dot(isectProps.N, -sunDirection); if (NoL > 0.0f) { // Cast shadow ray. Math::Ray shadowRay(isectProps.deltaP, -sunDirection, 99999999.9f); const float occlusionStrength = m_intersector->occlusion(shadowRay); // Light radiance. directCont = sunColor * sunIntensity; // Cosine term. directCont *= NoL; // Diffuse brdf. directCont *= Math::InvPi.getValue(); // Multiply by visibility. directCont *= (1.0f - occlusionStrength); } } RGBFColor indirectCont = RGBFColor(0.0f, 0.0f, 0.0f); #define MAX_DEPTH 32 if (++depth < MAX_DEPTH) { // Generate random sample. //@ See : https://www.scratchapixel.com/lessons/3d-basic-rendering/global-illumination-path-tracing/global-illumination-path-tracing-practical-implementation.html const Math::Vec3 axis = isectProps.N; Math::Vec3 axisT, axisB; createCoordinateSystem(axis, axisT, axisB); const float randomAzimut = m_nbGenerator.generateUnsignedNormalized(); const float randCosLatitude = m_nbGenerator.generateUnsignedNormalized(); const Math::Vec3 sample = uniformSphericalSample(randCosLatitude, randomAzimut); const Math::Vec3 rotatedSample = Math::Vec3( sample.x * axisB.x + sample.y * axis.x + sample.z * axisT.x, sample.x * axisB.y + sample.y * axis.y + sample.z * axisT.y, sample.x * axisB.z + sample.y * axis.z + sample.z * axisT.z); // // Compute reflected ray. const Math::Ray reflectedRay(Math::Ray(isectProps.deltaP, rotatedSample)); // Indirect lighting. indirectCont = gatherRadiance(reflectedRay, depth); // Diffuse brdf. indirectCont *= Math::InvPi.getValue(); // Cosine term. const float NoL = glm::dot(isectProps.N, reflectedRay.getDirection()); indirectCont *= NoL; #define HEMISPHERICAL_NORMALIZATION (2.0f * Math::Pi.getValue()) indirectCont *= HEMISPHERICAL_NORMALIZATION; } return directCont + indirectCont; } // ----------------------------------------------------------------
内容的提问来源于stack exchange,提问作者TheChamp
相关产品推荐
相关产品推荐

