 與立面規則剖分:程序化街區實現)
建筑物生成體量 (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 渲染指令與帶寬開銷壓制在可控范圍內。