Unity挑战大世界刷草(四)

Instancing

首先承接上一篇的内容,我们来算一算余下的草海信息大概有多少:

假设视距是100m,乘以2得200米的边长,也就是4万平米的大小地块围绕在摄像机周围,估算视锥内的面积在1万~2万平米之间,取个平均1.5万吧,以一个拥有5个三角形面的模型草为例,每平米铺满大概需要20株这样的模型草,那么我么就有 

15000 * 20 * 5 = 1.5m

一共一百多万个三角形面,移动端GPU渲染这1.5m个三角形... 感觉不算轻松,后续可能要做一些LOD减面处理了,但也不是不能渲,只要对应的shader足够简单高效,就不会影响游戏体验。

那么如何来创建并存储这些草体模型呢?

对于传统流程,可以简单归纳下渲染草海的步骤:

    1)首先是CPU对视锥内的每一块地砖提取密度信息,创建出对应若干株草的Mesh网格,

    2)然后这些三角形网格会被缓存到在本地内存中,以供下一帧复用

    3)在每帧提交给GPU前可能Unity还会帮助我们做一次静态合批,将若干块地砖上的草的网格进行合并,抛弃掉一些无用的数据和矩阵

    4)最后上传给GPU进行渲染,所有草的网格信息自然在GPU的显存中也会存一份

我想最主要矛盾还是在于这么多面的Mesh创建和存储起来很麻烦。美术大大只给了我们一株草的模型和一张密度图,所以如果设计不当,我们每移动一次摄像机,就会产生一片新的待渲染地砖,就需要根据其上的密度信息新生成一批可能不同的Mesh;假如我们在游戏里控制角色做了一次传送,或者长距离跳跃,弄不好可能就要再生成一边1.5M个三角形面片了,这是很不划算的事情。那么全部都提前生成好如何?这就更不可能了,这些三角形网格比密度信息还占空间得多,我们除了要存下所有顶点的位置信息外,还要准备诸如顶点UV,法线,甚至变化矩阵等各种辅助数据。

有一点很明确,我们不想在内存中创建存储和维护这150万片三角形,存储它们既消耗内存,处理起来也很消耗CPU,在某些时刻还得花费大量IO把数据从CPU搬运到GPU。可以的话最好能在GPU中动态的创建,以一种高效的方式:由CPU指定模板,指定实例化的个数,每个实例的位置等一些专有属性,其余所有的活都在GPU内完成(这其中包括创建对象,存储对象和渲染对象等操作)这些操作大家是否感觉有些熟悉?这其实就是之前提到过的基于 GPU Instance 技术的解决方案。

Instancing API

回顾一下之前提过的 DrawMeshInstancedIndirect 接口,它是Unity对GPUInstance的一种典型实现,接收的参数如图(也就是CPU向GPU提交的部分数据),第一个参数:vegeMesh 是实例模型本体,第二个入参是个数字,规定取用那个SubMesh(大家可以先不理会),下面的instanceMat则是草的材质,接下来的renderBound规定了这次操作所包围的空间边界,只有在这块空间与摄像机视锥有交集的前提下,当前接口才干活。最后面的argsBuffer是个结构化入参(格式是ComputeBuffer),里面得设置上需要实例化的个数等额外信息。顺便多提一嘴,这个接口所允许的实例个数上限没有限制,所以理论上对每一株草进行实例化都是可以的。

实际上我们不会为每株草各进行实例化,理由前面已经说明过,没有足够的空间来存储每一株草的位置和朝向等属性。

那么我们有什么呢?

密度!

如果vegeMesh这个变量代表的是一个平方米内所有草经过合并后的集合,然后通过GPUInstance接口,让vegeMesh在GPU内重复15000次(15000是视锥内草地面积),同时给每一个实例指派对应的坐标和密度信息,我们就能在地块的基础上,还原整个草海了。

不过如果真的这么照做的话,很快就会发现新问题,GPUInstance只接收一个模板Mesh,如何用一份Mesh来表现出草皮密度的多样性呢?如果什么都不做,只按照顶格密度去合并出一个Mesh传入接口,我们得到的是一个二阶分布的草海,每一平米的地皮要么寸草不生,要么郁郁葱葱,没有过度,这很可能不符合美术大大需求的,是要解决的问题。

这个问题的本质其实是:如何解决 GPUInstance 一次只接收一份Mesh作为模板,但是我们有非常多种密度的Mesh需要表现的矛盾?(X2次)

解决方案并不唯一,打破这对矛盾可以从2个方面入手,要么让GPUInstance"能够"接收多组Mesh,要么让合并后的一组Mesh能表现出某种多样性。

比如说:

    方案1:在CPU端就提前制备好各种密度的不同Mesh,多次调用GPUInstance,每次都针对不同密度的Mesh。

    方案2:是在GPU端玩花样,GPUInstance只执行一次,接收一份Mesh,然后想办法在着色器的顶点阶段修改这份Mesh,使之表现出不同的密度来

    方案3:仍然是在GPU端玩花样,这次使用Geometry Shader,直接在GPU内从0开始绘制草的模型,要什么样密度的就绘制什么样的

先说说我的想法,首先是方案3用到了Geometry Shader,是OpenGL ES 3.2支持的特性(和曲面细分一起),Unity2019也加了进去,(顺便说下,GPU Instance是ES 3.1特性,大概2014年面世的),回到这个几何着色器,我个人是愿意去尝试一下效果的,毕竟在GPU内手操绘图就不再需要CPU端传任何草的模型网格过来了,而且理论上提供了非常高的绘制自由度。不过也有些问题,其一就是绘图过程不宜复杂,复杂了工作流会很大,包括使用和维护等方面;其二是感性的直觉,就是复制草模型的速度应该是快于绘制草的速度,在需求绘制海量草Mesh的时候,综合效率可能还是GPUInstance来得好(当然这点需要验证)

2019版Unity后开始支持

再来是方案1,执行起来相对简单,也支持预置任意的Mesh,不过随着密度层级的增加,完全依靠方案1来处理草海在性能上就不够scale了,毕竟有多少层密度就要调用多少次GPUInstance接口,虽然我相信GPUInstance接口的效率,但是怎么说呢,如果能够一次搞定的话,尽量一次搞定比较好,把性能留给未来其他类型的植被需求吧。

所以综合考量,我个人觉得方案2是比较好的,也就是如果我们能够找到一种,只用一份Mesh来变化出不同密度层级的手段的话,问题就能在GPU内解决了。

当然答案是有的,前不久我阅读过Unity AssetStore上一个比较成功的刷草插件“uNature”,它实现了一种解决方案:

动态密度示意图

如图所示,假设满密度是10,代表我们把10株草的Mesh合并到一起,在将Mesh传入GPU之前我们得做一步额外的操作,也就是在合并的过程中,对每一株合并进来的草按顺序编入一个id,1,2,3,4,5,...,10,这些id被记录到了每个顶点上(可以参考图中地块上的数字角标,可以看到,属于一组Mesh的每一株草都被独立编号了)。这样当传入GPU后,满密度的Mesh大概会是最右侧这样。

我们知道,着色器在填充像素前,会有个处理顶点的阶段,每个顶点都会被单独处理,而处理逻辑我们是可以定制的。假设我通过某种方式知道了当前处理的这株草是来自于某个地砖的,而这个地砖的密度我也知道,比方说密度值是7吧(可以参考最中间的那个地块),那么着色器顶点逻辑中我们可以这么干:输入一个顶点,我们从顶点信息中找到编号id(这个id表示当前顶点属于第几号草,比如说6号),当我发现这个id <= 7时,就正常提交和渲染这个顶点,这时6号草会被完整渲染出来了;但是当id > 7时(比如8号),我们选择跳过这个顶点。这样做的后果就是,这块地砖上原本有10株草,经过过滤,只剩下编号为1到7的7株草了。7不就是这个地砖对应的密度么?匹配上了。

如果听起来迷糊,不要紧,罗里吧嗦这么多,就是为了让GPU剔除掉合并后Mesh里多余的草,让留下的草的个数符合当前地砖上对应的密度值即可。

方法能达成目标,但是细究起来感觉还是比较粗暴的,按照预置的id消减网格,每次都会是一个顺序,所以不加修饰的话在艺术表现上难免会有生硬的感觉。一般会安排一些高耸粗壮的草使用靠后的id,而矮小的草使用靠前的id,连续id的草之间的位置关系也最好是随机的,最后可能还得附上一些随机偏移来优化效果,这都是后话了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 阶段整理 到这里,知识点已经积累了不少,在这里我想做个简单的总结,梳理下眼下的信息,串一串流程: 一开始先让变化少...
    bbccyy阅读 1,156评论 0 1
  • 前言 大家好,作为博客开通后的第一篇技术类博文,我决定将前不久总结的一些关于如何在移动端开放大世界中高效渲染草海的...
    bbccyy阅读 1,876评论 0 3
  • 草海信息的存储 我们要求记录草海中草的位置信息是为了能够创造出一个尽可能"确定"和"可控"的世界,试想如果没有这条...
    bbccyy阅读 1,268评论 1 1
  • 书接上文 前文介绍了Unity内置的 地形草(Terrain Detail) 存在的问题以及一些优秀插件的优化方案...
    恶毒的狗阅读 891评论 0 0
  • Epic外放的两大特性Nanite跟Lumen,构成了UE版本升级的基石,关于这两大技术,已经有了众多的分享,不过...
    离原春草阅读 21,980评论 0 11