- A raycaster can cast a ray in a specific direction and test what objects intersect with it
-
Set up
<script setup>
import * as THREE from 'three'
import {OrbitControls} from 'three/addons/controls/OrbitControls.js'
/**
* scene
*/
const scene = new THREE.Scene()
/**
* object
*/
const object1 = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 16, 16),
new THREE.MeshBasicMaterial({ color: '#ff0000' })
)
object1.position.x = - 2
const object2 = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 16, 16),
new THREE.MeshBasicMaterial({ color: '#ff0000' })
)
const object3 = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 16, 16),
new THREE.MeshBasicMaterial({ color: '#ff0000' })
)
object3.position.x = 2
scene.add(object1, object2, object3)
/**
* camera
*/
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
100
)
camera.position.z = 3
/**
* renderer
*/
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})
/**
* axesHelper
*/
const axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)
/**
* control
*/
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
/**
* render
*/
const clock = new THREE.Clock()
const tick = () => {
const elapsedTime = clock.getElapsedTime()
// update
controls.update()
requestAnimationFrame(tick)
renderer.render(scene, camera)
}
tick()
</script>
-
Create the Raycaster
- if we're shooting a ray in a direction, we need to tell the origin of the ray and the direction of the ray, we can use
set()
to set them
- the direction has to be normalized
/**
* raycaster
*/
const raycaster = new THREE.Raycaster()
const rayOrigin = new THREE.Vector3(-3, 0, 0)
const rayDirection = new THREE.Vector3(10, 0, 0)
rayDirection.normalize() // 单位向量,保持方向
raycaster.set(rayOrigin, rayDirection)
-
Cast a Ray
-
intersectObject()
to test one object, its value also be an array, because a ray can go through the same object multiple times
-
intersectObjects()
to test an array of objects
- each object value of the array has some useful information:
- distance - distance between the origin of the ray and the collision point
- face - the face of the geometry that was hit by the ray
- faceIndex - the index of the face
- object - what object is concerned by the collision
- point - a Vector3 of the exact position of the collision
- uv - the uv coordinates in that geometry
const intersect = raycaster.intersectObject(object3)
console.log(intersect);
const intersects = raycaster.intersectObjects([object1, object2, object3])
console.log(intersects);
-
Test on each frame
- we're going to animate the spheres and turn them blue when the ray intersects with them
- remove the code we did previously and only keep the raycaster instantiation
/**
* raycaster
*/
const raycaster = new THREE.Raycaster()
- animate the spheres by using the elapsed time and Math.sin() in the
tick()
/**
* render
*/
const clock = new THREE.Clock()
const tick = () => {
const elapsedTime = clock.getElapsedTime()
// animate objects
object1.position.y = Math.sin(elapsedTime * 0.3) * 1.5
object2.position.y = Math.sin(elapsedTime * 0.8) * 1.5
object3.position.y = Math.sin(elapsedTime * 1.4) * 1.5
// update
...
}
- update the caster in the
tick()
/**
* render
*/
const clock = new THREE.Clock()
const tick = () => {
const elapsedTime = clock.getElapsedTime()
// animate objects
...
// cast a ray
const rayOrigin = new THREE.Vector3(-3, 0, 0)
const rayDirection = new THREE.Vector3(1, 0, 0)
rayDirection.normalize()
raycaster.set(rayOrigin, rayDirection)
// update
...
}
- cast a ray and color the objects
/**
* render
*/
const clock = new THREE.Clock()
const tick = () => {
const elapsedTime = clock.getElapsedTime()
// animate objects
...
// cast a ray
...
const objectsToTest = [object1, object2, object3]
const intersects = raycaster.intersectObjects(objectsToTest)
for(const object of objectsToTest) {
object.material.color.set(0xff0000)
}
for(const intersect of intersects) {
intersect.object.material.color.set('#0000ff')
}
// update
...
}
-
Use the raycaster with the mouse
- hovering, we need the coordinates of the mouse, but not in pixel, we need a value goes from -1 to 1 in horizon and vertical axes
/**
* mouse
*/
const mouse = new THREE.Vector2()
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1
})
- use the
setFromCamera()
method to orient the ray in the right direction
/**
* render
*/
const clock = new THREE.Clock()
const tick = () => {
const elapsedTime = clock.getElapsedTime()
// animate objects
object1.position.y = Math.sin(elapsedTime * 0.3) * 1.5
object2.position.y = Math.sin(elapsedTime * 0.8) * 1.5
object3.position.y = Math.sin(elapsedTime * 1.4) * 1.5
// cast a ray from the mouse
raycaster.setFromCamera(mouse, camera) // 根据camera的位置设置方向
const objectsToTest = [object1, object2, object3]
const intersects = raycaster.intersectObjects(objectsToTest)
for(const object of objectsToTest) {
object.material.color.set(0xff0000)
}
for(const intersect of intersects) {
intersect.object.material.color.set('#0000ff')
}
// update
controls.update()
requestAnimationFrame(tick)
renderer.render(scene, camera)
}
- mouse enter and mouse leave, we need to create a 'witness' variable containing the currently hovered object
/**
* render
*/
const clock = new THREE.Clock()
let currentIntersect = null
const tick = () => {
const elapsedTime = clock.getElapsedTime()
// animate objects
object1.position.y = Math.sin(elapsedTime * 0.3) * 1.5
object2.position.y = Math.sin(elapsedTime * 0.8) * 1.5
object3.position.y = Math.sin(elapsedTime * 1.4) * 1.5
// cast a ray from the mouse
raycaster.setFromCamera(mouse, camera) // 根据camera的位置设置方向
const objectsToTest = [object1, object2, object3]
const intersects = raycaster.intersectObjects(objectsToTest)
for(const object of objectsToTest) {
object.material.color.set(0xff0000)
}
for(const intersect of intersects) {
intersect.object.material.color.set('#0000ff')
}
// cast a ray from the mouse
raycaster.setFromCamera(mouse, camera)
if(intersects.length) {
// console.log('enter');
currentIntersect = intersects[0]
}else {
// console.log('leave');
currentIntersect = null
}
// update
controls.update()
requestAnimationFrame(tick)
renderer.render(scene, camera)
}
- mouse click event, we can test which sphere got clicked
/**
* mouse
*/
...
...
window.addEventListener('click', () => {
if(currentIntersect) {
switch(currentIntersect.object) {
case object1:
console.log('click obj1');
break
case object2:
console.log('click obj2');
break
case object3:
console.log('click obj3');
break
}
}
})
-
Raycasting with models
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
...
...
/**
* gltfLoader
*/
const gltfLoader = new GLTFLoader()
let model = null
gltfLoader.load('../public/models/Duck/glTF-Binary/Duck.glb', (gltf) => {
model = gltf.scene
model.position.y = - 1.2
scene.add(model)
})
- lights, the model is
MeshStandardMaterial
, it needs lights to be seen
/**
* light
*/
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.7)
directionalLight.position.set(1, 2, 3)
scene.add(directionalLight)
- intersect the model, we want the model to get larger when we hover it and when we leave it with mouse it will get back to the initial scale
const tick = () => {
...
...
// test intersect with a model
if(model) {
const modelIntersects = raycaster.intersectObject(model)
if(modelIntersects.length) {
model.scale.set(1.2, 1.2, 1.2)
}else {
model.scale.set(1, 1, 1)
}
}
...
}
raycasting with models.png