用 Nuxt3 + TresJS 簡單製作 3D 互動場景

用 Nuxt3 + TresJS 簡單製作 3D 互動場景

開發筆記

前言

在網頁開發中,只要提到 3D Library,最先聯想到的非 Three.js 莫屬。

Three.js 是一個優秀的 Javascript 3D 特效庫,能夠輕鬆地在網頁上建立 3D 場景,實現模型渲染、處理材質光影等功能。

在 Three.js 中,我們可以這樣建立一個場景:

import * as THREE from "three";
 
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
 
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

不過身為一個 Vue 的開發者,還是比較習慣宣告式渲染的寫法,這 Three.js 寫起來總覺得不太對勁…

味道不對啊

這時候像 TroisJS 這樣的 Wrapper Library 就是不錯的選擇,提供了更「Vue」的寫法:

<script setup>
import { ref } from "vue";
const renderer = ref(null);
</script>
 
<template>
  <Renderer ref="renderer">
    <Camera :position="{ z: 10 }" />
    <Scene />
  </Renderer>
</template>

但 Three.js 畢竟是相當熱門且更新頻繁的套件,TroisJS 這種 Wrapper Library 容易因此面臨維護困難與更新緩慢的問題,在第一時間也無法體驗到 Three.js 新功能,也不乏有相關 issue 在討論。

在 React 生態圈中就有一個相當不錯的的套件叫 React-Three-Fiber,可以將撰寫的 JSX 動態的轉換成 Three.js 的寫法,算解決了這個問題。

import { createRoot } from "react-dom/client";
import { Canvas } from "@react-three/fiber";
function App() {
  return (
    <div id="canvas-container">
      <Canvas />
    </div>
  );
}
createRoot(document.getElementById("root")).render(<App />);

而前陣子在翻找 Nuxt Modules 時,也發現一個類似作法的套件叫 TresJS

TresJS Github 頁面

雖然星星數還不多,但玩了一下發現意外不錯。Three.js 建立一個場景的程式碼在 TresJS 中可以寫成這樣:

<template>
  <TresCanvas window-size>
    <TresPerspectiveCamera />
  </TresCanvas>
</template>

我不懂但是好像很厲害

這篇文章就來動手體驗一下 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 中會這樣使用:

import { PerspectiveCamera } from "three";
 
const camera = new PerspectiveCamera(45, width / height, 1, 1000);

而在 TresJS 中,只要在元件前方加上 Tres 就能直接使用了:

<template>
  <TresPerspectiveCamera />
</template>

所以簡單來說,TresJS 其實就像讓你可以把 Three.js 改用 Vue Component 的方式來撰寫的套件。

因此使用 TresJS 前,先對 Three.js 有基礎概念才會比較容易上手。開發過程也可以直接參考 Three.js 的官方文件,然後轉成 TresJS 的寫法即可

安裝

可以根據需求選擇使用的框架,這邊我使用 Nuxt3 + TresJS:

安裝 Nuxt3

pnpm dlx nuxi@latest init <project-name>

安裝 TresJS

# 安裝 tresjs/nuxt
pnpm add three @tresjs/nuxt

新增到 @tresjs/nuxtnuxt.config.ts 中的 modules

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ["@tresjs/nuxt"],
});

另外還建議安裝 Cientos,它是基於 Three.js 中一個包含許多常用功能的套件 - three-stdlib 的 Wrapper Library,一樣提供了許多功能整合,方便我們搭配 TresJS 來快速完成一些功能。

安裝 Cientos

# 安裝 cientos
pnpm add @tresjs/cientos

建立 3D 場景

我們可以使用 <TresCanvas> 建立一個 Scene ,其中會自動幫你處理好 Renderer,所以只需要這樣寫:

<!-- TresJS 創建一個場景 -->
<template>
  <TresCanvas window-size />
</template>

要注意建立 TresCanvas需要設定大小,如果希望 Canvas 佔滿整個畫面,可以使用 window-size;但如果希望指定一個範圍,則可以考慮透過 CSS 來限制:

<template>
  <div id="container">
    <TresCanvas />
  </div>
</template>
<style>
#container {
  height: 300px;
  width: 300px;
}
</style>

建立成功後會發現什麼都沒有,這是因為預設的場景顏色是黑的。

預設 Canvas 畫面

我們可以使用 clear-color 來幫場景添加背景顏色,如果希望是透明背景,則可以調整色碼或是使用 alpha

<template>
  <!-- 將背景設成橙色 -->
  <TresCanvas clear-color="#FFA500" />
  <!-- 透明背景作法 1 -->
  <TresCanvas clear-color="#FFFFFF00" />
  <!-- 透明背景作法 2 -->
  <TresCanvas alpha />
</template>

兩種 Canvas 背景顏色

新增 3D 物件

建立一個 3D 物件(Object)會需要下面三個元素:

  • Geometry(幾何體):Geometry 定義物體的形狀架構。包含物體的頂點(Vertices)、連接頂點的邊。

  • Material(材質):Material 定義物體的外觀。可以控制物體在光線作用下的顏色、透明度、反射等屬性。

  • Mesh(網格):Mesh 會將 Geometry 和 Material 的組合在一起,形成最終的可視物件。

TresJS 中若要新增幾何相關的 Element,只要參考關於 Three.js 中的 Geometry 的文件,並只要依照前面所述,新增相對應的 Component 即可。

Three.js Geometry 頁面

例如我想新增一個方體,可以使用 Three.js 中的 BoxGeometry,在 TresJS 可以使用 <TresBoxGeometry>

<template>
  <TresCanvas window-size clear-color="#C0F7A4" />
    <TresMesh>
      <!-- :args"[width, height, depth]" -->
      <TresBoxGeometry :args="[1, 2, 3]" />
      <TresMeshBasicMaterial />
    </TresMesh>
  </TresCanvas>
</template>

如果在 Three.js 中有提供參數能使用,在 TresJS 中就可以使用 args 這個 Props 來傳遞 Three.js 中提供的參數,如上面的程式碼分別設定了 <TresBoxGeometry>長、寬、高

TresBoxGeometry

前面提到 Material 定義了物體的外觀,所以如果想調整模型的顏色,我們可以用 color 這個 Prop 來更改物體的材質顏色:

<template>
  <TresMesh>
    <TresBoxGeometry />
    <TresMeshBasicMaterial color="#7fc3ff" />
  </TresMesh>
</template>

為 BoxGeometry 添加顏色

除了 MeshBasicMaterial 外,Three.js 中還有許多 Material,不同 Material 會有各自的特性跟用途,可以參考 Three.js 文件

調整 3D 物件位置

目前這樣看起來物件似乎離我們有點太近,可以稍微來調整一下物件的位置。

Three.js 與部分主流 3D 開發軟體相同,使用的是 Right-Handed Cartesian Coordinate Systems,特別要注意 Y 軸是朝上的

Coordinate Systems

所以在 Three.js 會是:

  • X 為正,往右
  • Y軸為正,往上
  • Z軸為正,往後

所以如果想讓物件遠離我們,根據 Three.js 的使用的右手坐標系,我們只要給 z 軸負值,物件就會遠離我們。

在這邊可以用 position 給來指定 X、Y、Z 軸的值,也可以單獨用 position-z 來調整,要注意 position 是在 TresMesh 上使用。

<template>
  <TresCanvas window-size clear-color="#C0F7A4">
    <!-- 作法一:使用 position 定義 [x, y, z] 的移動位置 -->
    <TresMesh :position="[0, 0, -5]">
      <TresBoxGeometry :args="[1, 2, 3]" />
      <TresMeshBasicMaterial color="#7fc3ff" />
    </TresMesh>
    <!-- 作法二:使用 position-z 單獨指定調整 z 軸移動 -->
    <TresMesh :position-z="-5">
      <TresBoxGeometry :args="[1, 2, 3]" />
      <TresMeshBasicMaterial color="#7fc3ff" />
    </TresMesh>
  </TresCanvas>
</template>

調整物件 z 軸

建立攝影機

聰明的你可能會注意到一件事,雖然物體的確遠離我們跑到右上角了,但跟想像的好像有點不太對,Z 值為負不是應該往我們正前方遠離嗎?怎麼會是往右上角呢?

這是因為我們視角前方不是朝著 z 軸,所以調整位置時跟想像的有點偏差,這邊我用 Blander 的場景來示意:

3D 場景示意圖

那有沒有辦法調整我們的視角位置呢?這時候我們可以使用 攝影機(Camera) 這個物件。

攝影機的概念如下圖,藍綠色的四角錐台(Square Frustums)區塊是攝影機的可視範圍,在 Three.js 中有提供四個參數調整攝影機可視範圍:

Three.js 攝影機範圍

  • fov:攝影機視錐體的垂直視野角度,角度越大看見的物體越小(可以想像一下魚眼鏡頭),常用於遊戲中望遠鏡、瞄準鏡的運用,預設為 50 度
  • aspect:攝影機視錐體的長寬比,計算方式為 畫布寬度 / 畫布高度,預設為 1,也就是正方形(寬度 = 高度)。
  • near:攝影機視錐體的最近平面,小於這個距離的物體不會被渲染,預設為 0.1
  • far:攝影機視錐體的最遠平面,大於這個距離的物體不會被渲染,預設為 2000

攝影機建立可以使用 PerspectiveCamera,然後我們添加 position 來調整攝影機到 [0, 0, 0] 的位置,並且一樣可以使用 args 來調整攝影機可視範圍的參數:

<template>
  <TresCanvas window-size clear-color="#C0F7A4">
    <!-- :args="[for, aspect, near, far]" -->
    <TresPerspectiveCamera :position="[0, 0, 0]" :args="[50, 1, 0.1, 2000]" />
    <!-- 也可以寫成這樣 -->
    <TresPerspectiveCamera :position="[0, 0, 0]"
      :fov="50"
      :aspect="1"
      :near="0.1"
      :far="2000"
    />
    <TresMesh :position="[0,0,-5]">
      <TresBoxGeometry :args="[1,2,3]" />
      <TresMeshBasicMaterial color="#7fc3ff" />
    </TresMesh>
  <TresCanvas>
</template>

這樣一來攝影機就會被調整到 [0, 0, 0] 的位置,方塊就會在我們的正前方。

Three.js 調整攝影機位置

調整物體旋轉

如果想讓攝影機呈現有點俯瞰的角度,就如遊戲裡上帝視角的感覺,但又希望維持在現在 [0, 0, 0] 的位置該怎麼做呢?

我們會需要調整攝影機的高度,也就是增加 Y 軸,然後還需要調整攝影機的角度

在 Three.js 中,3D 物體旋轉使用的是 rotation,但在這邊要使用的單位是弧度radian),弧度中 π 是 180度,所以如果要轉 90 度可以輸入 π/2

所以想做到類似上帝視角的感覺,我們可以這樣寫:

<template>
  <TresPerspectiveCamera :position-y="3" :rotation="[-(Math.PI / 4), 0, 0]" />
</template>

這樣一來視角就比較理想了:

調整相機旋轉後效果

可能有些人會疑惑為何是 rotation-x 會是 -(Math.PI/4),這邊一樣用 Blander 示意會比較清楚:

3D 物件旋轉示意圖

另外如果是要看向固定的坐標,TresJS 有提供一個更簡單的作法叫 look-at,可以直接讓攝影機看向指定的坐標,這樣一來就不需要自己慢慢計算跟調整角度,非常實用。

所以我們可以直接改寫,直接讓他看向在坐標 [0, 0, -5] 的方塊:

<template>
  <TresPerspectiveCamera :position-y="3" :look-at="[0, 0, -5]" />
</template>

呈現效果幾乎相同:

使用 lookat 調整攝影機角度

另外 rotation 一樣可以簡寫,不過撰寫的順序會影響旋轉的先後順序;使用陣列寫法的預設旋轉順序是 X -> Y -> Z,而像是下面的程式碼則會使旋轉順序變成 Z -> Y -> X

<template>
  <!-- Z -> Y -> X -->
  <TresMesh :rotation-z="Math.PI / 2" :rotation-y="Math.PI / 2" :rotation-x="Math.PI / 2" />
</template>

順序之所以重要,是因為三維空間是 Euler angles 旋轉,所以會遇到 Gimbal lock 問題,可能導致物體的旋轉效果和預期不同,如果有較複雜的旋轉操作要特別留意。

3D 場景的距離單位

可能會有人好奇,上面使用 position 時輸入的數字,到底是什麼單位?

在 3D 軟體中,通常會直接用介面上的「一格」為單位,每一格都是 1 Unit,而每個軟體的 1 Unit 都會有點不太一樣,以 Three.js 來說 1 Unit 大約為 1 米,但通常我們不會特別在意換算的問題,只要確保所有物件都是通一使用同一單位即可。

在 Three.js 中也有比較圖像化的理解方式,可以使用 GridHelper 來顯示網格, 這樣一來調整 3D 物件時也會直觀許多。

<template>
  <TresCanvas window-size clear-color="#C0F7A4">
    <TresMesh :position="[0, 0, 0]">
      <TresBoxGeometry :args="[1, 2, 3]" />
      <TresMeshBasicMaterial color="#7fc3ff" />
    </TresMesh>
    <TresGridHelper />
  </TresCanvas>
</template>

使用 GridHelper

攝影機軌道移動

能展示一個 3D 物件後,如果希望使用者還能移動相機,從各個角度來觀看 3D 物件的話該怎麼做呢?

這邊會需要使用 Three.js 中的 OrbitControls,簡單來說就是一個攝影機控制器,可以圍繞著物件旋轉。

雖然 TresJS 幫我們簡化很多 Three.js 的實作,但在 TresJS 中實作 OrbitControls 也是有點複雜的,以下是 TresJS 文件中提供的實作方式:

<script setup lang="ts">
import { extend, useTres } from "@tresjs/core";
import { OrbitControls } from "three/addons/controls/OrbitControls";
extend({ OrbitControls });
const { state } = useTres();
</script>
<template>
  <TresCanvas shadows alpha>
    <TresPerspectiveCamera :args="[45, 1, 0.1, 2000]" />
    <TresOrbitControls v-if="state.renderer" :args="[state.camera, state.renderer?.domElement]" />
  </TresCanvas>
</template>

但還記得我們一開始有安裝一個套件叫做 Cientos 嗎?在 Cientos 中已經幫我們處理好了,我們只需要添加 <OrbitControls> 即可:

<script setup lang="ts">
import { OrbitControls } from "@tresjs/cientos";
</script>
<template>
  <TresCanvas shadows alpha>
    <TresPerspectiveCamera :args="[45, 1, 0.1, 2000]" />
    <OrbitControls />
  </TresCanvas>
</template>

就可以直接任意移動、拖曳、放大視角了,簡單吧!

OrbitControls 旋轉功能

不過因為 <OrbitControls> 預設會以 [0, 0 ,0] 坐標為中心,建議使用時也把目標物件放在 [0, 0, 0,],這樣就不會有旋轉過程中物件超出畫面,還要先拖曳畫面到中間,體驗會比較好。

OrbitControls 旋轉問題

另外除了 <OrbitControls>,還有另一個更進階的 CameraControls,能更詳細的調整相機旋轉的操作效果,有興趣的人可以參考 cientos 文件

匯入模型和 3D 物體

上面我們使用的都是 Three.js 內建的 3D 物體,如果我們想用自己的 3D 模型,就需要將模型匯入進來。

如果沒有自己的模型但是想嘗試,可以到 Sketchfab 這個網站;Sketchfab 上可以讓使用者分享或販售自己的 3D 模型,登入後我們就可以在這裡找免費的模型,這邊我會使用這個 MacBook Air M2 的模型。

MacBook Air M2 模型

TresJS 目前只提供 GLTFFBX 兩種格式可以匯入。

最簡單的方法是使用 Cientos 提供的 GLTFModelFBXModel,並且可以使用 draco 來壓縮模型大小以提高效能:

<script setup lang="ts">
import { GLTFModel, FBXModel } from "@tresjs/cientos";
</script>
<template>
  <TresCanvas clear-color="#82DBC5">
    <TresPerspectiveCamera :position="[0, 0, 10]" />
    <OrbitControls />
    <Suspense>
      <!-- 匯入 GLTF 模型 -->
      <GLTFModel path="/models/macbook_air.gltf" draco />
      <!-- 匯入 FBX 模型 -->
      <GLTFModel path="/models/macbook_air.fbx" draco />
    </Suspense>
  </TresCanvas>
</template>

這樣模型就成功匯入進來了~

匯入模型

如果需要在匯入時做更進階的調整,可以使用 Cientos 提供的 useGLTF,會稍微複雜一些,這邊就不另外說明。

調整模型的光線

匯入模型後,會發現怎麼模型整個都是黑的,這是因為我們沒有幫場景添加光線。

像是前面使用到的 <MeshBasicMaterial> 不會有這個問題,是因為這個材質本身不需要光線;如果使用需要光線的材質,或是像我們使用自己模型的材質通常都沒有處理光線,整個模型就會是黑的。

這邊我們可以添加 DirectionalLight,DirectionalLight 會往目標打出一個平行的光,position 設定的是照射目標的位置,這個方向光是不能調整角度的

假如我先將模型設定在 [0, 0, 0] 的位置,並使用 <TresDirectionalLight>[0, 0, 1] 的位置打一盞光:

<script setup lang="ts">
import { GLTFModel, FBXModel } from "@tresjs/cientos";
</script>
<template>
  <TresCanvas clear-color="#82DBC5">
    <TresPerspectiveCamera :position="[0, 0, 10]" />
    <!-- 使用 DirectionalLight -->
    <TresDirectionalLight :position="[0, 0, 1]" />
    <OrbitControls />
    <Suspense>
      <GLTFModel path="/models/macbook_air.gltf" :position="[0, 0, 0]" draco />
    </Suspense>
  </TresCanvas>
</template>

DirectionalLight 效果

稍微調整一下角度可以發現,因為是往 z=1 的方向打一盞平行光,所以鍵盤上是沒有光線的:

DirectionalLight 照射模型範圍

我們可以使用 intensity 來增加亮度,這樣一來光線影響的範圍就會變大,然後將照射的目標高度增加,這樣一來光線就能照到鍵盤上:

<script setup lang="ts">
import { GLTFModel, FBXModel } from "@tresjs/cientos";
</script>
<template>
  <TresCanvas clear-color="#82DBC5">
    <TresPerspectiveCamera :position="[0, 0, 10]" />
    <!-- 調整 y=1 並且增加 intensity 為 10 -->
    <TresDirectionalLight :position="[0, 1, 1]" :intensity="10" />
    <OrbitControls />
    <Suspense>
      <GLTFModel path="/models/macbook_air.gltf" :position="[0, 0, 0]" draco />
    </Suspense>
  </TresCanvas>
</template>

這樣看起來就比較正常了~

模型調整照射後

但旋轉視角後會發現,模型的背面沒有照射到光:

模型背面照射情形

如果希望整個模型看起來都是亮的,這邊有兩種簡單的解法:

  • 另一個方向再打一盞 DirectionalLight。
  • 使用 AmbientLight

AmbientLight 是環境光的概念,可以均勻的照亮場景中的所有物件:

<script setup lang="ts">
import { GLTFModel, FBXModel } from "@tresjs/cientos";
</script>
<template>
  <TresCanvas clear-color="#82DBC5">
    <TresPerspectiveCamera :position="[0, 0, 10]" />
    <TresAmbientLight :intensity="10" />
    <OrbitControls />
    <Suspense>
      <GLTFModel path="/models/macbook_air.gltf" :position="[0, 0, 0]" draco />
    </Suspense>
  </TresCanvas>
</template>

這樣一來背面的部分也會受到光線照射:

模型背面照射效果

還有其他各種不同的光線,可以參考 Three.js 的文件

模型的陰影效果

雖然模型已經看起來不錯了,但感覺少了一點空間的立體感,我們可以透過 <ContactShadows> 來幫物件之間的接觸區塊增加陰影:

<script setup lang="ts">
  import { GLTFModel, FBXModel } from '@tresjs/cientos'
</script>
<template>
  <TresCanvas clear-color="#82DBC5">
    <TresPerspectiveCamera :position="[0, 0, 10]" />
    <TresAmbientLight :intensity ="10" />
    <OrbitControls />
    <Suspense>
      <GLTFModel path="/models/macbook_air.gltf" :position="[0, 0, 0]" draco />
    </Suspense>
    <!-- 讓 3D 物件跟地板之間有陰影效果 -->
    <ContactShadows :blur="0.5" :resolution="512" :opacity="0.2">
  </TresCanvas>
</template>

使用 ContactShadows 陰影效果

此外可以添加 bluropacity 來讓陰影效果合理一點:

  • blur:陰影模糊的範圍。
  • opacity:陰影的透明度。
<script setup lang="ts">
  import { GLTFModel, FBXModel } from '@tresjs/cientos'
</script>
<template>
  <TresCanvas clear-color="#82DBC5">
    <TresPerspectiveCamera :position="[0, 0, 10]" />
    <TresAmbientLight :intensity ="10" />
    <OrbitControls />
    <Suspense>
      <GLTFModel path="/models/macbook_air.gltf" :position="[0, 0, 0]" draco />
    </Suspense>
    <!-- 調整陰影效果 -->
    <ContactShadows :blur="0.2" :opacity="0.5">
  </TresCanvas>
</template>

調整後的模型陰影效果

結語

這篇關注在 TresJS 提供的易用寫法,雖然篇幅稍微有點長,但可以發現 TresJS 讓我們幾乎不用寫太攏長的程式碼就能完成許多核心的 3D 功能,讓製作一個基礎 3D 互動效果的難度降低不少,而其他更進階的功能一樣可以藉由引入 Three.js 來達成。

不過 TresJS 畢竟算比較新的套件,目前文件內容和範例比較少,有些進階功能會需要挖掘 Source Code,如果要應用在比較龐大的 3D 互動專案,可能需要根據需求先研究一下。

但如果想製作的 3D 效果很簡單,TresJS 可以幫你免去了許多宣告 Three Object 的繁雜過程,直接幫你包裝好,那麼香不用看看嗎?