Unity 2019 新特性在次时代手游《黑暗之潮》中的应用经验及技术分享
林若峰 技术专家 ILRuntime作者 掌趣科技
-
ILRuntime
C#热更解决方案
-
已在大量商业项目中得到验证
龙族世界、境·界-灵压对决、初音未来 梦幻歌姬、真红之刃
Github: https://github.com/Ourpalm/ILRuntime
-
《黑暗之潮》项目所面临的挑战
- 基于PBR的次世代画面表现
- 尽可能广的适配机型范围
- 高强度的战斗
- 复杂的战斗机制
- 工作流的简化
-
渲染管线的选择和定制
-
选择URP的理由
-
适合移动平台的PBR渲染管线
- 非pbr渲染也可以
-
非浸入式修改即可实现管线自定义
- 不修改urp源码的情况下,可以进行比较多的定制
有全部c#源码,渲染过程基本能全掌控
源码结构清晰,组织合理,扩展和自定义容易
比Builtin管线性能更好
-
-
为什么需要自定义渲染管线
- 每个项目都有各自独特需求,需要对渲染管线定制
- 透明物体容易出现渲染错误
- 在Builtin管线中只能通过修改不稳定的Renderqueue来规避
- 在引入新的Shader后,容易再次造成渲染错误
- 在Builtin中一些效果只能通过Shader Pass实现,打断合批
- Builtin管线为了兼容性,会在渲染中添加Blit操作且无法关闭
- 全屏Blit操作对于移动平台来说开销较大
- 在确定的渲染管线中,可以明确知道Blit是否必须,不少情况下可以省去
- 需要相对容易的实现一些项目特有的效果
-
URP的渲染管线
-
Mainlight Shadowmap -> Additional Light Shadowmap -> Depth Prepass -> RenderOpaque -> RenderSkybox -> Copy Color -> Render Transparent -> Post Processing -> Render UI -> Final Blit
主光源-方向光
Depth Prepass在urp中并非原本作用
-
RenderObject的活用
URP内置的一个自定义RenderPass的工具
无需添加和编写一行代码
可以明确指定某一个Layer在何时渲染
通过RenderFeature界面可排序
可以重载摄像机属性和深度等渲染状态
RenderObject在《黑暗之潮》中的运用
- 明确确定地表透贴物体的渲染时机
- 辅助其他自定义RenderPass
- 实现可对透明物体生效的单Pass ColorTexture
-
自定义RenderFeature/RenderPass
可以被插入到任意指定时间点执行的自定义渲染操作
拥有更强的控制能力
可以手动调用CommandBuffer底层接口
-
可以控制切换RT时RenderBuffer的LoadStore操作
Tile-based rendering,片上内存
可以告诉gpu,不需要把rt上的内存加载到片上内存,无带宽开销
可以告诉gpu,渲染结果不需要写回rt内容
平面阴影
游戏中大多数地形均为平地
阴影质量高
无需额外渲染Shadowmap
-
用RenderFeature可非常容易实现
添加一个RenderFeature,把需要有阴影的角色用一个特殊的shader绘制一遍
沙盘地图地块描边
不能传统法线外扩
先用纯色渲染地形形状
降采样后在低分辨率情况下使用BoxFilter
最终再升采样实现模糊效果
使用透明色再一次渲染地块形状
-
自定义Renderer
- URP内置了Forward和2D两个Render
- 最新版urp集成了Deferred Renderer
- 可以自行通过添加Renderer类实现扩展
- 可以直接使用URP已经实现的各种Pass,自行进行编排
- 《黑暗之潮》在ForwardRenderer基础上进行了自定义
- 后效中不可避免会进行一次全屏Blit操作
- 默认情况会在渲染UI后,使用FinalBlit Pass,将结果复制到FrameBuffer
- 可以利用后效的Blit操作直接将结果复制到FrameBuffer,并直接在FrameBuffer上进行UI绘制,省一次Blit基础上还能实现3D场景与UI使用不同的分辨率渲染
- URP内置了Forward和2D两个Render
-
《黑暗之潮》最终的渲染管线
Mainlight Shadowmap -> Additional Light Shadowmap -> RenderOpaque -> Render ECS Skin Mesh -> RenderSkyBox -> Copy Depth -> Render Floor Transparent -> Render Planar Shadow -> Render ECS Shadow -> Worldmap outline -> Render Transparent -> Copy Color -> Render Refraction -> Post Processing -> Render UI
- Copy Depth,把不透明物体的深度给复制到一张单独的rt,不是每次渲染都有,只有开启沙盘地图用,渲染水体需要深度图
- Copy Color,rt降分辨率操作,抓取1/4屏幕分辨率颜色信息,给扭曲效果等使用,这些效果对分辨率要求不高
-
-
URP性能优势
单Pass实时光照
-
单Pass ColorTexture替代GrabPass
- 空气扰动效果,GrabPass无法预知当前渲染屏幕会被全屏抓取几次
- 单Pass一次抓取
切换rt可自定义loadstore操作,节省带宽
可根据实际情况去掉不必要的Blit操作
-
SRP Batcher!!
- Dynamic Batching要求苛刻且CPU开销大
- Static Batching对动态物体无效,且内存占用巨大,且对LOD不友好
- Instancing仅对Mesh和Material均一致的情况生效
- 上述三种合批方式对于次时代游戏抓襟见肘
- Draw Call开销最大的是其中的SetPassCall
- SRP Batcher原理通过降低SetPassCall的数量来达到性能提升
- 通过ConstantBuffer保存PerMaterial/PerDraw数据,实现Shader变种级别的合批
开启SRP Batcher后,相同Shader的一次Drawcall只需要传输一个CBuffer的内容和绑定所需贴图,即可进行绘制
为开启SRP Batcher的情况下,一次Drawcall必须完整设置所有渲染状态
- RenderDoc抓取一次Drawcall的渲染流程
测试 骁龙450 soc
- 3盏动态光源
- 场景约40w三角面,500dc
- 中配简化至32w三角面400dc
- 低配简化至25w三角面280dc
- 三档机型在实际表现中,在dc提交效率上均体现出较大幅度提升
开启SRP Batcher后,从Profiler可以看出,主线程和渲染线程的耗时都比不开启SRP Batcher时有显著提升,对于越复杂的场景,效果越明显,大幅度提升能承载的Drawcall数量
- 开启SRP Batcher 主线程 render camera 4.3ms. 渲染线程 render camera 14ms
- 关闭SRP Batcher 相同场景 7.8ms. 22ms
-
-
DOTS技术栈在商业项目中的运用
-
关于DOTS的常见误解
- 项目中没有用到多线程,不需要DOTS
- DOTS主要用于大规模集群模拟
- 不会ECS,项目转成ECS代价太大,用不了DOTS
-
关于DOTS
- Data-Oriented Technology Stack
- DOTS分为三个组件:ECS,JobSystem,Burst
- 三个组件可互相独立使用,并非必须捆绑使用
- JobSystem无需配合ECS使用,各种需要并行计算的需求都可以使用
- Burst同样无需配合ECS使用,也并不需要跟并行计算捆绑使用,计算密集的同步方法也可使用
- 使用ECS不代表这个项目必须全部用ECS来写,可根据项目需求将ECS和传统OOP组合使用
-
使用ECS渲染大量怪物
- 一组怪物通常有几名精英配合1、2种大量存在的爪牙
- SkinMeshRenderer无法合批,且动画更新开销较高
- 基本上画面上有多少个怪就有多少dc
- GameObject.Instantiate开销较大,瞬间刷一批怪只能依靠分帧
利用ECS制作了一套基于GPU蒙皮的Instancing渲染系统
事先将角色动作烘焙到一张贴图上,然后在VS中进行蒙皮操作
利用JobSystem + Burst实现视锥剔除和动画系统更新
传统OOP游戏逻辑控制ECS的Entity,ECS部分仅提供渲染和动作接口,各取所长
Drawcall数直接下降到怪物种类的数量
一帧实例化上千个怪物,在低端机上耗时也不足1ms
借由Burst的力量,上千个怪物的视锥剔除和动画更新部分的耗时在低端机上都可忽略不计
至此,能流畅支持多少个怪物完全取决于GPU本身的渲染性能,cpu端耗时忽略不计,不会出现卡顿
-
使用JobSystem实现怪物击飞
- 由于怪物数量多,可能会出现在一帧中同时击飞大量怪物的情况
- 直接使用Unity的Ragdoll会对低端机造成大量负担
- 简化成播放预制作动作,主要关注飞行轨迹的方案
- 需要能跟场景产生正确的交互效果
通过Job并行计算所有单位的飞行轨迹和动作
使用Unity提供的多线程Raycast方法进行射线检测
非ECS对象最后再通过一个单独的Job同步GameObject的结果位置
-
使用Burst加速射线技能特效的计算
射线技能需要同时跟场景和其他单位进行碰撞运算
不光玩家控制的角色,其他怪物也可能会用,因此可能会出现同时有很多射线的情况
角色施展射线时可随时变更方向
需要每帧都重新计算射线所能碰到的位置
- 需要将射线检测的CPU占用最小化
- Burst非常善于处理计算密集型的需求
- 新的数学库语法和数据类型都趋近于Shader,写起来特别方便
- 经过Burst编译后,相关计算性能可以成百倍的提升
- 使用Job.run接口可实现同步调用
Burst效果对比
- 在另外一个计算体素化模型的工具中测试,有无Burst总计算时间的差距在上百倍以上
- 214ms, 20s 每个线程都快了百倍
-
-
工作流的简化和改善
-
简化角色Prefab的制作
- 以往需要美术同学负责将新角色资源导入Unity,并按照规范创建材质球和Prefab
- 采用PBR流程后,创建材质球和Prefab的复杂程度大幅上升,尤其是ECS单位,动画还需要额外烘焙
- 大量重复且复杂的手动操作非常耗时且容易出错
AssetGraph是一个节点式自动化资源导入流程工具
通过自定义节点可以完全根据项目需求定制资源导入流程
整个复杂的Prefab创建过程均可一键完成
美术只需要将FBX和贴图文件按照要求放入指定目录即可
-
场景导出流程的优化
- 根据不同的情况,会需要设置正确的渲染选项,以达到最佳的渲染性能
- 具体的设置策略会根据技术团队的Profiling评估,进行细致调整,调整过程不英造成美术反复工作量
- 为了提升切换场景加载速度,需要对场景进行切块
Check LOD Static Flags -> Strip Temporary Objects -> Fix Mesh Colider Read/Write -> Strip Lod simplifier -> Configure ShadowMask -> Fix Instancing Settings -> Clustering -> Make Static Batch -> Convert TO ECS -> Calculate Bounding Volume
- urp现没有ShadowMask
- 场景导出完毕,整个场景空场景状态,只剩下簇的节点,摄像机进入范围时动态加载
-
-
Q & A
Frame Debugger
-
如何告诉gpu不进行loadstore
CommandBuffer.SetRenderTarget
Parameters
- loadAction Load action that is used for color and depth/stencil buffers.
- storeAction Store action that is used for color and depth/stencil buffers.
-
平面阴影渲染方式
顶点着色器 vertex shader,通过灯光的投影算出变换矩阵,拍平到地面上,直接绘制即可
-
《黑暗之潮》是否用了il
主要是业务逻辑编写用il,战斗部分用c#原生
RenderDoc
-
dots是否可以在2d游戏使用
肯定没问题,因为有几问题
JobSystem#并行运行#2d程序化生成一种地图
比如ai的计算,如果计算量比较大,可以把一部分用burst加速,对计算密集操作加速,和多线程无关
-
遮挡剔除
- 黑潮项目没用,unity默认提供的遮挡剔除是需要静态烘焙的,cpu开销不小,对于项目性价比不高
-
使用ECS后,是不是不需要使用对象池技术了
- 使用对象池,主要是实例化开销比较大
- ECS的实例化非常快,因为实例化过程只是内存复制,没有任何逻辑,非常快
-
关于烘焙动画,是否使用了插件
- 自己写的,unity开源了一个关于如何烘焙gpu用的动画贴图工程
-
游戏场景的切块和加载
- 切块,分簇算法,计算哪些物体和哪些物体是一个簇,最早是九宫格切分,场景物件不是均匀分布
- 自动分簇,把比较靠近的物体归为一类,一簇所有物体拆出来,对合并操作,存成单独prefab
- 计算prefab的Bounding Volume
- 运行时根据Bounding Volume,计算块是否能被摄像机看见,然后动态加载prefab
- 切块,分簇算法,计算哪些物体和哪些物体是一个簇,最早是九宫格切分,场景物件不是均匀分布
-
Static Batching选择
大幅增加内存开销,选择面比较小,且同一个物体重复次数不会特别多,如果再多就可能用Instancing了
根据具体情况选择不同的合批策略
最通用的是直接用srp batcher,无需额外设置
-
动画是否可以考虑用Playable制作
考虑,动作状态机只有locomotion和blend-tree,所有技能动作是通过Playable播放
-
ecs如何实现复杂ai
跟ai实现有关,是具体逻辑部分
用ecs实现类似是合适的,习惯面向数据的编程方式,需要思维转换
-
gpu切换渲染状态主要开销
就是调用api的耗时,等待
-
pbr shader
urp自带,Lit,Simple Lit
-
变体爆炸
变种收集,记录变种
IShaderPostProcess接口实现,打包过程执行脚本,告诉Unity哪些变种需要以及不需要,根据刚才记录的变种集进行剔除,只保留游戏中实际用到的变种,一个游戏最多2,300个
-
srp batcher是否在安卓上可以使用
- 每个平台都可以用,做过测试
-
il不兼容dots
dots用它是为了性能
热更是牺牲性能基础上热修复或者更新,两个目的相悖
强行结合无意义
-
urp实现moba游戏描边
- 最常见描边方式是在顶点着色器沿法线方向外扩,先渲这个,再把角色渲染一次
-
dots是否可商业化使用
- 三个,可选择其中一部分,burst和job system可以完全用于商业开发
- ecs底层框架比较稳定,周边设施缺乏,比如缺乏skinmesh渲染,自实现
- 不过在开发中,unity有一个基于dots的animation系统,处于早期,不适合商业项目
-
dots技术栈改造
可挑选项目比较费时的方法改造,比如技能的射线检测
C#中比较耗时,静态函数,工具方法非常适合用burst加速
JobSystem比较通用,需要多线程的事情都可以用
唯一考虑的是ECS,需要根据项目需求
-
JobSystem是否能用于asset bundle加载
不大能
现有接口是在主线程调用
现在本来的加载本来就是异步和多线程的了
-
urp的shader graph
比ase插件要弱 Amplify Shader Editor
好处和管线比较紧,扩展相对容易
当然大家如果习惯ase,可以代替shader graph
-
ecs和job system推荐资料
- unity官方github示例工程
- 自己上手测试和编写,和面向对象有很大区别
ecs可以在editor和打包中都可以使用
场景分块工具,自己写的,和ecs没有关系
-
burst代码不可能通过热修复
- 利用硬件指令运算加速,静态编译,无法热修复
- 用burst加速是通常是静态工具方法,很少变更,无热更需求
-
黑潮项目战斗
- 战斗部分目前是在c#中写的
- 战斗框架模块话,大部分是通过行为树和配置表控制,从底层逻辑角度来说,修改概率不大
- 结合热修复方式,在紧急情况下热修复,github有现成相关方案
-
dots是否可加速粒子
- Particle System,没有办法
- 最新VFX Graph用gpu加速,可以尝试一下
-
切换渲染状态
- 都是比较大,只是srp batcher,尽可能减少切换渲染状态,降低平均每个dc的开销
-
ecs配合mono脚本使用
- 通过mono脚本设置entity component的值达到传参给ecs对象,设置不能过于频繁,造成ecs 对象archetype重构
-
urp是否支持3s效果
- 目前不支持,是项目自己开发
- 不过urp roadmap会加,也会加比如Clear coat
-
il和lua热更优劣
il最大优势是c#,强类型语言在大型项目中优势还是比较大
比较有争议的是性能
如果说单纯论纯计算,加减乘除,没有api调用,那lua效率要高一些
-
但实际业务代码要调用很多unity api,在调用unity api的性能上,il比lua是要快的
综合下来,没有太大区别
该注意的还是要注意,在il比要在原生要注意,不可避免,热更在ios只能解译
-
constant buffer硬件要求
- opengl es 3,在国内无太大问题
-
urp性能
- 无论哪种情况,相对来说都应该更高,会比builtin高
- 检查一下渲染设置,urp默认情况下渲染设置比builtin高