具有显式光采样的渐进路径跟踪


14

我了解了BRDF部分重要性抽样背后的逻辑。但是,当涉及对光源进行显式采样时,一切都会变得混乱。例如,如果我的场景中有一个点光源,并且如果我不断地在每个帧中直接对其进行采样,那么我是否应该将其算作蒙特卡洛积分的另一个采样?也就是说,我从余弦加权分布中获取一个样本,从点光源中获取另一个样本。是总共两个样本还是一个样本?另外,我应该将直接采样的辐射光除以任何项吗?

Answers:


19

路径跟踪中有多个区域可以进行重要性采样。此外,这些领域中的每个领域都可以使用Veach和Guibas在1995年的论文中首次提出的多重重要性抽样。为了更好地解释,让我们看一下反向路径跟踪器:

void RenderPixel(uint x, uint y, UniformSampler *sampler) {
    Ray ray = m_scene->Camera->CalculateRayFromPixel(x, y, sampler);

    float3 color(0.0f);
    float3 throughput(1.0f);
    SurfaceInteraction interaction;

    // Bounce the ray around the scene
    const uint maxBounces = 15;
    for (uint bounces = 0; bounces < maxBounces; ++bounces) {
        m_scene->Intersect(ray);

        // The ray missed. Return the background color
        if (ray.GeomID == INVALID_GEOMETRY_ID) {
            color += throughput * m_scene->BackgroundColor;
            break;
        }

        // Fetch the material
        Material *material = m_scene->GetMaterial(ray.GeomID);
        // The object might be emissive. If so, it will have a corresponding light
        // Otherwise, GetLight will return nullptr
        Light *light = m_scene->GetLight(ray.GeomID);

        // If we hit a light, add the emission
        if (light != nullptr) {
            color += throughput * light->Le();
        }

        interaction.Position = ray.Origin + ray.Direction * ray.TFar;
        interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
        interaction.OutputDirection = normalize(-ray.Direction);


        // Get the new ray direction
        // Choose the direction based on the bsdf        
        material->bsdf->Sample(interaction, sampler);
        float pdf = material->bsdf->Pdf(interaction);

        // Accumulate the weight
        throughput = throughput * material->bsdf->Eval(interaction) / pdf;

        // Shoot a new ray

        // Set the origin at the intersection point
        ray.Origin = interaction.Position;

        // Reset the other ray properties
        ray.Direction = interaction.InputDirection;
        ray.TNear = 0.001f;
        ray.TFar = infinity;


        // Russian Roulette
        if (bounces > 3) {
            float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
            if (sampler->NextFloat() > p) {
                break;
            }

            throughput *= 1 / p;
        }
    }

    m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}

用英语讲:

  1. 通过场景拍摄光线
  2. 检查我们是否击中任何东西。如果不是,我们返回天空盒的颜色并中断。
  3. 检查我们是否打了灯。如果是这样,我们将发光添加到颜色累积中
  4. 选择下一条射线的新方向。我们可以统一执行此操作,也可以基于BRDF进行重要性示例
  5. 评估BRDF并进行累积。在这里,为了遵循蒙特卡洛算法,我们必须将所选方向的pdf除以。
  6. 根据我们选择的方向以及我们来自哪里来创建新的射线
  7. [可选]使用俄罗斯轮盘选择是否终止射线
  8. 转到1

使用此代码,只有在射线最终照射到光线时,我们才会获得颜色。另外,它不支持守时光源,因为它们没有面积。

为了解决这个问题,我们在每次反弹时都直接采样灯光。我们必须做一些小改动:

void RenderPixel(uint x, uint y, UniformSampler *sampler) {
    Ray ray = m_scene->Camera->CalculateRayFromPixel(x, y, sampler);

    float3 color(0.0f);
    float3 throughput(1.0f);
    SurfaceInteraction interaction;

    // Bounce the ray around the scene
    const uint maxBounces = 15;
    for (uint bounces = 0; bounces < maxBounces; ++bounces) {
        m_scene->Intersect(ray);

        // The ray missed. Return the background color
        if (ray.GeomID == INVALID_GEOMETRY_ID) {
            color += throughput * m_scene->BackgroundColor;
            break;
        }

        // Fetch the material
        Material *material = m_scene->GetMaterial(ray.GeomID);
        // The object might be emissive. If so, it will have a corresponding light
        // Otherwise, GetLight will return nullptr
        Light *light = m_scene->GetLight(ray.GeomID);

        // If this is the first bounce or if we just had a specular bounce,
        // we need to add the emmisive light
        if ((bounces == 0 || (interaction.SampledLobe & BSDFLobe::Specular) != 0) && light != nullptr) {
            color += throughput * light->Le();
        }

        interaction.Position = ray.Origin + ray.Direction * ray.TFar;
        interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
        interaction.OutputDirection = normalize(-ray.Direction);


        // Calculate the direct lighting
        color += throughput * SampleLights(sampler, interaction, material->bsdf, light);


        // Get the new ray direction
        // Choose the direction based on the bsdf        
        material->bsdf->Sample(interaction, sampler);
        float pdf = material->bsdf->Pdf(interaction);

        // Accumulate the weight
        throughput = throughput * material->bsdf->Eval(interaction) / pdf;

        // Shoot a new ray

        // Set the origin at the intersection point
        ray.Origin = interaction.Position;

        // Reset the other ray properties
        ray.Direction = interaction.InputDirection;
        ray.TNear = 0.001f;
        ray.TFar = infinity;


        // Russian Roulette
        if (bounces > 3) {
            float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
            if (sampler->NextFloat() > p) {
                break;
            }

            throughput *= 1 / p;
        }
    }

    m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}

首先,我们添加“颜色+ =吞吐量* SampleLights(...)”。我将详细介绍有关SampleLights()的信息。但是,从本质上讲,它遍历所有灯光,并返回它们对颜色的贡献,并由BSDF衰减。

很好,但是我们需要再进行一次更改以使其正确。具体来说,当我们开灯时会发生什么。在旧代码中,我们将光的发射添加到了颜色累积中。但是现在我们在每次反射时都直接采样光,因此,如果添加光的发射,我们将“两次浸入”。因此,正确的做法是……什么都不做;我们跳过累积光的发射。

但是,有两个极端的情况:

  1. 第一缕
  2. 完美的镜面反射(又称镜子)

如果第一束光线照射到光线,则应该直接看到光线的发射。因此,如果我们跳过它,即使它们周围的表面都被照亮,所有的灯都将显示为黑色。

当您击中完全镜面的表面时,您将无法直接对光线进行采样,因为输入光线只有一个输出。好吧,从技术上讲,我们可以检查输入光线是否会照亮,但是没有意义;无论如何,主要的路径跟踪循环都将这样做。因此,如果在击中镜面后立即击中灯光,则需要累积颜色。如果我们不这样做,镜子中的灯光将变黑。

现在,让我们深入研究SampleLights():

float3 SampleLights(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
    std::size_t numLights = m_scene->NumLights();

    float3 L(0.0f);
    for (uint i = 0; i < numLights; ++i) {
        Light *light = &m_scene->Lights[i];

        // Don't let a light contribute light to itself
        if (light == hitLight) {
            continue;
        }

        L = L + EstimateDirect(light, sampler, interaction, bsdf);
    }

    return L;
}

用英语讲:

  1. 遍历所有灯光
  2. 如果我们击中它就跳过光
    • 不要双沾
  3. 积累所有灯光的直接照明
  4. 返回直接照明

最后,EssentialDirect()仅评估BSDF(p,ωi,ωo)Li(p,ωi)

对于守时光源,这很简单:

float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
    // Only sample if the BRDF is non-specular 
    if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
        return float3(0.0f);
    }

    interaction.InputDirection = normalize(light->Origin - interaction.Position);
    return bsdf->Eval(interaction) * light->Li;
}

但是,如果要让灯光具有面积,则首先需要对灯光上的一个点进行采样。因此,完整的定义是:

float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
    float3 directLighting = float3(0.0f);

    // Only sample if the BRDF is non-specular 
    if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
        float pdf;
        float3 Li = light->SampleLi(sampler, m_scene, interaction, &pdf);

        // Make sure the pdf isn't zero and the radiance isn't black
        if (pdf != 0.0f && !all(Li)) {
            directLighting += bsdf->Eval(interaction) * Li / pdf;
        }
    }

    return directLighting;
}

我们可以根据需要实现light-> SampleLi;我们可以统一选择要点或重要性样本。无论哪种情况,我们都将辐射度除以选择点的pdf。再次,以满足蒙特卡洛的要求。

如果BRDF高度依赖于视图,则最好根据BRDF选择一个点,而不是灯光上的随机点。但是我们如何选择呢?基于光还是基于BRDF的样品?

为什么不兼得?输入多个重要性采样。简而言之,我们评估多次,使用不同的采样技术,然后使用基于其pdf的权重将它们平均在一起。在代码中,这是:BSDF(p,ωi,ωo)Li(p,ωi)

float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
    float3 directLighting = float3(0.0f);
    float3 f;
    float lightPdf, scatteringPdf;


    // Sample lighting with multiple importance sampling
    // Only sample if the BRDF is non-specular 
    if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
        float3 Li = light->SampleLi(sampler, m_scene, interaction, &lightPdf);

        // Make sure the pdf isn't zero and the radiance isn't black
        if (lightPdf != 0.0f && !all(Li)) {
            // Calculate the brdf value
            f = bsdf->Eval(interaction);
            scatteringPdf = bsdf->Pdf(interaction);

            if (scatteringPdf != 0.0f && !all(f)) {
                float weight = PowerHeuristic(1, lightPdf, 1, scatteringPdf);
                directLighting += f * Li * weight / lightPdf;
            }
        }
    }


    // Sample brdf with multiple importance sampling
    bsdf->Sample(interaction, sampler);
    f = bsdf->Eval(interaction);
    scatteringPdf = bsdf->Pdf(interaction);
    if (scatteringPdf != 0.0f && !all(f)) {
        lightPdf = light->PdfLi(m_scene, interaction);
        if (lightPdf == 0.0f) {
            // We didn't hit anything, so ignore the brdf sample
            return directLighting;
        }

        float weight = PowerHeuristic(1, scatteringPdf, 1, lightPdf);
        float3 Li = light->Le();
        directLighting += f * Li * weight / scatteringPdf;
    }

    return directLighting;
}

用英语讲:

  1. 首先,我们对光进行采样
    • 这将更新interact.InputDirection
    • 给我们李光
    • 以及根据光线选择该点的pdf
  2. 检查pdf是否有效且辐射度不为零
  3. 使用采样的InputDirection评估BSDF
  4. 给定样本InputDirection,计算BSDF的pdf
    • 从本质上讲,如果我们使用BSDF(而不是光)进行采样,则此采样的可能性有多大
  5. 使用light pdf和BSDF pdf计算重量
    • Veach和Guibas定义了几种不同的方法来计算重量。通过实验,他们发现2的幂启发法在大多数情况下效果最佳。有关更多详细信息,请参考本文。实现如下
  6. 重量乘以直接照明计算,然后除以pdf。(对于蒙特卡洛)并添加到直接光累积中。
  7. 然后,我们对BRDF进行采样
    • 这将更新interact.InputDirection
  8. 评估BRDF
  9. 获取基于BRDF选择此方向的pdf
  10. 给定采样的InputDirection,计算光线pdf
    • 这是以前的镜子。如果我们要采样光,这个方向的可能性有多大
  11. 如果lightPdf == 0.0f,则光线错过了光线,因此只需从灯光样本返回直接照明即可。
  12. 否则,计算重量,然后将BSDF直接照明添加到累加
  13. 最后,返回累积的直接照明

inline float PowerHeuristic(uint numf, float fPdf, uint numg, float gPdf) {
    float f = numf * fPdf;
    float g = numg * gPdf;

    return (f * f) / (f * f + g * g);
}

您可以对这些功能进行许多优化/改进,但我已对其进行了简化,以使它们更易于理解。如果您愿意,我可以分享其中的一些改进。

仅采样一盏灯

在SampleLights()中,我们遍历所有灯光,并获得它们的贡献。对于少量的灯,这很好,但是对于成百上千的灯,则变得昂贵。幸运的是,我们可以利用蒙特卡洛积分是一个巨大的平均值的事实。例:

让我们定义

h(x)=f(x)+g(x)

目前,我们通过以下方式估算:h(x)

h(x)=1Ni=1Nf(xi)+g(xi)

但是,计算和都很昂贵,所以我们这样做:f(x)g(x)

h(x)=1Ni=1Nr(ζ,x)pdf

其中是统一随机变量,而定义为:ζr(ζ,x)

r(ζ,x)={f(x),0.0ζ<0.5g(x),0.5ζ<1.0

在这种情况下,因为pdf必须集成为1,并且有2种功能可供选择。pdf=12

用英语讲:

  1. 随机选择或进行评估。g x f(x)g(x)
  2. 将结果除以(因为有两项)12
  3. 平均

随着N变大,估计将收敛到正确的解。

我们可以将相同的原理应用于光采样。而不是对每个光源进行采样,我们随机选择一个光源,然后将结果乘以光源数量(这与除以pdf分数相同):

float3 SampleOneLight(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
    std::size_t numLights = m_scene->NumLights();

    // Return black if there are no lights
    // And don't let a light contribute light to itself
    // Aka, if we hit a light
    // This is the special case where there is only 1 light
    if (numLights == 0 || numLights == 1 && hitLight != nullptr) {
        return float3(0.0f);
    }

    // Don't let a light contribute light to itself
    // Choose another one
    Light *light;
    do {
        light = m_scene->RandomOneLight(sampler);
    } while (light == hitLight);

    return numLights * EstimateDirect(light, sampler, interaction, bsdf);
}

在此代码中,所有灯光均具有平等的机会被拾取。但是,如果愿意,我们可以重视样本。例如,我们可以为较大的灯光提供更高的被拾取机会,或者使灯光更靠近命中表面。您只需要将结果除以pdf,就不再是。1numLights

多重重要性抽样“新射线”方向

当前的代码仅根据BSDF对“ New Ray”方向进行采样。如果我们还希望根据灯光的位置来重视样本怎么办?

从上面我们学到的知识中,一种方法是根据它们的pdf 来拍摄两条 “新”射线并权重。但是,这既计算量大,又难以递归实现。

为了克服这个问题,我们可以采用仅采样一盏灯所学到的相同原理。也就是说,随机选择一个样本,然后除以选择样本的pdf。

// Get the new ray direction

// Randomly (uniform) choose whether to sample based on the BSDF or the Lights
float p = sampler->NextFloat();

Light *light = m_scene->RandomLight();

if (p < 0.5f) {
    // Choose the direction based on the bsdf 
    material->bsdf->Sample(interaction, sampler);
    float bsdfPdf = material->bsdf->Pdf(interaction);

    float lightPdf = light->PdfLi(m_scene, interaction);
    float weight = PowerHeuristic(1, bsdfPdf, 1, lightPdf);

    // Accumulate the throughput
    throughput = throughput * weight * material->bsdf->Eval(interaction) / bsdfPdf;

} else {
    // Choose the direction based on a light
    float lightPdf;
    light->SampleLi(sampler, m_scene, interaction, &lightPdf);

    float bsdfPdf = material->bsdf->Pdf(interaction);
    float weight = PowerHeuristic(1, lightPdf, 1, bsdfPdf);

    // Accumulate the throughput
    throughput = throughput * weight * material->bsdf->Eval(interaction) / lightPdf;
}

话虽如此,我们真的重视根据光对“ New Ray”方向进行采样吗?对于直接照明,光能传递受表面的BSDF和光的方向的影响。但是对于间接照明,光能传递几乎完全由之前命中的表面的BSDF定义。因此,增加重要度抽样并不能给我们任何好处。

因此,通常仅使用BSDF对“新方向”进行重要性采样,而对直接照明应用多重重要性采样。


感谢您的澄清答案!我知道,如果不使用显式光采样就使用路径跟踪器,就永远不会碰到点光源。因此,我们基本上可以添加其贡献。另一方面,如果我们对面光源进行采样,则必须确保不要再用间接照明再次击中它,以避免两次倾斜
MustafaIşık17年

究竟!您是否需要澄清任何部分?还是没有足够的细节?
RichieSams

此外,多重重要性采样是否仅用于直接照明计算?也许我错过了,但没有看到另一个例子。如果我在路径跟踪器中每次反弹仅发射一束光线,似乎无法进行间接照明计算。
穆斯塔法·伊希克(MustafaIşık),

2
可以在使用重要性采样的任何地方应用多重重要性采样。多重重要性抽样的力量在于我们可以结合多种抽样技术的优势。例如,在某些情况下,轻度重要性采样将比BSDF采样更好。在其他情况下,反之亦然。MIS将结合两全其美。但是,如果BSDF采样在100%的时间内会更好,则没有理由增加MIS的复杂性。我在答案中添加了一些部分以进一步说明这一点
RichieSams

1
似乎我们将进入的辐射源分为直接和间接两部分。我们对直接部分的灯光进行了明确采样,并且在对该部分进行采样时,对灯光以及BSDF进行重要性采样是合理的。但是,对于间接部分,我们不知道哪个方向可能会给我们带来更高的辐射值,因为这是我们要解决的问题。但是,根据余弦项和BSDF,我们可以说哪个方向的贡献更大。这是我的理解。如果我错了,请指正我,并感谢您的出色回答。
穆斯塔法·伊希克(MustafaIşık),
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.