如果你做的草是一个3D模型,那么,你就得一个一个地放。
我们要求记录草海中草的位置信息是为了能够创造出一个尽可能" 确定 "和" 可控 "的世界,试想如果没有这条限制,我们不在某种程度上确切记录草的位置,那么一片草海位置信息是可以用 0 bit 的容量来记录的,毕竟只要写个随机函数,让草自己随地扎根就行了,不用浪费空间存储每一株草在哪里。然而我相信大多数项目中,策划和场景美术同学应该不同意这么干,他们会希望能精确绘制出地表植被的分布,规定哪儿有草,哪儿没有,甚至在哪里必须放上几株并设置好朝向等等。
可是,另一方面,想要 精确存储每一株草的位置信息似乎是一件不可能的事情 ,至少看起来很不划算。这点刚才已经算过,一个10公里的地形,每平方只种10根草,只记录位置一个信息,就要消耗掉数十GB的磁盘空间,更别提金贵的内存了。
余下能做的事情就是找出一种折中的办法来调和这对矛盾。这边我给出一种比较常用的解决思路:对于那些注定会成为草海的草(草海的主体),策划和美术同学放弃对每一株草的精确控制,转而控制一小块面积内草的各项属性。如果设置合理,我们能很轻易的降低整体信息量1到2个数量级,同时也能保持全局的草海分布趋势,达到效果和设备的平衡。按照这种思路,很自然的,我们想到使 用密度来表示一个单位面积的地皮上有多少草 ,这样原本需要记录的数值,已经不再是单株草的坐标了,而是这整块地皮的中心点坐标,以及地皮上草的密度值。
这种将单位地皮作为整体打包记录密度的做法,使我们就把原来10 ~ 100株草的数据量一下子压缩到了1株的数据量,当然这个密度转化的过程中是有信息丢失的,我们 丢失了单位面积内100株草的局部偏移信息 。这是一个问题,但也是一个相对容易处理的问题,比如我们可以赋予每株草一个单位面积内随机的偏移来补全/伪造因合并而丢失的信息,亦或者由美术同学直接指定这一平米面积内草的分布,规定他们的位置和朝向。更妙的是,借由密度化地块这个过程,我们其实也 离散化 了整个世界,现在的世界地图(至少密度地图)对我们来说是一个一个平铺开来的,拥有固定尺寸的单元格,为方便计量,不妨预设单元格尺寸是1米,这样密度的单位也就变成了"N株草每平方米"。离散化的好处有很多,之后会细讲,其中主要的一点是我们从此可以丢掉浮点数,专注于整型的计算和存储了,而且一些数据结构的运用也会变得更加高效。
一般而言草海的分布是具有连续性的,打个比方,只要不是草海的边缘,一块地皮的密度与它四周8块领域地块各自的密度有很大概率是相等的,这说明在不丢失信息量的前提下,还有压缩密度图的空间,所以不论是把密度图存成纹理格式,在编码成某种形式的图片,还是将密度图在平面上切分,编入四叉树结构,都能再次大幅降低草海信息的体积。
最后对于那些确实有需要精确到位置和朝向的植被,我们不妨将之梳理出来,再开个小灶。只要这部分需求的总量不大,就不会造成实质上的问题。之于如何开小灶,我想方法也有很多,比如我们可以针对这些特殊植被再生成一张密度图来渲染,只不过这次密度单位不是1米,而是0.1米或更小,只要特殊植被数量不多,这些密度图经过压缩后也挤占不了多少空间。
好了,到目前为止我们了解了渲染草海的基本特点,面对的挑战,以及一些重要概念和方案。是时候明确下基本技术选型了。
首先是草的几何表现形式,这里选择了 模型草 作为开发的基准。从技术实现角度出发,模型和面片草对程序来说区别其实不大,更多的是美术工作流的变化,以及视觉效果的转变。考虑到移动端对透明度测试的优化问题,以及植被与玩家交互作用的表现力等方面,暂时选择了模型草作为开发的基础,当然添加对面片草的支持相当容易,如果有需求的话,后续追加也不迟。
然后承接刚才密度的方案,我们需要将美术同学在世界空间中刷出来的草海, 以密度块为基本单元,存入某种数据结构中压缩起来 。这边选择支持简单的二维纹理编码(可以理解为压缩成图片),同时也支持四叉树编码密度,做四叉树这部分主要是基于个人兴趣,想尝试下构建一套快速范围搜索的算法,当然四叉树也是有别的好处的,之前也讲过,在内存中,四叉树理论上会比解压后的纹理密度图占用更少的体积。那么对于密度变化不复杂的地区,选用四叉树编码,就可能节约数兆字节的内存。
接下来是越来越为人所知的一个术语: GPU Instance 。 还记得我们在开篇研究了草海的基本特点么?它具有“海量”和“同质”的特点。而GPU Instance 可以理解为一般图像引擎会提供的一套接口组件,专门用来处理符合“同质”特点的“海量” 实例。处理时,我们只使用一套几何构造作为所有实例的标准“模板”,然后CPU会告知GPU:“需要复制多少份这样的模板,这些模板最后放置在哪儿”等很少的信息,余下的活就交给GPU去处理了。如此一来,虽然GPU的活没少干,但是CPU解放了,自然能提升不少性能。
规范得说,我们可以通过调用了基于 GPU Instance 技术的 DrawMeshInstancedIndirect(...)接口来消减CPU管理Mesh个数,同时也降低了和GPU之间的IO负载,提高Batch数。如果有的同学不理解,也不用担心,后续我们也还会结合示例来详细阐述。而且这些内容在Unity官方文档上都有详细介绍,有兴趣的同学可以去读一读。
此外还值得一提的是,我们选择了面向移动端的 URP 渲染管线,因为其底层的一些优化,特别是 srpBatch 可能会对降低开放世界整体的 DrawCall 消耗有所帮助,这里就不做展开了。
好了,今天我们先到这里,做一个总结:草海位置信息很多很头疼,我们选择用密度块方式,通过四叉树编码后存放起来;然后针对表现层面的需求,我们使用Mesh面数较高的模型草作为草海的主要载体;最后提了一点 GPU Instance 的信息。接下来的文章会进入所谓“绘制草海”的主要环节,即算法逻辑的实现。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)