光线投射Raycaster实现3d场景交互事件
原理就是从相机到你鼠标点击的位置发射一根射线,被这根射线射中的物体都被记录下来。
首先准备一些物体,用来和投射产生交互
// 创建一个组
const spherArr = new THREE.Group()
const colors = [0xffffff, 0xff0000, 0x00ff00]
for (let i = 0; i < 3; i++) {
// 循环往组中添加物体
const radius = 2
const spherGeometry = new THREE.SphereGeometry(radius, 32, 32)
const sphereMaterial = new THREE.MeshBasicMaterial({ color: colors[i] })
const spherMesh = new THREE.Mesh(spherGeometry, sphereMaterial)
spherMesh.position.x = i * radius * 3
spherArr.add(spherMesh)
}
scene.add(spherArr)
下一步给画布添加点击事件,获取点击的屏幕空间坐标
页面视口坐标通常被称为“屏幕空间坐标”(Screen Space Coordinates)。
这个术语指的是屏幕上的像素坐标,通常以屏幕的左上角为原点,向右为正 X 轴方向,向下为正 Y 轴方向。
然后我们需要把屏幕空间坐标转换为Three.js中的标准设备坐标(NDC)。
在Three.js中,标准设备坐标(Normalized Device Coordinates,NDC)是一种坐标系统,用于表示一个三维点相对于视口的相对位置,并且其范围是标准化的,通常在每个维度上都位于 [-1, 1] 的范围内。
在Three.js中,NDC通常是在渲染过程中使用的一种坐标空间。在投影变换之后,物体的顶点坐标会被转换为NDC坐标,然后被裁剪和投影到屏幕上。这样做的好处是可以保证在各种不同分辨率和屏幕尺寸的设备上,物体在屏幕上的位置和大小是一致的。
其实计算思路就是计算点击的百分比在[ -1,1 ]区间 的值
点击位置的屏幕坐标/屏幕视口宽度(高度)得到一个百分比的值(0-1 之间)然后乘 2减一就得到了-1,1 的结果!
renderer.domElement.addEventListener('click', e => {
const screenX = e.clientX
const screenY = e.clientY
const ndcX = (screenX / w) * 2 - 1
// 屏幕空间坐标系 Y 轴 向下越来越大 但是 Threejs 标准坐标系和屏幕空间坐标系 Y 相反!所以要反向百分比
const ndcY = 1 - (screenY / h) * 2
})
那么这样我们就完成了,点击位置的屏幕空间坐标转换为 Threejs 的标准坐标系!那么在 Threejs 的坐标系就知道点击了哪里的点!
我们来存储这个坐标!
const ndcPointer = new THREE.Vector2()
renderer.domElement.addEventListener('click', e => {
const screenX = e.clientX
const screenY = e.clientY
const ndcX = (screenX / w) * 2 - 1
const ndcY = 1 - (screenY / h) * 2
ndcPointer.x = ndcX
ndcPointer.y = ndcY
})
然后就用 Raycaster 让camera 和这个点之间确定一条射线!
const raycaster = new THREE.Raycaster();
// 然后就用 Raycaster 让camera 和这个点之间确定一条射线!
raycaster.setFromCamera(ndcPointer, camera)
// 射线照射的对象集!只监听我们那三个球
const aim = raycaster.intersectObjects(spherArr.children)
for (let i = 0; i < aim.length; i++) {
const item = aim[i]!.object as THREE.Mesh
// 自定义是否选择 选择过则恢复原来的颜色
if (item._selected) {
item.material.color.set(item._originColor)
item._selected = false
return
}
// 没选择过就设置为蓝色
const originColor = item.material.color.getHex()
item._originColor = originColor
item._selected = true
item.material.color.set(0x006aff)
}
完整代码:
import { useEffect } from 'react'
import { PerspectiveCamera } from 'three'
import * as THREE from 'three'
import { AxesHelper } from 'three'
import { OrbitControls } from 'three/examples/jsm/Addons.js'
const RaycasterDemoPage = () => {
useEffect(() => {
const w = window.innerWidth
const h = window.innerHeight
// 场景
const scene = new THREE.Scene()
// 相机
const camera = new PerspectiveCamera(45, w / h, 1, 1000)
camera.position.set(20, 20, 20)
camera.lookAt(0, 0, 0)
// 渲染器
const renderer = new THREE.WebGLRenderer()
renderer.setSize(w, h)
document.body.appendChild(renderer.domElement)
// 辅助器
const axesHelper = new AxesHelper(10)
scene.add(axesHelper)
// 控制器
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.1
// 创建一个组
const spherArr = new THREE.Group()
const colors = [0xffffff, 0xff0000, 0x00ff00]
for (let i = 0; i < 3; i++) {
// 循环往组中添加物体
const radius = 2
const spherGeometry = new THREE.SphereGeometry(radius, 32, 32)
const sphereMaterial = new THREE.MeshBasicMaterial({ color: colors[i] })
const spherMesh = new THREE.Mesh(spherGeometry, sphereMaterial)
spherMesh.position.x = i * radius * 3
spherArr.add(spherMesh)
}
scene.add(spherArr)
const raycaster = new THREE.Raycaster()
const ndcPointer = new THREE.Vector2()
renderer.domElement.addEventListener('click', e => {
const screenX = e.clientX
const screenY = e.clientY
const ndcX = (screenX / w) * 2 - 1
const ndcY = 1 - (screenY / h) * 2 // 屏幕坐标系 y轴方向和 Threejs 标准坐标系方向相反
ndcPointer.x = ndcX
ndcPointer.y = ndcY
// 然后就用 Raycaster 让camera 和这个点之间确定一条射线!
raycaster.setFromCamera(ndcPointer, camera)
// 射线照射的对象集!
const aim = raycaster.intersectObjects(spherArr.children)
for (let i = 0; i < aim.length; i++) {
const item = aim[i]!.object as THREE.Mesh
if (item._selected) {
item.material.color.set(item._originColor)
item._selected = false
return
}
const originColor = item.material.color.getHex()
item._originColor = originColor
item._selected = true
item.material.color.set(0x006aff)
}
})
// 动画帧渲染函数
const runRender = () => {
renderer.render(scene, camera)
camera.matrixWorldAutoUpdate = true
controls.update()
requestAnimationFrame(runRender)
}
runRender()
return () => {
document.body.removeChild(renderer.domElement)
}
}, [])
return <></>
}
export default RaycasterDemoPage
评论区