什么是重要性抽样?我读到的每篇文章都提到“ PDF”,那又是什么?
据我所知,重要性采样是一种仅对半球比其他区域更重要的区域进行采样的技术。因此,理想情况下,我应该向光源采样光线以减少噪声并提高速度。而且,某些BRDF在掠射角上的计算几乎没有差异,因此使用重要性抽样来避免这种情况是好的吗?
如果要对Cook-Torrance BRDF进行重要性抽样,该怎么做?
什么是重要性抽样?我读到的每篇文章都提到“ PDF”,那又是什么?
据我所知,重要性采样是一种仅对半球比其他区域更重要的区域进行采样的技术。因此,理想情况下,我应该向光源采样光线以减少噪声并提高速度。而且,某些BRDF在掠射角上的计算几乎没有差异,因此使用重要性抽样来避免这种情况是好的吗?
如果要对Cook-Torrance BRDF进行重要性抽样,该怎么做?
Answers:
重要采样是一种通过选择接近实际函数形状的估计量来减少蒙特卡洛积分中方差的方法。
PDF是“ 概率密度函数”的缩写。甲给出生成是随机样本的概率。
首先,让我们回顾一下什么是Monte Carlo积分,以及数学上的外观。
蒙特卡洛积分是一种估计积分值的技术。通常在没有闭合形式的积分解决方案时使用。看起来像这样:
用英语说,这可以通过对函数中连续的随机样本求平均来近似积分。随着变大,近似值越来越接近解。表示每个随机样本的概率密度函数。
让我们做一个例子:计算积分的值。
让我们使用蒙特卡洛积分:
一个简单的python程序来计算这个是:
import random
import math
N = 200000
TwoPi = 2.0 * math.pi
sum = 0.0
for i in range(N):
x = random.uniform(0, TwoPi)
fx = math.exp(-x) * math.sin(x)
pdf = 1 / (TwoPi - 0.0)
sum += fx / pdf
I = (1 / N) * sum
print(I)
如果我们运行程序,则得到
使用零件分离,我们可以获得确切的解决方案:
您会注意到,蒙特卡洛解决方案不太正确。这是因为这是一个估计。也就是说,随着趋于无穷大,估计值应该越来越接近正确答案。在一些运行几乎与正确答案相同。
关于PDF的注释:在这个简单的示例中,我们始终采用统一的随机样本。统一的随机样本意味着每个样本的选择概率完全相同。我们在范围内采样,因此
重要性抽样的工作方式不是统一抽样。取而代之的是,我们尝试选择更多对结果有很大贡献的样本(重要),而选择只对结果有少量贡献的样本(重要性较低)。故名,重要性抽样。
如果选择pdf与的形状非常匹配的采样函数,则可以大大减少方差,这意味着可以减少采样。但是,如果选择值与完全不同的采样函数,则可以增加方差。参见下图: Wojciech Jarosz 论文附录A的图像
“路径跟踪”中重要性采样的一个示例是如何在光线撞击表面后选择其方向。如果表面不是完全镜面反射(例如,镜子或玻璃),则出射光线可以在半球的任何位置。
我们可以对半球进行均匀采样以生成新射线。但是,我们可以利用以下事实:渲染方程中包含余弦因子:
具体来说,我们知道地平线上的所有光线都会被严重衰减(特别是)。因此,在地平线附近产生的光线对最终值的贡献不大。
为了解决这个问题,我们使用重要性抽样。如果我们根据余弦加权半球生成射线,则可以确保在水平线上方产生更多的射线,而在水平线附近产生的射线更少。这将降低方差并减少噪声。
在您的情况下,您指定将使用基于微面的Cook-Torrance BRDF。常见形式为:
哪里
博客“ A Graphic's Guy's Note”写了一篇出色的文章,介绍了如何对Cook-Torrance BRDF进行采样。我将推荐您阅读他的博客文章。也就是说,我将尝试在下面创建一个简短的概述:
NDF通常是Cook-Torrance BRDF的主要部分,因此,如果要进行重要性采样,则应基于NDF进行采样。
Cook-Torrance没有指定要使用的特定NDF。我们可以自由选择适合我们的任何一种。也就是说,有一些流行的NDF:
每个NDF都有自己的公式,因此每个样本必须以不同的方式采样。我将仅展示每个样本的最终采样功能。如果您想了解公式的派生方式,请参阅博客文章。
GGX定义为:
要采样球坐标角,我们可以使用以下公式:
其中是统一随机变量。
我们假设NDF是各向同性的,所以我们可以对均匀采样:
贝克曼的定义为:
可以通过以下方式采样:
最后,布林定义为:
可以通过以下方式采样:
让我们看一下基本的向后路径跟踪器:
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);
// Bounce the ray around the scene
for (uint bounces = 0; bounces < 10; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.geomID == RTC_INVALID_GEOMETRY_ID) {
color += throughput * float3(0.846f, 0.933f, 0.949f);
break;
}
// We hit an object
// 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 emmisive light
if (light != nullptr) {
color += throughput * light->Le();
}
float3 normal = normalize(ray.Ng);
float3 wo = normalize(-ray.dir);
float3 surfacePos = ray.org + ray.dir * ray.tfar;
// Get the new ray direction
// Choose the direction based on the material
float3 wi = material->Sample(wo, normal, sampler);
float pdf = material->Pdf(wi, normal);
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.org = surfacePos;
// Reset the other ray properties
ray.dir = wi;
ray.tnear = 0.001f;
ray.tfar = embree::inf;
ray.geomID = RTC_INVALID_GEOMETRY_ID;
ray.primID = RTC_INVALID_GEOMETRY_ID;
ray.instID = RTC_INVALID_GEOMETRY_ID;
ray.mask = 0xFFFFFFFF;
ray.time = 0.0f;
}
m_scene->Camera.FrameBuffer.SplatPixel(x, y, color);
}
IE浏览器 我们在场景中弹跳,并在进行过程中累积颜色和光衰减。每次反弹时,我们都必须为射线选择一个新的方向。如上所述,我们可以对半球进行均匀采样以生成新射线。但是,代码更聪明。重要的是根据BRDF对新方向进行采样。(注意:这是输入方向,因为我们是反向路径跟踪器)
// Get the new ray direction
// Choose the direction based on the material
float3 wi = material->Sample(wo, normal, sampler);
float pdf = material->Pdf(wi, normal);
可以实现为:
void LambertBRDF::Sample(float3 outputDirection, float3 normal, UniformSampler *sampler) {
float rand = sampler->NextFloat();
float r = std::sqrtf(rand);
float theta = sampler->NextFloat() * 2.0f * M_PI;
float x = r * std::cosf(theta);
float y = r * std::sinf(theta);
// Project z up to the unit hemisphere
float z = std::sqrtf(1.0f - x * x - y * y);
return normalize(TransformToWorld(x, y, z, normal));
}
float3a TransformToWorld(float x, float y, float z, float3a &normal) {
// Find an axis that is not parallel to normal
float3a majorAxis;
if (abs(normal.x) < 0.57735026919f /* 1 / sqrt(3) */) {
majorAxis = float3a(1, 0, 0);
} else if (abs(normal.y) < 0.57735026919f /* 1 / sqrt(3) */) {
majorAxis = float3a(0, 1, 0);
} else {
majorAxis = float3a(0, 0, 1);
}
// Use majorAxis to create a coordinate system relative to world space
float3a u = normalize(cross(normal, majorAxis));
float3a v = cross(normal, u);
float3a w = normal;
// Transform from local coordinates to world coordinates
return u * x +
v * y +
w * z;
}
float LambertBRDF::Pdf(float3 inputDirection, float3 normal) {
return dot(inputDirection, normal) * M_1_PI;
}
在对inputDirection(在代码中为“ wi”)进行采样之后,我们将其用于计算BRDF的值。然后按照蒙特卡洛公式除以pdf:
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
其中Eval()只是BRDF函数本身(Lambert,Blinn-Phong,Cook-Torrance等):
float3 LambertBRDF::Eval(float3 inputDirection, float3 outputDirection, float3 normal) const override {
return m_albedo * M_1_PI * dot(inputDirection, normal);
}
wi
?我知道如何采样球坐标角θ,但是对于实际方向矢量,该怎么做?
如果您具有一维函数并且想要将该函数从0集成到1,则执行此集成的一种方法是通过取[0,1]范围内的N个随机样本,对每个评估采样并计算样本平均值。但是,这种“幼稚的”蒙特卡洛积分被称为“缓慢收敛”,即,您需要大量样本才能接近基本事实,尤其是在函数具有较高频率的情况下。
使用重要性采样,您可以在的“重要”区域中获取更多对最终结果贡献最大的样本,而不是在[0,1]范围内获取N个随机样本。但是,由于将采样偏向函数的重要区域,因此必须减少这些采样的权重以抵消偏倚,这是PDF(概率密度函数)所伴随的地方。PDF表示给定位置的样本概率,并通过将每个样本除以每个样本位置的PDF值来计算样本的加权平均值。
使用Cook-Torrance重要抽样时,通常的做法是根据正态分布函数NDF分配样本。如果已经对NDF进行了归一化,则可以将其直接用作PDF,这很方便,因为它可以将其从BRDF评估中删除。只有你需要做的事情是一个基于PDF分发样本的位置和评估BRDF没有NDF项,即 和计算样本结果的平均值乘以您积分的区域的立体角(例如,半球为)。
对于NDF,您需要计算PDF的累积分布函数,以将均匀分布的样本位置转换为PDF加权样本位置。对于各向同性NDF,由于函数的对称性,因此简化为一维函数。有关CDF派生的更多详细信息,请查看此旧的GPU Gems文章。