从概念上讲,PCG(程序化生成)的含义很广:任何通过规则计算得到的内容,都可算作是PCG。但在很多游戏项目的资料,包括本篇,讨论PCG时特指是:用一些算法/工具(特别是Houdini)生成美术内容(特别是大世界场景)。
PCG的核心是算法,即到底通过什么样的逻辑来生成内容。算法可以直接通过代码编写,或者使用Houdini节点表达,或者借助GPU快速的并行计算能力。但本篇并不会讨论算法,而是算法之外的问题,往往被称为“流程”或“管线”。
在给本篇写标题时,我又觉得我所讨论的问题并非都能归为“流程”或“管线”,毕竟“流程管线”也是个模糊的概念。而我所讨论的,就只是纯粹的“问题”。另外,这个“问题”也包含两层含义,这里用英文表示或许更准确:
所以,标题是:算法之外的问题(Quesion)与问题(Problem)。之后的小节也以Q与P为单位讨论(需要说明:本篇主要讨论了问题是什么,并不讨论如何完全解决,因为不同项目背景不一样,而且很多问题我也不知道怎么解决🤷):
生成:程序化生成
生成计算:执行的一次生成过程
生成器:执行生成计算的实体
用户:执行生成计算,监督其生成结果,调整生成器的参数
流水线:运行在非用户的机器(构建机)上,自动执行的逻辑。
首先要考虑的问题是:
场景中会有哪些类型的内容,而其中的哪些涉及到程序化生成?
(下截图来源UOD2021重生边缘分享)
一种类型的内容是否由程序化生成,有多个考虑因素:
而比较头疼的就是处于上面二者中间的内容,它数量说多不多说少不少、效果不能太差但也不会要求太高、程序化生成虽然难但是也能开发,手动也不是不能做。这些内容就要凭经验去决定到底要不要程序化生成了。
目前比较常见的生成内容如:地形高度和材质分布、植被等实体的分布、河流面片。
但不同的项目类型会区别很大。如果场景只是地牢迷宫,那就不用考虑复杂的河流,而专心于生成墙面等地牢的元素。如果场景是现代都市,那么地形会相对平坦,就不用考虑生成山体的各种自然效果,而更关注高楼大厦的生成。如果游戏是赛车游戏,那么最重要的应该就是赛道和周边景物的生成了。
而不同艺术风格也会影响需要生成的内容,比如一些细腻的自然模拟效果,可能就无法在卡通风格的场景中很好的复现。
不同生成内容,都会有最适合的数据管线与交互方式。因此在最开始,搞清楚要生成什么东西是最重要的。
但实际上,很难在一开始就真的搞清楚所有要生成的东西。做到最后总会发现和一开始的计划有差别,总会有“想少了”和“想多了”的情况。
在我看来,“想多了” 的危害更大,因为它意味着白白耗费了工作量做了不需要的东西。而如果“想少了”只要后续添加上就行——但前提是管线流程的设计可以支持添加新的东西。
对于同一种类型的内容,其控制度也需要考虑。例如,对于 “路” 这个内容:
FarCry5分享中可以看到其路的走向是由用户画曲线控制的:
而GhostRecon分享中提到他们是根据算法自动计算道路的,因为他们的路太多了。这样,GhostRecon的路其控制度就会相对更低,但换句话说是自动化度更高。
不仅是“手动”和“自动”的区别,就算是手动控制,也有控制度高低的区别。
比如对于河流,使用一条简单的曲线就可以调控走向。但如果河流的宽度也要控制呢?那简单的曲线就无法满足了,需要更复杂的一些方式。
需要何种程度的控制,取决于项目自身的美术与Gameplay需求。
黑客帝国是一个很酷的程序化生成项目,但是如果作为游戏项目,就需要注意其控制度是否能满足需求。
【Q1】考虑了哪些类型的内容要程序化生成。理想情况下,当然是这种类型的内容完全由程序化生成。
但有时会有不得不让手动去做的情况。例如:虽然我的植被分布由程序化生成,但对于某处区域,其植被的分布真的很敏感,每棵树在哪都要精确的手动控制。
这时候就需要划分职责了,常见的做法是通过地形上指定mask来划分。
Ghost Recon 提到他们使用了 “减法” 的思路。即程序化生成默认生成所有的地方,而美术师可以划分一片区域来表示这里不需要程序化生成。
那与之相反的就可以称为 “加法” 的思路了。对于那些不想太依赖程序化生成,而只想在某些小范围使用程序化技术的项目会比较有用。
如果美术师/设计师认为生成的结果不好,想要修改结果怎么办?
这里的问题是:当下次再生成时,手动所作的修改怎么办?
默认情况下肯定是无法保留的,蜘蛛侠分享中也说了:“美术和设计师不应该插手任何程序化生成的内容,他们必须使用程序化系统中可控的参数和输入来影响效果。如果他们不这么做,就有丢失他们的工作(手工劳动)的危险。”
因此,一般是不允许手动对生成结果修改的。如果有修改的需求则首先要从其他方面找问题:
如果其他方面真的都无法解决,那么就要考虑这个内容是否是最后一次生成,或者说保证手动修改后不需要再次生成,或者说手动修改的丢失是可以接受的(比如虽然还会重新生成,但是频率很低,而手动的修改很小,那么此时每次生成后都再修改一次,也是可以接受的)。
最后,如果以上方面都不能解决,那就真的要建立某种机制来在每次重新生成时保留之前的手动修改了。这样的问题没有通用的解决方式,只能根据“生成内容”和“所要做的手动修改”做专门化的设计。
生成的执行者有两种:
例如,FarCry5每晚会执行流水线,完全程序化生成一遍世界:
GhostRecon分享中提到,当用户提交涉及到地形修改时,也会触发流水线:
Worldbatch(世界批量化工具)有助于自动完成任务,或允许您随时更新世界使用的任何类型的数据。
Worldbatch 基于 Houdini python (hython) 并使用 “Hqueue” (SideFX公司的)作业分配系统在“构建渲染农场”上启动数据“作业”。一个作业加载 Houdini 资产 (.otl) 并根据需要设置参数并从该 otl 启动数据渲染过程。
它可以在UI模式下单独使用,用户可以直接从世界地图中选择他想要重新计算的内容,并将作业发送到自己的计算机上,或发送到渲染农场。
我之前提到过 Perforce 插件,当用户在引擎中提交涉及地形的更改列表时,Worldbatch 也将自动启动,Worldbatch 将由命令行脚本启用,并将基本作业发送到农场。以确保即使地形发生变化,数据也是最新的。
“用户在编辑器内交互式生成”和“流水线自动生成”有着相反的优缺点:
用户交互式生成 | 流水线自动生成 |
---|---|
优:可不断调整参数 | 缺:固定的参数 |
优:可确认生成结果 | 缺:可能提交错误的结果 |
缺:消耗人力成本 | 优:完全自动,可夜间执行 |
缺:需要用户有一定经验,且可能失误 | 优:完全按照程序执行 |
程序化生成参与度较高且人手不足的项目,建立自动化流水线是很有必要的。像FarCry5这样每晚重新生成世界的流程也会让很多问题变得方便。
但对于一些程序化生成并没有大量覆盖的项目,让用户自己去生成内容是更方便的。
因此,用户和流水线各需要承担什么样的职责,也是需要结合项目情况考虑的问题。
在蜘蛛侠的分享中,他们将PCG管线分为了三个阶段,每个阶段所走的PCG流程是不一样的:
如果根据不同阶段采用不同的流程,则会减少很多问题,比如【Q4】我就可以直接对于上阶段生成的结果进行手动的修改,因为我知道在下一阶段不会再重新生成了。
但唯一的风险就是:阶段的划分真的是有效的吗?换句话说,有没有可能回到之前的阶段?
如果真的出了岔子需要回到之前的阶段,那就意味着现在阶段的有些工作有丢失的风险,毕竟我在做这个阶段的工作时我是认为上阶段的事情都结束了。
GhostRecon和蜘蛛侠有真实场景(玻利维亚和曼哈顿)作为基础,所以其底层的阶段具有一定的稳定性,毕竟真实的东西不会改变。但对于没有真实场景为基础的游戏项目,其底层阶段的变化的可能性,就是一个重要的考虑因素了。
如何将引擎里的一些数据输入给生成器,如何将生成器的结果转换为引擎内容,这个问题取决于你的生成器是什么。
如果你的生成器就是写在引擎中的代码,则就不需要考虑这个问题。如果你的生成器借助了GPU,则可能会需要一些与计算着色器进行交互的开发。
而目前比较流行的生成器是Houdini。那么主要考虑的就是Houdini数据如何转化成引擎内容。
Houdini官方给UE和Unity都提供了各自的HoudiniEngine插件可以直接用。其底层都使用了HAPI来解析后台Houdini节点里的数据,然后再转换为各自引擎的内容。不过,项目中可能会有些非通用的,官方插件里没考虑的引擎内容,比如项目专有的特殊Actor的数据,那么此时就需要一些额外的开发了。
另一种不使用HoudiniEngine插件的方式是将输入输出数据都放在磁盘上,比如FarCry5:
将数据放在磁盘上有一些好处,假设所有数据都被转换为了引擎内容,那么当有其他环节需要这个数据时,就只能从引擎中拿。而如果数据在磁盘上,则可以不启动引擎而直接访问。但要注意的是,要保证磁盘上的数据和转换为引擎的内容相匹配。(比如Houdini输出的高度场数据,要和UE生成的地形相匹配,正常流程下当然会是匹配的,但要考虑有没有什么情况会不匹配并导致问题?)
所存储在磁盘上的文件可以是Houdini的数据格式(比如bgeo),也可以是通用的格式(比如png)。通用格式的好处是,其他工具想访问这个数据时不需要使用Houdini的接口。而Houidni格式的好处是,更方便Houdini读取且可附加些额外的信息。
这里的“生成流程”指的是:生成器如何得到它的输入,做完计算后如何将其结果转化为引擎内容,后续还需要做什么操作?
“生成流程”并不是算法的一部分。因为算法在不同的生成流程中是复用的:比如对于同一个对地形处理的算法,在某个流程中其输入可能是所有地形,而在某个流程中可能是一块局部的地形,比如对于同一个生成植被分布的算法,在不同流程中可能会输出不同的植被,并且后续所需要的处理也不一样。
“生成流程”也不是用户所要考虑的问题,因为其牵扯到很多数据与操作是工程方面的问题,并非艺术或设计上的问题。所以生成流程的调整一般是技术美术的职责。
生成流程的调整并不是个难以解决的问题,但是必须要开发某种形式,而且最好是易于调整的形式。一种常见的想法是用节点的形式来表示。(下截图来源UOD2021重生边缘分享)
与节点相对应的是“脚本”。脚本和节点之间当然也互有优缺点,这里只是简单列举:
节点 | 脚本 |
---|---|
优:不容易出错 | 缺:容易失误 |
优:易于上手 | 缺:需要知道一定的语法才能上手 |
缺:屏幕利用率低 | 优:一页代码能表示更多的东西 |
缺:逻辑复杂时节点较乱 | 优:更利于表示复杂的逻辑 |
缺:二进制文件没法对比版本 | 优:可以对比版本间的修改 |
… | … |
另外,如果有自动化流水线,需要提供接口让流水线可以自动执行。
如前所述,用户不需要考虑生成流程,他们只需要能触发生成并看到结果就行了。也就是说,最好能提供给用户一个容易使用的界面来触发。
而“触发生成”只是界面上一个最基础的功能。很多用户需要的功能都可以放在界面上。
数值型的参数比较简单,只需要在界面上有面板即可。
不过,很多数值都有更具体的含义,最好根据这个进行优化。比如表示颜色的数值,就可以有颜色专用的面板来控制。再举例,如果一个参数代表的是一个资源,那么虽然你可以直接让用户填写资源的路径,但更合适的是提供面板让用户指定并且能预览到缩略图,这在UE中是很容易做到的,可以参考HoudiniEngine:
而另一种“参数”,或者说是“输入”,并非是数值能表示的。
例如:
这些曲线控件,mask工具或许不牵扯到复杂的技术,但是开发他们仍旧需要不小的工作量。
随着对控制度的要求的不断提高,操作控件可能会变得越来越复杂。例如控制河流的曲线控件:
这样,河流的曲线控件会变得越来越复杂。其他的操作控件也会有类似的问题。
当然,这些都是可以开发的,而且需求也算是有限的。但就是工作量会较大且繁琐。比如在UE中就需要很多C++代码来实现。
在【Q3】中,如果一种内容包含了“生成”与“手工”,那么就需要有方式能区分出来。
一方面,数据上需要区分。这意味着能通过某种规则明确地知道一个内容是手工的还是生成的。因为下一次生成时肯定不能影响手工的内容,而且也可能有些操作只针对于生成内容/手工内容。
另一方面,最好也让用户可以区分。这意味着界面上有某种工具能让人分辨出一个内容是手工的还是生成的。因为用户需要知道自己所维护的内容有哪些,而哪些是不需要自己关注且也不能修改的内容。
一个生成计算依赖于另一个生成计算是一种很常见的现象,特别是在生成一个强调真实性的世界中。
(下图为FarCry5各个工具的依赖关系)
依赖关系越多,则上游的变动就会需要下游越多的重新生成。
所以依赖关系肯定越少越好,但是如果你生成的世界是强调真实模拟的,那么依赖关系可能是无法避免的。
此外,像FarCry5那样每晚跑一次全局更新的流水线也能缓解这个问题。因为当用户改了上游的内容后,不需要亲自跑一遍所有下游的计算,只需要等晚上全局计算就好了。
生成计算时间过长,这可能是所有问题中最不好解决的问题。我想有很多原因或影响因素:
比如对于UE而言,生成的一些模型(如河流面片)会是一个单独的StaticMesh文件,但是有些数据是存放于关卡中的,比如地形和一些Actor。
而关卡中不仅有生成的数据,还会有些手工内容(手工摆放的一些模型,手刷的一些地形区域)。这样,关卡文件就同时包含了手工内容与生成内容,这会产生一些问题:
因此,最好将存放生成内容的文件与手工维护的文件分开。UE中也有支持这么做的机制(见
One File Per Actor)。
生成计算的过程中会有很多“中间数据”,但是转化到引擎中的是最终结果,毕竟很多中间数据在运行时是没用的。(这情况类似3dsMax的“塌陷”,即删除过程的数据)
但有时,后续的计算需要之前的中间数据。例如:前一步对地形的处理时计算了一个表示某种含义的mask,而这种mask对于后续的植被生成有用。
大概有两种思路解决:
PCG生成可能会产生很多资源的修改需要用户提交,用户不一定能分得清。
解决方式可以有:
当要生成的世界很大时,分区域进行计算是个常见的作法,但是这样遇到的一个问题就是“接缝”,即数据在与邻居相接部分的突变现象。
虽然“接缝”问题很常见,但是其原因并不相似,最常见的原因是计算这个区域时,有和邻居区域相关的计算,但由于邻居未加载,所以边缘的数据并不正确。
要想解决最好先分析出接缝产生的原因。而通用的思路例如:
程序化生成可以轻而易举生成性能超标的内容。因此需要格外关注生成的方式。比如:
而且生成的内容量大时,人力是无法评估所有性能数据的,还是需要自动化的方式,比如生成性能热力图。
依据和程序化生成的关系,关卡中的手工劳动分为几类:
开发程序化生成的管线工具自然会有出错的情况,比如编辑器崩溃,或者生成错误的结果。
代码总会有出错的情况。可能是某个数据不符合写代码时的假设,代码没考虑某种特殊情况,规则有调整但是代码却没来得及调整。。。
但相较于其他问题,调试程序化生成里的错误较困难的是:生成的时间可能要很久,这导致复现时间会较慢。如果问题还不是稳定复现的,那么调试将会成为一种折磨。
因此需要考虑如何能更好地调试错误,比如:
需要考虑,有可能会有非美术内容(比如Gameplay,音频等数据)需要借助PCG的力量或者要利用PCG生成的内容。
一种情况是,这些内容可以作为生成流程的后续操作,在生成原先的美术内容后再生成。
另一种情况是,这些内容的生成想使用PCG生成的数据作为输入。这时候正如【P1】所讨论,如果一些数据是通用格式而非Houdini格式,那么就会比较方便(因为其他内容的生成不需要使用Houdini接口)。
还有情况是,要专门为这些内容生成专用的数据结构。
正如【Q5】所讨论,流水线的一个缺点是,生成的结果没有人手动验证。那么就还是需要开发自动化的方式来验证。
对于生成错误数据的情况。正如【P9】所讨论,如果能将手动维护的文件和生成内容的文件分开,那么就会方便很多,就算生成了错误的数据只要再生成一遍就行了。但如果没有分开,还要考虑是否损坏了手动劳动。
截图都来源于上面几个分享。