Unity挑战大世界刷草(六)

工程向杂谈

需求总结

利用GPU Instance技术在游戏内大量绘制草海是一件既简单又复杂的事情,说它简单是因为所有问题的核心点只有一个,那就是GPU Instance 相关接口并不多,大体上渲染流程都一致:提交Mesh和实例参数,Pipeline帮你在某个时刻通过 vs 和 ps 展开每一个实例化的个体;说它复杂是因为我们的很多后续需求需要经得住这套渲染流程的考验,有时候一些看似简单的需求,很可能实现起来就束手束脚的。一般而言项目越大,需求种类越多越杂,这些累积在一起的工程向问题就越棘手,这主要是基于一点事实:移动设备资源有限,而需求之间必须精细协调资源分配,规划执行路线。那么我们会有哪些需求呢?简单的归类总结下,我个人的结论是这样的:

  1. 在平面上正确绘制草海本体(模型和世界空间<X,Z>坐标)
  2. 可以在错落有致的平面上绘制草海本体(高度信息*)
  3. 草随风缓慢摆动 (背景风噪)
  4. 草随风有方向的摆动 ,方向可调
  5. 草可以被角色影响:
    • 角色站立处有压草效果
    • 草可变色(烧灼后表现)
    • 草可被切除(切削后表现)
  6. 模型草分层细节显示(LOD)
  7. 模型草变体和碎花*
  8. 分层草海*

让我对其中几个打上“星号”的部分稍微多做一些延伸:

首先是条目2,所谓错落有致是指的附带上高度信息后草模型在世界空间中可以表现出正确的位置,这很好理解,之所以把它作为工程向的一点技术问题单独拎出来是因为获取到“正确”的高度信息方法有很多,但是这部分信息对于开放世界来说,如果处理不善的话会造成很多性能和资源的浪费,需要小心应对;再一方面大家可能会注意到我对正确两个字打上了引号,这是因为不能简单的认为高度信息 = Unity Terrain 里导出的那张 Height map 所记录的高度数组。正确的高度值是在布置完场景后,通过由上而下的扫描来得到的,高度信息需要考虑到所有放置在地形上的石料,土坡,建筑等额外物件的影响——对于高度图生成的地形,最多可以表示下地表的简单起伏,想要表现局部复杂的几何细节,高度图并不适合。

然后是条目7中提到的模型变体和碎花,这些东西首先具有所有草海的性质(比如能被压弯,点燃,切除等),但是它们在视觉效果中并不算是普通草海,而是点缀在草海中的一些特例。举个《原神》中的例子,如图中箭头所示:


碎花展示

上方白色碎花和下方成片的红色碎花都属于模型草的变体,显然它们从Mesh角度来说是区别于基础款模型草的,而且数量一般不多,主要用于增加草海的“物种多样性”。如何将它们协调的渲染出来也是值得推敲的部分,如果将这些变体全部打包进基础款模型草Mesh然后一次提交给GPU,显然会让GPU负担爆炸般增加,所以比较合理的做法是分开批次走GPU Instance 接口提交,先渲染绿色草海,再渲染红色和/或白色花朵(如果它们Mesh一致只是颜色不同的话,可以合并成一次提交)。

还有是分层草海,为了便于理解还是先上图:


分层草海

参考其中突出山崖部分,这种地形结构很容易造成同一个世界坐标<X,Z>下会取到2种不同高度的Y方向高度值,既一个点落在山崖上面,一个点落在山崖下方的地面上。那么在使用采样一张纹理来获取高度的前提下,我们就没有办法在这上下两处都记录上草海密度的,也就意味着要么山崖上没草,要么山崖下没草。当然要解决这个问题前最好确认下这是否是一个问题,如果场景美术能很好的处理山上山下一侧的地貌,那不解决也无妨;然而如果有类似在近地面的浮空岛这类地形,且岛上和岛下都需要覆盖草海时,我们就得通过摄像机位置来即时判断当前帧使用哪一张高度纹理和密度纹理,确保让摄像机附近的草能正确的显示出来。可以预见这样的需求势必增加我们对高度和密度信息的编码和存储方式,增加一些额外的复杂度。

量化(离散化)的世界

离散化世界

之前谈密度的时候我们说过一点,这里作为重要的技术细节,我愿意再次强调一下:我们在存储、加载密度块时,使用了离散的世界坐标系,任何坐标都是基础地砖长度的整数倍。在这里就是1米的整数倍,像
<x=1,z=1>,<x=-2,z=3>
这样的坐标是存在的,但是
<x=0.2,z=1.5>
这样的坐标就不存在。
当然如果嫌1米为基本单位太长,也可以使用0.5m作为基准,届时
<0.5,1.5>,<1,1>
就是存在的,但是
<0.2,1.5>
还是不存在,因为0.2不是0.5的整数倍。

这是一个非常基础的前提,为之后的一些算法和数据结构能够正确又高效的工作做了铺垫。
也许有人疑惑有什么样的便利?
打个比方,离散化地块后,我们能非常轻易的计算并定位任何一块地砖在世界空间中的准确位置,这为我们精确加载和释放资源提供了便利;再比如超大世界地图在远离坐标中心的地区,表示位置的整数部分会占用许多float数值的有效位,留给小数部分的精确度就会大幅下降,有了离散化坐标,我们可以精确定位任何一个地砖的四个端点位置,从而为后续坐标平移,消除精度损失提供便利。再比如,离散后的地砖也为编码四叉树提过了便利,一个地砖大小的面积自然就成了树的在理论上最小的叶子节点。

平移和补缺

前文介绍过,我们用一个矩形框来确定摄像机周围的一定范围,称之为视场,在程序里所有视场中的密度信息会被缓存和管理起来。如图:


视场内密度缓冲

当视场随着摄像机在世界空间中移动了一段距离后,自然会产生一个新的视场矩形,新旧相减一下就产生了如下图阴影部分的可复用区域,以及蓝色方块内的待加载区域。
那么这2块蓝色区域的密度信息从哪儿获取呢?这取决于我们怎么存储每块chunk(Terrain)上的密度:对于纹理格式,相当于二维数组,没什么难度;对于四叉树格式,就得开发一套接口去匹配整块读取数据的需求,这部分内容在上一篇接口类的分享中也提到过,我说到需要至少2个接口负责读取(Search)和写入(InsertRange)密度信息,接口中的入参里有一对 Min Max 数据结构,用来表示的正是我们感兴趣的蓝色区块。

假设上图区域R已经转化成了左下角(Min)和右上角(Max)2个点(都是离散化后的世界坐标点),如下图中标注出的样子:

R区域在Chunk(Terrain)空间的表示

假设发现R的覆盖面积需要用到6块chunk中存储的数据。那么每个chunk都需要贡献出一定范围的子区域上存储的信息,这些子区域的是我们感兴趣的地方,通过总结所有可能出现的覆盖形状,我发现了一些规律:

矩形区块与切割边形成的可能交集

如图所示,一共存在16种不同的状态,简单分类下可以归结为1边相交,2边相交,3边相交和4边相交这样几种类型。

我们对四叉树通过输入切割边搜索数据的过程,就是在这16种状态中连续不断跳转的过程,虽然状态很多,但好在所有状态的跳转都是收束的!简单解释下:当遇到一个四叉树内的矩形块有3边相交时,所有它的子节点的状态只可能等于3边或小于3边,而不会跳转到4边及以上的状态。此外每种状态在四叉树的父节点只需要判断一次,其子节点可以继承父节点判断的结论,从而在一个较小的状态空间中进行后续判断和过滤。再者对于所有叶子节点所代表的区域,其中的密度自然都相等,可以很方便的搬运(Blit)到目标纹理中去。所以最终的结论是,在四叉树中搜索一片区域的数据可以很高效。

运用多线程

所有在CPU中进行的密度信息索引,拷贝和处理的任务,都是与Unity 自带函数无关的事情,完全可以脱离Unity的框架,丢给另一个线程去做,然后在合适的时机切回Unity的主线程。

我们以更新视距内密度信息为例:


提交异步任务

蓝色主线程负责发起任务(Req),具体来说是请求一份当前帧时刻摄像机周围一定范围内的草海密度信息。而图中下方的绿色则代表被唤起的子线程,负责处理 Req 中所定义的需求,这里边包括搜索四叉树,提取四叉树各个相关叶子节点信息,甚至从磁盘读取序列化好的四叉树二进制并反序列化为C Sharp对象。

一次发自主线程的异步请求,为了简化并发逻辑,我们规定只有当子线程处理完毕且提交回主线程后,主线程这边才允许发起新一轮的异步请求。这样的好处是全程只有2个线程参与,且由于所有请求都来自主线程,所以只需要在主线程里设置一个状态位就能确保线程安全。唯一的问题是,当请求来的过快,就有可能出现这样一种状况:在2次成功的请求之间,可能存在一个或多个被阻塞并被抛弃的请求。形象的说明下,可以想象摄像机在高速移动并连续发起了视场位置更新的请求(req1,req2,req3...),当处于请求req1还未得到响应的情况时,表现层只能使用原有资源渲染草海(可以已经处于摄像机后方),此间发出的所有请求(req2)会被直接丢弃,直到req1返回后才能继续处理新来的请求 req3,以此类推。这到底是不是一个需要克服的问题应该视项目具体需求而定,如果确实有频繁的高速移动需求,且会出现明显的草海刷新间隔的段落感,那么就有必要在位移前做好预加载,或者利用虚化特效短暂模糊视角,这里就不多展开了。

九宫格和十六宫格

当使用9宫格来管理chunk的时候,我们会遇到一些麻烦的问题,比如下图:


9宫格边缘来回切换

玩家来回在A,B两处切换位置,为了满足摄像机始终处于9宫格中心的需求,就会频繁的加载和卸载图中黄色和绿色 2 块区域的chunk资源。我在这里提供一种比较简单的解决方案,并不唯一,主要是抛砖迎玉。

算法很简单:以少量的额外空间做一层缓存,实际可能使用到的格子数是9 + 7 = 16个,每当摄像机有跨越区域的移动时,算法会检查一下摄像机所在的当前地块附近,是否有1格以内的chunk没有加载,有则加载之,同时检查一下在所有已经加载完毕的chunk里,是否有距离摄像机所在地块 2 格以上的地块资源,有则卸载之。


好了,到目前为止关于绘制草海的基本概念和比较通用的算法及思路已经介绍完毕,都是我在设计项目过程中总结的一些共性的东西,虽然不涉及代码,也没有公式(什么?画草还需要公式?)但是应当有助于梳理流程,为后续的深挖和优化提供了框架基础。而且必须指出的是,这套流程只是草海的解决方案之一,是否基于密度,如何划分地块,如何获取高度等等方面都会产生众多的变体,所以只要适合项目,经得起实践检验,就是好的方案。

最后再开个坑,GPU渲染草海的很多技术细节也是值得深挖一下的,比如植被的风动效果就涉及到GPU中如何模拟自然风,如何模拟风与不同形态植被的作用,期间会涉及不少数学公式(好吧,真有公式);再比如利用GPGPU加速渲染,主要从GPU视椎裁剪以及Hi-Z剔除方面入手,并会利用到Unity包装起来的几个Indirect技术;还有是读取磁盘文件的一些新尝试(主要用来规避大量向Unity导入图片纹理带来的恼人的加载等待);亦或是基于虚拟纹理的全局高度图提取......

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

推荐阅读更多精彩内容

  • 草海信息的存储 我们要求记录草海中草的位置信息是为了能够创造出一个尽可能"确定"和"可控"的世界,试想如果没有这条...
    bbccyy阅读 1,294评论 1 1
  • 接口简介 介绍完基本方案和原理,也许你会发现很多东西都是概念上的和思路上的体现,那么我们再来过一下实际工程中用到的...
    bbccyy阅读 1,094评论 0 1
  • 阶段整理 到这里,知识点已经积累了不少,在这里我想做个简单的总结,梳理下眼下的信息,串一串流程: 一开始先让变化少...
    bbccyy阅读 1,190评论 0 1
  • Instancing 首先承接上一篇的内容,我们来算一算余下的草海信息大概有多少: 假设视距是100m,乘以2得2...
    bbccyy阅读 1,693评论 0 2
  • 前言 大家好,作为博客开通后的第一篇技术类博文,我决定将前不久总结的一些关于如何在移动端开放大世界中高效渲染草海的...
    bbccyy阅读 1,903评论 0 3