news 2026/9/11 11:55:10

建筑物生成体量 (Massing) 与立面规则剖分:程序化街区实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
建筑物生成体量 (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 List<FacadePolygon> GenerateMassing(List<float2> 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; List<FacadePolygon> facades = new List<FacadePolygon>(); List<float2> currentFootprint = new List<float2>(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 List<FacadePolygon> ExtrudeFootprint(List<float2> polygon, float bottomY, float height) { List<FacadePolygon> result = new List<FacadePolygon>(); 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 List<float2> InsetPolygon(List<float2> poly, float offset) { List<float2> insetList = new List<float2>(); 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 List<FacadePolygon> GenerateRoofCap(List<float2> outer, List<float2> inner, float y) => new List<FacadePolygon>(); private static List<FacadePolygon> GenerateFlatRoof(List<float2> poly, float y) => new List<FacadePolygon>(); } public class FacadePolygon { public Vector3[] Vertices; public bool IsGroundFloor; public FacadePolygon(Vector3[] vertices, bool isGroundFloor) { Vertices = vertices; IsGroundFloor = isGroundFloor; } }

形状文法(Shape Grammar)剖分流水线

体量生成完毕后,每个立面四边形会被送入基于形状文法(CGA-like Grammar)的规则解析器。立面剖分遵循层次化分割原则:

  1. 垂直分割(Split Y):将整面墙沿垂直方向切分为“底层商业区”、“标准住宅/办公层”以及“屋顶檐口女儿墙”。
  2. 水平重复(Repeat X):在标准层内部,根据立面总宽度,计算出整数倍的开间(Bay),每个开间宽度约为 3.0m ~ 4.5m。
  3. 单元细分(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 List<GrammarNode> Children = new List<GrammarNode>(); 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。实际运行管线中,必须采用动态合批生成单一多边形网格:

  1. 顶点坐标投影变换:文法树生成的节点是二维局部坐标 $(x, y)$,需要根据立面原始三维平面的基底向量(切线向量 $\vec{T}$ 与副法线向量 $\vec{B}$)变换至世界/局部空间:
    $$\vec{P}_{3D} = \vec{Origin} + x \cdot \vec{T} + y \cdot \vec{B}$$
  2. 纹理图集(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, List<Vector3> outVertices, List<int> outIndices, List<Vector2> 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 渲染指令与带宽开销压制在可控范围内。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 11:53:28

DBeaver 如何提交 Bug 报告并提供日志与版本信息

DBeaver 如何提交 Bug 报告并提供日志与版本信息 【免费下载链接】dbeaver Free universal database tool and SQL client 项目地址: https://gitcode.com/GitHub_Trending/db/dbeaver 在使用 DBeaver 时遇到可复现的 bug 或回归问题&#xff0c;需要通过项目仓库的 Iss…

作者头像 李华
网站建设 2026/9/11 11:51:48

Python网页抓取实战:requests_html与JSON解析技巧

1. 现代网页抓取利器&#xff1a;requests_html与JSON解析实战在数据驱动的时代&#xff0c;网页抓取技术已成为数据分析师、开发者和研究人员的必备技能。Python生态中的requests_html库以其独特的HTML解析能力和简化的API设计&#xff0c;正在改变传统爬虫的开发模式。配合轻…

作者头像 李华
网站建设 2026/9/11 11:51:18

教育场景AI内容检测与优化工具全解析

1. 项目概述&#xff1a;教育场景下的AI内容检测与优化在继续教育和学术写作领域&#xff0c;AI生成内容的检测已经成为刚需。最近三个月内&#xff0c;全球主流教育机构对AI生成内容的识别准确率提升了37%&#xff0c;这直接导致使用常规AI辅助工具的学习者面临作业被标记的风…

作者头像 李华
网站建设 2026/9/11 11:50:41

国内GEO优化服务商:AI原生营销时代的必选项

国内GEO优化服务商&#xff1a;AI原生营销时代的必选项你可能最近频繁听到一个说法&#xff1a;AI正在重塑营销。但到底怎么个"重塑"法&#xff1f;是不是就是用AI写几篇文案、做几张海报、剪几条视频&#xff1f;说实话&#xff0c;这些只是AI在营销执行层面的工具性…

作者头像 李华