ThreeJS——交互事件原理

153 阅读3分钟

检测二维状态是否交互

判断目标的屏幕位置与鼠标的屏幕坐标是否重合

检测三维状态是否交互

从摄像机位置发出射线,转换鼠标的屏幕坐标为标准化设备坐标(NDC),判断射线是否穿过三维目标

屏幕坐标转换NDC原理

在 Three.js 中,将鼠标的屏幕坐标转换为 标准化设备坐标(NDC,Normalized Device Coordinates)  是一个常见的操作,用于与 3D 场景中的对象进行交互(如点击、悬停检测)。以下是转换的原理和步骤:

1. 屏幕坐标(Screen Coordinates)

  • 屏幕坐标是指鼠标在浏览器窗口中的位置,通常以像素为单位。
  • 原点 (0, 0) 位于窗口的左上角,x 轴向右,y 轴向下。
  • 示例:如果鼠标位于窗口的中心,屏幕坐标可能是 (window.innerWidth / 2, window.innerHeight / 2)

2. 标准化设备坐标(NDC)

  • NDC 是一个标准化的坐标系,范围从 -1 到 1
  • 原点 (0, 0) 位于画布的中心,x 轴向右,y 轴向上。
  • NDC 是 Three.js 中 Raycaster 使用的坐标系。

3. 转换原理

将屏幕坐标转换为 NDC 的公式如下:

NDC.x = (屏幕坐标.x / 窗口宽度) * 2 - 1
NDC.y = -(屏幕坐标.y / 窗口高度) * 2 + 1
公式解析:
  1. 水平方向(x 轴)

    • 屏幕坐标.x / 窗口宽度:将屏幕坐标归一化到 [0, 1] 范围。
    • * 2 - 1:将范围从 [0, 1] 转换为 [-1, 1]
  2. 垂直方向(y 轴)

    • 屏幕坐标.y / 窗口高度:将屏幕坐标归一化到 [0, 1] 范围。
    • * 2 - 1:将范围从 [0, 1] 转换为 [-1, 1]
    • -:反转 y 轴方向(因为屏幕坐标的 y 轴向下,而 NDC 的 y 轴向上)。

4. 代码实现

以下是将鼠标屏幕坐标转换为 NDC 的代码示例

// 获取鼠标的屏幕坐标
const mouse = new THREE.Vector2();

function onMouseMove(event) {
    // 1. 获取鼠标在屏幕上的位置
    const x = event.clientX;
    const y = event.clientY;

    // 2. 将屏幕坐标转换为 NDC
    mouse.x = (x / window.innerWidth) * 2 - 1;
    mouse.y = -(y / window.innerHeight) * 2 + 1;

    console.log('NDC:', mouse);
}

// 监听鼠标移动事件
window.addEventListener('mousemove', onMouseMove);

5. 示例

假设:

  • 窗口宽度为 800px,高度为 600px
  • 鼠标位于屏幕中心,屏幕坐标为 (400, 300)

转换过程:

  1. 水平方向(x 轴)

    NDC.x = (400 / 800) * 2 - 1 = 0.5 * 2 - 1 = 0
    
  2. 垂直方向(y 轴)

    NDC.y = -(300 / 600) * 2 + 1 = -0.5 * 2 + 1 = 0
    

结果:

  • NDC 坐标为 (0, 0),即画布的中心。

6. 应用场景

  • Raycaster 交互:将鼠标的 NDC 坐标传递给 Raycaster,用于检测与 3D 对象的交互。
  • 自定义交互:根据 NDC 坐标实现自定义的交互逻辑(如点击、拖拽)。

7. 注意事项

  • 窗口大小变化:如果窗口大小发生变化,需要重新计算 NDC 坐标。
  • 画布偏移:如果画布不是全屏的,需要考虑画布的偏移量。

三维状态产生交互案例

鼠标移动事件

  1. 获取鼠标位置
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) return;

mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
  1. 射线检测
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(bars);
  1. 添加交互事件(重置和高亮)
// 重置所有柱子的透明度
bars.forEach((bar) => {
    (bar.material as THREE.MeshPhongMaterial).opacity = 1;
});

if (intersects.length > 0) {
    // 设置悬停柱子的透明度
    const hoveredBar = intersects[0].object as THREE.Mesh;
    (hoveredBar.material as THREE.MeshPhongMaterial).opacity = 0.7;
  1. 完整样例
 //鼠标移动
       const onMouseMove = (event: MouseEvent) => {
           const rect = containerRef.current?.getBoundingClientRect();
           if (!rect) return;

           mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
           mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

           raycaster.setFromCamera(mouse, camera);
           const intersects = raycaster.intersectObjects(bars);

           // 重置所有柱子的透明度
           bars.forEach((bar) => {
               (bar.material as THREE.MeshPhongMaterial).opacity = 1;
           });

           if (intersects.length > 0) {
               // 设置悬停柱子的透明度
               const hoveredBar = intersects[0].object as THREE.Mesh;
               (hoveredBar.material as THREE.MeshPhongMaterial).opacity = 0.7;

               // 更新tooltip位置和内容
               tooltip.style.display = 'block';
               tooltip.style.left = `${event.clientX}px`;
               tooltip.style.top = `${event.clientY - 40}px`;
               const data = hoveredBar.userData;
               tooltip.textContent = `Value: ${data.value}`;
           } else {
               tooltip.style.display = 'none';
           }
       };