CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator
CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator
我还没有用过Compute Shader,所以现在把红宝书里的例子拿来了,加入CSharpGL中。
效果图
如下图所示。

或者看视频演示。
下面是红宝书原版的代码效果。

下载
CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
Compute Shader
Compute Shader的运行与Vertex Shader等不同:它不在pipeline上运行。调用它时用的是这样的命令:
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。
Particle Simulator
设计
这个例子中,我们用Compute Shader来更新1百万个粒子的位置和速度。为了简单,我们不考虑粒子之间的碰撞问题。
首先分配2个大缓存,一个存放粒子的速度,一个存放粒子的位置。每个时刻里,Compute Shader开始运行,并且每个请求都只处理一个单一的粒子。Compute Shader从缓存中读取当前的速度和位置,计算出新的速度和位置,然后写入缓存中。
然后设置几个引力器,他们都有质量和位置。每个粒子的质量都视作1。每个粒子都受到这些引力器的作用。引力器的位置和质量用一个uniform块保存。
粒子还有生命周期,每次更新时都减少之。少到0时就重置为1,且重置其位置到原点附近。这样模拟过程就能持续进行下去。
模拟粒子的Compute Shader
此Compute Shader如下。
#version core
// 引力器的位置和质量
layout (std140, binding = ) uniform attractor_block
{
vec4 attractor[]; // xyz = position, w = mass
};
// 每块中粒子的数量为128
layout (local_size_x = ) in;
// 使用两个缓存来记录粒子的速度和质量
layout (rgba32f, binding = ) uniform imageBuffer velocity_buffer;
layout (rgba32f, binding = ) uniform imageBuffer position_buffer;
// 时间间隔
uniform float dt = 1.0; void main(void)
{
// 读取当前粒子的速度和位置
vec4 vel = imageLoad(velocity_buffer, int(gl_GlobalInvocationID.x));
vec4 pos = imageLoad(position_buffer, int(gl_GlobalInvocationID.x)); int i;
// 更新位置和生命周期
pos.xyz += vel.xyz * dt;
pos.w -= 0.0008 * dt;
// 对每个引力器
for (i = ; i < ; i++)
{
// 计算受力情况并更新速度
vec3 dist = (attractor[i].xyz - pos.xyz);
vel.xyz += dt * dt * attractor[i].w * normalize(dist) / (dot(dist, dist) + 10.0);
}
// 如何粒子过期,重置它
if (pos.w <= 0.0)
{
pos.xyz = -pos.xyz * 0.01;
vel.xyz *= 0.01;
pos.w += 1.0f;
}
// 将新的速度和位置保存到缓存
imageStore(position_buffer, int(gl_GlobalInvocationID.x), pos);
imageStore(velocity_buffer, int(gl_GlobalInvocationID.x), vel);
}
初始化
创建2个缓存,把粒子的初始位置放到原点附近,生命周期在0~1之间随机。
protected override void DoInitialize()
{
{
// 创建 compute shader program
var computeProgram = new ShaderProgram();
var shaderCode = new ShaderCode(File.ReadAllText(@"Shaders\particleSimulator.comp"), ShaderType.ComputeShader);
var shader = shaderCode.CreateShader();
computeProgram.Create(shader);
shader.Delete();
this.computeProgram = computeProgram;
}
{
GL.GetDelegateFor<GL.glGenVertexArrays>()(, render_vao);
GL.GetDelegateFor<GL.glBindVertexArray>()(render_vao[]);
// 初始化粒子位置
GL.GetDelegateFor<GL.glGenBuffers>()(, position_buffer);
GL.BindBuffer(BufferTarget.ArrayBuffer, position_buffer[]);
var positions = new UnmanagedArray<vec4>(ParticleSimulatorCompute.particleCount);
unsafe
{
var array = (vec4*)positions.FirstElement();
for (int i = ; i < ParticleSimulatorCompute.particleCount; i++)
{
array[i] = new vec4(
(float)(random.NextDouble() - 0.5) * ,
(float)(random.NextDouble() - 0.5) * ,
(float)(random.NextDouble() - 0.5) * ,
(float)(random.NextDouble())
);
}
}
GL.BufferData(BufferTarget.ArrayBuffer, positions, BufferUsage.DynamicCopy);
positions.Dispose();
GL.GetDelegateFor<GL.glVertexAttribPointer>()(, , GL.GL_FLOAT, false, , IntPtr.Zero);
GL.GetDelegateFor<GL.glEnableVertexAttribArray>()();
// 初始化粒子速度
GL.GetDelegateFor<GL.glGenBuffers>()(, velocity_buffer);
GL.BindBuffer(BufferTarget.ArrayBuffer, velocity_buffer[]);
var velocities = new UnmanagedArray<vec4>(ParticleSimulatorCompute.particleCount);
unsafe
{
var array = (vec4*)velocities.FirstElement();
for (int i = ; i < ParticleSimulatorCompute.particleCount; i++)
{
array[i] = new vec4(
(float)(random.NextDouble() - 0.5) * 0.2f,
(float)(random.NextDouble() - 0.5) * 0.2f,
(float)(random.NextDouble() - 0.5) * 0.2f,
);
}
}
GL.BufferData(BufferTarget.ArrayBuffer, velocities, BufferUsage.DynamicCopy);
velocities.Dispose();
// 把缓存绑定到TBO
GL.GenTextures(, textureBufferPosition);
GL.BindTexture(GL.GL_TEXTURE_BUFFER, textureBufferPosition[]);
GL.GetDelegateFor<GL.glTexBuffer>()(GL.GL_TEXTURE_BUFFER, GL.GL_RGBA32F, position_buffer[]);
GL.GenTextures(, textureBufferVelocity);
GL.BindTexture(GL.GL_TEXTURE_BUFFER, textureBufferVelocity[]);
GL.GetDelegateFor<GL.glTexBuffer>()(GL.GL_TEXTURE_BUFFER, GL.GL_RGBA32F, velocity_buffer[]); // 初始化引力器
GL.GetDelegateFor<GL.glGenBuffers>()(, attractor_buffer);
GL.BindBuffer(BufferTarget.UniformBuffer, attractor_buffer[]);
GL.GetDelegateFor<GL.glBufferData>()(GL.GL_UNIFORM_BUFFER, * Marshal.SizeOf(typeof(vec4)), IntPtr.Zero, GL.GL_DYNAMIC_COPY);
GL.GetDelegateFor<GL.glBindBufferBase>()(GL.GL_UNIFORM_BUFFER, , attractor_buffer[]);
}
{
// 初始化渲染器
var visualProgram = new ShaderProgram();
var shaderCodes = new ShaderCode[];
shaderCodes[] = new ShaderCode(File.ReadAllText(@"Shaders\particleSimulator.vert"), ShaderType.VertexShader);
shaderCodes[] = new ShaderCode(File.ReadAllText(@"Shaders\particleSimulator.frag"), ShaderType.FragmentShader);
var shaders = (from item in shaderCodes select item.CreateShader()).ToArray();
visualProgram.Create(shaders);
foreach (var item in shaders) { item.Delete(); }
this.visualProgram = visualProgram;
}
}
protected override void DoInitialize()
渲染
渲染循环
渲染过程有3个步骤,首先要更新引力器,然后更新粒子速度和位置,最后渲染出来。
float time = 0.0f;
protected override void DoRender(RenderEventArgs arg)
{
// 更新time和deltaTime
float deltaTime = (float)random.NextDouble() * ;
time += (float)random.NextDouble() * ; // 更新引力器位置和质量
GL.BindBuffer(BufferTarget.UniformBuffer, attractor_buffer[]);
IntPtr attractors = GL.MapBufferRange(BufferTarget.UniformBuffer,
, * Marshal.SizeOf(typeof(vec4)),
MapBufferRangeAccess.MapWriteBit | MapBufferRangeAccess.MapInvalidateBufferBit);
unsafe
{
var array = (vec4*)attractors.ToPointer();
for (int i = ; i < ; i++)
{
array[i] = new vec4(
(float)(Math.Sin(time * (float)(i + ) * 7.5f * 20.0f)) * 50.0f,
(float)(Math.Cos(time * (float)(i + ) * 3.9f * 20.0f)) * 50.0f,
(float)(Math.Sin(time * (float)(i + ) * 5.3f * 20.0f))
* (float)(Math.Cos(time * (float)(i + ) * 9.1f)) * 100.0f,
ParticleSimulatorCompute.attractor_masses[i]);
}
} GL.UnmapBuffer(BufferTarget.UniformBuffer);
GL.BindBuffer(BufferTarget.UniformBuffer, ); // 激活compute shader,绑定速度和位置缓存
computeProgram.Bind();
GL.GetDelegateFor<GL.glBindImageTexture>()(, textureBufferVelocity[], , false, , GL.GL_READ_WRITE, GL.GL_RGBA32F);
GL.GetDelegateFor<GL.glBindImageTexture>()(, textureBufferPosition[], , false, , GL.GL_READ_WRITE, GL.GL_RGBA32F);
// 指定deltaTime
computeProgram.SetUniform("dt", deltaTime);
// 执行compute shader
GL.GetDelegateFor<GL.glDispatchCompute>()(1000000, , );
// 确保compute shader的计算已完成
GL.GetDelegateFor<GL.glMemoryBarrier>()(GL.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); // 渲染出粒子效果
visualProgram.Bind();
mat4 view = arg.Camera.GetViewMat4();
mat4 projection = arg.Camera.GetProjectionMat4();
visualProgram.SetUniformMatrix4("mvp", (projection * view).to_array());
GL.GetDelegateFor<GL.glBindVertexArray>()(render_vao[]);
GL.Enable(GL.GL_BLEND);
GL.BlendFunc(GL.GL_ONE, GL.GL_ONE);
GL.DrawArrays(DrawMode.Points, , ParticleSimulatorCompute.particleCount);
GL.Disable(GL.GL_BLEND);
}
protected override void DoRender(RenderEventArgs arg)
粒子着色程序
渲染粒子的vertex shader很简单。
#version core in vec4 position; uniform mat4 mvp; out float intensity; void main(void)
{
intensity = position.w;//生命周期(0~1)
gl_Position = mvp * vec4(position.xyz, 1.0);
}
下面是fragment shader。粒子有生命周期,我们就据此赋予其不同的颜色。
#version core layout (location = ) out vec4 color; in float intensity; void main(void)
{
color = vec4(0.0f, 0.2f, 1.0f, 1.0f) * intensity + vec4(0.2f, 0.05f, 0.0f, 1.0f) * (1.0f - intensity);
}
万事俱备,效果就出来了。
2016-05-17
尝试新的粒子运动方式,效果如图。

所需的compute shader代码如下。
#version core layout (std140, binding = ) uniform attractor_block
{
vec4 attractor[]; // xyz = position, w = mass
}; layout (local_size_x = ) in; layout (rgba32f, binding = ) uniform imageBuffer velocity_buffer;
layout (rgba32f, binding = ) uniform imageBuffer position_buffer; uniform float dt = 1.0; void main(void)
{
vec4 vel = imageLoad(velocity_buffer, int(gl_GlobalInvocationID.x));
vec4 pos = imageLoad(position_buffer, int(gl_GlobalInvocationID.x)); int i;
float factor = 0.05f;
pos.xyz += vel.xyz * dt;
pos.w -= factor * 0.025f * dt; vel.xyz += vec3(, factor * -0.02f, ); if (pos.w <= 0.0)
{
pos.xyz = -pos.xyz * 0.01;
vel.x = factor * sin(gl_GlobalInvocationID.x);
vel.y = factor * 3f;
vel.z = factor * cos(gl_GlobalInvocationID.x);
pos.w += 1.0f;
} imageStore(position_buffer, int(gl_GlobalInvocationID.x), pos);
imageStore(velocity_buffer, int(gl_GlobalInvocationID.x), vel);
}
fountain effect
总结
原CSharpGL的其他功能(3ds解析器、TTF2Bmp、CSSL等),我将逐步加入新CSharpGL。
欢迎对OpenGL有兴趣的同学关注(https://github.com/bitzhuwei/CSharpGL)
CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator的更多相关文章
- CSharpGL(24)用ComputeShader实现一个简单的图像边缘检测功能
CSharpGL(24)用ComputeShader实现一个简单的图像边缘检测功能 效果图 这是红宝书里的例子,在这个例子中,下述功能全部登场,因此这个例子可作为使用Compute Shader的典型 ...
- 一个简单的CSS示例
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8" /> 5 & ...
- 应用OpenMP的一个简单的设计模式
小喵的唠叨话:最近很久没写博客了,一是因为之前写的LSoftmax后馈一直没有成功,所以在等作者的源码.二是最近没什么想写的东西.前两天,在预处理图片的时候,发现处理200w张图片,跑了一晚上也才处理 ...
- 《Entity Framework 6 Recipes》翻译系列 (3) -----第二章 实体数据建模基础之创建一个简单的模型
第二章 实体数据建模基础 很有可能,你才开始探索实体框架,你可能会问“我们怎么开始?”,如果你真是这样的话,那么本章就是一个很好的开始.如果不是,你已经建模,并在实体分裂和继承方面感觉良好,那么你可以 ...
- 通过Knockout.js + ASP.NET Web API构建一个简单的CRUD应用
REFERENCE FROM : http://www.cnblogs.com/artech/archive/2012/07/04/Knockout-web-api.html 较之面向最终消费者的网站 ...
- [stm32] 一个简单的stm32vet6驱动的天马4线SPI-1.77寸LCD彩屏DEMO
书接上文<1.一个简单的nRF51822驱动的天马4线SPI-1.77寸LCD彩屏DEMO> 我们发现用16MHz晶振的nRF51822驱动1.77寸的spi速度达不到要求 本节主要采用7 ...
- ios开发UI篇—使用纯代码自定义UItableviewcell实现一个简单的微博界面布局
本文转自 :http://www.cnblogs.com/wendingding/p/3761730.html ios开发UI篇—使用纯代码自定义UItableviewcell实现一个简单的微博界面布 ...
- 一个简单的Servlet容器实现
上篇写了一个简单的Java web服务器实现,只能处理一些静态资源的请求,本篇文章实现的Servlet容器基于前面的服务器做了个小改造,增加了Servlet请求的处理. 程序执行步骤 创建一个Serv ...
- 一个简单的Java web服务器实现
前言 一个简单的Java web服务器实现,比较简单,基于java.net.Socket和java.net.ServerSocket实现: 程序执行步骤 创建一个ServerSocket对象: 调用S ...
随机推荐
- Unity3d学习 相机的跟随
最近在写关于相机跟随的逻辑,其实最早接触相机跟随是在Unity官网的一个叫Roll-a-ball tutorial上,其中简单的涉及了关于相机如何跟随物体的移动而移动,如下代码: using Unit ...
- VisualVM通过jstatd方式远程监控远程主机
配置好权限文件 [root@test bin]# cd $JAVA_HOME/bin [root@test bin]# vim jstatd.all.policy grant codebase &qu ...
- ES5对Array增强的9个API
为了更方便的对Array进行操作,ES5规范在Array的原型上新增了9个方法,分别是forEach.filter.map.reduce.reduceRight.some.every.indexOf ...
- 使用 JavaScriptService 在.NET Core 里实现DES加密算法
文章<ASP.NET Core love JavaScript>和<跨平台的 NodeJS 组件解决 .NetCore 不支持 System.Drawing图形功能的若干问题> ...
- macOS 我的装机
最近多次配置 Mac 的开发环境,稍微记录一下 1 创建无付费信息的Apple ID 2 Xcode gem 源更改 3 Alfred 4 微信 5 SourceTree 6 Sublime Te ...
- PHP代码优化
1 代码优化 1 尽量静态化 如果一个方法能被静态,那就声明它为静态的,速度可提高1/4,甚至我测试的时候,这个提高了近三倍. 当然了,这个测试方法需要在十万级以上次执行,效果才明显. 其实静态方法和 ...
- [原]Cachedb 网络模块文档
Cachedb 网络模块文档 整体结构 多路复用 (epoll 模块) 事件驱动 (事件封装) 缓冲管理 (上层buffer管理) 设计思想 层次化的设计,每一个模块只调用上一个模块的接口,并将耦合聚 ...
- ASP.NET SignaiR 实现消息的即时推送,并使用Push.js实现通知
一.使用背景 1. SignalR是什么? ASP.NET SignalR 是为 ASP.NET 开发人员提供的一个库,可以简化开发人员将实时 Web 功能添加到应用程序的过程.实时 Web 功能是指 ...
- 将DataTable中的某列转换成数组或者List
string[] arrRate = dtRate.AsEnumerable().Select(d => d.Field<string>("arry")).ToA ...
- Linux测试环境搭建的学习建议
随着Linux应用的扩展许多朋友开始接触Linux,根据学习Windwos的经验往往有一些茫然的感觉:不知从何处开始学起.这里介绍学习Linux测试环境搭建的一些建议. 一.Linux测试环境搭建从基 ...