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的更多相关文章

  1. CSharpGL(24)用ComputeShader实现一个简单的图像边缘检测功能

    CSharpGL(24)用ComputeShader实现一个简单的图像边缘检测功能 效果图 这是红宝书里的例子,在这个例子中,下述功能全部登场,因此这个例子可作为使用Compute Shader的典型 ...

  2. 一个简单的CSS示例

    1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8" /> 5 & ...

  3. 应用OpenMP的一个简单的设计模式

    小喵的唠叨话:最近很久没写博客了,一是因为之前写的LSoftmax后馈一直没有成功,所以在等作者的源码.二是最近没什么想写的东西.前两天,在预处理图片的时候,发现处理200w张图片,跑了一晚上也才处理 ...

  4. 《Entity Framework 6 Recipes》翻译系列 (3) -----第二章 实体数据建模基础之创建一个简单的模型

    第二章 实体数据建模基础 很有可能,你才开始探索实体框架,你可能会问“我们怎么开始?”,如果你真是这样的话,那么本章就是一个很好的开始.如果不是,你已经建模,并在实体分裂和继承方面感觉良好,那么你可以 ...

  5. 通过Knockout.js + ASP.NET Web API构建一个简单的CRUD应用

    REFERENCE FROM : http://www.cnblogs.com/artech/archive/2012/07/04/Knockout-web-api.html 较之面向最终消费者的网站 ...

  6. [stm32] 一个简单的stm32vet6驱动的天马4线SPI-1.77寸LCD彩屏DEMO

    书接上文<1.一个简单的nRF51822驱动的天马4线SPI-1.77寸LCD彩屏DEMO> 我们发现用16MHz晶振的nRF51822驱动1.77寸的spi速度达不到要求 本节主要采用7 ...

  7. ios开发UI篇—使用纯代码自定义UItableviewcell实现一个简单的微博界面布局

    本文转自 :http://www.cnblogs.com/wendingding/p/3761730.html ios开发UI篇—使用纯代码自定义UItableviewcell实现一个简单的微博界面布 ...

  8. 一个简单的Servlet容器实现

    上篇写了一个简单的Java web服务器实现,只能处理一些静态资源的请求,本篇文章实现的Servlet容器基于前面的服务器做了个小改造,增加了Servlet请求的处理. 程序执行步骤 创建一个Serv ...

  9. 一个简单的Java web服务器实现

    前言 一个简单的Java web服务器实现,比较简单,基于java.net.Socket和java.net.ServerSocket实现: 程序执行步骤 创建一个ServerSocket对象: 调用S ...

随机推荐

  1. TypeScript: Angular 2 的秘密武器(译)

    本文整理自Dan Wahlin在ng-conf上的talk.原视频地址: https://www.youtube.com/watch?v=e3djIqAGqZo 开场白 开场白主要分为三部分: 感谢了 ...

  2. 一个免费的、跨平台的、开源音频编辑器Audacity

    Audacity 是一个免费的开源程序,用于编辑音频录制.它可在多个平台(windows/linux)上运行.Audacity 基于 GUI,是一个具有多种选项的强大程序.它支持您录制各种类型的声音. ...

  3. 【原】AFNetworking源码阅读(三)

    [原]AFNetworking源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇的话,主要是讲了如何通过构建一个request来生成一个data tas ...

  4. Android数据加密之Base64编码算法

    前言: 前面学习总结了平时开发中遇见的各种数据加密方式,最终都会对加密后的二进制数据进行Base64编码,起到一种二次加密的效果,其实呢Base64从严格意义上来说的话不是一种加密算法,而是一种编码算 ...

  5. dubbox微服务实例及引发的“血案”

    Dubbo 是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成. 主要核心部件: Remoting: 网络通信框架 ...

  6. 步入angularjs directive(指令)--点击按钮加入loading状态

    今天我终于鼓起勇气写自己的博客了,激动与害怕并存,希望大家能多多批评指导,如果能够帮助大家,也希望大家点个赞!! 用angularjs 工作也有段时间了,总体感觉最有挑战性的还是指令,因为没有指令的a ...

  7. Oracle学习之路-- 案例分析实现行列转换的几种方式

    注:本文使用的数据库表为oracle自带scott用户下的emp,dept等表结构. 通过一个例子来说明行列转换: 需求:查询每个部门中各个职位的总工资 按我们最原始的思路可能会这么写:       ...

  8. springmvc+bootstrap+jquerymobile完整搭建案例(提供下载地址)

    用一张简单的截图说明下,然后提供一个下载地址. bootstrap的大部分样式官方都是写好的,所以只需要class="官方样式即可",具体可以看官方的案例,下面来个地址 http: ...

  9. Android开发学习—— Fragment

    #Fragment* 用途:在一个Activity里切换界面,切换界面时只切换Fragment里面的内容* 生命周期方法跟Activity一致,可以理解把其为就是一个Activity* 定义布局文件作 ...

  10. Linux系统中的Device Mapper学习

    在linux系统中你使用一些命令时(例如nmon.iostat 如下截图所示),有可能会看到一些名字为dm-xx的设备,那么这些设备到底是什么设备呢,跟磁盘有什么关系呢?以前不了解的时候,我也很纳闷. ...