iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
佛心分享-IT 人自學之術

從空拍到3D展示系列 第 15 篇

Day15. Three.js 之燈光與控制器

  • 分享至 

  • xImage
  •  

昨天把獅山公園載進 Three.js,看得到模型了,但有兩個問題:

  1. 顏色看起來偏暗
  2. 畫面是靜止的,沒辦法拖曳旋轉

今天就來解決這兩件事,並弄清楚為什麼昨天的方塊不需要燈光,今天的模型卻需要。

一、為什麼今天的模型需要燈光?

昨天的方塊用的是 MeshBasicMaterial,官方文件有說明:這種材質不受燈光影響,所以沒有燈也看得到綠色。

而昨天載入的模型,透過 glTF 走的是 PBR(物理式繪製)材質流程,Three.js 官方論壇維護者的說法是,最常見的是 MeshStandardMaterial,這種材質需要光源才看得到,沒有燈就是一片黑。
加上以下程式碼,在載入成功的 callback 內,以確認自己的模型是什麼材質:

gltf.scene.traverse((o) => { if (o.isMesh) console.log(o.material.type); });

或是把與燈光有關的程式碼註解掉,畫面會一片黑

scene.add(new THREE.AmbientLight(0xffffff, 1));

AmbientLight 與 DirectionalLight

scene.add(new THREE.AmbientLight(0xffffff, 0.6));

const sun = new THREE.DirectionalLight(0xffffff, 0.8);
sun.position.set(5, 10, 5);
scene.add(sun);
燈光 作用 比喻
AmbientLight(環境光) 均勻照亮所有物體,沒有方向,不會產生陰影 陰天的天光,到處一樣亮
DirectionalLight(方向光) 光線平行、來自特定方向,官方教學說常用來代表太陽 太陽光,有明暗

官方教學提到,環境光單獨用會看起來很平,作用主要是避免暗部太黑;方向光則讓模型有立體感。所以兩個搭配使用。這是最常見的入門組合,我選它們是因為簡單,不是唯一解。

0.6、0.8 是光的強度,只是起點,沒有標準答案,要邊看畫面邊調。方向光只看方向(從 position 指向原點),不看距離,所以模型再大也不影響。


二、顏色怪怪的?可能跟 Three.js 版本有關

我第一次看到畫面時,顏色偏暗、對比很強。

[待補:顏色偏暗的截圖]

我查到 Three.js 官方在 r152 更新了色彩處理:輸出色彩空間預設改成 sRGB,色彩管理也預設開啟。r152 就是 three@0.152.0,版本名稱在 GitHub 上叫 r152,在 npm 和 CDN 網址上叫 0.152.0。

Day13 的 importmap 用的是 0.149.0,比 r152 舊,我因此在 html import 0.160.0 版本的 three, three/addons

<body>
    <script type="importmap">
    {
        "imports": {
            "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
            "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
        }
    }
    </script>
    <script type="module" src="model_1st_fail.js"></script>

</body>

三、讓畫面能動:OrbitControls

模型載入後畫面不動,是因為沒有寫任何互動。Three.js 官方有現成的 OrbitControls:左鍵拖曳旋轉、滾輪縮放、右鍵拖曳平移。要加三處:

// 1. 檔案最上方 import
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 2. 建立 renderer 之後、載入模型之前,建立 controls
const controls = new OrbitControls(camera, renderer.domElement);

// 3. 模型載入成功的 callback 裡,讓旋轉中心對準模型
controls.target.copy(center);
controls.update();
  • controls.target:相機繞著轉、看著的那個點。預設是原點,要改成模型的中心(center,昨天用 Box3 算出來的)。
  • 新手的坑:controls 要建在載入 callback 的外面。 我一開始把它宣告在 callback 裡面,動畫迴圈在外面,拿不到它,所以呼叫不到 controls.update()。
  • 官方文件對 update() 的說明:手動改過相機位置後要呼叫一次;只有開了 enableDamping(慣性)或 autoRotate(自動旋轉)時,才必須在動畫迴圈每一幀呼叫。這次我還是把它放進動畫迴圈,以後開這些功能就不用再改。
renderer.setAnimationLoop(() => {
  controls.update();          // 新增這行
  renderer.render(scene, camera);
});

四、OrbitControls 和 TrackballControls 有什麼不一樣?

我一開始想沿 X 軸翻轉模型,卻發現 OrbitControls 只能延z軸轉動和整個水平面 y 軸翻轉,沒辦法水平轉動,於是試了另一個控制器 TrackballControls。

關鍵差別:相機的「上方」

以地球儀為例:

  • OrbitControls:你繞著地球儀走,可以左右繞、抬頭低頭,但天空永遠在畫面上方。抬頭看到正上方就停住,畫面不會顛倒。這個「天空永遠朝上」的設定就是相機的上方(官方預設 Y 軸朝上)。
  • TrackballControls:像用手拿著地球儀隨意翻轉,沒有固定的上下,轉過頭頂後畫面會倒過來。官方文件的說法是:它不維持固定的相機上方向。

使用上的差別

OrbitControls TrackballControls
手感 繞著模型走,天空永遠在上 像手裡翻一顆球,想怎麼翻都行
畫面會不會顛倒 不會 會
視窗縮放時 不用另外處理 官方文件說要呼叫 handleResize()
自動旋轉 有 autoRotate 我查到的文件沒有這個屬性
預設速度 我實測拖曳比較順 官方預設 rotateSpeed 1.0、panSpeed 0.3、zoomSpeed 1.2,並有慣性(staticMoving 預設 false)

實測下來,TrackballControls 拖起來比 OrbitControls 慢。官方文件沒有說明為什麼預設手感較慢,只列出可調整的屬性:

import { TrackballControls } from 'three/addons/controls/TrackballControls.js';

const controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 4.0;      // 預設 1.0,數字越大轉越快
controls.panSpeed = 1.0;         // 預設 0.3
controls.zoomSpeed = 2.0;        // 預設 1.2
controls.staticMoving = true;    // 關掉慣性,放開滑鼠就停

我實際調成以下內容覺得順手

    controls.rotateSpeed = 3.0;     // 預設 1.0,從 3 到 5 試起
    controls.zoomSpeed = 2.5;       // 預設 1.2
    controls.panSpeed = 1.0;        // 預設 0.3
    controls.staticMoving = true;   // 關掉慣性,放開滑鼠就停

動手驗證:TrackballControls 要不要每幀 update?

官方文件的摘錄沒有明說,我用實驗確認:

  • 實驗 A:把動畫迴圈裡的 controls.update() 註解掉,拖曳畫面。沒反應所以代表需要每幀呼叫。
  • 實驗 B:加一行 console.log('autoRotate' in controls),印 false 就代表沒有autorotate這個屬性,所以才需要加上 control.supdate()。

我的選擇

最後決定用 TrackballControls,因為想要三個軸都能自由翻轉,方便從任何角度檢視、尋找彩蛋,不想被 OrbitControls 限制住某一個軸無法翻轉


最後的程式碼

以下是今天內容的所有程式碼,供各位參考

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
//import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TrackballControls } from 'three/addons/controls/TrackballControls.js';

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);

// 燈光:模型材質多半需要光源才看得到,先加上
scene.add(new THREE.AmbientLight(0xffffff, 1));
const sun = new THREE.DirectionalLight(0xffffff, 1);
sun.position.set(5, 10, 5);
scene.add(sun);

camera.position.set(0, 5, 10);

//const controls = new OrbitControls(camera, renderer.domElement);
const controls = new TrackballControls(camera, renderer.domElement);
console.log('autoRotate' in controls); // check if there's autoRotate in TrackballControls

const loader = new GLTFLoader();
loader.load(
  'blender_output.glb', // 放在跟 index.html 同一個資料夾
  (gltf) => {
    scene.add(gltf.scene);

    // 印出模型的大小與中心,判斷位置或比例有沒有問題
    const box = new THREE.Box3().setFromObject(gltf.scene);
    const center = box.getCenter(new THREE.Vector3());
    const size = box.getSize(new THREE.Vector3());


    console.log('size', size);
    console.log('center', center);

    const maxDim = Math.max(size.x, size.y, size.z);

    controls.rotateSpeed = 3.0;     // 預設 1.0,從 3 到 5 試起
    controls.zoomSpeed = 2.5;       // 預設 1.2
    controls.panSpeed = 1.0;        // 預設 0.3
    controls.staticMoving = true;   // 關掉慣性,放開滑鼠就停

    camera.position.set(center.x, center.y + maxDim, center.z + maxDim);
    camera.lookAt(center);
    camera.far = maxDim * 10;      // 模型很大時,避免遠處被裁掉
    camera.updateProjectionMatrix();

    controls.target.copy(center);
  },
  undefined,
  (error) => console.error('載入失敗', error)
);

renderer.setAnimationLoop(() => {
    controls.update();
  renderer.render(scene, camera);
});

接下來

有關 Three.js 的研究其實非常多,還需要再一篇,才能把模型載入相關的知識補齊
包含如果視窗大小改變了,模型是否也能跟著變換? 以及使用者點擊畫面中的某一個點,要判斷這個點擊區域是不是某個物件 (彩蛋判斷)


上一篇
Day 14. 拆解範例程式碼 + 把獅山公園載進 Three.js
下一篇
Day16. 視窗縮放: 拉動瀏覽器視窗,畫面不跑掉
系列文
從空拍到3D展示 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言