ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity开放世界地形实战:TerrainComposer程序化生成管线搭建

Unity开放世界地形实战:TerrainComposer程序化生成管线搭建 开放世界项目的场景搭建里地形永远是最先卡住团队的环节。这一章我把项目里使用TerrainComposer构建地形系统、做程序化生成的完整实践整理出来重点讲清楚三件事为什么换成这个方案、节点树怎么搭、参数怎么调才能出想要的地貌。TerrainComposer下文统一叫TC是Unity生态里老牌的地形程序化生成插件它的核心思路是把地形生成拆成一条可视化管线高度、坡度、纹理、草、树、石块、地面摆放物都由可重复的规则生成而不是美术一个地块一个地块手动刷。这套东西适合正在做开放世界、大世界场景或者想把地形制作流程标准化、可复现的团队也适合个人开发者在起步阶段就建立正确的地形工作流。1. 为什么在项目里换用TerrainComposer做地形1.1 传统地形搭建的痛点早期做地形的时候我们用的是Unity自带地形工具美术先造高度图再一层层刷纹理、种树、放石头。单看几百米的小场景问题不大一旦放大到2公里乘2公里的区域痛点会非常集中。第一是工作量和资产量不可控美术手动刷一块地形可能要刷几天刷完之后很难统一调整。比如总体高度想抬高10%所有笔刷痕迹都得重来。第二是随机性和多样性很难平衡手刷容易留下整齐的笔触边界看起来很不自然而程序化方法可以用噪声和曲线把随机性控制得更好。第三是协作困难同一个地形场景谁打开谁改SVN/Git合并时全是冲突。第四是运行版本迭代策划经常要换地形整体气质手动方案改一轮成本很高。这几个痛点在实际项目中会累积成巨大的技术债。地形不只是高度它还牵扯纹理、碰撞、植被密度、NavMesh寻路区域、性能分区任何一个环节改变都可能牵动全局。所以我们从第二个版本开始彻底放弃手动刷地形转向TC这种程序化管线。1.2 TerrainComposer的核心设计理念TC和Unity自带地形工作流最大的差别是“生成”和“雕刻”的分离。在TC里地形高度、纹理、植被、物体都被看作一个个生成模块每个模块有自己的节点树。节点树里串联的是噪声、曲线、蒙版、函数、常量这些节点它们负责描述“这块地形应该长成什么样”而不是直接操作地形网格。点击Generate之后TC按节点树的逻辑实时算出结果并输出到Unity Terrain或自定义Mesh上。这种设计能带来三个直接收益。第一是参数化调整改一个高度曲线整个地形一起变不需要重新刷。第二是可复用同一个节点树换一组Seed就能生成完全不同的地貌相当于地形资产被“程序化工厂化”。第三是可审查节点树本身是文档团队成员打开就能明白地形逻辑不用去猜美术当时为什么在这里放一座山。TC在选择生成策略上还做了一件很实用的事它支持分层生成。高度、纹理、草、树、物体各走各的模块互不阻塞模块之间能通过高度坡度蒙版互相引用。这意味着我们先确定大块山脉走势再根据山脉高度和坡度去填充植被和岩石逻辑非常接近自然生态的分布规则。2. 地形系统的整体设计与资源规划2.1 地形区块划分与层次关系程序化地形不是启动TC疯狂点Generate就完事前期规划至关重要。我们在项目里用的是Unity Terrain的区块化方案把整个地图切成多个Terrain块每块尺寸和分辨率固定这样既能规避Unity地形单块大小上限也能利用流式加载控制性能。我们这个案例项目设定的世界范围是2048米乘2048米地形块尺寸用的是256米每块分辨率设定为257高度图采样点。为什么选256米因为TC的采样颗粒度、纹理密度和刷草密度都能在这个尺度下取得较好的平衡而且256米是2的幂便于后续做LOD和流式加载的层级划分。整个地图被切成8乘8共64块地形块与块之间通过相同分辨率和对齐接缝处理。TC在层级上的处理方式是把生成逻辑分成几个层级。第一层级是全局地形负责山脉走向、整体海拔、噪声基底。第二层级是分区地形按生态区域区分为平原、丘陵、高山、沼泽各自有独立的高度定义和纹理模块。第三层级是细节层只负责局部小起伏、道路压平、河流凹陷等修正。这样分的好处是调整某座山不会影响整体海拔而调整全局气候带也能快速作用于所有区块。2.2 程序化模块的责任边界TC里模块类型很多Terrain高度、Splat纹理、Grass草、Tree树、Object物体、Mesh网格等但我们给每个模块划了严格的工作边界避免一个节点树把什么活都干了。高度模块只负责宏观地形和微观细节宏观用低频噪声和曲线微观用高频Perlin噪声或FBM做细节扰动。纹理模块只根据高度、坡度、法线、噪声生成SplatMap不参与地形几何。草模块负责短植被树模块负责大型植被Object模块负责石块、枯木、建筑遗址等摆放物。Mesh模块单独用来生成道路压平区、河床这类需要明确几何形状的区域。这样划分有一个非常实际的好处当某个区域植被分布不合理时可以只改Grass模块或Tree模块的蒙版条件而不会连地形几何一起破坏。每个模块在TC节点树里都有独立预览和日志输出出现问题能快速定位到具体节点。为了更直观地管理64块地形我们在TC的生成流程里使用批处理。先在一张空场景里配置好全局模块和蒙版然后通过TC脚本接口循环创建Terrain块传入每块对应的Seed和位置。这样只需要维护一套主节点树跑一轮批处理就得到完整大世界地形矩阵。3. 核心参数与节点树配置实操3.1 节点树的组成与连接顺序TC的节点树是自上而下执行的每个节点代表一个操作、一个输入或者一个控制条件。我们在高度模块里最常用的节点组合是Constant节点作为基底高度Noise节点提供随机起伏Curve节点重新映射高度分布Math节点做加法或乘法混合最终接入Terrain Height输出。举个实际例子。我们想让主山脉高度在海拔200米到600米之间起伏山脊线清楚山谷平缓平原地带几乎零起伏。这个需求对应的节点链是Constant设成200代表最低海拔FBM Noise节点的Frequency设为0.008Octaves设为5输出范围是0到1的随机噪声Curve节点把噪声值映射成“山脊急、山谷缓”的非线性曲线最后用Math Multiply把噪声乘以400再与Constant相加得到最终高度范围200到600。如果只是这样整个地图所有区域都会是同样强度的山脉。所以我们再接一个Mask节点用一个大尺度的Perlin Noise作为区域蒙版只让蒙版亮度高的地方山脉密集蒙版暗的地方保持平原。这个流程在TC里实现起来就是拖节点、连线、设参数完全没有写一行代码但结果已经非常接近真实自然地形。3.2 关键参数详解高度、坡度、噪声倍频高度参数决定地形宏大的性格坡度参数决定植被和纹理分布是否合理噪声倍频则决定地形的细节丰富程度。这三个参数在TC里通常是联动调整的单独改某一个常常会出现“看着不对”的尴尬。先说高度。TC里Terrain模块的Height输出范围通常是0到1实际海拔由Unity Terrain的Terrain Height乘回来。比如我们把Terrain Height设成800TC内部高度值0.5就对应400米实际高度。所以不要被节点里那些0.3、0.7的值迷惑心里先要把0到1映射到真实海拔上。坡度在TC中可以从高度图自动计算出来不需要额外算法。TC的Slope节点会根据相邻像素高度差输出坡度值范围还是0到1。真正需要注意的是坡度与纹理的配合。我们在Splat模块里用坡度蒙版控制岩石和草地坡度小于0.25的区域铺草地坡度大于0.6的区域露出岩石中间用0.35到0.6的过渡带做混合渐变避免出现一刀切的分界线。噪声倍频即Octaves它直接影响地形的细节颗粒。FBM Noise把几个不同频率的噪声叠在一起第一个Octave决定大轮廓后面的Octave逐渐增加细节。Octaves越多地形越碎性能开销和噪声计算时间也越高。我们在全局高度上Octaves只用4够用细节高度模块上用到6但要控制幅值不超过0.03倍只做微起伏否则跑出来的地形会全是高频毛刺。3.3 纹理与细节的蒙版控制纹理不像高度那么直观它更依赖环境要素的叠加。TC的Splat模块内建了多个Splat Layer每个Layer对应Terrain的Albedo贴图并通过输出Map控制覆盖范围。这里最关键的技巧是“不同Layer不要用不同大小的噪声分别生成”那样会得到完全随机的RGB斑块真实感很差。正确的做法是先定义环境规则。例如海拔300米以下以绿草为主300到500米是低矮灌木和裸土500米以上是岩石坡度大于50度强制岩石阳光朝向北方的地方多雪南方多草。这些规则全部可以在TC节点树里用高度、坡度、法线、方向这几个Mask节点组合出来。具体实现上每增加一个Splat Layer我们就在它的Map输入上接一个自己的节点组合。草地层的Map是这样接的高度Mask输出一个0到1的渐变海拔低于300米的部分为1高于500米为0中间区间线性过渡坡度Mask反向使用坡度大时衰减再将这两个Mask用Math Multiply相乘最终得到草地层的覆盖范围。岩石层则反梯度使用高度Mask并叠加一个坡度阈值做强化。雪地层额外接入一个Normal节点根据地形的法线方向调整覆盖位置让背阴面优先。草、树和Object模块也全部依赖蒙版控制而不是全地图随机散布。比如树只长在海拔250米到450米、坡度小于30度、草地层蒙版大于0.6的区域这样树林带非常自然地和草原、高山分离。这些条件都在TC的Spawn节点里设置核心逻辑是“条件满足才生成”不是“在随机位置生成后检查”。4. 从生成到落地地形数据的导出与运行时流程4.1 离线生成与运行时生成的选择TC支持在编辑器里生成地形也支持运行时动态生成。这两种模式我们分别在两个项目里试过经验是除非你有一个内容无限变化的玩法需求否则优先使用编辑器离线生成把结果烘焙成普通Unity地形资产。原因很简单运行时生成会增加加载耗时还要处理Shader变体、碰撞体烘焙和植被摆放状态性能压力非常大。我们在当前项目里走离线生成路线。编辑器中配好节点树点一下GenerateTC把高度图、SplatMap、TreeInstance、DetailObject全部写入Unity Terrain。生成完成后Terrain数据完全脱离TC依赖即使项目里卸载TC插件地形仍然正常。这对版本管理和打包都有好处构建产物里不会多出一套程序化生成代码。但如果是小型Demo或者想要玩家自定义世界运行时生成是可行的。TC提供Runtime API可以在场景加载时创建地形块并执行节点树。这块需要特别注意运行时地形不能直接用编辑器里的Terrain绘制组件碰撞体需要在生成后通过TerrainCollider重新创建植被摆放密度要调低否则生成瞬间Draw Call会爆炸。4.2 生成后的烘焙、碰撞与性能优化地形生成只是第一步后续的烘焙和优化往往占整个地形系统70%的工作量。我们把64块Terrain生成完毕后先做了一次NavMesh整体烘焙。这里有个坑TC默认生成的地形高度图如果分辨率过低坡度数据会比较粗糙NavMesh在一些狭窄山谷会生成断点。我们后来把每块Terrain的Detail Resolution从1024提高到2048NavMesh生成明显顺畅代价是内存涨了将近1GB但换来的是更准确的寻路区域值得。碰撞方面Unity Terrain自带TerrainCollider用高度图做碰撞天然高效。但TC生成的Object模块会产生很多单独的石头和物件如果每个都是带碰撞的GameObject物理引擎压力会很大。我们采用的策略是只给玩家可交互的大物件保留碰撞小的碎石和草使用碰撞体关闭的粒子化存放或者用TC的Spawn节点里的Collider选项按距离层自动裁剪。性能优化最核心的是LOD和Culling。TC本身不做地形LOD它是调用Unity Terrain的设置所以我们在每块Terrain的Base Map Distance和Detail Distance上做了统一。Base Map Distance设为160米近处用高质量Splat纹理远处用低精度BaseMapDetail Distance设为80米超出这个距离草和细节物体直接被裁剪。Object模块装出来的石头和树不要只依赖Terrain Culling需要额外挂一个简单的距离管理器超过视距直接SetActive(false)。5. 实操中踩过的坑与排查技巧5.1 地形纹理接缝与Tiling问题TC生成SplatMap的时候如果每块Terrain独立采样噪声相邻两块之间的纹理分布不会连续接缝会非常明显。这个问题折磨了我们差不多一周。后来处理方式是给TC的Terrain位置节点做一个统一的世界坐标偏移采样纹理时使用世界位置作为坐标而不是本地坐标。这样噪声采样在地图范围内是全局连续的相邻块接缝自然消失。另一个常见问题是Tiling也就是纹理贴图重复感太强。Splat纹理如果Tiling设8米整片草地在远距离看会有规则重复网格。我们把草地纹理的Tiling提高到16米岩石纹理提高到10米并用TC提供的小幅Random UV Offset节点让每块地形内的纹理坐标在范围内随机偏移一点肉眼基本看不出规律。5.2 生成结果与预期差异大的定位方法TC有一个非常实用的功能节点级预览。选中任意节点可以看到该节点的输出纹理或高度值。遇到整块地形完全不对的情况不要去调最末端的输出节点而是从最上游的Constant和Noise开始看一级一级往下查。正常流程是先看Noise输出是否为0到1的随机值再看Curve映射后是否满足预期范围再看Mask作用区域是否准确。有次我们发现某块地形整体凹陷排查后是Math节点把高度值减多了。还有一次所有树全生成在水里排查过程是先看Tree模块的Spawn条件确认海拔范围无误后查看它依赖的地形高度蒙版输出发现蒙版节点误连到了SplatMap的河流纹理上导致树区域反了。TC的节点树不像写代码那样有报错机制出错多数是连线逻辑问题养成逐节点预览的习惯能省很多时间。5.3 插件版本与Unity版本兼容性TC版本和Unity版本需要严格匹配这不是危言耸听。我们在Unity 2021升级到2022后TC的Runtime API出现了一个函数签名变化导致原来能用的运行时生成代码全部编译报错。查了官方更新日志才发现TC针对新版Unity做了底层Terrain Data接口调整。建议任何团队在使用TC前先固定Unity小版本比如2021.3.10f1然后锁定TC的版本号千万不要Unity一升级就跟着升。TC的编辑器脚本在部分版本中会依赖UnityEditor命名空间打包时必须把编辑器相关代码排除。我们最初遇到过打包后TC生成地形时崩溃就是因为在运行时误调用了编辑器API。如果你遇到插件突然失效的情况先看Console里有没有关于SerializedObject或Module Manager的报错。TC在加载场景和节点树时会注册大量SerializedObject版本不兼容最常见的症状是模块列表空白、节点树不显示、点击Generate没反应。处理方式是把TC组件所在的GameObject重新激活让它重新执行OnEnable注册很多时候能直接解决。最后再分享一点小技巧在实际项目里用TC生成地形我们自己的原则是“能参数化就参数化能离线就离线”。节点树不是搭完就固定不动的它应该像项目里的其他代码模块一样有版本记录。每次大改地形风格我会把之前的节点树导出成TC的配置文件保存再复制一份做实验。踩坑好几次之后才深刻体会到程序化地形的前期规划比生成本身重要得多地块划分、模块边界、蒙版规则这几个设计决策做对了后面调整都比别人快一倍。这套工作流我们目前已经稳定跑了大半年如果你也在考虑引入TC建议先拿一张256米乘256米的小地形把节点树的逻辑跑通再铺到大世界。
返回列表