{T}

裁剪平面、选择高亮与辅助几何体

本篇涵盖 Three.js 中三个实用主题:裁剪平面(Clipping Planes)用于精确控制渲染区域、对象选择高亮(Selection Highlighting)用于交互反馈,以及精灵(Sprite)、线条(Line)和形状几何体等辅助几何体的使用。

第一部分:裁剪平面

裁剪平面概述

裁剪平面(Clipping Plane)是一个无限延伸的虚拟平面,用于在渲染时"切掉"场景中位于平面一侧的所有内容。这在建筑剖面图、医学可视化、产品截面展示等场景中非常有用。

code
裁剪平面原理:

        法向量 n
           ↑
           │
    ┌──────┼──────────┐
    │      │   渲染区  │  ← 平面正方向(保留)
    │      │ (visible)│
  ──┼──────●──────────┼── 裁剪平面(无限大)
    │      │          │
    │  裁剪区         │  ← 平面负方向(隐藏)
    │ (clipped)       │
    └─────────────────┘

平面方程: dot(point, normal) + constant = 0
  > 0 → 保留(可见)
  < 0 → 裁剪(不可见)

裁剪平面基础用法

javascript
import * as THREE from 'three'

// ==================== 基础配置 ====================

// 1. 创建裁剪平面
const clippingPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
// 参数:(法向量, 常数)
// 法向量指向的方向为"保留"侧,反方向为"裁剪"侧

// 2. 启用渲染器的局部裁剪
renderer.localClippingEnabled = true  // 必须!

// 3. 在材质上设置裁剪平面
const material = new THREE.MeshStandardMaterial({
  color: 0x4488ff,
  clippingPlanes: [clippingPlane],     // 应用裁剪平面数组
  clipShadows: true                     // 阴影也被裁剪
  clipIntersection: false               // false=并集(任一平面外), true=交集(所有平面内)
})

// 4. 创建被裁剪的对象
const geometry = new THREE.BoxGeometry(3, 3, 3)
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)

// 5. 可视化裁剪平面(调试用)
const planeHelper = new THREE.PlaneHelper(clippingPlane, 10, 0xff0000)
scene.add(planeHelper)  // 显示一个红色网格平面表示裁剪位置

移动裁剪平面

javascript
// 方法一:修改常量(沿法向移动)
// constant 为正值时,平面向法向反方向移动
clippingPlane.constant = -2  // 向上移动 2 个单位

// 方法二:使用 translate 方法
clippingPlane.translate(new THREE.Vector3(0, 1, 0), -1)

// 方法三:重新定义平面
clippingPlane.normal.set(0, 1, 0).normalize()
clippingPlane.constant = -position.y

// 动画示例:让裁剪平面来回移动
let time = 0
function animate() {
  requestAnimationFrame(animate)
  time += 0.02
  
  // 正弦运动
  clippingPlane.constant = Math.sin(time) * 2
  
  renderer.render(scene, camera)
}

多裁剪平面

Three.js 支持最多同时使用 8 个 裁剪平面:

javascript
const planes = [
  new THREE.Plane(new THREE.Vector3(1, 0, 0), 0),   // 右面裁剪
  new THREE.Plane(new THREE.Vector3(-1, 0, 0), 0),  // 左面裁剪
  new THREE.Plane(new THREE.Vector3(0, 1, 0), 0),   // 上方裁剪
  new THREE.Plane(new THREE.Vector3(0, -1, 0), 0),   // 下方裁剪
  new THREE.Plane(new THREE.Vector3(0, 0, 1), 0),   // 前面裁剪
  new THREE.Plane(new THREE.Vector3(0, 0, -1), 0)   // 后面裁剪
]

const material = new THREE.MeshStandardMaterial({
  color: 0x44aaff,
  clippingPlanes: planes,
  clipShadows: true
})

clipIntersection 模式

javascript
// clipIntersection: false(默认)
// 对象必须在所有平面之外才可见(OR 逻辑:任一平面外侧 = 可见)

// clipIntersection: true
// 对象必须在所有平面之内才可见(AND 逻辑:所有平面内侧 = 可见)
// 用于创建"窗口"效果——只显示多个平面围成的区域内的内容

const material = new THREE.MeshStandardMaterial({
  color: 0x44ff88,
  clippingPlanes: [
    new THREE.Plane(new THREE.Vector3(1, 0, 0), -2),
    new THREE.Plane(new THREE.Vector3(-1, 0, 0), -2),
    new THREE.Plane(new THREE.Vector3(0, 1, 0), -2),
    new THREE.Plane(new THREE.Vector3(0, -1, 0), -2)
  ],
  clipIntersection: true,  // 只显示 4x4 区域内的内容
  side: THREE.DoubleSide
})

动态裁剪控制

javascript
class ClippingPlaneController {
  constructor(renderer, scene, options = {}) {
    this.renderer = renderer
    this.scene = scene
    this.planes = []
    this.helpers = []
    this.maxPlanes = options.maxPlanes || 6
    
    renderer.localClippingEnabled = true
  }

  addPlane(normal = new THREE.Vector3(0, 1, 0), constant = 0) {
    if (this.planes.length >= this.maxPlanes) {
      console.warn(`已达到最大裁剪平面数 (${this.maxPlanes})`)
      return null
    }

    const plane = new THREE.Plane(normal.clone().normalize(), constant)
    
    const helper = new THREE.PlaneHelper(plane, 15, this.getPlaneColor(this.planes.length))
    this.scene.add(helper)

    this.planes.push(plane)
    this.helpers.push(helper)
    
    return { plane, helper }
  }

  removePlane(index) {
    if (index >= 0 && index < this.planes.length) {
      this.scene.remove(this.helpers[index])
      this.planes.splice(index, 1)
      this.helpers.splice(index, 1)
    }
  }

  clearAll() {
    this.helpers.forEach(h => this.scene.remove(h))
    this.planes = []
    this.helpers = []
  }

  setAllMaterials(materials) {
    materials.forEach(mat => {
      mat.clippingPlanes = [...this.planes]
      mat.clipShadows = true
    })
  }

  getPlaneColor(index) {
    const colors = [0xff4444, 0x44ff44, 0x4444ff, 0xffff44, 0xff44ff, 0x44ffff]
    return colors[index % colors.length]
  }

  // 将裁剪平面附加到对象上(随对象移动/旋转)
  attachToObject(object3d, planeIndex) {
    const plane = this.planes[planeIndex]
    const helper = this.helpers[planeIndex]

    object3d.add(helper)
    plane.applyMatrix4(object3d.matrixWorld)
  }
}

// 使用
const clipController = new ClippingPlaneController(renderer, scene)
clipController.addPlane(new THREE.Vector3(0, 1, 0), 0)

// 给材质应用裁剪
mesh.material.clippingPlanes = clipController.planes

实战:剖面视图

javascript
class SectionView {
  constructor(scene, camera, renderer) {
    this.scene = scene
    this.camera = camera
    this.renderer = renderer
    this.renderer.localClippingEnabled = true
    
    this.clipPlane = null
    this.helper = null
    this.sectionMeshes = []  // 被裁剪的物体
    this.fillMesh = null     // 截面填充
    this.isAnimating = false
  }

  enable(targetMeshes) {
    this.sectionMeshes = targetMeshes
    
    this.clipPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
    this.helper = new THREE.PlaneHelper(this.clipPlane, 20, 0xff6644)
    this.scene.add(this.helper)
    
    targetMeshes.forEach(mesh => {
      mesh.material.clippingPlanes = [this.clipPlane]
      mesh.material.clipShadows = true
    })
    
    this.createSectionFill()
  }

  createSectionFill() {
    // 创建一个大的平面作为截面填充
    const fillGeo = new THREE.PlaneGeometry(50, 50)
    const fillMat = new THREE.MeshBasicMaterial({
      color: 0xff6644,
      opacity: 0.15,
      transparent: true,
      side: THREE.DoubleSide
    })
    this.fillMesh = new THREE.Mesh(fillGeo, fillMat)
    this.fillMesh.visible = false
    this.scene.add(this.fillMesh)
  }

  setPlaneConstant(value) {
    if (!this.clipPlane) return
    this.clipPlane.constant = value
    
    // 同步更新截面填充的位置和朝向
    if (this.fillMesh) {
      this.fillMesh.position.set(0, -value, 0)
      this.fillMesh.rotation.x = -Math.PI / 2
      this.fillMesh.visible = true
    }
  }

  animate(fromValue, toValue, duration = 1000) {
    if (this.isAnimating) return
    this.isAnimating = true
    
    const startTime = performance.now()
    
    const step = () => {
      const elapsed = performance.now() - startTime
      const t = Math.min(elapsed / duration, 1)
      
      const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
      const current = fromValue + (toValue - fromValue) * eased
      
      this.setPlaneConstant(current)
      
      if (t < 1) {
        requestAnimationFrame(step)
      } else {
        this.isAnimating = false
      }
    }
    
    step()
  }

  disable() {
    if (this.helper) this.scene.remove(this.helper)
    if (this.fillMesh) this.scene.remove(this.fillMesh)
    
    this.sectionMeshes.forEach(mesh => {
      mesh.material.clippingPlanes = []
      mesh.material.clipShadows = false
    })
    
    this.clipPlane = null
    this.helper = null
    this.fillMesh = null
  }
}

第二部分:选择高亮

选择高亮概述

当用户点击或悬停在 3D 对象上时,提供视觉反馈是提升用户体验的关键。常见的高亮方式包括:

方式效果性能复杂度
边缘轮廓在对象周围绘制发光边框
颜色变化改变材质颜色最高最低
发光/Bloom对象发出光晕
OutlinePass后处理描边

轮廓高亮

最简单的方式是创建一个稍大的线框版本叠加在原对象上:

javascript
class OutlineHighlighter {
  constructor(scene) {
    this.scene = scene
    this.outlines = new Map()  // uuid -> outline mesh
  }

  highlight(object, color = 0xffffff, thickness = 0.05) {
    if (!object || !object.geometry) return
    
    const id = object.uuid
    
    if (this.outlines.has(id)) return  // 已高亮
    
    // 使用 EdgesGeometry 提取边缘
    const edgesGeo = new THREE.EdgesGeometry(object.geometry, 1)
    const lineMat = new THREE.LineBasicMaterial({ 
      color,
      linewidth: 1 
    })
    
    const outline = new THREE.LineSegments(edgesGeo, lineMat)
    outline.position.copy(object.position)
    outline.rotation.copy(object.rotation)
    outline.scale.copy(object.scale).multiplyScalar(1 + thickness)
    
    // 如果对象有父级,添加到同一父级
    if (object.parent) {
      object.parent.add(outline)
    } else {
      this.scene.add(outline)
    }
    
    this.outlines.set(id, outline)
  }

  unhighlight(object) {
    if (!object) return
    const id = object.uuid
    const outline = this.outlines.get(id)
    
    if (outline) {
      this.scene.remove(outline)
      outline.geometry.dispose()
      outline.material.dispose()
      this.outlines.delete(id)
    }
  }

  unhighlightAll() {
    this.outlines.forEach((outline, id) => {
      this.scene.remove(outline)
      outline.geometry.dispose()
      outline.material.dispose()
    })
    this.outlines.clear()
  }
}

// 使用
const highlighter = new OutlineHighlighter(scene)

raycaster.intersectObjects(objects).forEach(hit => {
  highlighter.highlight(hit.object, 0xffcc00)
})

发光效果

通过 Emissive 属性实现自发光高亮:

javascript
class EmissiveHighlighter {
  constructor() {
    this.originalColors = new Map()
    this.originalEmissive = new Map()
    this.originalIntensity = new Map()
  }

  highlight(object, highlightColor = 0xffaa00, intensity = 0.8) {
    if (!object?.material) return
    const id = object.uuid
    
    if (this.originalColors.has(id)) return  // 已高亮
    
    const mat = object.material
    
    // 保存原始值
    this.originalColors.set(id, mat.color.clone())
    this.originalEmissive.set(id, mat.emissive ? mat.emissive.clone() : new THREE.Color(0x000000))
    this.originalIntensity.set(id, mat.emissiveIntensity ?? 0)
    
    // 设置高亮
    mat.emissive = new THREE.Color(highlightColor)
    mat.emissiveIntensity = intensity
  }

  restore(object) {
    if (!object?.material) return
    const id = object.uuid
    
    const origColor = this.originalColors.get(id)
    const origEmis = this.originalEmissive.get(id)
    const origInt = this.originalIntensity.get(id)
    
    if (origColor && object.material) {
      object.material.color.copy(origColor)
      if (object.material.emissive) {
        object.material.emissive.copy(origEmis)
      }
      object.material.emissiveIntensity = origInt
    }
    
    this.originalColors.delete(id)
    this.originalEmissive.delete(id)
    this.originalIntensity.delete(id)
  }
}

颜色变化

最简单直接的高亮方式:

javascript
class ColorHighlighter {
  constructor() {
    this.saved = new Map()
  }

  highlight(object, color = 0xffcc00) {
    if (this.saved.has(object.uuid)) return
    
    const mat = object.material
    this.saved.set(object.uuid, {
      color: mat.color.clone(),
      opacity: mat.opacity,
      transparent: mat.transparent
    })
    
    mat.color.set(color)
    mat.opacity = 0.9
    mat.transparent = true
  }

  restore(object) {
    const saved = this.saved.get(object.uuid)
    if (!saved || !object.material) return
    
    object.material.color.copy(saved.color)
    object.material.opacity = saved.opacity
    object.material.transparent = saved.transparent
    
    this.saved.delete(object.uuid)
  }
}

OutlinePass 后处理

使用后处理实现高质量描边效果:

javascript
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'

class PostProcessHighlighter {
  constructor(scene, camera, renderer) {
    this.composer = new EffectComposer(renderer)
    
    this.composer.addPass(new RenderPass(scene, camera))
    
    this.outlinePass = new OutlinePass(
      new THREE.Vector2(window.innerWidth, window.innerHeight),
      scene,
      camera
    )
    
    this.outlinePass.edgeStrength = 3.0     // 边缘强度
    this.outlinePass.edgeGlow = 0.5         // 边缘发光
    this.outlinePass.edgeThickness = 2.0    // 边缘厚度
    this.outlinePass.pulsePeriod = 0        // 脉冲周期(0=不脉冲)
    this.outlinePass.visibleEdgeColor.set(0xffcc00)  // 可见边颜色
    this.outlinePass.hiddenEdgeColor.set(0x220000)   // 隐藏边颜色
    
    this.composer.addPass(this.outlinePass)
    this.composer.addPass(new OutputPass())
    
    this.selectedObjects = []
  }

  select(object) {
    this.selectedObjects = [object]
    this.outlinePass.selectedObjects = this.selectedObjects
  }

  deselect() {
    this.selectedObjects = []
    this.outlinePass.selectedObjects = []
  }

  render() {
    this.composer.render()
  }

  resize(width, height) {
    this.composer.setSize(width, height)
    this.outlinePass.resolution.set(width, height)
  }
}

// 使用
const ppHighlighter = new PostProcessHighlighter(scene, camera, renderer)

// 点击事件
function onClick(event) {
  raycaster.setFromCamera(mouse, camera)
  const hits = raycaster.intersectObjects(clickableObjects)
  
  if (hits.length > 0) {
    ppHighlighter.select(hits[0].object)
  } else {
    ppHighlighter.deselect()
  }
}

// 渲染循环中使用 composer.render() 替代 renderer.render()

实战:可选中场景

javascript
class SelectableScene {
  constructor(scene, camera, renderer, container) {
    this.scene = scene
    this.camera = camera
    this.renderer = renderer
    this.container = container
    
    this.raycaster = new THREE.Raycaster()
    this.mouse = new THREE.Vector2()
    this.selectableObjects = []
    this.hoveredObject = null
    this.selectedObject = null
    
    this.highlighters = {
      hover: new EmissiveHighlighter(),
      select: new PostProcessHighlighter(scene, camera, renderer)
    }
    
    this.bindEvents()
  }

  registerSelectable(object, data = {}) {
    object.userData.selectable = true
    Object.assign(object.userData, data)
    this.selectableObjects.push(object)
  }

  bindEvents() {
    this.container.addEventListener('mousemove', (e) => this.onMouseMove(e))
    this.container.addEventListener('click', (e) => this.onClick(e))
  }

  getNormalizedMouse(event) {
    const rect = this.container.getBoundingClientRect()
    this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
    this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
  }

  onMouseMove(event) {
    this.getNormalizedMouse(event)
    this.raycaster.setFromCamera(this.mouse, this.camera)
    
    const hits = this.raycaster.intersectObjects(
      this.selectableObjects.filter(o => o.userData.selectable)
    )
    
    const newHovered = hits.length > 0 ? hits[0].object : null
    
    if (newHovered !== this.hoveredObject) {
      if (this.hoveredObject && this.hoveredObject !== this.selectedObject) {
        this.highlighters.hover.restore(this.hoveredObject)
      }
      
      this.hoveredObject = newHovered
      
      if (this.hoveredObject && this.hoveredObject !== this.selectedObject) {
        this.highlighters.hover.highlight(this.hoveredObject, 0x88ccff, 0.5)
      }
      
      this.container.style.cursor = this.hoveredObject ? 'pointer' : 'default'
      
      if (this.onHoverChange) {
        this.onHoverChange(this.hoveredObject)
      }
    }
  }

  onClick(event) {
    if (this.hoveredObject) {
      if (this.selectedObject === this.hoveredObject) {
        this.deselect()
      } else {
        this.select(this.hoveredObject)
      }
    } else {
      this.deselect()
    }
  }

  select(object) {
    if (this.selectedObject) {
      this.highlighters.hover.restore(this.selectedObject)
    }
    
    this.selectedObject = object
    this.highlighters.select.select(object)
    
    if (this.onSelectChange) {
      this.onSelectChange(object)
    }
  }

  deselect() {
    if (this.selectedObject) {
      this.highlighters.select.deselect()
      this.highlighters.hover.restore(this.selectedObject)
      this.selectedObject = null
      
      if (this.onSelectChange) {
        this.onSelectChange(null)
      }
    }
  }

  update() {
    // 使用 post-processing composer 替代普通渲染
    this.highlighters.select.render()
  }
}

第三部分:辅助几何体

Sprite 精灵

Sprite 是始终面向相机的 2D 平面,常用于粒子效果、图标标记、标签等。

javascript
import * as THREE from 'three'

// ==================== 基础 Sprite ====================
const spriteMaterial = new THREE.SpriteMaterial({
  map: texture,              // 纹理
  color: 0xffffff,
  sizeAttenuation: true,     // 近大远小(透视相机下有效)
  rotation: 0                // 旋转弧度
})

const sprite = new THREE.Sprite(spriteMaterial)
sprite.position.set(5, 3, 2)
sprite.scale.set(2, 2, 1)  // 宽度, 高度, z 无效
scene.add(sprite)

// ==================== 从 Canvas 创建纹理 ====================
function createTextSprite(text, options = {}) {
  const {
    fontSize = 48,
    fontFace = 'Arial',
    textColor = '#ffffff',
    backgroundColor = 'rgba(0,0,0,0.6)',
    padding = 12,
    borderRadius = 8
  } = options

  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')
  
  ctx.font = `${fontSize}px ${fontFace}`
  const metrics = ctx.measureText(text)
  
  const width = metrics.width + padding * 2
  const height = fontSize * 1.4 + padding * 2
  
  canvas.width = width * 2  // 高 DPI
  canvas.height = height * 2
  ctx.scale(2, 2)
  
  // 背景
  ctx.fillStyle = backgroundColor
  roundRect(ctx, 0, 0, width, height, borderRadius)
  ctx.fill()
  
  // 文字
  ctx.fillStyle = textColor
  ctx.font = `${fontSize}px ${fontFace}`
  ctx.textAlign = 'center'
  ctx.textBaseline = 'middle'
  ctx.fillText(text, width / 2, height / 2)
  
  const texture = new THREE.CanvasTexture(canvas)
  texture.needsUpdate = true
  
  const material = new THREE.SpriteMaterial({
    map: texture,
    transparent: true,
    depthTest: false,       // 不参与深度测试(始终在最前)
    depthWrite: false
  })
  
  const sprite = new THREE.Sprite(material)
  sprite.scale.set(width / 100, height / 100, 1)
  
  return sprite
}

function roundRect(ctx, x, y, w, h, r) {
  ctx.beginPath()
  ctx.moveTo(x + r, y)
  ctx.lineTo(x + w - r, y)
  ctx.quadraticCurveTo(x + w, y, x + w, y + r)
  ctx.lineTo(x + w, y + h - r)
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h)
  ctx.lineTo(x + r, y + h)
  ctx.quadraticCurveTo(x, y + h, x, y + h - r)
  ctx.lineTo(x, y + r)
  ctx.quadraticCurveTo(x, y, x + r, y)
  ctx.closePath()
}

// 使用
const label = createTextSprite('重要地标', {
  fontSize: 32,
  textColor: '#ffffff',
  backgroundColor: 'rgba(255,68,68,0.85)'
})
label.position.set(0, 5, 0)
scene.add(label)

Sprite vs CSS2DObject

特性SpriteCSS2DObject
内容图片/CanvasHTML 元素
样式受限于纹理完整 CSS 支持
事件需射线检测原生 DOM 事件
性能更好略差
文字清晰度取决于分辨率始终清晰
适用图标、粒子信息卡片、标签

Line 线条几何体

javascript
// ==================== Line(基础线段)====================
const points = [
  new THREE.Vector3(-5, 0, 0),
  new THREE.Vector3(0, 3, 0),
  new THREE.Vector3(5, 0, 0)
]

const lineGeometry = new THREE.BufferGeometry().setFromPoints(points)
const lineMaterial = new THREE.LineBasicMaterial({ 
  color: 0xff4444,
  linewidth: 1  // 注意:大多数 WebGL 实现不支持 > 1 的 linewidth
})
const line = new THREE.Line(lineGeometry, lineMaterial)
scene.add(line)

// ==================== LineLoop(闭合环线)====================
const loopPoints = [
  new THREE.Vector3(-2, 0, -2),
  new THREE.Vector3(2, 0, -2),
  new THREE.Vector3(2, 0, 2),
  new THREE.Vector3(-2, 0, 2)
]
const loopGeo = new THREE.BufferGeometry().setFromPoints(loopPoints)
const loopLine = new THREE.LineLoop(loopGeo, new THREE.LineBasicMaterial({ color: 0x00ff00 }))
scene.add(loopLine)

// ==================== LineSegments(独立线段)====================
// 每 两个点构成一条独立的线段
const segPoints = [
  new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 0, 0),  // 第一条
  new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 1, 0),  // 第二条
  new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, 1)   // 第三条
]
const segGeo = new THREE.BufferGeometry().setFromPoints(segPoints)
const segLine = new THREE.LineSegments(segGeo, new THREE.LineBasicMaterial({ color: 0x4444ff }))
scene.add(segLine)  // 绘制坐标轴

// ==================== LineDashedMaterial(虚线)====================
const dashPoints = [
  new THREE.Vector3(-5, 0, 0),
  new THREE.Vector3(5, 0, 0)
]
const dashGeo = new THREE.BufferGeometry().setFromPoints(dashPoints)
const dashMat = new THREE.LineDashedMaterial({
  color: 0xffff00,
  dashSize: 0.5,      // 实线段长度
  gapSize: 0.25,      // 间隙长度
  scale: 1            // 缩放因子
})
const dashedLine = new THREE.Line(dashGeo, dashMat)
dashedLine.computeLineDistances()  // 必须调用!
scene.add(dashedLine)

// ==================== 曲线可视化 =====================
const curve = new THREE.CatmullRomCurve3([
  new THREE.Vector3(-5, 0, -5),
  new THREE.Vector3(-2, 3, 0),
  new THREE.Vector3(2, -1, 2),
  new THREE.Vector3(5, 2, 5)
])

const curvePoints = curve.getPoints(100)
const curveGeo = new THREE.BufferGeometry().setFromPoints(curvePoints)
const curveLine = new THREE.Line(curveGeo, new THREE.LineBasicMaterial({ color: 0xff00ff }))
scene.add(curveLine)

// ==================== 箭头辅助器 =====================
const dir = new THREE.Vector3(1, 2, 0).normalize()
const origin = new THREE.Vector3(0, 0, 0)
const length = 5
const arrowHelper = new THREE.ArrowHelper(dir, origin, length, 0xff8800, 0.5, 0.3)
scene.add(arrowHelper)

// ==================== 坐标轴辅助器 =====================
const axesHelper = new THREE.AxesHelper(5)  // 线长 5
scene.add(axesHelper)  // X红 Y绿 Z蓝

// ==================== 网格辅助器 =====================
const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x888888)
scene.add(gridHelper)

// ==================== 自定义线宽方案(使用 TubeGeometry)====================
function createThickLine(points, radius = 0.05, color = 0xff0000) {
  const curve = new THREE.CatmullRomCurve3(points)
  const tubeGeo = new THREE.TubeGeometry(curve, points.length * 4, radius, 8, false)
  const tubeMat = new THREE.MeshBasicMaterial({ color })
  return new THREE.Mesh(tubeGeo, tubeMat)
}

const thickLine = createThickLine([
  new THREE.Vector3(0, 0, -5),
  new THREE.Vector3(2, 3, 0),
  new THREE.Vector3(-1, 1, 5)
], 0.08, 0x00ffcc)
scene.add(thickLine)

Shape 形状几何体

Shape 用于定义二维轮廓,可以转换为多种 3D 几何体。

javascript
// ==================== 定义 Shape ====================
const shape = new THREE.Shape()

// 画一个房子形状
shape.moveTo(-2, 0)
shape.lineTo(2, 0)
shape.lineTo(2, 2)
shape.lineTo(0, 4)      // 屋顶尖端
shape.lineTo(-2, 2)
shape.closePath()

// 添加孔洞(窗户)
const hole = new THREE.Path()
hole.moveTo(-0.7, 0.7)
hole.lineTo(0.7, 0.7)
hole.lineTo(0.7, 1.7)
hole.lineTo(-0.7, 1.7)
hole.closePath()
shape.holes.push(hole)

// ==================== ShapeGeometry(2D 平面)====================
const shapeGeo = new THREE.ShapeGeometry(shape)
const shapeMesh = new THREE.Mesh(shapeGeo, new THREE.MeshStandardMaterial({
  color: 0xcc9966,
  side: THREE.DoubleSide
}))
scene.add(shapeMesh)

// ==================== ExtrudeGeometry(拉伸成 3D)====================
const extrudeSettings = {
  depth: 1,             // 拉伸深度
  bevelEnabled: true,
  bevelThickness: 0.2,
  bevelSize: 0.1,
  bevelSegments: 3
}

const extrudeGeo = new THREE.ExtrudeGeometry(shape, extrudeSettings)
extrudeGeo.center()

const extrudeMesh = new THREE.Mesh(extrudeGeo, new THREE.MeshStandardMaterial({
  color: 0x88aacc,
  metalness: 0.2,
  roughness: 0.6
}))
scene.add(extrudeMesh)

// ==================== LatheGeometry(旋转体)====================
// 用 Shape 的点创建花瓶
const vaseShape = new THREE.Shape()
vaseShape.absarc(0, 0, 0.5, 0, Math.PI * 2, false)
vaseShape.moveTo(0.5, 0)
for (let i = 1; i <= 20; i++) {
  const t = i / 20
  const r = 0.5 + 0.3 * Math.sin(t * Math.PI * 2) + 0.1 * Math.sin(t * Math.PI * 6)
  vaseShape.lineTo(r, t * 4)
}

const vaseGeo = new THREE.LatheGeometry(vaseShape.getPoints(30), 32)
const vaseMesh = new THREE.Mesh(vaseGeo, new THREE.MeshStandardMaterial({
  color: 0xdddddd,
  side: THREE.DoubleSide
}))
scene.add(vaseMesh)

辅助工具类

Three.js 内置了丰富的辅助工具类,用于调试和开发:

javascript
// ==================== 常用辅助器 =====================

// Box3Helper - 包围盒可视化
const box = new THREE.Box3().setFromObject(myMesh)
const boxHelper = new THREE.Box3Helper(box, 0x00ff00)
scene.add(boxHelper)

// SphereHelper - 包围球可视化
const sphere = new THREE.Sphere()
box.getBoundingSphere(sphere)
const sphereHelper = new THREE.SphereHelper(sphere, 0xff8800)
scene.add(sphereHelper)

// PolarGridHelper - 极坐标网格
const polarGrid = new THREE.PolarGridHelper(10, 16, 8, 64, 0x444444, 0x888888)
scene.add(polarGrid)

// CameraHelper - 相机视锥体
const helperCam = new THREE.PerspectiveCamera(75, 16/9, 0.1, 100)
helperCam.position.set(3, 3, 3)
helperCam.lookAt(0, 0, 0)
const camHelper = new THREE.CameraHelper(helperCam)
scene.add(camHelper)

// DirectionalLightHelper - 方向光
const lightHelper = new THREE.DirectionalLightHelper(directionalLight, 2)
scene.add(lightHelper)

// PointLightHelper - 点光源
const pointLightHelper = new THREE.PointLightHelper(pointLight, 0.5)
scene.add(pointLightHelper)

// SpotLightHelper - 聚光灯
const spotHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotHelper)

// SkeletonHelper - 骨骼
if (model.skeleton) {
  const skeletonHelper = new THREE.SkeletonHelper(model)
  scene.add(skeletonHelper)
}

// ==================== TransformControls 可视化变换工具 =====================
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'

const transformControl = new TransformControls(camera, renderer.domElement)
scene.add(transformControl)

transformControl.attach(targetObject)
transformControl.setMode('translate')  // 'translate' | 'rotate' | 'scale'

// 防止拖拽时 OrbitControls 冲突
transformControl.addEventListener('dragging-changed', (e) => {
  orbitControls.enabled = !e.value
})