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。
如果射线在更远的物体之前击中光线,这将始终为真。
因此,这等效于当前代码,但不需要射线存储长度。
不反射的光
该代码当前分别为灯光和曲面建模。这意味着,如果一个物体是光,那么它只是发光的,不会反射其他物体的光。
这导致问题中示例场景的差异可忽略不计,该场景只有一个明亮的光。但是,如果与许多调光器一起使用,它们将不会相互点亮会更加明显。在许多情况下,差异不会很明显,因此,如果它适用于您要渲染的场景,这不是问题。