项目介绍
《云境》是一款使用Unity引擎开发的WebGL产品,有展厅,剧本,Avatar换装,画展,语音聊天等功能,运行在微信小程序和PC,移动端网页,即开即用。
当前问题和现状
当前项目面临的问题和场景现状
- 首次场景包体大,首次下载时间长,用户首次进入到场景耗时较长。
- 项目场景本身偏展厅风格,非大世界,非大型游戏场景,云境的场景元素整体比较集中,当前项目场景多数多数是室内场景或者小场景,拆分出去的内容有限,美术人力资源有限,对场景做更细致的拆分处理和分块加载成本较高。
我们需要一套简单适用的方案,降低场景包体大小。为了优化场景资源包大小,我们首先需要分析资源包里面的不同类型的资源占比,看哪些资源可以做优化和拆分
场景资源包分析
AssetBundle使用LZ4压缩,下面是针对三个原始场景资源包的分析
公园场景park_001.bundle,8.63MB,Mesh(6.5MB)+Texture2D(5.7MB)
剧本场景scenes_bxzf.bundle,11.3MB,Mesh(8.9MB),Texture2D(8.0MB)
某个展馆scenes_jttd.bundle,5.3MB,Mesh(439.3KB),Texture2D(8.0MB)
通过针对多个场景的资源包分析,会看到Mesh+Texture2D是一个场景资源包占比最高的两种资源类型,Texture2D基本占比在50%以上,而且针对个别场景Texture2D的资源占比会超过80%
方案确定
根据场景资源包的分析,可以得出的结论,针对云境目前的场景特点,多数场景Texture纹理贴图占比在50%~80%,如果采用Texture的处理能够很大程度上降低场景资源包的大小,收益比较明显,同时TextureStreaming的实现,相对MeshStreaming相对简单,投入产出比高,TextureStreaming不会改变场景原有的结构,可以降低美术人员的参与成本。
第一期设计一套简单,易实现,工作流完善的TextureStreaming方案,降低场景资源包中Texture的占比。
Unity引擎自身基于MipMapLevel实现的TextureStreaming,在WebGL平台该功能无法使用 所以需要自己开发一套简化版本并和平台无关的TextureStreaming功能。和Unity的TextureStreaming方案相比,解决的问题有相似的地方,比如控制Texture在运行时的内存占用,将纹理内存维持在一个范围内。但是我们设计的TextureStreaming方案需要关注场景资源包大小同时能够在运行时降低Texture内存的占用。
什么是TextureStreaming
定义的轻量级TextureStreaming功能:将使用高清纹理的场景,离线处理成极低分辨率的场景叫做Lod Scene,在运行时加载Lod Scene,根据规则,从外部加载相应的高清纹理,对低分辨率的纹理做替换。原理简单容易理解。
整体设计目标-TextureStreaming
核心目标:实现简单,工作流程清晰,全自动化
方案需要达成的目标
- 非侵入式,做材质和纹理贴图的全量拷贝,降低复杂度和资源引用维护成本
- 不改变原场景
- 不修改原场景引用的材质,避免造成材质混乱,错误修改
- 不修改原场景引用的纹理贴图
- 不修改打包规则和之前的加载逻辑
- 保留原始场景光照贴图和渲染设置,忽略对光照贴图的Lod处理
- 一键式处理,通过EditorSceneDescriptionConfig文件控制生成的Lod Scene内容,避免人工处理,节省人力
- EditorSceneDescriptionConfig需要支持的动态配置
- 控制单个贴图LOD分辨率,提供x16,x32,x64,x128和原始贴图尺寸
- Editor离线和运行时配置分开,运行时配置数据结构简单
- Texture AssetBundle包大小,根据配置自动分割AB包
- 最小化更新,Texture AssetBundle打包更新不会造成,整个Lod Scene也被更新
- 需要HD Texture AssetBundle和Lod Scene AssetBundle无依赖关系
- 运行时,支持Texture Streaming功能分场景动态开关,加载HD Scene还是Lod Scene
- 运行时,支持Streaming流式加载(分Grid,分帧加载TextureCell)
- 运行时,支持Texture Bundle分帧请求加载,降低可能带来的卡顿
- 运行时,支持Material分帧修改,降低可能带来的卡顿(经过测试,暂时未发现该部分的耗时情况)
- 运行时,Grid直接选择九宫格方式,当前Grid索引+外部8个Grid的内容
- 运行时,使用最简单的加载规则,玩家在场景中的世界坐标作为整体输入参数
- 运行时,场景切换HD Texture加载和卸载正常
- 运行时,纹理切换不会卡顿
方案设计和实现
资源管理
目前使用Unity Addressable进行资源运行时管理和资源打包
Texture Streaming美术处理管线
完整的处理流程图如下:
场景处理管线具体介绍
创建SceneDescriptionConfig配置
采集场景资源:材质,纹理
获取场景中,所有引用的材质球和关联的纹理贴图,为后续生成LodScene做数据准备
忽略的部分:
- 光照贴图
- TMP Shader相关的材质球和纹理
拷贝Material,生成LodTexture,LodScene
生成LodScene
- 将依赖的材质拷贝一份,生成Lod Materials
- 将材质球依赖的所有Textures拷贝一份,生成HD Textures
- 根据设定的HD Texture 分辨率执行Texture缩放,生成Lod Textures
- 拷贝原始场景,生成Lod Scene
- Lod Materials引用的Texture更换为Lod Textures
- Lod Scene关联的Material替换为拷贝的Lod Materials
经过上述的处理,完成了Lod Scene的生成,保证对原始场景的资源无侵入处理,会带来一定的资源冗余比如Lod Textures,不过牺牲一小部分的内存,能够简化流程和实现方式,在当前项目目前是可以接受的
设置Grid参数,按照规则Grid分割场景
目前只支持规则的Grid切分,Grid的可调整的参数如下:
- Grids起始位置
- Grids,行列大小
- 每个Grid大小的大小
获取每个Grid包含的Renders
遍历场景中的所有Renders,确定所在的Grid,确定每个Grid依赖的Renders,通过Renders确定当前Grid依赖的Lod Materials,根据Lod Materials确定当前Grid需要加载的HD Textures
创建运行时Config,建立Grid,材质和贴图的映射关系配置
建立Grid-HD Texture-Materials之间的映射关联配置
运行时配置文件
Grid配置,一个Grid关联到多个Texture Cells
Texture Cells配置
- 资源Addressable Key(GUID),用于Addressable通过Key执行加载
- 对应的Lod texture引用
- 当前HD Texture关联的材质球和纹理槽位,如果当前HD Texture加载完毕,更新关联的Material对应的纹理槽位即可
自动化资源分组
Lod Scene:Lod场景
- Lod Scene Material + Runtime Config:Lod场景引用的材质球和运行时配置
- Lod Scene HD Textures:Lod场景引用的HD Textures,按照Texture单个包体指定的大小,自动生成对应的Label做分包处理,Group使用Pack Together By Label模式打包
最终管线输出产物
自动化生成的工程产物
- HDTextures:存放拷贝出来的原始纹理资源
- LodTextures:存放处理过后的低分辨率的纹理资源
- Mats:存放Lod Scenes引用的材质球资源
- outdoors_001_gen_lod.scene:Lod 场景文件
- outdoors_001_gen_lod_runtime.asset:TextureStreaming运行时配置文件
Addressable资源打包分组
Lod Scene-单独Group分组
- Lod Materials+Runtime Config-单独的Group分组
- Lod Textures-同一个Group分组,但是打包的采用Packed Together By Label方式打包成多个AssetBundle包
运行时逻辑
运行时的逻辑比较简单直接
接收角色位置输入->确定Gird->确定关联的TextureCells->使用Key加载->加载完毕,更新TextureCell关联的Materials
最终实现Lod Texture到HD Texture的动态更新
为了防止Grids关联的TextureCells过多,导致请求数量多,加载多,做了简单的“分帧”处理
- 玩家位置设定更新间隔,一定间隔检查Grid是否变换,有变换执行Grid管理资源加载
- 增加最大加载TextureCells数量限制,超过当前加载的数量,放入到等待队列中
- 等待队列检测增加轮询间隔
- HD Textures加载完毕放入队列,增加间隔去设置SetTexture,经过测试SetTexture并不会造成卡顿,所以取消对SetTexture的分帧处理
场景切换之后需要调用Addressables.Release(tc.Value);释放Texture2D资源
TextureStreaming方案收益
场景资源包大小在使用TextureStreaming之后的大小优化对比,目前已有的18个场景,场景包体大小都控制在5.5MB以下,首次加载时间优化,按照4G网络情况理论测算缩短3~5s,贴图资源量越大,包体减小越大,收益会比较明显
基础场景实现效果
streaming展示
总结和展望
本文介绍了适用于WebGL平台的场景包体大小优化的TextureStreaming方案,相对大世界流式加载复杂完善的方案,当前的TextureStreaming方案在一定程度上解决项目的痛点,优化了WebGL应用的场景包体大小,缩短下载时间,但是该方案还有很多需要继续细化和完善的地方,比如Grid粒度“粗糙”,加载逻辑更细致的控制,后续也会持续改进。
TextureStreaming后续完善的方向
- 场景处理管线,细化分割粒度和规则
- 运行时,细化加载规则,更精准的优先级,支持更多参数的输入视角,朝向
- 运行时,根据规则预加载Grid,降低延迟
- 运行时,支持动态卸载Grid,降低内存占用
- 打包资源,更精确化的Texture分包,目前采用Editor模式获取纹理大小,某些情况下获取的纹理大小和实际打包压缩之后的AB包大小差别比较大,导致Texture,AB分包并不均匀
- 针对LOD的纹理图片可以直接关闭MipMap,更快的加载速度和更小的内存,开启MipMap会带来33%的内存增大
场景资源包优化其他思路
- 支持Mesh Streaming
- 支持场景物件的拆分,将物件拆分出去,降低包体大小和场景对象数量
- LightMap的处理,进一步减少Texture2D的占用
TextureStreaming方案从问题分析,方案设计,到工具开发,运行时实现,整体比较简单易理解。
只有适合项目并解决项目痛点的方案才是合理的方案。当前的方案还有许多可以继续优化和扩展的地方,后续会持续优化。
希望能够提供一些思路和帮助,欢迎交流和指正~