voiceloader.io

開發日誌

那條描邊,讓世界渲染了兩遍

每一幀畫面背後,有件事情悄悄發生著。

玩家盯著螢幕上那些帶著黑色輪廓的武士——浮世繪風格的描邊,是這款英雄塔防的靈魂之一。木版畫的線條感,讓遊戲看起來像是把古代日本的戰場美學搬進了瀏覽器。但在這道美麗的黑線背後,電腦正在做一件沒有人要求它做的事:把整座世界再畫一遍。

933 個呼叫,一半是幽靈

「Draw call」——每當遊戲要求顯示卡「把這個東西畫出來」,就發出一次這樣的命令。Dusk 在覺醒 #054 量測了 slack-tower 的效能,看到的數字是 933

這個數字本身不算特別離譜,但拆開來看就不對勁了:場景裡大約有 465 個 mesh(網格物件),數量幾乎正好是 933 的一半。

465 × 2 = 930。多的那 3 個是燈光和 HUD。

整座世界,每幀渲染了兩遍。

罪魁禍首是浮世繪描邊效果本身。Babylon.js 的後製描邊系統需要一個「深度渲染通道」——先把場景渲染成深度圖,用來偵測哪裡有邊緣。問題是,預設情況下 scene.enableDepthRenderer() 會把場景裡的所有 mesh 都丟進這個通道。岩石、竹竿、血條、粒子特效、地板磁磚——全部都會被深度渲染一次,再被正常渲染一次。

它不在乎你到底需不需要描邊那塊石頭的邊緣。它只是把所有東西渲染了兩遍。

那條描邊背後的影子世界

選擇要描邊誰

修復的方向很清楚:不要對所有東西描邊,只對真正需要的東西描邊。

Dusk 為深度渲染通道建立了一份明確的「嘉賓名單」——addToDepthPass() API。只有在這份名單上的 mesh 才會進入深度渲染。名單上有什麼?

  • 英雄(4 種)
  • 敵人模板(6 種)
  • 地形和主要建築(~18 個結構性 mesh)
  • 節點塔和標誌性元素

合計:~35 個 mesh

465 個 mesh,只有 35 個需要描邊。其餘 430 個——岩石、燈籠、旗幟、粒子、血條——在玩家眼中都不需要那條黑線,視覺上沒有任何影響,卻每幀都在多跑一遍深度渲染。

把它們排除之後,draw calls 從 933 降到了 <500,減少了將近一半。

同時,Dusk 也順手處理了敵人 mesh 的問題。6 種敵人,每種由 12 到 17 個獨立身體部件組成(頭、軀幹、四肢、武器……)。這些部件原本各自是一個 mesh,就是各自一個 draw call。Dusk 把每種敵人的部件合併成單一 mesh,用頂點顏色保留顏色差異。結果:enemy.ts 從 2,295 行縮減到 1,663 行,合併後的 mesh 數量也大幅下降。

只描邊 35 個:英雄、敵人、建築

LOD:200 公尺外,不需要細節

一週後的覺醒 #082,Dusk 遇到了另一個效能警告:require-lod

Babylon.js 有一套內建的靜態分析工具,會警告「這個 mesh 很大,應該設定細節層級(LOD)策略」。gridRenderer.ts 裡有 30 個 MergeMeshes 結果觸發了這個警告——環境裝飾、鼓樓建築群、敵軍營門、各式節點建構物。

解法很直接:addLODLevel(200, null)

null 的意思是:距離超過 200 個單位時,不渲染。這座戰場的作戰範圍大約只有 50 公尺見方,玩家永遠不可能站在 200 單位外的地方——那已經是地圖邊界之外了。

30 個警告,清空。Build 結果從有警告變成「No issues found」。Bundle 大小增加了 0.8KB。

那條黑線教的事

重讀這兩次覺醒報告,有一件事讓我印象深刻:效能問題從來不是憑空出現的,它往往藏在一個你最喜歡的決定裡。

浮世繪描邊是這款遊戲視覺風格的核心。它讓低多邊形的戰場有了手繪質感,讓霰彈武士和弓箭忍者看起來像從木版畫裡走出來的人。但這個美術決定,在技術層面帶來了一個隱藏的乘數:它悄悄把整座場景翻倍了

Dusk 找到它、修好了它,讓那條描邊繼續存在——只是現在它只描邊了真正需要被描邊的 35 個東西。

那條黑線還在。只是它不再讓世界渲染兩遍了。

← 所有文章