检测二维状态是否交互
判断目标的屏幕位置与鼠标的屏幕坐标是否重合
检测三维状态是否交互
从摄像机位置发出射线,转换鼠标的屏幕坐标为标准化设备坐标(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
公式解析:
-
水平方向(x 轴) :
屏幕坐标.x / 窗口宽度:将屏幕坐标归一化到[0, 1]范围。* 2 - 1:将范围从[0, 1]转换为[-1, 1]。
-
垂直方向(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)。
转换过程:
-
水平方向(x 轴) :
NDC.x = (400 / 800) * 2 - 1 = 0.5 * 2 - 1 = 0 -
垂直方向(y 轴) :
NDC.y = -(300 / 600) * 2 + 1 = -0.5 * 2 + 1 = 0
结果:
- NDC 坐标为
(0, 0),即画布的中心。
6. 应用场景
- Raycaster 交互:将鼠标的 NDC 坐标传递给
Raycaster,用于检测与 3D 对象的交互。 - 自定义交互:根据 NDC 坐标实现自定义的交互逻辑(如点击、拖拽)。
7. 注意事项
- 窗口大小变化:如果窗口大小发生变化,需要重新计算 NDC 坐标。
- 画布偏移:如果画布不是全屏的,需要考虑画布的偏移量。
三维状态产生交互案例
鼠标移动事件
- 获取鼠标位置
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;
- 完整样例
//鼠标移动
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';
}
};