從零開始構建自己的WebGL3D引擎---思考與設計

引言 :
學習webgl已經接近2年時間了,對常見的開源3D引擎也比較熟悉了,但是目前爲止three.js、babyLon、clayGL、cesium對webgl2新特性的支持也不是特別多。從今天開始,準備着手構建自己的3D引擎,一是希望能有自我提升,二是希望能做一個更快速、更炫酷、更容易擴展的引擎,並且希望能對webgl2的新特性有更多的支持。(目前引擎設計主要參考three.js和clayGL、少部分可能會參考babylon、如果後續做地球部分應該會參考cesium,由於對playCanvas使用不多,後續在引擎設計的時候也會着重去看playCanvas的源碼並將好的思想融入進來。總而言之,在設計引擎的過程中自己多思考,然後多看這些開源引擎的思路,取其精華)

  1. 首先介紹下幾個開源引擎:

Three.js
https://github.com/mrdoob/three.js
最火的web3D引擎,功能最豐富而且最便於使用,筆者對其的研究比較深入,上層的一些設計可能會對其參考較多

ClayGL
https://github.com/pissang/claygl
ClayGL是一個用於構建可擴展Web3D應用程序的WebGL圖形庫,目前主要應用到echarts中,筆者對其比較熟悉,後續對它的一些分析可能會比較多

Babylon.js
https://github.com/BabylonJS/Babylon.js
一個完整的JavaScript框架,用於構建HTML5,WebGL,WebVR和Web Audio的3D遊戲和體驗,該引擎主要使用typescript,性能強大,對webgl2支持較多

Cesium.js
https://github.com/CesiumGS/cesium
一個3D地球引擎,主要是解決地球上的一些計算和大尺度問題,後續做地球可以參考

PlayCanvas
https://github.com/playcanvas/engine
PlayCanvas是一個開源遊戲引擎。目前對其使用不多。它是ECS架構,架構比較清晰,渲染列表的標髒處理的不錯。

2.搭建工具和語言:

要着手做自己的引擎,首先要構建一套打包工具,我這裏使用webpack+gulp+eslint進行構建。由於對typescript不太熟悉,目前還是主要使用es6的語法(後續會逐步適應typescript)。用構建工具構建項目的過程比較簡單,這裏就不一一記錄了。

3.架構和渲染設計:

該如何設計一個架構:我們先看一下three做了什麼?
其實只幹了兩件事
Graph Tree 場景樹管理
Render 渲染

如何構建場景樹:我們看看three的設計思路:

Render渲染
首先反思一下Three.js,three在渲染上有什麼改進空間?
答:對Three比較熟悉的人應該知道,Three的最大痛點就是Renderer的設計包含了太多東西:
更新場景中所有物體的矩陣
生成渲染列表(分類,收集, 視錐裁剪,排序…)
更新燈光信息
執行渲染

Three在renderer中做了太多的事情,這確實不是太合適,主要是爲了讓用戶更加容易使用吧,但是這個設計確實也帶來了很多擴展和性能上的問題。
這樣設計使其在實現VAO、UniformBlock、MRT這些關鍵技術的時候帶來了極大的困難,現在也沒有實現這幾個非常關鍵的技術,這其實才是最大的痛點。
而一個引擎是否強大的核心是有沒有強大的延遲渲染系統,顯然Three不具備,我們看看爲什麼?

後期和延遲渲染經常會遇到的場景:
一般在後處理的時候我們希望替換某些物體的材質爲材質1,替換另一些物體的材質爲材質2,也就是常用的scheme策略
graph LR
場景–>物體
物體–>材質1
物體–>材質2

如果要用Three.js要如何實現那?
scene.traverse(object => {
// change material1
});

renderer.render(scene, camera, target);

scene.traverse(object => {
// change material2
});

renderer.render(scene, camera, target);

// …

實際上:
1.遍歷場景替換材質1(遍歷浪費)
2.更新場景中所有物體的矩陣
3.生成渲染列表(分類,收集, 視錐裁剪,排序…)
4.執行渲染
5.遍歷場景替換材質2(遍歷浪費)
6.更新場景中所有物體的矩陣(浪費)
7.生成渲染列表(分類,收集, 視錐裁剪,排序…)(浪費)
8.執行渲染
9…
(參考shawn0326的文章https://juejin.im/post/5e5dbe23e51d4526f363b123)

所以我們在實現Render的時候,希望render能只專注於渲染,而且爲了後處理,我們希望render在渲染之前用戶可以自定義自己的材質,默認使用MRT
scene.updateMatrix();//將更新矩陣移出renderer,可以手動更新

const renderList = renderer.getRenderList(scene, camera);//獲取渲染列表,將渲染列表的獲取分離出來

const light = renderer.getLight();//燈光信息單獨處理,生成uniformBlock
renderer.renderBackground(scene);//單獨渲染背景
// 渲染渲染列表
renderer.renderRenderList(renderList, camera, light, {
getMaterial: item => {
// 這裏可以通過item.object動態判斷使用哪種材質
return material1;
}
});
//當然我們也提供一個直接的render函數,便於用戶使用,該render直接調用上述幾個接口組裝在一起
renderer.render(scene, camera);

實際上:
1.更新場景樹
2.生成渲染列表(分類,收集, 視錐裁剪,排序…)
3.更新並計算燈光和陰影
4.渲染背景
5.使用材質1渲染
目前這樣設計思路非常明確,用戶可以更加靈活的使用renderer,並以更加優化的方式實現自己的延遲渲染策略。後續還應該思考如何內置一個更加方便的延遲渲染策略以及MRT。
當然渲染這塊在實現的時候我會使用VAO和uniformBlock,後續在實現渲染之後再詳細說明思路和好的設計方式。

4.矩陣運算設計

矩陣運算的設計:這部分是我們要關注的重點,仔細看了下three的設計,矩陣運算這部分的設計是直接使用數組進行計算的,這種設計不是太理想。爲什麼那?因爲後續很難將矩陣運算移植到webAssembly,而且還有很多的js對象和數組的創建,內存開銷也不小。
因此我設計的引擎需要摒棄這種思路,希望後續能更好更快的切換到webAssembly。目前矩陣運算主要藉助gl-matrix庫(https://github.com/toji/gl-matrix),這個庫比較簡單輕量而且是直接使用並得到內存數組。接口調用完成可以直接上傳uniform,非常方便快捷。
想要讓引擎更加快速簡單,我還是希望使用dirty機制來觸發引擎的更新,特別是矩陣運算的時候。如果你做了position、rotation包括燈光的修改,我希望能有個好的機制直接觸發引擎的更新,這樣就不用每幀去更新和計算這些參數,能給引擎帶來更好的性能。

5.上游工具

第一步先支持gltf以及一些gltf的擴展吧。目前大部分工具都可以導出到gltf。後續希望能實現更加強大的粒子系統和編輯器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章