CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator

CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator

我还没有用过Compute Shader,所以现在把红宝书里的例子拿来了,加入CSharpGL中。

效果图

如下图所示。

或者看视频演示。

下面是红宝书原版的代码效果。

CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator_第1张图片

 

下载

CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)

Compute Shader

Compute Shader的运行与Vertex Shader等不同:它不在pipeline上运行。调用它时用的是这样的命令:

1 void glDispatchCompute(GLuint um_groups_x, Luint num_groups_y, GLuint num_groups_z);

Compute Shader把并行的计算单元看做一个global work group,它下面分为若干个local work group,local work group又分为若干个执行单元。一个执行单元对应一次对Compute Shader的调用。目前我还不知道这种分为2级的设定有什么好处。

CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator_第2张图片

Compute Shader可以像其他Shader一样操作纹理、buffer、原子计数器等资源;它也有一些特有的内置变量(用于获取此执行单元的位置,即属于哪个local work group,是第几个)。

下面通过本文开头的ParticleSimulator的例子来学习一下如何使用Compute Shader。

Particle Simulator

设计

这个例子中,我们用Compute Shader来更新1百万个粒子的位置和速度。为了简单,我们不考虑粒子之间的碰撞问题。

首先分配2个大缓存,一个存放粒子的速度,一个存放粒子的位置。每个时刻里,Compute Shader开始运行,并且每个请求都只处理一个单一的粒子。Compute Shader从缓存中读取当前的速度和位置,计算出新的速度和位置,然后写入缓存中。

然后设置几个引力器,他们都有质量和位置。每个粒子的质量都视作1。每个粒子都受到这些引力器的作用。引力器的位置和质量用一个uniform块保存。

粒子还有生命周期,每次更新时都减少之。少到0时就重置为1,且重置其位置到原点附近。这样模拟过程就能持续进行下去。

模拟粒子的Compute Shader

此Compute Shader如下。

 1 #version 430 core
 2 // 引力器的位置和质量
 3 layout (std140, binding = 0) uniform attractor_block
 4 {
 5     vec4 attractor[64]; // xyz = position, w = mass
 6 };
 7 // 每块中粒子的数量为128
 8 layout (local_size_x = 128) in;
 9 // 使用两个缓存来记录粒子的速度和质量
10 layout (rgba32f, binding = 0) uniform imageBuffer velocity_buffer;
11 layout (rgba32f, binding = 1) uniform imageBuffer position_buffer;
12 // 时间间隔
13 uniform float dt = 1.0;
14 
15 void main(void)
16 {
17     // 读取当前粒子的速度和位置
18     vec4 vel = imageLoad(velocity_buffer, int(gl_GlobalInvocationID.x));
19     vec4 pos = imageLoad(position_buffer, int(gl_GlobalInvocationID.x));
20 
21     int i;
22     // 更新位置和生命周期
23     pos.xyz += vel.xyz * dt;
24     pos.w -= 0.0008 * dt;
25     // 对每个引力器
26     for (i = 0; i < 4; i++)
27 {
28     // 计算受力情况并更新速度
29         vec3 dist = (attractor[i].xyz - pos.xyz);
30         vel.xyz += dt * dt * attractor[i].w * normalize(dist) / (dot(dist, dist) + 10.0);
31     }
32     // 如何粒子过期,重置它
33     if (pos.w <= 0.0)
34     {
35         pos.xyz = -pos.xyz * 0.01;
36         vel.xyz *= 0.01;
37         pos.w += 1.0f;
38     }
39     // 将新的速度和位置保存到缓存
40     imageStore(position_buffer, int(gl_GlobalInvocationID.x), pos);
41     imageStore(velocity_buffer, int(gl_GlobalInvocationID.x), vel);
42 }

初始化

创建2个缓存,把粒子的初始位置放到原点附近,生命周期在0~1之间随机。

 1 protected override void DoInitialize()    
 2 {
 3 {
 4     // 创建 compute shader program
 5         var computeProgram = new ShaderProgram();
 6         var shaderCode = new ShaderCode(File.ReadAllText(@"Shaders\particleSimulator.comp"), ShaderType.ComputeShader);
 7         var shader = shaderCode.CreateShader();
 8         computeProgram.Create(shader);
 9         shader.Delete();
10         this.computeProgram = computeProgram;
11     }
12 {
13         GL.GetDelegateFor<GL.glGenVertexArrays>()(1, render_vao);
14         GL.GetDelegateFor<GL.glBindVertexArray>()(render_vao[0]);
15         // 初始化粒子位置
16         GL.GetDelegateFor<GL.glGenBuffers>()(1, position_buffer);
17         GL.BindBuffer(BufferTarget.ArrayBuffer, position_buffer[0]);
18         var positions = new UnmanagedArray<vec4>(ParticleSimulatorCompute.particleCount);
19         unsafe
20         {
21             var array = (vec4*)positions.FirstElement();
22             for (int i = 0; i < ParticleSimulatorCompute.particleCount; i++)
23             {
24                 array[i] = new vec4(
25                     (float)(random.NextDouble() - 0.5) * 20,
26                     (float)(random.NextDouble() - 0.5) * 20,
27                     (float)(random.NextDouble() - 0.5) * 20,
28                     (float)(random.NextDouble())
29                     );
30             }
31         }
32         GL.BufferData(BufferTarget.ArrayBuffer, positions, BufferUsage.DynamicCopy);
33         positions.Dispose();
34         GL.GetDelegateFor<GL.glVertexAttribPointer>()(0, 4, GL.GL_FLOAT, false, 0, IntPtr.Zero);
35         GL.GetDelegateFor<GL.glEnableVertexAttribArray>()(0);
36         // 初始化粒子速度
37         GL.GetDelegateFor<GL.glGenBuffers>()(1, velocity_buffer);
38         GL.BindBuffer(BufferTarget.ArrayBuffer, velocity_buffer[0]);
39         var velocities = new UnmanagedArray<vec4>(ParticleSimulatorCompute.particleCount);
40         unsafe
41         {
42             var array = (vec4*)velocities.FirstElement();
43             for (int i = 0; i < ParticleSimulatorCompute.particleCount; i++)
44             {
45                 array[i] = new vec4(
46                     (float)(random.NextDouble() - 0.5) * 0.2f,
47                     (float)(random.NextDouble() - 0.5) * 0.2f,
48                     (float)(random.NextDouble() - 0.5) * 0.2f,
49                     0);
50             }
51         }
52         GL.BufferData(BufferTarget.ArrayBuffer, velocities, BufferUsage.DynamicCopy);
53         velocities.Dispose();
54         // 把缓存绑定到TBO
55         GL.GenTextures(1, textureBufferPosition);
56         GL.BindTexture(GL.GL_TEXTURE_BUFFER, textureBufferPosition[0]);
57         GL.GetDelegateFor<GL.glTexBuffer>()(GL.GL_TEXTURE_BUFFER, GL.GL_RGBA32F, position_buffer[0]);
58         GL.GenTextures(1, textureBufferVelocity);
59         GL.BindTexture(GL.GL_TEXTURE_BUFFER, textureBufferVelocity[0]);
60         GL.GetDelegateFor<GL.glTexBuffer>()(GL.GL_TEXTURE_BUFFER, GL.GL_RGBA32F, velocity_buffer[0]);
61 
62         // 初始化引力器
63         GL.GetDelegateFor<GL.glGenBuffers>()(1, attractor_buffer);
64         GL.BindBuffer(BufferTarget.UniformBuffer, attractor_buffer[0]);
65         GL.GetDelegateFor<GL.glBufferData>()(GL.GL_UNIFORM_BUFFER, 64 * Marshal.SizeOf(typeof(vec4)), IntPtr.Zero, GL.GL_DYNAMIC_COPY);
66         GL.GetDelegateFor<GL.glBindBufferBase>()(GL.GL_UNIFORM_BUFFER, 0, attractor_buffer[0]);
67     }
68 {
69     // 初始化渲染器
70         var visualProgram = new ShaderProgram();
71         var shaderCodes = new ShaderCode[2];
72         shaderCodes[0] = new ShaderCode(File.ReadAllText(@"Shaders\particleSimulator.vert"), ShaderType.VertexShader);
73         shaderCodes[1] = new ShaderCode(File.ReadAllText(@"Shaders\particleSimulator.frag"), ShaderType.FragmentShader);
74         var shaders = (from item in shaderCodes select item.CreateShader()).ToArray();
75         visualProgram.Create(shaders);
76         foreach (var item in shaders) { item.Delete(); }
77         this.visualProgram = visualProgram;
78     }
79 }
protected override void DoInitialize()

渲染

渲染循环

渲染过程有3个步骤,首先要更新引力器,然后更新粒子速度和位置,最后渲染出来。

 1 float time = 0.0f;
 2 protected override void DoRender(RenderEventArgs arg)
 3 {
 4     // 更新time和deltaTime
 5     float deltaTime = (float)random.NextDouble() * 5;
 6     time += (float)random.NextDouble() * 5;
 7 
 8     // 更新引力器位置和质量
 9     GL.BindBuffer(BufferTarget.UniformBuffer, attractor_buffer[0]);
10     IntPtr attractors = GL.MapBufferRange(BufferTarget.UniformBuffer,
11         0, 64 * Marshal.SizeOf(typeof(vec4)),
12         MapBufferRangeAccess.MapWriteBit | MapBufferRangeAccess.MapInvalidateBufferBit);
13     unsafe
14     {
15         var array = (vec4*)attractors.ToPointer();
16         for (int i = 0; i < 64; i++)
17         {
18             array[i] = new vec4(
19                 (float)(Math.Sin(time * (float)(i + 4) * 7.5f * 20.0f)) * 50.0f,
20                 (float)(Math.Cos(time * (float)(i + 7) * 3.9f * 20.0f)) * 50.0f,
21                 (float)(Math.Sin(time * (float)(i + 3) * 5.3f * 20.0f))
22                 * (float)(Math.Cos(time * (float)(i + 5) * 9.1f)) * 100.0f,
23                 ParticleSimulatorCompute.attractor_masses[i]);
24         }
25     }
26 
27     GL.UnmapBuffer(BufferTarget.UniformBuffer);
28     GL.BindBuffer(BufferTarget.UniformBuffer, 0);
29 
30     // 激活compute shader,绑定速度和位置缓存
31     computeProgram.Bind();
32     GL.GetDelegateFor<GL.glBindImageTexture>()(0, textureBufferVelocity[0], 0, false, 0, GL.GL_READ_WRITE, GL.GL_RGBA32F);
33     GL.GetDelegateFor<GL.glBindImageTexture>()(1, textureBufferPosition[0], 0, false, 0, GL.GL_READ_WRITE, GL.GL_RGBA32F);
34     // 指定deltaTime
35     computeProgram.SetUniform("dt", deltaTime);
36 // 执行compute shader
37 GL.GetDelegateFor<GL.glDispatchCompute>()(1000000, 1, 1);
38 // 确保compute shader的计算已完成
39 GL.GetDelegateFor<GL.glMemoryBarrier>()(GL.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
40 
41 
42     // 渲染出粒子效果
43     visualProgram.Bind();
44     mat4 view = arg.Camera.GetViewMat4();
45     mat4 projection = arg.Camera.GetProjectionMat4();
46     visualProgram.SetUniformMatrix4("mvp", (projection * view).to_array());
47     GL.GetDelegateFor<GL.glBindVertexArray>()(render_vao[0]);
48     GL.Enable(GL.GL_BLEND);
49     GL.BlendFunc(GL.GL_ONE, GL.GL_ONE);
50     GL.DrawArrays(DrawMode.Points, 0, ParticleSimulatorCompute.particleCount);
51 GL.Disable(GL.GL_BLEND);
52 }
protected override void DoRender(RenderEventArgs arg)

粒子着色程序

渲染粒子的vertex shader很简单。

 1 #version 430 core
 2 
 3 in vec4 position;
 4 
 5 uniform mat4 mvp;
 6 
 7 out float intensity;
 8 
 9 void main(void)
10 {
11     intensity = position.w;//生命周期(0~1)
12     gl_Position = mvp * vec4(position.xyz, 1.0);
13 }

下面是fragment shader。粒子有生命周期,我们就据此赋予其不同的颜色。

 1 #version 430 core
 2 
 3 layout (location = 0) out vec4 color;
 4 
 5 in float intensity;
 6 
 7 void main(void)
 8 {
 9     color = vec4(0.0f, 0.2f, 1.0f, 1.0f) * intensity + vec4(0.2f, 0.05f, 0.0f, 1.0f) * (1.0f - intensity);
10 }

万事俱备,效果就出来了。

总结

原CSharpGL的其他功能(3ds解析器、TTF2Bmp、CSSL等),我将逐步加入新CSharpGL。

欢迎对OpenGL有兴趣的同学关注(https://github.com/bitzhuwei/CSharpGL)

你可能感兴趣的:(CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator)