Three.js 点模型、线模型、精灵模型拾取实现

一、点模型(Points)拾取实现

实现步骤:

  1. 创建点模型:使用 THREE.Points 和点材质
  2. 设置点大小:在材质中设置 size 属性
  3. Raycaster配置:设置 Points 的拾取阈值
  4. 拾取检测:使用 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>

关键点说明:

  1. PointsMaterial.size:控制点的大小
  2. raycaster.params.Points.threshold:设置点拾取的敏感度
  3. intersect.index:获取选中点的索引
  4. intersect.point:获取选中点的具体位置

二、线模型(Line)拾取实现

实现步骤:

  1. 创建线模型:使用 THREE.LineTHREE.LineSegments
  2. Raycaster配置:设置 Line 的拾取阈值
  3. 提高拾取精度:通过辅助方法增加拾取成功率
  4. 处理拾取结果:获取线段信息

完整案例:

<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>

关键点说明:

  1. raycaster.params.Line.threshold:控制线拾取的敏感度
  2. 线宽限制:WebGL中大多不支持大于1的linewidth
  3. intersect.faceIndex:获取选中线段的索引
  4. 使用辅助标记:通过添加标记点显示拾取位置

三、精灵模型(Sprite)拾取实现

实现步骤:

  1. 创建精灵模型:使用 THREE.Sprite 和精灵材质
  2. 设置精灵大小:通过 scale 属性控制
  3. Raycaster配置:Sprite会自动被检测,无需特殊配置
  4. 处理拾取结果:获取精灵信息

完整案例:

<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>

关键点说明:

  1. 精灵创建:使用 THREE.SpriteSpriteMaterial
  2. 大小控制:通过 sprite.scale.set() 控制精灵大小
  3. 朝向:精灵始终面向相机(这是Sprite的特性)
  4. 拾取:Sprite会自动被Raycaster检测,无需特殊配置
  5. 纹理创建:通常使用Canvas创建动态纹理

总结对比

模型类型关键配置特点拾取难度
点模型raycaster.params.Points.threshold需要设置阈值,可获取点索引中等
线模型raycaster.params.Line.threshold需要增大阈值,线宽有限制较高
精灵模型无需特殊配置始终面向相机,自动检测容易
网格模型无需特殊配置最常见的3D物体最容易

每个模型类型都有其特定的应用场景和拾取配置,根据实际需求选择合适的模型类型和拾取策略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值