在上一篇文章中,我们多次提到了PDF(概率密度函数)这个概念,但可能没有深入解释它到底是什么,以及它在光线追踪中为什么如此重要。今天,让我们彻底搞清楚这个蒙特卡洛积分中的核心概念。
一、什么是PDF(概率密度函数)?
1.1 从离散到连续的概率
首先,让我们理解什么是概率密度函数。想象一下:
- 离散概率 :掷骰子,每个点数出现的概率是1/6,我们可以直接说 P ( 3 ) = 1 / 6 P(3)=1/6 P(3)=1/6
- 连续概率 :在 [ 0 , 1 ] [0,1] [0,1] 区间内随机取一个数,取到恰好 0.5 0.5 0.5 的概率是 0 0 0(因为有无限多个可能的值)
这就是为什么我们需要PDF!对于连续随机变量,我们不能说取某个特定值的概率,而只能说取某个值附近的概率密度。
1.2 PDF的数学定义
概率密度函数 p ( x ) p(x) p(x) 满足:
- 非负性 : p ( x ) ≥ 0 p(x) \ge 0 p(x)≥0 对所有 x x x
- 归一化 : ∫ − ∞ ∞ p ( x ) d x = 1 \int_{-\infty}^{\infty} p(x) dx = 1 ∫−∞∞p(x)dx=1
- 概率计算 : P ( a ≤ X ≤ b ) = ∫ a b p ( x ) d x P(a \le X \le b) = \int_a^b p(x) dx P(a≤X≤b)=∫abp(x)dx
直观理解:
- PDF的值本身不是概率,是"密度"
- 概率 = 密度 × 区间长度(在一维情况下)
- PDF越高的区域,随机变量落在那里的可能性越大
1.3 一个简单的例子
cpp
// 均匀分布U[0,1]的PDF
float uniformPDF(float x) {
if (x >= 0 && x <= 1) {
return 1.0f; // 在整个区间上密度恒定
}
return 0.0f;
}
// 验证归一化:∫₀¹ 1 dx = 1 ✓
// 标准正态分布N(0,1)的PDF
float normalPDF(float x) {
return (1.0f / std::sqrt(2.0f * M_PI)) * std::exp(-0.5f * x * x);
}
二、光线追踪中的PDF为什么重要?
在蒙特卡洛积分公式中:
F ≈ 1 N ∑ k = 1 N f ( X k ) p ( X k ) F \approx \frac{1}{N} \sum_{k=1}^{N} \frac{f(X_k)}{p(X_k)} F≈N1k=1∑Np(Xk)f(Xk)
分母上的 p ( X k ) p(X_k) p(Xk) 就是PDF!这个公式告诉我们:如果某个样本被抽中的概率很小,它的贡献就要被放大;如果被抽中的概率很大,贡献就要被缩小。
2.1 PDF的作用:偏差校正
假设我们要估计一个函数 f ( x ) f(x) f(x) 的平均值:
cpp
// 错误的方式:不考虑采样概率
float badEstimate = 0;
for (int i = 0; i < N; i++) {
float x = sampleImportant(); // 偏向重要区域采样
badEstimate += f(x); // 直接加和会导致过高估计
}
badEstimate /= N; // 结果会偏大!
// 正确的方式:除以PDF
float goodEstimate = 0;
for (int i = 0; i < N; i++) {
float x = sampleImportant();
float pdf = getPDF(x); // 获取这个样本被抽中的概率密度
goodEstimate += f(x) / pdf; // 除以PDF进行校正
}
goodEstimate /= N; // 无偏估计!
2.2 PDF的理想形式
理论上,最优的PDF应该正比于被积函数:
p ( x ) ∝ f ( x ) p(x) \propto f(x) p(x)∝f(x)
这样, f ( x ) / p ( x ) f(x)/p(x) f(x)/p(x) 变成常数,方差降为零!但现实中我们不知道 f ( x ) f(x) f(x)(这正是我们要计算的),所以我们只能寻找近似。
三、光线追踪中常见PDF的推导
3.1 均匀半球面采样
问题:在半球面上均匀采样方向,PDF是什么?
推导过程:
-
立体角 :半球面的总面积是 2 π 2\pi 2π 球面度(整个球面是 4 π 4\pi 4π,半球面是它的一半)
-
均匀分布要求 :每个方向的概率密度应该相同,即 p ( ω ) = constant p(\omega) = \text{constant} p(ω)=constant
-
归一化条件 :
∫ Ω p ( ω ) d ω = 1 \int_{\Omega} p(\omega) d\omega = 1 ∫Ωp(ω)dω=1
p ( ω ) ⋅ ∫ Ω d ω = 1 p(\omega) \cdot \int_{\Omega} d\omega = 1 p(ω)⋅∫Ωdω=1
p ( ω ) ⋅ 2 π = 1 p(\omega) \cdot 2\pi = 1 p(ω)⋅2π=1 -
结果 :
p ( ω ) = 1 2 π p(\omega) = \frac{1}{2\pi} p(ω)=2π1
代码实现:
cpp
class UniformHemisphereSampler {
public:
vec3 sample(float& pdf) {
// 在半球面上均匀采样
float z = rand() * 2.0f - 1.0f; // 实际需要投影到半球
float r = std::sqrt(std::max(0.0f, 1.0f - z*z));
float phi = 2.0f * M_PI * rand();
vec3 direction(r * std::cos(phi), r * std::sin(phi), z);
pdf = 1.0f / (2.0f * M_PI); // 恒定PDF
return direction;
}
};
3.2 余弦加权采样(重要性采样的一种)
问题 :为什么需要余弦加权?因为渲染方程中有个 cos θ \cos\theta cosθ 项,我们希望采样方向更多集中在法线方向。
推导过程:
我们希望 PDF 正比于 cos θ \cos\theta cosθ:
p ( ω ) = C ⋅ cos θ p(\omega) = C \cdot \cos\theta p(ω)=C⋅cosθ
归一化 :
∫ Ω C cos θ d ω = 1 \int_{\Omega} C \cos\theta d\omega = 1 ∫ΩCcosθdω=1
在球坐标中, d ω = sin θ d θ d ϕ d\omega = \sin\theta d\theta d\phi dω=sinθdθdϕ,且 cos θ sin θ = 1 2 sin 2 θ \cos\theta \sin\theta = \frac{1}{2}\sin 2\theta cosθsinθ=21sin2θ:
C ∫ 0 2 π ∫ 0 π / 2 cos θ sin θ d θ d ϕ = 1 C \int_0^{2\pi} \int_0^{\pi/2} \cos\theta \sin\theta d\theta d\phi = 1 C∫02π∫0π/2cosθsinθdθdϕ=1
C ⋅ 2 π ∫ 0 π / 2 1 2 sin 2 θ d θ = 1 C \cdot 2\pi \int_0^{\pi/2} \frac{1}{2} \sin 2\theta d\theta = 1 C⋅2π∫0π/221sin2θdθ=1
C ⋅ 2 π ⋅ 1 2 = 1 C \cdot 2\pi \cdot \frac{1}{2} = 1 C⋅2π⋅21=1
C = 1 π C = \frac{1}{\pi} C=π1
结果 :
p ( ω ) = cos θ π p(\omega) = \frac{\cos\theta}{\pi} p(ω)=πcosθ
采样方法(Malley's method):
cpp
class CosineWeightedSampler {
public:
vec3 sample(float& pdf) {
// 在单位圆盘上均匀采样
float r = std::sqrt(rand()); // 注意:不是均匀分布,需要变换
float phi = 2.0f * M_PI * rand();
float x = r * std::cos(phi);
float y = r * std::sin(phi);
float z = std::sqrt(std::max(0.0f, 1.0f - x*x - y*y));
// 将圆盘上的点投影到半球面,得到余弦加权分布
vec3 direction(x, y, z);
// PDF = cosθ/π,其中cosθ就是z
pdf = z / M_PI;
return direction;
}
};
3.3 BRDF重要性采样
问题:对于特定材质(如Phong、GGX),如何推导对应的PDF?
Phong材质示例:
Phong材质的BRDF:
f r ( ω o , ω i ) = k d ⋅ 1 π + k s ⋅ n + 2 2 π cos n α f_r(\omega_o, \omega_i) = k_d \cdot \frac{1}{\pi} + k_s \cdot \frac{n+2}{2\pi} \cos^n\alpha fr(ωo,ωi)=kd⋅π1+ks⋅2πn+2cosnα
其中 α \alpha α 是反射方向和视线方向的夹角。
推导镜面反射部分的PDF:
我们希望 PDF ∝ cos n α \cos^n\alpha cosnα。经过归一化:
p ( α ) = n + 1 2 π cos n α p(\alpha) = \frac{n+1}{2\pi} \cos^n\alpha p(α)=2πn+1cosnα
验证归一化 :
∫ 0 2 π ∫ 0 π / 2 n + 1 2 π cos n α sin α d α d ϕ = 1 \int_0^{2\pi} \int_0^{\pi/2} \frac{n+1}{2\pi} \cos^n\alpha \sin\alpha d\alpha d\phi = 1 ∫02π∫0π/22πn+1cosnαsinαdαdϕ=1
cpp
class PhongBRDFSampler {
private:
float n; // 光泽度参数
public:
vec3 sample(const vec3& reflectDir, float& pdf) {
// 在反射方向周围采样,分布由cosⁿα控制
float alpha = std::acos(std::pow(rand(), 1.0f/(n+1.0f)));
float phi = 2.0f * M_PI * rand();
// 构建局部坐标系
vec3 w = reflectDir;
vec3 u = normalize(cross(vec3(0,1,0), w));
vec3 v = cross(w, u);
// 从球坐标转换
float x = std::sin(alpha) * std::cos(phi);
float y = std::sin(alpha) * std::sin(phi);
float z = std::cos(alpha);
vec3 direction = x*u + y*v + z*w;
// PDF = (n+1)/(2π) * cosⁿα
pdf = (n + 1.0f) / (2.0f * M_PI) * std::pow(std::cos(alpha), n);
return direction;
}
};
3.4 光源采样PDF
问题:当对光源表面直接采样时,PDF应该是什么?
推导过程:
假设在光源表面 A A A 上均匀采样,那么面积上的PDF是:
p A ( p o i n t ) = 1 A p_A(point) = \frac{1}{A} pA(point)=A1
但我们需要的是方向上的PDF p ω ( ω ) p_\omega(\omega) pω(ω)。两者之间的关系是:
p ω ( ω ) = p A ( p o i n t ) ⋅ ∣ d A d ω ∣ p_\omega(\omega) = p_A(point) \cdot \left|\frac{dA}{d\omega}\right| pω(ω)=pA(point)⋅ dωdA
几何关系告诉我们:
d A d ω = r 2 cos θ ′ \frac{dA}{d\omega} = \frac{r^2}{\cos\theta'} dωdA=cosθ′r2
其中 r r r 是距离, θ ′ \theta' θ′ 是光源表面法线和光线方向的夹角。
所以:
p ω ( ω ) = 1 A ⋅ r 2 cos θ ′ p_\omega(\omega) = \frac{1}{A} \cdot \frac{r^2}{\cos\theta'} pω(ω)=A1⋅cosθ′r2
cpp
class LightSampler {
public:
vec3 sampleDirection(const vec3& shadingPoint,
const Light& light,
float& pdf) {
// 在光源表面采样一个点
vec3 lightPoint = light.samplePoint();
// 计算方向
vec3 direction = lightPoint - shadingPoint;
float distance = length(direction);
direction = normalize(direction);
// 计算光源面积上的PDF
float areaPDF = 1.0f / light.area();
// 计算立体角PDF
float cosThetaPrime = std::max(0.0f, dot(-direction, light.normal()));
pdf = areaPDF * distance * distance / cosThetaPrime;
return direction;
}
};
四、PDF在渲染方程中的完整推导
让我们从头推导完整的蒙特卡洛渲染方程,看看PDF如何自然地出现。
4.1 从渲染方程开始
渲染方程:
L o ( p , ω o ) = L e ( p , ω o ) + ∫ Ω f r ( p , ω i , ω o ) L i ( p , ω i ) cos θ i d ω i L_o(p, \omega_o) = L_e(p, \omega_o) + \int_{\Omega} f_r(p, \omega_i, \omega_o) L_i(p, \omega_i) \cos\theta_i d\omega_i Lo(p,ωo)=Le(p,ωo)+∫Ωfr(p,ωi,ωo)Li(p,ωi)cosθidωi
4.2 引入采样分布
假设我们按照某个分布 p ( ω ) p(\omega) p(ω) 采样 N N N 个方向 { ω 1 , ω 2 , . . . , ω N } \{\omega_1, \omega_2, ..., \omega_N\} {ω1,ω2,...,ωN}:
蒙特卡洛估计量:
⟨ L o ⟩ = L e + 1 N ∑ k = 1 N f r ( ω k ) L i ( ω k ) cos θ k p ( ω k ) \langle L_o \rangle = L_e + \frac{1}{N} \sum_{k=1}^{N} \frac{f_r(\omega_k) L_i(\omega_k) \cos\theta_k}{p(\omega_k)} ⟨Lo⟩=Le+N1k=1∑Np(ωk)fr(ωk)Li(ωk)cosθk
4.3 证明无偏性
取期望:
E [ ⟨ L o ⟩ ] = L e + ∫ [ f r ( ω ) L i ( ω ) cos θ p ( ω ) ] p ( ω ) d ω E[\langle L_o \rangle] = L_e + \int \left[ \frac{f_r(\omega) L_i(\omega) \cos\theta}{p(\omega)} \right] p(\omega) d\omega E[⟨Lo⟩]=Le+∫[p(ω)fr(ω)Li(ω)cosθ]p(ω)dω
化简:
E [ ⟨ L o ⟩ ] = L e + ∫ f r ( ω ) L i ( ω ) cos θ d ω E[\langle L_o \rangle] = L_e + \int f_r(\omega) L_i(\omega) \cos\theta d\omega E[⟨Lo⟩]=Le+∫fr(ω)Li(ω)cosθdω
正好等于渲染方程!这就是为什么只要 p ( ω ) > 0 p(\omega) > 0 p(ω)>0 对所有可能的 ω \omega ω 成立,蒙特卡洛估计就是无偏的。
4.4 方差分析
方差公式:
V [ ⟨ L o ⟩ ] = 1 N ∫ [ f r ( ω ) L i ( ω ) cos θ ] 2 p ( ω ) d ω − 1 N L o 2 V[\langle L_o \rangle] = \frac{1}{N} \int \frac{[f_r(\omega) L_i(\omega) \cos\theta]^2}{p(\omega)} d\omega - \frac{1}{N} L_o^2 V[⟨Lo⟩]=N1∫p(ω)[fr(ω)Li(ω)cosθ]2dω−N1Lo2
重要结论:
- 当 p ( ω ) ∝ f r ( ω ) L i ( ω ) cos θ p(\omega) \propto f_r(\omega) L_i(\omega) \cos\theta p(ω)∝fr(ω)Li(ω)cosθ 时,方差最小
- 这解释了为什么我们需要根据BRDF和光照来设计PDF
五、实践中的PDF处理技巧
5.1 多PDF的混合(多重重要性采样)
cpp
class MISPDF {
private:
std::vector<std::function<float(vec3)>> pdfs;
std::vector<float> weights;
public:
float evaluate(vec3 direction) {
float sum = 0.0f;
for (size_t i = 0; i < pdfs.size(); i++) {
sum += weights[i] * pdfs[i](direction);
}
return sum;
}
vec3 sample(float& totalPDF) {
// 随机选择一个PDF
int idx = choosePDF();
// 从选中的PDF采样
float componentPDF;
vec3 direction = samples[idx](componentPDF);
// 计算总PDF(考虑选择概率)
totalPDF = weights[idx] * componentPDF;
return direction;
}
};
5.2 PDF的数值稳定性
cpp
class SafePDF {
public:
static float evaluateWithGuard(float pdf, float minPDF = 1e-6f) {
return std::max(pdf, minPDF);
}
static float divideWithGuard(float value, float pdf) {
if (pdf <= 0.0f) {
return 0.0f; // 避免除以零
}
return value / pdf;
}
};
5.3 PDF的验证
cpp
class PDFValidator {
public:
static bool validatePDF(std::function<float(vec3)> pdf,
std::function<vec3(float&)> sampler,
int numTests = 1000000) {
// 验证归一化
float sum = 0.0f;
for (int i = 0; i < numTests; i++) {
float samplePDF;
vec3 direction = sampler(samplePDF);
// 重要:应该用evaluate验证,而不是用采样返回的PDF
float evaluatedPDF = pdf(direction);
// 用蒙特卡洛估计积分
sum += 1.0f / evaluatedPDF;
}
float estimatedIntegral = sum / numTests;
float error = std::abs(estimatedIntegral - 1.0f);
return error < 0.01f; // 允许1%的误差
}
};
六、总结:PDF选择的艺术
在光线追踪中,选择PDF是一门艺术,需要在几个因素间权衡:
| PDF类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 均匀采样 | 简单、鲁棒 | 方差大、收敛慢 | 测试、简单场景 |
| 余弦加权 | 匹配几何项 | 不匹配光照 | 漫反射表面 |
| BRDF采样 | 匹配材质特性 | 忽略光照分布 | 光泽表面 |
| 光源采样 | 匹配光照分布 | 忽略材质特性 | 小光源场景 |
| 多重重要性采样 | 结合多种优点 | 实现复杂 | 通用场景 |
记住黄金法则:
- PDF必须覆盖所有可能的采样方向(支撑集条件)
- 尽量让PDF的形状接近被积函数
- 在复杂场景中,多重重要性采样是最安全的选择
理解PDF的本质,你就掌握了蒙特卡洛光线追踪的精髓。它不仅是数学公式,更是我们对物理世界的理解和模拟的智慧结晶。