如何流式加载高度图
解读
面试官问“流式加载高度图”,并不是想听你背一段“用AssetBundle.LoadAssetAsync”就完事,而是考察你对超大开放世界地形的落地经验:
- 高度图动辄16K×16K、单通道16bit,原始体积512MB+,手机内存根本扛不住;
- 国内项目普遍要跑中低端安卓机,必须分块(Tile)+LOD+异步;
- 还要兼顾Editor烘焙管线与真机热更,否则策划一改地形就要整包重发,TapTap评分直接爆炸。
因此,回答要围绕“如何把512MB的高度图拆成N个小块,并在玩家跑动时以每帧<1ms的耗时把需要的那几兆送进GPU,同时保证CPU、GPU、带宽都不炸”展开,给出可落地的C#+Native代码框架与性能数据。
知识点
- 地形分块策略
- 逻辑层:把世界切成32×32 地形页(TerrainPage),每页再分8×8 地形块(TerrainTile),Tile是流式最小单元;
- 物理层:Tile文件用CRUD命名规则(x_y_lod.dat),放可寻址地址(Addressables)远端,支持CDN分片下载;
- 存储格式:高度图16bit灰度PNG→LZ4HC压缩后≈原体积8%,Android端OBB内再整包Zip对齐,iOS端放按需资源包;
- GPU友好布局
- 每Tile 127×127顶点,边界留1像素skirt防止裂缝;
- 用R16G16纹理存高度+法线长度,BC4压缩后显存占用再降50%;
- 双缓冲异步加载
- 主线程只维护环形加载队列(RingQueue),每帧预算0.8ms;
- 子线程用Unity.Mathematics+UnsafeUtility把下载字节流直接memcpy进NativeArray,避免GC;
- 加载完抛回主线程,调用Graphics.CopyTexture上传GPU,耗时<0.1ms;
- LOD与淘汰
- 用四叉树+Horizon Culling计算Tile屏幕误差,>2像素才加载下一级LOD;
- 卸载用LRU+引用计数,>150MB显存时触发强制降LOD;
- 热更方案
- 地形Tile打可寻址组(Addressables Group),ContentUpdatePath指向OSS+CDN,版本号走ScriptableBuildPipeline生成的hash文件;
- 玩家进游戏先比对本地catalog与远端catalog,差异Tile后台Wi-Fi自动下载,4G弹**“是否更新地形”**;
- 性能指标
- 低端骁龙660:内存峰值<180MB、上传GPU耗时<1ms/帧、下载带宽<200KB/s;
- 编辑器下256×256km世界,16K×16K高度图,首次烘焙<15min,增量烘焙<30s。
答案
分五步给面试官讲清楚落地细节,并直接背出关键代码片段:
- 分块导出
在Editor脚本里把TerrainData.GetHeights(0,0,w,h)读出的float[,]按TileSize=127切分,转ushort[]后PNG→LZ4HC压缩,命名“x_y_lod.dat”扔进Addressables分组,Build Script选ScriptableBuildPipeline以便增量。 - 运行时加载器
主线程每帧调用StreamingUpdater.Run():
子线程用UnityWebRequest下载,完成后把DownloadHandler.data用UnsafeUtility.Malloc拷到NativeArray<ushort>,再建Texture2D.R16调用CopyTexture到AtlasTexture的对应区域,耗时<0.1ms。//主线程预算0.8ms while(Time.realtimeSinceStartupAsDouble-start<0.0008f && _loadQueue.TryDequeue(out var req)) { if(req.state==ReqState.Downloaded) JobManager.ScheduleUpload(req); //子线程memcpy+CopyTexture } - LOD切换
以玩家为中心画环形LOD0→LOD3,距离阈值80m/160m/320m,屏幕误差>2像素才加载下一级;卸载用LRU链表,显存>150MB时强制降一级LOD并ReleaseTexture。 - 裂缝消除
相邻TileLOD差≤1,边界顶点用skirt(多出一圈退化三角形),Shader里**clip(v.vertex.z-0.001)**即可。 - 热更
地形Tile打可寻址组,ContentUpdatePath指到阿里云OSS,版本号走catalog.hash;玩家进游戏比对本地与远端catalog,差异Tile后台Wi-Fi自动下载,4G弹系统对话框。
拓展思考
- GPU Driven管线
如果项目目标骁龙8Gen2+,可把所有Tile高度图合并成2D TextureArray,用ComputeShader做GPU Culling+Indirect Draw,CPU侧零开销,但需Vulkan/Metal支持,**Unity2022.3+**才稳定。 - 物理碰撞流式
高度图流式后,PhysX TerrainCollider也要跟着换,可用Unity.Physics的HeightField子线程重建,耗时<3ms;但Android 32位机型PhysX版本<4.6有128层HeightField上限,需拆成多Collider。 - 法线与材质同步
高度图Tile加载完,法线图可在ComputeShader里Sobel算子实时生成,0.05ms/Tile;但SplatMap(地表贴图)体积更大,可BC7压缩+稀疏存储,只存非零权重块,内存再降70%。