iT邦幫忙

2026 iThome 鐵人賽

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

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

Day16. 視窗縮放: 拉動瀏覽器視窗,畫面不跑掉

  • 分享至 

  • xImage
  •  

昨天把 TrackballControls 加上去,畫面能拖曳旋轉、縮放,但還留了一個問題:拉動瀏覽器視窗大小時,瀏覽器上會出現捲軸,公園模型會隨著視窗變小,而跑位置,需要透過捲軸才能看到模型,並不是很方便,如圖
https://ithelp.ithome.com.tw/upload/images/20260930/20112674L0T3XHIJac.png

瀏覽器在視窗大小改變時,會發出一個叫 resize 的通知。
我們可以利用這個通知,來執行想要的事情,在 JS 裡面是 「聽事件」

window.addEventListener('resize', () => {
  // 視窗改變時要做的事
});

Resize event

依據:MDN 的 Window: resize event 文件說明,這個事件會在文件視圖(document view)被調整大小時觸發。

視窗變了,要更新三件事

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  controls.handleResize();
});
  1. camera.aspect:視窗縮放沒有正確更新相機和畫布時,可能出現的狀況包括畫面出現捲軸、物體像被裁切。
  2. camera.updateProjectionMatrix():同一份文件說明,修改相機任何參數後必須呼叫這個方法讓修改生效,只改 aspect 不會自動套用。
  3. renderer.setSize(...):依 WebGLRenderer 官方文件,設定 renderer 的輸出畫布大小。沒呼叫,畫布停留在原本尺寸。
  4. controls.handleResize():官方說視窗縮放時應該呼叫,用途是讓內部的 screen 屬性(用來換算滑鼠拖曳距離的比例尺)跟著更新。但我實際測試,拿掉這行也感覺不出操作手感有差異,可能是因為這次測試的視窗縮放幅度不夠極端,效應不明顯。保留這行是因為官方文件明確建議這麼做,即使目前感覺不出差別。

調整完後可以看到,視窗大小被調整後,也不會如前面出現捲軸,導致模型畫面被切掉;而是模型大小因此跟著調整,不會需要移動捲軸才能看到模型
https://ithelp.ithome.com.tw/upload/images/20260930/20112674uYhkPVoTXt.png

https://ithelp.ithome.com.tw/upload/images/20260930/20112674YxUUwixiUj.png

完整程式碼

接在昨天的版本後面,標明新加的部分
// ★ 標記的是今天新加的,其他是昨天已有的:

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.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);

const controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 3.0;
controls.zoomSpeed = 2.5;
controls.panSpeed = 1.0;
controls.staticMoving = true;

const loader = new GLTFLoader();
loader.load(
  'blender_output.glb',
  (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());
    const maxDim = Math.max(size.x, size.y, size.z);

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

// ★ 今天新加的部分:視窗大小改變時,讓相機和畫布跟著更新
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;   // ★ 1. 重算長寬比
  camera.updateProjectionMatrix();                            // ★    並讓相機套用這個改變
  renderer.setSize(window.innerWidth, window.innerHeight);   // ★ 2. 畫布跟著撐滿或縮小
  controls.handleResize();                                    // ★ 3. TrackballControls 需要另外呼叫這行
});

為什麼放在檔案最後面: 這四行不依賴模型有沒有載入完成,camera、renderer、controls 一開始就建立好了,所以放在檔案最外層,不用放進 loader.load 的 callback(跟 Day14 的原則一致:只有依賴 gltf 或 center 的動作才要放進去)。

補充:如果 resize 觸發太頻繁

MDN 的 resize 事件文件提醒:如果 resize 事件觸發得太頻繁,需要控制事件觸發後實際執行的時間間隔(提到 debounce/節流的處理方式)。這次的運算量不大,先不處理,只記下來,等 Raycasting 判定範圍也要跟著更新時再回頭考慮。

接下來

視窗縮放處理好了,接下來要進入找威利遊戲最核心的技術:Raycasting,判斷滑鼠點擊的位置有沒有剛好點中藏起來的彩蛋。


上一篇
Day15. Three.js 之燈光與控制器
下一篇
Day 17. Raycasting: 判斷滑鼠點到了什麼
系列文
從空拍到3D展示 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言