ThreeJS中的射线检测


参考:https://threejs.org/open in new window

Raycaster

  • Raycaster( origin : Vector3, direction : Vector3, near : Float, far : Float )

    • origin —— 光线投射的原点向量。
    • direction —— 向射线提供方向的方向向量,应当被标准化。
    • near —— 返回的所有结果比near远。near不能为负值,其默认值为0。
    • far —— 返回的所有结果都比far近。far不能小于near,其默认值为Infinity(正无穷。)

    这将创建一个新的raycaster对象。

  • .setFromCamera ( coords : Vector2, camera : Camera ) : undefined

    • coords —— 在标准化设备坐标中鼠标的二维坐标 —— X分量与Y分量应当在-1到1之间。
    • camera —— 射线所来源的摄像机。

    使用一个新的原点和方向来更新射线。

  • .intersectObject ( object : Object3D, recursive : Boolean, optionalTarget : Array ) : Array

    • object —— 检查与射线相交的物体。
    • recursive —— 若为true,则同时也会检查所有的后代。否则将只会检查对象本身。默认值为true。
    • optionalTarget — (可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。

    检测所有在射线与物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个。 该方法返回一个包含有交叉部分的数组:

const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2()
function render() {
	// 通过摄像机和鼠标位置更新射线
	raycaster.setFromCamera( pointer, camera );
	// 计算物体和射线的焦点
	const intersects = raycaster.intersectObjects( scene.children );
	for ( let i = 0; i < intersects.length; i ++ ) {
		intersects[ i ].object.material.color.set( 0xff0000 );
	}
	renderer.render( scene, camera );
}

demo

// 创建一个红色的半透明方块
const rollOverGeo = new THREE.BoxGeometry( 50, 50, 50 );
const rollOverMaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, opacity: 0.5, transparent: true } );
const rollOverMesh = new THREE.Mesh( rollOverGeo, rollOverMaterial );
scene.add( rollOverMesh );

// 创建一个黄色的方块,准备用于添加到场景中
const cubeGeo = new THREE.BoxGeometry( 50, 50, 50 );
const cubeMaterial = new THREE.MeshLambertMaterial( { color: 0xfeb74c, map: new THREE.TextureLoader().load( '/assets/textures/square-outline-textured.png' ) } );

// 创建网格辅助线
const gridHelper = new THREE.GridHelper( 1000, 20 );
scene.add( gridHelper );

// 创建一个平面面板
raycaster = new THREE.Raycaster();
pointer = new THREE.Vector2();
const geometry = new THREE.PlaneGeometry( 1000, 1000 );
geometry.rotateX( - Math.PI / 2 );
const plane = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { visible: false } ) );
scene.add( plane );

// 用于射线检测
objects.push( plane ); // 加入物体数组

// 事件处理函数
function onPointerMove(event){
	pointer.set( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1 );
	raycaster.setFromCamera( pointer, camera );
	const intersects = raycaster.intersectObjects( objects, false );
	if ( intersects.length > 0 ) {
		const intersect = intersects[ 0 ];
		// 将红色半透明方块移动到鼠标所在的位置
    // intersect.point 鼠标悬停点在场景中的位置
    // intersect.face.normal 三维模型子网格(intersect.face)的法向量(normal)
		rollOverMesh.position.copy( intersect.point ).add( intersect.face.normal );
    // 将方块对准到悬停点的中心位置
    /*
    divideScalar( 50 ):将向量坐标除以 50,这是为了将位置信息进行规范化,使每个方块的位置都对应网格的中心点,避免出现半个方块的情况。
    floor():将向量的每个坐标值按照四舍五入的方式转换成整数,这是确保每个网格的中心点都是整数。
    multiplyScalar( 50 ):将向量坐标乘以 50,把网格中心点的位置重新还原成一个完整的坐标系。这一步的作用是反向处理前面的操作,确保位置信息没有改变。
    addScalar( 25 ):最后加上 25,是为了让方块更好地显示在屏幕上,因为像素的起始点是在网格左上角,而不是中心点。
    
    */ 
		rollOverMesh.position.divideScalar( 50 ).floor().multiplyScalar( 50 ).addScalar( 25 );
		render();
	}      
}

function onPointerDown(event){
	pointer.set( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1 );
	raycaster.setFromCamera( pointer, camera );
	const intersects = raycaster.intersectObjects( objects, false );
	if ( intersects.length > 0 ) {
		const intersect = intersects[ 0 ];
		// 删除方块
		if ( isShiftDown ) {
			if ( intersect.object !== plane ) {
				scene.remove( intersect.object );
				objects.splice( objects.indexOf( intersect.object ), 1 );
			}
		// 添加方块
		} else {
			const voxel = new THREE.Mesh( cubeGeo, cubeMaterial );
			voxel.position.copy( intersect.point ).add( intersect.face.normal );
			voxel.position.divideScalar( 50 ).floor().multiplyScalar( 50 ).addScalar( 25 );
			scene.add( voxel );
			objects.push( voxel );
		}
		render();
	}
}

const material = new THREE.MeshBasicMaterial({ // 创建网格基本材质
    wireframe: true  // wireframe 为 true 时,表示显示为线框模式,否则就是默认表示实心的
})
const redMaterial = new THREE.MeshBasicMaterial({ // 创建颜色材质,设置为红色 
    color:'#ff0000'
})

let cubeArr = []
for(let i = -5;i<5;i++) { // 创建多个立方体
    for(let j =  -5;j <5;j++) {
        for(let z = -5;z<5;z++) {
            const cube = new THREE.Mesh(cubeGeometry,material); // 创建立方体网格
            cube.position.set(i,j,z); // 设置立方体网格的位置
            scene.add(cube); // 将立方体网格添加到场景中
            cubeArr.push(cube) // 将立方体放入 cubeArr 数组中
        }
    }
}

function onPointerMove(event) {
    mouse.x = (event.clientX / window.innerWidth) *2 - 1; // 计算鼠标在屏幕上的位置,转换为 Three.js 坐标系的位置
    mouse.y = -((event.clientY / window.innerHeight)* 2 - 1);
    raycaster.setFromCamera(mouse, camera); // 从相机透视投影的位置发射一个射线,并求出射线经过的物体
    let result = raycaster.intersectObjects(cubeArr); // 射线选择器与立方体数组作为参数,返回一个对象数组 result,其中包含射线经过的物体
    result.forEach((item) => { // 遍历射线碰撞得到的所有物体,将它们的材质改为红色
      item.object.material = redMaterial;
    });
}

文字显示隐藏

// 月亮Label隐藏
const elapsed = clock.getElapsedTime();
moon.position.set(Math.sin(elapsed) * 8, 0, Math.cos(elapsed) * 8);

const moonPosition = moon.position.clone();
const cameraPos = camera.position.clone()
/*
通过 moonPosition.clone() 和 camera.position.clone() 克隆 moon 的位置和摄像机的位置,避免直接修改 moonPosition 和 cameraPos 对象的值。
通过 moonPosition.sub(cameraPos) 计算出从摄像机指向 moon 的向量。
通过 .normalize() 将向量转换为单位向量,即长度为 1 的向量,方便后续的计算。这样得到的向量就是一个方向,指向摄像机位置和 moon 位置之间的向量。
*/
rayCasterMoon.set(cameraPos,moonPosition.sub(cameraPos).normalize())
const intersectsMoon = rayCasterMoon.intersectObjects(scene.children,true)
if(intersectsMoon.length >0 && intersectsMoon[0].object !== moon) {
    moonLabel.element.style.visibility = 'hidden'
}else{
    moonLabel.element.style.visibility = 'initial'
}


// 中国Label隐藏

const chinaPosition = chinaLabel.position.clone();

// 计算出标签跟摄像机的距离
const labelDistance = chinaPosition.distanceTo(camera.position);
// 检测射线的碰撞
// 向量(坐标)从世界空间投影到相机的标准化设备坐标 (NDC) 空间。
/*
project() 方法是 Object3D 类的一个方法,用于将三维坐标转换为屏幕上的二维坐标。
它需要传入一个参数 camera,表示用于投影计算的相机
*/
chinaPosition.project(camera);

raycaster.setFromCamera(chinaPosition,camera);
       
const intersects = raycaster.intersectObjects(scene.children,true)

if(intersects.length == 0){
  chinaLabel.element.style.visibility = 'initial'
}else{
  const minDistance = intersects[0].distance;
  if(minDistance<labelDistance){
    chinaLabel.element.style.visibility = 'hidden'
  }else{
    chinaLabel.element.style.visibility = 'initial'
  }
}


评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.13.0