采样技术 -- 在单位正方形上均匀采样
在上一篇专栏里说过, 几何表面上的采样的步骤为: 1. 在单位正方形上采样, 2. 把采样投射到几何表面上, 并且对几种几何表面的投射方法进行了讨论, 那么这篇专栏来讨论一下在单位正方形上的均匀采样.

关于采样的生成方法
为了方便管理代码, 理想的采样生成方法应该为: 1. 初始化采样生成器, 2. 直接从生成器里获得下一个采样. 用代码来说就是这样:
这对于一部分采样方法来说是可以实现的 (比如说随机采样, 均匀采样等), 但是对于一些更好的采样方法 (含有 NRooks 条件的) 这是难以实现的.
所以实际的采样生成方法应该为: 1. 从采样生成器里获得一大片采样, 2. 通过索引获取采样. 下面是大概的代码逻辑:
需要注意的是, 从采样生成器里取得采样集时, 应该取不止一个采样集, 因为当只有一个采样集时, 两次完整采样之间的分布将会是一模一样, 从而造成渲染质量低下的情况. (下图为 1 个采样集与 83 个采样集的渲染结果, 可以看到 1 个采样集出现了很多奇怪的条纹)


下面将会详细讨论几种不同的采样生成方法, 并且所有不同的采样生成方法都继承自 `IGenerator` 这个模板, 并且只需重写 `GenerateSamples` 方法, 这个方法接受两个正整数参数: 需要的采样集数量 和 一个采样集里的采样数量.

随机采样
最简单的在单位正方形上的均匀采样就是选两个随机数拼起来:
下图为随机采样的其中一个采样集 (64个采样)

可以看到采样分布非常不均匀, 有些地方出现大片空缺, 但有些地方采样过于密集. 在需要采样数量较少的光追算法里, 这样不均匀的采样是不能很好地反映几何表面的特征的.

均匀采样
保证采样分布均匀, 最简单的方法就是直接让采样等距分布, 如下图所示

实现这个算法也是非常简单的:
实现代码时需要注意到, 因为均匀采样的采样数量永远都是平方数, 所以实际生成的采样数量与输入的数量可能会不一样, 并且又因为这个采样不包括随机项, 无论生成多少个采样集都是一样的, 所以这里可以直接忽略输入的采样集数量.
但是均匀采样的均匀性反而是最大的坏处, 在实际场景里会因为采样过于规则产生严重的摩尔纹或者光线方向过于固定的情况:


同时均匀采样在某些地方甚至会产生比随机采样更加不均匀的采样 (见底部 -- 对于足够均匀的采样的投射问题).

抖动采样
抖动采样引入了分层技术, 分层技术就是把单位正方形分割为多个区域, 然后在每个区域上生成采样, 这可以确保采样不会过于集中. 算法实现如下:
下面是抖动采样生成的采样集 (64个采样)

可以看到采样分布比随机采样均匀了很多.

n-Rooks 采样
在抖动采样里仍然存在一些问题, 采样在某个轴方向上并不是分层的, 这样可能会造成在某些几何表面上采样极其不均匀: 在上一篇专栏里讨论过, 几何表面的采样只是把单位正方形上采样的 x, y 分量投射到几何表面的参数分量上, 也就是说原采样在分量上的不均匀会导致几何表面上采样的不均匀, 下图是不使用 Shirley 的同心圆投射的圆上抖动采样:

可以看到采样点分布比较不均匀. 于是提出了 n-Rooks 采样:
n-Rooks 采样会先预生成在 x, y 分量上分层的采样:

然后打乱采样的分量
下面是 n-Rooks 的全部代码
虽然 n-Rooks 采样确保了采样在分量上分层, 但不确保采样在单位正方形内分层, 所以实际效果可能连随机采样都不如, 下面是 n-Rooks 采样的一个采样集 (64个采样)


多重抖动采样
虽然 n-Rooks 采样表现不佳, 但是这个想法与抖动采样结合产生了多重抖动采样. 多重抖动采样在抖动采样的分层条件上继续分层, 产生了两层网格, 而采样则生成在网格内:

采样的预生成部分为:
当预生成好采样后, 需要对分量进行打乱. 需要注意的是, 这里不能随便打乱, 而是对分布在第一层网格上同一列的采样打乱 x 分量, 而同一行的采样打乱 y 分量:
多重抖动采样是综合表现最好的采样方法, 下面的一个采样集和这个采样集在圆上的投射 (64个采样)



正确多重采样
多重采样有一个改进的版本, 可以使得采样之间的分布更加均匀. 多重采样里采样的不均匀的原因是来自打乱分量这个操作. 为了确保采样之间距离的均匀, 打乱分量是一个对同一行 (或列) 的 y (或 x) 分量进行打乱, 亦即:
这样生成的采样会更加均匀:


Hammersley 采样 和 Halton 采样
Hammersley 采样是早期用于替代均匀采样的一种非随机采样算法, Hammersley 采样因为是非随机的, 所以与均匀采样一样只有一个采样集. 但除了只有一个采样集这个缺点, 其他方面表现得比多重抖动采样还好.
在 p 进制下, 所有非负整数 k 都可以表示为 , 那么定义 Φ 为 k 在 p 进制下沿小数点的镜像对称:
.
那么早期的 Hammersley 采样算法为
但是这种早期算法有点分层上的问题, 于是改进后的 Halton 算法为
其中 p0, p1 为两个不同的质数, 下面展示了几种不同 p0, p1 产生的采样集 (256个采样)




两种算法里的 Phi 为

对于足够均匀的采样的投射问题
以单位圆面为例, 投射公式为 , 当采样集足够均匀时 (差不多是等距分布, 如均匀采样和正确多重抖动采样), 采样集投射后会形成一条一条从原点出发的射线, 如下图所示: (均匀采样, 256个采样)

虽然 Shirley 提出的同心圆投射方法解决了这个问题 (见下).

但类似的现象还会发生在球面和半球投射上, 因为这个原因, 正确多重抖动采样非常不适合在球面和半球上采样 (见下, 图一为正确多重采样, 图二为多重采样, 2048个采样)


可以看到就算是多重抖动采样在半球边缘也出现了类似条纹的图案, 但是 Hammersley 采样 和 Halton 采样却没有这种情况, 因为这两个采样是在接近对角线上等距分布, 而不是在水平和垂直方向上 (见下, 图一 Hammersley 采样, 图二 Halton 采样)


所以我猜想真正好的采样应该是类似正确多重采样的随机采样, 但是采样是沿对角线等距分布的, 但是在我的知识范围内好像不存在这样的采样方法, 如果有人有头猪的话欢迎在评论区补充.

混合索引
在上述的采样方法里, 生成的采样集多数都是沿着索引, 采样从左下角开始到右上角的. 这种普遍性会带来强烈的索引相关性 (也就是多个采样集都在相近的地方采样) 从而造成不必要的渲染错误, 对此可以重写 `Sampler` 类的初始化函数, 在初始化时对索引进行打乱:
类似地, 采样集上也存在索引相关性, 所以当切换采样集时应该随机选取采样集, 而不是跳转到下一个:
下面是不使用和使用混合索引的区别:



关于光追里的采样技术的全部内容就这些了, 不出意料的话下一篇专栏就终于可以正式开始了.
不过趁着专栏还没正式开始, 还是想纠结一下到底是用什么编程语言做专栏里的主要语言呢.
如果要速度优先的话, 最好当然是C艹, 但是C艹写起来挺复杂的, 我只是打算搓一个 MWE 用于展示而已.
如果要简洁的话可能是 python 会好一点, 但是这样说, py算光追, 洗洗睡吧.
其实既简洁又快速的语言不是没有, julia 就是一个非常完美的选项, 但是b站专栏没有对 jl 做代码适配, 发代码就只能贴 PlaneText 了.
折中一下的话其实 C# 也可以接受, 但是就算是 C# 运行速度也不尽人意.
不过, 再说吧. (大概率是选 jl 的了)
最后宣传一下涩弔图群: 274767696
封面pid: 35231457