ThreeJs 学习之旅(十四)—Raycaster(射线)

部分摘抄自:
three.js学习笔记(九)——光线投射_hongsir_12的博客-CSDN博客

光线投射(RayCaster)可以向特定方向投射光线,并测试哪些对象与其相交。光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。

ThreeJs 学习之旅(十四)—Raycaster(射线)_第1张图片

用法示例:

  1. 测试相机前方是否有一堵墙(障碍)
  2. 光线是否击中目标
  3. 当鼠标移动时测试是否有物体位于光标下方,以此模拟鼠标事件
  4. 当物体朝向特定某处时提示信息

初始场景

三个球体,然后我们要发射一条光线穿过这些球体看其是否与之相交

/**
 * 球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)

ThreeJs 学习之旅(十四)—Raycaster(射线)_第2张图片

创建光线投射(RayCaster)

const raycaster = new THREE.Raycaster()

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

 查看打印结果对象包含了什么信息

ThreeJs 学习之旅(十四)—Raycaster(射线)_第3张图片

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);
};

实际效果,三个球体分别以不同速率上下移动,当其与射线相交时,即被射线射中时,该球体颜色变为蓝色,未被射线射中时颜色为红色

ThreeJs 学习之旅(十四)—Raycaster(射线)_第4张图片

通过鼠标使用光线投射

我们可以使用光线投射来测试物体是否在鼠标下面。为此我们需要的是鼠标的坐标,一个在水平轴和垂直轴上范围从-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);
};

ThreeJs 学习之旅(十四)—Raycaster(射线)_第5张图片

鼠标点击事件

同样借助当前鼠标移入对象变量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
    }
  }
})

ThreeJs 学习之旅(十四)—Raycaster(射线)_第6张图片

最后全量代码

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();

你可能感兴趣的:(ThreeJs,学习)