天天看點

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

作者|王哲

編輯|孫瑞瑞

你好,我是 Cocos 引擎的創始人王哲。跟看到這篇文章的你一樣,我也是一位程式員,雖然現在的主要工作更偏向于經營管理,但是程式員的底子已經刻到基因裡了,是以在此想跟你分享一下 Cocos 引擎的技術架構與相關實踐,希望能給你帶來一些新的認識。

重新認識 Cocos

去年,我在 GMTC 深圳演講開場時,對現場的聽衆做了一個小小的調研,我問他們中有多少人用過或者聽過 Cocos,當時有很多人舉手了,我當時十分開心。

但是大家對于 Cocos 的認識停留在什麼階段還是存在差别的。GMTC 演講那天,我邊上坐了兩位來自阿裡的哥們兒,寒暄之間我介紹自己是 Cocos 的,對方立刻反應過來了,同時說了《捕魚達人》《歡樂鬥地主》《開心消消樂》等多款用 Cocos 開發的遊戲。我當時便調侃道,哥們你的資訊有點“過季”了。

這幾年我們做了很多事情,下面和大家一一道來。

目前市場上近乎所有的休閑、卡牌遊戲,絕大部分的傳奇類遊戲,以及約 64% 的小遊戲都是用 Cocos 開發的。是以在多數人印象中,Cocos 能做的遊戲大約可以概括為下圖所示的幾種:

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

事實上,基于 Cocos 引擎開發的遊戲類型遠不止如此,還包括以《列王的紛争》和《亂世王者》為代表的 SLG 類,以《熱血傳奇系列》為代表的 RPG 類,以《動物餐廳》為代表的模拟經營類等遊戲。2021 年,Cocos 還有一個更大的突破,就是合并了 2D 和 3D 兩條産品線,推出了“ Cocos Creator 3.X ”版本,實作了在同一個編輯器既可以開發 2D 内容又能夠開發 3D 内容的能力。

那這裡的 3D 内容是前文所述的休閑 3D 嗎?早已不是,我們今天的 3D 内容是這樣的:

(賽博朋克)

對于上面這個 Demo,場景裡有幾百個動态光源,行業内的朋友認為可以達到 3A 級效果。而下面這個 Demo 則展示了在 Cocos Creator 中使用延遲渲染管線實作的環境光、烘焙等效果,而且可以在海思的 GPU 上跑到滿幀。

(海思 CGKIT 光球)

Cocos 引擎架構的疊代演進

從支援簡單休閑的 2D 和 2.5D ,進化到如今的 3D 引擎,Cocos 的架構曆經了多次疊代和演進,在這個過程中,我們有哪些思考?踩過哪些坑?以及 Cocos 和 Unity、Unreal 有哪些差別,我們為什麼這麼做?接下來,我将結合 Cocos 架構的演進曆程,把這些心得分享給大家。

首先,我們先來看一下 Cocos 的早年架構是什麼樣子的。

Cocos 早年架構

早幾年我還在自己寫代碼的時候,Cocos 引擎的架構是像下圖這樣的:

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

在上面的架構圖中可以看到,圖形渲染這塊,我們每個功能就是一個 Node,Node 中有兩個最核心的函數:update 和 render。update 會将你的位置、形變等全部計算清楚,而 render 則把它畫出來。

這就像兩個循環,第一個先根據場景數把所有東西全部 update,比如說實體碰撞後産生的幾個碎片,分别飛到什麼位置,把這些全部計算清楚了。第二個就是畫,它就是一個渲染技術,渲染節點畫出來。

再往下,這個就是用 OpenGL 了,OpenGL ES,WebGL,就這一套,其他的渲染标準我不管,因為在以前也沒有這些,是以我們隻要把 OpenGL 搞定就行了,然後在各個平台全部跑通。

但是這個架構到了 3D 遊戲的時代,它的支撐力就開始比較差了,因為 2D 遊戲隻要渲染一遍就夠了,但是 3D 遊戲的粒子系統、動畫系統、光照和陰影等等都是獨立計算又互相影響的,這個時候沒辦法再簡單地通過不同節點來組裝,渲染器也不能簡單地通過一個個順序的渲染函數組織,這個時候我們就需要一個新的架構來支撐。

Cocos 目前架構

經過我們多次的疊代演進,Cocos 目前架構如下圖所展示:

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

這裡是以渲染器架構為主,從上面的架構圖中可以看到,從渲染場景(Render Scene)開始,會有模型(Model)、光照(Light)、天空盒(Skybox)等,然後要做場景裁剪(Scene Culling)等等。

場景剪裁完成後會生成一系列的渲染對象,進入 Renderer 開始基于 Camera、材質(Material)、Pass 的優先級來組織渲染隊列。

接下來,開始有渲染管線(Render Pipeline)。目前主流的渲染管線以 Forward Rendering/Deferred Rendering 為主,Cocos 和大多數引擎一樣,支援這兩種渲染管線。在此,特别提一下移動端的渲染管線,自從《原神》遊戲火爆了之後,移動端也開始流行做延遲渲染。這個時候 Cocos 想到了 Frame Graph,在複雜的渲染管線下,存在各種不同需求的渲染 Pass 和流程,那麼 Frame Graph 的價值就是把這些流程變成像拼樂高一樣,把這些小零件組裝起來,而且在未來還可以将這種友善的定制能力開放給開發者。

渲染器的最後一層就是 GFX 裝置層。其實 GPU 的架構大同小異,但是不同作業系統和不同環境下使用的圖形 API 不同,我們的 GFX 的做法是用統一的接口将所有圖形 API 封裝起來,無論底層是 OpenGL、Vulkan、Metal,還是 WebGPU,我們逐一接入封裝清楚後,引擎就能直接使用統一的 GFX API 了。

mainloop 時序

接下來是 mainloop 時序。

你可以先思考一下,為什麼目前應用引擎有很多,而遊戲引擎在全球能打的就剩 Unreal、Unity、Cocos 三家了?在我看來,是因為遊戲引擎和應用渲染本質上是不一樣的,應用渲染它可以描述為“敵不動我不動”,因為隻要你沒有輸入或者沒有通知響應,應用渲染是不會去重繪界面的,可以說是怎麼省電怎麼來。

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

遊戲引擎可不是這樣,遊戲引擎可以了解為和視訊播放器一樣,是一個不斷循環的過程。一般電影的幀率是 1 秒 24 幀就可以了,因為電影再高的重新整理率理論上人眼就無法分辨了。但遊戲與電影不同,遊戲因為都是逐個靜幀沒有視覺殘留可以過度,是以遊戲裡 30 幀的效果很勉強,後來人們覺得 30 幀也卡得不行,要求就變得更高了,變成 60 幀,現在還有高刷屏,有 90 幀、120 幀。

遊戲引擎就是不管玩家動不動,畫面一定處于一種運動的狀态中。你可以注意一下所有的遊戲畫面,當你靜止的時候,畫面絕對不會一點都不更新,策劃不會留出這種空間出來的,不然你會覺得這個遊戲卡死了,對吧?是以當你玩家人物靜止的時候,正常的畫面裡還會有人物的呼吸起伏,會有風吹草動,然後會有飛鳥過境等等這些。

在 2D 引擎時代,大家做 Batch 就很開心了,但在 3D 引擎時代多了 Camera 的概念,這裡如何了解?可能有個主要的 Camera,但場景中還可能有個後視鏡,還可能有一灘水,也可能有其他更複雜的情況。比如說一個人是一個 Model ,而你的這個人物身體可能由多個不同材質的 subModel 組成,然後每個 subModel 的材質可能包含多個渲染 Pass,他可能要畫好多次,比如物體表面光照要畫一次,陰影要畫一次,如果有鏡面有投影這些的話還要再畫。這些複雜 Pass 的組織就是在 Frame Graph 裡面去處理的,處理完以後就變成大量的渲染指令丢給 RenderQueue。

好,到這裡就算渲染期結束了,接下來就是 GFX 的活了,讓它直接去跟硬體互動就行。

移動優先:TBR 實踐

Cocos 與競争對手不太一樣的地方是:有些引擎是在 PC 時代就已經有了,比如第一代 Unreal 是在 1998 年始發。而 Cocos 則是移動端優先,即優先在移動端上運作,移動端中引擎的 GPU 架構跟 PC 端是完全不同的。是以,無論是 PC 端優先還是移動端優先,亦或是兩者兼顧,都會影響 Cocos 的整個架構設計。

舉一個典型的例子,在 PC 端,它的渲染方式叫 Immediate Mode Rendering(IMR),它是如何立刻執行?隻要渲染指令一下到 GPU 裡面,它“啪”一條線就渲染出來,馬上畫出來了,它的 Buffer 在哪裡?在顯存裡面。如果這時候出現問題了,比如說顯存 64G 不夠怎麼辦?不夠就加顯存,128G,256G,加完顯存以後,GPU 再跟顯存的互動就得加帶寬,帶寬加上去還不夠?加電壓,加電壓不就快了嗎?那這個功耗一上去,發熱量提高怎麼辦?沒關系,加風扇!就是“老夫一把梭,一條幹到底”,有問題解決問題,反正 PC 端就是你愛幹啥愛加什麼裝備都可以。

但是移動端不一樣,移動端我們是沒法加風扇的,而且最慘的是它沒有顯存這個概念,一塊晶片 SoC 裡面 GPU 得跟 CPU 去共享記憶體,這時候渲染跟記憶體的讀寫,IO 就已經卡在你這邊了,那更不用說性能的提升了。

上帝說要有光,于是我們針對移動端做了一套渲染的解決方案。

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

上圖是傳統延遲管線在移動端 Tiled Based GPU 上的簡要渲染流程。在流程中可以看到,有 GBuffer 和 Tiled Memory,這一般就是 32KB、64KB,如何處理?其實和易旭昕老師講的方法類似,就是分區,分而治之。

這是在程式設計裡是很常見的問題,一個東西太大了,要塞到外面的記憶體去,讀寫、IO 特别慢。沒關系,我們将它拆成一個個小格子。Tiled Memory 有 32×32,也有 64×64,現在比較新的是 64×64,可以把整個畫面拆成小小的。

這裡需要注意的是,如果要優化好移動端的功耗,就要避免把 GBuffer 存儲到 System memory 中,直接在 TIled Memory 中處理完以後不要往外讀了,每個 Tile 存完 GBuffer 以後直接做光照,這樣就可以省掉 Gbuffer 的 IO 開銷,做完光照計算,結束。這時候因為它很小,不像 PC 的顯存,你可以愛用多少用多少,在移動端就感覺像玩十字繡一樣,你得很小心地去用它。

是以在整個的渲染流程中,你需要提前規劃好每一步緩存。如果存完之後又想重新畫一個東西,那麼就浪費了這部分緩存,浪費以後又開始在手機上面頻繁地讀寫系統的記憶體,那之前所有功夫就全部白廢了。

這是移動端跟 PC 端的一個非常本質的差別,Cocos 是移動端優先,是以我們的整體架構就是符合這種 best practice。當然大家在使用引擎的時候是無需關心這些内容的,一般現代引擎都會把這些東西都封裝好供你使用。這裡提一句,上文所述的這些在開源倉庫裡都可以拿到,因為 Cocos 是開源的。

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

多後端的 GFX

前文提到,GFX 可以把所有不同的地方全部封裝出來,這部分 Cocos 也是開源的,是 MIT license,是以大家可以商用。是以如果你們要做圖形渲染的話,就算代碼看不懂也沒關系,可以整個直接拿去使用。比如說,後面 WebGPU 出來,或者 Metal 的新版本、Vulkan 的新版本出來,直接再往裡面加就可以了。這樣一來,就不用再去管那些晶片廠商有什麼新技術,免得影響到你上面整個架構的運作。

均衡負載的多線程設計

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

接下來說一下多線程。我見過一些剛入門的程式員寫多線程都很開心,直接來一個 slightly poor、Job System,各種線程滿天飛,也覺得自己特别酷。在我最早的時候是做硬體的,實際上隻有一個單獨的硬體的 IO,才值得你去開一個線程,否則的話,在 CPU 裡面切換線程的開銷,大于你多線程的收益,這并不劃算。

像 GPU、網絡、實體可能值得開一個線程,但也不是說實體全部值得去開,像最新的骁龍 888 CPU,也叫 1+3+4,即 1 個超大核加 3 個大核加 4 個小核,那也就 8 個核。8 個核你沒有必要開十幾個線程,是以這個多線程的結構圖,多線程裡面常見的就是這種生産者消費者模型,大家應該都玩過。

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

上圖中的 Render Thread,以及實體、網絡你也不用管。從渲染的線程,我處理完一幀,然後壓到 RenderCmd Queues 這個 Buffer 以後,丢給 GFX。這個地方就像一個用來錄制 GPU 的獨立裝置線程,上面是生産者,下面是消費者,這種模型比較普遍。

不過,我曾經見過一個遊戲公司寫的多線程,他們是沒有這種消費者的 wait,wait signal,類似這種寫法,他們是直接拿一個 while(1),然後讀一下有沒有,沒有的話是 false。好,我 sleep10 毫秒,然後接着再上去再讀一下,看沒有,我再 sleep10 毫秒,結果導緻整個功耗就非常高。

這件事讓我印象特别深刻,當時這個遊戲公司整個組有 20 個人通宵加班了差不多十來天,還沒找到什麼問題,覺得引擎為什麼發燙呢?一定是引擎性能有問題。然後實在解決不了就找到了我們,結果那行代碼他們付了我二三十萬,最後發現其實根本不是渲染問題,是在外面,他們在性能同步的地方出問題了。

FrameGraph 自定義渲染管線

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

FrameGraph 自定義渲染管線,實際上這個是寒霜引擎在 GDC 上分享的一個設計。

基本流程分為 Setup 初始化階段、Compile 編譯階段和 Execute 執行階段。Setup 是使用者指定的渲染流程描述,接着引擎每幀都會實時針對所有 Pass 建構渲染圖,梳理整個渲染流程,最後再去執行使用者的實際 Pass 回調。

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

這是我們引擎延遲渲染管線簡化後的流程,橙色方塊都是渲染 Pass,藍色方塊是 Compute Shader 計算 Pass,我們的計算 Pass 是基于視錐簇的光源剪裁算法,針對每個光源計算影響範圍,最後 Lighting Pass 階段隻考慮對物體可能有影響的光源,減少對光照的計算量。

在移動端上,我們将前向管線的動态光源限制為 4 個,超過 4 個以後會對性能有影響,因為每個光源在每個物體上都需要增加一個 Pass,複雜度是乘法關系。前面提到我可以跑上千個光源,雖然裡面也有視錐簇裁剪的貢獻,但很重要一點就是必須用延遲渲染,延遲渲染它實際上就是所有物體全部畫完以後,存在 GBuffer 裡面,最後在光照 Pass 中一次性計算所有光源的影響,是以增加光源變成是一個加法關系,而不是一個乘法關系,性能就會高非常多。

當然 GBuffer 占用的存儲空間較大,因為現在的手機記憶體會比較大一些,是以是在拿記憶體去換性能,但是也因為延遲渲染本身比較占記憶體,後面如果做後效做抗鋸齒用 MSAA 的算法,記憶體占用還要乘 4,這是一個問題。

第二個問題是它沒辦法去處理那種半透明的物體,是以如果遇到半透明物體的話,還是要按照 Forward 正常的渲染的流程,比如有個玻璃瓶、小彈珠等半透明的這些東西還要單獨再畫一輪,這是一個缺點。

是以就目前前端來講,我還沒有看到哪一個項目有瘋狂到要用這個 Deferred Rendering,因為感覺更多是炫技用,證明已經可以做到這個程度了,但實際上大家還是保守一點在用 Forward Rendering,不然記憶體真的很容易爆掉。

(延遲渲染管線 - 動态光源)

(上百個動态光源)

這裡提一下 Bloom,這個比較普遍,所有光照的後渲染效果,都需要稍微有點灰光的效果才不會顯得虛假,比如說下面這張圖的光都帶有 Bloom 的效果。

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

(加入後處理特效:Bloom)

還有抗鋸齒,因為用延遲渲染 MSAA 用不了,記憶體會爆掉,那我們就用 TAA 了,效果也還不錯的。上圖的是剔除的,正常的視錐體裁剪,它是按物體去裁剪,算物體的遮擋關系,但是物體奇形怪狀的,你也不能指望說前面就是一個特别大的物體,把後面小的全部擋住,是以這邊算的方式是跟前面的做法一樣,先把場景切成一個個小格,然後計算格子之間的遮擋關系,預存儲起來。運作時直接取遮擋關系,如果後面的格子被遮擋了,後面格子裡面的所有物體就不需要了,所有光源物體全部拿掉即可。

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

然後接着再去根據剔除完以後剩的那些格子,把這個格子裡面的物體的遮擋再進一步去計算。

(加入 PVS (Potential Visible Set Culling))

像上面左邊這張圖是還沒有加入 PVS 的情況,所有紅色東西都是要畫的,但如果是先做了一次裁剪的話,可以看到整個需要畫的東西就少非常多(如右圖),因為很多東西我們根本看不見,這時候整個 Drawcall 能夠降低 50% 左右。

前面分享的都是開源代碼,接下來,我想分享一些宏觀的觀點。

國内外的“流量底座”之分

同樣是引擎,Cocos 與 Unity、Unreal 有很大的差別,相比之下,另外兩家更側重原生,在 H5 和前端技術上似乎和他們沒有關系,而 Cocos 引擎為什麼要做這件事情呢?

這裡,我想分享一個概念叫“流量底座”,這是我自己造的詞。

國外的生态流量底座是 iOS 和 Andriod,是作業系統。這種流量底座有幾個明顯的特點:

一是對内容有巨大的需求,系統直接面對 C 端使用者,系統沒辦法全部做完所有内容,是以需要大量内容供應商開發出各種 App 形式,來滿足 C 端五花八門的需求。

二是禁止二級生态,比如 iOS 下的 App Store 就禁止開發者用下載下傳可執行的腳本,例如動态地下載下傳 H5、JavaScript 代碼、Lura 代碼到 App 裡面運作,因為隻要開放這樣做,開發者就可以做出各種遊戲盒子、遊戲大廳,進而産生二級生态。

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

再看國内,華為、小米、OPPO、vivo 等各類手機品牌都有各自的應用商店和生态,但國内真正的流量底座卻是超級 App,坐擁真正的流量入口。

為何這樣說呢?流量底座,其實就是有議價能力的談判權。在國内,手機使用者已經不喜歡去下載下傳一個新的 App 了,大家更習慣用手機掃碼完成健康卡查詢、點外賣、共享單車、支付等操作。是以,國内的這些超級 App 在流量底座之上,會建立自己的生态。比如說微信,微信為了滿足 C 端使用者五花八門的需求,推出了小程式、小遊戲,并在此上建立生态。

值得注意的是,App 用 Natice 技術開發的情況下,其上部的生态技術隻能選擇前端技術,是以,國内的前端技術發展相比國外,是更加活躍的。

但前端技術并非隻是為了降低跨平台開發的成本,在之前 GMTC 演講的現場,有位來自位元組的老師也提到兩點原因,一是降低開發成本,二是降低管道更新成本。其實,隻有小廠才有跨平台的需求,大廠有足夠的能力可以直接分為兩個不同的項目組進行,以保證原生版本有更好的體驗。但核心是,除了大廠自身的選擇之外,還要考慮小程式、小遊戲的生态。

這個差異導緻了我們的設計,還記得本文開頭的 Cocos 引擎的架構圖嗎?其實已經很複雜了,已經寫了幾百萬行代碼。

國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC
國産引擎 Cocos 的跨平台渲染器架構與實踐|GMTC

實際上,我們的架構是上圖這樣的,這部分是前端會用到的,我們用 TypeScript 去寫,整個是落在 Web 平台上面,非常适用于各種小遊戲、手機廠商的快遊戲、抖音快手小遊戲、Web H5 等等,現在已經有成千上萬的開發者在用 Cocos 開發适用自己平台的産品。說句題外話,這也衍生出了很多市場的崗位需求,很多知名廠商比如愛奇藝、美團、騰訊等,我之前還看到 QQ 音樂有個進階職位在找 Cocos 人才。

另外一方面,Cocos 同樣服務于原生手遊,許多手遊要上硬核廠商管道,也要出海,而海外則沒有 H5 遊戲生态。這時候怎麼辦?我用 C++ 實作一遍,是以這兩塊引擎是有兩組人在寫的。這裡可能會有問發問,那你為什麼不用 WebAssembly 直接編譯過來?我隻能告訴你 WebAssembly 是有坑的,這個事情我們有實踐經驗,如果我把這一塊東西用 WebAssembly、C++ 直接編譯到 TypeScript,跑在浏覽器上可不可以?是可以的。

但問題在于什麼?前文提及,大家還是要熱更的,不能夠動态更新的前端是沒有靈魂的。我們的遊戲都是有靈魂的,是以它這上面用動态腳本去寫,但是動态腳本如果去調 WebAssembly 這坨東西,開銷非常大,無論是從 JavaScript 也好,TypeScript 也好,調用 WebAssembly 編譯出來的這套東西,消耗都是非常大的,最後性能其實還不如直接再寫一遍。是以我們為了讓大家提高性能,這麼多的東西真的就寫兩份了,這個是目前我們的實際情況。

在這裡,其實我隻是想說的是,輪子是真的不好造,我們做 Cocos 已經今年第十一年了,我覺得我們算世界第三吧。

最後跟大家分享兩個開源倉庫,第一個倉庫是我們 H5 這一塊的引擎,純前端的代碼全部開源了;第二個 native-engine 是跑在 iOS、安卓上面,還有 windows 和 macOS 上面的原生引擎,也是開源了,都是 MIT license。大家要用的話,不管你要用 Rendering 這層,還是你要用底下 GFX 這一層,直接拿去用,就不要自己造輪子了,有問題可以到“Cocos.com”的論壇上面問我們,我也經常在上面回答問題。

在遊戲行業之外,Cocos 也已經進入了更多的領域。比如為教育行業推出的互動課件編輯器 Cocos ICE、為 IoT 裝置以及更多螢幕推出的 HMI 人機互動界面,并在 XR、AR、汽車駕駛導航、兒童程式設計、手表甚至虛拟角色等多個領域都有了方案積累。

也有很多人問我們是不是元宇宙,我的回答是:Cocos 不是元宇宙,但 Cocos 是用來生産元宇宙的工具。不管元宇宙的發展是怎樣,總是要有内容,要有實時互動,那麼 Cocos 就專門做這個的。

本文由 InfoQ 整理自雅基軟體 CEO 王哲在 GMTC 全球大前端技術大會(深圳站)2021 上的演講《Cocos 引擎的跨平台渲染器架構與實踐》。

嘉賓介紹

王哲 雅基軟體 CEO

Cocos 引擎的創始人和 CEO。經過十年的深耕,目前 Cocos 在全球擁有 150 萬的注冊開發者,遍布全球超過 203 個國家和地區,服務了 40% 的手機遊戲、64% 的 H5 和小遊戲、90% 的線上教育 App,以及大量的 IoT 和數字孿生開發者。

活動推薦

今年 6 月 10 日 -11 日,第一場 GMTC 全球大前端技術大會即将落地北京。大會策劃涵蓋前端業務架構、前端 DevOps、前端性能優化、IoT 動态應用開發、TypeScript、移動端性能與效率優化、前端成長實戰、團隊可持續建設、跨端技術選型等 15 個專題,邀你一起探尋前端技術的熱門方向及落地實踐。

繼續閱讀