ComputShader入门:计算分形柏林噪声

一直想从头做些图形学的小demo玩玩,之前熟悉了下C++下的Vulkan和Rust的wgpu这几个库。C++太复杂了光是各种特性就要理解好久,编译链接更是头大,决定还是使用Rust。Rust下用的人比较多的图形库就是wgpu了,它甚至可以视为是一个RHI,用了下API挺方便的,文档注释也很全,但它没有对光追做原生支持,最后挑了个相对小众的库Vulkano作为入门。

好了,回到标题上来,先放最终结果:

原理说明

柏林噪声(Perline Noise)是一种常用的平滑的随机噪声,它的主要思想是划分出网格(或者理解成在网格内采样也行),在网格顶点上随机出一个梯度值(Grade),网格内任意的点的值由对这些梯度值进行采样后再进行插值获得,具体的数学原理可以看维基百科

  1. 首先要划分网格,二维下通常是正方形。
  2. 然后对网格顶点进行hash运算,怎么在GPU上又快又好的产生具有哈希性的随机数是这个算法最难的地方了;既可以用查表法也可以用线性同余等均匀的方法。
  3. 最后是插值,有快速和更好两种,快速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,结果就好了,但没有具体论证过。

最终结果如下:

大小512,频率32,(没有分形)
大小512,频率32,分形3次

结论

这其实是为了自己的小项目做地形生成的预备,但bench了一下GPU版本还没有CPU快,可能是计算规模太小了不显著,CPU的版本那边还做了可Tile的修改,简单来说就是右下边缘的随机梯度复用左上的。

噪声作为一个很常用的算法,自己实现一遍也是加深了很多了解,特别是对shader debug的难度有了深刻理解,输出了好多图片用于辅助判断计算是否准确。

Vulkano是一个很好的项目,大大减轻了我使用Vulkan的负担,C++下那一堆一堆的结构体太吓人了。

参考

  1. 游戏开发中的噪声算法
  2. Gold Noise Uniform Random Static

评论

  1. ZhoyoQTE
    4月前
    2024-5-16 15:06:24

    棒耶!

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇