一直想从头做些图形学的小demo玩玩,之前熟悉了下C++下的Vulkan和Rust的wgpu这几个库。C++太复杂了光是各种特性就要理解好久,编译链接更是头大,决定还是使用Rust。Rust下用的人比较多的图形库就是wgpu了,它甚至可以视为是一个RHI,用了下API挺方便的,文档注释也很全,但它没有对光追做原生支持,最后挑了个相对小众的库Vulkano作为入门。
好了,回到标题上来,先放最终结果:
原理说明
柏林噪声(Perline Noise)是一种常用的平滑的随机噪声,它的主要思想是划分出网格(或者理解成在网格内采样也行),在网格顶点上随机出一个梯度值(Grade),网格内任意的点的值由对这些梯度值进行采样后再进行插值获得,具体的数学原理可以看维基百科。
- 首先要划分网格,二维下通常是正方形。
- 然后对网格顶点进行hash运算,怎么在GPU上又快又好的产生具有哈希性的随机数是这个算法最难的地方了;既可以用查表法也可以用线性同余等均匀的方法。
- 最后是插值,有快速和更好两种,快速s(t)=3t^2−2t^3,更好s(t)=6t^5−15t^4+10t^3
分形噪声则是指在噪声上叠加自相似的噪声达到更多细节的结果。
- 频率(frequencies):影响网格的密集程度,数值越大单位面积下的网格越密集,噪声也有更多变化。
- 振幅(amplitudes):影响噪声相互叠加时的强度。
以一维噪声为例:
上面的6组噪声被称之为噪声的不同倍频(Octave)。随着倍频增大,噪声对于最终叠加噪声的影响程度变小。将上述多个一维噪声叠加得到:
公式可写为:
\sum_{i=0}^{n} =\frac{Noise(2^iPoint)}{2^i}
Vulkano代码
首先是输入部分,使用一个简单的Buffer作为Compute Shader的输入。定义如下:
///作为输入Buffer的结构体
#[derive(BufferContents)]
#[repr(C)]
struct GenNoiseInput {
seed: u32,
size: u32,
frequency: u32,
fbm_time :u32,
}
输出方面使用一张Image来存储Compute Shader的直接输出,但由于图像内部的数据结构比较复杂,不方便直接读取,这里再把它的值拷贝给另一个Buffer传递给CPU这边作为输出。Image和Buffer的创建代码如下:
let image = Image::new(
memory_allocator.clone(),
ImageCreateInfo {
image_type: ImageType::Dim2d,
format: Format::R8G8B8A8_UNORM,
extent: [size, size, 1],
usage: ImageUsage::TRANSFER_DST | ImageUsage::TRANSFER_SRC | ImageUsage::STORAGE,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE,
..Default::default()
},
)
.unwrap();
let view = ImageView::new_default(image.clone()).unwrap();//image view
let output_data: Vec<u8> = vec![0; (size * size * 4) as usize];
let output_buffer = Buffer::from_iter(
memory_allocator.clone(),
BufferCreateInfo {
usage: BufferUsage::TRANSFER_DST,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_HOST | MemoryTypeFilter::HOST_RANDOM_ACCESS,
..Default::default()
},
output_data,
)
.expect("failed to create destination buffer");
接下来就是比较常规的读取shader,创建pipeline,绑定shader、descriptor到pipeline,创建Commend Buffer,等待执行完毕。具体代码可以去Github上查看。链接:https://github.com/YostGray/LearnVulkano/tree/master/computer_pipeline
GLSL Shader代码
shader代码就是之前说的原理的具体实现,这还是我第一次写GLSL,如果有可以优化地方还请各位斧正。
#version 460
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(set = 0, binding = 0) buffer Data {
uint seed;
uint size;
uint frequency;
uint fbm_time;
} buf;
layout(set = 0, binding = 1, rgba8) uniform writeonly image2D img;
//from https://www.shadertoy.com/view/ltB3zD
const float PHI = 1.61803398874989484820459;
float gold_noise(float seed, vec2 pos)
{
float n = fract(tan(distance(pos*PHI, pos) * seed) * pos.x);
return n * 2.0 - 1.0;
}
vec2 get_random_grade(float seed, vec2 pos)
{
//it just works
seed /= 24371;
pos += vec2(0.5,0.5);
float x = gold_noise(seed, pos);
float y = gold_noise(seed + 0.1, pos);
return normalize(vec2(x,y));
}
float perline_noise_lerp(float a,float b,float t)
{
float new_t = 3 * pow(t,2) - 2 * pow(t,3);
return a + new_t * (b - a);
}
void main() {
vec2 pos = vec2(gl_GlobalInvocationID.xy);
// uint block_size = buf.size / buf.frequency;
float result = 0;
float totalScale = 0;
for(int fbm = 0; fbm < buf.fbm_time; fbm++)
{
float frequency = buf.frequency / pow(2,fbm);
float block_size = frequency;
float seed = buf.seed + fbm;
vec2 p0 = pos - mod(pos, block_size);
vec2 p2 = p0 + vec2(block_size, block_size);
vec2 p1 = vec2(p0.x, p2.y);
vec2 p3 = vec2(p2.x, p0.y);
vec2 dir_P0 = (pos - p0) / block_size;
vec2 dir_P1 = (pos - p1) / block_size;
vec2 dir_P2 = (pos - p2) / block_size;
vec2 dir_P3 = (pos - p3) / block_size;
vec2 grade_P0 = get_random_grade(seed, p0);
vec2 grade_P1 = get_random_grade(seed, p1);
vec2 grade_P2 = get_random_grade(seed, p2);
vec2 grade_P3 = get_random_grade(seed, p3);
float v0 = dot(grade_P0, dir_P0);
float v1 = dot(grade_P1, dir_P1);
float v2 = dot(grade_P2, dir_P2);
float v3 = dot(grade_P3, dir_P3);
vec2 t = dir_P0;
float v = perline_noise_lerp (
perline_noise_lerp(v0,v3,t.x),
perline_noise_lerp(v1,v2,t.x),
t.y
);
float scale = pow(0.5,fbm);
result += v * scale;
totalScale += scale;
// vec4 to_write = vec4((grade_P0 + vec2(1,1))/2, 0, 1.0);
// imageStore(img, ivec2(gl_GlobalInvocationID.xy), to_write);
}
result = (result / totalScale + 1.0) / 2.0;
vec4 to_write = vec4(result, result, result, 1.0);
imageStore(img, ivec2(gl_GlobalInvocationID.xy), to_write);
}
其中值得一提的是哈希性随机函数,我在照抄ShaderToy上的实现的时候出现了很多黑块:
经过一番排查我猜测是tan()函数遇到了周期性的π导致的,于是给seed除以了一个素数24371,结果就好了,但没有具体论证过。
最终结果如下:
结论
这其实是为了自己的小项目做地形生成的预备,但bench了一下GPU版本还没有CPU快,可能是计算规模太小了不显著,CPU的版本那边还做了可Tile的修改,简单来说就是右下边缘的随机梯度复用左上的。
噪声作为一个很常用的算法,自己实现一遍也是加深了很多了解,特别是对shader debug的难度有了深刻理解,输出了好多图片用于辅助判断计算是否准确。
Vulkano是一个很好的项目,大大减轻了我使用Vulkan的负担,C++下那一堆一堆的结构体太吓人了。
棒耶!