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

如何改进单向路径追踪器的收敛效率?附实现代码

路径追踪器收敛性优化方案

不需要立刻切换到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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 22:03:09