昨天把獅山公園載進 Three.js,看得到模型了,但有兩個問題:
今天就來解決這兩件事,並弄清楚為什麼昨天的方塊不需要燈光,今天的模型卻需要。
昨天的方塊用的是 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));
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 官方在 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>
模型載入後畫面不動,是因為沒有寫任何互動。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);
});
我一開始想沿 X 軸翻轉模型,卻發現 OrbitControls 只能延z軸轉動和整個水平面 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; // 關掉慣性,放開滑鼠就停
官方文件的摘錄沒有明說,我用實驗確認:
controls.update() 註解掉,拖曳畫面。沒反應所以代表需要每幀呼叫。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 的研究其實非常多,還需要再一篇,才能把模型載入相關的知識補齊
包含如果視窗大小改變了,模型是否也能跟著變換? 以及使用者點擊畫面中的某一個點,要判斷這個點擊區域是不是某個物件 (彩蛋判斷)