iT邦幫忙

2026 iThome 鐵人賽

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

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

Day 17. Raycasting: 判斷滑鼠點到了什麼

  • 分享至 

  • xImage
  •  

視窗縮放處理好了,今天要做找彩蛋的最核心技術:怎麼知道玩家點的是不是藏起來的彩蛋。
今天先研究 Raycasting 並只先做出點一下畫面,Console 印出點到的資訊。

Raycasting 是什麼

官方文件的說明:

Raycasting is used for mouse picking (working out what objects in the 3d space the mouse is over) amongst other things.

意思:被用於滑鼠選取(也就是算出滑鼠目前所在位置,對應到 3D 空間裡的哪個物體),以及其他用途。

比喻: 想像拿一支雷射筆,站在螢幕前面,對著畫面上某一點射出去。雷射會穿過螢幕,射進你佈置好的 3D 場景裡,打中它第一個碰到的物體。Raycasting 做的就是這件事,只是用數學算出這條線會先打中誰,不是真的有一道光。

滑鼠座標要先「翻譯」一次

滑鼠在螢幕上的座標,瀏覽器給的是像素,例如 (960, 540),左上角是 (0, 0)。但 Three.js 的 Raycaster 要的不是這個,依官方文件對 setFromCamera 的說明:

2D coordinates of the mouse, in normalized device coordinates (NDC)---X and Y components should be between -1 and 1.

這叫 NDC(標準化裝置座標),範圍是 -1 到 1,而且原點在畫面正中央,往右上是正的,往左下是負的。

換算公式:

pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

換算講解
NDC 的 X、Y 範圍是 -1 到 1,原點 0 在正中央。
而瀏覽器像素座標是 0 ~ window.innerWidth, 0 ~ window.innerHeight

X 軸:
我們先做正規化 (x / window.innerWidth) → 讓範圍變成 0 ~ 1
接著 * 2 - 1 達到 -1 ~ 1,也就是 Three.js 的 Vector2 範圍

Y 軸不能直接套用同一個公式,因為方向相反:
像素座標的 Y 愈往下數字愈大(最上面是 0),但 NDC 的 Y 愈往上數值愈大(上方是正值)。兩者方向相反,所以做完正規化 (y / window.innerHeight) 後,要先在前面乘上 -1 把方向反過來,寫法就變成 -(y / window.innerHeight) * 2 + 1,而不是 (y / window.innerHeight) * 2 - 1。

用圖示理解這個轉換:

瀏覽器的像素座標(左上角是原點)        Raycaster 要的 NDC(正中央是原點)
(0,0)                 (1920,0)        (-1, 1)              (1, 1)
┌─────────────────────────┐           ┌─────────────────────────┐
│                         │           │                         │
│         ●(960,540)      │   ──→     │          ●(0, 0)        │
│                         │           │                         │
└─────────────────────────┘           └─────────────────────────┘
(0,1080)              (1920,1080)     (-1,-1)              (1,-1)

為什麼 Y 的公式前面要加負號: 瀏覽器的像素座標,Y 愈往下數字愈大;NDC 剛好相反,Y 愈往上數值愈大。所以公式裡故意加一個負號,把方向反過來。

完整程式碼

import * as THREE from 'three';
// ...(Day16 原本的 scene、camera、renderer、燈光、controls、GLTFLoader、resize 都不動)

// 以下是今天新加的部分
const raycaster = new THREE.Raycaster();
const coords  = new THREE.Vector2();

window.addEventListener('click', (event) => {
  // 1. 把滑鼠的像素座標,換算成 NDC
  coords .x = (event.clientX / window.innerWidth) * 2 - 1;
  coords .y = -(event.clientY / window.innerHeight) * 2 + 1;

  // 2. 從相機、往滑鼠指的方向,射出這條線
  raycaster.setFromCamera(coords , camera);

  // 3. 問這條線,打中了 scene 裡的哪些東西(true 代表連子物件也要檢查)
  const intersects = raycaster.intersectObjects(scene.children, true);

  if (intersects.length > 0) {
    console.log('點到了:', intersects[0].object.name, intersects[0].object);
  } else {
    console.log('沒點到任何東西');
  }
});

逐行對照官方文件:

  • new THREE.Raycaster():建立一個 Raycaster。依官方文件,建構子可以帶入起點、方向等參數,但這裡用 setFromCamera 會自動幫忙設定,所以不用自己填。

  • raycaster.setFromCamera(coords, camera):用相機的位置、加上滑鼠指的方向,算出射線該怎麼射。不過 setFromCamera 的 coords 是一個 Vector2,所以在使用前需要產生 Vector2 物件,也就是例子中的 coords. 計算方式前面在介紹 NDC 時已說明,這裡就不再贅述

  • raycaster.intersectObjects(scene.children, true):依官方文件,這個方法會回傳一個陣列,列出射線打中的所有物體,依距離近到遠排序。第二個參數 recursive 設 true,代表連 gltf.scene 底下包著的子物件(Mesh)也要一起檢查,不然只會檢查到最外層那個 Group,檢查不到裡面真正的模型。

  • intersects[0]:排序後的第一個,就是離相機最近、第一個被打中的物體,這正是「點到了什麼」的答案。

為什麼用 click 不是滑鼠移動

官方範例常常用 mousemove(滑鼠移動時就一直檢查),適合「滑鼠滑過去就要變色」這種效果。找威利遊戲是「點擊才算數」,所以這裡選 click 事件,玩家按一下才去判斷,不用每次滑鼠移動都算一次(場景物件越多,每次都算會越耗效能,click 比 mousemove 省資源)。

動手試

打開瀏覽器,隨便點公園模型上的幾個地方,看 Console 印出什麼。
https://ithelp.ithome.com.tw/upload/images/20261001/20112674W8HPMxYCvg.png
可以看到 Mesh 物件、uuid,以及很奇怪的名字,把資訊展開如下
https://ithelp.ithome.com.tw/upload/images/20261001/20112674ILt2WmnFDB.png

模型裡的物件名稱是 Blender 匯出時自動產生的,不一定有意義。這沒關係,今天只求「能抓到有沒有點中東西」,下一步才是「怎麼認出點到的是彩蛋」。

已經能抓到「點到了什麼」,接下來要讓程式認出「點到的剛好是彩蛋」,點中時跳出通知訊息,預計明天就能把整個流程都打通起來,之後再花時間調整輸出模型的完整度。

參考連結


上一篇
Day16. 視窗縮放: 拉動瀏覽器視窗,畫面不跑掉
系列文
從空拍到3D展示 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言