CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator
我还没有用过Compute Shader,所以现在把红宝书里的例子拿来了,加入CSharpGL中。
如下图所示。
或者看视频演示。
下面是红宝书原版的代码效果。
CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
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级的设定有什么好处。
Compute Shader可以像其他Shader一样操作纹理、buffer、原子计数器等资源;它也有一些特有的内置变量(用于获取此执行单元的位置,即属于哪个local work group,是第几个)。
下面通过本文开头的ParticleSimulator的例子来学习一下如何使用Compute Shader。
这个例子中,我们用Compute Shader来更新1百万个粒子的位置和速度。为了简单,我们不考虑粒子之间的碰撞问题。
首先分配2个大缓存,一个存放粒子的速度,一个存放粒子的位置。每个时刻里,Compute Shader开始运行,并且每个请求都只处理一个单一的粒子。Compute Shader从缓存中读取当前的速度和位置,计算出新的速度和位置,然后写入缓存中。
然后设置几个引力器,他们都有质量和位置。每个粒子的质量都视作1。每个粒子都受到这些引力器的作用。引力器的位置和质量用一个uniform块保存。
粒子还有生命周期,每次更新时都减少之。少到0时就重置为1,且重置其位置到原点附近。这样模拟过程就能持续进行下去。
此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 }
渲染过程有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 }
渲染粒子的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)