传统建模能够精细调整模型,工作量=产出需求量*某一较小常数。
程序建模建不了太精细,工作量=某一较大常数
对于需求较小的东西,传统建模的工作量可能会更小,但对于像是城市这种巨大的建模需求量,使用工作量不随需求量增加而增加的建模方式就再合适不过啦!
我会按照自己的想法从头建立一座二次元风格化的小城市。
如果你和我一样也是Houdini小白,想要复现,建议搭配AI食用哦。
我们先定义一些名词,它们由大到小是:
城市块:将要生成的城市随意地划分为多个城市块
建筑块:城市块内的道路将城市块划分为多个建筑块
建筑项:建筑块通过切分切分为小块的建筑项用于建筑生成
整个城市生成的输入是一个多边形片元,所有的操作在这个片元之内完成,我们将这个片元称为 边界样条。
这次主要内容是城市布局生成。
生成城市块
我们在这个片元上随意撒一些点,再用VoronoiFracture 将其切分。

切分出的每一个片元作为一个城市块,城市块之间的边拓宽作为马路。
划分城市块的一个原因是,各个城市块里使用不同的旋转,希望的效果如图:

紧接着,给每个城市块片元分配一条属性,用以随机旋转,可以使用AttributeNoise 节点,也可以像我一样拿AttributeWrangle :
int pts[] = primpoints(0,@primnum);
f@rot = rand(point(0,"P",pts[0]) + point(0,"P",pts[2]) * 361 + 3) * 360;这条属性会在之后使用。
然后对城市块的边倒角。

倒角后,倒角产生的片元放到一边,之后作为道路。
城市块放入逐片元Foreach循环,在循环中处理城市块内的内容。
城市块内道路规划
为了更条理,建议将此处放到一个Subnet中进行。
首先,用Transform旋转片元,旋转角度用给城市块片元分配的随机旋转。
转到其他地方去了无所谓,到最后还需要反向转回来,这是为了让生成的道路有旋转。
然后创建一个平面Grid ,这是接下来生成道路的核心。
平面的大小和位置,使用城市块的BBox(此处有多种方法,如果你不知道,看看bbox()函数怎么用吧)
这个平面上的某些边将会作为道路,所以这个平面怎么划分也要思考一下下。
接下来要对平面上的边随机筛选,通过AttributeNoise 给所有点加个属性用来筛选(其实也可以不用加,产生随机数据不是在哪儿都可以嘛)
接下来需要用到Python的图论(我是这么做的,效果还不错)
参考另一篇Blog:
这里我用这样的代码:
import hou
import networkx as nx
from networkx.algorithms.tree import minimum_spanning_tree
from ffutils.hou import get_grp,output_grp
grp = get_grp("noise")
mtree = minimum_spanning_tree(grp)
output_grp(mtree)用不同的noise进行了两次最小生成树,然后进行了合并。
然后通过PolyExpand2D 将线变成面(上面生成的线数据似乎并不是太规范,需要试着调调PolyExpand2D的参数)
理论上现在会的到这样的效果:

(小贴士:最好给PolyExpand2D 的结果Y坐标置一下零,不然布尔可能会失败)
然后布尔,将城市块和上面生成的道路取交集。
(小贴士:为了之后城市块内道路和城市间道路能够严丝合缝,布尔之前最好给城市块给个PolyExpand2D 扩大一个很小的数字(0.001))
最后,别忘了旋转回去。
生成建筑块
城市块间的道路之前已经有了,我们可以使用PolyExpand2D 创建围绕边界的道路。
这样就有三部分马路了:城市块马路、城市块间马路、城市边界马路。
我们将其与城市块间的道路用布尔合并,得到合并的道路。
然后拿城市边界样条和合并的道路做差集,就得到了这样的效果:

其中每个片元是一个建筑块。
接下来逐建筑块操作。
(小贴士:会出现很小的建筑块,这些可以直接在此处测量面积,作为道路的一部分,不参与接下来的生成)
生成人行道
建筑块的外围是人行道。
(小贴士:为了接下来的布尔能够正确进行,接下来的挤出操作注意要封闭(该输出背面的时候输出背面),而且不要产生非流形多面体(不该输出的时候不要输出))
此处只需要很简单的用PolyExpand2D 找出人行道的区域再向上挤出就可以啦,如图:

但是,我们希望在人行道的转角处有个圆角,加圆角最方便的方法是——布尔!
我们将缩小后的建筑块(上图中间空白的三角),向上挤出到人行道高度,然后再从侧面分别挤出(挤出时选择Individual Elements)

然后创建圆柱体,给缩小后的建筑块的每个顶点复制一个过去,与上图布尔到一起,我们将这个图形称为人行道边界。
(小贴士:圆柱体细分有限的原因,圆柱体的半径应当略大于人行道宽度,不然圆柱体与人行道相切处会有问题)

上图稍加扩大后与我们一开始尖锐的人行道交集布尔,删掉在下面看不到的面,得到最终的人行道。
(小贴士:为了布尔后上表面没有多余的边,扩大后再布尔是必要的)
人行道圆角后比以前更小了,切掉的部分应该是马路。
依然是布尔,将建筑块减去人行道边界。
(小贴士:此处人行道边界往外扩大一个很小的数字(0.001)是有必要的,否则布尔后会出现退化多边形(没有面积的多边形),之后将反复出现这个问题,小贴士不再提及)

生成建筑项
接下来需要对缩小后的建筑块进行操作。
我们想要把建筑块切分为足够小的一个一个小块。
如果是编程语言,此处想到的应该是写一个递归,但···图节点嘛,怎么可能递归啊。
不过,众所周知,递归都是可以写成循环的形式的。
在Houdini里,是可以实现循环形式的递归,用反馈循环。
我们实现如下伪代码:
片元 = [缩小后的建筑块]
for _ in 最大循环次数:
新片元 = []
for 一个片元 in 片元:
if 计算面积(一个片元) > 面积阈值:
bbox = 取BBox(一个片元)
if bbox.size.x > bbox.size.y:
新片元.添加(X上切分(一个片元))
else:
新片元.添加(Y上切分(一个片元))
else:
新片元.添加(一个片元)
片元 = 新片元附节点连接:

为了好切,我将片元先移动到原点后切的,节点看上去有些杂乱(不光乱,还慢,截图时就看到了可优化的地方)
(小贴士:为了给之后生成建筑提供参考,最好让Clip节点给切出来边分配组)
最终效果——
