一、点模型(Points)拾取实现
实现步骤:
- 创建点模型:使用
THREE.Points和点材质 - 设置点大小:在材质中设置
size属性 - Raycaster配置:设置
Points的拾取阈值 - 拾取检测:使用
intersectObjects检测相交
完整案例:
<template><divclass="container"ref="containerRef"></div></template><scriptsetup>import{onMounted,ref}from"vue";import*asTHREEfrom"three";import{OrbitControls}from"three/examples/jsm/controls/OrbitControls";constcontainerRef=ref(null);// 创建场景constscene=newTHREE.Scene();scene.background=newTHREE.Color(0xf0f0f0);// 创建相机constcamera=newTHREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,0.1,1000);camera.position.set(0,0,10);// 创建渲染器constrenderer=newTHREE.WebGLRenderer({antialias:true});renderer.setSize(window.innerWidth,window.innerHeight);// 创建点模型functioncreatePointCloud(){// 创建100个随机点constvertices=[];for(leti=0;i<100;i++){constx=(Math.random()-0.5)*10;consty=(Math.random()-0.5)*10;constz=(Math.random()-0.5)*10;vertices.push(x,y,z);}constgeometry=newTHREE.BufferGeometry();geometry.setAttribute('position',newTHREE.Float32BufferAttribute(vertices,3));// 创建点材质 - 关键:设置点大小constmaterial=newTHREE.PointsMaterial({color:0xff0000,size:0.2,// 点的大小sizeAttenuation:true// 点大小是否随距离衰减});// 创建点云对象constpoints=newTHREE.Points(geometry,material);points.name='pointCloud';scene.add(points);returnpoints;}// 创建点模型constpointCloud=createPointCloud();// 添加光源constambientLight=newTHREE.AmbientLight(0xffffff,0.5);scene.add(ambientLight);constdirectionalLight=newTHREE.DirectionalLight(0xffffff,0.8);directionalLight.position.set(10,10,5);scene.add(directionalLight);// 添加坐标轴辅助constaxesHelper=newTHREE.AxesHelper(5);scene.add(axesHelper);// 动画循环functionanimate(){requestAnimationFrame(animate);// 让点云缓慢旋转pointCloud.rotation.y+=0.005;renderer.render(scene,camera);}animate();// 点模型拾取函数functionpickPoints(event){// 1. 获取鼠标归一化设备坐标constmouse=newTHREE.Vector2();mouse.x=(event.clientX/window.innerWidth)*2-1;mouse.y=-(event.clientY/window.innerHeight)*2+1;// 2. 创建Raycasterconstraycaster=newTHREE.Raycaster();raycaster.setFromCamera(mouse,camera);// 3. 关键:设置点模型的拾取阈值// 这个值决定点击距离点多近才算选中,值越大越容易选中raycaster.params.Points={threshold:0.2};// 4. 检测相交constintersects=raycaster.intersectObjects([pointCloud]);// 5. 处理结果if(intersects.length>0){constintersect=intersects[0];console.log('选中了点:',intersect);// 获取选中的点的索引constpointIndex=intersect.index;// 创建高亮点(在该位置添加一个更大的点)consthighlightGeometry=newTHREE.BufferGeometry();constposition=intersect.object.geometry.attributes.position;constpointPosition=[position.getX(pointIndex),position.getY(pointIndex),position.getZ(pointIndex)];highlightGeometry.setAttribute('position',newTHREE.Float32BufferAttribute(pointPosition,3));consthighlightMaterial=newTHREE.PointsMaterial({color:0xffff00,size:0.5,sizeAttenuation:true});consthighlight=newTHREE.Points(highlightGeometry,highlightMaterial);highlight.name='highlightPoint';// 移除之前的高亮点constoldHighlight=scene.getObjectByName('highlightPoint');if(oldHighlight)scene.remove(oldHighlight);scene.add(highlight);// 显示信息console.log('点位置:',pointPosition);console.log('点索引:',pointIndex);}}// 添加点击事件window.addEventListener('click',pickPoints);// 挂载到DOMonMounted(()=>{constcontrols=newOrbitControls(camera,containerRef.value);controls.enableDamping=true;containerRef.value.appendChild(renderer.domElement);// 窗口大小变化处理window.addEventListener('resize',()=>{camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);});});</script><style>.container{width:100%;height:100vh;}</style>关键点说明:
PointsMaterial.size:控制点的大小raycaster.params.Points.threshold:设置点拾取的敏感度intersect.index:获取选中点的索引intersect.point:获取选中点的具体位置
二、线模型(Line)拾取实现
实现步骤:
- 创建线模型:使用
THREE.Line或THREE.LineSegments - Raycaster配置:设置
Line的拾取阈值 - 提高拾取精度:通过辅助方法增加拾取成功率
- 处理拾取结果:获取线段信息
完整案例:
<template><divclass="container"ref="containerRef"></div></template><scriptsetup>import{onMounted,ref}from"vue";import*asTHREEfrom"three";import{OrbitControls}from"three/examples/jsm/controls/OrbitControls";constcontainerRef=ref(null);// 创建场景constscene=newTHREE.Scene();scene.background=newTHREE.Color(0x222222);// 创建相机constcamera=newTHREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,0.1,1000);camera.position.set(5,5,5);camera.lookAt(0,0,0);// 创建渲染器constrenderer=newTHREE.WebGLRenderer({antialias:true});renderer.setSize(window.innerWidth,window.innerHeight);// 创建复杂的线模型functioncreateComplexLine(){// 创建曲线路径constcurve=newTHREE.CatmullRomCurve3([newTHREE.Vector3(-4,0,0),newTHREE.Vector3(-2,3,1),newTHREE.Vector3(0,0,2),newTHREE.Vector3(2,3,1),newTHREE.Vector3(4,0,0)]);// 获取曲线上的点constpoints=curve.getPoints(50);// 创建线几何体constgeometry=newTHREE.BufferGeometry().setFromPoints(points);// 创建线材质constmaterial=newTHREE.LineBasicMaterial({color:0x00aaff,linewidth:3// 注意:大多数浏览器不支持大于1的线宽});// 创建线对象constline=newTHREE.Line(geometry,material);line.name='complexLine';scene.add(line);returnline;}// 创建网格线(更容易拾取)functioncreateGridLines(){constgroup=newTHREE.Group();// 创建水平线for(leti=-5;i<=5;i++){constgeometry=newTHREE.BufferGeometry().setFromPoints([newTHREE.Vector3(-5,i,0),newTHREE.Vector3(5,i,0)]);constmaterial=newTHREE.LineBasicMaterial({color:0x666666,linewidth:2});constline=newTHREE.Line(geometry,material);line.userData.type='gridLine';line.userData.index=i;group.add(line);}// 创建垂直线for(leti=-5;i<=5;i++){constgeometry=newTHREE.BufferGeometry().setFromPoints([newTHREE.Vector3(i,-5,0),newTHREE.Vector3(i,5,0)]);constmaterial=newTHREE.LineBasicMaterial({color:0x666666,linewidth:2});constline=newTHREE.Line(geometry,material);line.userData.type='gridLine';line.userData.index=i;group.add(line);}scene.add(group);returngroup;}// 创建线模型constcomplexLine=createComplexLine();constgridLines=createGridLines();// 添加光源constambientLight=newTHREE.AmbientLight(0xffffff,0.6);scene.add(ambientLight);constdirectionalLight=newTHREE.DirectionalLight(0xffffff,0.8);directionalLight.position.set(10,10,5);scene.add(directionalLight);// 添加坐标轴辅助constaxesHelper=newTHREE.AxesHelper(5);scene.add(axesHelper);// 动画循环functionanimate(){requestAnimationFrame(animate);renderer.render(scene,camera);}animate();// 线模型拾取函数functionpickLines(event){// 获取鼠标位置constmouse=newTHREE.Vector2();mouse.x=(event.clientX/window.innerWidth)*2-1;mouse.y=-(event.clientY/window.innerHeight)*2+1;// 创建Raycasterconstraycaster=newTHREE.Raycaster();raycaster.setFromCamera(mouse,camera);// 关键:设置线模型的拾取阈值(增大以提高拾取成功率)// 这个值表示距离线多远的点击算选中(单位:世界单位)raycaster.params.Line={threshold:0.3};// 收集所有线对象constlineObjects=[];scene.traverse((object)=>{if(object.type==='Line'||object.type==='LineSegments'){lineObjects.push(object);}});// 检测相交constintersects=raycaster.intersectObjects(lineObjects);if(intersects.length>0){constintersect=intersects[0];constline=intersect.object;console.log('选中了线:',line);console.log('相交点:',intersect.point);console.log('距离:',intersect.distance);console.log('线段索引:',intersect.faceIndex);// 改变线的颜色line.material.color.set(Math.random()*0xffffff);// 在相交点添加标记addIntersectionMarker(intersect.point);// 如果是网格线,显示信息if(line.userData.type==='gridLine'){console.log(`网格线类型:${line.userData.type}, 索引:${line.userData.index}`);}}}// 添加相交点标记functionaddIntersectionMarker(position){// 移除旧的标记constoldMarker=scene.getObjectByName('intersectionMarker');if(oldMarker)scene.remove(oldMarker);// 创建标记几何体constmarkerGeometry=newTHREE.SphereGeometry(0.1,16,16);constmarkerMaterial=newTHREE.MeshBasicMaterial({color:0xff0000});constmarker=newTHREE.Mesh(markerGeometry,markerMaterial);marker.position.copy(position);marker.name='intersectionMarker';scene.add(marker);// 3秒后移除标记setTimeout(()=>{if(marker.parent)scene.remove(marker);},3000);}// 添加点击事件window.addEventListener('click',pickLines);// 挂载到DOMonMounted(()=>{constcontrols=newOrbitControls(camera,containerRef.value);controls.enableDamping=true;containerRef.value.appendChild(renderer.domElement);// 窗口大小变化处理window.addEventListener('resize',()=>{camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);});});</script><style>.container{width:100%;height:100vh;}</style>关键点说明:
raycaster.params.Line.threshold:控制线拾取的敏感度- 线宽限制:WebGL中大多不支持大于1的
linewidth intersect.faceIndex:获取选中线段的索引- 使用辅助标记:通过添加标记点显示拾取位置
三、精灵模型(Sprite)拾取实现
实现步骤:
- 创建精灵模型:使用
THREE.Sprite和精灵材质 - 设置精灵大小:通过
scale属性控制 - Raycaster配置:Sprite会自动被检测,无需特殊配置
- 处理拾取结果:获取精灵信息
完整案例:
<template><divclass="container"ref="containerRef"></div></template><scriptsetup>import{onMounted,ref}from"vue";import*asTHREEfrom"three";import{OrbitControls}from"three/examples/jsm/controls/OrbitControls";constcontainerRef=ref(null);// 创建场景constscene=newTHREE.Scene();scene.background=newTHREE.Color(0x111122);// 创建相机constcamera=newTHREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,0.1,1000);camera.position.set(0,0,15);// 创建渲染器constrenderer=newTHREE.WebGLRenderer({antialias:true});renderer.setSize(window.innerWidth,window.innerHeight);// 创建精灵材质(使用Canvas绘制纹理)functioncreateSpriteMaterial(text,color='#ff0000'){// 创建Canvasconstcanvas=document.createElement('canvas');canvas.width=256;canvas.height=256;constcontext=canvas.getContext('2d');// 绘制圆形背景context.beginPath();context.arc(128,128,120,0,2*Math.PI);context.fillStyle=color;context.fill();// 添加描边context.lineWidth=8;context.strokeStyle='#ffffff';context.stroke();// 添加文字context.font='bold 60px Arial';context.fillStyle='#ffffff';context.textAlign='center';context.textBaseline='middle';context.fillText(text,128,128);// 创建纹理consttexture=newTHREE.CanvasTexture(canvas);// 创建精灵材质constmaterial=newTHREE.SpriteMaterial({map:texture,transparent:true});returnmaterial;}// 创建多个精灵constsprites=[];constspriteGroup=newTHREE.Group();functioncreateSprites(){constpositions=[{x:-5,y:0,z:0,text:'A',color:'#ff0000'},{x:-2.5,y:3,z:-2,text:'B',color:'#00ff00'},{x:0,y:-2,z:2,text:'C',color:'#0000ff'},{x:2.5,y:3,z:-2,text:'D',color:'#ffff00'},{x:5,y:0,z:0,text:'E',color:'#ff00ff'},{x:0,y:5,z:0,text:'F',color:'#00ffff'}];positions.forEach((pos,index)=>{constmaterial=createSpriteMaterial(pos.text,pos.color);constsprite=newTHREE.Sprite(material);// 设置位置sprite.position.set(pos.x,pos.y,pos.z);// 设置大小 - 精灵的大小通过scale控制sprite.scale.set(2,2,1);// 添加自定义数据sprite.userData={type:'interactiveSprite',id:index,text:pos.text,originalColor:pos.color,originalScale:{x:2,y:2,z:1}};sprite.name=`sprite_${pos.text}`;sprites.push(sprite);spriteGroup.add(sprite);});scene.add(spriteGroup);}// 创建精灵createSprites();// 添加一个立方体作为参考constcubeGeometry=newTHREE.BoxGeometry(1,1,1);constcubeMaterial=newTHREE.MeshBasicMaterial({color:0x888888,wireframe:true});constcube=newTHREE.Mesh(cubeGeometry,cubeMaterial);scene.add(cube);// 添加光源constambientLight=newTHREE.AmbientLight(0xffffff,0.4);scene.add(ambientLight);constdirectionalLight=newTHREE.DirectionalLight(0xffffff,0.6);directionalLight.position.set(10,10,5);scene.add(directionalLight);// 添加坐标轴辅助constaxesHelper=newTHREE.AxesHelper(10);scene.add(axesHelper);// 动画循环functionanimate(){requestAnimationFrame(animate);// 让精灵组缓慢旋转spriteGroup.rotation.y+=0.005;renderer.render(scene,camera);}animate();// 精灵模型拾取函数functionpickSprites(event){// 获取鼠标位置constmouse=newTHREE.Vector2();mouse.x=(event.clientX/window.innerWidth)*2-1;mouse.y=-(event.clientY/window.innerHeight)*2+1;// 创建Raycasterconstraycaster=newTHREE.Raycaster();raycaster.setFromCamera(mouse,camera);// 注意:精灵模型会自动被Raycaster检测,无需特殊配置// 检测相交constintersects=raycaster.intersectObjects(sprites);if(intersects.length>0){constintersect=intersects[0];constsprite=intersect.object;console.log('选中了精灵:',sprite.name);console.log('精灵数据:',sprite.userData);console.log('相交点:',intersect.point);console.log('距离:',intersect.distance);// 高亮效果:放大精灵constoriginalScale=sprite.userData.originalScale;sprite.scale.set(originalScale.x*1.5,originalScale.y*1.5,originalScale.z);// 3秒后恢复原大小setTimeout(()=>{sprite.scale.set(originalScale.x,originalScale.y,originalScale.z);},300);// 显示选中信息showSelectionInfo(sprite.userData);}}// 显示选中信息functionshowSelectionInfo(spriteData){// 移除旧的信息显示constoldInfo=scene.getObjectByName('selectionInfo');if(oldInfo)scene.remove(oldInfo);// 创建信息精灵constcanvas=document.createElement('canvas');canvas.width=512;canvas.height=128;constcontext=canvas.getContext('2d');// 绘制背景context.fillStyle='rgba(0, 0, 0, 0.8)';context.fillRect(0,0,canvas.width,canvas.height);// 绘制文字context.font='bold 40px Arial';context.fillStyle='#ffffff';context.textAlign='center';context.textBaseline='middle';context.fillText(`选中: 精灵${spriteData.text}(ID:${spriteData.id})`,canvas.width/2,canvas.height/2);// 创建纹理和精灵consttexture=newTHREE.CanvasTexture(canvas);constmaterial=newTHREE.SpriteMaterial({map:texture});constinfoSprite=newTHREE.Sprite(material);// 设置位置(相机上方)infoSprite.position.set(0,8,0);infoSprite.scale.set(8,2,1);infoSprite.name='selectionInfo';scene.add(infoSprite);// 5秒后移除信息setTimeout(()=>{if(infoSprite.parent)scene.remove(infoSprite);},5000);}// 添加点击事件window.addEventListener('click',pickSprites);// 挂载到DOMonMounted(()=>{constcontrols=newOrbitControls(camera,containerRef.value);controls.enableDamping=true;containerRef.value.appendChild(renderer.domElement);// 窗口大小变化处理window.addEventListener('resize',()=>{camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);});});</script><style>.container{width:100%;height:100vh;}</style>关键点说明:
- 精灵创建:使用
THREE.Sprite和SpriteMaterial - 大小控制:通过
sprite.scale.set()控制精灵大小 - 朝向:精灵始终面向相机(这是Sprite的特性)
- 拾取:Sprite会自动被Raycaster检测,无需特殊配置
- 纹理创建:通常使用Canvas创建动态纹理
总结对比
| 模型类型 | 关键配置 | 特点 | 拾取难度 |
|---|---|---|---|
| 点模型 | raycaster.params.Points.threshold | 需要设置阈值,可获取点索引 | 中等 |
| 线模型 | raycaster.params.Line.threshold | 需要增大阈值,线宽有限制 | 较高 |
| 精灵模型 | 无需特殊配置 | 始终面向相机,自动检测 | 容易 |
| 网格模型 | 无需特殊配置 | 最常见的3D物体 | 最容易 |
每个模型类型都有其特定的应用场景和拾取配置,根据实际需求选择合适的模型类型和拾取策略。