半球上的Hammersley

源作者:Holger Dammertz

一组关于如何在2D中使用Hammersley点集以在着色器程序中快速实用地生成半球方向的笔记。如果你发现任何错误或有意见,不要犹豫,请联系我或在我的博客上留言。

1. 概述

在编写与光照有关的着色器时,人们经常需要一些以表面法线为方向的半球方向。通常的做法是预先计算这些方向,并将它们存储在一个静态/统一的数组中,或者创建一个包含这些方向的可查询的纹理。在这一页,我研究了如何在着色器中直接使用2d中的Hammersley点集来快速计算合理的分布方向。当然,这个点集并不局限于取样方向,它还可以用于阴影图过滤、屏幕空间环境遮蔽(SSAO)或其他任何需要2D取样模式的应用。

二维的Hammersley点集是最简单的低差异序列之一,在计算机图形中的伪蒙特卡洛(Quasi Monte Carlo)集成方面有许多可能的应用。对各种不同的伪蒙特卡罗序列的总体概述可以在Niederreiter [niederreiter92]的书中找到。例如在 [wong97]中可以找到明确使用Hammerysley点集进行球体采样的方法。

下面我首先简要介绍一下如何从数学上构造Hammersley点集,然后展示这些点在2d中的样子,以及它们在映射到半球时的样子。之后,我向你展示了一个可以直接插入GLSL着色器中的实现(或者很容易翻译成任何其他语言)。

2. Hammersley点集

我们考虑一个点集

\[P_N=\left\{ x_1,...,x_N \right\}
\]

与点的数量\(N≥1\)在二维单位面积上\([0,1)\)(原本中关于此处均使用左闭右开)Hammersley点集\(H_N\),在2D中,现在被定义为

\[H_N=\left\{ x_i=\left( \begin{array}{c}
i/N\\
\varPhi _2\left( i \right)\\
\end{array} \right) ,for\,\,i=0,...,N-1 \right\} \,\, \left( 1 \right)
\]

其中\(\varPhi _2(i)\)是Van der Corput序列

Van der Corput序列的想法是在小数点处镜像\(i\)的二进制表示,以得到区间[0,1]中的一个数字。

Van der Corput序列\(\varPhi _2\)的数学定义如下:

\[\varPhi _2\left( i \right) =\frac{a_0}{2}+\frac{a_1}{2^2}+...+\frac{a_r}{2^{r+1}}\,\, \left( 2 \right)
\]

其中\(a_0a_1...a_r\)是\(i\)用二进制表示的各个数字(i.e. \(i=a_0+a_12+a_{2}2^{2}+a_{3}2^{3}+...a_{r}2^r\)).

2.1 Radical Inverse Examples

这一点最好通过一个例子来理解。在下面的表格中,我给出了\(i\)的十进制和二进制表示,以及相关的Van der Corput 基数逆(radical inverse)(如公式2中的定义),在小数点处二进制表示。为了便于理解,我还在\(i\)的二进制表示中加入了小数点,尽管在考虑二进制整数时通常会省略这一部分。

\(i\) decimal \(i\) binary \(Φ_2(i)\) binary \(Φ_2(i)\) decimal
0 0000.0 0.0000 0.0
1 0001.0 0.1000 0.5
2 0010.0 0.0100 0.25
3 0011.0 0.1100 0.75
4 0100.0 0.0010 0.125
5 0101.0 0.1010 0.625
...
11 1011.0 0.1101 0.8125
...

该表显示了Van der Corput序列中的前几个数字。它只是i在小数点处镜像的二进制表示。

这种简单的镜像二进制表示法使得Hammersley点集称为在着色器中快速和容易实现的点集的理想人选。在我讨论代码之前,我想向你展示点集在2D中的实际情况。

2.2 Hammersley点集图像

在这个交互式图像中,你可以改变点的数量\(N\),并得到由公式1定义的、在单位方格\([0,1)^2\)中可视化的结果点集。你可以看到,点\(\left( \begin{array}{c}
0\\
0\\
\end{array} \right)\)总是这个序列的一部分。你还可以看到,对于任何数量的N个点来说,这些点的分布都很好

N=3000

3. 生成半球上的点

为了从\(H_N\)创建半球上的方向,可以使用从\([0,1)\)到半球的任何映射,但必须注意所选择的映射要满足应用的要求。在光传输背景下进行积分时,通常需要球面上的均匀或余弦加权分布。

让\(x_i=
\left( \begin{array}{c}
u\\
v\\
\end{array} \right)
∈H_N\)是我们选择的Hammersley点集的一个点。让两个坐标\(u\)和\(v\),我们现在可以计算处以下的映射。把球坐标系转成直接坐标系即可。

3.1 统一映射

\[θ=cos^{-1}(1-u)\\
ϕ=2πv
\]

3.2 余弦映射

\[θ=cos^{-1}(\sqrt{(1-u)})\\
ϕ=2πv
\]

4. 源代码

为了实现Hammersley点集,我们只需要一个有效的方法来实现Van der Corput基数逆(radical inverse)\(Φ_2(i)\)。由于它是以2为基数的,我们可以使用一些基本的位操作来实现。Hacker's Delight [warren01]这本书为我们提供了一个简单的方法来反转一个给定的32位整数的位。使用这个方法,下面的代码就可以在GLSL中实现\(Φ_2(i)\)

 float radicalInverse_VdC(uint bits) {
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}

然后通过以下方式计算出第\(i\)个点\(x_i\)

 vec2 hammersley2d(uint i, uint N) {
return vec2(float(i) / float(N), radicalInverse_VdC(i));
}

一个小的优化是将上面的代码中除以N的部分改为乘以它的逆数,因为无论如何N都需要被固定。

为了完整起见,这里还有从Hammersley点集\({x_i=
\left( \begin{array}{c}
u\\
v\\
\end{array} \right)
}\)创建均匀或余弦分布方向(z-up)的代码。请注意,代码中的\(1-u\)是必要的,以便将序列中的第一个点映射到向上的方向而不是切平面。

 const float PI = 3.14159265358979;

 vec3 hemisphereSample_uniform(float u, float v) {
float phi = v * 2.0 * PI;
float cosTheta = 1.0 - u;
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
return vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
} vec3 hemisphereSample_cos(float u, float v) {
float phi = v * 2.0 * PI;
float cosTheta = sqrt(1.0 - u);
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
return vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
}

请注意,返回的方向当然是在默认的坐标框架中,并带有z-up(如果你愿意,你也可以称之为切线空间)。因此,在应用程序中可能需要执行的最后一步是通过使用由本地法向量构建的正交法向量基矩阵将返回的方向转换为世界空间。

5. 补充说明

在这里,我收集了一些关于如何使用这些点的进一步说明,以及其他一些值得了解的事情。

5.1. 这些点有多好呢

质量当然取决于应用,但首先要注意的是,对于少量的点,2D中的Hammersley点集在半球上的分布不是很好。这一点可以在一节中很容易得到验证。当需要很多点的时候,这个问题就会减轻,例如在进行交错采样的时候。虽然有许多其他的准随机序列可以使用,但到目前为止,我还没有发现任何可以像Hammeysley点集那样有效实施的序列(除了秩-1的格子,但他们不容易用于不同数量的点)。

这种采样模式的另一个问题是,在开始计算任何一个点之前,需要预先知道\(N\)个点的数量。这在实时渲染应用中几乎不是问题,但一旦进行某种形式的渐进式渲染,使用一个准蒙特卡洛序列(而不是一个点集)可能是一个更好的选择。

5.2. 交错式采样

交错采样[keller01]是一个经常使用的优化方法 [segovia06] ,用于实时渲染,其中一个积分的计算被分散到几个像素上,然后在一个额外的过滤步骤中合并。

当然,任何点的集合都可以用于此,但是简单的构造和使用使它特别容易将点集合分割成几个片段和/或半球。

6. 补充原文

原文中虽然有可视化生成Hammerslep随机点的例子,但是后续解释公式的时候,并未给出详细的例子,所以此处参考低差异序列(一)- 常见序列的定义及性质补充更为详细的例子

6.1. Radical Inversion与Van der Corput序列

接下来在介绍这些序列定义之前,先介绍一个基本的运算,Radical Inversion。这篇专栏将会介绍的所有序列都会用到这个运算过程。

\[i=\sum_{l=0}^{M-1}{a_l\left( i \right) b^l}
\]
\[\varPhi _{b,C}(i)=\left( b^{-1}...b^{-M} \right) \left[ C\left( a_0\left( i \right) ...a_{M-1}\left( i \right) \right) ^T \right]
\]

这个操作非常直观,\(b\)时一个正整数,则任何一个整数\(i\),如果先将\(i\)表示成\(b\)进制的数,然后把得到的数中的每一个位上的数字\(a_l(i)\)排成一个向量,和一个生成矩阵(genetator matrix)\(C\)相乘得到一个新的向量,最后再把这个新向量中镜像到小数点右边去就能得到这个数以\(b\)为底数,以\(C\)为生成矩阵的radical inversion(基数逆)\(\varPhi_{b,C}(i)\)

上面的描述可能略微有些复杂,但如果矩阵\(C\)是单位矩阵(Identity Matrix)的时候,这个过程会简化很多,即是直接把这个\(b\)进制数镜像到小数点右边去即可。同事也是Van der Corput序列的定义。

\[\varPhi _{b,C}\left( i \right) =\left( b^{-1}...b^{-M} \right) \left( a_0\left( i \right) ...a_{M-1}\left( i \right) \right) ^T=\sum_{l=0}^{M-1}{a_l\left( i \right) b^{-l-1}}
\]

举个例子,正整数8以2为底数的radical inverse的计算过程如下,首先算出8的2进制表示,1000。此处假设\(C\)为单位矩阵,所以直接将1000镜像到小数点右边,0.0001.这个二进制数的值就是最终结果,把它转换回10进制就得到1/16,既\(\varPhi_2(8)=\frac{1}{16}=0.0625\)。下面的表给出了更多的以2为底数的Van der Corput序列的例子。

\(i\) decimal \(i\) binary \(Φ_2(i)\) binary \(Φ_2(i)\) decimal
0 0000.0 0.0000 0.0
1 0001.0 0.1000 0.5
2 0010.0 0.0100 0.25
3 0011.0 0.1100 0.75
4 0100.0 0.0010 0.125
5 0101.0 0.1010 0.625
...
11 1011.0 0.1101 0.8125
...

Van der Corput序列有几个属性:

  • 每一个样本点都会落在当前已经有的点里"最没有被覆盖"的区域。例如\(\varPhi_2(3)=\frac{3}{4}\)时刚好落在了\([0,1)\)区间中被覆盖最少的区域(\(\varPhi_2(0)=0,\varPhi_2(1)=\frac{1}{2},\varPhi_2(2)=\frac{1}{4}\))
  • 样本个数到达\(b^m\)个点时对\([0,1)\)会形成uniform的划分。例如\(\varPhi_2(0)=0,\varPhi_2(1)=\frac{1}{2},\varPhi_2(2)=\frac{1}{4},\varPhi_2(3)=\frac{3}{4}\)
  • 很多时候并不能代替伪随机数,因为点的位置和索引有很强的关系,例如在以2为底的Van der Corput序列中,索引为奇数时候序列的值大于等于0.5,偶数时则小于0.5

6.2. 使用Hammersley产生随机数的伪代码

产生二维随机数,效率较高

// 此处以2为底数产生二维随机数所用,直接用位操作效率极高,如要拓展到多维,则看下一种做法
float RadicalInverse_VdC(uint bits)
{
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// ----------------------------------------------------------------------------
vec2 Hammersley(uint i, uint N)
{
return vec2(float(i)/float(N), RadicalInverse_VdC(i));
} const uint sample_count = 1024u;
for(uint i = 0u; i < sample_count; ++i)
{
Vector2f u_v = Hammersley(i, sample_count);
.....
// random_x = u_v.x;
// random_y = u_v.y;
}

扩展到多维

// 0.35
double make_hammersley_sequence(int index, int base) {
double f = 1, r = 0;
while (index > 0) {
f = f / base;
r = r + f * (index % base);
index = index / base;
}
return r;
} void hammersley_random()
{
for (uint i = 0u; i < max_num; ++i)
{
draw_points.emplace_back(float(i) / float(max_num), make_hammersley_sequence(i, 2));
// 三维
draw_points.emplace_back(float(i) / float(max_num),make_hammersley_sequence(i, 2)
, make_hammersley_sequence(i, 3));
}
}

6.3. 效果对比图

可以看到Hammersley序列比c++自身标准库的random实现更加的分布均匀

翻译自:http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html

引用博客:

可拓展阅读:

低差异序列 (low-discrepancy sequences)之Hammerysley在半球中采样点方法的介绍的更多相关文章

  1. 低差异序列 (low-discrepancy sequences)之Halton序列均匀产生多维随机数的介绍与实现

    Halton序列 在统计学中,Halton序列是用于生成空间中的点的序列,如Monte Carlo模拟的数值方法,虽然这些序列是确定性的,但它们的差异性很低,也就是说,在许多方面看起来是随机的.它们在 ...

  2. 洛谷P2687 [USACO4.3]逢低吸纳Buy Low, Buy Lower

    P2687 [USACO4.3]逢低吸纳Buy Low, Buy Lower 题目描述 “逢低吸纳”是炒股的一条成功秘诀.如果你想成为一个成功的投资者,就要遵守这条秘诀: "逢低吸纳,越低越 ...

  3. Python数据类型之“序列概述与基本序列类型(Basic Sequences)”

    序列是指有序的队列,重点在"有序". 一.Python中序列的分类 Python中的序列主要以下几种类型: 3种基本序列类型(Basic Sequence Types):list. ...

  4. 【转】Python数据类型之“序列概述与基本序列类型(Basic Sequences)”

    [转]Python数据类型之“序列概述与基本序列类型(Basic Sequences)” 序列是指有序的队列,重点在"有序". 一.Python中序列的分类 Python中的序列主 ...

  5. MATLAB最大均值差异(Maximum Mean Discrepancy)

    MATLAB最大均值差异(Maximum Mean Discrepancy) 作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 更多内容,请看标签:MAT ...

  6. [Swift]LeetCode187. 重复的DNA序列 | Repeated DNA Sequences

    All DNA is composed of a series of nucleotides abbreviated as A, C, G, and T, for example: "ACG ...

  7. [Swift]LeetCode946. 验证栈序列 | Validate Stack Sequences

    Given two sequences pushed and popped with distinct values, return true if and only if this could ha ...

  8. [USACO4.3]逢低吸纳Buy Low, Buy Lower

    https://daniu.luogu.org/problemnew/show/2687 求方案数: if(f[j]+1==f[i] && a[j]>a[i]) s[i]+=s[ ...

  9. LeetCode 187. 重复的DNA序列(Repeated DNA Sequences)

    187. 重复的DNA序列 187. Repeated DNA Sequences 题目描述 All DNA is composed of a series of nucleotides abbrev ...

随机推荐

  1. 软件研发中也有5S 管理?

    在精益生产中,价值流是贯穿生产全程的关键要素,标准化作业是实现生产线同步的关键工具,而生产现场的5S 管理则是管理一切生产要素的基础,所以我们将5S 称为精益的基础并不为过.5S 管理不仅可以应用到车 ...

  2. 带你读AI论文丨LaneNet基于实体分割的端到端车道线检测

    摘要:LaneNet是一种端到端的车道线检测方法,包含 LanNet + H-Net 两个网络模型. 本文分享自华为云社区<[论文解读]LaneNet基于实体分割的端到端车道线检测>,作者 ...

  3. 关于Java集合框架的总结

    Java集合框架(都实现了Cloneable和Serializable接口)支持以下两个类型的容器: 一种是为了存储一个元素集合,简称集合(collection). 另一种是为了存储键/值对,称为图( ...

  4. netty系列之:自建客户端和HTTP服务器交互

    目录 简介 使用客户端构建请求 accept-encoding server解析HTTP请求 总结 简介 上一篇文章,我们搭建了一个支持中文的HTTP服务器,并且能够从浏览器访问,并获取到相应的结果. ...

  5. Linux命令集锦之·字符截取命令

    时间:2018-11-15 记录:byzqy 字符截取命令: cut.printf.awk.sed cut $ cut [选项] 文件名 选项: -f 列号:提取第几列: -d 分隔符:按照指定分隔符 ...

  6. ffplay 播放网络摄像头视频

    shell脚本如下,无须加port ffplay rtsp://cameral_ip

  7. sql函数大全

    sql函数大全 一.内部函数 1.内部合计函数 1)COUNT(*) 返回行数 2)COUNT(DISTINCT COLNAME) 返回指定列中唯一值的个数 3)SUM(COLNAME/EXPRESS ...

  8. JavaWeb使用Filter进行字符编码过滤 预防web服务中文乱码

    JavaWeb使用Filter进行字符编码过滤 预防web服务中文乱码 准备条件:一个创建好的 JavaWeb 项目 步骤: 1.创建一个类并实现 Filter 接口 import javax.ser ...

  9. H5、C3基础知识笔记

    HTML5 本文内容参考于"HTML5|W3scool"教程 简介 是最新的 HTML 标准,拥有新的语义.图形以及多媒体元素 提供了新的 API 简化了 web 应用程序的搭建 ...

  10. JDK1.8源码阅读笔记(2) AtomicInteger AtomicLong AtomicBoolean原子类

    JDK1.8源码阅读笔记(2) AtomicInteger AtomicLong AtomicBoolean原子类 Unsafe Java中无法直接操作一块内存区域,不能像C++中那样可以自己申请内存 ...