文章

球面均匀采样

球面均匀采样:为什么是 ​cos(\theta)=1-2v

来源:UE4 MonteCarlo.ush 中的 UniformSphereSample,用于 PRT/Surfel 探针采样时在球面上均匀地撒方向。

前言

在图形学里,"在球面上均匀采样一个方向"是最基础的操作之一:环境光积分、球谐函数拟合、Surfel 分布、蒙特卡洛估计……都离不开它。

UE4 的 MonteCarlo.ush 给出的标准实现非常短:

float3 UniformSphereSample(float u, float v)
{
    const float C_PI = 3.14159265359f;
    float phi = 2.0 * C_PI * u;
    float cosine_theta = 1.0 - 2.0 * v;
    float sine_theta = sqrt(1.0 - cosine_theta * cosine_theta);

    float x = sine_theta * cos(phi);
    float y = sine_theta * sin(phi);
    float z = cosine_theta;

    return float3(x, y, z);
}

最容易让人困惑的是这一行:

float cosine_theta = 1.0 - 2.0 * v;

为什么不是 ​\theta=\pi v,而是先算出一个"余弦"?本文从数学上把这件事讲清楚。

先明确目标:什么叫"均匀"

球面上均匀,指的是:落在任一小片曲面上的概率,等于该片面积占球总面积(​4\pi)的比例:

dP=\frac{dA}{4\pi}

关键词是面积,不是角度。这一点是全部推导的源头——很多人直觉上想让极角 ​\theta 均匀分布,但那不是面积均匀,后面会看到它会导致采样点在两极堆积。

第一步:球面面积微元

对球面做参数化,​\theta 为极角,从北极 ​0 到南极 ​\pi;​\varphi 为方位角,从 ​0 到 ​2\pi:

\begin{cases} x=sin(\theta)cos(\varphi) \\ y=sin(\theta)sin(\varphi) \\ z=cos(\theta) \end{cases}

在 ​(\theta, \varphi) 处取一小块:​\theta 方向宽 ​d\theta,​\varphi 方向宽 ​d\varphi。这一小块到竖直转轴的距离(即所在纬度环的半径)是 ​r=sin(\theta),于是这块的"横向弧长"是 ​sin(\theta)d\varphi,"纵向弧长"是 ​d\theta:

dA=(sin(\theta)d\varphi)\cdot d\theta=sin(\theta)d\theta d\varphi

注意 ​sin(\theta) 这个因子:赤道处(​\theta=\frac{\pi}{2})环带最宽,两极附近(​\theta\rightarrow 0 或 ​\pi)环带收缩成一个点。同样大小的 ​d\theta,在不同纬度对应的真实面积完全不同。

第二步:​\theta 的概率密度必须是 ​sin(\theta)/2

我们要的是 ​\theta 的边缘分布。把面积微元代进均匀定义,再对 ​\varphi 积分(​\varphi 从 ​0 到 ​2\pi):

p(\theta)d\theta=\int_0^{2\pi}\frac{sin(\theta)d\theta d\varphi}{4\pi}=\frac{2\pi\cdot sin(\theta)}{4\pi}d\theta

所以:

p(\theta)=\frac{sin(\theta)}{2}

为什么必须是它? 因为只有当 ​p(\theta)\propto sin(\theta) 时,概率密度里的 ​sin(\theta) 才会和面积微元里的 ​sin(\theta) 相消,使单位面积的概率密度处处等于常数 ​\frac{1}{4\pi}——这才是球面均匀。

验证归一化(概率为1):

\int_0^{\pi}\frac{sin(\theta)}{2}d\theta=\left[-\frac{cos(\theta)}{2}\right]_0^{\pi}=\frac{1-(-1)}{2}=1

反面教材:如果天真地取 ​\theta=\pi v(即 ​p(\theta)=\frac{1}{\pi},​\theta 均匀),那么单位面积的概率密度为:

\frac{p}{2\pi\,sin(\theta)}\propto\frac{1}{sin(\theta)}\;\;\rightarrow\;\;\theta\rightarrow 0\ 或\ \pi\ 时发散

两极处面积趋近于零、概率却不衰减,采样点全部涌向两极。采样出来的方向分布会明显"扎堆",任何依赖均匀性的积分估计都会有系统性偏差。

第三步:逆变换采样(Inverse Transform Sampling)

现在已知 ​\theta 应服从密度 ​p(\theta)=\frac{sin(\theta)}{2},如何用 ​[0,1] 均匀随机数 ​v 生成它?标准答案是逆变换采样。

求累积分布函数(CDF):

F(\theta)=\int_0^{\theta}\frac{sin(t)}{2}dt=\frac{1-cos(\theta)}{2}

令 ​F(\theta)=v,反解出 ​\theta:

\frac{1-cos(\theta)}{2}=v\;\;\Rightarrow\;\;cos(\theta)=1-2v

这正是那一行代码。而且代码里不需要真的算 ​\theta=arccos(1-2v)——因为后面只会用到:

sin(\theta)=\sqrt{1-cos^2(\theta)},\;\;\;z=cos(\theta)

保留 ​cos(\theta) 直接参与运算即可,省掉一次反三角函数。

而方位角 ​\varphi 没有这个问题:绕轴旋转的环带面积与 ​\varphi 无关,​\varphi 天然均匀,直接取:

\varphi=2\pi u

一个漂亮的直觉:​z 均匀 ​\Leftrightarrow 面积均匀

​cos(\theta) 不是别的,正是方向向量的 ​z 坐标。所以 ​cos(\theta)=1-2v 等价于:

让 ​z 在 ​[-1, 1] 上均匀取值。

为什么这样就均匀了?想象用一组水平平面以等间距 ​dz 把球切成薄片——高度 ​z 处那片的球面面积是:

A(z)=2\pi\,dz

环带半径 ​sin(\theta) 乘上周长 ​2\pi,恰好与 ​z 无关。球面在 ​z 轴上的"投影"是均匀的:每一层等厚的球壳切片面积完全相同。所以 ​z 均匀分布 ​\Leftrightarrow 面积均匀分布。这也是阿基米德球冠定理的现代版:球冠面积只取决于它的高,不取决于它的张角。

回到完整代码

把上面三步合起来看 CSMain 的调用链:

// [0,1] 均匀随机数(两路独立噪声)
float u = rand(xy * 1.0);
float v = rand(xy * 2.0);

// u → 方位角 φ(天然均匀)
// v → 极角余弦 cosθ(逆变换采样,补偿 sinθ 面积权重)
float3 dir = UniformSphereSample(u, v);

​u 和 ​v 地位并不对称:​u 直接映射角度,​v 必须先经过 ​\frac{1-cos(\theta)}{2} 这个 CDF 的"整形",才能抵消球面面积随纬度的变化。

总结

量 分布 原因
​\varphi(方位角) 均匀于​[0, 2\pi] 环带面积与​\varphi 无关
​\theta(极角) 密度​\frac{sin(\theta)}{2} 环带面积​\propto sin(\theta)
​cos(\theta)(即 ​z) 均匀于​[-1, 1] 等厚切片面积恒为​2\pi dz

一句话记住:

球面均匀采样 = 方位角均匀 + 极角余弦均匀。 ​cos(\theta)=1-2v 就是逆变换采样对 ​p(\theta)=\frac{sin(\theta)}{2} 求逆后的解析解。

参考

  • Unreal Engine 4, MonteCarlo.ush — UniformSphereSample
  • PBRT (Physically Based Rendering), 第 13 章 — Monte Carlo Integration / Transforming between distributions
  • Archimedes' Hat-Box Theorem — 球冠面积与 ​z 的线性关系
许可协议:  CC BY 4.0