资讯动态

CityEngine道路规则库实战:从CGA规则到参数化路网生成

发布时间:2026/8/29 3:37:55 来源:尧图企业网站定制
简介在数字孪生与城市三维建模场景中道路作为城市骨架的底层要素其建模效率和可调性直接影响项目交付质量。传统手工建模难以应对大规模路网修改需求而基于CityEngine的CGA规则提供了一种参数化生成道路的解决路径。通过将路网Graph转化为Segment与Junction形状再由规则驱动挤出、切分和材质赋予即可实现道路横断面、交叉口、标线等细节的自动生成。这种程序化建模方式不仅大幅降低修改成本还能保证多路段风格统一。该技术广泛适用于三维辅助规划、智慧城市视觉底座、方案快速比选等场景。本文以道路规则库为主线拆解CGA流转逻辑、横断面设计、路口处理与性能优化要点帮助读者从零构建一套可复用、易调参的城市道路生成体系。 几年前我第一次在CityEngine里认真建一条道路两公里长的主干道机动车道、绿化隔离带、非机动车道、人行道、路缘石一层层往上叠模型拉了整整一下午。结果甲方第二天说路网骨架要微调我当场血压拉满——所有跟道路相关的几何体全部要动。那之后我彻底明白CityEngine的玩法从来不是手工建模而是用规则去驱动路网生成。今天这篇就把我沉淀下来的CityEngine道路规则库拆开讲讲从CGA的流转逻辑、道路横断面细节拆解到交叉口的处理、参数化设计和实战中容易踩的坑一次说清楚。这套内容适合谁如果你在折腾数字孪生、三维辅助规划、城市级场景搭建或者单纯想用CityEngine把路网数据变成有细节的模型那这篇可以帮你理清思路少走不少弯路。1. 为什么我放弃手工建路道路规则库解决的真实痛点先聊点背景。CityEngine的建模思路和传统建模软件完全不一样。传统DCC软件里一条路就是一条路你画一条样条线挤出、倒角、贴图完事。但在城市级场景里路网往往有成百上千条街道如果每一条都靠手动处理工作量是灾难性的。更麻烦的是道路不是独立存在的——一条路要和人行道连通要和交叉口衔接要在转弯处生成平滑的路缘石这些几何关系在手工建模时很容易顾此失彼。道路规则库做的事情本质上是把路网Graph转成带细节的三维道路模型。CityEngine里会先生成一个路网拓扑结构——街道中心线Street Graph然后每条边会生成一个Segment shape每个节点会生成一个Junction shape这些shape会进入各自的CGA规则中由规则负责挤出、切割、贴材质。换句话说你不需要画每一条路只需要告诉规则库这条路多宽、几条车道、有没有中央隔离带剩下的几何生成交给规则去算。这种思路带来的直接好处有三点修改成本极低。道路宽度从24米调到30米改一个attr参数重新生成全场景同步更新。手工模型要改的话每条路挨个拖点。质量稳定。规则是程序化的每条路生成出来都严格符合你预设的横断面逻辑不会出现某条路人行道宽、某条路人行道窄这种人工误差。可批量复用。一套规则库可以投到不同的路网数据上换一个城市、换一个区域只要路网Graph还在生成逻辑完全一致。所以当时我从一条路一条路画转向搭规则库之后基本就再也回不去了。CGA这套东西刚开始有点门槛但一旦摸透几个核心逻辑收益非常直接。2. CGA规则在路网上的流转逻辑从Graph到Segment再到Junction既然要搭道路规则库第一件事不是急着写split而是先理解CGA是怎么被路网调用的。CityEngine的CGA规则有几个固定的入口对应不同类型的shapeStreet规则作用于路网的边Segment也就是每一条街道。Junction规则作用于路网的节点也就是交叉口。Lot规则作用于道路围合出的地块建筑生成走这条。Block规则作用于街区常用于更粗粒度的城市分区逻辑。写道路规则库核心盯住Street和Junction就够。我实际项目里的规则文件通常会先定义一堆attr作为全局参数放文件顶部。CGA的attr有点类似编程语言里的常量也可以是公开给外部调用的参数。在CityEngine的Inspector面板里你可以直接看到这些attr并实时调整这是规则库参数化交互的基础。下面是一段我一直沿用的Street规则骨架先看整体结构细节后面拆version 2019.1 Group(道路参数) Order(1) attr streetWidth 24 Group(道路参数) Order(2) attr sidewalkWidth 4 Group(道路参数) Order(3) attr curbHeight 0.15 Group(车道参数) Order(1) attr laneWidth 3.5 Group(车道参数) Order(2) attr laneCount 4 Group(车道参数) Order(3) attr markingWidth 0.15 StartRule Street -- s(scope.sx, 0, streetWidth) t(0, 0, -streetWidth / 2) split(z) { sidewalkWidth: Sidewalk | streetWidth - 2 * sidewalkWidth: Carriageway | sidewalkWidth: Sidewalk } Sidewalk -- extrude(world.y, curbHeight) setupProjection(0, scope.xy, 1, 1) set(material.colors.diffuse, sidewalkTexture) Carriageway -- s(1, 0, 1) split(z) { laneWidth: AsphaltLane | markingWidth: LaneMarking }*这段规则看起来简单但里面有几个非常关键的CGA知识点。第一Street -- s(scope.sx, 0, streetWidth)。这里是把街道shape的宽度设置成streetWidth同时y方向高度设为0因为初始的街道shape是一个平面不需要高度。scope.sx是当前shape在局部x方向的尺寸在路网Segment里x方向基本就是街道的走向。第二split(z)。split是CGA里最常用的切分命令括号里的z表示沿着shape本地坐标系的z轴方向切。在道路Segment中z方向通常对应街道的横断面方向。所以split(z)就是在切道路横断面先切出两侧人行道再切中间车行道。如果你生成的街道shape坐标轴向和预期不一致把z换成x试试这是CGA里常见的轴向调理问题不是bug多试几次就有感觉了。第三Carriageway里的split用了repeat语法split(z) { laneWidth: AsphaltLane | markingWidth: LaneMarking }*后面这个*号表示在当前剩余的空间里循环切分直到剩余尺寸不足以容纳一次完整的分割为止。这个写法在车道数量不固定、但每条车道宽度固定时非常好用你不用写死四车道还是六车道路网里某条路的宽度变了车道数量会自动适应。有一点要特别提醒split里的字符|是分隔符不是CGA的或逻辑。CGA的或逻辑用case和else实现。新手经常在这儿绕晕。这段骨架跑通之后你就有了一个最小的道路规则库雏形。接下来要做的是把每条路的横断面细节填进去让道路从几块平板变成有层次的城市肌理。3. 道路横断面拆解车道、标线、人行道与路缘石的规则写法道路横断面是道路规则库的核心表达。一条完整的城市道路从中间往两侧大概由这几个部分组成中央隔离带如果有、机动车道、车道分界线、路缘带、非机动车道、机非隔离带、人行道、盲道、路缘石。数字孪生项目里当然不用每一步都按施工图来但几个关键层次一定要有否则模型一眼假。我通常把横断面拆成若干个子规则处理每个子规则只负责一个组成部分。这样逻辑清晰后续也好维护。先看车行道Carriageway -- s(1, 0, 1) split(z) { laneWidth : AsphaltLane | markingWidth : LaneMarking }* AsphaltLane -- extrude(world.y, 0.1) setupProjection(0, scope.xy, 4, 4) set(material.colors.diffuse, asphaltTexture) LaneMarking -- s(1, 0, markingWidth) t(0, 0, -markingWidth / 2) extrude(world.y, 0.02) set(material.colors.r, 0.95) set(material.colors.g, 0.95) set(material.colors.b, 0.95) set(material.colors.diffuse, #f2f2f2)AsphaltLane里先压扁为平面然后挤出0.1米的厚度这样沥青路面在场景中不是一张薄片不会出现从侧面看消失的问题。setupProjection(0, scope.xy, 4, 4)是设置UV投射4和4代表纹理在x、y方向各重复4次避免一张贴图拉伸到整条路导致纹理糊掉。很多新手路面贴图全是花斑就是因为忘了设置纹理投射参数。LaneMarking里我把标线宽度缩到markingWidth再整体沿z轴平移负方向一半让标线居中。接着挤出0.02米的厚度并设置白色材质。一条标线在宏观场景里其实很细小但有了这个厚度和颜色摄像机贴近地面时能看出细节对场景表现力帮助很大。人行道的规则稍微复杂一点因为要处理路缘石的竖面Sidewalk -- extrude(world.y, sidewalkHeight) setupProjection(0, scope.xy, 1, 1) set(material.colors.diffuse, sidewalkTexture)这里的sidewalkHeight我习惯设为0.12到0.15米。注意人行道挤出后侧面和顶面会共用材质贴图如果纹理的方向不对可以在侧面上单独设置材质比如Sidewalk -- extrude(world.y, sidewalkHeight) comp(f) { top : SidewalkTop | side : CurbFace } CurbFace -- set(material.colors.diffuse, curbTexture) setupProjection(0, scope.yz, 1, 1)comp(f)是CGA里的组件分割命令top对应顶面side对应侧面。这种顶面和侧壁分材质的做法在路缘石、中央隔离带、花坛边缘等场景里非常常用。否则你会在渲染结果里看到人行道的侧面也被贴了砖纹贴图视觉上会很怪。中央隔离带可以做成一个独立的可开关参数。很多城市主干道都有2到4米宽的中央分隔带里面要么做绿植要么做防撞护栏。我在规则库里会定义一个centerIslandWidth参数默认0表示没有隔离带大于0时在车行道中间插入隔离带逻辑Carriageway -- case centerIslandWidth 0: split(z) { laneWidth: AsphaltLane | markingWidth: LaneMarking | centerIslandWidth / 2: CenterIslandHalf | ~1: CenterIslandEdge | centerIslandWidth / 2: CenterIslandHalf }* else: split(z) { laneWidth: AsphaltLane | markingWidth: LaneMarking }*这里用case centerIslandWidth 0判断是否启用中央隔离带。CGA的case后面可以接数值比较多个分支用else兜底写法上接近传统编程语言。隔离带在实际表现中我更倾向用一条窄绿化带加两侧路缘石来表示而不是真的种一排树模型因为路网规模大时大量植物实例会严重拖累性能。横断面细节拆得越细规则文件越长但这恰恰是规则库的价值——你把城市道路的类型抽象成参数和分支每一条生成出来的路都符合同一套逻辑语言。到后面你换一个城市的路网数据跑的还是这套规则效果却很本地化因为只要调几个参数就能改变整个街道的性格。4. 十字路口和边界连接Junction规则中的细节与取舍道路与道路相交的地方才最考验规则库的功力。CityEngine里路网节点生成的shape会自动进入Junction规则。一个路口如果就是一块平地那当然很简单但实际项目中交叉口涉及转弯半径、人行横道、停止线、导流岛还有路口与道路的衔接平滑度。Junction规则最简单的版本是这样Junction -- s(1, 0, 1) extrude(world.y, 0.05) setupProjection(0, scope.xy, 4, 4) set(material.colors.diffuse, asphaltTexture)这个版本的效果就是一块和道路同高的沥青平板适合快速验收和数据调试。但真实场景里路口不能这么糊弄至少要处理两个问题。第一个问题是转角的平滑。CityEngine中街道中心的转角圆角通常不是在CGA里做的而是在Graph层面设定。你选中路网的节点在属性面板里调整cornerType和cornerRadius可以让路网的交点转弯半径变大。Segment shape进入Street规则后道路几何会沿着这个转弯路径生成道路本身也就有了平滑的转弯。如果路口的Junction规则和Street规则各自是孤立生成的你会发现转弯处有一块几何重叠或缝隙。我常用的办法是让Junction规则先根据shape.adjacentEdges判断与几条道路相连然后针对不同连接形态生成对应的人行横道和停止线。第二个问题是人行横道。斑马线这种东西纯用CGA几何去生成非常痛苦——你要在路口范围内找方向、定宽度、画条纹。我实际项目里的做法是用贴图代替几何。在Junction规则的顶面专门投影一张人行横道贴图配合透明通道在路面上画出斑马线。模型量小渲染也没压力。Junction -- s(1, 0, 1) extrude(world.y, 0.05) comp(f) { top : JunctionTop } JunctionTop -- case (hasCrosswalk): setupProjection(0, scope.xy, 1, 1) set(material.colors.diffuse, crosswalkTexture) set(material.opacity, 1) else: setupProjection(0, scope.xy, 4, 4) set(material.colors.diffuse, asphaltTexture)这里的hasCrosswalk可以定义成一个attr也可以在生成路网时通过规则属性值注入。如果你希望在不同路口做差异化处理可以把hasCrosswalk和路口等级挂钩——主干道交叉口有斑马线支路交叉口没有。这样规则会显得更智能。还有一个容易忽略的细节是交叉口地面与道路路面的高差衔接。如果Street规则中道路有0.1米的挤出厚度Junction规则里如果没有同样挤出那你会在路口看到一条台阶线。我踩过这个坑最后统一约定Street的沥青面挤出高度和Junction的挤出高度保持一致。这个约定要写进规则库的设计文档里否则三个月后你自己都会忘。Circle环岛的处理也值得一提。CityEngine路网本身支持环岛生成环路节点会作为特殊的Junction shape进入规则。我的做法是识别环岛节点后套用独立的RingRoad规则让环岛内侧单独生成一块绿地或硬质铺装外侧生成环形车行道。具体的识别方法是在Junction规则里判断geometry.isCircular之类的属性如果版本里取不到可以在Graph层的属性面板手动为环岛节点打标签然后再用attr接过来。路口规则写多了之后我最大的体会是不要试图用一个万能规则覆盖所有路口形态。程序化建模追求的是大多数情况自动处理、特殊路口手动覆盖所以我的规则库里经常写这么一行Junction -- case (fakeJunctionExists): FakeJunction else: GenericJunction预留一个手工覆盖的口子当你遇到复杂畸形路口时直接导一个模型进去替换比在CGA里死磕几何要高效得多。5. 规则库的参数化设计一套规则适配不同城市风格道路规则库做到能跑通路面、路口、标线之后就要往可配置的方向发展。因为我很快发现一个问题——项目A是新城区的宽马路双向八车道加宽绿化带项目B是旧城区的窄街道路幅不到12米两侧还是小店招牌。如果每换一个项目就改一堆规则文件那和手工建路的效率有什么区别参数化是这个阶段的核心目标。CGA的attr在设计时就要分好组、排好序、给好取值范围。这样在CityEngine的Inspector面板里别人一看到的就是一个干干净净的参数面板而不是一堆底层代码。我常用的属性分组这样写Group(整体路幅) Order(1) Range(6, 80) attr streetWidth 24 Group(整体路幅) Order(2) Range(0, 8) attr centerIslandWidth 2 Group(人行系统) Order(1) Range(1, 10) attr sidewalkWidth 4 Group(人行系统) Order(2) Range(0, 0.5) attr sidewalkHeight 0.15 Group(车道) Order(1) Range(2.8, 4.5) attr laneWidth 3.5 Group(车道) Order(2) Range(0, 12) attr laneCount 6Group负责在面板里分组Order控制排序Range限定参数范围。这样一个道路规则文件暴露出来的参数就是路幅多宽、几车道、人行道多宽、有没有隔离带这种对规划师友好的概念而不是那些split里头的技术细节。更进一步的参数化是做风格预设。CGA里可以用const定义一组预设配置也可以把一套参数保存成.cga文件里的preset。我的做法是在规则文件里定义几套命名配置RoadStyle -- case style urban_main: streetWidth 40 centerIslandWidth 3 sidewalkWidth 5 laneWidth 3.5 laneCount 8 case style urban_branch: streetWidth 20 centerIslandWidth 0 sidewalkWidth 4 laneWidth 3.5 laneCount 4 else: streetWidth 12 centerIslandWidth 0 sidewalkWidth 3 laneWidth 3.25 laneCount 2在Inspector里你只要切一个style下拉框整个场景的道路规格就会整体切换。这个功能在给甲方出多方案对比的时候非常好使窄路密网和宽马路大尺度两种方案一键切换方案汇报直接变成现场演示。不过要提醒一句参数化不是把所有东西都做成参数。有些东西做成参数反而增加理解负担。我自己掌握的原则是——影响道路性格的特征做成参数纯技术性的细节写死在规则里。比如车道宽度、人行道宽度、标线样式这种对城市风貌影响明显的东西必须暴露至于沥青材质贴图的投射次数这种改了一百次也没有视觉质变的细节直接写死就好。参数化设计还会引出一个资产管理问题。道路规则库通常需要配套一批贴图资源沥青纹理、人行道砖纹、路缘石灰浆、斑马线贴图、隔离带绿化贴图等。建议在项目目录里建立一个规范的资产文件夹rules/ street_rule.cga junction_rule.cga common_utils.cga assets/ textures/ asphalt/ sidewalk/ crosswalk/ markings/ models/ pole/ barrier/CGA里引用贴图时用相对路径不要用绝对路径。否则把项目文件拷贝到另一台机器上贴图全部失效这是最让人抓狂的移机事故。6. 实测中反复踩的坑与性能优化建议规则库写多了自然攒了一批坑。挑几个高频的说说。第一个坑是单位混乱。CityEngine的默认世界单位是米但很多从CAD导入的底图数据可能是毫米单位或者度单位WGS84经纬度。路网Graph宽度是24还是24000完全取决于数据源。解决办法是导入数据时统一做坐标转换或者在attr里用一个补偿系数兜底。我见过项目组因为单位不对Road生成出几百米宽的路面还一脸懵。第二个坑是split方向判断失误。前面提到过Segment shape的局部轴向并不总是统一的尤其是从复杂Graph生成的道路某些路段的shape局部z轴可能和其他路段不一致。表现就是大部分路生成正常个别路段车道方向跑偏。排查这种问题最有效的办法是开启CityEngine的Debug: Show Scope可视化。在Viewport里把shape的scope坐标轴显示出来一眼就能看出哪条路的轴向有问题再单独处理。第三个坑是超量细分导致场景卡死。split有一个maxIteration概念如果在repeat split里没有限制次数碰到一条特别长的路迭代生成的面片数量会非常惊人。CGA里可以给规则加一个兜底控制Hidden attr maxLaneIterations 12 Carriageway -- case laneCount 0: s(1, 0, 1) case laneCount maxLaneIterations: print(车道数超限截断为 maxLaneIterations) s(1, 0, 1) else: split(z) { laneWidth: AsphaltLane | markingWidth: LaneMarking }*别看这么简单加一行截断判断能在场景出错时救你很多时间。CGA里有print()函数用来输出调试信息规则卡住的时候多打印几个变量比盲猜高效得多。第四个坑是贴图UV漂移。这个问题在长距离道路上特别常见。一条几公里的路如果贴图投射坐标没有和世界坐标对齐路面纹理会在某个节点处突然跳变相当于一张贴图被折断了。解决办法是使用projectUV(0, world.xy, 1, 1)之类的世界坐标投射而不是依赖shape自身的局部坐标AsphaltLane -- extrude(world.y, 0.1) projectUV(0, world.xy, laneWidth, laneWidth) set(material.colors.diffuse, asphaltTexture)这里world.xy表示在世界坐标系的x-y平面上投射右侧两个参数是贴图在x、y方向的缩放尺寸我习惯直接取loneWidth这样每一段路面上的纹理大小看起来都均匀不会出现某段路纹理大、某段路纹理小的现象。第五个坑也是性能问题——几何实例化(GI)没开或不会用。CityEngine生成的模型导出到其他软件或引擎里几何体数量往往非常大。解决办法之一是把道路两侧的路灯、护栏、井盖这类重复物件用i命令做实例化StreetLight -- i(/assets/models/street_light.fbx)只要规则里用了i(模型路径)CityEngine生成的模型就自动以实例引用方式存在导出的场景文件里这些重复模型不会产生大量独立几何体。我实测过一条5公里的道路两边每隔30米一根路灯不实例化时导出模型可能有几百万面实例化之后只有一份模型数据加几百个引用性能差别是按数量级算的。另外关于导出CityEngine直接导出的模型很多时候贴图路径是相对工程文件的。如果要把模型交给渲染团队记得勾选复制贴图到输出目录或者用FBX/glTF格式时检查贴图是否成功嵌入。否则对方打开模型全是灰模然后跑来问你是不是模型坏了很浪费沟通成本。还有一点我特别想说的经验是先做一个极小的规则原型跑通再往里面加细节。写CGA规则很像写代码一个逻辑错误可能要到生成阶段才暴露。我见过太多人一上来就写几百行规则想一步到位做一个完美的道路系统结果运行报错根本定位不了。正确节奏是先写一个只含Street和Junction两个规则的骨架确保路网能生成再逐步加入人行道、标线、斑马线、隔离带。每加一个功能跑一次生成确认效果再往下走。这样你的规则库质量是可控的每一层都经过验证。最后关于规则库的长期维护我建议在每条规则文件顶部写清楚版本标记和适用范围。CGA本身没有强制要求版本号但CityEngine的版本升级偶尔会带来语法兼容问题养成写version 2019.1这种标注的习惯至少可以帮你定位是不是版本导致的语法错误。如果你和我一样经常要同时维护多个项目的道路规则库建议把通用的道路横断面逻辑抽成一个common文件然后各个项目里用import引进来只覆盖本项目特有的参数和特殊处理。这样一套通用规则库打底配合项目级定制既保证跨项目的复用性又不会让每个项目都绑死在同一套规则上。做规则库这件事比起写一条能让某段路好看的规则更值钱的是写一套能让所有路都协调且可调的规则系统。参数暴露得干净、分支处理得克制、错误拦截得及时等你在给甲方演示时一键切换方案就知道前面这些较真都值了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价