基于Unity3D的体素沙盒游戏设计与实现(上)
Posted 爱编程的鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Unity3D的体素沙盒游戏设计与实现(上)相关的知识,希望对你有一定的参考价值。
基于Unity3D的体素沙盒游戏设计与实现
摘 要
随着计算机硬件和软件技术的逐步发展,世界游戏开发行业也在日益壮大,涌现出不少优秀的作品,逐渐成为各国文化创意领域一张闪亮的名片。本文以全球知名的沙盒建造游戏我的世界(Minecraft)为灵感,以Unity3D为开发平台,融入了星际元素,创作了一款以太空探索为主题的体素化沙盒建造游戏。
主要工作内容如下:首先是星球的地形创建,基于2D柏林噪声实现了星球6个表面的基础地貌创建、边界平滑和地形翻转,并根据地势生成草坪、树木或水源,并在星球内部根据3D噪声生成洞穴、矿物。为减轻单个网格(Mesh)的绘制量,将星球分为若干的区块。根据得到的方块数据,绘制方块与空气接触的面,实现每个区块Mesh的构造,完成星球的地形创建。其次是玩家与星球的更新,通过射线检测,动态修改区块Mesh,实现自由建造和拆除方块的功能,并调用基于对象池、协程回收的粒子系统,再更新玩家的背包数据。每个星球都分为6个引力区域,用于方块纹理朝向判定和修改玩家重力方向。再者是星系系统。在完成每个星系随机生成参数的星球后,以玩家所属星球为静止参考系,动态计算并更新其他星球的位置,并通过允许玩家在不同的星球间飞行转移并更新天空盒,使用高光着色器以营造星球的大气层效果。除此之外,还制作了用户图形界面,点击设置可以对音乐、音效、全屏模式等属性进行调整,而存档功能则是通过序列化方式存取数据实现。
1 绪 论
1.1 引言
随着科技的进步和经济水平的提高,世界的电子游戏行业也在不断优化资源配置,逐渐形成完善的产业链,有着规模化和产业化的发展趋势,成为许多人日常生活中不可或缺的调味剂。随着游戏作品质量的提高和种类的逐步细化,越来越多人对于游戏的不同的需求得到了满足。诸如以自由度闻名的开放世界、沙盒类游戏,则是满足了用户对于沉浸式的虚拟世界的好奇心和探索欲[1]。
1.2 开发背景
1.2.1 沙盒概述
沙盒(Sandbox)原指装满沙子一个区域,孩子们可以随心所欲地用沙子搭建城堡、长城等建筑。故沙盒游戏被引申为一种“可以在游戏的世界中自由探索,与各种元素随意交互,改造世界”的游戏设计理念。近年来,沙盒游戏在全世界范围内日益风靡,其极高的自由度不仅能让玩家沉浸其中,还能轻而易举完成很多现实生活中实现不了的创意和想法[2]。
1.2.2 平台概述
Unity是由Unity Technologies开发的一个让用户轻松创建诸如三视频游戏、实时3D动画、建筑可视化等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎[3]。而Unity3D支持的语言之一C#,则是由C和C++衍生出来的一种稳定、可靠、简单、安全的面向对象编程语言。它不仅继承C和C++强大功能,还简化了某些原有的复杂特性。它综合了C++的高运行效率和VB简单的可视化操作,其创新的语言特性、强大的操作能力、优雅的语法风格和便捷的面向组件编程支持使之成为.NET开发的首选语言[4]。本文使用C#作为脚本编写语言。
1.3 国内外发展与现状
1.3.1 沙盒发展史
对自由度的追求在世界游戏史早期,就已体现在了设计之中。如1980年欧美早期RPG重要作品《创世纪》就已具备了一个开放大世界地图。虽受当时技术限制较为简陋,且地图并不像后来的开放世界一样采用无缝衔接,但仍为玩家提供了一个可供自由探索的世界。随后1983年《Elite》问世。它是第一个采用3D网格画面的PC端游戏。玩家在游戏里扮演一名宇航员,在太空中穿梭完成任务,探索不同星球。沙盒游戏“让玩家的行为来影响游戏世界的生态”的特性便由此而来[5]。随后16年间,从《Elite》到《孢子》,其开放概念基本一以贯之。将沙盒类游戏推向全新高度则是《我的世界》。
这款自2009年的独立游戏既无华丽的画面或复杂的系统,也没有任何明确的任务目标,更是抛开传统游戏中剧情竞技等元素。玩家只需通过破坏和建造行为,加上天马行空想象力,就能将一个个体素方块组合成家具、房屋乃至城市,创造出现实中不存在的神奇世界[6]。其颠覆式的高自由度玩法,将沙盒游戏的特点诠释得淋漓尽致,也使该作一举成为了游戏界的黑马,不仅其销量至今久居不下,更被评为高分沙盒佳作。
1.3.2 以体素构建的三维世界
随着真实感图形技术以及计算机图形学的快速推进,同时伴随计算机系统的革新,计算机图形领域因此得到迅速和长远的发展。三维地形建模逐渐成为计算机图形学的一个研究热点。多数游戏的大世界使用三维三角形网格(3D Triangle Mesh)建构,其中包括地形、建筑、植被及其他静态物件。如大部分RPG、MOBA类型游戏等一些以自然户外环境为主的游戏,则会使用高度场(Height Field)去表示地形;一些室内为主的游戏,如一些FPS、TPS、ACT等,会使用构造实体几何(Constructive Solid Geometry,CSG)技术去创建基础的室内环境,这在许多游戏引擎中称为二叉空间分割(Binary Space Partitioning,BSP),即笔刷[7]。
由于游戏世界占总制作成本的一大部分,而随着游戏平台的性能提升,以及游戏内容需求的膨胀,故其制作成本也在不断提高。以上制作方法各有优缺点。三维网格是较自由的建模方式,也是合乎当代硬件的游戏世界表示方式,且有成熟的数字创作工具(Digital Content Creation Tool)如 3ds Max 和 Maya[8]。但不容易修改(尤其是在展开UV后)、仅为表面表示方式(可能无法判断一个任意点在其外还是其内)、建模成本高、不容易做连续或离散级数的细致程度(Level Of Detail,LOD)等则是其缺点。高度场和 BSP 较容易修改和实现 LOD,且制作成本较网格低,但其适用场合较为局限。有无更好的三维游戏制作方式一直是游戏界重要探索方向。
自2009年Minecraft成功后,体素(Voxel)进入开发者的眼球,成为另一种建构游戏世界的可行方式。以二维空间的像素来类比,体素则是其三维版本。在二维中,我们可使用二维颜色数组表示一个图像(Image);在三维中也可以用体素的三维数组表示一个栅格化的三维空间,每个体素储存一个比特,以表示该空间为空心或实心。这种二元体素(Binary Voxel)是最简单的体素形式[9]。当然,体素还可以储存其他属性。例如在医学上会把CT扫描得来的X射线不透光性(Opacity)储存在体素中,而在Minecraft中则会使用体素储存空间的方块种类(例如泥土、石头、水)等各项信息。
相对于高度场地形及BSP,体素还可以制作一般地表、山洞、建筑等更多自然或人工场景[10]。由于体素数据的结构均匀且简单,相比于网格而言则更容易修改。因此,游戏可以让玩家建设游戏世界(此为用户生成内容,User-Generated Content,UGC),也可以依照游戏规则动态地改变游戏世界,例如地形修整(Terrain Morphing)、可破坏对象(Destructible Object)等。除了修改,这使得无中生有也成为可能。这类沙盒游戏通常都包含程序式生成内容(Procedurally Generated Content),比如可以创建一个大型的RPG地图及丰富的游戏性内容等。
本文则是以Minecraft的玩法为出发点,将场景和视野放在更为广阔的宇宙,基于Unity3D引擎,旨在实现一款以太空探索、自由建造为主题的体素沙盒游戏。
1.4 设计与开发的内容
2 星球区块网格的创建
在本文中,由若干个体素构成的星球是玩家能够进行奔跑、跳跃、建造、拆除等基础活动的一块区域。在一般的开放世界游戏中,玩家无法对现有地形进行改造,故地形网格只需提前制作好再导入Unity3D烘焙即可。但对于支持随机生成、实时更新地形的沙盒游戏而言,如何构建每个星球的渲染和碰撞网格则是本章需要探讨的主要问题。
2.1 噪声的选取
2.1.1 普通噪声
一个噪声函数从本质上来说就是一个有种子的随机数生成器,它基于输入参数返回一个随机数,如果用同一个参数输入两次,则噪声函数两次产生相同的返回值,这一特性非常重要,便于控制噪声函数值域[11]。沙盒游戏的核心之一是地图的随机生成。
代码清单 2.1 普通噪声的应用
public void BuildTerrain()// 地形创建
for (int x = l1; x < l2; x++)
for (int z = l1; z < l2; z++)
for (int y = 0; y < Random(seed).Next(1,20); y++)
blocks[x, y, z] = Block.Dic[BlockType.Grass]; // 生成草方块
案例
要生成一个以体素方块为最小单元的地图,如果仅使用普通噪声实现,得到的将会是如图2.1所示的无规律地形。这就需要使用一个更加连续平滑的噪声算法来创建地形,而柏林噪声(Perlin Noise)则很好的满足了这一需求。
2.1.2 柏林噪声
Perlin Noise,译作柏林噪声,是由肯·柏林于上世纪八十年代提出的可以在某一空间生成连续的噪声,噪声函数是一系列随机噪音叠加起来产生的连续函数[12]。在1983年,他不满足于当时计算机产生的极不自然纹理效果,因此提出了此噪声。而后被迅速应用到各种商业软件中。随后他继续研究程序纹理生成,并提出了超级纹理(Hypertexture)。他们使用分形噪声实现了更多复杂而奇妙的效果。到1990年,已有大量公司在他们的产品中使用了柏林噪声。
在各种需要连续随机变量的应用场景往往会广泛使用柏林噪声,例如水面波纹效果或是各类沙盒游戏中的地形生成。它能很好地模拟云彩、火焰、奇形怪状的山体,以及树木和大理石表面等,故常用于制作凸凹贴图。在本文中,柏林噪声用于生成星球起伏的地表、洞穴、矿物资源。在此运用噪声尝试生成一小块区域,如图2.2所示。
代码清单 2.2 柏林噪声的应用
var xSample = (x + dx + seedX) * scale; //采样x=(x+dx+种子seedX)*选区大小
var zSample = (z + dz + seedZ) * scale;
var noise = PerlinNoise(xSample, zSample); // 根据采样x和采样z生成noise
var y0 = (int) (height_max * noise) + grass_min;
blocks[x, y0, z] = Block.Dic[BlockType.Grass]; // 生成草方块
2.2 柏林噪声原理
柏林噪声基于随机,并在此基础上通过缓动曲线进行平滑插值,使最终计算效果更加趋于自然。基于不同的采样空间,柏林噪声可以分为不同维度,常用的有二维和三维等。不同维度的基本原理相同,通常经过以下三个步骤[13]:
(1) 初始化相关数据,包括建立噪声对应的排列表(Permutation Table)和梯度表(Gradient Table)等。
(2) 建立采样空间。对于二维柏林噪声而言,一个二维坐标系为其采样空间,坐标系中横纵坐标为整数的地方均有一点。三维柏林噪声则同理。
(3) 可以根据最近的参考点的缓动曲线和梯度进行插值计算,以应对于采样点在不同空间的不同类型噪声。
2.2.1 晶格划分
以二维柏林噪声为例,其可以表示为输入二维向量(x, y)和seed随机种子的函数,输出为取值通常约在[0,1]的浮点数。
晶格
首先将空间划分为尺寸为size的晶格,二维空间下为矩形,N 维空间下即是 N 维超立方体,该噪声图的空间尺度由晶格大小size决定。
对于空间上一点pos=float(x, y),将该点所属晶格四个角落顶点分别命名为p0, p1, p2, p3,其中p0 = floor(pos/size)*size作为晶格左下角坐标,当前点在晶格内的相对偏移dp等于到左下角顶点的单位距离。
2.2.2 伪随机梯度生成
接着,对晶格中每个顶点生成一个伪随机梯度,表示为一个二维向量,该随机向量必须跟晶格的位置与随机种子相关,即对于相同的晶格位置 (Xg, Yg) 和 seed 应该生成相同的梯度向量。随机梯度由伪随机函数生成随机的x和y并归一化而得出。将四个顶点的伪随机梯度分别命名为g0, g1, g2, g3。
2.2.3 晶格内插值
随后分别计算当前点pos相对晶格四个顶点偏移向量d0,d1,d2,d3。分别计算偏移向量di与晶格顶点梯度gi的点积作为v0,v1,v2,v3,根据当前点在晶格内的相对偏移dp,分别对四个点进行插值计算最终结果。
2.2.4 分形噪声
通常对于自然界中复杂的噪声现象而言,仅通过晶格化随机梯度生成的二维噪声图过于平滑,难以模拟。即便缩小晶格尺寸,也只能提升噪声图的颗粒感。故可通过将多种不同晶格尺寸噪声图叠加得到更加自然的分形噪声图[14]。比如洞穴生成所使用的3D柏林噪声便用到了分形噪声叠加。
2.3 方块结构
方块是地形构建的最小单位。使用枚举类型BlockTypeAir,Dirt,Grass,Stone…列举所有的方块种类。使用字典Dic存储BlockType,方块实例以便之后查找引用。方块类Block实例拥有各自的方块状态(空气、不透明、透明等)、破坏或建造时的音效索引、纹理坐标数组uvs、粒子纹理坐标数组uvsParticle等。
方块根据纹理种数可分为单面型、双面型和三面型,分别拥有1,2,3组纹理坐标。如草方块是三面型,其本地空间里的顶面、侧面、底面纹理坐标各不相同。而橡木是双面型,其顶面和底面纹理相同,侧面不同。钻石矿则是单面型,其各面使用的纹理坐标均相同。在了解柏林噪声原理并确定方块结构后,便开始正式构建地形。
2.4 地形构建
2.4.1 初始化基底
当对每个星球类对象执行初始化函数Init时,首先对其基底进行初始化构建,填充相应的方块。如代码清单2.3所示。
代码清单 2.3 森林行星基底初始化
private static void BuildTerrain(Star s) // 构建地形
const int l1 = L / 2; // 基底边界坐标1,其中L=64
const int l2 = (int) (L * 1.5); // 基底边界坐标2
Star.Fill(s, l1 + 2, l2 - 2, BlockType.Stone); // 填充石头
Star.Replace(s, l1, l2, BlockType.Dirt, null); // 将null替换为泥土
Star.Fill(s, L - 1, L + 1, BlockType.Bedrock); // 中心填充基岩
...
每个Star类实例拥有各自的Block[,,]数组,用于存储该星球的全部方块数据。恒星、行星、卫星的基底通常为边长32、64、32的正立方体。Fill函数表示在Block数组索引为[l1+2,l2-2]之间内均设为石头方块BlockType.Stone,使用Replace函数在相应索引区间内将null替换为泥土方块。并在正中心填充基岩方块
2.4.2 表面构建
每个星球均有6个表面,在此将这6个面依次编号为0-5。其中用dx、dz表示每个表面的采样点相对偏移量,其中第0面(Y轴正方向)的dx和dz均为0。而temp数组则记载每个表面待翻转映射的方块数据。
以森林行星为例,其每个表面的temp数组最多可以存储64x32x64的方块数据,俯视图为64x64的矩形。接着,根据先前得到的采样点相对偏移量、随机种子、采样选区大小算出俯视图上每个单元格的采样点坐标(xSample,zSample),代入函数GetY求出每格的地势高度y0,即本格可生成方块的最大高度。最高处生成草方块,即令temp[x,y0,z]存储草方块对象,以便后续网格创建和更新。接着,在草方块以下、基底之上填充泥土。
代码清单 2.4 计算采样点求出该处的地势高度并生成方块数据
for (var x = 0; x < L; x++) // L=64
var xSample = (x + dx + s.seedX) * s.scale; // 采样x=(x+dx+随机种子seedX)*选区大小
for (var z = 0; z < L; z++)
var zSample = (z + dz + s.seedZ) * s.scale;
var dBoundary=l1-1-Star.LayerJudge(x, z, l1-0.5f); // 与边界距离=层级总数-当前层级
var y0 = s.GetY(xSample,zSample, dBoundary); // 根据柏林噪声确定y0并边界平整
temp[x, y0, z] = Block.Dic[BlockType.Grass]; // 生成草方块
for (var y = 0; y < y0; y++) // 在其下面填充泥土
temp[x, y, z] = Block.Dic[BlockType.Dirt];
…
2.4.3 边界平滑
GetY函数使用了2D柏林噪声以判断地势高度。根据输入的采样点坐标得到噪声返回的0-1区间值noise,再乘以方块最大落差dHeightMax并加上方块生成最小高度topMin(默认为0),得到本格地势高度topHeight。当然,为了能让每个表面的边界间衔接得更加流畅,还需对边界格进行平滑处理。根据输入的边界距离dBoundary判断该格地势是否需要进一步处理,若满足且该格地势超过对应比例的限制,则使用插值函数Lerp降低该格相应比例的高度。返回该格的最终地势高度。
代码清单 2.5 GetY函数返回每格的最终地势高度
public int GetY(float xSample,float zSample,float dBoundary)
var noise = Mathf.Clamp(Mathf.PerlinNoise(xSample, zSample),0f,1f); // 根据采样xz生成noiseY
var topHeight = (int) (dHeightMax * noise) + topMin; // 顶层方块高度
var smooth = Mathf.Pow(2,chunkPer+1); // 平滑最大距离
if (dBoundary<=smooth) // 平滑边界Y
if (topHeight >= Mathf.Lerp(2,(dHeightMax+topMin)/2,dBoundary/smooth))
topHeight = (int) (Mathf.Lerp(0.1f,1.0f,dBoundary/smooth) * topHeight);
return topHeight;
2.4.4 翻转映射
在此,根据之前已经生成的方块数组temp[,,]和对应的表面编号,翻转至相应的星球方块数组Block[,,]对应索引区间里。初步得到星球表面地貌
2.5 资源生成
2.5.1 植被生成
植被是部分生命星球的天然资源,也是玩家获取木材的主要来源。在生成初步地表方块数据后,在数组翻转映射之前,将进一步对地表植被进行构建。对于森林和冰原行星而言,其地表主要生成的植被为橡树。首先,根据所在格顶层方块高度y0进行判定,当该格最大生成高度超过行星对应的海平面高度SeaLevel时,则调用BuildOak进行一次橡树的生成尝试。详见代码清单2.7。
代码清单 2.7 橡树生成
private static void BuildOak(int x, int z, int y0, Block[,,] temp, int dB) // 随机生成橡木
if (Random.value > oakValue || dB < 2) return; // 处于边缘则不生成
var h = Random.Range(oakMin, oakMax); // 树干随机高度
for (var y1 = 1; y1 <= h; y1++) // 防密集生成
for (var x1 = -2; x1 <= -1; x1++)
for (var z1 = -2; z1 <= 2; z1++)
if (temp[x + x1, y0 + y1, z + z1] != null &&
temp[x + x1, y0 + y1, z + z1].type == BlockType.Oak)
return;
for (var z1 = -2; z1 <= -1; z1++)
if (temp[x, y0 + y1, z + z1] != null && temp[x, y0 + y1, z + z1].type == BlockType.Oak)
return;
for (var y1 = 1; y1 <= h; y1++)
temp[x, y0 + y1, z] = Block.Dic[BlockType.Oak]; // 生成树干
…
当随机值不满足该星球的植被生成概率或该格处于星球边缘时,则跳过。接着进行树干防密集生成判定,根据确定生成树干的随机高度对其周围已确定y0的若干格进行检测,若周围已存在橡木,则跳过。接着便开始生成树干BlockType.Oak和多层树叶方块BlockType.OakLeave
对于沙漠行星而言,则在地表生成不同高度的仙人掌方块。
2.5.2 水源生成
在森林和冰原行星上会生成水方块。在俯视图上,将地势高度低于海平面的单元格定义为低洼格。由若干低洼格构成的区域定义为低洼区域。如图2.12所示,黄色格子为低洼格,绿色格子为非低洼格。
当低洼区域周围地势全高于海平面,则称该低洼区域满足封闭条件(若低洼区域与边界相连,则不满足),即满足生成水池的条件。如图2.13所示,蓝色格子代表水源。满足生成池塘条件的低洼区域共有4个。
首先,使用_isPool记录该低洼区域是否封闭,使用一个数组_maxY记录每格最大高度,使用一个列表_Pool存储当前低洼区域已到访过的索引。在前面进行表面构建时,对于非低洼格则令_maxY[x,z]=-1表示该格为不可能生成水,而对于低洼格则令_maxY[x,z]等于当前高度。
接着对x、z区间为[1,L-2]的格子进行遍历(L为一个区块的长度,对于森林行星而言,即为该表面的长度)。如果_maxY<0或者>99则跳过,表示此格为非低洼格或已到访过的低洼格。反之则判断此低洼格所在的低洼区域是否符合水池条件。
对于低洼区域的判定,可使用深度优先或广度优先搜索。
代码清单 2.8 使用DFS判断该低洼区域是否满足水池条件
private static void JudgePool(int x,int z)
if (_maxY[x, z] < 0 || _maxY[x, z] > 99) return; // 遇到高地/已到访过的低洼格则中止
_maxY[x, z] += 100; // 将该低洼格标记为到访过,并对其4个邻格进行判断
if (x > 0)
JudgePool(x - 1, z);
else // x<=0 此低洼格位于边界
_isPool = false;
if (x < L - 1)
JudgePool(x + 1, z);
…if (!_isPool) return; // 该低洼区域已经不符合标准则中止
Pool.Add(x);
Pool.Add(z);
同样,如果此格非低洼或已到访过则跳过。反之则令_maxY[x,z]加上100表示此低洼格已到访过,并对其4个邻格进行深度优先遍历。如果低洼格位于边界,则令_isPool为False。如果周围四格都遍历完后此格所在区域仍满足条件,则令Pool列表添加该格索引x、z。
当DFS结束后,如果_isPool仍为True,则表明该低洼区域可以生成水。将Pool存储的所有索引代入y0=_maxY[x,z]-100得到该低洼格顶层方块高度,并在海平面至顶层方块之间生成水,顶层方块以下生成沙子。接着清空Pool列表,重复上述步骤,直至遍历完[1,L-2]的单元格
2.5.3 洞穴和矿物生成
深入洞穴探索、挖矿是玩家获取资源的主要途径。洞穴位于星球基底内部,使用3D柏林噪声构建。由于Unity3D本身并不支持3D噪声,故通过将2D噪声的分形叠加以模拟3D噪声。详见代码清单2.9。
代码清单 2.9 3D噪声模拟函数
public static float Noise3D(float x, float y, float z,float scale,int octave = 2,float dMine=0)
x += dMine; y += dMine;z += dMine; // 选区偏移
var noise = 0.0f;
var amplitude = 1.0f; // 振幅
for (var i = 0; i < octave; i++) // 迭代octave次
// 获取每个轴的所有噪声排列
var xy = Mathf.PerlinNoise(x * scale, y * scale) * amplitude;
var xz = Mathf.PerlinNoise(x * scale, z * scale) * amplitude;
var yz = Mathf.PerlinNoise(y * scale, z * scale) * amplitude;
// 每个轴噪声的反向排列
var yx = Mathf.PerlinNoise(y * scale, x * scale) * amplitude;
var zx = Mathf.PerlinNoise(z * scale, x * scale) * amplitude;
var zy = Mathf.PerlinNoise(z * scale, y * scale) * amplitude;
noise += (xy + xz + yz + yx + zx + zy) / 6.0f; // 平均
scale *= 2.0f;
amplitude *= 2.0f;
return noise / octave; // 使用所有迭代的平均值
其中xyz均为采样点坐标值,scale决定生成频率(即采样区域大小),默认迭代次数octave=2,采样区偏移dMine=0。每次迭代都获取每个轴的所有噪声排列及其反向排列,求出平均值并累加至噪声noise,同时频率和振幅翻倍。多次迭代后返回最终平均值作为本格3D噪声[15]。
以不同随机种子,对基底区间每格计算100次Noise3D返回值并求平均值,得到的噪声值平均分布、方块生成率(即生成数量占总基底方块数量的比例)与返回的最大噪声值的关系
观察图2.17可以发现,方块生成率随最大噪声值呈正比。如果想让某种矿物的生成率约为10%,最大噪声值应设为0.6。即只有当某格对应的Noise3D小于0.6时,才允许生成该矿物。
每种矿拥有独立的采样区偏移量dMine,每格以空气、煤矿、铁矿、泥土等为优先级依次判断是否满足生成条件,若均不满足保持原样(即生成石头)
值得注意的是,由于柏林噪声具有连续性,故最大噪声值越大,每簇矿物群的生成规模也就越大,生成率因而随之越大。洞穴本质是在基底内部生成空气方块,故控制洞穴的生成规模等价于控制空气方块的生成规模。
令行星中心2x2x2的基岩方块对应的层级为0,某格所在层级编号等于其与中心x、y、z轴距离的最大值。铁矿只在低于15层生成。
对于森林行星而言,洞穴空气生成率约为6.75%,煤矿0.96%,铁矿0.072%,泥土1.37%,花岗岩1.38%。
论文笔记:NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis
目录
1 Neural Radiance Field Scene Representation (基于神经辐射场的场景表示)
2 Volume Rendering with Radiance Fields (基于辐射场的体素渲染)
3 Optimizing a Neural Radiance Field (优化一个神经辐射场)
3.1 Positional Encoding (位置编码)
3.2 Hierarchical Sampling Procedure (分层采样方案)
3.3 Implementation Details (实现细节)
文章摘要
NeRF 是 ECCV 2020 的 Oral,影响非常大,可以说从基础上创造出了新的基于神经网络隐式表达来重建场景的路线。由于其简洁的思想和完美的效果,至今仍然有非常多的 3D 相关工作以此为基础。
NeRF 所做的任务是 Novel View Synthesis(新视角合成),即在若干已知视角下对场景进行一系列的观测(相机内外参、图像、Pose 等),合成任意新视角下的图像。传统方法中,通常这一任务采用三维重建再渲染的方式实现,NeRF 希望不进行显式的三维重建过程,仅根据内外参直接得到新视角渲染的图像。为了实现这一目的,NeRF 使用用神经网络作为一个 3D 场景的隐式表达,代替传统的点云、网格、体素、TSDF 等方式,通过这样的网络可以直接渲染任意角度任意位置的投影图像。
NeRF 的思想比较简单,就是通过输入视角的图像每个像素的射线对于密度(不透明度)积分进行体素渲染,然后通过该像素渲染的 RGB 值与真值进行对比作为 Loss。由于文中设计的体素渲染是完全可微的,因此该网络可学习:
其主要工作和创新点如下:
1)提出一种用 5D 神经辐射场 (Neural Radiance Field) 来表达复杂的几何+材质连续场景的方法,该辐射场使用 MLP 网络进行参数化;
2)提出一种基于经典体素渲染 (Volume Rendering) 改进的可微渲染方法,能够通过可微渲染得到 RGB 图像,并将此作为优化的目标。该部分包含采用分层采样的加速策略,来将 MLP 的容量分配到可见的内容区域;
3)提出一种位置编码 (Position Encoding) 方法将每个 5D 坐标映射到更高维的空间,这样使得我们可以让我们优化神经辐射场更好地表达高频细节内容。
1 Neural Radiance Field Scene Representation (基于神经辐射场的场景表示)
NeRF 将一个连续的场景表示为一个 5D 向量值函数(vector-valued function),其中:
- 输入为:3D 位置 x=(x, y, z)x=(x,y,z) 和 2D 视角方向 (θ,ϕ)
- 输出为:发射颜色 c=(r, g, b)c=(r,g,b) 和体积密度(不透明度) σ。
对于其中的 2D 视角方向可以用下图进行直观解释:
在实际实现中,视角方向表示为一个三维笛卡尔坐标系单位向量 d,也就是图像中的任意位置与相机光心的连线,我们用一个 MLP 全连接网络表示这种映射:
通过优化这样一个网络的参数Θ来学习得到这样一个 5D 坐标输入到对应颜色和密度输出的映射。
为了让网络学习到多视角的表示,我们有如下两个合理假设:
- 体积密度(不透明度) σ 只与三维位置 x 有关而与视角方向 d 无关。物体不同位置的密度应该和观察角度无关,这一点比较显然。
- 颜色 c 与三维位置 x 和视角方向 d 都相关。
预测体积密度 σ 的网络部分输入仅仅是输入位置 x,而预测颜色 c 的网络输入是视角和方向 d。在具体实现上:
- MLP 网络 Θ 首先用 8 层的全连接层(使用 ReLU 激活函数,每层有 256 个通道),处理 3D 坐标 x,得到 σ 和一个 256 维的特征向量。
- 将该 256 维的特征向量与视角方向 d 与视角方向一起拼接起来,喂给另一个全连接层(使用 ReLU 激活函数,每层有 128 个通道),输出方向相关的 RGB 颜色。
本文中一个示意的网络结构如下:
Fig 3 展示了我们的网络可以表示出非朗伯体效应(non-Lambertian effects);Fig 4 展示了如果训练时没有视角 (View Dependence) 的输入(只有 x),则网络无法表示高光效果。
2 Volume Rendering with Radiance Fields (基于辐射场的体素渲染)
2.1 经典渲染方程
也就是二者的乘积为光速 cc。而我们又知道可见光的颜色 RGB 就是不同频率的光辐射作用于相机的结果。因此在 NeRF 中认为辐射场就是对于颜色的近似建模。
2.2 经典的体素渲染方法
我们通常接触的是渲染有网格渲染、体素渲染等,对于很多类似云彩、烟尘等等特效,常用的就是体素渲染:
Ray casting for volume rendering (© Zhanping Liu).
2.3 基于分段采样近似的体素渲染方法
下图非常形象地演示了体素渲染的流程:
3 Optimizing a Neural Radiance Field (优化一个神经辐射场)
上述方法就是 NeRF 的基本内容,但基于此得到的结果并能达到最优效果,存在例如细节不够精细、训练速度较慢等问题。为了进一步提升重建精度和速度,我们还引入了下面两个策略:
- Positional Encoding (位置编码):通过这一策略,能够使得 MLP 更好地表示高频信息,从而得到丰富的细节;
- Hierarchical Sampling Procedure (金字塔采样方案):通过这一策略,能够使得训练过程更高效地采样高频信息。
3.1 Positional Encoding (位置编码)
相似地,在 Transformer 中也有一个类似的位置编码操作,不过本文中与其还是根本不同。在 Transformer 中位置编码是用来表示输入的序列信息的,而这里的位置编码是做用于输入将输入映射到高维从而让网络能够更好地学习到高频信息。
3.2 Hierarchical Sampling Procedure (分层采样方案)
分层采样方案来自于经典渲染算法的加速工作,在前述的体素渲染 (Volume Rendering) 方法中,对于射线上的点如何采样会影响最终的效率,如果采样点过多计算效率太低,采样点过少又不能很好地近似。那么一个很自然的想法就是希望对于颜色贡献大的点附近采样密集,贡献小的点附近采样稀疏,这样就可以解决问题。基于这一想法,NeRF 很自然地提出由粗到细的分层采样方案(Coarse to Fine)。
Fine 部分:在第二阶段,我们使用逆变换采样 (Inverse Transform Sampling),根据上面的分布采样出第二个集合 N_fNf,最终我们仍然使用公式 (3) 来计算 。但不同的是使用了全部的 N_c + N_fNc+Nf 个样本。使用这种方法,第二次采样可以根据分布采样更多的样本在真正有场景内容的区域,实现了重要性抽样 (Importance Sampling)。
如图所示,白色点为第一次均匀采样的点,通过白色均匀采样后得到的分布,第二次再根据分布对进行红色点采样,概率高的地方密集,概率低的地方稀疏 (很像粒子滤波)。
3.3 Implementation Details (实现细节)
4 Results (实验结果)
本文对比了很多相关的工作例如:
- Neural Volumes (NV):GitHub - facebookresearch/neuralvolumes: Training and Evaluation Code for Neural Volumes
- Scene Representation Networks (SRN):GitHub - vsitzmann/scene-representation-networks: Official Pytorch implementation of Scene Representation Networks: Continuous 3D-Structure-Aware Neural Scene Representations
- Local Light Field Fusion (LLFF):GitHub - Fyusion/LLFF: Code release for Local Light Field Fusion at SIGGRAPH 2019
4.1 Datasets (数据集)
作者对比了在不同数据集中的表现,可以看出基本上所有数据集上都是遥遥领先的:
在仿真数据集上的可视化效果:
在真实数据集上的可视化效果:
4.2 Ablation Studies (消融实验)
我们在数据集 Realistic Synthetic 360◦ 上进行了不同参数和设置下的消融实验,结果如下:
主要对比的是如下几个设置:
- Positional encoding (PE),即 x
- View Dependence (VD),即 d
- Hierarchical sampling (H)
其中:
第1行表示不包含以上任何一个部分的最小网络;
第2-4行表示每次分别去掉一个部分;
第5-6行表示样本图像更少时的效果差异;
第7-8行表示频率 LL (也就是位置编码x 的频率展开级别)设置不同时的效果差异。
论文小结
本文最大的创新点就是通过隐式表达绕过了人工设计三维场景表示的方法,能够从更高维度学习到场景的三维信息。但缺点是速度非常慢,这一点在后续很多工作也有改进。另一方面本文的可解释性,隐式表达的能力,依然需要更多工作来探索。
但归根结底,相信这样简洁有效的方式,未来会成为 3D 和 4D 场景重建的革命点,给三维视觉带来新的爆发。
论文下载
PDF | Website | Code (Official) | Code (Pytorch Lightning) | Recording | Recording (Bilibili)
Colab Example:Tiny NeRF | Full NeRF
参考材料
[1] Mildenhall B, Srinivasan P P, Tancik M, et al. Nerf: Representing scenes as neural radiance fields for view synthesis[C]//European Conference on Computer Vision. Springer, Cham, 2020: 405-421.
[2] https://www.cnblogs.com/noluye/p/14547115.html
[3] https://www.cnblogs.com/noluye/p/14718570.html
[4] https://github.com/yenchenlin/awesome-NeRF
[5] https://zhuanlan.zhihu.com/p/360365941
[6] https://zhuanlan.zhihu.com/p/380015071
[7] https://blog.csdn.net/ftimes/article/details/105890744
[8] https://zhuanlan.zhihu.com/p/384946242
[9] https://zhuanlan.zhihu.com/p/386127288
[10] https://blog.csdn.net/g11d111/article/details/118959540
[11] https://www.bilibili.com/video/BV1fL4y1T7Ag
[12] https://zh.wikipedia.org/wiki/%E6%B8%B2%E6%9F%93%E6%96%B9%E7%A8%8B
[13] https://zhuanlan.zhihu.com/p/380015071
[14] https://blog.csdn.net/soaring_casia/article/details/117664146
[15] https://www.youtube.com/watch?v=Al6NTbgka1o
[16] https://github.com/matajoh/fourier_feature_nets
相关工作
DSNeRF:https://github.com/dunbar12138/DSNeRF (SfM 加速 NerF)
BARF:https://github.com/chenhsuanlin/bundle-adjusting-NeRF
PlenOctrees:https://alexyu.net/plenoctrees/ (使用 PlenOctrees 加速 NeRF 渲染)
https://github.com/google-research/google-research/tree/master/jaxnerf (使用 JAX 实现加快训练速度)
参考网址
以上是关于基于Unity3D的体素沙盒游戏设计与实现(上)的主要内容,如果未能解决你的问题,请参考以下文章
论文笔记:NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis