ARTICLE DETAIL

资讯详情

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

Unity地形Terrain转FBX全流程:从原理到数字孪生应用

Unity地形Terrain转FBX全流程:从原理到数字孪生应用 1. 为什么大家都在找Terrain转FBX我最近接到的三类真实需求先说我自己的经历。前阵子一个做智慧园区的朋友找上来问能不能把Unity场景里的Terrain地形丢给Cesium用他原话是你导出个FBX给我不就行了。我当时第一反应是Terrain不是Mesh哪能说导就导但人家需求摆在那儿我后来还真花时间把这件事做顺了。做顺之后我复盘了一下发现Terrain转FBX这个需求压根不是个例最近半年我在不同项目里至少碰上过三类完全不一样的使用场景每类背后都有不同的痛点和诉求。第一类是数字孪生和离线地图接管线。很多数字孪生项目里Unity负责交互Cesium负责全球地形和影像两边要无缝衔接。Cesium for Unity加载离线地图时需要把本地地形数据转成它能够识别的模型或瓦片格式而FBX是中间交换格式里最通用的一种。尤其是FBX转3DTiles这条链路很多团队就是先把Unity里的地形导出成FBX再用工具转成3DTiles这样Web端和桌面端都能加载。如果不转Terrain组件的数据别人根本拿不到。第二类是跨软件协作。合作方不一定装了Unity客户可能用Blender、3ds Max或者Maya改地形。我遇到过甲方要求把这个山头稍微抬一抬的情况他们在Blender里编辑改完再导回来。这时候如果只给一个Unity场景文件对方完全没法下手唯一可行的就是给一个标准FBX让他们在DCC软件里改完重新导出。第三类是运行时性能和可控性。Unity的Terrain组件在编辑器里确实好用但是在移动端、WebGL、甚至部分电脑上性能表现并不稳定。地形实时渲染要处理LOD、细节层级、树和草地实例一旦地形尺寸大DrawCall和内存都会上来。Mesh则简单直接一个静态网格加上材质贴图能合并、能简化、能套LOD Group还可以放到烘焙管线里处理阴影和光照。所以很多性能敏感的项目干脆把Terrain烤成Mesh再用。三类需求看起来不同但都指向同一个动作把Terrain变成一种可迁移、可编辑、可优化为标准网格资产。这也是这篇文章想聊透的东西。2. 动手前必须想明白的一件事Terrain和Mesh根本不是一回事我见过不少人在网上问为什么我选中Terrain按导出Unity怎么没有这个按钮其实根源问题是没搞清楚Terrain的本质。2.1 Terrain地形到底存在哪Unity的Terrain不是一个模型而是一组高度数据加上一套实时渲染规则。它的核心是heightmapResolution也就是高度图分辨率。你可以把heightmap理解成一张灰度图每个像素的高度值对应地形某个格点的高度。除此之外还有SplatMap也就是多层地表贴图的混合权重还有Detail对象花花草草和TreeInstance树实例的分布数据。还有一个非常关键的参数叫size定义了地形的长宽高默认是1000x600x1000。实际项目里我会把地形拉得很大比如2公里见方这时候Terrain会按区块自动做quadtree LOD确保远处近处都能跑得起来。也就是说Terrain在运行时是一个实时细分、动态加载的东西它本身并不存在顶点数组让你直接导出。2.2 Mesh由什么构成为什么需要导出而FBX里的Mesh是显式几何数据它必须有明确的vertices顶点、triangles三角形索引、uv贴图坐标、normals法线可能还有tangents和骨骼权重。你导出的FBX本质上是一份已经算好所有顶点在哪里的快照。所以从Terrain到FBX中间必然经历一次采样和网格化用高度图生成一个静态Mesh再把这个Mesh序列化成FBX。这一步是不可跳过的。Terrain组件本身不能导出成FBX它不具备Mesh的数据结构。2.3 贴图为什么总丢SplatMap和单张贴图的差别很多人第一次导出后发现模型是白模或者全黑这就要说到Terrain贴图的工作方式。Terrain上面的草地、泥地、岩石是好几张diffuse贴图按SplatMap权重混合出来的SplatMap本质上是一张或多张RGBA图每个通道控制一种贴图的分布。也就是说地形最终呈现在你眼前的画面是运行时混合的结果并没有一张现成的完整贴图等着给你导出。Mesh一个材质槽通常只能放一张漫反射贴图所以转换的时候必须把多层混合烘焙成一张单独的albedo贴图。这个操作如果工具没有帮你做你就只能拿到一个没有贴图的裸网格。这也是整个过程里最容易踩的坑后面我会专门讲。3. 3分钟导出流水线我从选中地形到拿到FBX的完整操作既然原理清楚了实际操作就简单了。我先说结论想在3分钟内把Terrain变成FBX最稳妥的组合是社区传送脚本 Unity官方FBX Exporter。你不需要手写FBX序列化也不需要打开Blender来回导全部在Unity编辑器里就能完成。3.1 方案对比用脚本还是插件先说选型。网上目前能搜到几条路线我按实际体验整理了一张表方案是否自动烘焙贴图操作时间适用场景坑点Asset Store的TerrainToMesh类插件部分支持几分钟一次性导出、快速交付插件可能停止维护需手动下载脚本自写编辑器脚本 FBX Exporter否需自己处理贴图十几分钟干净、可控、学习原理要处理UV、法线、索引和材质导出OBJ再用Blender转FBX否需要建模软件后续还要修模型多了中间步骤容易丢贴图Terrain2Mesh等付费工具支持最快商业项目、需要批量处理付费五六十美金起如果你只是想最快拿到一个带贴图的FBX建议搜一下社区流传的TerrainToMesh.cs编辑器脚本把它放进Assets/Editor目录然后选中Terrain在Tools菜单里运行。这类脚本通常会在场景里生成一个MeshFilterMeshRenderer的物体有的还会给你烘焙一张albedo贴图并生成材质。但如果你想彻底理解这个过程或者你老板不让你装来路不明的脚本我建议你用下面这套最小方案代码不复杂而且完全透明。3.2 把Terrain变成Mesh最小可用脚本这里我给出一份可以在Unity 2020及以上版本直接编译的编辑器脚本。它做的事情很简单把heightmap的每个采样点变成顶点按行列拓扑连成三角形计算UV保存为一个mesh asset。using UnityEngine; using UnityEditor; using System.Collections.Generic; public static class TerrainToMeshTool { [MenuItem(Tools/Terrain/Export Mesh Asset)] public static void ExportFromScene() { Terrain terrain Object.FindObjectOfTypeTerrain(); if (terrain null) { Debug.LogError(场景中没有Terrain对象); return; } TerrainData data terrain.terrainData; int res data.heightmapResolution; float[,] heights data.GetHeights(0, 0, res, res); ListVector3 vertices new ListVector3(res * res); ListVector2 uvs new ListVector2(res * res); Listint triangles new Listint((res - 1) * (res - 1) * 6); float inv 1f / (res - 1); float sizeX data.size.x; float sizeZ data.size.z; float heightY data.size.y; for (int z 0; z res; z) { for (int x 0; x res; x) { vertices.Add(new Vector3(x * sizeX * inv, heights[z, x] * heightY, z * sizeZ * inv)); uvs.Add(new Vector2(x * inv, z * inv)); } } for (int z 0; z res - 1; z) { for (int x 0; x res - 1; x) { int i0 z * res x; int i1 i0 1; int i2 (z 1) * res x; int i3 i2 1; // 两个三角形逆时针绕序朝上法线默认朝Y正方向 triangles.Add(i0); triangles.Add(i2); triangles.Add(i1); triangles.Add(i1); triangles.Add(i2); triangles.Add(i3); } } Mesh mesh new Mesh(); mesh.indexFormat res * res 65535 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16; mesh.SetVertices(vertices); mesh.SetUVs(0, uvs); mesh.SetTriangles(triangles, 0); mesh.RecalculateNormals(); mesh.RecalculateBounds(); string path EditorUtility.SaveFilePanelInProject( 保存Mesh, TerrainMesh, asset, 选择保存位置); if (string.IsNullOrEmpty(path)) return; AssetDatabase.CreateAsset(mesh, path); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log(地形Mesh已导出到: path); } }这段代码里要注意几个细节。第一heightmapResolution是含边界的比如513x513而你实际能用的三角形个数是512x512个小格所以循环里res不减1会稍微多出一圈问题不大但会增加顶点数。第二indexFormat必须根据顶点数判断是否超过65535否则生成的Mesh在Unity里无法使用UInt16索引。第三RecalculateNormals是面法线平均地形边缘会看起来柔和如果你希望棱角分明可以手动根据高度梯度计算法线。脚本执行后在Project窗口里会出现一个mesh asset。接下来新建一个空GameObject添加MeshFilter并拖入这个mesh再添加MeshRenderer。注意如果你直接用这段脚本Mesh上是没有贴图的你得给它赋一个材质后面再说贴图怎么办。3.3 安装Unity官方FBX Exporter并导出有了带MeshFilter的GameObject接下来就是导出FBX了。现在Unity官方提供了一个免费包叫FBX Exporter在Package Manager里直接搜FBX Exporter就能找到注意要选择Unity官方发布的版本。安装完成后选中刚才那个带MeshFilter的物体在菜单栏的GameObject或者Assets下都会出现Export To FBX的入口。我习惯用GameObject Export To FBX因为这样可以直接导出当前选中物体。面板里比较关键的选项有这么几个Export Format选Binary文件更小兼容性最好。Include选Model(s)如果场景里只有这个地形物体就够了。Textures如果有材质贴图可以选Embedded Textures这样贴图会直接内嵌到FBX文件内部发给别人不会出现贴图路径失效的问题。Level of Detail如果物体上挂了LOD Group可以选择导出全部LOD级别如果没有就保持None或者All。导出后你会得到一个.fbx文件把它拖到Blender或者3ds Max里试一下正常情况下几何是能看到的。这就是3分钟内完成的完整流程前提是你已经把脚本和FBX Exporter都装好了。如果你第一次操作建议先把一个小尺寸地形跑通别第一次就导2公里的大地形不然生成网格那一步会卡你半天。3.4 为什么这套流程能控制在3分钟核心原因是我没有手写FBX序列化器也没有引入第三方建模软件。Terrain到Mesh这段用脚本批量算Mesh到FBX这段用官方导出器总共两步。你真正花时间的地方是脚本环境的准备和导出参数的熟悉一旦路径熟了以后每次操作确实就一两分钟的事。不过要泼一盆冷水3分钟只是拿到裸网格或带基础贴图网格的时间。如果你的地形上有SplatMap多层混合、有树木和细节对象想把这些一起完美还原到FBX里3分钟是做不到的。地形本身的植被实例、细节对象无法直接转成FBX网格通常需要额外导出或跳过。我做项目时一般只转地形高度和地表贴图花草树木都在DCC软件里重新放置这是更务实的方案。4. 我第一次导出后几乎崩溃贴图、法线、坐标和顶点规模的排查记录工具跑通不等于一切顺利真正折磨人的是导出后的各种看起来不对。我把自己的排查过程完整写下来这些坑恐怕每个做过的人都会碰到。4.1 导出到Blender后一片漆黑材质和贴图到底去哪了我第一次导出时给MeshRenderer挂了一个用SplatMap的地形材质然后自认为有材质了。结果FBX一到Blender里模型是黑色的材质面板干干净净连一张贴图都没有。排查链路是这样的。首先我在Unity里选中导出物体检查MeshRenderer材质是否生效——确实生效Editor里能看到绿色草地。然后我用FBX Exporter面板里的Textures选项切到Embedded Textures再导出发现Blender里依然黑色。最后我意识到问题出在这个材质根本没有一张有效的albedo贴图上。SplatMap只是混合权重FBX导不出这种运行时混合效果。解决方案是先用一个烘焙工具把地形多层贴图合成一张RGBA的albedo图再把这张图赋给Standard材质最后导出FBX。如果你用的是TerrainToMesh类脚本它一般会内置Bake Textures选项勾上它得到的Mesh会自动带一张烘焙出来的完整贴图。否则你就得自己用相机俯拍地形烘焙或者用RenderTexture把Albedo拷下来。另一个坑是材质连接。即使你把一张albedo贴图放进了材质FBX导入Blender后如果Blender的材质节点没有自动连上Base Color画面还是黑的。需要在材质节点编辑器里手动把Image Texture连到Principled BSDF的Base Color上。不要以为Unity导出了就万事大吉DCC软件对PBR材质连接链路的支持差异很大。4.2 阴影好端端地反了法线方向与顶点绕序问题第二回我学乖了烘焙了albedo贴图导出后在Blender里看到草地颜色正常但整个地形从上方看是暗的只有从下方看才是亮的。这就是典型的法线方向反了。原因可以从两点追溯。一是我的编辑器脚本里三角形索引的绕序如果生成顶点时从左上往右下采样索引顺序不对也会出现这种问题。二是FBX导出器在不同软件之间切换时坐标系的Y轴方向和三角形绕序会发生变化。Unity是左手坐标系3ds Max和Blender是右手坐标系轴转换过程中如果三角形翻转法线方向自然会反。排查渠道是这样先在Unity里把刚才生成Mesh的地方放大开启Scene视图的Wireframe模式找一个三角面观察顶点的绕序是顺时针还是逆时针。Unity规定正面是逆时针方向在Scene视图里应该能看到三角形正面朝上。如果Unity里看着就是反的说明脚本的索引顺序有问题改代码。如果Unity里正常导入Blender后反了那就是FBX轴转换造成的直接在Blender里编辑模式全选Mesh Normals Recalculate Outside一次就能修好。这里还牵扯到一个Unity阴影问题很多时候我们把一个Mesh拉进场景发现阴影斑驳或者部分面没有阴影第一个念头是调Shadow Distance其实多半是法线不一致。建议在Unity导入FBX后打开模型的Import Settings如果Model标签页里有Swap UVs或Generate Normals选项可以试着把Normal置为Calculate再配合RecalculateNormals重新算一遍阴影问题会少很多。4.3 坐标偏移和尺寸变形大尺寸地形导出后的精度问题还有一个非常隐蔽的坑出在坐标上。我的地形放在世界坐标的(5000, 0, 3000)附近场景里Unity跑着没问题但导出的FBX在Blender里打开模型的中心点跑到了远处而且细看顶点坐标的数值非常大有的甚至出现了抖动和缝面。原因很简单我的编辑器脚本生成Mesh时顶点坐标是用相对于Terrain物体自身原点的方式计算的但如果我在导出FBX前把Terrain物体整体拖到了很远的世界坐标位置FBX Exporter在导出时会把GameObject的Transform烘焙进Mesh导致FBX里的顶点坐标绝对值非常大。单精度float的有效位数大约7位当地形尺寸有四五个零的时候小数点后面的精度就开始丢了地形边缘会出现锯齿和小裂缝。解决方法是统一的导出前把目标GameObject的Position尽量归到原点附近或者FBX Exporter面板里有类似Convert to FBX时自动重置Transform的选项勾上它。如果实在不能移动原物体那就先在场景里复制一份把复制出来的物体归零再导出。另外注意单位问题Unity里1单位1米Blender默认单位也是米但3ds Max默认可能是厘米导入后检查一下缩放是否为1比1否则地形会被放大100倍。4.4 顶点数爆炸500米见方的地形为什么能把WebGL跑崩我一开始贪心把heightmapResolution保持默认的1025生成Mesh后顶点数超过了100万。然后想着FBX而已导出就行。结果文件倒是不大但导入到Unity WebGL工程里内存直接飙得很高帧率掉到个位数甚至有几次发布过程直接崩了。细细分析下来有两点。第一Terrain组件本身有动态LOD它不会一次性把100万顶点全压给GPU但Mesh没有这个能力转成Mesh后就是实实在在的100万顶点GPU压力大很正常。第二FBX格式本身支持大网格但Unity在运行时加载FBX并实例化时会为顶点数组分配大量内存如果Mesh的indexFormat是UInt32在某些移动端GPU上兼容性也差。所以我不再执着于原始分辨率而是老老实实把heightmapResolution降到129或者65再采样再配合后面提到的简化手段才能保证WebGL上是可用的。5. 进阶玩法这块FBX如何更顺滑地融入数字孪生与3DTiles管线如果只是导出一次看看效果上面的内容已经够了。但只要牵扯到实际项目交付尤其是数字孪生、WebGL、离线地图这类场景你就得继续往下做优化否则这块FBX就是一个能看不能用的巨物。5.1 网格简化哪些必须保留哪些可以直接砍转成Mesh之后下一步通常是减面。对于地形你完全不需要保留原始高度图的所有细节。只做远景展示的场景我把网格砍到原始面数的5%到10%视觉上基本察觉不到差别。近景交互较多的场景保留20%左右也够了。减面工具有很多。Blender内置的Decimate Modifier简单免费但它的三角剖分不够规则容易出长条面。商业的Simplygon或者Mesh Baker质量更高尤其适合游戏资源规范。如果只想在Unity内部做也可以用MeshUtility或者专门的减面插件。减面后的网格一定要重新检查法线和UV。Decimate这类工具会让法线分布变得不均匀导入后你经常能看到阴影断层。正常流程是减面 - Recalculate Normals - 重新烘焙光照贴图。5.2 贴图分辨率该设多少移动端和WebGL下的平衡Unity编辑器里分辨率设置很容易被忽略但这里皮肤大小直接决定FBX纹理的显存占用。我的经验是地形尺寸在1公里以内的albedo贴图用2048x2048足够最多4096x4096超过1公里的大地形与其做一张超大贴图不如把地形分成几个区块每个区块配一张2048贴图这样加载和显存布局都更合理。WebGL项目尤其要控制贴图大小。浏览器里有显存和内存上限一张8192x8192的RGBA贴图就要256MB显存很快就能干爆低端手机。建议发布WebGL时用Texture Compression或者直接降低Import Settings里的Max Size2048是一个比较舒服的档位。5.3 从FBX到3DTilesCesium for Unity离线地图衔接要点很多人做离线地图时想把Unity里的地形FBX转成3DTiles喂给Cesium这个思路没错但要注意几个环节。首先Cesium本身适合加载全球瓦片数据本地一块小地形的FBX如果直接转3DTiles地理参考和坐标原点要对齐否则模型会悬在半空或者陷入地下。最好把地形摆到真正的地理坐标附近再导出或者在Cesium里手动设置模型的经纬高偏移。其次FBX转3DTiles的工具链一般会做纹理转换内嵌贴图的FBX转出来的瓦片通常会有很大的文件体积建议处理前先压缩贴图。我自己的做法是Unity里降采样mesh - 导出FBX - 在Blender里减面和重拓扑 - 导出glTF - 再转3DTiles。整个过程比较绕但每一步都能控制质量和大小比直接一把梭要稳得多。最后如果目标平台是Unity WebGL而你自己在跑Cesium for Unity的离线地图千万不要在运行时动态加载一个几十万面的大FBX。Cesium本质上适合流式瓦片加载你把整块地形一次性塞进去性能会非常难看。正确做法是预先切瓦片、做LOD或者干脆把地形切割成小块。5.4 后续优化MeshCollider、物理、光照贴图烘焙转成Mesh之后地形还可以获得一些Terrain组件很难给的能力。比如Terrain自带的Collider在角色跳跃、载具越野时偶尔会穿模而MeshCollider配合减面后的低模反而能稳定很多。我会专门做一个只用于碰撞的低模勾选Convex还是不勾选看需求然后再隐藏渲染。还有一个很值得做的优化是光照贴图烘焙。Terrain组件配合Lightmapper不是不行但烘焙时间很长而且地形一改就要全部重烘。转成Mesh后你可以在低模上轻量烘焙或者用GPU Lightmapper一次搞定效果和性能都可控。烘焙前记得把Mesh设成Static并且打开Receive GI选项光照贴图的UV如果不对就勾选Generate Lightmap UVs生成一组新的UV。Mesh转出来后还有一个经常被忽略的细节是LOD Group。我刚才说过转成Mesh等于放弃了Terrain的动态LOD所以你得手动把高模、中模、低模编成一个LOD Group。在离相机远的位置直接切到低模近处才用高模这样WebGL和移动端都能救回来。LOD链接的过程不难但一定要在导出FBX之前就把不同精度的Mesh版本准备好不然又得重新导一轮。最后说几句实在话我这套流程走下来最大的体会是把Terrain转成FBX这件事本身不难难的是在每个环节保持清晰的目标你是要一个能看的白模还是要一个带贴图可交付的资产你是要给Blender同事改还是要上WebGL跑目标不同heightmapResolution要不要降、贴图要不要烘焙、FBX Exporter里Embedded Textures要不要勾全都随之变化。每次我在导出前多花两分钟想清楚这些问题后面就能少折腾两小时。如果你第一次导出就遇到异常别急着骂工具先按我说的三个方向排查贴图有没有生成、法线有没有反、顶点数是不是超了大概率能找到原因。
返回列表