28. 射线检测 1. 概述 射线检测(Raycaster)是 Three.js 中实现交互的核心技术。它通过发射射线检测与物体的交点,用于实现鼠标拾取、碰撞检测等功能。
┌─────────────────────────────────────────────────────────────┐ │ 射线检测原理 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 射线 (Ray) │ │ ├── 起点 (origin):射线发射位置 │ │ ├── 方向 (direction):射线方向向量 │ │ └── 距离 (distance):检测距离 │ │ │ │ 检测流程 │ │ ├── 创建射线 │ │ ├── 计算射线与物体的交点 │ │ ├── 返回交点信息 │ │ └── 处理交互 │ │ │ └─────────────────────────────────────────────────────────────┘2. Raycaster 类 2.1 创建 Raycaster // 创建射线检测器 const raycaster= new THREE. Raycaster ( ) ; // 设置射线起点和方向 const origin= new THREE. Vector3 ( 0 , 0 , 0 ) ; const direction= new THREE. Vector3 ( 0 , 0 , 1 ) ; raycaster. set ( origin, direction) ; // 设置检测距离 raycaster. far= 100 ; // 近平面距离 raycaster. near= 0 ; 2.2 属性详解 属性 说明 ray射线对象(origin + direction) near近平面距离 far远平面距离 params精度参数 intersectObjects检测物体列表
3. 交点检测 3.1 检测物体列表 // 检测多个物体 const intersects= raycaster. intersectObjects ( objects) ; // 检测单个物体 const intersects= raycaster. intersectObject ( object) ; // 递归检测子物体 const intersects= raycaster. intersectObjects ( objects, true ) ; 3.2 交点信息 if ( intersects. length> 0 ) { const hit= intersects[ 0 ] ; // 被击中的物体 const object= hit. object; // 击中点的世界坐标 const point= hit. point; // 击中点的法线 const normal= hit. normal; // 击中点的 UV 坐标 const uv= hit. uv; // 从射线起点到击中点的距离 const distance= hit. distance; } 4. 鼠标交互 4.1 鼠标坐标转换 const mouse= new THREE. Vector2 ( ) ; window. addEventListener ( 'click' , ( event ) => { // 将鼠标坐标转换为标准化设备坐标 (-1 到 +1) mouse. x= ( event. clientX/ renderer. domElement. clientWidth) * 2 - 1 ; mouse. y= - ( event. clientY/ renderer. domElement. clientHeight) * 2 + 1 ; // 从相机发射射线 raycaster. setFromCamera ( mouse, camera) ; // 检测物体 const intersects= raycaster. intersectObjects ( objects) ; if ( intersects. length> 0 ) { console. log ( '点击了物体:' , intersects[ 0 ] . object) ; } } ) ; 4.2 鼠标悬停 let hoveredObject= null ; window. addEventListener ( 'mousemove' , ( event ) => { mouse. x= ( event. clientX/ renderer. domElement. clientWidth) * 2 - 1 ; mouse. y= - ( event. clientY/ renderer. domElement. clientHeight) * 2 + 1 ; raycaster. setFromCamera ( mouse, camera) ; const intersects= raycaster. intersectObjects ( objects) ; if ( intersects. length> 0 ) { if ( hoveredObject!== intersects[ 0 ] . object) { if ( hoveredObject) { // 恢复原样式 hoveredObject. material. emissive. setHex ( 0x000000 ) ; } hoveredObject= intersects[ 0 ] . object; hoveredObject. material. emissive. setHex ( 0x444444 ) ; } } else if ( hoveredObject) { hoveredObject. material. emissive. setHex ( 0x000000 ) ; hoveredObject= null ; } } ) ; 5. 完整示例 import * as THREE from 'three' ; import { OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js' ; import { CSS2DRenderer, CSS2DObject} from 'three/examples/jsm/renderers/CSS2DRenderer.js' ; const scene= new THREE. Scene ( ) ; scene. background= new THREE. Color ( 0x111122 ) ; const camera= new THREE. PerspectiveCamera ( 45 , window. innerWidth/ window. innerHeight, 0.1 , 1000 ) ; camera. position. set ( 5 , 4 , 8 ) ; camera. lookAt ( 0 , 0 , 0 ) ; const renderer= new THREE. WebGLRenderer ( { antialias : true } ) ; renderer. setSize ( window. innerWidth, window. innerHeight) ; renderer. shadowMap. enabled= true ; document. body. appendChild ( renderer. domElement) ; const labelRenderer= new CSS2DRenderer ( ) ; labelRenderer. setSize ( window. innerWidth, window. innerHeight) ; labelRenderer. domElement. style. position= 'absolute' ; labelRenderer. domElement. style. top= '0px' ; labelRenderer. domElement. style. left= '0px' ; labelRenderer. domElement. style. pointerEvents= 'none' ; document. body. appendChild ( labelRenderer. domElement) ; const controls= new OrbitControls ( camera, renderer. domElement) ; controls. enableDamping= true ; // 光源 const ambientLight= new THREE. AmbientLight ( 0x404040 , 0.5 ) ; scene. add ( ambientLight) ; const directionalLight= new THREE. DirectionalLight ( 0xffffff , 1 ) ; directionalLight. position. set ( 5 , 10 , 7 ) ; directionalLight. castShadow= true ; scene. add ( directionalLight) ; // 辅助对象 const axesHelper= new THREE. AxesHelper ( 5 ) ; scene. add ( axesHelper) ; const gridHelper= new THREE. GridHelper ( 10 , 20 ) ; scene. add ( gridHelper) ; // 创建可交互物体 const objects= [ ] ; const colors= [ 0xff3333 , 0x33ff33 , 0x3333ff , 0xffcc33 , 0xff33cc , 0x33ccff ] ; for ( let i= 0 ; i< 6 ; i++ ) { const geometry= new THREE. BoxGeometry ( 0.8 , 0.8 , 0.8 ) ; const material= new THREE. MeshStandardMaterial ( { color : colors[ i] , metalness : 0.5 , roughness : 0.3 } ) ; const cube= new THREE. Mesh ( geometry, material) ; const angle= ( i/ 6 ) * Math. PI * 2 ; const radius= 2.5 ; cube. position. x= Math. cos ( angle) * radius; cube. position. z= Math. sin ( angle) * radius; cube. position. y= 0.5 ; cube. castShadow= true ; cube. userData= { id : i, color : colors[ i] , originalColor : material. color. clone ( ) } ; scene. add ( cube) ; objects. push ( cube) ; } // 地面 const planeGeometry= new THREE. PlaneGeometry ( 8 , 8 ) ; const planeMaterial= new THREE. MeshStandardMaterial ( { color : 0x336699 , side : THREE . DoubleSide} ) ; const plane= new THREE. Mesh ( planeGeometry, planeMaterial) ; plane. rotation. x= - Math. PI / 2 ; plane. position. y= - 0.5 ; plane. receiveShadow= true ; scene. add ( plane) ; // 射线检测器 const raycaster= new THREE. Raycaster ( ) ; const mouse= new THREE. Vector2 ( ) ; // 状态 let hoveredObject= null ; let selectedObject= null ; // 标签 const createLabel= ( text, position, color= '#ffffff' ) => { const div= document. createElement ( 'div' ) ; div. textContent= text; div. style. color= color; div. style. background= 'rgba(0,0,0,0.6)' ; div. style. padding= '2px 6px' ; div. style. borderRadius= '4px' ; div. style. fontSize= '12px' ; div. style. pointerEvents= 'none' ; const label= new CSS2DObject ( div) ; label. position. copy ( position) ; scene. add ( label) ; return label; } ; objects. forEach ( ( obj, i ) => { createLabel ( ` 物体 ${ i+ 1 } ` , new THREE. Vector3 ( obj. position. x, obj. position. y+ 0.8 , obj. position. z) ) ; } ) ; // 信息显示 const infoDiv= document. createElement ( 'div' ) ; infoDiv. style. position= 'absolute' ; infoDiv. style. bottom= '20px' ; infoDiv. style. left= '20px' ; infoDiv. style. background= 'rgba(0,0,0,0.7)' ; infoDiv. style. color= 'white' ; infoDiv. style. padding= '10px' ; infoDiv. style. borderRadius= '5px' ; infoDiv. style. fontFamily= 'monospace' ; infoDiv. style. fontSize= '14px' ; document. body. appendChild ( infoDiv) ; // 鼠标移动事件 window. addEventListener ( 'mousemove' , ( event ) => { mouse. x= ( event. clientX/ renderer. domElement. clientWidth) * 2 - 1 ; mouse. y= - ( event. clientY/ renderer. domElement. clientHeight) * 2 + 1 ; raycaster. setFromCamera ( mouse, camera) ; const intersects= raycaster. intersectObjects ( objects) ; if ( intersects. length> 0 ) { const hit= intersects[ 0 ] . object; if ( hoveredObject!== hit) { if ( hoveredObject) { hoveredObject. material. emissive. setHex ( 0x000000 ) ; hoveredObject. scale. set ( 1 , 1 , 1 ) ; } hoveredObject= hit; hoveredObject. material. emissive. setHex ( 0x444444 ) ; hoveredObject. scale. set ( 1.1 , 1.1 , 1.1 ) ; } infoDiv. innerHTML= ` 悬停物体: ${ hoveredObject. userData. id+ 1 } <br>距离: ${ intersects[ 0 ] . distance. toFixed ( 2 ) } ` ; } else { if ( hoveredObject) { hoveredObject. material. emissive. setHex ( 0x000000 ) ; hoveredObject. scale. set ( 1 , 1 , 1 ) ; hoveredObject= null ; } infoDiv. innerHTML= '无物体' ; } } ) ; // 点击事件 window. addEventListener ( 'click' , ( event ) => { if ( hoveredObject) { if ( selectedObject=== hoveredObject) { selectedObject. material. color. setHex ( selectedObject. userData. originalColor) ; selectedObject. scale. set ( 1 , 1 , 1 ) ; selectedObject= null ; } else { if ( selectedObject) { selectedObject. material. color. setHex ( selectedObject. userData. originalColor) ; selectedObject. scale. set ( 1 , 1 , 1 ) ; } selectedObject= hoveredObject; selectedObject. material. color. setHex ( 0xffaa00 ) ; selectedObject. scale. set ( 1.2 , 1.2 , 1.2 ) ; } } } ) ; // 动画 function animate ( ) { requestAnimationFrame ( animate) ; // 旋转物体 objects. forEach ( ( obj, i ) => { obj. rotation. y+= 0.01 ; obj. rotation. x+= 0.005 ; } ) ; controls. update ( ) ; renderer. render ( scene, camera) ; labelRenderer. render ( scene, camera) ; } animate ( ) ; window. addEventListener ( 'resize' , onWindowResize, false ) ; function onWindowResize ( ) { camera. aspect= window. innerWidth/ window. innerHeight; camera. updateProjectionMatrix ( ) ; renderer. setSize ( window. innerWidth, window. innerHeight) ; labelRenderer. setSize ( window. innerWidth, window. innerHeight) ; } 6. 总结 类/方法 用途 Raycaster射线检测器 setFromCamera()从相机设置射线 intersectObjects()检测物体列表 intersectObject()检测单个物体
交点信息 说明 object被击中的物体 point击中点坐标 normal击中点法线 distance距离 uvUV 坐标