部分摘抄自:
three.js学习笔记(九)——光线投射_hongsir_12的博客-CSDN博客
光线投射(RayCaster)可以向特定方向投射光线,并测试哪些对象与其相交。光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。
用法示例:
三个球体,然后我们要发射一条光线穿过这些球体看其是否与之相交
/**
* 球1
*/
const sphere1=new THREE.Mesh(
new THREE.SphereBufferGeometry( 0.5, 32, 32 ),
new THREE.MeshBasicMaterial( {color: "#ff0000"} )
)
sphere1.position.x=-2
const sphere2=new THREE.Mesh(
new THREE.SphereBufferGeometry( 0.5, 32, 32 ),
new THREE.MeshBasicMaterial( {color: "#ff0000"} )
)
const sphere3=new THREE.Mesh(
new THREE.SphereBufferGeometry( 0.5, 32, 32 ),
new THREE.MeshBasicMaterial( {color: "#ff0000"} )
)
sphere3.position.x=2;
scene.add(sphere1,sphere2,sphere3)
const raycaster = new THREE.Raycaster()
这个类用于进行raycasting(光线投射)。 光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。
构造器
Raycaster( origin : Vector3, direction : Vector3, near : Float, far : Float ) {
origin —— 光线投射的原点向量。
direction —— 向射线提供方向的方向向量,应当被标准化。
near —— 返回的所有结果比near远。near不能为负值,其默认值为0。
far —— 返回的所有结果都比far近。far不能小于near,其默认值为Infinity(正无穷。)这将创建一个新的raycaster对象。
.intersectObject ( object : Object3D, recursive : Boolean, optionalTarget : Array ) : Array
object —— 检查与射线相交的物体。
recursive —— 若为true,则同时也会检查所有的后代。否则将只会检查对象本身。默认值为false。
optionalTarget — (可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。检测所有在射线与物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个。
该方法返回一个包含有交叉部分的数组:
.intersectObjects ( objects : Array, recursive : Boolean, optionalTarget : Array ) : Array
objects —— 检测和射线相交的一组物体。
recursive —— 若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分。默认值为false。
optionalTarget —— (可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。检测所有在射线与这些物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个),相交部分和.intersectObject所返回的格式是相同的。
/**
* 射线
*/
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)
const intersect=raycaster.intersectObject(sphere1)
console.log(intersect)
const intersects=raycaster.intersectObjects([sphere1,sphere2,sphere3])
console.log(intersects)
查看打印结果对象包含了什么信息
distance
– 光线原点与碰撞点之间的距离face
– 几何体的哪个面被光线击中faceIndex
– 那被击中的面的索引object
– 什么物体与碰撞有关point
– 碰撞准确位置的矢量uv
– 该几何体中的UV坐标
设置动画函数
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// Update controls
controls.update();
raycaster.setFromCamera(mouse,camera)
sphere1.position.y=Math.sin(elapsedTime* 0.3) * 1.5
sphere2.position.y=Math.sin(elapsedTime* 0.7) * 1.5
sphere3.position.y=Math.sin(elapsedTime* 1.4) * 1.5
const objectsToTests=[sphere1,sphere2,sphere3]
const intersectObjects=raycaster.intersectObjects(objectsToTests)
for(const object of objectsToTests){
object.material.color.set('#ff0000')
}
for(const intersect of intersectObjects){
intersect.object.material.color.set('#0000ff')
}
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
实际效果,三个球体分别以不同速率上下移动,当其与射线相交时,即被射线射中时,该球体颜色变为蓝色,未被射线射中时颜色为红色
我们可以使用光线投射来测试物体是否在鼠标下面。为此我们需要的是鼠标的坐标,一个在水平轴和垂直轴上范围从-1到1的值。
因为鼠标只在屏幕移动,所以使用二维向量Vector2来创建鼠标变量,并监听鼠标移动事件,获取鼠标位置。
因为需要水平方向由左往右和垂直方向由下往上的值范围始终在[-1,1]的区间内,因此需要对鼠标坐标位置进行标准化。
const mouse=new THREE.Vector2()
window.addEventListener("mousemove",(event)=>{
mouse.x=(event.clientX/sizes.width*2)-1
mouse.y=-(event.clientY/sizes.height*2)+1
})
避免在mousemove
事件回调中投射光线,而是要回动画函数中去投射光线。
之后使用setFromCamera()
方法将光线定向到正确的方向
.setFromCamera ( coords : Vector2, camera : Camera ) : null
coords —— 在标准化设备坐标中鼠标的二维坐标 —— X分量与Y分量应当在-1到1之间。
camera —— 射线所来源的摄像机。
我们可能有时候有这么一个需求,当鼠标移动到物体上时触发一次鼠标移入事件mouseenter ,鼠标离开物体时触发一次鼠标移出事件mouseleave,可以在动画函数中添加如下代码:
思路是先在外面定义一个当前鼠标移入对象变量currentIntersect,值为null,然后对被光线射中的对象数组长度进行判断,如果不为0则说明射线与物体相交了,在里面判断当前鼠标移入对象的值,为空则触发mouseenter事件,然后将射线首先照射到的第一个对象赋值给currentIntersect,后面触发mouseleave事件相信也明白怎么做了。
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// Update controls
controls.update();
raycaster.setFromCamera(mouse,camera)
sphere1.position.y=Math.sin(elapsedTime* 0.3) * 1.5
sphere2.position.y=Math.sin(elapsedTime* 0.7) * 1.5
sphere3.position.y=Math.sin(elapsedTime* 1.4) * 1.5
const objectsToTests=[sphere1,sphere2,sphere3]
const intersectObjects=raycaster.intersectObjects(objectsToTests)
for(const object of objectsToTests){
object.material.color.set('#ff0000')
}
for(const intersect of intersectObjects){
intersect.object.material.color.set('#0000ff')
}
if(intersectObjects.length){
currentIntersect=intersectObjects[0]
console.log('mouse enter')
}else{
if(currentIntersect==null){
console.log('mouse leave')
}
currentIntersect=null
}
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
同样借助当前鼠标移入对象变量currentIntersect
window.addEventListener('click',()=>{
if(currentIntersect){
console.log(currentIntersect.object)
switch(currentIntersect.object){
case sphere1:
console.log('click on object1')
break
case sphere2:
console.log('click on object2')
break
case sphere3:
console.log('click on object3')
break
}
}
})
import "./style.css";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import * as dat from "dat.gui";
/**
* Base
*/
// Debug
const gui = new dat.GUI();
// Canvas
const canvas = document.querySelector("canvas.webgl");
// Scene
const scene = new THREE.Scene();
/**
* light
*/
const light = new THREE.AmbientLight(0xffffff); // soft white light
scene.add( light );
/**
* 球1
*/
const sphere1=new THREE.Mesh(
new THREE.SphereBufferGeometry( 0.5, 32, 32 ),
new THREE.MeshBasicMaterial( {color: "#ff0000"} )
)
sphere1.position.x=-2
const sphere2=new THREE.Mesh(
new THREE.SphereBufferGeometry( 0.5, 32, 32 ),
new THREE.MeshBasicMaterial( {color: "#ff0000"} )
)
const sphere3=new THREE.Mesh(
new THREE.SphereBufferGeometry( 0.5, 32, 32 ),
new THREE.MeshBasicMaterial( {color: "#ff0000"} )
)
sphere3.position.x=2;
scene.add(sphere1,sphere2,sphere3)
/**
* 射线
*/
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)
const intersect=raycaster.intersectObject(sphere1)
console.log(intersect)
const intersects=raycaster.intersectObjects([sphere1,sphere2,sphere3])
console.log(intersects)
/**
* Sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
window.addEventListener("resize", () => {
// Update sizes
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// Update camera
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
// Update renderer
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
/**
* Camera
*/
// Base camera
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100
);
camera.position.z = 3;
scene.add(camera);
// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const mouse=new THREE.Vector2()
window.addEventListener("mousemove",(event)=>{
mouse.x=(event.clientX/sizes.width*2)-1
mouse.y=-(event.clientY/sizes.height*2)+1
})
window.addEventListener('click',()=>{
if(currentIntersect){
console.log(currentIntersect.object)
switch(currentIntersect.object){
case sphere1:
console.log('click on object1')
break
case sphere2:
console.log('click on object2')
break
case sphere3:
console.log('click on object3')
break
}
}
})
let currentIntersect=null
const clock=new THREE.Clock()
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// Update controls
controls.update();
raycaster.setFromCamera(mouse,camera)
sphere1.position.y=Math.sin(elapsedTime* 0.3) * 1.5
sphere2.position.y=Math.sin(elapsedTime* 0.7) * 1.5
sphere3.position.y=Math.sin(elapsedTime* 1.4) * 1.5
const objectsToTests=[sphere1,sphere2,sphere3]
const intersectObjects=raycaster.intersectObjects(objectsToTests)
for(const object of objectsToTests){
object.material.color.set('#ff0000')
}
for(const intersect of intersectObjects){
intersect.object.material.color.set('#0000ff')
}
if(intersectObjects.length){
currentIntersect=intersectObjects[0]
console.log('mouse enter')
}else{
if(currentIntersect==null){
console.log('mouse leave')
}
currentIntersect=null
}
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();