传统建模能够精细调整模型,工作量=产出需求量*某一较小常数

程序建模建不了太精细,工作量=某一较大常数

对于需求较小的东西,传统建模的工作量可能会更小,但对于像是城市这种巨大的建模需求量,使用工作量不随需求量增加而增加的建模方式就再合适不过啦!

我会按照自己的想法从头建立一座二次元风格化的小城市。

如果你和我一样也是Houdini小白,想要复现,建议搭配AI食用哦。

https://srcblog.ffeng123.win:23443/1735626051665

我们先定义一些名词,它们由大到小是:

  • 城市块:将要生成的城市随意地划分为多个城市块

  • 建筑块:城市块内的道路将城市块划分为多个建筑块

  • 建筑项:建筑块通过切分切分为小块的建筑项用于建筑生成

整个城市生成的输入是一个多边形片元,所有的操作在这个片元之内完成,我们将这个片元称为 边界样条

这次主要内容是城市布局生成。

生成城市块

我们在这个片元上随意撒一些点,再用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:

https://srcblog.ffeng123.win:23443/archives/284179f7-25c4-4fe9-894f-b575758dfdbd

这里我用这样的代码:

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节点给切出来边分配组)


最终效果——

我能想到的,最大的成功就是无愧于自己的心。