如何流式加载高度图

解读

面试官问“流式加载高度图”,并不是想听你背一段“用AssetBundle.LoadAssetAsync”就完事,而是考察你对超大开放世界地形的落地经验:

  1. 高度图动辄16K×16K、单通道16bit,原始体积512MB+,手机内存根本扛不住;
  2. 国内项目普遍要跑中低端安卓机,必须分块(Tile)+LOD+异步
  3. 还要兼顾Editor烘焙管线真机热更,否则策划一改地形就要整包重发,TapTap评分直接爆炸。
    因此,回答要围绕“如何把512MB的高度图拆成N个小块,并在玩家跑动时以每帧<1ms的耗时把需要的那几兆送进GPU,同时保证CPU、GPU、带宽都不炸”展开,给出可落地的C#+Native代码框架性能数据

知识点

  1. 地形分块策略
    • 逻辑层:把世界切成32×32 地形页(TerrainPage),每页再分8×8 地形块(TerrainTile),Tile是流式最小单元;
    • 物理层:Tile文件用CRUD命名规则(x_y_lod.dat),放可寻址地址(Addressables)远端,支持CDN分片下载
    • 存储格式:高度图16bit灰度PNG→LZ4HC压缩后≈原体积8%,Android端OBB内再整包Zip对齐,iOS端放按需资源包
  2. GPU友好布局
    • 每Tile 127×127顶点,边界留1像素skirt防止裂缝;
    • R16G16纹理存高度+法线长度,BC4压缩后显存占用再降50%;
  3. 双缓冲异步加载
    • 主线程只维护环形加载队列(RingQueue),每帧预算0.8ms
    • 子线程用Unity.Mathematics+UnsafeUtility把下载字节流直接memcpy进NativeArray,避免GC;
    • 加载完抛回主线程,调用Graphics.CopyTexture上传GPU,耗时<0.1ms;
  4. LOD与淘汰
    • 四叉树+Horizon Culling计算Tile屏幕误差,>2像素才加载下一级LOD;
    • 卸载用LRU+引用计数>150MB显存时触发强制降LOD
  5. 热更方案
    • 地形Tile打可寻址组(Addressables Group)ContentUpdatePath指向OSS+CDN,版本号走ScriptableBuildPipeline生成的hash文件
    • 玩家进游戏先比对本地catalog远端catalog,差异Tile后台Wi-Fi自动下载,4G弹**“是否更新地形”**;
  6. 性能指标
    • 低端骁龙660:内存峰值<180MB上传GPU耗时<1ms/帧下载带宽<200KB/s
    • 编辑器下256×256km世界,16K×16K高度图,首次烘焙<15min增量烘焙<30s

答案

分五步给面试官讲清楚落地细节,并直接背出关键代码片段:

  1. 分块导出
    在Editor脚本里把TerrainData.GetHeights(0,0,w,h)读出的float[,]按TileSize=127切分,转ushort[]PNG→LZ4HC压缩,命名“x_y_lod.dat”扔进Addressables分组,Build ScriptScriptableBuildPipeline以便增量。
  2. 运行时加载器
    主线程每帧调用StreamingUpdater.Run()
    //主线程预算0.8ms
    while(Time.realtimeSinceStartupAsDouble-start<0.0008f && _loadQueue.TryDequeue(out var req))
    {
        if(req.state==ReqState.Downloaded)
            JobManager.ScheduleUpload(req);  //子线程memcpy+CopyTexture
    }
    
    子线程用UnityWebRequest下载,完成后把DownloadHandler.dataUnsafeUtility.Malloc拷到NativeArray<ushort>,再建Texture2D.R16调用CopyTextureAtlasTexture的对应区域,耗时<0.1ms
  3. LOD切换
    以玩家为中心画环形LOD0→LOD3,距离阈值80m/160m/320m,屏幕误差>2像素才加载下一级;卸载用LRU链表,显存>150MB时强制降一级LODReleaseTexture
  4. 裂缝消除
    相邻TileLOD差≤1,边界顶点用skirt(多出一圈退化三角形),Shader里**clip(v.vertex.z-0.001)**即可。
  5. 热更
    地形Tile打可寻址组ContentUpdatePath指到阿里云OSS,版本号走catalog.hash;玩家进游戏比对本地与远端catalog,差异Tile后台Wi-Fi自动下载,4G弹系统对话框

拓展思考

  1. GPU Driven管线
    如果项目目标骁龙8Gen2+,可把所有Tile高度图合并成2D TextureArray,用ComputeShaderGPU Culling+Indirect Draw,CPU侧零开销,但需Vulkan/Metal支持,**Unity2022.3+**才稳定。
  2. 物理碰撞流式
    高度图流式后,PhysX TerrainCollider也要跟着换,可用Unity.PhysicsHeightField子线程重建,耗时<3ms;但Android 32位机型PhysX版本<4.6128层HeightField上限,需拆成多Collider
  3. 法线与材质同步
    高度图Tile加载完,法线图可在ComputeShaderSobel算子实时生成,0.05ms/Tile;但SplatMap(地表贴图)体积更大,可BC7压缩+稀疏存储,只存非零权重块内存再降70%