适用于 OpenCL 内核的并行噪声和随机函数

立即下载Noise.zip

示例介绍

本文中的噪声示例代码包括一个柏林噪声算法的实施,可为 3D 显卡生成外观自然的纹理,如大理石和云。 其中还包括使用柏林噪声生成“云”图片的测试。 (如欲了解关于柏林噪声的更多信息,请参阅“参考文献”部分。) 包括 2D 和 3D 两种版本,这意味着函数需要两个或三个输入才能生成一个柏林噪声输出值。

“噪声”示例还包括伪随机数生成器 (RNG) 函数,它可以生成较好的结果,至少能够让生成的图像看上去具备随机的效果。 包括 1D、2D 和 3D 三种版本,同样,需要参考输入的个数来生成一个伪随机值。

介绍和目的

许多应用都需要一定程度的“随机性” — 事实上是“伪随机性”。 即一系列的值以随机或“噪声”的形式呈现。 但是,为了能够重复,应用通常还要求,在给定相同的输入“种子”值时,RNG 能够可靠地生成相同的值序列。

多数 RNG 算法都是通过根据上一个生成值来创建生成值,并且直接从种子值中生成序列的第一个值来满足这些要求。 RNG 的这种方法不适用于高度并行的处理语言,如 OpenCL。 强迫诸多处理线程中的每个线程等待一个连续 RNG 源,将会降低或取消使用该源的算法的并行性。

解决这一问题的其中一个方法是,预生成一个包含随机值的大型表,并让每个并行线程向该表生成唯一但确定的目录。 例如,处理一个映像的 OpenCL 内核根据正在处理或生成的像素坐标来计算索引,进而从预生成的表中选择一个条目。

但是,这种方法要求在开始并行算法前,先执行可能会非常耗时的串行 RNG 流程,这可能会限制并行性带来的性能改进。 它还要求在运行并行算法前,了解大概需要使用的随机数个数。 对于需要动态决定每个线程需要使用多少个随机值的并行算法而言,这可能不太适用。

本文“噪声”示例代码中的 OpenCL 内核层函数更适合使用 OpenCL 方法将任务划分为并行操作。

面向 OpenCL 的噪声和随机数生成

OpenCL 使用一维、二维或三维定义了一个全局工作空间(工作项目阵列)。 该全局工作空间内的每个工作项目都有一个唯一的数据集,可根据全局空间内的 x、y 和 z 坐标来确认整数值。

“噪声”示例中的柏林噪声和 RNG 函数可根据最多三个输入值(这可作为每个工作项目的全局 ID)来生成一个随机数或噪声序列。 或者,您也可以结合全局 ID 和内核获取或生成的一些数据值来生成一个或多个值。

例如,以下 OpenCL 内核代码片段展示了根据工作项目的 2D 全局 ID 生成随机数的情况。

kernel void	genRand()
{
	uint	x = get_global_id(0);
	uint	y = get_global_id(1);

	uint	rand_num = ParallelRNG2( x, y );

	...

图 1随机数使用示例 — 二维。

 

这种方法可让随机或噪声函数在不同的工作项目间并行运行,生成的结果拥有可重复的值序列,对工作项目均呈现为“噪声”状态,且在工作项目中为顺序状态。 如果需要生成多个 2D 数值集,则可使用 3D 生成函数,前两个输入根据工作项目的全局 ID 生成,按顺序增加每个所需的新增值的初值来生成第三个维度。 这可延伸为提供多个 3D 随机或噪声值集,如以下柏林噪声示例所示:

kernel void multi2dNoise( float fScale, float offset )
{
float	fX = fScale * get_global_id(0);
float	fY = fScale * get_global_id(1);
float	fZ = offset;

float	randResult = Noise_3d(  fX,  fY,  fZ );
...

图 2柏林噪声使用示例 — 三维。

 

限制

Noise_2d 和 Noise_3d 函数按照相同的基本柏林噪声算法进行计算,但是根据柏林噪声算法的推荐,在实施上会有所差别。 (参见参考资料 1。) 在“噪声”示例中,只有 Noise_3d 实施“噪声”示例,但是 Noise.cl 中包含了针对 Noise_2d 的测试内核,以便读者在修改示例时,可以测试该变量。

Noise_2d 和 Noise_3d 函数应使用浮点输入值来调用。 这些值应限定在一个范围内(如 (0.0, 128.0)),以设置随机值“网格”的尺寸(参见 图 3)。 读者应了解一下示例,以便了解如何将柏林噪声转换为各种“真实自然”的图像。

随机测试中使用的默认 ParallelRNG 函数可提供视觉上随机排列的结果,但并不是最快的 RNG 算法。 此函数基于 “Wang hash”,并非专门用作 RNG 而设计。 但是,当填充 2D 图像,尤其在低阶的二进制结果中,一些常用的 RNG 函数(Noise.cl 文件中包含一个注释掉的示例)会表现出明显的规律。 读者可能想要尝试一些其他更快的 RNG 函数。

默认的 ParallelRNG 函数仅生成无符号 32 位整数结果,如果需要一定范围(如 (0.0, 1.0))内的浮点值,那么应用必须将映射应用到该范围内。 随机示例将无符号的随机整数结果映射到 (0, 255) 的范围内,以生成灰度像素值,仅使用一个 AND 二进制操作选择 8 位。

默认 ParallelRNG 函数在使用以前生成的值进行顺序调用时,不会生成全部的 4,294,967,296 (2^32) 无符号整数值。 对于一个初始种子值而言,伪随机序列/循环最小为 7,000 个唯一值长度,最大约为 20 亿个值的长度。 ParallelRNG 函数默认约生成 20 个不同的周期。 作者认为,一个 OpenCL 内核的工作项目所需的顺序生成随机数多于最小周期可提供的数量的情况不太常见。

2D 和 3D 版函数,即 ParallelRNG2 和 ParallelRNG3,使用混合周期,将上一个调用之间的 XOR 二进制操作应用到 ParallelRNG 和下一个输入值中,这将会改变周期的长度。 但是,改变行为尚未做出具体的归纳,所以建议读者仔细确认 ParallelRNG 函数是否满足应用的需求。

项目结构

这一片段仅列出了示例应用源代码的主要元素。

NoiseMain.cpp:

main()
主要入口点函数。 解析命令行选项后,它将初始化 OpenCL,从 Noise.cl 文件构建 OpenCL 内核程序,准备一个要运行的内核,调用 ExecuteNoiseKernel() 以及 ExecuteNoiseReference()。 在确认两个实施可以生成相同的结果后,main() 可输出每个实施返回的定时信息,并存储每个实施生成的图像。

ExecuteNoiseKernel()
设置并运行所选的采用 OpenCL 的“噪声”内核。

ExecuteNoiseReference()
设置并运行所选择的“噪声”参数 C 代码。

Noise.cl:

defaut_perm[256]
3D 柏林噪声内核随机值 0—255 的表。 注意,这可生成并传递到柏林噪声内核,以提高随机程度。

grads2d[16]
16 个面向 2D 柏林噪声内核均匀间隔的单位向量、梯度。

grads3d[16]
16 个面向 3D 柏林噪声内核的向量梯度。

ParallelRNG()
伪随机数生成器,每个随机数通过 1 个输入传递。 一个可替换的 RNG 函数已经注释掉,以防读者在测试较快的函数时得到较差的结果。

ParallelRNG2()
RNG 针对 2 个输入执行 2 次传递

ParallelRNG3()
RNG 针对 3 个输入执行 3 次传递

weight_poly3()、weight_poly5() 和 WEIGHT()
它们是柏林噪声使用的替换加权函数,可确保所有位置以连续梯度计算。 第二个(首选)函数也可让所有位置以连续二阶导数计算。 WEIGHT 宏选择使用哪一个。

NORM256()
宏将范围 (0, 255) 转换为 (-1.0, 1.0)

interp()
使用一个 OpenCL 构建的双线性插值

hash_grad_dot2()
选择一个梯度,用输入 xy 和部分柏林 Noise_2d 函数进行点积运算。

Noise_2d()
包含 2 个输入的柏林噪声生成器。

hash_grad_dot3()
选择一个梯度,用输入 xyz 和部分柏林 Noise_3d 函数进行点积运算。

Noise_3d()
包含 3 个输入的柏林噪声生成器。

cloud()
使用 Noise_3dCloudTest 生成一个像素的“云”输出图像。

map256()
从柏林噪声输出范围 (-1.0, 1.0) 转换为灰度像素所需的范围 (0, 255)。

CloudTest()
云图像生成测试。 将 slice 参数传递到 cloud,让主机代码生成可替代的云图像。

Noise2dTest()
Noise_2d 的测试 – 默认状态下不使用。

Noise3dTest()
Noise_3d 的测试 – 默认柏林噪声函数。 使用 map256 为灰度图像生成像素值。

RandomTest()
ParallelRNG3 测试,目前使用低阶字节的无符号整数结果输出灰度图像。

针对 Visual Studio 2012 版和 2013 版提供了两个 Microsoft Visual Studio 解决方案文件,  分别是 “Noise_2012.sln” 和 “Noise_2013.sln”。   如果读者有更新的 Visual Studio 版本,应使用 Visual Studio 解决方案或者项目更新根据当前解决方案创建新的解决方案。

注意,两种解决方案都假定已经安装了英特尔® OpenCL™ Code Builder

控制示例

本示例可从 Microsoft Windows* 命令行控制台上通过包含 EXE 文件的文件夹运行:

Noise.exe < Options >

选项:

-h--help
显示命令行帮助。 不会运行任何演示。

-t or --type [ all | cpu | gpu | acc | default |<OpenCL constant for device type>
按设备类型选择运行 OpenCL 内核的设备。 默认值: 全部

<OpenCL constant for device type>

CL_DEVICE_TYPE_ALL | CL_DEVICE_TYPE_CPU | CL_DEVICE_TYPE_GPU |
CL_DEVICE_TYPE_ACCELERATOR | CL_DEVICE_TYPE_DEFAULT

-p or --platform< number-or-string > 
选择要使用的平台。 当运行演示时,可输出一个包含所有平台编号和名称的列表。 所使用的平台将会在右侧输出 “[Selected]”。 如果使用 string,请使用较多的字母作为平台名称,以便将其区别开来。 默认值: 英特尔

-d or --device < number-or-string >
根据设备编号或名称选择运行内核的设备。运行演示时,将会输出正在使用的平台的设备编号和名称。 当前平台将会在右侧输出 “[Selected]”。 默认值: 0

-r or --run [ random | perlin | clouds ]
选择要运行的函数演示。 随机数、柏林噪声或云图像生成器各自拥有自己的演示内核。 默认值: 随机

-s or --seed < integer >
提供整数值,以区分算法输出。 默认值: 1

Noise.exe 可输出 OpenCL 内核和参考 C 编写的等值的参数运行的时间,以及二者输出文件各自的名称。 当程序输出信息完成后,它会等待用户按 ENTER 后再退出。 请注意,我们没有对用 C 语言编写的参考代码函数的性能进行优化;这些函数仅用作验证 OpenCL 内核代码是否正确。

检查结果

Noise.exe运行完成后,在工作文件夹中检查生成的 BMP 格式图像文件 OutputOpenCL.bmpOutputReference.bmp ,分别与 OpenCL 和 C++ 的代码结果进行比较。 两个图像应是相同的,但是两个柏林噪声或云图像之间可能会有少许的差别。

(柏林)噪声输出应如图 3 所示:


图 3柏林噪声输出。

随机输出应与图 4 相似:


图 4随机噪声输出。

函数输出应与图 5 相似:


图 5生成的云输出。

参考资料

  1. Perlin, K., “噪声改进”,http://mrl.nyu.edu/~perlin/paper445.pdf
  2. “4 位整数散列法”,http://burtleburtle.net/bob/hash/integer.html
  3. Overton, M. A.,“快速、高质量的并行随机数生成器”,Dobb 博士的网站(2011 年)。 http://www.drdobbs.com/tools/fast-high-quality-parallel-random-number/229625477
  4. 英特尔® 数字随机数生成器 (DRNG) 库实施和使用,https://software.intel.com/zh-cn/articles/intel-digital-random-number-generator-drng-library-implementation-and-uses?utm_source=Email&utm_medium=IDZ
  5. 英特尔示例源代码许可协议,https://software.intel.com/zh-cn/articles/intel-sample-source-code-license-agreement/
  6. 英特尔® OpenCL™ Code Builderhttps://software.intel.com/zh-cn/opencl-code-builder
有关编译器优化的更完整信息,请参阅优化通知