在实现渲染方程式时,我使用了一些特殊技术来对曲面进行采样。例如:当我的一条光线撞击到漫反射表面时,将从该表面反射的下一条光线将使用余弦加权随机方向进行计算。这意味着在我的渲染方程式中,我必须考虑该特定随机分布所暗示的PDF,并在方程式中对此PDF进行特定划分。到目前为止,一切都还不错。
现在,我的问题是。我想实现一种称为“下一个事件估计”的特定技术,该技术仅对灯光进行采样。为此,我想使用以下代码(C ++)在球形光上选择一个随机点:
Vec3<float> randomPoint() const
{
float x;
float y;
float z;
// random vector in unit sphere
do
{
x = ((float)rand() / (RAND_MAX)) * 2 - 1; // random coordinates between -1 and 1
y = ((float)rand() / (RAND_MAX)) * 2 - 1;
z = ((float)rand() / (RAND_MAX)) * 2 - 1;
} while (pow(x, 2) + pow(y, 2) + pow(z, 2) > 1); // simple rejection sampling
return centerOfSphere + Vec3<float>(x, y, z) * radius;
}
我了解(如果错误请纠正我),这实现了统一的随机采样。而且,据我所读,这种采样的PDF = 1 /(b-a)。我的问题:因为它是均匀分布,我是否必须像使用余弦加权随机分布一样使用该PDF?如果是,PDF讨论的范围(b-a)是多少?提前谢谢!
#1 楼
首先,正如@trichoplax正确指出的那样,您的randomPoint函数将计算多维数据集中的一个点,然后使用拒绝采样返回单位球体内的所有点。为了返回球体上的点,您需要将“大于”更改为“等于”。也就是说,剔除采样效率非常低。一种更好的球形采样方法是在球形空间中采样,然后转换为笛卡尔坐标。即:
float phi = PI * randf();
float theta = 2 * PI * randf();
float x = radius * std::sinf(phi) * std::sinf(theta);
float y = radius * std::cosf(phi);
float z = radius * std::sinf(phi) * std::cosf(theta);
但是,这是不均匀的,会导致样品在两极聚集。为了防止这种情况,我们转换phi: \ begin {align *}
\ sin(\ cos ^ {-1}(x))\ equiv&\ \ sqrt {1-x ^ 2} \\
\ cos(\ cos ^ {- 1}(x))\ equiv&\ x \\
\\
\ phi =&\ \ cos ^ {-1}(2 \ xi-1)\\
\ sin( \ phi)=&\ sin(\ cos ^ {-1}(2 \ xi-1))\\
=&\ \ sqrt {1-(2 \ xi-1)^ 2} \\
\ cos(\ phi)=&\ 2 \ xi-1
\ end {align *} $$
所以完整的更改是:
float phi = std::acosf(2.0f * randf() - 1.0f);
float theta = 2 * PI * randf();
但是,在下一个事件估计的情况下,对整个球体进行均匀采样效率不高,因为射线只能在一个位置“看到”球体的一半时间。因此,如果我们在球体的“背面”生成一个点,则球体将遮挡该点,并且您的计算将被浪费。
相反,您将生成一个圆锥形的样本,该样本涵盖了从射线的起点看是球体的圆。
float cosPhi = 2.0f * randf() - 1.0f;
float sinPhi = std::sqrt(1.0f - cosPhi * cosPhi);
float theta = 2 * PI * randf();
float x = radius * sinPhi * std::sinf(theta);
float y = radius * cosPhi;
float z = radius * sinPhi * std::cosf(theta);
注意:x,y,z在局部坐标空间中。
因此我们需要转换为世界坐标:
// Sample sphere uniformly inside subtended cone
float rand1 = randf();
float rand2 = randf();
// Compute theta and phi values for sample in cone
float distanceSquared = DistanceSquared(rayOrigin, sphereCenter);
// We use geometry to calculate the angle of the cone (aka, the maximum phi can be when we sample)
// It's easier / cheaper to use geometry to calculate sin/cos phi directly, rather than generating phi and using sin/cos
float sinPhiMaxSquared = radius * radius / distanceSquared;
float cosPhiMax = std::sqrt(1.0f - sinPhiMaxSquared);
float cosPhi = (1.0f - rand1) + rand1 * cosPhiMax;
float sinPhi = std::sqrt(1.0f - cosPhi * cosPhi);
// Phi can be anything in 2 PI
float theta = 2 * PI * rand2;
float x = radius * sinPhi * std::sinf(theta);
float y = radius * cosPhi;
float z = radius * sinPhi * std::cosf(theta);
pdf的计算公式如下:
此处的代码在很大程度上受PBRT v3的影响/复制。它们具有用于从形状采样的一系列类和函数。
最后是pdf。在蒙特卡洛集成中,我们需要对我们进行的每个集成的pdf进行合并。在路径跟踪中,我们可以集成很多东西。例如,一般的渲染方程式对半球上的入射光进行积分,视场深度可以视为焦距上的积分,等等。分为两个整数,即直接照明和间接照明。
标准渲染公式:
$$ L _ {\ text {o}}(p,\ omega _ {\ text {o}}) = L_ {e}(p,\ omega _ {\ text {o}})\ + \ \ int _ {\ Omega} f(p,\ omega _ {\ text {o}},\ omega _ {\ text {i}} )L _ {\ text {i}}(p,\ omega _ {\ text {i}})\ left | \ cos \ theta _ {\ text {i}} \ right | d \ omega _ {\ text {i}} $$
下一个事件估计:
$$ L _ {\ text {o}}(p,\ omega _ {\ text {o}} )= L_ {e}(p,\ omega _ {\ text {o}})\ + \ \ int _ {\ Omega} f(p,\ omega _ {\ text {o}},\ omega _ {\ text {i} })L _ {\ text {i,direct}}(p,\ omega _ {\ text {i}})\ left | \ cos \ theta _ {\ text {i}} \ right | d \ omega _ {\ text {i}} \ \ + \ \ int _ {\ Omega} f(p,\ omega _ {\ text {o}},\ omega _ {\ text {i}})L _ {\ text {i ,间接}}(p,\ omega _ {\ text {i}})\ left | \ cos \ theta _ {\ text {i}} \ right | d \ omega _ {\ text {i}} $$
在简单的朴素前向路径跟踪中,所有内容都被视为间接光源。在下一个事件估计中,我们直接计算直接照明并将其添加到间接照明中。而且,如果我们击中一盏灯,我们将忽略直接的贡献,因为我们正在计算直接照明。
由于我们有两个集成,所以每个集成都有自己的pdf。又名:
$$ L _ {\ text {o}}(p,\ omega _ {\ text {o}})= L_ {e}(p,\ omega _ {\ text {o}})\ \ + \ \ sum_ {k = 0} ^ {\ infty} \ frac {f(p,\ omega _ {\ text {o}},\ omega _ {\ text {i}})L _ {\ text {i,direct}} (p,\ omega _ {\ text {i}})\ left | \ cos \ theta _ {\ text {i}} \ right | } {pdf_ {direct}} \ \ + \ \ sum_ {k = 0} ^ {\ infty} \ frac {f(p,\ omega _ {\ text {o}},\ omega _ {\ text {i}}) L _ {\ text {i,indirect}}(p,\ omega _ {\ text {i}})\ left | \ cos \ theta _ {\ text {i}} \ right | } {pdf_ {indirect}} $$
如果您想了解其实现方式,可以在此处查看我的实现。注意:我的灯光采样会稍微复杂一点,因为它会进行多个重要性采样。但这可能很简单:
float3 zAxis = normalize(sphereCenter - rayOrigin);
float3 xAxis;
if (std::abs(zAxis.x) > std::abs(zAxis.y))
xAxis = float3(-zAxis.z, 0.0f, zAxis.x) / std::sqrt(zAxis.x * zAxis.x + zAxis.z * zAxis.z);
else
xAxis = float3(0.0f, zAxis.z, -zAxis.y) / std::sqrt(zAxis.y * zAxis.y + zAxis.z * zAxis.z);
yAxis = cross(zAxis, xAxis);
float3 samplePoint = x * xAxis + y * yAxis + z * zAxis;
评论
$ \ begingroup $
我相信你是正确的。到家后,我将检查PBRT书籍并更正代码。 IIRC,我需要添加平方根。
$ \ endgroup $
–RichieSams
16年5月12日在23:09
$ \ begingroup $
是的,@ trichoplax是正确的,对两个角度进行均匀采样将导致点在两极聚集。固定它的最简单方法是均匀地采样theta和Y,然后将XZ半径设置为$ \ sqrt {r ^ 2-y ^ 2} $。顺便说一句,顺便说一句,这就是您均匀采样cosPhi(与Y成比例)时在第二个代码段中所做的事情。另请参见此MathWorld文章。
$ \ endgroup $
–内森·里德(Nathan Reed)
16年5月13日在2:01
$ \ begingroup $
修复和更新。
$ \ endgroup $
–RichieSams
16年5月13日在2:36
$ \ begingroup $
在MathWorld的文章中:Marsaglia(1972)-磁盘中的点方法应胜过trig。
$ \ endgroup $
– MB雷诺兹
16年5月17日在9:18
$ \ begingroup $
我需要进行一些分析,但是我并不完全相信拒绝采样会胜过Trig。
$ \ endgroup $
–RichieSams
16年5月17日在11:56
评论
该代码似乎返回一个从单位球体内部体积均匀选择的点。这将与球体表面产生不同的结果。这是你打算的吗?我问,是因为您提到在灯光“上”选择一个点,听起来像表面上的一个点。但是.. centerOfSphere + Vec3
如果这些点已经在球体的表面上,则将其乘以半径将得到该半径的球体的表面。如果这些点已经在球体的内部体积中,则将其乘以半径会将其放置在该半径的球体的内部体积中。乘以例如半径2,将得到一个两倍大的球体。但是,这些点是在此较大球体的表面上还是在其体积中,取决于它们在表面还是原始单位球体内的体积