天谕手游体质(天谕钓鱼经验《天谕》手游的体素方案实践)
树木盘根错节的热带雨林、中世纪幽暗阴森的哥特城堡、人声鼎沸的商业街道…… 这些庞大的游戏世界,既可以是游戏制作人员通过专业的工具编辑生成,也可以像《我的世界》那样一块一块细小的“砖头”堆砌起来。
我们把这些“砖头”当做三维空间中不可继续分割的最小单位,并称之为体素(Voxel)。这个概念可以很自然地从二维空间的像素(Pixel)联想到。体素作为一种不同于三角形网格的数据形式,在构建游戏世界时可以提供更多的可能性。在将体素应用到项目时需要思考一些问题,比如:体素数据有什么优势和特征?如何使用体素做高效的碰撞检测?如何使用体素解决平面和空间寻路问题?采用什么样的数据结构来合理控制数据规模?如何高效获取冗余较少的体素数据?项目中的体素工具链是什么样的?等等。
本文将以《天谕》手游的体素方案为例进行详细介绍,希望读完文章之后你会有自己的答案。

一、为什么要引入体素?
三角形网格主要展示的“表面细节”, 体素不仅可以展示表面细节,还可以表示“内部细节”。利用体素构建的虚拟世界更加贴合真实世界的构成方式。然而由于体素巨大的数据量和运行时算法的计算量,使其难以在当前的移动平台上进行实时渲染相关应用。渲染相关的工作虽然难以进行,但是体素由于其数据结构的特性,可以在Gameplay方面大展拳脚。下面列一下体素数据的一些特点和优势:
1、规则的数据结构
体素相比于三角形网格有更加规则的数据结构,为游戏提供更多的功能支持,例如
1.1 高效的空间数据查询
规则的数据结构可以用更加高效的数据格式存储,例如数组。在做碰撞检测之类需要进行大量查询行为的算法时,体素可以提供更加高效的相邻空间的碰撞检测,为大规模玩家同时战斗提供性能保障。
1.2 服务器拥有一份和客户端一致的场景表示数据
服务器也可以和客户端一样持有一份相同的场景表示数据。为了防止客户端作弊,很多需要服务器强校验的应用情景(技能设计、副本机制)在体素数据的支持下可以更加自然、合理地实现,而不是利用各种编程Trick、限制策划设计方案来实现最终效果。
1.3 更加适合规律总结和算法创新
规则的数据结构更容易总结和发现规律,在特定的功能中通过枚举、预处理、特定的公式达到不规则数据无法达到的性能效果,例如寻路。同时规则的数据也更容易进行数据压缩和合并。
2、自由的粒度选择
相比于三角形网格不同级别的LOD数据,体素可以程序化地自由选择数据粒度,然后在权衡性能与效果后作出最优的选择。 目前《天谕》手游中体素的精度是全局固定的,精度为 XZ平面0.5M*0.5M,Y轴高度方向为0.01M。当然游戏也可以针对不同场景、不同区域、不同模型采用完全不同的精度表示。 另外,不同精度的体素数据还可以构建多层级的体素数据,用来平衡性能和数据规模的问题。
3、动态构建
由于体素数据结构的特性,程序可以方便的在静态体素上进行动态构建,例如添加,减少,修改等等,类似于我的世界。同时可以利用等值面提取的方法(Marching Cubes)用体素实现类似于使用三角形网格的渲染效果,而不是局限于像素风格;或者是用等值面的方法提升直接利用体素渲染带来的性能压力。《天谕》手游的设计目标没有像我的世界那样自由定义场景,也不依赖体素数据进行渲染,体素的主要用途仍然是碰撞和逻辑。项目中实现了动态的体素机制,例如动态的空气墙、即将上线的家园的自定义地形、自定义物件等等。
4、离散的单位表示
每一个体素是不可分割的最小单位,所以空间中的某个体素可以通过离散的整形数据表示。在实时的网络游戏中经常需要客户端的预表现计算与服务器的逻辑结算来保证用户体验和程序逻辑的正确性,整型数据可以避免两端因为浮点精度问题导致的游戏逻辑错误。
这里有一篇很好的总结性文章,感兴趣的读者可以进一步参阅以体素建构三维游戏世界。
二、如何得到体素数据?
现实世界中,体素数据可以通过CT技术进行扫描、转化和生成。虚拟世界中如果要美术人员手动构建这样的数据将是一个巨大工程,现阶段也没有比较成熟的数字创作软件支持这样的工作模式。 所以目前体素数据的获取方法主要是将三维的三角形网格数据转换为体素数据,这个过程称为体素化(Voxelization)。

体素化的实现可以有多种方法,比较常用的是光栅化(Rasterization)方法,即对光栅化时网格覆盖到的格子进行标记,代表这一个格子上有体素数据。如果网格上带有额外的自定义信息(例如物体属性:地面,水等等),也可以在这一步把数据应用到体素中。《天谕》手游中采用的是保守光栅化方法,即某一块体素格子只要有被网格覆盖的部分(不需完整覆盖,不需考虑采样点是否被覆盖)即视为有体素数据。更加精确的体素化过程还可以参考这篇文章: The Basics of GPU Voxelization。体素化的效果示意如图2.1所示。
还有一种体素化方法,与光栅化方法不同,前者从网格数据出发生成体素数据,后者从体素格子出发,对于每一个格子(其本身就是一个AABB盒)与模型网格进行求交运算,如果成功即代表这个格子有体素数据。当然,这些格子需要先进行筛选,至少在整个模型的AABB盒之中。这样的方法更好理解也更好实现,但是效率上可能会差一些。方法可以参考3D模型体素化,有兴趣的读者可以尝试一下。
《天谕》手游使用Untiy引擎,游戏场景的体素化过程如图2.2所示。

1、场景物体收集
这个阶段中将游戏场景内的所有需要烘焙体素的物体收集起来,包括地面,建筑物、石头、水面等等。这部分工作由C#层的脚本完成。收集后的物体还要进行一些过滤,例如树叶、较小的物件等等。
2、数据准备
前一阶段收集了所有GameObject的引用,本阶段中将所有物体上的网格数据进行复制,传递给下一阶段。同时,一些体素的编辑数据(比如策划布置的触发器, 这些数据存储在单独的编辑文件中)也被读取和序列化,进行传递。
3、场景分割
这一阶段来到了C++层。C++中代码中包含了所有体素相关的逻辑,包括数据格式定义,访问接口、算法等等。场景中所有的网格数据被传递到这里后,首先会计算全场景的包围盒,然后将场景分割成若干个区域。每个区域的数据相互独立,客户端可以按照需求只加载玩家周围的体素。《天谕》手游中根据64米*64米的面积划分为一个独立区域,即每个区域XZ平面上被分割成128*128个格子。
4、光栅化
确定好区域之后即可进行网格数据的光栅化,也就是原始体素数据生成最关键的一步。由于每个区域数据独立,所以光栅化阶段可以用多线程并行烘焙,加速全场景的烘焙过程。光栅化的实现中可以使用相似三角形,依据XZ平面的格子分割的结果求得Y坐标上对应的线段长度。这个长度代表着Y轴高度上一块连续的体素。光栅化输出的原始体素数据结构是一个高度场, 由一个二维指针数组组成,(二维数组的规模和区域一致,每个数组元素对应一块0.5*0.5的格子区域)。每一个数组元素是一个链表的头指针,链表中的每一个元素代表该平面格子上一段连续的体素。连续的体素数据由下表面高度和上表面高度组成,同时携带一些自定义数据。
5、内部区域合并
三角形网格描述的是物体的表面细节,网格栅格化之后形成的体素数据和网格一样,内部是空心的,例如大型的石头、封闭的楼梯和小房屋等,如图2.3所示。

这些空间的内部需要合并掉,一方面是减少体素的数据量,另一方面可以避免因为表面过薄而产生的穿插问题。合并的算法很简单,就是计算每一个体素的联通性。相邻体素的联通性判定是一次模拟移动,即人物是否可以从原点体素移动到相邻的目标点体素。只有人物可以达到的地方,才是合法的移动区域,否则就会被认为是不可达区域而被合并,例如石头的内部。全地图联通性计算后得到是若干个联通子图,每个体素上都有一个该体素所处的联通子图Id。计算完毕后,将策划布置的所有传送点所在的联通子图视为可达区域,然后对不可达区域进行体素合并。
计算联通性的工作量比较大,所以需要进行并行处理。Map阶段将每一个区域放在对应工作线程中独立烘焙该区域内的所有联通子图,Reduce阶段在主线程中将每个相邻区域进行联通区域合并,合并的方法是测试两个区域相邻体素的联通性,如果联通则合并联通子图(修改联通子图Id为同一个),否则跳过。
6、编辑数据应用
这个阶段主要将一些自定义数据应用到对应的体素上,例如触发器的ID。这些数据都是离线编辑时生成的。一部分数据根据体素坐标存储,可以直接进行定位和应用;还有一些数据存储为一个由多边形组成的区域,这里需要先将多边形进行三角剖分转换为一个平面Mesh,然后利用相同的光栅化接口确定覆盖的体素位置,最后进行应用。这一阶段同样可以应用一些自由编辑的体素数据,例如添加一小块体素,去掉一块多余体素等等。
7、数据合并与输出
烘焙的最后一步就是数据整理和序列化,写入最终的体素文件。未经处理的体素数据需要进行合并,合并的策略详见下文的数据结构部分。输出的体素数据文件同样需要进行压缩,来减少对客户端包体大小的压力。
《天谕》手游中单个场景的体素烘焙时间在十几秒(副本、竞技场)到2分钟(大世界)不等。
三、体素的数据长什么样?
体素的数据结构会深刻的影响相应的算法实现,进而影响整个功能的效果和性能表现,所以采用一个可以兼顾多方面性能考虑的数据结构非常重要。体素一个经典的数据结构是稀疏八叉树SVO(sparse voxel octree)。这样的数据结构可以大量减少空间某点无体素数据的存储,使数据规模和运行时访问速度达到一个平衡的状态。SVO数据结构可以参考图3.1和图3.2。图3.1中是简化为二维空间的版本,图3.2是三维空间的版本。实时渲染中不少光线追踪算法都会采用SVO的数据结构。


而《天谕》手游在选择体素的数据结构时,需要考虑以下几个原则:
原则一:速度尽可能的快,任意目标点的体素查询在常数级别的时间内。这样可以保证服务器大量玩家碰撞结算的效率,同时给客户端提供更高效的检测接口。
原则二:保证速度的基础上尽量减少体素的数据量,以保证客户端占用更小的内存、体素文件占用更小的游戏包体。
《天谕》手游作为一款MMORPG游戏,有几个基本的特征:
特征一:对性能要求较大的PK、群战玩法,大多出现在场景的地面。
特征二:大多数场景高度上并不复杂,游戏中存在大量的开阔外部世界,像是主城中的商业街、深海中的大瀑布这样的高度上差异较大的场景较少。
根据SVO的数据结构,查询的耗时和八叉树的高度有关。而树的高度与场景复杂程度、体素分割的最小粒度有关。根据原则一,我们希望可以有更加快速的查询效率,最好就是O(1)的时间,而不是树高。根据原则二,我们希望数据尽可能的小,达到这样的目的就要尽可能消除或者合并冗余数据。SVO的树形结构难以进行更进一步的数据压缩。
所以,《天谕》手游采用的体素数据是一个经过合并的高度场数据。高度场的每个数据元素不是一个高度数值,而是若干块不连续的体素数据。每一块体素数据我们称为Span,数据定义如下:
Min:代表体素的下表面高度 UInt16
Max: 代表体素的上表面高度,和下表面高度一起确定了体素的最终形状 UInt16
《天谕》手游中体素高度精度为1cm, 所以支持的场景高度跨度为655.35米。
CustomData:自定义数据,包括层级信息、物理材质信息、触发器信息等。UInt32
其中层级信息代表这块体素的层级属性,比如地面、水面、空气墙等;物理材质记录了额外的物理属性例如木头、沙滩等,用于决定播放脚底特效的效果和声音;触发器信息记录了一些策划布置的触发器,用于特定的玩法。

如图所示,体素数据由索引表和体素数组两部分组成。其中索引表是一个二维数组,每一个数组对应着XZ平面一块0.5m*0.5m的体素格子。索引表中的元素值为体素数组中的下标,代表着该平面格子上第一个体素。下标+1位置代表该平面格子上第二个元素(如果有的话)。特定XZ平面格子上所有的体素Span我们暂时称为Span Array。定位一个三维空间中任意点的体素,可以利用XZ坐标在O(1)的时间内定位到索引表的特定位置,然后依次进行该XZ位置上体素的遍历。这个遍历似乎与原则一不相符,但是这里要引入特征一说明一下。大部分对性能有较高要求的结算场景都是在地面,也就是说遍历到第一个体素数据就找到了,所以整体的开销就是1次内存访问的CPU周期。对于其他场景而言,根据特征二的描述,很多大世界场景中特定XZ平面格子上的Span数量为2-3,开阔外部世界一般只有1,商业街可能有5-8不等。根据这个数据规模,遍历的开销也是比较小的。所以在绝大多数的应用场景下,查询体素的效率是常数级别。
原则一的设计目标达到了,现在要兼顾原则二的目标。我们可以观察到场景中会出现大量地形相同的体素,例如城镇中的各种平地,高台等等。这种情况非常常见,所以需要把具有相同地形部分的体素Span合并掉,如图3.4所示。具体方法是在烘焙阶段,将每一个XZ平面上的若干Span数据拼接成一个字符串Key,和其他XZ平面生成的字符串Key进行对比。如果是相同的,则两个XZ平面格子对应的索引表中指向同一份体素Span Array数据,多余的体素数据也就不需要进行额外存储。

经过前面的步骤,体素Span数组已经没有冗余数据了,现在冗余数据的大头是索引表。因为索引表元素数量是固定的,也是二维数组高效访问的一个代价。索引表元素数量固定的话就要尽可能减少每个数据元素的数据量。这里有两个方法:1. 索引表元素不存储Span Array的长度,仅存储头元素的位置。没有长度如何确定遍历的终点呢?方法是Span Array内的Span是根据高度进行排序的,所以遍历时如果后面一个元素的下表面高度低于或等于前一个,代表当前Span Array已经遍历结束。在烘焙时,我们会将第一个体素Span下表面不为0的数据强制设置为0,这样就可以保证上述遍历规则的正确性。 2. 根据该区域Span的数量动态变化索引数据的大小:如果不同的Span Array的数量在255内,那么索引表的元素可以用UINT8来表示,以此类推。
另外需要提及一点的是,场景中若干个区域的数据是相互独立的,所以每一块区域都有独立的索引表和体素数组。通过世界坐标进行查询时,先定位到特定的区域,然后转换为区域内坐标进行查询。
业界已经有很多成熟的体素应用方案,比如《天涯明月刀》项目。天刀的体素数据采用分层存储的方式,利用去掉第一层下表面数据、层中打Patch的方案去除冗余,以二维数组的O(1)查询效率和连通性Cache达到高效的数据访问。有兴趣的读者可以参考《腾讯编程精粹》中的《3D游戏碰撞之体素内存、效率优化》一章。
四、体素用到了什么地方?
1、移动碰撞检测
移动碰撞检测是MMO游戏中发生频率最高的检测行为之一,高效率的完成人物的移动碰撞检测是提升服务器性能的一项重要工作。基于体素的碰撞检测也非常简单,如图所示。当目标点的体素高度小于当前体素高度与攀爬距离之和,而且目标点有足够容纳人物的移动空间,即可视为合法的移动。跨越多个体素格子的移动需要转换为若干次相邻体素移动测试。由于体素中除了高度数据还有一些额外的层级属性,碰撞检测时也可以加入一些自定义的碰撞检测规则,比如对应层存在空气墙属性的体素就一定不允许通过。移动碰撞示意如图4.1所示。
上述运算中只有查询体素的访存操作是最耗时的,其他都是整数加减判定或者位运算。在副本、竞技场、开阔地形的地方,查询结果都是地面,即遍历Span Array的第一个体素即可获得结果。这样整体的碰撞检测效率很高,可以实现百万次相邻体素碰撞检测,耗时在几十毫秒级别。

2、 射线检测
射线检测可以视为移动碰撞检测的通用版本,基本原理就是从射线原点出发,依次检测射线经过的路线是否有体素。分割射线上的监测点,可以从原点出发,沿着射线朝向移动一个略小于体素精度的长度(例如0.5米的精度采用0.49米的检测间隔)来确定下一个检测点。也可以在XZ平面将射线进行Bresenham找到若干个相邻的XZ平面坐标,然后根据XZ平面上的距离和高度差进行插值得到Y坐标,以此确定若干个检测点的位置。
从上述描述大家可以看出体素射线检测与Unity中基于碰撞体射线检测的优缺点。
Unity中Physic库提供的Raycast接口,基本流程是先通过Bounding Volume Hierarchy结构,利用多次简单的AABB测试找到条件符合的碰撞体集合,然后依次根据碰撞体类型进行射线-碰撞体的求交测试。对于简单形状碰撞体可以方便地利用公式直接计算得到交点,对于网格形的碰撞体就要与网格中每一个三角形进行求交运算。Unity中基于碰撞体的射线检测,耗时与场景的复杂度、碰撞体类型有关;基于体素的射线检测,与射线检测距离有关。所以体素的射线检测适合短距离的检测,而宽阔地形上的远距离检测还是碰撞体的射线检测更加高效。《天谕》手游中保留了碰撞体机制,项目的开发人员可以根据不同应用情景选择不同的射线检测方式。
另外提及一点,体素射线检测还可以弥补Unity中碰撞体射线检测的缺陷,那就是如果射线原点在碰撞体内部,则不会检测到碰撞体。(具体内容可以参考Physics.Raycast的说明部分)而借助体素的射线检测可以更高效的解决这个问题,而不是用反方向射线、Sphere Cast等方法解决。
3、 寻路
前面提到了,体素是一种非常规则的场景表示的数据结构。在规则的数据结构上,往往可以通过规律总结,运用一些预处理、枚举、特定的计算公式来大大提升算法的效率。体素寻路是一种典型的基于格子(Grid)的寻路,这一类问题业界已经有不少的研究和解决方法。在这些方法中,JPS(Jump Point Search)类的算法在性能上表现优异,而且空间占用合理。《天谕》手游中实现了基于JPS算法的寻路,并且已经应用在服务器端部分场景的寻路业务需求中。
3.1快速的JPS寻路
最短路径算法实际上是一个搜索的过程,其时间消耗取决于搜索节点的数量。我们最熟悉、且应用最广泛的寻路算法之一是A*算法。传统的A*算法在实际测试中发现,大量的时间消耗发生在最小堆的和其他数据集合的操作上。这些操作主要是因为A*探索的策略是搜索当前节点所有相邻节点,然后找出F值最小的一个进行下一次迭代。这样的策略会产生不少的冗余搜索。

JPS算法是一种基于A*算法的快速寻路算法,该算法认为A*算法在搜索相邻节点上花费了大量时间,而最后决定最终路径的节点应该是一些决定路径方向变化的“拐点”。类似这样的“拐点”,算法中提出了“跳点”(Jump Point)的概念(“跳点”与“拐点”并不完全相同,具体含义参考JPS算法的相关文章,本文不做详细介绍)。 所以JPS算法和A*的最大区别是确定后继节点的策略,JPS会根据特定的联通方向确定后续的搜索节点(跳点)。而JPS算法比A*算法快速的一项重要实践依据是:从算法实现上,确定跳点的过程可以利用一些编程技巧和手段变得非常迅速。所以在实现JPS算法时一定要注意搜索跳点的代码实现。因为理论上跳点是根据当前方向上若干个相邻格子的联通情况确定的,存在一些隐性的遍历成本。如果这个过程没有处理妥当,则可能搜索跳点过程比A*的周围相邻节点的搜索还要慢,也是一些“为什么我写的JPS比A*要慢”问题的原因。
根据前面的内容可知,《天谕》手游中体素的遍历操作最为耗时的是访存操作,所以在Runtime搜索跳点必然是非常耗时的。所以我们采用的基本策略是预处理,提前烘焙好每一个体素格子四个联通方向上的跳点位置,这样一来搜索跳点的操作简化为了一次访存操作而不是同一方向上的若干次体素格子访问。预处理是提升效率效果最为明显的手段,除此之外还有诸如剪枝、优化数据结构等方法。这篇文章中总结了一些不同的优化手段:最快速的寻路算法 Jump Point Search,读者们可以根据自身项目的要求、数据结构采用不同的优化方法。经过这些手段可以使JPS算法比同数据结构下的A*算法快几十倍甚至上百倍。
JPS算法接入《天谕》手游的动机是为了解决寻路网格的寻路结果和体素碰撞产生冲突的问题。所以正式接入项目要接受的一大考验是性能,这个性能不仅要和基于体素的A*做对比,而是要与基于导航网格(Navmesh)的寻路做对比。基于导航网格的寻路也是一种A*算法,但是它所需要处理的数据集合远比基于体素格子的数量要小(可能相差1-2个数量级)。在经过一系列优化手段之后,我们完成了一部分的性能目标。《天谕》手游中基于JPS的寻路效率在短距离上略优于Navmesh,中远距离不及Navmesh。但是在服务器中短距离寻路的应用情景是最多的,比如AI机器人的追赶。所以JPS从可用性上达到了要求。《天谕》手游目前JPS算法只应用在服务器中,考虑到JPS的路径效果、寻路时要使用全图内存的机制,没有应用到客户端的地表寻路中。
图4.3 《天谕》手游中JPS路径效果JPS算法的原理和实现具体可以参考论文 Online Graph Pruning for Pathfinding on Grid Maps,wiki上也可查询到不少该算法相关的论文。《腾讯编程精粹》一书中也有JPS寻路相关章节。
另外,因为基于格子的寻路在远距离表现上欠佳的问题也有解决方案,例如GDC的这篇基于层级结构的远距离动态寻路,这个算法利用不同粒度的格子数据组成不同的层级,先找到原距离上一条粗粒度的可达路径,然后异步慢慢细化最终路径。有兴趣的同学可以参考一下。
3.2 三维空间寻路
基于体素的寻路相比于NavMesh还有一项优势,那就是体素有更加完整的三维空间数据结构,具备三维空间寻路的可能性。《天谕》手游中在客户端实现了基础的三维空间寻路,并应用到了海底寻路、部分任务的空中寻路等。三维空间寻路需要注意的一大问题是如何妥善处理由二维扩展到三维时带来的数据规模变化。如果处理不好就意味着低下的性能表现,甚至无法正常的在项目中使用。前面提到过,《天谕》手游体素采用的是经过合并的高度场数据,这样的数据其实已经在第三维高度空间上做出了简化(可以理解为同一水平面上不同高度的体素数量是一个较小的常数)。这样的数据为三维空间寻路带来了便利,空间寻路采用A*算法,然后对A*算法的每个节点进行一个特殊设计。空间寻路中的每个搜索节点不再是一个平面上的“质点”,而是平面上一个带有空间体积的节点,代表着一段玩家可以移动的物理空间。数据上可以通过将某个平面上的空间不连续的Span数组进行集合的取反操作获得,这个语义上也较好理解,体素代表着空间上不可达的实心碰撞,取反代表着空间上可达的“空气”空间。另外,水中的水体素,也可以在水下寻路时视为这样的“空气”空间。这样的设计可以使A*节点的搜索数量控制在合理范围内。节点和节点的联通性判定也有一些不同,即两个节点是否联通取决于两段“空气”空间的交集是否能够容纳一个人的高度。算法结束后,根据若干“空气”空间节点进行路径细化,转为一条最终玩家移动的路径。《天谕》手游中的空间寻路效果如图4.3所示。
图4.4 空间寻路效果示意截止投稿时,当前版本的空间寻路只是同步执行,考虑到性能问题只允许逻辑层进行较短距离的空间寻路。在接下来的开发中会扩展为异步寻路,以支持远距离空间寻路的业务需求场景。
4、触发机制
前面提到过每一块体素数据上除了上下表面高度,还有一个32位的自定义数据。当前版本的自定义数据由层级Layer、物理材质Material、触发器Trigger组成。对于Layer属性可以判定玩家是否在水面上,判定是否是空气墙等。Material属性决定玩家移动到某个格子上时播放什么样的音效和脚底特效,比如走在海边上会脚下会泛起水花,踩在木板上有嘎吱的声音。 Trigger属性决定一些玩法上的逻辑,比如是否是PK区域、Boss区域、隐身区域、钓鱼区域等等。 其中Layer和Material来自于美术资源上的设置,Trigger来源自于策划完成的相关玩法区域编辑。玩家在任意时刻都维护一个脚底的体素格子数据,在玩家移动且跨越体素时更新。基于体素的触发机制,触发的范围形状可以自定义且不会影响到效率,同时支持在服务器中做严格的校验。
图4.5 游戏中的触发器示意(隐身草丛)五、有没有制作相关的工具?
1、 自动烘焙
游戏开发阶段,场景会有频繁的修改和迭代。《天谕》手游中在持续集成工具Jenkins上布置了自动任务,每隔2个小时进行一次全游戏场景的体素烘焙和提交。美术同学也可以自行在修改后烘焙体素上传。
2、可视化查看
可视化查看体素是必不可少的工具,以便美术同学和QA同学对场景中的碰撞进行检查。可视化后的体素可以清晰的看到体素的形状、层级信息、触发器ID等等。游览方式可以是操控场景相机导航查看,或者运行时随着人物移动查看。效果可以参考前文的标题图、图2.3、图4.5等。
3、数据刷工具
策划同学可以在体素上布置自定义的触发器数据,工具支持用刷子在单个体素上刷数据,也支持用类似PS钢笔工具的方法勾出一片区域,如图5.1所示。
图5.1 触发区域编辑视图4、自定义编辑工具
可以在场景中对已有的体素进行修饰,例如去掉一块多余的体素,拉高一部分区域的体素等等。目前《天谕》手游种的自定义编辑工具还有待完善。
总结和遐想
本文从体素的基本概念出发,结合《天谕》手游项目的实践经验,介绍了体素数据引入的缘由、通过光栅化网格获取体素数据的方法、经过合并的高度场体素数据结构、碰撞检测、寻路等算法实现以及项目中体素的相关工具链。《天谕》手游的体素方案仍有不少不完善的地方,后续的开发中还会持续的改进。
当然,走出项目之外,体素数据在渲染上有着更为成熟和广泛的应用。例如广泛应用在医学与生物研究领域的Volume Rendering、实时渲染中依据快速的体素射线检测原理延伸出的一类光线追踪算法等等,例如Voxel Cone Tracing,具体内容可以参考资料1,资料2,资料3。体素数据既可以用来渲染,也可以用来做物理、做Gameplay。个人拙见,不同的系统使用相同的组件数据,这样整个产品可以最终呈现给玩家更加统一真实的世界,而不是存在各种穿模、不自然的物理模拟、逻辑和表现相差较大的问题。这里引一个使用体素模拟水流且完成渲染的例子,如图6.1所示。体素既作为物理模拟的节点,也作为渲染的节点。
图6.1 体素水流模拟示意 来源:Fast Fluid Simulations with Sparse Volumes on the GPU图6.1 体素水流模拟示意 来源:Fast Fluid Simulations with Sparse Volumes on the GPU
这样的系统局限于当前的硬件条件无法实现一个理想的效果,不过在未来某个硬件条件和软件系统需求相对平衡的时间节点,可能会诞生出一个玩家体验发生质变的作品。
笔者能力有限,没有对这些领域进行深入的挖掘,有兴趣的读者可以参考上面提到的文献进行进一步的研究。
最后想说的是,《天谕》手游的体素系统不是一人一朝一夕之功,感谢项目组大佬们和同事们的指点和付出,感谢每一位参与《天谕》手游体素系统开发的同伴,感谢业界同行无私的技术分享。站在巨人的肩膀上!