做前端的怎麼很快的學會 Web 3D 遊戲的開發

weixin_53135002發表於2020-12-07

本文以「餘額寶3D跑酷遊戲」為例,介紹了前端如何快速上手 Web 3D 遊戲的開發。跑酷遊戲是餘額寶七週年的主玩法,使用者通過做任務來獲取玩遊戲的機會並且解鎖遊戲道具,從而在遊戲中獲得更多的金幣,最終可以利用金幣兌換一些權益,同時我們也在遊戲中植入了一些禮包,先看看具體效果。

 

遊戲設計

我們把遊戲的3D場景分成了三大模組,分別是賽道、金幣(道具)和人物。

賽道設計

賽道包含了樓房和地面,由於人物需要不停地往前跑,基於相對運動的原理,我們複製了兩段樓房(如圖1),並同時做逆時針旋轉,當旋轉至 -theta 角度的時候,把樓房的旋轉角度置為0(如圖2)。地面是一個靜止的圓弧模型,通過改變紋理的 UV 值來實現地面滾動的效果。

 


圖1 賽道結構圖

 

 


圖2 樓房運動軌跡

金幣佈局

由以上圖1可知,我們以 theta 角度的圓弧為一個控制單元,我們希望能控制遊戲的總時長、每段圓弧旋轉的時間,以及每段圓弧擺放的金幣行數,這些引數如何控制3D場景的運作呢?根據已知欄位推匯出以下幾條公式(藍色欄位為可配引數):

  • 需要生成金幣的總行數 = (遊戲總時長 /圓弧旋轉theta角度的時間 )x 每段圓弧擺放的金幣行數
  • 每兩行金幣之間的時間間隔 = 遊戲總時長 / 需要生成金幣的總行數
  • 每行金幣出現的時間 = 每兩行金幣之間的時間間隔 x 金幣索引

這裡主要得出 遊戲總時長 和 每行金幣出現時間 之間的關係,而每行金幣該如何擺放以及道具出現的時機由具體的業務邏輯控制,這裡不展開來講。最終我們得到了一個控制金幣擺放的佇列:


  
  1. [
  2. {
  3. "index": 0, // 索引,代表每一行
  4. "item": {
  5. "position": "center", // 擺放位置
  6. "type": "coin" // 應該擺放的模型型別
  7. },
  8. "time": 0 // 每行金幣出現的時間
  9. },
  10. {
  11. "index": 1,
  12. "item": {
  13. "position": "left",
  14. "type": "coin"
  15. },
  16. "time": 0.25
  17. },
  18. // more......
  19. ]

這個佇列如何與我們的3D場景關聯呢?

由以上圖2可知,一共有兩段圓弧在交替旋轉,假設每段圓弧擺放的金幣行數定義為 rowsPerPart,當前圓弧的索引定義為 index,那麼每次旋轉至0度的時候,取 [index * rowsPerPart, (index + 1), rowsPerPart] 區間的資料進行擺放。陣列中 position 表示擺放位置,一共有左中右三條道,也可能三條道都擺放,根據配置建立金幣節點,並設定好節點的 position。type 表示應該擺放的模型型別,除了金幣還可能是道具、禮包、終點線等。

開發流程

設計好遊戲思路之後,可以正式開始製作我們的遊戲啦~

跑酷遊戲是通過 Oasis Editor 開發的,這是一個 web 3D 內容線上開發平臺,底層用的是 Oasis 3D(螞蟻自研的3D引擎)。這時候你可能會問,為什麼要用 Oasis Editor 開發呢?


接下來分為「場景搭建」、「邏輯開發」、「業務聯動」來講解整個3D工作流。

場景搭建

上傳資產

在編排場景之前我們需要先上傳好遊戲資產,一般美術提供的模型檔案格式為 fbx 或 gltf,紋理推薦使用 webp 格式,我們在資源區右側點選上傳。


在開發過程中,美術可能經常需要替換紋理,所以建議美術將紋理與模型解綁,通過手動上傳的形式將紋理繫結到模型上,避免同時載入兩個紋理。

如圖,我們已經在資源區上傳好樓房、道具、金幣等模型和相應紋理。

 

場景編排

有了資產之後我們需要繫結到節點上,然後進行場景編排,如下視訊以樓房和地面為例進行演示:

  1. 建立場景樹
  2. 繫結GLTF模型
  3. 編輯器PBR材質,繫結紋理
  4. 調整編輯器相機,拷貝編輯視角
  5. 轉換相機視角,微調相機引數

 

按照同樣的方法我們完成了整個場景的編排,某些節點需要通過指令碼控制展示,可以點選場景樹左邊的小眼睛進行隱藏,場景效果如下:

 

粒子系統

遊戲開發的時候,經常會用到粒子系統來幫助我們實現一些比較酷炫的效果,在我們這個專案中,在人物節點(person)下面有2個子節點,分別來負責吃到金幣(coinParticle)和道具(toolParticle)時的粒子效果,遊戲過程中效果如下:

 

 

當我們點選選中一個粒子節點的時候,編輯器右側會出來對應的屬性皮膚,屬性皮膚中就能夠看到我們的粒子元件以及相關引數,通過設定引數可以調整我們的粒子效果:


接下來一步就是來設定引數來控制我們的粒子效果了,下面給大家介紹下幾個常用引數:

 

 

邏輯開發

以上場景可由前端協助美術同學進行搭建,接下來這一步就正式進入程式設計階段了。

指令碼能力

1、cli
Oasis Cli 是連線業務和 Oasis 3D 編輯器的橋樑,在使用我們引擎的時候,建議提前安裝好 Cli 的環境:

tnpm i @alipay/oasis-cli -g
  

安裝好 Cli 之後,我們就可以將場景匯出到我們的本地專案,並且隨時將最新的場景編排同步至本地。首先,我們進入跑酷專案根目錄,並執行如下命令,將我們已經建好的3D場景和當前專案連線:

oasis pull sceneId
  

上面的 pull 命令中,sceneId是我們的場景id,執行完該命令後,會在根目錄下自動新增了1個目錄和1個檔案,如下:


當我們需要對場景進行編輯,並且將最新修改同步至本地,我們只需要執行如下命令即可:

oasis dev
  

2、金幣轉動
這裡以金幣轉動為例演示如何新增指令碼控制,首先在資源皮膚新增一個指令碼,然後在將指令碼掛在節點上:

 


完成這一步後,我們就可以在coinAni的指令碼中實現對coin節點的控制了,金幣一直旋轉我們在指令碼的onUpdate 中處理即可:


  
  1. onUpdate() {
  2. const { node } = this;
  3. TWEEN.update();
  4. if ( this._isRotate && node.parentNode.isActive) {
  5. node.setRotationAngles( 0, globalVal.coinAngle % 360, 0);
  6. }
  7. }

碰撞檢測

利用碰撞檢測來反應人物與金幣之間的碰撞,首先需要給人物和金幣都加上碰撞體包圍盒。Oasis Editor 提供了立方體碰撞體和球型碰撞體,引擎會在每幀更新時計算本節點的 collider 與其他 collider 的相交情況,球型碰撞體之間只需要比較球心距離與兩個半徑之間的大小關係,而立方體碰撞體需要計算八個頂點的位置關係,所以使用球型碰撞體效能上會好一些。

如下圖,我們給人物新增了一個球型碰撞體,可以調節它的球心和半徑。視覺化包圍盒只是編輯器執行時的外掛,因此不會出現在我們的 H5 場景中。


編輯完碰撞體包圍盒之後,我們需要在指令碼中進行碰撞檢測,監聽 collision 事件:


  
  1. let cd = node.createAbility(o3.ACollisionDetection);
  2. cd.addEventListener( 'collision', e => {
  3. const colliderNode = e.data.collider.node; // 拿到被碰撞的節點
  4. const name = colliderNode.name;
  5. // do something...
  6. });

Shader

嘿嘿,看到 Shader 別急著划走,掌握了 Shader 你就可以:

  • 自定義光照、物理等模型,可以開發更多酷炫的效果
  • 能夠優化渲染效能
  • 能夠幫助我們排查渲染上的問題

列舉幾個 Shader 的效果,更多效果可以前往shadertoy:

 

 

 

1、 什麼是 Shader
Shader(著色器)是執行在 GPU 上的小程式,這些小程式為圖形渲染管線的某個特定部分而執行,它用於告訴圖形硬體如何計算和輸出影像。為了更深入瞭解 Shader 的原理,我們需要了解 OpenGL 的渲染流水線,這裡以渲染跑酷遊戲的地面模型為例:

CPU 應用階段

我們在3.1.1中上傳了地面的 fbx 模型檔案,其中包含了頂點位置、UV、法線、切線等資訊,CPU 將這些資訊載入到視訊記憶體中,然後設定渲染狀態,告訴 GPU 如何進行渲染工作。最後 CPU 會發出渲染命令(Drawcall),由GPU 接收並進行渲染。

GPU 渲染管線


GPU 渲染管線包含了幾何階段和光柵化階段,頂點著色器(Vertex Shader)和片元著色器(Fragment Shader)分別位於這兩個階段中。

幾何階段:頂點著色器接收 CPU 傳過來的頂點資料,通常在這個階段做一些空間變換、頂點著色等操作。接著會經過裁剪,把不在相機視野中的頂點裁剪掉,並剔除某些圖元,然後將物體座標系轉換到螢幕座標系。

光柵化階段:兩個頂點之間有很多個畫素,片元著色器會對畫素進行處理,除了進行紋理取樣,還會將畫素與燈光進行計算,產生反射、折射等效果。同一個螢幕畫素點可能會有多個物體,這時候需要通過 alpha 測試、深度測試、模板測試、混合(blend)等處理,把同一位置的畫素進行過濾或合併,最終渲染到螢幕上。
2、如何編寫Shader
Oasis Editor 中寫 Shader 需要經過這幾個步驟:
(1)、在資源區中新增“Shader 材質”,然後繫結到模型上

 


(2)、編輯 Shader 材質,屬性皮膚中提供了常見的渲染狀態配置,也可以直接編輯著色器定義(ShaderDefine)。


整個 ShaderDefine 結構如下,其中 vertexShader 和 fragmentShader 分別存放頂點著色器和片元著色器程式碼,採用 GLSL ( OpenGL 著色語言,OpenGL Shading Language )編寫。states 用來定義渲染狀態控制物件,對應上文提到的合併階段。


  
  1. export const ShaderMaterial = {
  2. vertexShader: ``,
  3. fragmentShader: ``,
  4. states: {},
  5. uniforms: {},
  6. attributes: {},
  7. };

(3)、如果要動態改變材質引數值,需要建立指令碼,在節點每幀執行的回撥函式中修改屬性值。

下面通過跑道滾動和光波兩個示例來講解。

3、 跑道滾動
如2.1中所述,跑道是一個靜止的圓弧模型,通過改變紋理的UV值來實現跑道滾動的效果。為了實現給人物打光的效果,我們在基礎顏色紋理上面疊加了一張漸變紋理,並給人物加上了一個靜態的陰影(實際上是一個面片)。

 


( 基礎顏色紋理)

 

(漸變紋理)
=


( 疊加效果)

相關的Shader程式碼如下:


  
  1. export const ShaderMaterial = {
  2. // Vertex Shader 程式碼
  3. vertexShader: `
  4. uniform mat4 matModelViewProjection;
  5. uniform float utime;
  6. attribute vec3 a_position;
  7. attribute vec2 a_uv;
  8. varying vec2 v_uv;
  9. varying vec2 v_uv_run;
  10. void main() {
  11. gl_Position = matModelViewProjection * vec4(a_position, 1.0 );
  12. v_uv = a_uv;
  13. v_uv_run = vec2( v_uv.s, v_uv.t + utime );
  14. }
  15. `,
  16. // Fragment Shader 程式碼
  17. fragmentShader: `
  18. varying vec2 v_uv;
  19. varying vec2 v_uv_run;
  20. uniform sampler2D texturePrimary;
  21. uniform sampler2D textureLight;
  22. void main() {
  23. vec4 texSample = texture2D( texturePrimary, v_uv_run ).rgba;
  24. vec4 texLightSample = texture2D( textureLight, v_uv ).rgba;
  25. gl_FragColor = vec4(texSample.rgb * texSample.a + texLightSample.rgb * texLightSample.a, texSample.a);
  26. }
  27. `,
  28. states: {},
  29. }

Vertex Shader 和 Fragment Shader 都包含了一個 mian 入口函式。

初次看 Shader 程式碼會發現很多陌生的符號,其中 uniform、attribute 和 varying 都是變數限定符,attribute 只能存在於 Vertex Shader 中,一般用來放置程式傳過來的頂點、法線、顏色等資料;uniform 是程式傳入到 Shader 中的全域性資料;varying 主要負責在Vertex Shader 和 Fragment Shader 之間傳遞變數。

mat4、vec3、sampler2D 都是基本變數型別,分別代表矩陣、向量和紋理,後面的數字代表n維,例如 mat4表示 4x4 矩陣。

本例的 Vertex Shader 中,頂點位置 a_position 與 matModelViewProjection 矩陣相乘,其實是把三維世界的物體投影到二維的螢幕上。a_uv 存放了 UV 資訊,我們想要把一張貼圖貼到模型表面,需要紋理對映座標,即UV座標,分別代表橫縱兩個方向。為了使地面能滾動起來,我們需要每幀改變 UV 的縱座標,並通過變數 v_uv_run 傳遞給 Fragment Shader。

在 Fragment Shader 中,texturePrimary 和 textureLight 都是從 CPU 程式傳過來的紋理。通過 texture2D 取樣基礎顏色紋理 texturePrimary,得到了紋理貼圖在模型上滾動的效果。接著拿取樣後的顏色值與透明漸變紋理 texLightSample 進行疊加,得到了近亮遠暗的效果。

最後,我們在 CPU 中每幀更新 utime 的值,並傳入 Shader。


  
  1. onUpdate(deltaTime) {
  2. if (! this.running || ! this._streetMaterial) return;
  3. // 賽道滾動
  4. this._time -= deltaTime * 0.0002;
  5. this._time %= 1.0;
  6. this._streetMaterial.setValue( 'utime', this._time);
  7. }

4、光波特效

 

 

人物吃到吸吸卡之後會有一個光波特效,由於是不規則動畫,我們採取了幀動畫來實現。首先需要拿到這樣nn的幀序列。注意,瀏覽器會對紋理尺寸進行限制,可以通過 gl.MAX_TEXTURE_SIZE 拿到這個值,最好別超過20482048。


接著在 Shader 中進行紋理取樣。假設一個 100 * 100 的正方形,它的頂點著色器執行4次(因為有4個頂點),但片元著色器會執行 10000 次,所以儘量把 UV 等計算放在 Vertex Shader 中,再通過 varying 傳給 Fragment Shader。程式碼如下:


  
  1. export const ShaderMaterial = {
  2. // Vertex Shader 程式碼
  3. vertexShader: `
  4. attribute vec3 a_position;
  5. attribute vec2 a_uv;
  6. uniform mat4 matModelViewProjection;
  7. uniform float uFrame;
  8. varying vec2 v_uv;
  9. void main(void)
  10. {
  11. gl_Position = matModelViewProjection * vec4(a_position, 1.0);
  12. float cellCount = 8.0;
  13. float row = floor(uFrame / cellCount); // 當前第幾行
  14. float col = mod(uFrame, cellCount); // 當前第幾列
  15. float cellSize = 1.0 / cellCount;
  16. v_uv = vec2(a_uv.s * cellSize + col * cellSize, a_uv.t * cellSize + row * cellSize);
  17. }
  18. `,
  19. // Fragment Shader 程式碼
  20. fragmentShader: `
  21. varying vec2 v_uv;
  22. uniform sampler2D uDiffuseMap;
  23. void main(void)
  24. {
  25. gl_FragColor = texture2D(uDiffuseMap, v_uv);
  26. }
  27. `,
  28. states: {},
  29. uniforms: {
  30. uDiffuseMap: {
  31. name: 'uDiffuseMap',
  32. type: o3.DataType.SAMPLER_2D
  33. },
  34. uFrame: {
  35. name: 'uFrame',
  36. type: o3.DataType.FLOAT
  37. }
  38. },
  39. attributes: {},
  40. };

CPU需要傳入幀序列紋理uDiffuseMap,還要每幀更新uFrame的值:


  
  1. onUpdate(deltaTime) {
  2. // update per frame
  3. if ( this.material) {
  4. this.frame++
  5. if ( this.frame > 57) {
  6. this.frame = 0;
  7. }
  8. this.material && this.material.setValue( 'uFrame', this.frame)
  9. }
  10. }

業務聯動

餘額寶跑酷是一個跑在 h5 環境下的專案,其中就涉及到業務層(react)和遊戲層(oasis),我們在業務層和遊戲層之間加了一個膠水層(gameController)來進行兩者通訊,結構如下:


從上面結構圖可以看出,作為膠水層的gameController,主要做了2件事情,一個是給業務層提供api呼叫,並且通知遊戲層,另外一個是監聽遊戲層的訊息,並且通知業務層,下面來看看示例:


  
  1. import * as o3 from '@alipay/o3';
  2. export default class GameController extends o3.EventDispatcher {
  3. constructor (rootNode, dispatch) {
  4. super();
  5. this._dispatch = dispatch;
  6. this._oasis = this._rootNode.engine;
  7. // 獲取需要監聽的節點
  8. this._rootNode = rootNode;
  9. this._magnetCollidNode = rootNode && rootNode.findChildByName( 'magnetCollid');
  10. this._buildNNode1 = rootNode && rootNode.findChildByName( 'part1');
  11. this._buildNNode2 = rootNode && rootNode.findChildByName( 'part2');
  12. this._streetNode = rootNode && rootNode.findChildByName( 'street');
  13. // 註冊監聽
  14. this.getMessage(rootNode);
  15. }
  16. // 註冊監聽
  17. getMessage(rootNode) {
  18. // 註冊監聽遊戲層訊息
  19. this._magnetCollidNode.addEventListener( 'magnetCoinCollide', (event) => {
  20. // 反饋給業務層
  21. this._dispatch && this._dispatch({type: 'collideHappen', payload:{ type: 'coin' }});
  22. });
  23. // todo 其他節點註冊監聽
  24. }
  25. // 給業務層呼叫的api
  26. gameInit(iconList, gameData) {
  27. const gameInit = new o3.Event( 'gameInit');
  28. gameInit. data = {
  29. iconList,
  30. gameData,
  31. };
  32. this._oasis && this._oasis.resume();
  33. // 通知遊戲層
  34. this._buildNNode1.trigger(gameInit);
  35. this._buildNNode2.trigger(gameInit);
  36. this._streetNode.trigger(gameInit);
  37. }
  38. }

效能優化

除錯工具

工欲善其事必先利其器,當我們需要對專案進行效能優化的時候,我們首先需要分析效能瓶頸點,然後對症下藥,很幸運的是chrome本身就自帶效能分析工具(Performance:開啟頁面進入開發者工具即可看到),如下:


除了效能除錯工具外,有時候我們還會遇到一些渲染異常,大多是給到GPU的資料有問題,而這部分資料我們沒法console.log,chrome提供了一個非常好用的外掛(Spector.js)幫助我們檢視每一幀的資料,如下:

 

降低三角面

三角面越多,gpu的計算量也會越大,結合遊戲實際的玩法,我們對三角面這塊的優化主要就是不同模型進行減面,最終三角面從20萬+降低到6萬+,具體如下:

1、人物這塊,因為在跑動過程中,我們始終只能看到背面,所以把人物前面的三角面全部去掉

2、金幣這塊,在保證視覺效果看起來比較圓的前提下儘可能的減少三角面

3、樓房和人物類似,把賽道外部的遊戲過程中根本看不到的面去除

 

 

提升幀率

提升幀率本質上就是減少cpu的運算時間,通過前面提到的分析工具分析,我們發現節點數量過多是導致cpu運算量大的主要原因,所以我們的優化重點是在降低節點數量上,最終我們的 fps 在低端機上面從10優化到25,下面來具體說下:

1、金幣模型裡面有很多沒有用的空節點,這個我們找美術同學幫忙重新簡化模型檔案

2、金幣模型簡化後,其實模型裡面還有2個節點(其中有一個rootnode其實沒啥用,和美術同學交流,反饋是目前沒有辦法去掉),加上掛載模型的節點,我們一個金幣物件其實就有3個節點,為了進一步優化,我們通過程式碼動態去掉多餘節點並進行節點合併。

3、使用物件池來避免反覆建立金幣。在主迴圈中,對一些迴圈出現的元素,我們一種優化手段就是在初始化的時候事先建立一定數量的物件,然後用的時候來取,用完就還回來,而快取建立好的物件的結構就是我們的物件池了。物件池帶來的好處:減少主迴圈過程中建立物件帶來的開銷、可以有效避免因建立釋放等操作帶來的GC。我們遊戲中金幣數量很多,並且是高頻出現的,所以要用物件池來快取,相應的設計如下:


  
  1. class CoinPool {
  2. private _originNode = null;
  3. private _pool = [];
  4. constructor () {
  5. }
  6. init (originNode: o3.Node, capacity: number = 5) {
  7. this._originNode = originNode;
  8. this._genNode(capacity);
  9. }
  10. destroy () {
  11. this._originNode = null;
  12. this._pool.length = 0;
  13. }
  14. getNode () {
  15. if ( this._pool.length === 0) {
  16. this._genNode();
  17. }
  18. return this._pool.shift();
  19. }
  20. putNode (node: o3.Node) {
  21. if ( this._pool.indexOf(node) === - 1) {
  22. this._pool.push(node);
  23. }
  24. }
  25. _genNode (num: number = 1) {
  26. const pool = this._pool;
  27. for (let i = 0; i < num; ++i) {
  28. let node = this._originNode.clone();
  29. // 對金幣模型節點的優化在這裡統一處理
  30. changeParent(node);
  31. purifyNode(node);
  32. pool.push(node);
  33. }
  34. }
  35. }

物件池使用方式:


  
  1. // 建立並初始化
  2. const originCoin = node.findChildByName( 'coinParent'); // 掛載金幣模型的節點
  3. const coinPool = new CoinPool();
  4. coinPool.init(originCoin, 24);
  5. // 從池子裡面獲取金幣節點
  6. const coinNode = coinPool.getNode();
  7. // 金幣節點不需要使用了,進行回收
  8. coinPool.putNode(coinNode);
  9. // 整個節點池銷燬
  10. coinPool.destroy();

其他

上述兩項其實都是針對跑酷專案本身做的一些特定優化,其他專案未必能夠完全照搬,我們的塵沫大神針對業務方面的效能優化做了比較通用全面的總結,這裡簡單列舉一下:

語言

  • 使用列舉:在標記判斷if或switch語句中儘量使用number型列舉,避免使用字串作為判斷標記,字串作為判斷標記效能損耗較大
  • 使用Number做Object的Key:Object作為Map使用時儘量不要使用string作為Key,而是傾向使用Number作為Key,其中Number的範圍越小效能越高,通常小於65535效能較優
  • 使用“.”訪問物件屬性:避免使用["string"]訪問物件的屬性和方法,會導致JIT優化失效,應使用“.”訪問屬性
  • 儘量使用for迴圈遍歷:幀級呼叫盡量使用for迴圈進行遍歷操作提升效能,相對於語法糖迴圈更純粹,需要提前快取長度n進行迴圈判斷,減少紋理定址效能損耗

邏輯

  • 多用物件池機制:由於JS本身機制和原理,需要避免在幀迴圈中new物件,避免GC卡頓,在業務開發中的模型抽象強烈建議使用物件池機制做物件管理
  • 善用例項或靜態全域性變數:除了物件池機制避免GC外,還需要利用例項或靜態全域性變數減少GC損耗,比如一些用於中轉數學計算的臨時變數可使用靜態全域性變數快取,另外一些可逐例項的類變數可快取為例項全域性變數,減少使用時的頻繁new操作帶來的開銷和GC。
  • 慎用事件:在大型專案中慎用事件,事件本身的靈活性是一把雙刃劍,在解耦的同時也帶來了邏輯可讀性低等困難,尤其在多人協作開發的專案中,所以在業務系統中該解耦的模組用事件,不需要的地方需要用明確的設計呼叫邏輯解決,切記不要因為設計的懶惰把專案搞亂

資源優化

  • 模型合併優化:美術需將不可獨立移動的模型儘可能合併減少渲染批次,同時注意不要合併場景範圍跨度過大的模型導致模型無法裁剪的問題
  • 材質優化:
    • 儘可能合併材質,材質作為三維引擎的合併根基,一切引擎級渲染批次的合併前提都是使用相同材質,所以要保持材質物件儘可能的少
    • 材質模型選擇需要根據美術風格儘量精簡,比如直接把光照合併在漫反射貼圖的的卡通風格模型可以直接選擇unlit材質,而無需使用複雜的PBR材質模型

 

  • 貼圖優化:貼圖尺寸不可能盲目追求質量使用超大尺寸,需要評估實際專案貼圖光柵化後的實際顯示畫素來使用接近的貼圖尺寸,否則使用過大尺寸不僅得不到效果手機還浪費視訊記憶體。除此之外還可使用紋理壓縮優化視訊記憶體
  • 畫素填充率優化:
    • 儘量減少全屏渲染的繪製,比如UI或遮罩使用類似全屏但大部分透明的圖片繪製會帶來大幅的GPU渲染負擔
    • 在移動端等高DPI的裝置中可適當降低DPI配置,減少GPU負擔

 

玩法系統優化

  • 碰撞系統優化:
    • 善用主動碰撞和被動碰撞概念,減少主動碰撞器可以大幅減少碰撞檢測的迴圈遍歷次數
    • 善用碰撞組概念,將物體劃分所屬碰撞組和可與之發生碰撞的組作為過濾器,根據業務規則劃分可以減少不必要的碰撞檢測迴圈

 

  • 跑酷彎道優化:可嘗試利用頂點著色器模擬彎道跑酷效果,減少CPU端相關跑酷彎道邏輯的計算負擔,降低美術製作複雜度

Oasis 3D V2.x To V3.x

隨著 Oasis 3D 服務的業務數量越來越多、業務負責度越來越大,也暴露出不少問題,為此,我們對現有引擎進行了大重構,也就是V3.x版本,此版本主要目標是:更快、更方便、更高效。

這裡先簡單介紹幾個重構模組,希望讓大家有個初步體感。

資源管理模組

資源管理模組我們從底層實現進行了大重構,主要目的是簡化開發者的使用,下面是v2.x版本和v3.x版本載入一個帶有骨骼動畫的模型示例,對比可以看出v3.x版本的api是特別精簡的,除了api的簡化外,功能上我們還提供了下載重試、重試間隔、下載超時、下載進度、取消下載等。

V2.x版本載入資源:


  
  1. let gltfRes = new Resource( "skin_gltf", {
  2. type: "gltf",
  3. url: "xxx.gltf"
  4. });
  5. let resourceLoader = new ResourceLoader(engine);
  6. resourceLoader.load(gltfRes, (err, gltf) => {
  7. if (err) return;
  8. const fairyPrefab = gltf.asset.rootScene.nodes[ 1];
  9. const fairy1 = fairyPrefab;
  10. rootNode.addChild(fairy1);
  11. const animator = fairy1.addComponent(Animation);
  12. const animations = gltf.asset.animations;
  13. animations.forEach( (clip) => {
  14. animator.addAnimationClip(clip, clip.name);
  15. });
  16. animator.playAnimationClip( "Take 001");
  17. });

V3.x版本載入資源:


  
  1. const { defaultSceneRoot, animations } = await engine.resourceManager.load( "xxx.gltf");
  2. rootEntity.addChild(defaultSceneRoot);
  3. const animator = root.getComponent(Animation);
  4. animator.playAnimationClip( "Take 001");

數學庫

數學庫整個進行重構,主要有2方面改善:寫法更簡捷、效能更優。老的數學庫都是函式式的,並且向量、四元數等低層其實都是Array,而V3.x採用Class的方式來實現,底層資料結構改為object。

新的數學庫不僅支援更為豐富的寫法,效能上面,通過數學庫重構以及使用資料庫相關的優化,效能提升比較明細,下面是我們的測試結果:

線上 coding

目前我們編輯器實現了線上coding,意味著你只需要一臺電腦,並且安裝一個瀏覽器,即可完成3D專案的建立、開發、釋出等


在上面的介面中,即可完成線上coding,然後儲存,即可實時檢視最新的效果。進一步的,我們還提供了事件皮膚,模擬和業務層的互動,這樣我們就可以在3D專案中自測完整個流程,然後釋出給業務層使用


當我們開發完專案後,需要交付給業務方使用,在V3.x中,我們只需要點選發布至對應平臺即可(這塊還在持續優化中)

相關文章