一、点模型(Points)拾取实现
实现步骤:
- 创建点模型:使用
THREE.Points和点材质 - 设置点大小:在材质中设置
size属性 - Raycaster配置:设置
Points的拾取阈值 - 拾取检测:使用
intersectObjects检测相交
完整案例:
<template>
<div class="container" ref="containerRef"></div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const containerRef = ref(null);
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// 创建相机
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 10);
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// 创建点模型
function createPointCloud() {
// 创建100个随机点
const vertices = [];
for (let i = 0; i < 100; i++) {
const x = (Math.random() - 0.5) * 10;
const y = (Math.random() - 0.5) * 10;
const z = (Math.random() - 0.5) * 10;
vertices.push(x, y, z);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
// 创建点材质 - 关键:设置点大小
const material = new THREE.PointsMaterial({
color: 0xff0000,
size: 0.2, // 点的大小
sizeAttenuation: true // 点大小是否随距离衰减
});
// 创建点云对象
const points = new THREE.Points(geometry, material);
points.name = 'pointCloud';
scene.add(points);
return points;
}
// 创建点模型
const pointCloud = createPointCloud();
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 10, 5);
scene.add(directionalLight);
// 添加坐标轴辅助
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 让点云缓慢旋转
pointCloud.rotation.y += 0.005;
renderer.render(scene, camera);
}
animate();
// 点模型拾取函数
function pickPoints(event) {
// 1. 获取鼠标归一化设备坐标
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 2. 创建Raycaster
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
// 3. 关键:设置点模型的拾取阈值
// 这个值决定点击距离点多近才算选中,值越大越容易选中
raycaster.params.Points = { threshold: 0.2 };
// 4. 检测相交
const intersects = raycaster.intersectObjects([pointCloud]);
// 5. 处理结果
if (intersects.length > 0) {
const intersect = intersects[0];
console.log('选中了点:', intersect);
// 获取选中的点的索引
const pointIndex = intersect.index;
// 创建高亮点(在该位置添加一个更大的点)
const highlightGeometry = new THREE.BufferGeometry();
const position = intersect.object.geometry.attributes.position;
const pointPosition = [
position.getX(pointIndex),
position.getY(pointIndex),
position.getZ(pointIndex)
];
highlightGeometry.setAttribute('position',
new THREE.Float32BufferAttribute(pointPosition, 3));
const highlightMaterial = new THREE.PointsMaterial({
color: 0xffff00,
size: 0.5,
sizeAttenuation: true
});
const highlight = new THREE.Points(highlightGeometry, highlightMaterial);
highlight.name = 'highlightPoint';
// 移除之前的高亮点
const oldHighlight = scene.getObjectByName('highlightPoint');
if (oldHighlight) scene.remove(oldHighlight);
scene.add(highlight);
// 显示信息
console.log('点位置:', pointPosition);
console.log('点索引:', pointIndex);
}
}
// 添加点击事件
window.addEventListener('click', pickPoints);
// 挂载到DOM
onMounted(() => {
const controls = new OrbitControls(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>
<div class="container" ref="containerRef"></div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const containerRef = ref(null);
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);
// 创建相机
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// 创建复杂的线模型
function createComplexLine() {
// 创建曲线路径
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(-4, 0, 0),
new THREE.Vector3(-2, 3, 1),
new THREE.Vector3(0, 0, 2),
new THREE.Vector3(2, 3, 1),
new THREE.Vector3(4, 0, 0)
]);
// 获取曲线上的点
const points = curve.getPoints(50);
// 创建线几何体
const geometry = new THREE.BufferGeometry().setFromPoints(points);
// 创建线材质
const material = new THREE.LineBasicMaterial({
color: 0x00aaff,
linewidth: 3 // 注意:大多数浏览器不支持大于1的线宽
});
// 创建线对象
const line = new THREE.Line(geometry, material);
line.name = 'complexLine';
scene.add(line);
return line;
}
// 创建网格线(更容易拾取)
function createGridLines() {
const group = new THREE.Group();
// 创建水平线
for (let i = -5; i <= 5; i++) {
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-5, i, 0),
new THREE.Vector3(5, i, 0)
]);
const material = new THREE.LineBasicMaterial({
color: 0x666666,
linewidth: 2
});
const line = new THREE.Line(geometry, material);
line.userData.type = 'gridLine';
line.userData.index = i;
group.add(line);
}
// 创建垂直线
for (let i = -5; i <= 5; i++) {
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(i, -5, 0),
new THREE.Vector3(i, 5, 0)
]);
const material = new THREE.LineBasicMaterial({
color: 0x666666,
linewidth: 2
});
const line = new THREE.Line(geometry, material);
line.userData.type = 'gridLine';
line.userData.index = i;
group.add(line);
}
scene.add(group);
return group;
}
// 创建线模型
const complexLine = createComplexLine();
const gridLines = createGridLines();
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 10, 5);
scene.add(directionalLight);
// 添加坐标轴辅助
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 动画循环
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
// 线模型拾取函数
function pickLines(event) {
// 获取鼠标位置
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 创建Raycaster
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
// 关键:设置线模型的拾取阈值(增大以提高拾取成功率)
// 这个值表示距离线多远的点击算选中(单位:世界单位)
raycaster.params.Line = { threshold: 0.3 };
// 收集所有线对象
const lineObjects = [];
scene.traverse((object) => {
if (object.type === 'Line' || object.type === 'LineSegments') {
lineObjects.push(object);
}
});
// 检测相交
const intersects = raycaster.intersectObjects(lineObjects);
if (intersects.length > 0) {
const intersect = intersects[0];
const line = 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}`);
}
}
}
// 添加相交点标记
function addIntersectionMarker(position) {
// 移除旧的标记
const oldMarker = scene.getObjectByName('intersectionMarker');
if (oldMarker) scene.remove(oldMarker);
// 创建标记几何体
const markerGeometry = new THREE.SphereGeometry(0.1, 16, 16);
const markerMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000
});
const marker = new THREE.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);
// 挂载到DOM
onMounted(() => {
const controls = new OrbitControls(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>
<div class="container" ref="containerRef"></div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const containerRef = ref(null);
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111122);
// 创建相机
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 15);
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// 创建精灵材质(使用Canvas绘制纹理)
function createSpriteMaterial(text, color = '#ff0000') {
// 创建Canvas
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const context = 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);
// 创建纹理
const texture = new THREE.CanvasTexture(canvas);
// 创建精灵材质
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true
});
return material;
}
// 创建多个精灵
const sprites = [];
const spriteGroup = new THREE.Group();
function createSprites() {
const positions = [
{ 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) => {
const material = createSpriteMaterial(pos.text, pos.color);
const sprite = new THREE.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();
// 添加一个立方体作为参考
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0x888888, wireframe: true });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
scene.add(cube);
// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight.position.set(10, 10, 5);
scene.add(directionalLight);
// 添加坐标轴辅助
const axesHelper = new THREE.AxesHelper(10);
scene.add(axesHelper);
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 让精灵组缓慢旋转
spriteGroup.rotation.y += 0.005;
renderer.render(scene, camera);
}
animate();
// 精灵模型拾取函数
function pickSprites(event) {
// 获取鼠标位置
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 创建Raycaster
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
// 注意:精灵模型会自动被Raycaster检测,无需特殊配置
// 检测相交
const intersects = raycaster.intersectObjects(sprites);
if (intersects.length > 0) {
const intersect = intersects[0];
const sprite = intersect.object;
console.log('选中了精灵:', sprite.name);
console.log('精灵数据:', sprite.userData);
console.log('相交点:', intersect.point);
console.log('距离:', intersect.distance);
// 高亮效果:放大精灵
const originalScale = 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);
}
}
// 显示选中信息
function showSelectionInfo(spriteData) {
// 移除旧的信息显示
const oldInfo = scene.getObjectByName('selectionInfo');
if (oldInfo) scene.remove(oldInfo);
// 创建信息精灵
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 128;
const context = 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);
// 创建纹理和精灵
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({ map: texture });
const infoSprite = new THREE.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);
// 挂载到DOM
onMounted(() => {
const controls = new OrbitControls(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物体 | 最容易 |
每个模型类型都有其特定的应用场景和拾取配置,根据实际需求选择合适的模型类型和拾取策略。
1137

被折叠的 条评论
为什么被折叠?



