ProjectS中的地形系统-Procedural Content Generation(PCG)
本篇文章是对ProjectS中地形PCG模块的总结,可能需要一些前置知识,可以前往本文的 参考 部分进行相关引用
先明确下项目需求
- 20km x 20km的世界大小
- 大多数地貌为起伏幅度较小的平原
- 需要分tile编辑,预览
- 需要导出LOD高度图
- 支持植被和物件Instance的纹理生成和手工修改
- 需要一个工具统筹管理整个PCG管线,并实时查看每一步的结果
最终选择Houdini作为目标软件,因为Houdini作为一款相当成熟的PCG向的DCC软件,提供了相当多地形创建,编辑和工作流工具,应对我这个个人项目的简单地形PCG工作自然不成问题
项目概览
项目结构如下:
- terrian_pdg:地形PDG模块,组织整个地形的PCG流程
- terrian_sop:地形PCG中用到的所有SOP集合,大部分都导出成了HDA,供PDG使用
PDG流程
我在学习和解构一个对象的时候习惯从宏观看起,大概掌握每个环节的功能,有个全局的概念,再看具体细节的时候不会有一头雾水的感觉,所以我们从PDG开始看起
整个流程非常的直观
- 创建地形骨架
- 拆分地形
- 根据地形编号查找hda文件,如果存在则进行处理,否则直接输出
- 整合分类所有tile结果,即把经过hda处理的tile和原始tile统一通过编号分类,确保输出的为最终的结果
- Merge所有tile
- 导出纹理(如果需要分tile导出,则需要再次进行split后导出)
唯一需要注意的一点是通过PythonProcessor将tile信息透传到merge节点的时候,需要做一些处理
1 | # Called when this node should generate new work items from upstream items. |
地形制作
整体流程
基础地形骨架
由于暗黑like游戏大部分的地貌都偏平坦,所以以比较低的噪声频率和迭代次数就能的得到一个合适的地形骨架
即使是如此简单的地形骨架,生成也是需要花费一定事件的,而且后续可能会有一些额外的全局处理,例如一些世界级的标志地貌,所以不能每次都重新生成,这里使用switch节点来根据传递的参数决定是否需要重生成地形,如果需要重新生成,则调用file cache节点来强制触发重建地形
1 | node = hou.pwd() |
地形拆分
拆分单位由HDA暴露的参数决定
地形Tile处理
可以针对每个Tile制作一个HDA用于处理这个Tile,也可以制作一个HDA支持同时处理多个Tile
,这里以前者为例,名称为terrian_tile_handle_*
(星号为tile编号)
这里的示例就是对0号tile做了手工处理:
- 通过绘制line来构筑一些山脉
- 增强地表细节
需要注意的是,我们需要通过bound来构造一个mask,用于限定编辑区域(最好对Bound的Mask开启Blur,效果更好一些),这样可以避免tile merge时产生过大的接缝
地形Tile合并
处理好所有Tile之后,我们需要将Tile合并起来,全局处理一下,预览最终的世界效果
本来是使用foreach block + python节点进行处理,但实践下来发现python节点内部的逻辑只会在cook的时候执行一次,不能保证foreach循环中每次都执行
而且Houdini中的Channel表达式对嵌套的支持并不友好,我尝试使用string+map的时候就失败了,无法计算map的value
所以最终方案是自己书写Python节点处理所有Tile,最终通过tilesplice进行Volumn合并
Python节点内容如下:
可以看到读取了tile_file_path_map channel值,这是从PDG传递到HDA的参数,包含了所有Tile数据
(吐槽一下:Houdini到19为止都还没有数组类型的参数,只能用key-value Dictionary,而key-value Dictionary在通过Python或VEX,HScript传递时总是会报出 TypeError: Cannot set a numeric parm to a non-numeric value
,所以这里通过string进行参数透传,最后在Python转回key-value Dictionary)
1 | import json |
最终Merge结果如下:
纹理切分导出
根据 ProjectS中的地形系统-Terrian Rendering 中提及的世界大小规划:
可以得知,对于各个LOD等级的高度纹理,假设为全地图覆盖纹理,我们需求如下:
- LOD0,覆盖64m,320x320个 = 102400
- LOD1,覆盖128m,160x160个 = 25600
- LOD2,覆盖256m,80x80个 = 6400
- LOD3,覆盖512m,40x40个 = 1600
- LOD4,覆盖1024m,20x20个 = 400
- LOD5,覆盖2048m,10x10个 = 100
通过HeightField Output节点即可对高度图进行导出,对于上面的LOD需求,通过HDA参数暴露出去,然后通过foreach block进行流程控制即可
假设已定义:
- terrian_size:传入的地形大小,默认2048
- terrian_lod_count:LOD等级数,从0开始计数
- sector_cover_size:sector大小
- height_texture_resolution:高度图分辨率,默认128
- iteration:foreach迭代index
则在每次for循环中,对于HeightField Output的参数设置遵循以下算法:
- resolution:
terrian_size * 2 * 1 / pow(2, iteration)
,各个LOD值分别为4096,2048,1024,512,256,128 - num_tiles:
resolution / height_texture_resolution
,各个LOD值分别为32,16,8,4,2,1
PDG中的节点为
由于Python节点编译特性限制,我们只能手动触发HeightField Output节点的导出操作
1 | node = hou.pwd() |
最终导出的部分高度图如下