渲染TA实战:Houdini程序化古代城市开发分享
哈喽,我是畅游引擎部的程序向技美Joon。本文主要分享一下构思以及部分实现的一个程序化古城市系统的设计思路,以及介绍一下几个主要部分的实现细节。
这里的古城市特指中国封建时代的城市聚落,参考的城市原型有长安城,古开封,以及水乡苏州古城。但是老实说,这些在历史上曾经光芒璀璨的古老城池,妄图用程序化语言去解构去复现,属实是有点自不量力。我接到这个需求之后,怀着万分之一的侥幸心,搜寻各路学术大神的论文,看看是否存在一种算法能描述或者大致趋近这些古城的布局特征。最后结果是没找着。考虑过L-sys和二叉分割,但是得出的结果在表现上很难达到项目要求,古城布局对于宏观和微观的景致都有比较细致的需求。固然,通过万能的机器学习来生成相对合理的布局图是一条值得尝试的思路,但是这超出了我的能力范围,希望有感兴趣的相关领域大佬能尝试一下。
既然先随机生成再细微手调的方向走不通,那就只能让输入端变成人手画的路网了。定下交互方式之后,我们开始考虑如何抽象化这些古代城市。研究了真实古城池复原图,遍历各个武侠类型游戏的场景布局,以及和公司武侠项目的地编沟通过之后,我们总结出了这些城市最基础的特征:
- 城市基本是由一个个高度不同的平整大地块叠加而成,基本没有斜坡,在地块交接处会有阶梯衔接。
- 每一个大地块边缘存在某种类型的包边(栏杆,或者是突起的装饰性石头)。
- 路网基本是横平竖直,转角不会有圆滑,并且一般道路上两边会有凸起的装饰物。
- 路两边的建筑除去闹市区的摊贩以外,居住区基本都是用结构偏方正的围墙围成院落,院落内部形成独立的二级结构。
固然,有水系穿行的古城与内陆古城分别有不同的特点,但本着自顶向下原则,我们先着手实现最基本的规则。在这个阶段,大致总结一下需要开发的流程依赖关系如下:
系统总体设计上述部分中目前已实现的有:画路网、地块的工具;生成道路,阶梯,以及地块包边撒点的主逻辑;庭院的围墙工具。目前生成的样式如下:
生成的最终结果截图(材质资源来自megascans)一、输入:
很简单的俩画线工具,材质和装饰物信息跟着线走,画出来这样的输入:
二、主逻辑:
这个部分主要处理三部分,程序化建模的道路,以及走instance的楼梯和包边(栏杆和包边石头)。
生成道路第一步是把路网resample之后ray到地块上,识别出交接处的点。由于需要在交接处生成楼梯,而楼梯要与地块边缘垂直并且能在上下方都接上原来的道路,这就需要我们对交接处的曲线进行修改。可以看到上面楼梯部分的UI有一个容差角度,以及两个容差距离,这个部分的计算直观来讲就是做了这么一个事:
楼梯点处理这部分的操作没啥难的,不断addpoint和addprim就完了,主要是比较繁琐。由于暴露的参数分上下、存在是否需要做曲线处理的角度输入判断、楼梯本身会占用一定距离,以及生成新的点和线的同时需要传递原来线上的材质信息、路宽等,导致这些点的移动有非常多种组合条件。我没什么好办法,只能不断地试几个可能出现的组合条件然后if else,主要的移动代码迭代了好几次,写了260行。
把线处理到这个阶段,往下就分三个部分分别生成了,分别是主路,楼梯(包含楼梯和两侧的instance点生成以及楼梯侧面的遮挡三角片),包边(栏杆和石头)。
1.主路:
上阶段处理完之后,得到主路使用的曲线长这个样子:
这部分要注意点在于,道路沿着截面会分成好几段,需要用一条分段的线做横截面去sweep。在这给这条分段的线上的prim和point相应的左右、中间/两边的区分变量,可以方便后续需要分辨左右的情况:
线上的属性uv方面,由于sweep自动算的uv存在拉伸,所以得自己来算。直接用每段直路的走向计算uv的旋转角度,uvproject然后uvtransform到原点就行,生成往v轴延伸,u轴铺满并且没有拉伸的uv。
uv旋转计算后续还有一个切割操作,即两条路如果交叠了,不是像现代道路一样生成十字路口,而是两条路之间做boolean,我这边设计的是用宽的去切窄的,一样宽就随机。判断是否切割以及谁切谁,使用的是scatter往一小段路上撒点然后再逐点往下intersect,碰到了再判断下宽度,不是特别严谨,但是也够用。这部分处理完是这样:
然后,仿照上面的计算方式,给路横截面的几个段生成突起的装饰物。由于这个装饰物横截面是暴露的参数,可以允许美术自己画的,就有可能出现非对称的图案像这样:
所以,这个道道生成的时候得把左右计算进去。这里就直接使用了前面sweep道路的时候存下来的左右信息,得到了这样的结果:
至此,主干道部分生成完成,主要是涉及的变量比较多,得静下心一点点捋。
2.楼梯:
这部分就是在不断resample生成点,然后往上写instance路径就完了。注意区分好左右,在生成侧面三角片的时候会需要判断一下面法线方向,最后生出来这样的玩意(实际上除了侧面三角片之外都是点云,贴上cube示意):
3.包边
这部分的线框处理比较简单,把地块法线朝上的面的边框掏出来之后,和其他的地块ray一下就能知道地块的重叠关系,把重叠的部分去掉。然后再把路口位置传过来,把线框resample之后用nearpoint找到和路口的最近点,处理完之后就是干净的线框了:
接下来就是各种resample和一些细节表现调整,没啥好说的。目前读取prefab信息是给定了相应的文件夹结构和命名规则,输入文件夹路径自动读取,然后在对应点上赋值的时候按总数量进行等比随机抽取。
三、沿线摆放工具
这个工具本来是设计来自定义生成庭院围墙的,后来考虑了一下发现基本上沿线的玩意都能摆,像什么园林里头的长走廊之类的,可扩展性很高。由于初衷是做古城市围墙,所以定死了画出来的曲线拐角需要是直角,当然轮子在这,改成平滑的也不是问题。目前摆出来是这个样子:
抽象出来就是可以在连续曲线上插入任意数量自定义长度的块,每一个块都可以沿着曲线移动。
这个工具的核心是用vex实现类似carve的功能,在官方的include文件中,groom.h里头定义了一个adjustPrimLength函数,正好就是我这需要的。
在我这个工具里头,只用到了diff < 0的情况。具体表现为在线上一个自定义位置加入一个点,并且带有需要插入的物体的宽度,prefab路径等信息。
然后用convert line把prim按点分段,以顶点id递增方向为位移方向把插入物体的宽度给减掉,得到处理好的结果:
然后就是一些角点处理,特殊情况处理和不断resample了。
多提一嘴,关于生成大mesh的加载问题,我这边做了个切块工具对这些大mesh进行棋盘格切分。最近有一个新的想法,道路部分其实也可以走instance(参考UE5城市demo),但那是另一种工作流了,再看看有没有这个需要吧。
至此,就是关于这个程序化古城市目前实现的所有东西了。老实说呢,由于已经是一年前做的东西,写这篇文章的时候重新看才发现很多可以优化的地方,当年由于对结点不太熟悉,很多本身结点就能实现的功能我全用代码自己写了,走了很多弯路,但同时也对这软件的理解更深了一点。当然,程序化系统并非一朝一夕能够做好的,现在做的只是单纯把架子搭起来了,往下的每一个部分,都是大坑:古建本体生成,庭院二级结构生成以及内部各种物件摆放,水路、水边的道路和桥,植被,摊贩,以及永远逃不掉的优化问题,场景加载问题,前路漫漫……
感谢您的阅读,若此文能使您稍有裨益,本人将无比欢欣。如果有写的不对的地方,还请评论指出,若有更好的想法,还请不吝赐教,共同学习。
欢迎加入我们!
感兴趣的同学可以在官网投递简历:
内推码:NTAI1kh