用 Nuxt3 + TresJS 簡單製作 3D 互動場景
前言
在網頁開發中,只要提到 3D Library,最先聯想到的非 Three.js 莫屬。
Three.js 是一個優秀的 Javascript 3D 特效庫,能夠輕鬆地在網頁上建立 3D 場景,實現模型渲染、處理材質光影等功能。
在 Three.js 中,我們可以這樣建立一個場景:
不過身為一個 Vue 的開發者,還是比較習慣宣告式渲染的寫法,這 Three.js 寫起來總覺得不太對勁…
這時候像 TroisJS 這樣的 Wrapper Library 就是不錯的選擇,提供了更「Vue」的寫法:
但 Three.js 畢竟是相當熱門且更新頻繁的套件,TroisJS 這種 Wrapper Library 容易因此面臨維護困難與更新緩慢的問題,在第一時間也無法體驗到 Three.js 新功能,也不乏有相關 issue 在討論。
在 React 生態圈中就有一個相當不錯的的套件叫 React-Three-Fiber,可以將撰寫的 JSX 動態的轉換成 Three.js 的寫法,算解決了這個問題。
而前陣子在翻找 Nuxt Modules 時,也發現一個類似作法的套件叫 TresJS。
雖然星星數還不多,但玩了一下發現意外不錯。Three.js 建立一個場景的程式碼在 TresJS 中可以寫成這樣:
這篇文章就來動手體驗一下 TresJS 這個套件吧!
什麼是 TresJS
根據 TresJS 文件 描述,TresJS 的核心概念是一個自動生成的 Three.JS Elements 目錄,這個目錄是從 Three.JS 源碼生成的,TresJS 會自動根據你想要使用的 Three Object 去生成一個 Vue Component。
所以 TresJS 不但能使用最新的 Three.JS 功能,而且只要使用 CamelCase 的方式命名並帶 Tres 前綴,就能使用 Three.JS 的文件中的 Elements,同時又有 Vue 強大的功能。
如果我想使用 Three.js 的 PerspectiveCamera
這個元件,Three.js 中會這樣使用:
而在 TresJS 中,只要在元件前方加上 Tres 就能直接使用了:
所以簡單來說,TresJS 其實就像讓你可以把 Three.js 改用 Vue Component 的方式來撰寫的套件。
因此使用 TresJS 前,先對 Three.js 有基礎概念才會比較容易上手。開發過程也可以直接參考 Three.js 的官方文件,然後轉成 TresJS 的寫法即可。
安裝
可以根據需求選擇使用的框架,這邊我使用 Nuxt3 + TresJS:
安裝 Nuxt3:
安裝 TresJS:
新增到 @tresjs/nuxt
到 nuxt.config.ts
中的 modules
:
另外還建議安裝 Cientos,它是基於 Three.js 中一個包含許多常用功能的套件 - three-stdlib 的 Wrapper Library,一樣提供了許多功能整合,方便我們搭配 TresJS 來快速完成一些功能。
安裝 Cientos:
建立 3D 場景
我們可以使用 <TresCanvas>
建立一個 Scene ,其中會自動幫你處理好 Renderer,所以只需要這樣寫:
要注意建立 TresCanvas
時需要設定大小,如果希望 Canvas 佔滿整個畫面,可以使用 window-size
;但如果希望指定一個範圍,則可以考慮透過 CSS 來限制:
建立成功後會發現什麼都沒有,這是因為預設的場景顏色是黑的。
我們可以使用 clear-color
來幫場景添加背景顏色,如果希望是透明背景,則可以調整色碼或是使用 alpha
。
新增 3D 物件
建立一個 3D 物件(Object)會需要下面三個元素:
-
Geometry(幾何體):Geometry 定義物體的形狀架構。包含物體的頂點(Vertices)、連接頂點的邊。
-
Material(材質):Material 定義物體的外觀。可以控制物體在光線作用下的顏色、透明度、反射等屬性。
-
Mesh(網格):Mesh 會將 Geometry 和 Material 的組合在一起,形成最終的可視物件。
TresJS 中若要新增幾何相關的 Element,只要參考關於 Three.js 中的 Geometry 的文件,並只要依照前面所述,新增相對應的 Component 即可。
例如我想新增一個方體,可以使用 Three.js 中的 BoxGeometry,在 TresJS 可以使用 <TresBoxGeometry>
。
如果在 Three.js 中有提供參數能使用,在 TresJS 中就可以使用 args
這個 Props 來傳遞 Three.js 中提供的參數,如上面的程式碼分別設定了 <TresBoxGeometry>
的長、寬、高。
前面提到 Material 定義了物體的外觀,所以如果想調整模型的顏色,我們可以用 color
這個 Prop 來更改物體的材質顏色:
除了 MeshBasicMaterial
外,Three.js 中還有許多 Material,不同 Material 會有各自的特性跟用途,可以參考 Three.js 文件。
調整 3D 物件位置
目前這樣看起來物件似乎離我們有點太近,可以稍微來調整一下物件的位置。
Three.js 與部分主流 3D 開發軟體相同,使用的是 Right-Handed Cartesian Coordinate Systems,特別要注意 Y 軸是朝上的。
所以在 Three.js 會是:
- X 為正,往右。
- Y軸為正,往上。
- Z軸為正,往後。
所以如果想讓物件遠離我們,根據 Three.js 的使用的右手坐標系,我們只要給 z 軸負值,物件就會遠離我們。
在這邊可以用 position
給來指定 X、Y、Z 軸的值,也可以單獨用 position-z
來調整,要注意 position
是在 TresMesh
上使用。
建立攝影機
聰明的你可能會注意到一件事,雖然物體的確遠離我們跑到右上角了,但跟想像的好像有點不太對,Z 值為負不是應該往我們正前方遠離嗎?怎麼會是往右上角呢?
這是因為我們視角前方不是朝著 z 軸,所以調整位置時跟想像的有點偏差,這邊我用 Blander 的場景來示意:
那有沒有辦法調整我們的視角位置呢?這時候我們可以使用 攝影機(Camera) 這個物件。
攝影機的概念如下圖,藍綠色的四角錐台(Square Frustums)區塊是攝影機的可視範圍,在 Three.js 中有提供四個參數調整攝影機可視範圍:
- fov:攝影機視錐體的垂直視野角度,角度越大看見的物體越小(可以想像一下魚眼鏡頭),常用於遊戲中望遠鏡、瞄準鏡的運用,預設為 50 度。
- aspect:攝影機視錐體的長寬比,計算方式為 畫布寬度 / 畫布高度,預設為 1,也就是正方形(寬度 = 高度)。
- near:攝影機視錐體的最近平面,小於這個距離的物體不會被渲染,預設為 0.1。
- far:攝影機視錐體的最遠平面,大於這個距離的物體不會被渲染,預設為 2000。
攝影機建立可以使用 PerspectiveCamera,然後我們添加 position
來調整攝影機到 [0, 0, 0]
的位置,並且一樣可以使用 args
來調整攝影機可視範圍的參數:
這樣一來攝影機就會被調整到 [0, 0, 0]
的位置,方塊就會在我們的正前方。
調整物體旋轉
如果想讓攝影機呈現有點俯瞰的角度,就如遊戲裡上帝視角的感覺,但又希望維持在現在 [0, 0, 0]
的位置該怎麼做呢?
我們會需要調整攝影機的高度,也就是增加 Y 軸,然後還需要調整攝影機的角度。
在 Three.js 中,3D 物體旋轉使用的是 rotation
,但在這邊要使用的單位是弧度(radian),弧度中 π 是 180度,所以如果要轉 90 度可以輸入 π/2
。
所以想做到類似上帝視角的感覺,我們可以這樣寫:
這樣一來視角就比較理想了:
可能有些人會疑惑為何是 rotation-x
會是 -(Math.PI/4)
,這邊一樣用 Blander 示意會比較清楚:
另外如果是要看向固定的坐標,TresJS 有提供一個更簡單的作法叫 look-at
,可以直接讓攝影機看向指定的坐標,這樣一來就不需要自己慢慢計算跟調整角度,非常實用。
所以我們可以直接改寫,直接讓他看向在坐標 [0, 0, -5]
的方塊:
呈現效果幾乎相同:
另外 rotation
一樣可以簡寫,不過撰寫的順序會影響旋轉的先後順序;使用陣列寫法的預設旋轉順序是 X -> Y -> Z
,而像是下面的程式碼則會使旋轉順序變成 Z -> Y -> X
:
順序之所以重要,是因為三維空間是 Euler angles 旋轉,所以會遇到 Gimbal lock 問題,可能導致物體的旋轉效果和預期不同,如果有較複雜的旋轉操作要特別留意。
3D 場景的距離單位
可能會有人好奇,上面使用 position
時輸入的數字,到底是什麼單位?
在 3D 軟體中,通常會直接用介面上的「一格」為單位,每一格都是 1 Unit,而每個軟體的 1 Unit 都會有點不太一樣,以 Three.js 來說 1 Unit 大約為 1 米,但通常我們不會特別在意換算的問題,只要確保所有物件都是通一使用同一單位即可。
在 Three.js 中也有比較圖像化的理解方式,可以使用 GridHelper 來顯示網格,
這樣一來調整 3D 物件時也會直觀許多。
攝影機軌道移動
能展示一個 3D 物件後,如果希望使用者還能移動相機,從各個角度來觀看 3D 物件的話該怎麼做呢?
這邊會需要使用 Three.js 中的 OrbitControls
,簡單來說就是一個攝影機控制器,可以圍繞著物件旋轉。
雖然 TresJS 幫我們簡化很多 Three.js 的實作,但在 TresJS 中實作 OrbitControls
也是有點複雜的,以下是 TresJS 文件中提供的實作方式:
但還記得我們一開始有安裝一個套件叫做 Cientos 嗎?在 Cientos 中已經幫我們處理好了,我們只需要添加 <OrbitControls>
即可:
就可以直接任意移動、拖曳、放大視角了,簡單吧!
不過因為 <OrbitControls>
預設會以 [0, 0 ,0]
坐標為中心,建議使用時也把目標物件放在 [0, 0, 0,]
,這樣就不會有旋轉過程中物件超出畫面,還要先拖曳畫面到中間,體驗會比較好。
另外除了 <OrbitControls>
,還有另一個更進階的 CameraControls
,能更詳細的調整相機旋轉的操作效果,有興趣的人可以參考 cientos 文件。
匯入模型和 3D 物體
上面我們使用的都是 Three.js 內建的 3D 物體,如果我們想用自己的 3D 模型,就需要將模型匯入進來。
如果沒有自己的模型但是想嘗試,可以到 Sketchfab 這個網站;Sketchfab 上可以讓使用者分享或販售自己的 3D 模型,登入後我們就可以在這裡找免費的模型,這邊我會使用這個 MacBook Air M2 的模型。
TresJS 目前只提供 GLTF、FBX 兩種格式可以匯入。
最簡單的方法是使用 Cientos 提供的 GLTFModel
、FBXModel
,並且可以使用 draco 來壓縮模型大小以提高效能:
這樣模型就成功匯入進來了~
如果需要在匯入時做更進階的調整,可以使用 Cientos 提供的 useGLTF,會稍微複雜一些,這邊就不另外說明。
調整模型的光線
匯入模型後,會發現怎麼模型整個都是黑的,這是因為我們沒有幫場景添加光線。
像是前面使用到的 <MeshBasicMaterial>
不會有這個問題,是因為這個材質本身不需要光線;如果使用需要光線的材質,或是像我們使用自己模型的材質通常都沒有處理光線,整個模型就會是黑的。
這邊我們可以添加 DirectionalLight,DirectionalLight 會往目標打出一個平行的光,position
設定的是照射目標的位置,這個方向光是不能調整角度的。
假如我先將模型設定在 [0, 0, 0]
的位置,並使用 <TresDirectionalLight>
往 [0, 0, 1]
的位置打一盞光:
稍微調整一下角度可以發現,因為是往 z=1
的方向打一盞平行光,所以鍵盤上是沒有光線的:
我們可以使用 intensity
來增加亮度,這樣一來光線影響的範圍就會變大,然後將照射的目標高度增加,這樣一來光線就能照到鍵盤上:
這樣看起來就比較正常了~
但旋轉視角後會發現,模型的背面沒有照射到光:
如果希望整個模型看起來都是亮的,這邊有兩種簡單的解法:
AmbientLight
是環境光的概念,可以均勻的照亮場景中的所有物件:
這樣一來背面的部分也會受到光線照射:
還有其他各種不同的光線,可以參考 Three.js 的文件。
模型的陰影效果
雖然模型已經看起來不錯了,但感覺少了一點空間的立體感,我們可以透過 <ContactShadows>
來幫物件之間的接觸區塊增加陰影:
此外可以添加 blur
跟 opacity
來讓陰影效果合理一點:
- blur:陰影模糊的範圍。
- opacity:陰影的透明度。
結語
這篇關注在 TresJS 提供的易用寫法,雖然篇幅稍微有點長,但可以發現 TresJS 讓我們幾乎不用寫太攏長的程式碼就能完成許多核心的 3D 功能,讓製作一個基礎 3D 互動效果的難度降低不少,而其他更進階的功能一樣可以藉由引入 Three.js 來達成。
不過 TresJS 畢竟算比較新的套件,目前文件內容和範例比較少,有些進階功能會需要挖掘 Source Code,如果要應用在比較龐大的 3D 互動專案,可能需要根據需求先研究一下。
但如果想製作的 3D 效果很簡單,TresJS 可以幫你免去了許多宣告 Three Object 的繁雜過程,直接幫你包裝好,那麼香不用看看嗎?