建筑物生成体量 (Massing) 与立面规则剖分:程序化街区实现
建筑物生成体量 (Massing) 与立面规则剖分程序化街区实现在开放世界游戏的大规模城市构建中如果完全依赖关卡美术纯手工摆放每一栋建筑不仅生产管线会被极其庞大的资产吞吐量拖垮更会导致包体和内存被海量的唯一网格Unique Meshes撑爆。将程序化内容生成PCG引入城市建筑核心在于两套连续计算自底向上的体量推导Massing Generation与自外向内的立面规则语法剖分Facade Split Grammar。这两步并非简单的随机堆叠几何体而是从二维用地红线多边形Lot Polygon出发结合容积率、退界限高与视觉韵律先生成具有空间层次的三维包络体再将包络体的每个侧面作为图元输入文法解析器递归剖分为底商、标准层、顶层、窗间墙与阳台模块。建筑体量推导与退界算法建筑体量的生成始于地块的多边形顶点轮廓。为了形成具有现代感或古典渐进式的建筑造型不能仅做简单的垂直拉伸需要引入直骨架Straight Skeleton收缩或分段退界Setback。退界计算的数学本质是多边形的多层内缩缓冲Polygon Insetting。当建筑达到特定高度阈值例如裙楼与塔楼的分界上层截面需要沿边界法线向内收缩$$\vec{P}{inset} \vec{P}i d \cdot \frac{\vec{n}{prev} \vec{n}{next}}{1 \vec{n}{prev} \cdot \vec{n}{next}}$$其中 $d$ 为退界距离$\vec{n}{prev}$ 与 $\vec{n}{next}$ 为相邻两条线段的向内单位法向量。using System.Collections.Generic; using Unity.Mathematics; using UnityEngine; public struct BuildingMassingParams { public float GroundFloorHeight; // 首层高度通常较高如 4.5m public float FloorHeight; // 标准层高如 3.2m public int MinFloors; // 最低层数 public int MaxFloors; // 最高层数 public float SetbackHeight; // 触发退界的高度 public float SetbackDistance; // 退界缩进距离 } public class BuildingMassingGenerator { // 将2D底面多边形拉伸为带有退界结构的三维体量体素/面片 public static ListFacadePolygon GenerateMassing(Listfloat2 footprint, BuildingMassingParams massParams, uint seed) { var random new Unity.Mathematics.Random(seed); int totalFloors random.NextInt(massParams.MinFloors, massParams.MaxFloors 1); float totalHeight massParams.GroundFloorHeight (totalFloors - 1) * massParams.FloorHeight; ListFacadePolygon facades new ListFacadePolygon(); Listfloat2 currentFootprint new Listfloat2(footprint); float currentElevation 0.0f; // 生成裙楼阶段 float podiumHeight math.min(massParams.SetbackHeight, totalHeight); facades.AddRange(ExtrudeFootprint(currentFootprint, currentElevation, podiumHeight)); currentElevation podiumHeight; // 如果建筑总高超过退界高度执行轮廓收缩并继续拉伸塔楼 if (totalHeight massParams.SetbackHeight) { currentFootprint InsetPolygon(currentFootprint, massParams.SetbackDistance); if (currentFootprint.Count 3) { // 生成退界处的屋顶过渡面 facades.AddRange(GenerateRoofCap(footprint, currentFootprint, currentElevation)); // 向上拉伸塔楼 facades.AddRange(ExtrudeFootprint(currentFootprint, currentElevation, totalHeight - currentElevation)); currentElevation totalHeight; } } // 生成最终平顶或人字顶 facades.AddRange(GenerateFlatRoof(currentFootprint, currentElevation)); return facades; } private static ListFacadePolygon ExtrudeFootprint(Listfloat2 polygon, float bottomY, float height) { ListFacadePolygon result new ListFacadePolygon(); int count polygon.Count; for (int i 0; i count; i) { float2 p0 polygon[i]; float2 p1 polygon[(i 1) % count]; // 构造四边形墙面顶点按逆时针缠绕 Vector3 v0 new Vector3(p0.x, bottomY, p0.y); Vector3 v1 new Vector3(p1.x, bottomY, p1.y); Vector3 v2 new Vector3(p1.x, bottomY height, p1.y); Vector3 v3 new Vector3(p0.x, bottomY height, p0.y); result.Add(new FacadePolygon(new Vector3[] { v0, v1, v2, v3 }, bottomY 0.0f)); } return result; } private static Listfloat2 InsetPolygon(Listfloat2 poly, float offset) { Listfloat2 insetList new Listfloat2(); int n poly.Count; for (int i 0; i n; i) { float2 prev poly[(i n - 1) % n]; float2 curr poly[i]; float2 next poly[(i 1) % n]; float2 dirPrev math.normalize(curr - prev); float2 dirNext math.normalize(next - curr); float2 normPrev new float2(-dirPrev.y, dirPrev.x); float2 normNext new float2(-dirNext.y, dirNext.x); float2 bisector math.normalize(normPrev normNext); float sinHalfAngle math.dot(bisector, normPrev); if (math.abs(sinHalfAngle) 0.001f) continue; float actualOffset offset / sinHalfAngle; insetList.Add(curr bisector * actualOffset); } return insetList; } private static ListFacadePolygon GenerateRoofCap(Listfloat2 outer, Listfloat2 inner, float y) new ListFacadePolygon(); private static ListFacadePolygon GenerateFlatRoof(Listfloat2 poly, float y) new ListFacadePolygon(); } public class FacadePolygon { public Vector3[] Vertices; public bool IsGroundFloor; public FacadePolygon(Vector3[] vertices, bool isGroundFloor) { Vertices vertices; IsGroundFloor isGroundFloor; } }形状文法Shape Grammar剖分流水线体量生成完毕后每个立面四边形会被送入基于形状文法CGA-like Grammar的规则解析器。立面剖分遵循层次化分割原则垂直分割Split Y将整面墙沿垂直方向切分为“底层商业区”、“标准住宅/办公层”以及“屋顶檐口女儿墙”。水平重复Repeat X在标准层内部根据立面总宽度计算出整数倍的开间Bay每个开间宽度约为 3.0m ~ 4.5m。单元细分Sub-divide Unit在单个开间网格内再剖分为窗间柱Pillar、窗过梁Lintel以及嵌入门窗图元的凹陷区域Sub-mesh Component。------------------------------------------------------- - 顶层檐口 | [Pillar] [Window] [Pillar] [Window] [Pillar] | - 标准层 N ------------------------------------------------------- | [Pillar] [Window] [Pillar] [Window] [Pillar] | - 标准层 1 ------------------------------------------------------- | [Entrance/Shopfront Door] [Large Display Glass] | - 底层商业 -------------------------------------------------------public enum GrammarNodeType { Container, Wall, Window, Door, Cornice } public class GrammarNode { public GrammarNodeType Type; public Rect Bounds; // 局部归一化或实际米制坐标 [x, y, width, height] public ListGrammarNode Children new ListGrammarNode(); public GrammarNode(GrammarNodeType type, Rect bounds) { Type type; Bounds bounds; } } public class FacadeGrammarParser { public static GrammarNode ParseFacade(float width, float height, bool hasGroundFloor) { var root new GrammarNode(GrammarNodeType.Container, new Rect(0, 0, width, height)); float groundHeight hasGroundFloor ? 4.2f : 0f; float corniceHeight 0.8f; float remainingHeight height - groundHeight - corniceHeight; float standardFloorHeight 3.0f; int floorCount Mathf.Max(1, Mathf.FloorToInt(remainingHeight / standardFloorHeight)); float actualFloorHeight remainingHeight / floorCount; // 垂直分割 if (hasGroundFloor) { var groundNode new GrammarNode(GrammarNodeType.Container, new Rect(0, 0, width, groundHeight)); SplitGroundFloorHorizontal(groundNode, width, groundHeight); root.Children.Add(groundNode); } // 标准层水平循环切割 for (int i 0; i floorCount; i) { float floorBottom groundHeight i * actualFloorHeight; var floorNode new GrammarNode(GrammarNodeType.Container, new Rect(0, floorBottom, width, actualFloorHeight)); SplitStandardFloorHorizontal(floorNode, width, actualFloorHeight); root.Children.Add(floorNode); } // 屋顶女儿墙 var corniceNode new GrammarNode(GrammarNodeType.Cornice, new Rect(0, height - corniceHeight, width, corniceHeight)); root.Children.Add(corniceNode); return root; } private static void SplitStandardFloorHorizontal(GrammarNode floorNode, float width, float height) { float targetBayWidth 3.5f; int bayCount Mathf.Max(1, Mathf.RoundToInt(width / targetBayWidth)); float actualBayWidth width / bayCount; for (int i 0; i bayCount; i) { float bayX i * actualBayWidth; float pillarWidth 0.6f; float windowWidth actualBayWidth - pillarWidth; // 柱子 窗体 窗下实墙 floorNode.Children.Add(new GrammarNode(GrammarNodeType.Wall, new Rect(bayX, floorNode.Bounds.y, pillarWidth, height))); // 窗体区域垂直再分窗台、玻璃窗、窗楣 float sillHeight 0.8f; float windowHeight height - sillHeight - 0.4f; float lintelHeight 0.4f; floorNode.Children.Add(new GrammarNode(GrammarNodeType.Wall, new Rect(bayX pillarWidth, floorNode.Bounds.y, windowWidth, sillHeight))); floorNode.Children.Add(new GrammarNode(GrammarNodeType.Window, new Rect(bayX pillarWidth, floorNode.Bounds.y sillHeight, windowWidth, windowHeight))); floorNode.Children.Add(new GrammarNode(GrammarNodeType.Wall, new Rect(bayX pillarWidth, floorNode.Bounds.y sillHeight windowHeight, windowWidth, lintelHeight))); } } private static void SplitGroundFloorHorizontal(GrammarNode node, float width, float height) { node.Children.Add(new GrammarNode(GrammarNodeType.Door, new Rect(0, 0, width, height))); } }网格拓扑组装与 UV 图集烘焙如果将解析出来的每个 Grammar 叶子节点作为独立的 GameObject 实例化一个街区就会产生数十万个 Draw Call。实际运行管线中必须采用动态合批生成单一多边形网格顶点坐标投影变换文法树生成的节点是二维局部坐标 $(x, y)$需要根据立面原始三维平面的基底向量切线向量 $\vec{T}$ 与副法线向量 $\vec{B}$变换至世界/局部空间$$\vec{P}_{3D} \vec{Origin} x \cdot \vec{T} y \cdot \vec{B}$$纹理图集Texture Atlas与 UV 映射为了在单个材质球单次 Draw Call下绘制水泥墙、红砖、反光玻璃与金属窗框材质库被整合成一张 PBR Texture Array 或 Atlas。每个 GrammarNodeType 映射到图集中的固定 UV 区块并在生成顶点时将对应的 UV 缩放并平铺填入uv0同时将材质 ID 或凹凸深度作为顶点色color.r传递给 Shader。public class FacadeMeshBuilder { public static void EmitGeometry(GrammarNode node, Vector3 origin, Vector3 right, Vector3 up, ListVector3 outVertices, Listint outIndices, ListVector2 outUvs) { if (node.Children.Count 0) { foreach (var child in node.Children) { EmitGeometry(child, origin, right, up, outVertices, outIndices, outUvs); } return; } // 计算当前叶子节点在世界空间中的 4 个顶点 float rx node.Bounds.x; float ry node.Bounds.y; float rw node.Bounds.width; float rh node.Bounds.height; Vector3 p0 origin right * rx up * ry; Vector3 p1 origin right * (rx rw) up * ry; Vector3 p2 origin right * (rx rw) up * (ry rh); Vector3 p3 origin right * rx up * (ry rh); // 如果是窗户增加向内凹陷Recess Depth增强侧光下的体积感与阴影表现 if (node.Type GrammarNodeType.Window) { Vector3 normal Vector3.Cross(right, up).normalized; float depth 0.25f; // 内凹 25cm p0 - normal * depth; p1 - normal * depth; p2 - normal * depth; p3 - normal * depth; } int startIndex outVertices.Count; outVertices.Add(p0); outVertices.Add(p1); outVertices.Add(p2); outVertices.Add(p3); outIndices.Add(startIndex 0); outIndices.Add(startIndex 2); outIndices.Add(startIndex 1); outIndices.Add(startIndex 0); outIndices.Add(startIndex 3); outIndices.Add(startIndex 2); // 依据图元类型映射 UV 图集 Vector2 uvMin GetAtlasOffset(node.Type); Vector2 uvScale GetAtlasScale(node.Type); outUvs.Add(uvMin); outUvs.Add(new Vector2(uvMin.x uvScale.x, uvMin.y)); outUvs.Add(uvMin uvScale); outUvs.Add(new Vector2(uvMin.x, uvMin.y uvScale.y)); } private static Vector2 GetAtlasOffset(GrammarNodeType type) type switch { GrammarNodeType.Wall new Vector2(0.0f, 0.0f), GrammarNodeType.Window new Vector2(0.5f, 0.0f), GrammarNodeType.Door new Vector2(0.0f, 0.5f), GrammarNodeType.Cornice new Vector2(0.5f, 0.5f), _ Vector2.zero }; private static Vector2 GetAtlasScale(GrammarNodeType type) new Vector2(0.5f, 0.5f); }通过将退界多边形骨架与文法解析管线解耦可以在烘焙阶段批量生成整个城区的低 LOD 代理网格Proxy Mesh以及高精度视距近景网格。运行期仅需维护轻量化的变换矩阵和实例化参数既满足了街道建筑形态的多样性又将 GPU 渲染指令与带宽开销压制在可控范围内。