在Godot里初尝计算着色器
2024-03-25
发生了啥事?
出于某些原因,我需要风格化处理我的场景。
戈多先前合并的一个pr实现了渲染Hook(虽然我也不知道是什么东西,但是它似乎有办法让我使用自定义的渲染管线),为此我阅读了官方的示例,发现需要用到一个叫做计算着色器的东西。
这是那个实现了Render Hooks的PR:
https://github.com/godotengine/godot/pull/80214
Godot的compute shader需要在godot editor外编写。这点与内置的GDShader不一样,你需要外部编辑器才可以编写。我推荐使用Visual Studio Code进行编写。
你需要自行寻找能够对GLSL有着支持的插件。绝大多数插件对GLSL的支持不能说很烂,只能说约等于没有。如果你需要GLSL内置语法的自动补全的话,我的一位群友推荐了这个插件:Glsl Analyzer 你只要在VSCode内搜索并寻找这个插件,安装即可。你可能还需要安装一些外部依赖程序。顺带一题,该插件不支持格式化与代码高亮。你可以尝试安装其他插件来实现高亮。
计算着色器和你平时使用的片元/顶点着色器有些许不同,所以有些基础概念你需要理解:
计算着色器独立于戈多的渲染管线之外,这代表它不参与片段/顶点着色器所做的过程。
着色器可以拿来对付那些需要大规模并行计算的东西,比如你的屏幕。假设你有一块1920*1080,刷新率为60Hz的屏幕,那么你将需要每秒渲染124416000个像素点!你的CPU顶多有16个左右的线程,这对于渲染上述数量级的像素而言有些捉襟见肘。而你得GPU有成千上万个可同时并发运行的线程,用于渲染屏幕再合适不过了。
计算着色器的作用,就是让你能够发挥这成千上万个线程的力量,去做一些CPU做起来很吃力的事情,比如渲染,或者为屏幕增加后处理效果之类的。虽然GPU很快,但是GPU很蠢,需要CPU告诉它该做什么。
计算着色器运行在GPU上,这代表它是“高度并行”的,它的输入输出需要手动指定(废话)。如果想调用,你需要在GDS代码里手动指定调用多少个“Workgroup”。
你可以将“Workgroup” 看作一个三维数组,它的大小在GDScript里指定; 将"Invocation"看作另一个三维数组,它的大小在GLSL里定义。
一般来说,我们会希望Invocation的规模是64的倍数,这对某些硬件有利。
每个Workgroup, 以及每个Workgroup内的Invocation都是独立并行运行的,它们互不干扰,且完成顺序完全不可预测。
Workgroup的数量需要在gdscript里指定,是可变的;而每个workgroup内的invocation数量是固定的,不可以修改。
尽管你无法判断Invocation的执行顺序,但你可以通过gl_GlobalInvocationID来获取一个Invocation的次序.在每个着色器中,gl_GlobalInvocationID是全局唯一的,可以拿来判断是第几个元素。
简单的实例
我编写了一个最简单的计算着色器。 它接受一个数组的输入,对数组的每个元素乘2,输出。我使用了两个缓冲区:分别容纳输入元素、输出元素。其实官方示例使用的是一个缓冲区,如果只使用一个缓冲区,也是没有问题的。
#[compute]
#version 450
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in; // 定义每个WorkGroup内的WorkItem的结构。 这里,每个workgroup对应一个workitem.
layout (set=0, binding=0, std430) restrict readonly buffer InputBuffer {
float input_array[];
}
input_buffer; // 输入
layout (set=0, binding=1, std430) restrict writeonly buffer OutBuffer {
float result_array[];
}
out_buffer; // 输出数组
void main(){
uint id = gl_GlobalInvocationID.x;
out_buffer.result_array[id] = input_buffer.input_array[id] * 3.0;
return;
}
我通过下列代码调用我的着色器:
func _ready() -> void:
# 创建本地渲染设备
var rd:= RenderingServer.create_local_rendering_device()
# 准备shader文件
var shader_file := preload("res://ComputeShader/CSSelfLearn/main.glsl")
var shader_spirv := shader_file.get_spirv()
var shader : RID = rd.shader_create_from_spirv(shader_spirv)
# 输入缓冲区
var input_buffer_data := PackedFloat32Array([1,2,3,4,5,6])
var input_buffer_bytes := input_buffer_data.to_byte_array()
var buffer_size := input_buffer_bytes.size()
var input_buffer :RID= rd.storage_buffer_create(buffer_size, input_buffer_bytes)
var output_buffer :RID= rd.storage_buffer_create(buffer_size, input_buffer_bytes)
var uniform1 : RDUniform = RDUniform.new()
uniform1.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
uniform1.binding = 0
uniform1.add_id(input_buffer)
var uniform2 : RDUniform = RDUniform.new()
uniform2.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
uniform2.binding = 1
uniform2.add_id(output_buffer)
var uniform_set := rd.uniform_set_create([uniform1, uniform2], shader, 0)
var pipeline := rd.compute_pipeline_create(shader)
var compute_list := rd.compute_list_begin()
rd.compute_list_bind_uniform_set(compute_list,uniform_set,0)
rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
rd.compute_list_dispatch(compute_list, 3,1,1)
rd.compute_list_end()
rd.submit()
rd.sync()
# 读取
var output_bytes := rd.buffer_get_data(output_buffer)
var output := output_bytes.to_float32_array()
print(output)
如上。