← 日志列表 / All journal entries

从不规则地块到可用空间

From Irregular Parcels to Usable Space

Houdini 与 UE5 PCG 之间的约束传递

Constraint Transfer Between Houdini and UE5 PCG

在这个聚落生成系统里,建筑不会直接从地块边界开始生成。

道路划分出来的 parcel 往往是不规则多边形。它可能很长,也可能有斜边,还可能有凹凸。UE5 PCG 如果直接处理这些 polygon,就要同时判断建筑能放多大、朝哪个方向。这样一来,PCG 既要处理几何,也要负责生成内容。

所以我把这个问题放在 Houdini 里解决:

在道路方向已经确定的前提下,一个不规则地块还能给建筑留下多大的规则可用空间?

这里不是在求数学意义上的“任意多边形最大内接矩形”。现在这套方法更接近一种道路方向约束下的启发式可用矩形估计。目标是得到一组可以批量计算,也可以继续传给下游的数据,不去追求理论上的全局最优解。

先确定局部空间,再讨论尺寸

如果直接在世界坐标里计算 Bounding Box,同一个地块只要旋转,宽度和深度就会跟着变化。

所以在算矩形之前,我先给地块建立一个局部坐标系。

我先从地块和道路的关系里拿到一个参考方向。地块法线作为 up axis。再用叉乘建立一组正交基。之后把地块顶点转到这个局部坐标系里,再计算各轴的 min / max。这样就能得到一个和道路方向一致的定向外包矩形。

所以顺序不是:

Parcel
→ Bounding Rectangle
→ 再决定建筑朝向

而是:

Parcel + Road Direction
→ Local Frame
→ Oriented Bounding Rectangle

也就是说,先用道路确定空间方向,再在这个方向里做几何计算。

HDA 里会先建立局部正交坐标系,再在这个坐标系里计算矩形范围。

这个矩形还只是外包矩形。对于梯形、L 形或者有斜边的 parcel,一些角点可能会落到地块外面。

接下来要解决的问题就是:

如何保留这个方向,同时把矩形限制在 parcel 的边界里?

从外包矩形得到一个可用矩形

pcgtown1_lotlimits_p2

现在这套系统用的是一套比较直接的启发式方法。

第一步,从矩形中心连到四个角,再检查这些方向和 parcel 边界的关系。每个方向都会得到一个可用比例。四个方向里最小的那个比例,会拿来统一缩小矩形。HDA 里会从 rect_corners 生成中心到四角的测试线,再从求交结果里取最小参数。

这一步可以写成:

Oriented Bounding Rectangle

检测 parcel 边界

按最严格方向整体收缩

Safe Rectangle

这样可以先得到一个安全范围。

如果只有一个角被斜边卡住,整体缩放也会把另外几个方向一起缩小。这样会损失一部分空间。

所以第二步会找到第一轮里最受限制的角点。然后固定这个角,再朝另外几个方向回扩。扩张范围还是由 parcel 边界决定。HDA 里会用 min_curveu_index 找到这个角,再从它向其他角生成测试线。

整个过程可以写成:

道路导向的外包矩形

整体收缩到安全范围

确定主要受限角

以受限角为锚重新回扩

近似可用矩形

这不是全局优化,也不保证最大面积。

它解决的是一个工程问题:

尽量得到一个方向稳定、边界可控、空间利用率尚可的规则区域。

真正重要的是把 polygon 压缩成 constraint point

如果最后还是把整个矩形,甚至原始 parcel,直接传给 Unreal,那 Houdini 做的就只是一次几何预处理。

我更希望 Houdini 直接给出下游真正需要的数据。

所以在算完可用区域以后,我会删掉地块 primitive,只留下一个位于可用区域中心的 point。然后把结果写到这个 point 上。实际输出里有 rect_centerfacade_orientlimit_widthlimit_depth

从概念上看,一个有很多顶点的 polygon 最后会变成:

P + R + W + D

其中:

P Position
可用空间位于哪里

R Orientation
这个局部空间朝向哪里

W Usable Width
宽度方向最多允许多少空间

D Usable Depth
深度方向最多允许多少空间

这里说的“压缩”,只是工程上的数据整理。

把下游需要的空间约束放到一个 point 上。

原始 parcel 用来描述边界。constraint point 用来回答另一个问题:

“如果下游只需要在这里放一个规则建筑,它到底需要知道什么?”

这也是这一段里我最关心的地方:

Houdini 输出的不是“建筑”,也不是“矩形 Mesh”,而是一份关于可用局部空间的描述。

UE5 PCG 消费的是空间事实,而不是重新理解 parcel

UE5 会先从 Data Asset 里读取 Houdini 发布的数据,再通过 pcg_layer 找到 building_points

之后 PCG 不再重新分析原始 polygon。它直接读取 Houdini 给出的空间约束,再按自己的规则使用这些数据。

比如,当前 Graph 会把 limit_width_cmlimit_depth_cm 按 200 cm 的模块尺度换算成可用模块数量。

这些尺寸也可以直接和候选对象的 Bounds 比较,用来判断对象能不能放进这个空间。

所以 Houdini 和 UE 之间有一条很清楚的边界:

Houdini
负责回答:
“这里实际有多少空间?”

UE PCG
负责回答:
“这些空间应该怎样被使用?”

也就是把“空间事实”和“生成策略”分开。

如果 Houdini 直接输出:

这里生成 5 个模块
这里必须使用某一种房型

那 Houdini 的地块求解就会和 UE 的建筑规则绑在一起。

现在传的是物理空间约束:

Position
Orientation
Usable Width
Usable Depth

这样 UE 可以改模块尺寸,也可以换候选建筑,还可以改生成方式。Houdini 里的 parcel 求解不用跟着改。

这部分最终不是在求一个“漂亮的矩形”,而是在把复杂地块转换成下游可以直接消费的空间约束。

In this settlement generation system, buildings are not generated directly from parcel boundaries.

The parcels created from the road layout are often irregular polygons. They can be long. They can have angled edges. They can also have concave parts. If UE5 PCG handles these polygons directly, it must decide both building size and building direction. PCG would then handle geometry analysis and content generation at the same time.

So I solve this problem in Houdini:

When the road direction is already known, how much regular usable space can an irregular parcel still provide for a building?

This is not a mathematical solution for the “maximum inscribed rectangle of an arbitrary polygon.” The current method is closer to a heuristic usable rectangle estimate under a road direction constraint. The goal is to produce data that can be calculated in batches and passed to the next stage. It does not try to find a global optimum.

Define the local space before calculating the size

If I calculate a Bounding Box in world space, the width and depth change when the parcel rotates.

So I first build a local coordinate system for the parcel.

I get a reference direction from the relationship between the parcel and the road. The parcel normal is used as the up axis. I then use cross products to build an orthogonal basis. After that, I transform the parcel points into this local coordinate system. I calculate the min and max values on each axis. This gives me an oriented bounding rectangle that follows the road direction.

So the order is not:

Parcel
→ Bounding Rectangle
→ Decide building orientation

It is:

Parcel + Road Direction
→ Local Frame
→ Oriented Bounding Rectangle

In other words, the road defines the spatial direction first. The geometry is then calculated inside that local frame.

The HDA builds this local orthogonal frame and calculates the rectangle inside it.

This rectangle is still only an outer bounding rectangle. For a trapezoid, an L-shaped parcel, or a parcel with angled edges, some corners can fall outside the parcel.

The next problem is:

How can I keep this direction and limit the rectangle to the parcel boundary?

From an outer rectangle to a usable rectangle

pcgtown1_lotlimits_p1

The current system uses a simple heuristic method.

First, I connect the rectangle center to its four corners. I then check how these directions intersect the parcel boundary. Each direction gets a usable ratio. The smallest ratio is used to shrink the whole rectangle. The HDA creates test lines from rect_corners and reads the minimum value from the intersection results.

This step can be written as:

Oriented Bounding Rectangle

Check parcel boundary

Shrink with the strictest direction

Safe Rectangle

This gives me a safe range first.

If only one corner is limited by an angled edge, the full rectangle still shrinks. This wastes some usable space.

So the second step finds the most constrained corner from the first pass. That corner stays fixed. The rectangle then expands again toward the other directions. The parcel boundary still controls the expansion. In the HDA, min_curveu_index is used to find this corner. New test lines are then created from that corner.

The full process is:

Road-Oriented Bounding Rectangle

Shrink to a safe range

Find the main constrained corner

Expand again from that corner

Approximate Usable Rectangle

This is not a global optimization method. It does not guarantee the maximum area.

It solves a practical engineering problem:

Try to get a regular region with a stable direction, controlled boundaries, and reasonable space usage.

The important part is compressing the polygon into a constraint point

If I still send the whole rectangle, or even the original parcel, to Unreal, then Houdini is only doing geometry preprocessing.

I want Houdini to output only the data that the downstream system actually needs.

So after the usable region is calculated, I remove the parcel primitive and keep one point at the center of the usable region. I write the result to this point. The output includes rect_center, facade_orient, limit_width, and limit_depth.

At a conceptual level, a polygon with many vertices becomes:

P + R + W + D

Where:

P Position
Where is the usable space?

R Orientation
Which direction does the local space face?

W Usable Width
How much width is available?

D Usable Depth
How much depth is available?

The word “compression” here only means data organization.

The spatial constraints needed by the next system are stored on one point.

The original parcel describes the boundary. The constraint point answers another question:

“If the downstream system only needs to place a regular building here, what does it actually need to know?”

This is the part I care about most:

Houdini does not output a building. It does not even output a rectangle mesh. It outputs a description of usable local space.

UE5 PCG consumes spatial facts instead of reading the parcel again

UE5 first reads the data published from Houdini through the Data Asset. It then uses pcg_layer to find the building_points.

After that, PCG does not analyze the original polygon again. It reads the spatial constraints from Houdini and uses them with its own rules.

For example, the current Graph converts limit_width_cm and limit_depth_cm into module counts based on a 200 cm module size.

The same dimensions can also be compared with the Bounds of a candidate object. This is used to check whether the object can fit inside the available space.

So there is a clear boundary between Houdini and UE:

Houdini
answers:
“How much space is actually available here?”

UE PCG
answers:
“How should this space be used?”

This separates spatial facts from generation strategy.

If Houdini directly outputs:

Generate 5 modules here
Use this exact building type here

then the parcel solver becomes tightly coupled to the building rules in UE.

Instead, it sends physical spatial constraints:

Position
Orientation
Usable Width
Usable Depth

UE can then change the module size. It can change the building candidates. It can also change the generation rules. The parcel solver in Houdini does not need to change.

This part is not really about finding a “nice rectangle.” It is about converting a complex parcel into spatial constraints that the downstream system can consume directly.