Unity 2019 新特性在次時代手游《黑暗之潮》中的應用經驗及技術分享
林若峰 技術專家 ILRuntime作者 掌趣科技
-
ILRuntime
C#熱更解決方案
-
已在大量商業項目中得到驗證
龍族世界、境·界-靈壓對決、初音未來 夢幻歌姬、真紅之刃
-
《黑暗之潮》項目所面臨的挑戰
- 基于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高