0x00 前言

在今年6月的Unite Europe 2017大会上 Unity 的CTO Joachim Ante演示了未来Unity新的编程特性——C# Job系统,它提供了编写多线程代码的一种既简单又安全的方法。Joachim通过一个大规模群落行为仿真的演示,向我们展现了最新的Job系统是如何充分利用CPU多核架构的优势来提升性能的。

但是吸引我的并非是C# Job如何利用多线程实现性能的提升,相反,吸引我的是如何在现在还没有C# Job系统的Unity中实现类似的效果。



在Ante的session中,他的演示主要是利用多核CPU提高计算效率来实现大群体行为。那么我就来演示一下,如何利用GPU来实现类似的目标吧。利用GPU做一些非渲染的计算也被称为GPGPU——General-purpose computing on graphics processing units,图形处理器通用计算。

0x01 CPU的限制

为何Joachim 要用这种大规模群落行为的仿真来宣传Unity的新系统呢?

其实相对来说复杂的并非逻辑,这里的关键词是“大规模”——在他的演示中,实现了20,000个boid的群体效果,而更牛逼的是帧率保持在了40fps上下。

事实上自然界中的这种群体行为并不罕见,例如大规模的鸟群,大规模的鱼群。

在搜集资料的时候,我还发现了一位优秀的水下摄影师、加利福尼亚海湾海洋计划总监octavio aburto的个人网站上的一些让人惊叹的作品。



图片来自Octavio Aburto



图片来自Octavio Aburto

而要在计算机上模拟出这种自然界的现象,乍看上去似乎十分复杂,但实际上却并非如此。

查阅资料,可以发现早在1986年就由Craig Reynolds提出了一个逻辑简单,而效果很赞的群体仿真模型——而作为这个群体内的个体的专有名词boid(bird-oid object,类鸟物)也是他提出的。

简单来说,一个群体内的个体包括3种基本的行为:

  • Separation:顾名思义,该个体用来规避周围个体的行为。

  • Alignment:作为一个群体,要有一个大致统一的前进方向。因此作为群体中的某个个体,可以根据自己周围的同伴的前进方向获取一个前进方向。

  • Cohesion:同样,作为一个群体肯定要有一个向心力。否则队伍四散奔走就不好玩了,因此每个个体就可以根据自己周围同伴的位置信息获取一个向中心聚拢的方向。

以上三种行为需要同时加以考虑,才有可能模拟出一个接近真实的效果。

    Vector3 direction = separation+ alignment + (cohesion - boid.position).normalized;

可以看出,这里的逻辑并不复杂,但是麻烦的问题在于实现这套逻辑的前提是每个个体boid都需要获取自己周围的同伴信息。

因此最简单也最通用的方式就是每个boid都要和群落中的所有boid比较位置信息,获取二者之间的距离,如果小于阈值则判定是自己周围的同伴。而这种比较的时间复杂度显然是O(\(n^2\))。因此,当群体是由几百个个体组成时,直接在cpu上计算时的表现还是可以接受的。但是数量一旦继续上升,效果就很难保证了。

当然,在Unity中我们还可以利用它的物理组件来获取一个boid个体周围的同伴信息:

Physics.OverlapSphere(Vector3 position, float radius, int layerMask);

这个方法会返回和自己重叠的对象列表,由于unity使用了空间划分的机制,所以这种方式的性能要好于直接比较n个boid之间的距离。

但是即便如此,cpu的计算能力仍然是一个瓶颈。随着群体个体数量的上升,性能也会快速的下降。

0x02 GPU的优势

既然限制的瓶颈在于CPU面对大规模个体时的计算能力的不足,那么一个自然的想法就是将这部分计算转移到更擅长大规模计算的GPU上来进行。

CPU的结构复杂,主要完成逻辑控制和缓存功能,运算单元较少。与CPU相比,GPU的设计目的是尽可能的快速完成图像处理,通过简化逻辑控制并增加运算单元实现了高性能的并行计算。



利用GPU的超强计算能力来实现一些渲染之外的功能并非一个新的概念,早在十年前nvidia就为GPU引入了一个易用的编程接口,即CUDA统一计算架构,之后微软推出了DirectCompute——它随DirectX 11一同发布。

和常见的vertex shader和fragment shader类似,要在GPU运行我们自己设定的逻辑也需要通过shader,不过和传统的shader的不同之处在于,compute shader并非传统的渲染流水线中的一个阶段,相反它主要用来计算原本由CPU处理的通用计算任务,这些通用计算常常与图形处理没有任何关系,因此这种方式也被称为GPGPU——General-purpose computing on graphics processing units,图形处理器通用计算。

利用这些功能,之前由CPU来实现的计算就可以转移到计算能力更强大的GPU上来进行了,比如物理计算、AI等等。

而Unity的Compute Shader十分接近DirectCompute,最初Unity引入Compute Shader时仅仅支持DirectX 11,不过目前的版本已经支持别的图形API了。详情可以参考:Unity - Manual: Compute shaders

在Unity中我们可以很方便的创建一个Compute Shader,一个Unity创建的默认Compute Shader如下所示:

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain // Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> Result; [numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
// TODO: insert actual code here! Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}

这里我先简单的介绍一下这个Compute Shader中的相关概念,首先在这里我们指明了这个shader的入口函数。

#pragma kernel CSMain

之后,声明了在compute shader中操作的数据。

RWTexture2D<float4> Result;

这里使用的是RWTexture2D,而我们更常用的是RWStructuredBuffer(RW在这里表示可读写)。

之后是很关键的一行:

[numthreads(8,8,1)]

这里首先要说一下Compute Shader执行的线程模型。DirectCompute将并行计算的问题分解成了多个线程组,每个线程组内又包含了多个线程。

[numthreads(8,8,1)]的意思是在这个线程组中分配了8x8x1=64个线程,当然我们也可以直接使用

[numthreads(64,1,1)]

因为三维线程模型主要是为了方便某些使用情景,和性能关系不大,硬件在执行时仍然是把所有线程当做一维的。

至此,我们已经在shader中确定了每个线程组内包括几个线程,但是我们还没有分配线程组,也没有开始执行这个shader。

和一般的shader不同,compute shader和图形无关,因此在使用compute shader时不会涉及到mesh、material这些内容。相反,compute shader的设置和执行要在c#脚本中进行。

 this.kernelHandle = cshader.FindKernel("CSMain");
......
cshader.SetBuffer(this.kernelHandle, "boidBuffer", buffer);
......
cshader.Dispatch(this.kernelHandle, this.boidsCount, 1, 1);
buffer.GetData(this.boidsData);
......

在c#脚本中准备、传送数据,分配线程组并执行compute shader,最后数据再从GPU传递回CPU。

不过,这里有一个问题需要说明。虽然现在将计算转移到GPU后计算能力已经不再是瓶颈,但是数据的转移此时变成了首要的限制因素。而且在Dispatch之后直接调用GetData可能会造成CPU的阻塞。因为CPU此时需要等待GPU计算完毕并将数据传递回CPU,所以希望日后Unity能够提供一个异步版本的GetData。

最后将行为模拟的逻辑从CPU转移到GPU之后,模拟10,000个boid组成的大群组在我的笔记本上已经能跑在30FPS上下了。

完整的项目可以到这里到这里下载:

chenjd/Unity-Boids-Behavior-on-GPGPU

ref:

【1】wikipedia-Boids

【2】Craig Reynolds

【3】Compute Shader Overview

【4】Compute shaders

各位如果觉得有趣的话,欢迎点个赞。

-EOF-

最后打个广告,欢迎支持我的书《Unity 3D脚本编程》

欢迎大家关注我的公众号慕容的游戏编程:chenjd01

利用GPGPU计算大规模群落仿真行为的更多相关文章

  1. 利用sklearn计算文本相似性

    利用sklearn计算文本相似性,并将文本之间的相似度矩阵保存到文件当中.这里提取文本TF-IDF特征值进行文本的相似性计算. #!/usr/bin/python # -*- coding: utf- ...

  2. 利用Python计算π的值,并显示进度条

    利用Python计算π的值,并显示进度条  第一步:下载tqdm 第二步;编写代码 from math import * from tqdm import tqdm from time import ...

  3. Ajax实例一:利用服务器计算

    Ajax实例一:利用服务器计算 HTML代码 //输入两个数 <input id="number1" type="number"> <inpu ...

  4. 利用函数计算构建微信小程序的Server端

    10分钟上线 - 利用函数计算构建微信小程序的Server端-博客-云栖社区-阿里云 https://yq.aliyun.com/articles/435430 函数计算  读写 oss import ...

  5. 【改革春风吹满地 HDU - 2036 】【计算几何-----利用叉积计算多边形的面积】

    利用叉积计算多边形的面积 我们都知道计算三角形的面积时可以用两个邻边对应向量积(叉积)的绝对值的一半表示,那么同样,对于多边形,我们可以以多边形上的一个点为源点,作过该点并且过多边形其他点中的某一个的 ...

  6. EDU 50 E. Covered Points 利用克莱姆法则计算线段交点

    E. Covered Points 利用克莱姆法则计算线段交点.n^2枚举,最后把个数开方,从ans中减去. ans加上每个线段的定点数, 定点数用gcs(△x , △y)+1计算. #include ...

  7. MergeSort归并排序和利用归并排序计算出数组中的逆序对

    首先先上LeetCode今天的每日一题(面试题51. 数组中的逆序对): 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这个数组中的逆序对的总数. ...

  8. 利用GPU实现大规模动画角色的渲染(转)

    原文: https://www.cnblogs.com/murongxiaopifu/p/7250772.html 利用GPU实现大规模动画角色的渲染 0x00 前言 我想很多开发游戏的小伙伴都希望自 ...

  9. Watchdogs利用Redis实施大规模挖矿,常见数据库蠕虫如何破?

    背景 2月20日17时许,阿里云安全监测到一起大规模挖矿事件,判断为Watchdogs蠕虫导致,并在第一时间进行了应急处置. 该蠕虫短时间内即造成大量Linux主机沦陷,一方面是利用Redis未授权访 ...

随机推荐

  1. jquery(select)下拉框 选取选中的值

    var get_date_type=$("#date_type").find("option:selected").val(); var get_date_ty ...

  2. Chapter 4. The MPEG-4 and H.264 Standards

    本章节介绍一些关于MPEG-4标准与H.264标准的基本知识 比较重要的是第95页关于两种标准的对比表格.其他部分没有什么特别重要的细节.

  3. 如何使用apktool反编译,查看androidmanifest的内容

    1.下载apktool http://pan.baidu.com/s/1o7Jy090 2.使用方法

  4. 【知识整理】这可能是最好的RxJava 2.x 教程(完结版)

    为什么要学 RxJava? 提升开发效率,降低维护成本一直是开发团队永恒不变的宗旨.近两年来国内的技术圈子中越来越多的开始提及 RxJava ,越来越多的应用和面试中都会有 RxJava ,而就目前的 ...

  5. accp8.0转换教材第2章初识MySQL

    首先安装MySQL: 一.单词部分: ①networking网络②option选择③port端口④firewall防火墙⑤engine引擎 ⑥standard标准⑦character字符⑧collat ...

  6. encodeURI与decodeURI

    Global对象的ecodeURI方法可以对URI进行编码,与其类似的还有一个方法encodeURIComponent方法. 相应的对URI的解码方法也有两个:decodeURI.decodeURIC ...

  7. MyBatis源码解析【5】工厂的构建

    前言 这个分类比较连续,如果这里看不懂,或者第一次看,请回顾之前的博客 http://www.cnblogs.com/linkstar/category/1027239.html 终于算是把装备弄齐全 ...

  8. spring整合axis2(最小配置化)的示例

    参考文档: http://blog.csdn.net/xinhaoluan/article/details/3605234 环境配置: spring-framework-3.2.7 axis2-1.6 ...

  9. centos 6.5系统判断软件是否安装,如果没安装,则直接使用yum安装,并添加启动项

    function install_software(){ software=$1 s=`rpm -qa|grep ${software}` result=$(echo ${s}|grep ${soft ...

  10. shell脚本中字符串的常见操作及"command not found"报错处理(附源码)

    简介 昨天在通过shell脚本实现一个功能的时候,由于对shell处理字符串的方法有些不熟悉导致花了不少时间也犯了很多错误,因此将昨日的一些错误记录下来,避免以后再犯. 字符串的定义与赋值 # 定义S ...