本文是Hello Games的主程Innes Mckendrick在GDC 2017上关于No Mans Sky(无人深空)球形地形实时生成与渲染方案的实施细节分享的学习总结,文末给出了原始视频地址。
首先看下无人深空的项目背景:
小团队,最开始整个团队只有四个人,其中三个程序一个美术,即使到做这个演讲的时刻整个团队也就20多人,其中10多个程序,好处就是可以实现敏捷开发,快速迭代,坏处就是任何决策都必须谨慎
程序驱动,说的是程序驱动,但从后面描述来看,更多是用户体验驱动,不太过于看重策划文档
放大美术需求
无人深空是用自研引擎开发的,引擎用C++写的,自研引擎的好处就是对于自定义需求可以得到很好很快的支持,而如果使用商业引擎的话,可能需要投入更多的人力,这对小团队而言是很重的负担,此外在项目启动的13年,UE跟Unity都不具备项目所需要的voxel based地形方案,这也是其中选择自研引擎的一个原因。
引擎是具备跨平台支持的(PC跟PS),由于内容需要涵盖多个星球,每个星球的面积都很大,因此离线存储与运行时装载到内存中都是不现实的,因此无人深空最终选择的是运行时的world generation而非离线创建,不过运行时生成就需要面对传统游戏中贴图烘焙地形预生成等做法所不会面临的运行时计算压力,而且由于运行时只会生成较少的局部信息,因此游戏的玩法就只能局限于局部空间,无法添加需要全局要素支持的玩法逻辑。
整篇文章的结构跟GDC PPT结构保持一致。
1. World Structure
这一节主要介绍整个世界的架构,包括球体的分布,星球的构成,voxel的应用等等。
World Structure中首先需要介绍的是星球的实现,由于无人深空支持玩家在星球之间通过飞行器进行穿梭,在星球内也可以自由飞行,且Hello Games希望玩家在探索的过程中感受到星空的辽阔与自身的孤寂,因此希望将星球设计的比较大,在星球的实现上,Hello Games做了很多探索,下面我们一起来看下他们的探索结果与心得体会。
1.1 平面映射到球面
第一个想法是在星球上直接使用平面来代表球面(当半径较大的时候,表现是基本一致的),而当远离星球进入太空,则会将表面弯曲变成一个球体。
这种映射方案的优点在于y轴永远朝上(不是背离球心),从而使得simulation跟地形的生成都会变得简单,具体而言,当玩家在星球上沿着X或者Z轴行走,在运行时会不断的生成新的地形来填补视野的空缺(这里的一个问题是,生成的内容是否会tiling从而使得玩家沿着某个方向走上一段之后产生绕着星球走了一圈的感觉?理论上应该需要添加这种处理,不过视频中没有介绍这部分内容)。当玩家乘坐飞行器起飞离开星球的时候,就会通过shader用地形把球包起来,为了避免玩家看到极点处的扭曲,这里会将极点设置为在远离玩家的位置。
这种方案会导致当飞到太空的时候,会在极点位置处看到不正常的扭曲(pinching),不过这个现象不是很明显,所以不是太大的问题。
比较严重的问题是,玩家会在星球上行走,而持续的行走会导致一种叫做"swimming coordinates/texture"的现象,这里没有解释这种现象的具体表现与原因,网上也没有找到相关说明,后面有更详细的了解再来补充。
另外,由于是根据玩家的XZ坐标来生成平面地形的,而这种生成方式得到的最多是两个圆柱体的交集,如下图所示,没有办法完全匹配到球面,因此就会导致玩家可能会出现在一些根本不存在的坐标上,而这些坐标如果想要分享给游戏好友,而由于生成方式是受玩家降落点影响的(简单来说就是以降落点为圆柱与,那么就会出现好友根据坐标跳转并不能跳转到玩家当前的位置。
上述效果为两个垂直正交的圆柱体相交的效果,通过CSG.js完成模拟,模拟代码为:
var a = CSG.cylinder({ radius: 0.5, start: [-0.5, 0, 0], end: [0.5, 0, 0] });
var b = CSG.cylinder({ radius: 0.5, start: [0, -0.5, 0], end: [0, 0.5, 0] });
a.setColor(1, 1, 0);
b.setColor(0, 0.5, 1);
return a.intersect(b);
理论上来说,无法根据访问的坐标抵达同样的位置可以通过一些方式解决掉,但是这个方案还有另外一个问题,那就是由于这个运行时生成过程是无限循环的,那么在坐标是通过float表达的情况下,就会出现浮点精度问题,而要想解决这个问题,就需要额外的处理机制对坐标进行wrap(重置,类似于tiling),而这个处理过程消耗不低,这里说到都快比原始的生成算法开销还高了,因此这个方案只能算是一个尝鲜的naive版本。
1.2 Map Transform
Hello Games想到的第二个方案就是类似于我们日常生活中的世界地图投影方式,通过一定的映射规则完成2D平面贴图到球体的映射。他们通过对wiki上的各种世界地图投影方式进行实验,尝试找到一种distortion最小的投影方式。
不过在搜寻的过程中发现并没有一个十分契合需求的方案,因为这里我们不但需要从2D平面映射到球面,同时还要从球面映射回2D平面,而其中很多算法在无法保证两个方向的映射实现都十分高效。
另外有一些算法可以在性能上与效果上都能满足需要,如上图左侧的2D平面贴图映射到右边的球面贴图上,但是这种方案存在一定的限制,那就是星球必须要包含一定尺寸的海面,通过压缩海面的占比来实现效果的融洽,对于地球来说这种方案是可行的,但是Hello Games希望能够生成各种类型的星球,其中就包含了大量的不带海洋的星球,这种方案就不太实用了。
1.3 球面坐标系方案
Hello Games最终决定尝试使用球面坐标系完成模拟,这种方案是最简单直观的,但是会有很多额外的成本在里面。
一般来说,如果地形的生成使用球面空间坐标系,那就需要解决星球上的球面坐标如何跟星系的3D坐标衔接起来的问题,而这里的做法是直接在星球上使用3D平面坐标系。
Hello Games在采用这种方案后导致的一个直观问题是,up方向不再是(0,1,0)了,而是需要根据当前所在的位置与球心所在的位置计算得到,而且这个情况也会导致物理计算中的重力永远指向球心,因此物理计算也需要做额外的处理,此外一些面向3D平面空间的第三方库将不再可用;不过这些问题都还不算困难,而且up方向的计算也很简单高效。
采用这种方案遇到的第二个问题是毛球问题,根据毛球理论,3D球面上不存在一片连续的与球面相切的非零向量场(向量长度非零),也就是说在3D球面上的切线向量场中必定有一些位置的切向量长度为0,而这种情况对于游戏中的一些计算比如光照(除了光照还有很多其他你想不到的需要使用切线的场合)来说是十分不利的(因为光照经常会需要使用切线空间来计算对应的法线,当切线为0就会导致光照效果异常),切线为0导致的大部分问题在一些场合下可以通过将3个垂直平面投影到球面上来解决(triplanar projection,没太懂,具体怎么做?是相当于将cube上的切线投射过去吗?),在另外一些场合下可以将一个long single plane的法线投射到球面上来解决,另外,还可以考虑通过local mapping方案来解决,比如以玩家抵达星球的点作为初始点,不断沿着星球表面创建连续切线,从而通过将0值切线放到星球背面来解决这个问题,虽然切线不再是deterministic(即不再是与落脚点无关的绝对数值分布),但是可以保证连续,不过这种做法对于新加入项目的同学来说可能需要一定的理解成本。
另外,采用球面坐标系的问题还有数据(比如voxel数据)要如何存储,这里的做法是,simulation(地形生成)采用球面空间完成,但是当需要将数据存储下来时,则会将球面空间映射到3D平面空间,使用cube的方式进行存储(从这个角度来说,对于球面地形而言,其使用的高度图就从原来的2D贴图变成了cubemap)。
从上面的对比图可以看到,这个数据存储的映射关系是从cube出发的, 也就是说对于cube上细分的每个顶点找到球面上对应的映射点,并将结果取出来存入对应的位置即可。voxel数据怎么存储呢,看起来也是使用cube作为发起方,不过这样做在进行计算模拟(按照上下文的说法,使用体素进行地形生成的过程是在球形空间中进行的,假设marching cube算法也是在这个空间中进行的,那么其计算所得结果与将voxel数据映射到cube,之后从cube读取出来的结果是一致的?)的时候不会有问题吗?
确实,这种方式是会有一些失真,不过丢失的信息只有voxel的密度以及每个voxel占据的的空间大小,只表现为对应位置的数据精度有所下降,这个瑕疵是可以接受的。
说到cubemap,一个经常被问到的问题是,为什么不直接在cubemap上完成地形生成过程,Hello Games这边应该曾经尝试过这种方案,不过在边界(多面交接处)处遇到了众多的问题,经常会有QA跑过来告诉开发团队哪哪又出问题了,太过揪心且至今没能找到一劳永逸的解决方案。
这里需要注意的是,Hello Games说的使用sphere进行模拟,并不是说是在球面(sphere surface)空间进行相应计算,实际上是在以球体表面作为基础高度的偏移值或者说高度图的方式在进行相应计算模拟,而这种偏移方式或者说高度图的方式可以直接转换为cube上的高度图来实现。
上图算法给出了根据星球上的相对坐标(相对于球心的坐标,这个坐标是3D平面空间的,而非球面空间的)计算出cube上的对应位置的具体实现,可以分成如下几步:
首先计算出当前位置相对于球体表面的偏移(可以理解为海拔),这一步只需要计算相对坐标(也可以看成是球心出发的向量)的长度减去星球半径即可,这个数值后面会用到
计算这个位置在球面上的投影点,跟上一步类似,只需要对相对坐标(向量)的长度缩放到星球半径即可
根据投影点坐标,可以计算出对应位置在cube上对应surface上的法线(这一点有点不解)
根据对应位置以及法线计算出与上一步cube上的surface的交点,如下图中的红色圆点所示
-
在这个交点的基础上加上法线与此前的球体海拔偏移就得到了cube上对应的3D坐标点
- 按照这种计算方式相当于将下图中的球面压扁至与cube重合,也就是说,相当于省去了球体与cube之间的space,但实际上我们完全可以将这一段加上,得到更为正确的显示效果的,不知道是我的理解存在问题,还是出于其他的考虑(如计算复杂度以及浮点精度等)
此外,在voxels数目固定的情况下(也就是分辨率确定的情况下),需要模拟的地形高度越大,其对应的voxel的精度也就越低,且当需要模拟的地形高度越大,这种方案模拟的distortion会变得越来越明显,出于这个考虑,这里选择使用voxel来模拟球体上128m厚度的地形效果。
不过如果只能模拟128m的体素地形的话,就没有办法得到大家想要的高山跟深海效果,Hello Games这里的做法是在地形上添加一个低频噪声,使得基础地形高度不再是平滑的球面,而是如上图右侧小图所示的具有高低起伏的曲面(这个高低起伏的效果称之为elevation,上下范围为600~1000m,足以实现高山跟深海效果),之后在这个基础上再添加128m厚度的voxel层。
上面是添加了elevation data之后的计算逻辑。
下面来介绍一下voxel地形方案的细节。这里将整个地形分割成一个个的chunk或者说region,每个region对应于32x32x32个voxels,每个voxel对应一米的尺度,考虑到voxel在三角化的时候边界效果难以表达(顶点是在8个voxel中间创建的?所以最外层的voxel通常无法表达所有的效果),因此在32的基础上左右各加一列得到34,此外,为了避免因为精度问题导致的两个region之间的seams,这里还需要再在外面加一层,从而使得两个region的三角化结果能够完美衔接起来。
各个region的处理是独立的,每个region处理完成之后就对应着一片区域的地形。每个voxel包含了6个bytes的数据,其中
2个bytes的density数据,每个byte对应于两种不同的材质(也就是说,总共对应了4种材质的密度?)的密度,这些材质用于表明地形是什么类型的,比如grass/moutain/rock/sand等,这些材质除了后面进行贴图采样得到正确的渲染效果之外,还会影响到放置到上面的物件类型与放置规则
Material Type x 2对应的是什么意思,对应着当前voxel上的四种材质?
2个bytes用于给出材质的浓度(the extent to which something is rock or grass),这个数据有什么用,用于进行材质混合?这个extent跟前面的density的作用有什么区别?
由于数据压缩非常低效,因此目前这块的存储都是没有经过压缩的,后面可能会考虑优化。
前面36x36x36精度的region是近景区的用法,对于一些远离相机的区域,就没有办法使用这么高的精度了,这里就需要考虑LOD数据,将8个高精度的voxel组合成一个低精度的voxel,这里总共设定了6级LOD,且这里为了避免接缝,视线中相邻位置的不同LOD的Region依然会共面(共用一个voxel)。
数据存在如上图所示的八叉树中,这种做法的好处就不需要多说了,这里需要注意的是,前面说过voxel数据是存储在cube上的,如右边小图中的红色正方形就表示对应的cube,因此各个voxel是围绕着cube存放的,也就是说红色方框外面的才对应着真正的voxel数据。
即使使用了八叉树进行数据存储,依然会有不低的内存消耗,对于一颗大星球而言,即使使用最低的lod voxel也会超过内存的上限,因此这里还增加了一个更低的lod voxel。
整个太阳系对应着一棵相同的八叉树,在星球间切换的时候,不会尝试创建新的region,而是会将之前的八叉树拿出来重用。不过,在玩家乘坐飞船飞向太空的时候则是另外一套做法,这个时候会创建新的region,不过由于星球像素占比小,因此消耗的内存也很少。这个时候,星球基本上就是一个采样cubemap的模型了。
2. Generation Pipelines
这一节会一步步的介绍如何在运行时完成整个世界的创建与生成,包括地形如何从无到有的生成(比如voxel数据是如何填充的),并能够正确的渲染(voxel数据如何绘制)与体验,并会介绍与之相关的线程逻辑等细节。
No Mans Sky的数据生成都是在运行时完成的,其中在加载阶段会完成一部分工作,比如一些PCG所需要的贴图的生成,加载模型对应的vertex数据等,之后在体验的时候再渐进式的完成地形创建(星球过大,不会一次性完成整个星球的生成,而是只创建局部数据,通过类似贴图streaming等策略不断更新)等剩余工作。
上面是生成管线的主要执行步骤:
Generation对应着voxel数据的填充,这个过程是通过噪声函数实现的,每次填充是以region作为基本单位(多个region的生成可并行?)
当某个region的density数据已经生成完毕之后,就会进入region的三角化,最终得到一个mesh,这个阶段跟上一个阶段都是在cube sapce完成的。
之后将mesh映射到球面上
在mesh的基础上完成物理网格的创建
在mesh的基础上完成navmesh数据的生成
在地形的基础上添加其他物件,比如植被,建筑,动物以及其他游戏道具
整个生成过程不会放在主线程进行,而是通过job system来完成的,引擎中的线程管理比较简单,包含一个主要的update线程跟一个主要的图形渲染线程,之后在每帧会进行一次同步,地形的生成由于需要占用较多的资源,为了避免影响到性能,这里会尽量避免这块的计算与上述两个线程之间的交互。
地形的生成计算在PC上是通过CPU完成,而在PS4上则是通过CS(Compute Shader)来完成。
上图中的黄色字体对应的是需要与主线程进行交互的工作,除了这些工作之外的其他生成工作都是在jobs中完成。
由于相机是被一个个的region所环绕的,因此考虑到性能以及玩家体验,这里需要一套良好的生成策略,如上图所示,这里根据到玩家的距离为region分派了对应的优先级,其中数字越小表明优先级越高(其实就是越近的region优先级越高)。最开始的时候会快速创建一些粗精度的lod数据,对应的是上图中的紫色方块,之后根据优先级逐个逐个完成较高精度的lod的生成,对应的是图中的黑色方块,整个过程的执行速度还算比较快。
另外也可以看到,生成方块并没有环绕在玩家四周,而是优先生成相机前方的region的数据。
噪声的生成算法这里不会多说,在Sean Murray的GDC 2017演讲中会介绍其中的细节,youtube上有对应的视频,有兴趣的同学可以去搜索来看看。
No Mans Sky的生成过程是自顶向下的,每个阶段的输入都来自于上一个阶段的输出,之后又会输出更多的东西,直到最终得到自己想要的voxel数据。
solar system的输入是一个position种子,之后会输出solar system的相关信息,比如某个星球距离太阳多远,这个星球的大气层或者说天空的表现如何
将上一个阶段的输出加上每个星球的position种子传入到星球生成阶段,这个阶段会输出星球的整体风格数据,比如是岩石类星球还是悬崖林立类星球或者其他风貌的,星球上主要地貌占比等。
最后将上个阶段的数据传给地形生成阶段,这个阶段会输出voxel数据。
上面各个阶段输出的数据都是通过xml格式进行存储,这样做的好处是调试方便,可以随时知道哪个环节存在问题。
地形生成阶段包含了一系列的子阶段,比如贴图生成、voxel生成等,其中各个子阶段之间可能存在交互,因此这里需要为整个生成过程添加一些整体约束:
directable对应的是美术同学要有足够的控制空间,需要通过调整参数得到想要的效果,consistent对应的是同样的参数要有同样的输出
实时生成
整个地形很大,需要在不同的地方有不同的地形效果,避免重复导致的无聊感
为了提升玩家的新奇感,这里需要能够支持真实世界中的地形以及真实世界中不存在的一些地形效果
希望整个系统是模块化的,能够自适应的,从而可以实现快速的添加与修正
最后一点是需要保证数据是局部的,也就是说,每个region的生成过程需要是相互独立的,但是有需要保证各个region能够平滑衔接,这个比较麻烦,想象一下水体对地形的侵蚀,如何通过局部数据完成全局的侵蚀效果是一个很大的挑战。
为了对整个过程做详细说明,这里先以2D平面的地形生成过程为例。
第一个阶段就是在2D平面上划出一些区域,比如上面这张图中就划分出山脉、平缓区域(Smooth),河流以及Rocky区域等四块。这些数据不是使用voxel来存储的,而是用来标注对应位置的voxel在mountain/smooth/river/rocky等类型地形上的倾向程度。
虽然这块的数据会占用较大的空间,但是由于只是临时存储一下,等到切换到3D空间后,这块的数据就会销毁,所以不会有太多的问题。
这里的每个数据对应的是一个voxel colume(指的是从上到下的128个voxels?),而整个星球的地形数据就类似于heightmap(这里应该要使用cubemap)
这是没有做任何处理的星球地形,很平整,没有任何的起伏,就是一个球体。
这个是加上了低频elevation噪声后的地形效果图,球体上有了高低起伏的效果,不过由于没有添加高度图数据,所以是比较平滑的星球。
在上一步的基础上,添加了高度图效果(这里的高度图其实应该不是传统的高度图,而是根据前面生成的地形类型贴图,添加对应的噪声得到的效果),这一步还没有涉及到对应的地形材质的处理过程,因此整个地面都是sandy & flat & brown的。
在2D贴图的基础上,这里考虑通过voxel来生成3D地形,这一步跟前面的2D贴图一样,也是需要添加各种噪声效果,包括simplex、perlin或者其他的比如voronoi或者cellular噪声等。除了噪声之外,这里还会添加一些turbulence效果,目的是为了创建一些类似pinching之类的漂亮效果。
不过通过3D噪声使用voxel来创建地形会遇到的一个问题是,最终生成的地形可能会出现3D空间的blob data(分离的大块数据),这些data会导致一些异常的效果,比如大块的浮空岛屿等在现实中不存在或者不符合物理定律的效果。因此,这里需要通过一定的方法对这些异常数据fade off处理甚至裁切处理,一种策略是根据voxel的高度不断降低其density,从而保证在mountain或者hill上随着高度的增加不会出现density还增加的异常
虽然最终模拟的效果是在球体上,但是voxel数据前面说过是存储在cube space的,之后在需要的时候在映射到球面上,从而避免distortion(这里说的不是很详细)。
在前面的3D地形的基础上,这里还可以通过添加或者删除voxel中的数据实现一些特殊效果,比如这里可以将地形挖空做成洞穴。
完成前面的3D数据生成后,我们就得到了一系列的voxel,每个voxel上包含了density跟material数据,不过voxel要转换成polygons只需要density数据就够了。
voxel转换成polygon最常用的是marching cube算法,不过这里没有使用这个算法,使用的是其改进版的dual contouring算法。之所以没有使用marching cube,是因为当我们有8个voxels组成的一个大的voxel,我们想要对这个voxel进行polygonize的话,最终生成的三角面片只会lie on在这些小的voxel的edge上面,而这种方式对于创建corner(比如两堵墙的锐利夹角)效果就不太方便,会丢失很多关键信息。
关于这个技术的具体细节可以参考上面图中下方给出的两个链接。
根据前面的内容,我们最终可以得到上图中的山脉效果。对于voxel数据,No Man Sky会在一些情况下使用不同的三角化方案,比如上图中的cube就是直接采用类似“我的世界”中的三角化方案输出的,虽然这种方案会需要消耗较多的顶点的,但是其三角化方案的计算消耗是很轻的。这种三角化方案对于一些高密度voxel紧邻着低密度voxel的情况会有作用,比如对于一个mountain与一块air(无任何填充)相邻的情况,就可以创建一个flat plane之后在上面添加cube(没看出来这么做的意义是啥,估计是哪些关键信息没有传达出来)。这种做法会创建较多的顶点,因此只会在距离相机较近的区域采用。
这里对于水面则是采用第三种三角化方案,flat plain polygonization,这是一种十分廉价的三角化方案。这里需要注意的是,由于前面的elevation数据的存在,如果只是使用voxel数据来填充的话,可能会导致水面存在凹凸不平的情况(低频噪声),这里在三角化的时候需要根据星球上的水域分布对这种情况做处理,使之符合物理规律。Hello Games也说了,这种方式其实不太合理,后面会考虑摒弃使用三角化的方式来生成水面,而是会考虑通过在shader中完成水面的生成(implicitly)。
最终生成的地形数据除了顶点数据之外,还包含上述一些数据。
法线有两套,分别对应smooth normal与face normal,其中前者用于进行贴图采样,后者用于进行光照(还是没看出来,为什么不可以使用同一套?),face normal用于计算光照会使得渲染效果看起来有点粗糙,不过这种方式却能使得能够在surface表面得到一个连贯的blend效果(?)。
这里还会为每个region筛选出两个材质,前面说过,每个voxel中都包含两个材质,在最终的效果中最好的效果是每个voxel的材质效果都能够兼顾到,但是这种做就会很低效,这里的做法是从voxel中选出出现频次最高的两个材质,虽然其他材质的丢失会使得效果存在一些偏差,但是从整体上来看也没发现太大问题(PCG的还好,如果这是提供给用户的一个工具,说不定就差评爆表了)。
在得到上述数据之后,就可以尝试通过triplanar方法进行贴图采样了。按照这种采样方法,在blend zone上可能会预计会得到较为奇怪丑陋的blend artifacts,但是由于地形的形状导致这个blend是均匀分散到整个星球的,因此虽然有一些瑕疵但是看起来并不明显。
前面说到的贴图采样,实际上这里地形使用的基础贴图是一系列的噪声贴图,而真正的输入是高度图(也就是小图中的左上角的图),而顶点中存储的材质blend value实际上对应的是不同材质的贴图组成的atlas(这里有点绕),这里会根据之前的材质数据选择对应的两种材质(小图中的两行分别对应两种不同材质的输入贴图),之后基于高度数据对两者进行混合。
另外这里需要注意的是,地形的贴图数据有两套,分别对应着低精度与高精度版本,当玩家飞起来进入太空的时候,会逐渐从高精度过渡到低精度,反之则从低精度过渡到高精度。低精版本会负责覆盖一块更大的区域,而高精版本则是覆盖一块较小的区域,通过这种方式来降低消耗,这里只介绍了基本的理念,具体的实施细节,这里没有聊到。
地形生成完成之后整个过程并不是就全部完成了,还有很多收尾工作要做,比如物件的摆放,物理碰撞以及NavMesh的生成等,实际上这块的时间消耗不比地形的创建时间消耗要低。
首先第一步要做的就是在地图上完成物件的摆放,这块的时间消耗其实比较高,因此在PS4中也是通过compute shader完成,基本算法与地形生成所使用的的噪声算法十分相似。
上面这张图是俯视图,从色块的整体分布来看,有经验的同学可以比较容易看出这其实跟simplex噪声很像,实际上这里的物件摆放就是在噪声的基础上添加cutoff规则,也就是说将低于某个值的噪声直接清零,只保留噪声数据中较高密度的部分。
物件摆放是遵循美术同学的布局风格进行的,比如说地形上有一棵树,之后在下面添加灌木会使得效果变得好看,之后再添加一些小石块还会继续美化效果,按着这种套路继续下去就会使得场景变得越来越自然美观,而这里的做法是通过噪声来完成上述过程。
这里在物件摆放的时候,为了对物件摆放的密度进行控制,还给出了一种offset grid的方案,将场景划分成一个个的cell,对每个cell中的某种物件数目施加约束,通过这种方案可以保证同类物件之间的摆放距离。
这里说到,物件的摆放也会考虑地形的lod,在不同的lod上会摆放不同精度的物件,比如较为粗糙的lod地形上只会摆放大型的物件(比如树木),之后会添加灌木,以及杂草等更为精细的物件。
这里给出的是同一个物件在不同LOD精度的地形上是如何实现position适配从而保证物件不会出现悬空或者深陷地底的效果,从图中描述来看,就是在地形LOD精度发生变化时,根据高度差做了一个偏移,只是不知道物件朝向是否会发生变化。在地形LOD切换的过程中,也同时完成了物件(也就是树木)的LOD切换,比如可能会从imposter切换到真正的模型。
这里还给出了一个视频来介绍最终的实现效果,从视频中可以看到位置的变化是逐帧进行的,也就是说是平滑过渡的,而非直接跳变的,这样看起来也更为精美。
建筑的摆放也借助了类似的生成算法,建筑是需要能够在太空中看到的物件,因此就需要在玩家进入星球之前就应该能够知道任何位置都有哪些建筑,也就是说,需要一个技术,只需要指定球上的一点,就能够找到最近的建筑物,因此这里最终使用的是offset grid技术。
将地形分割成regions,之后就可以找到每个点相对于region中心点的偏移,之后就可以根据这个偏移生成对应的数据。
3. Simulation with Real-time generation
这一节主要介绍运行时生成的世界是如何完成更新与模拟的,在这个过程中,由于世界是实时生成的,且当前的可视范围内不会覆盖全图,因此会需要考虑到很多未生成的数据的影响,会需要作一些特别的考虑。
simulation需要解决的首要问题是场景中的动物的行走路径,AI行走范围,行走方式如果没有仔细处理就会得到很奇怪的表现。
这里为动物生成了一系列的path,这些动物的行走可以遵循这里路径。不过只有当玩家十分靠近的时候才会进行动物的创建,不过动物创建完成之后,即使玩家远离也不会立马销毁,还会保留一个距离。
物件的渐入渐出是通过dither完成的,通过传入shader的[0, 1]范围的可见度数值,可以控制物件的消隐程度。这种做法在玩家突然飞入到某个区域的时候会看到明显的渐变过程,但是当玩家在星球上平滑飞行的时候,这个渐变过程是不明显的。
这里为植被添加了随着距离fade的效果,当相机远离的时候,会逐步消除较远处的植被(比如草),当相机靠近的时候,则会不断创建新的植被。
参考
[1]. Continuous World Generation in No Mans Sky