我正在尝试通过遵循已经实现该代码的家伙的代码来实现自己的Gradient Domain Path Tracer:

https://gist.github.com/BachiLi/4f5c6e5a4fef5773dab1

我已经设法完成了多个步骤,但我想做更多的事情。我通过实现下一个事件估计扩展了参考中的代码,这是一些结果。

正常路径跟踪器图像:



梯度域生成的图像:



结果已经很好了。但是如前所述,我想要更多。因此,我实现了Next Event Estimation,这是基本Path Tracer的结果:



这是我的代码:

private Vector3 SampleWithNEE( Ray ray )
{
  // prepare
  Vector3 T = (1,1,1), E = (0,0,0), NL = (0,-1,0);
  int depth = 0;
  // random walk
  while (depth++ < MAXDEPTH)
  {
    // find nearest ray/scene intersection
    Scene.Intersect( ray );
    if (ray.objIdx == -1) break; //if there is no intersection
    Vector3 I = ray.O + ray.t * ray.D; //go to the Hit Point on the scene
    Material material = scene.GetMaterial( ray.objIdx, I );
    if (material.emissive) //case of a light
    {
        E += material.diffuse;
        break;
    }
    // next event estimation
    Vector3 BRDF = material.diffuse * 1 / PI;
    float f = RTTools.RandomFloat();
    Vector3 L = Scene.RandomPointOnLight() - I;
    float dist = L.Length();
    L = Vector3.Normalize( L );
    float NLdotL = Math.Abs( Vector3.Dot( NL, -L ) );
    float NdotL = Vector3.Dot( ray.N, L );
    if (NdotL > 0)
    {
        Ray r = new Ray( I + L * EPSILON, L, dist - 2 * EPSILON ); //make it a tiny bit shorter otherwise I risk to hit my starting and destination point
        Scene.Intersect( r );
        if (r.objIdx == -1) //no occlusion towards the light
        {
            float solidAngle= (nldotl * light.getArea()) / (dist * dist);
            E += T * (NdotL) * solidAngle * BRDF * light.emission;
        }
    }
    // sample random direction on hemisphere
    Vector3 R = DiffuseReflectionCosWeighted( ray.N );
    float hemi_PDF = Vector3.Dot( R, ray.N ) / PI;
    T *= (Vector3.Dot( R, ray.N ) / hemiPDF) * BRDF;
    ray = new Ray( I + R * EPSILON, R, 1e34f );
  }
  return E;
}


一切正常,结果如上图所示。还有一件事:我的场景中只有分散的曲面。

现在,问题是,在这种方法中,我使用两种PDF:


一种是通过在“直接照明”的“随机照明”中随机采样光来给出的下次事件估计,实际上是SolidAngle是我们的PDF,或者是更好的1 / PDF。
而第二个PDF是DiffuseReflectionCosWeighted的使用,它带来的PDF等于CosTheta / PI。

到目前为止,一切都很好,对于任何实现细节,您都可以看一下我的代码,但是我的Gradient Domain Path Tracer存在问题。确实,在那里,就像在上面Tzu-Mao Li所实现的参​​考链接中一样,我需要整个路径的最终概率密度来计算最终的梯度图像。如果没有下一个事件估计(NEE),如何计算?在那种情况下(因为我只有漫反射曲面),该概率是场景中每次反弹时CosTheta / PI的乘积。一切都很好,上面显示了所得的渐变图像。

相反,如果我使用NEE,则由于我的整个路径的概率密度会发生变化,并且我无法理解它的状态,因此不再起作用。带有下一个事件估计的最终梯度域图像为:



我需要了解如何计算路径的最终密度概率。你能帮我做吗?提前谢谢!

#1 楼

我没有使用梯度域路径跟踪的任何经验,但是我的想法是:

似乎有一个不同的问题

如果您仔细看一下尖峰,最终图像中的失真,您将看到它们都是从相同方向照亮的-在其左上方以一致的45度角照明。球体也似乎是从这个角度照亮的,而不是从光源上方照亮的。

这不可能由错误的路径概率估计来解释。我希望代码会有一个不同的问题,这些失真提示。

因此,我将解决这两个不同的问题:


想知道在使用“下一个事件估计”时如何计算路径的概率密度。
有证据表明与此无关的一些问题。

我还将查看非基本要点-但我将其留在基本要点之后。

使用Next Event Estimation时路径的概率密度

查看您所使用的代码所在的文件基于此,似乎可以根据在路径的顶点处发现的表面的反射特性来定义第5.2节中描述的新颖的偏移映射。我必须强调,我对此没有完全的了解,但是它表明,下一事件估计可能不需要对此方法进行更改,因为遇到的表面将是相同的。希望一旦解决了其他问题,就可以更容易判断图像看起来是否正确。

请注意,本文第5.2节已经提到(恰好在图10下方),它们考虑了采样问题。发射器“使用BSDF或区域采样”。

与Next Event Estimation的区别在于,区域采样发生在路径的每个顶点,但是对我来说这并不明显,这会导致问题。

场景仅使用漫反射曲面这一事实意味着,在大多数情况下,偏移路径应在第二个顶点处重新加入基本路径,因此您只需要重新计算偏移路径的第一个顶点的面积采样。

照明方向不正确的原因

在通读代码以熟悉其工作原理时,我注意到NLdotL是经过计算的,但并未使用。文本搜索显示唯一发生的另一例情况不同:nldotl。以下是上下文中的两个变量(此摘录的第一行和第9行):由于未定义nldotl,因此代码的结果为未定义行为。在实践中,该程序很可能像nldotl为零那样工作,或者对于某些编译器而言,可能在每次迭代时都具有恒定的任意值,甚至具有不同的任意值。对于您的特定编译器,它似乎是一个恒定值,我强烈怀疑这是所有散斑和球体上照明角度明显对齐的原因。如果还存在另一个造成问题的问题,则在解决此初始问题后,将更容易在一个单独的问题中进行分析。

可能值得考虑使用编译器和/或设置为错误,或者至少是对未定义变量的警告,因为这种错误很容易造成,以后也很容易忽略。

光源的其他贡献

似乎还有另一个问题,它将以更细微的方式导致结果不正确,并且没有明显的失真。由于下一个事件估计,光源对路径上的每个步骤都起作用。这意味着如果路径本身直接撞击光源,则不应做出任何贡献。否则,对于该路径,光源将贡献两次。您可以通过以下方法更正此问题:

float NLdotL = Math.Abs( Vector3.Dot( NL, -L ) );
float NdotL = Vector3.Dot( ray.N, L );
if (NdotL > 0)
{
    Ray r = new Ray( I + L * EPSILON, L, dist - 2 * EPSILON ); //make it a tiny bit shorter otherwise I risk to hit my starting and destination point
    Scene.Intersect( r );
    if (r.objIdx == -1) //no occlusion towards the light
    {
        float solidAngle= (nldotl * light.getArea()) / (dist * dist);
        E += T * (NdotL) * solidAngle * BRDF * light.emission;
    }
}


至:

if (material.emissive) //case of a light
{
    E += material.diffuse;
    break;
}


与光线的交点将产生零贡献。

请注意,因为我只能看到此功能,所以我无法猜测这是否也会使光线在图像中显示为黑色。您可能需要调整,也可能不需要调整从相机开始并直接照射到光线的光线。


代码审查

双重有限光线

我习惯将射线定义为无限远的半线段-有起点但没有终点。我注意到该代码为射线提供了起点和长度。我能看到的唯一原因是在对光源测试阴影射线时:代码检查到光的路径上没有相交,因此必须在光后面(或在光本身上)相交。排除在外。在所有其他地方,该射线定义为伪无限长(1e34f)。

以下建议不会影响代码的正确性,但可能更易理解,并且避免了可以解决无穷远的需要,并且必须考虑两次epsilon。

如果射线只是起点和方向,那么阴影射线可以简单地检查第一个交点是光,而不是而不是检查没有路口。例如,通过将以下内容替换为:

if (material.emissive) //case of a light
{
    break;
}


,将其替换为:

if (r.objIdx == -1) //no occlusion towards the light


这里我将LIGHT用作占位符光源的ID,因为该部分代码未包含在问题中。


如果光源被较近的物体遮挡,则此参数始终为false。
如果射线在更远的物体之前击中光线,这将始终为真。

因此,这等效于当前代码,但不需要射线存储长度。

不反射的光

该代码当前分别为灯光和曲面建模。这意味着,如果一个物体是光,那么它只是发光的,不会反射其他物体的光。

这导致问题中示例场景的差异可忽略不计,该场景只有一个明亮的光。但是,如果与许多调光器一起使用,它们将不会相互点亮会更加明显。在许多情况下,差异不会很明显,因此,如果它适用于您要渲染的场景,这不是问题。