目 录CONTENT

文章目录

光线投射Raycaster实现3d场景交互事件

Hello!你好!我是村望~!
2024-02-20 / 0 评论 / 0 点赞 / 668 阅读 / 1,228 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

光线投射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)

image-20240220135028310

下一步给画布添加点击事件,获取点击的屏幕空间坐标

页面视口坐标通常被称为“屏幕空间坐标”(Screen Space Coordinates)。

这个术语指的是屏幕上的像素坐标,通常以屏幕的左上角为原点,向右为正 X 轴方向,向下为正 Y 轴方向。

然后我们需要把屏幕空间坐标转换为Three.js中的标准设备坐标(NDC)。

在Three.js中,标准设备坐标(Normalized Device Coordinates,NDC)是一种坐标系统,用于表示一个三维点相对于视口的相对位置,并且其范围是标准化的,通常在每个维度上都位于 [-1, 1] 的范围内。

在Three.js中,NDC通常是在渲染过程中使用的一种坐标空间。在投影变换之后,物体的顶点坐标会被转换为NDC坐标,然后被裁剪和投影到屏幕上。这样做的好处是可以保证在各种不同分辨率和屏幕尺寸的设备上,物体在屏幕上的位置和大小是一致的。

image-20240220164323863

其实计算思路就是计算点击的百分比在[ -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)
}

iShot_2024-02-20_17.27.23

完整代码:

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

0

评论区