{T}

曲线与路径动画

Three.js 提供了丰富的曲线(Curve)和路径(Path)类,用于创建平滑的曲线轨迹、沿路径移动物体、生成管道几何体等。路径动画是 3D 场景中非常常见的需求,如相机飞行、物体轨道运动、粒子轨迹等。

系统架构

code
┌─────────────────────────────────────────────────────────────────────────┐
│                        曲线体系结构                                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   THREE.Curve (抽象基类)                                                 │
│   └── getPoint(t) → 返回参数 t 处的点 (t: 0~1)                         │
│       getTangent(t) → 返回参数 t 处的切线                                │
│       getPoints(divisions) → 获取等间距点数组                            │
│                                                                         │
│   ┌─────────────────────┬──────────────────────────┐                    │
│   │   Curve (3D 曲线)    │   CurvePath (组合路径)     │                    │
│   ├─────────────────────┼──────────────────────────┤                    │
│   │ • LineCurve3        │                          │                    │
│   │ • CatmullRomCurve3  │                          │                    │
│   │ • CubicBezierCurve3 │                          │                    │
│   │ • QuadraticBezierC.3│                          │                    │
│   │ • EllipseCurve      │                          │                    │
│   └──────────┬──────────┴──────────────────────────┘                    │
│              │                                                         │
│              ▼                                                         │
│   ┌──────────────────────────────────────────────┐                     │
│   │              应用场景                          │                     │
│   ├──────────────────────────────────────────────┤                     │
│   │ 1. 对象沿曲线运动 (getPoint + position)        │                     │
│   │ 2. 相机飞行路径 (lookAt + path)               │                     │
│   │ 3. TubeGeometry 管道/管道                      │                     │
│   │ 4. LatheGeometry 旋转体                        │                     │
│   │ 5. ExtrudeGeometry 拉伸形状                    │                     │
│   │ 6. Line 可视化曲线                             │                     │
│   │ 7. 粒子沿路径分布                              │                     │
│   └──────────────────────────────────────────────┘                     │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

参数 t 的含义:
t = 0.0  →  曲线的起点
t = 0.5  →  曲线的中点
t = 1.0  →  曲线的终点

注意:t 是归一化参数,不是弧长!均匀变化的 t 不代表均匀的移动距离。

概述

核心概念:参数 t

所有 Three.js 曲线都使用参数 t(范围 0 到 1)来表示曲线上的位置:

code
曲线上的点分布:
起点 ●━━━━━━━━━━━━━━━● 中点 ━━━━━━━━━━━━━━━ ● 终点
t=0                  t=0.5                   t=1.0

常用方法速查

方法说明返回值
getPoint(t)获取 t 处的点Vector3
getTangent(t)获取 t 处的单位切向量Vector3
getPoints(divisions)获取等参数间距的点数组Vector3[]
getSpacedPoints(divisions)获取近似等距的点数组Vector3[]
getLength()获取曲线总长度number
getLengths(divisions)获取各段的累计长度number[]
getPointAt(length)按弧长获取点Vector3
getUtoTmapping(u, distance)弧长到参数映射number
computeFrenetFrames(segments)计算法向量/副法向量Object
clone()克隆曲线Curve

Curve 基类

javascript
import * as THREE from 'three'

// Curve 是抽象基类,通常不直接实例化
// 但了解其接口有助于使用所有曲线子类

const curve = new SOME_CURVE_TYPE(...)

// ==================== 核心方法 ====================

// 获取曲线上的点 (t ∈ [0, 1])
const point = curve.getPoint(0.5)  // 返回 Vector3

// 获取切线方向
const tangent = curve.getTangent(0.5)  // 返回单位向量 Vector3

// 批量获取点
const points = curve.getPoints(50)     // 获取 51 个点(含首尾)
const spacedPoints = curve.getSpacedPoints(50)  // 近似等距的 51 个点

// 获取曲线长度
const length = curve.getLength()

// 按弧长获取点(更精确的匀速运动)
const pointAtLength = curve.getPointAt(0.3 * curve.getLength())

// 将曲线转换为 Line 几何体以可视化
const geometry = new THREE.BufferGeometry().setFromPoints(curve.getPoints(100))
const material = new THREE.LineBasicMaterial({ color: 0xff0000 })
const lineObject = new THREE.Line(geometry, material)
scene.add(lineObject)

CatmullRomCurve3 样条曲线

CatmullRomCurve3 是最常用的 3D 曲线类型,通过一系列控制点生成平滑的三次样条曲线。

基本用法

javascript
import * as THREE from 'three'

// 定义控制点
const points = [
  new THREE.Vector3(-10, 0, 10),
  new THREE.Vector3(-5, 5, 5),
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(5, -3, -5),
  new THREE.Vector3(10, 2, -10)
]

// 创建样条曲线
const curve = new THREE.CatmullRomCurve3(points)

// 可选参数
const curveWithOptions = new THREE.CatmullRomCurve3(
  points,           // 控制点数组
  false,            // closed: 是否闭合曲线(默认 false)
  'catmullrom',     // curveType: 插值类型
  0.5               // tension: 张力系数(0=松,1=紧)
)

// 获取曲线信息
console.log('曲线长度:', curve.getLength())
console.log('中点:', curve.getPoint(0.5))

曲线类型对比

javascript
const points = [
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(2, 4, 0),
  new THREE.Vector3(4, 4, 0),
  new THREE.Vector3(6, 0, 0)
]

// 三种插值类型
const types = ['centripetal', 'chordal', 'catmullrom']

types.forEach((type, i) => {
  const curve = new THREE.CatmullRomCurve3(points, false, type)
  
  const geo = new THREE.BufferGeometry().setFromPoints(curve.getPoints(50))
  const mat = new THREE.LineBasicMaterial({ 
    color: [0xff0000, 0x00ff00, 0x0000ff][i] 
  })
  const line = new THREE.Line(geo, mat)
  line.position.y = i * 2  // 错开显示
  scene.add(line)
})
类型说明特点
'catmullrom'标准 Catmull-Rom最常用,经典样条
'centripetal'向心 Catmull-Rom更好地处理尖角,不自交
'chordal'弦长 Catmull-Rom类似 centripetal 但更平滑

张力控制

javascript
const points = [
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(3, 5, 0),
  new THREE.Vector3(6, 2, 0),
  new THREE.Vector3(9, 6, 0)
]

// 不同张力效果
;[0, 0.25, 0.5, 0.75, 1].forEach((tension, i) => {
  const curve = new THREE.CatmullRomCurve3(points, false, 'catmullrom', tension)
  const geo = new THREE.BufferGeometry().setFromPoints(curve.getPoints(50))
  const mat = new THREE.LineBasicMaterial({ color: 0x444488 })
  const line = new THREE.Line(geo, mat)
  line.position.y = i * 1.5
  scene.add(line)
})

// tension = 0: 非常松弛,接近直线
// tension = 0.5: 默认值,平衡
// tension = 1: 非常紧绷,曲线贴近控制点多边形

闭合曲线

javascript
const points = [
  new THREE.Vector3(3, 0, 0),    // 右
  new THREE.Vector3(0, 3, 0),    // 上
  new THREE.Vector3(-3, 0, 0),   // 左
  new THREE.Vector3(0, -3, 0)    // 下
]

// 闭合曲线(首尾相连)
const closedCurve = new THREE.CatmullRomCurve3(points, true)  // true = 闭合

// 可视化
const geo = new THREE.BufferGeometry().setFromPoints(closedCurve.getPoints(100))
const mat = new THREE.LineBasicMaterial({ color: 0x00ff88 })
scene.add(new THREE.Line(geo, mat))

// 闭合曲线的 t=0 和 t=1 在同一个点
console.log(closedCurve.getPoint(0).equals(closedCurve.getPoint(1)))  // true

其他内置曲线类型

LineCurve3 直线段

javascript
const start = new THREE.Vector3(0, 0, 0)
const end = new THREE.Vector3(5, 3, 2)
const lineCurve = new THREE.LineCurve3(start, end)

lineCurve.getPoint(0)   // (0, 0, 0)
lineCurve.getPoint(0.5) // (2.5, 1.5, 1)
lineCurve.getPoint(1)   // (5, 3, 2)

CubicBezierCurve3 三次贝塞尔曲线

javascript
const bezier = new THREE.CubicBezierCurve3(
  new THREE.Vector3(0, 0, 0),    // 起点
  new THREE.Vector3(2, 5, 0),    // 控制点1
  new THREE.Vector3(4, -2, 3),   // 控制点2
  new THREE.Vector3(6, 3, 0)     // 终点
)

// 可视化
const points = bezier.getPoints(50)
const geo = new THREE.BufferGeometry().setFromPoints(points)
const mat = new THREE.LineBasicMaterial({ color: 0xff00ff })
scene.add(new THREE.Line(geo, mat))

QuadraticBezierCurve2 / QuadraticBezierCurve3 二次贝塞尔

javascript
// 2D 版本
const quad2d = new THREE.QuadraticBezierCurve(
  new THREE.Vector2(0, 0),      // 起点
  new THREE.Vector2(3, 5),      // 控制点
  new THREE.Vector2(6, 0)       // 终点
)

// 3D 版本
const quad3d = new THREE.QuadraticBezierCurve3(
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(3, 5, 2),
  new THREE.Vector3(6, 0, 0)
)

EllipseCurve 椭圆弧

javascript
const ellipse = new THREE.EllipseCurve(
  0, 0,              // 中心点 x, y
  5, 3,              // x 半轴, y 半轴
  0, Math.PI * 2,    // 起始角, 结束角(弧度)
  false,             // 顺时针(false)或逆时针(true)
  0                  // 旋转角度
)

// 获取 2D 点
const pts2d = ellipse.getPoints(100)

// 转换为 3D 用于渲染
const pts3d = pts2d.map(p => new THREE.Vector3(p.x, p.y, 0))

const geo = new THREE.BufferGeometry().setFromPoints(pts3d)
const mat = new THREE.LineBasicMaterial({ color: 0xffff00 })
scene.add(new THREE.Line(geo, mat))

Path 二维路径

Path 可以将多条曲线/直线组合成一条复合路径。

基本用法

javascript
const path = new THREE.Path()

// 移动到起始点(不画线)
path.moveTo(0, 0)

// 画直线到目标点
path.lineTo(5, 0)
path.lineTo(5, 5)

// 画二次贝塞尔曲线
path.quadraticCurveTo(2.5, 7, 0, 5)

// 闭合路径
path.closePath()

// 获取路径上的点
const points = path.getPoints(50)
const pts3d = points.map(p => new THREE.Vector3(p.x, p.y, 0))

const geo = new THREE.BufferGeometry().setFromPoints(pts3d)
const mat = new THREE.LineBasicMaterial({ color: 0x00ffff })
scene.add(new THREE.Line(geo, mat))

Path 的完整 API

javascript
const path = new THREE.Path()

// 基础操作
path.moveTo(x, y)                           // 移动画笔
path.lineTo(x, y)                           // 直线
path.closePath()                            // 闭合路径

// 贝塞尔曲线
path.quadraticCurveTo(cpx, cpy, x, y)       // 二次贝塞尔
path.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)  // 三次贝塞尔

// 圆弧
path.absarc(x, y, radius, startAngle, endAngle, anticlockwise)
path.arc(x, y, radius, startAngle, endAngle, anticlockwise)
path.absellipse(x, y, xRadius, yRadius, startAngle, endAngle, anticlockwise, rotation)
path.ellipse(...)  // 同上但相对坐标

// 样条曲线
path.splineThru(pointsArray)  // 通过一组点的样条

// 实用方法
path.getPoint(t)              // 获取路径上的点
path.getPoints(divisions)     // 获取点数组
path.getLength()              // 路径总长度

使用 Path 创建复杂形状

javascript
function createHeartPath() {
  const path = new THREE.Path()
  const scale = 5
  
  path.moveTo(0, scale * 0.35)
  
  for (let t = 0; t <= Math.PI * 2; t += 0.01) {
    const x = scale * 16 * Math.pow(Math.sin(t), 3) / 16
    const y = scale * (13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t)) / 16
    path.lineTo(x, y)
  }
  
  path.closePath()
  return path
}

沿曲线路径运动

基础:匀速运动

javascript
const curve = new THREE.CatmullRomCurve3([
  new THREE.Vector3(-10, 0, 0),
  new THREE.Vector3(-5, 3, 5),
  new THREE.Vector3(0, 1, 0),
  new THREE.Vector3(5, 4, -5),
  new THREE.Vector3(10, 0, 0)
])

const object = new THREE.Mesh(
  new THREE.SphereGeometry(0.3, 16, 16),
  new THREE.MeshStandardMaterial({ color: 0xff4444 })
)
scene.add(object)

let progress = 0
const speed = 0.001  // 运动速度

function animate() {
  requestAnimationFrame(animate)
  
  progress += speed
  if (progress > 1) progress = 0
  
  // 获取当前点并设置对象位置
  const point = curve.getPoint(progress)
  object.position.copy(point)
  
  renderer.render(scene, camera)
}

animate()

进阶:按弧长匀速运动

由于 getPoint(t) 的 t 不是弧长,直接递增 t 会导致在曲线弯曲处速度变快。使用 getPointAt 解决:

javascript
class PathFollower {
  constructor(object, curve, options = {}) {
    this.object = object
    this.curve = curve
    this.progress = options.start || 0
    this.speed = options.speed || 0.0005
    this.loop = options.loop !== false
    this.lookAhead = options.lookAhead || false
    this.lookAheadOffset = options.lookAheadOffset || 0.01
    
    this.totalLength = curve.getLength()
  }

  update(deltaTime) {
    if (!this.curve) return
    
    this.progress += this.speed * deltaTime * 60  // 归一化到 60fps
    
    if (this.progress > 1) {
      if (this.loop) {
        this.progress -= 1
      } else {
        this.progress = 1
      }
    }
    
    // 使用弧长参数化确保匀速
    const distance = this.progress * this.totalLength
    const point = this.curve.getPointAt(this.progress)
    
    this.object.position.copy(point)
    
    if (this.lookAhead) {
      const lookProgress = Math.min(this.progress + this.lookAheadOffset, 1)
      const lookPoint = this.curve.getPointAt(lookProgress)
      this.object.lookAt(lookPoint)
    }
  }

  setProgress(value) {
    this.progress = Math.max(0, Math.min(1, value))
  }

  reset() {
    this.progress = 0
  }
}

// 使用
const follower = new PathFollower(myObject, myCurve, {
  speed: 0.002,
  loop: true,
  lookAhead: true,
  lookAheadOffset: 0.02
})

const clock = new THREE.Clock()
function animate() {
  requestAnimationFrame(animate)
  follower.update(clock.getDelta())
  renderer.render(scene, camera)
}

缓动效果

javascript
// 缓动函数库
const Easing = {
  linear: t => t,
  easeInQuad: t => t * t,
  easeOutQuad: t => t * (2 - t),
  easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
  easeInCubic: t => t * t * t,
  easeOutCubic: t => (--t) * t * t + 1,
  easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
  easeInElastic: t => t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * (t - 1)) * Math.sin((t - 1.1) * 5 * Math.PI),
  easeOutElastic: t => t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t - 0.1) * 5 * Math.PI) + 1,
  easeInOutElastic: t => {
    if (t === 0 || t === 1) return t
    return (t *= 2) < 1
      ? -0.5 * Math.pow(2, 10 * (t - 1)) * Math.sin((t - 1.1) * 5 * Math.PI)
      : 0.5 * Math.pow(2, -10 * (t - 1)) * Math.sin((t - 1.1) * 5 * Math.PI) + 1
  },
  easeOutBounce: t => {
    const n1 = 7.5625, d1 = 2.75
    if (t < 1 / d1) return n1 * t * t
    else if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75
    else if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375
    else return n1 * (t -= 2.625 / d1) * t + 0.984375
  }
}

// 应用缓动的路径动画
class EasedPathAnimation {
  constructor(object, curve, duration, easingFn = Easing.easeInOutCubic) {
    this.object = object
    this.curve = curve
    this.duration = duration
    this.easingFn = easingFn
    this.startTime = null
    this.onComplete = null
  }

  start() {
    this.startTime = performance.now()
  }

  update() {
    if (!this.startTime) return
    
    const elapsed = performance.now() - this.startTime
    let rawT = elapsed / this.duration
    
    if (rawT >= 1) {
      rawT = 1
      if (this.onComplete) this.onComplete()
    }
    
    const easedT = this.easingFn(rawT)
    const point = this.curve.getPoint(easedT)
    this.object.position.copy(point)
    
    return rawT < 1
  }
}

多物体沿同一路径运动

javascript
const curve = new THREE.CatmullRomCurve3([...], true)  // 闭合曲线

const objects = []
for (let i = 0; i < 8; i++) {
  const mesh = new THREE.Mesh(
    new THREE.BoxGeometry(0.3, 0.3, 0.3),
    new THREE.MeshStandardMaterial({
      color: new THREE.Color().setHSL(i / 8, 0.8, 0.5)
    })
  )
  scene.add(mesh)
  objects.push(mesh)
}

let time = 0
function animate() {
  requestAnimationFrame(animate)
  time += 0.005
  
  objects.forEach((obj, i) => {
    // 每个对象有不同的偏移量,形成队列效果
    const offset = i / objects.length
    const t = (time + offset) % 1
    obj.position.copy(curve.getPoint(t))
    obj.rotation.x += 0.02
    obj.rotation.y += 0.02
  })
  
  renderer.render(scene, camera)
}

TubeGeometry 管道几何体

TubeGeometry 沿着一条 3D 曲线创建管道/管状几何体。

基本用法

javascript
const curve = new THREE.CatmullRomCurve3([
  new THREE.Vector3(-5, 0, -5),
  new THREE.Vector3(0, 3, 0),
  new THREE.Vector3(5, 0, 5),
  new THREE.Vector3(8, -2, 2)
])

const tubeGeometry = new THREE.TubeGeometry(
  curve,          // 曲线
  64,             // 分段数(越高越平滑)
  0.3,            // 管道半径
  12,             // 径向分段数
  false           // 是否闭合
)

const tubeMaterial = new THREE.MeshStandardMaterial({
  color: 0x4488ff,
  metalness: 0.3,
  roughness: 0.4,
  side: THREE.DoubleSide
})

const tube = new THREE.Mesh(tubeGeometry, tubeMaterial)
scene.add(tube)

参数详解

参数类型默认值说明
pathCurve必填沿其生成管道的曲线
tubularSegmentsNumber64管道方向的分段数
radiusNumber1管道半径
radialSegmentsNumber8截面的分段数(圆形精度)
closedBooleanfalse是否闭合管道

高级用法:变截面管道

javascript
// 自定义管道截面函数
class CustomTubeGeometry extends THREE.BufferGeometry {
  constructor(path, options = {}) {
    super()
    
    const {
      tubularSegments = 64,
      radialSegments = 12,
      closed = false,
      radiusFunction = () => 1  // 自定义半径函数 (t) => radius
    } = options

    const frames = path.computeFrenetFrames(tubularSegments, closed)
    const vertices = []
    const normals = []
    const uvs = []
    const indices = []

    for (let i = 0; i <= tubularSegments; i++) {
      const t = i / tubularSegments
      const point = path.getPointAt(t)
      const N = frames.normals[i]
      const B = frames.binormals[i]
      
      const r = radiusFunction(t)  // 动态半径
      
      for (let j = 0; j <= radialSegments; j++) {
        const v = j / radialSegments * Math.PI * 2
        const sin = Math.sin(v)
        const cos = -Math.cos(v)
        
        const normal = new THREE.Vector3()
        normal.x = cos * N.x + sin * B.x
        normal.y = cos * N.y + sin * B.y
        normal.z = cos * N.z + sin * B.z
        normal.normalize()
        
        vertices.push(
          point.x + r * normal.x,
          point.y + r * normal.y,
          point.z + r * normal.z
        )
        normals.push(normal.x, normal.y, normal.z)
        uvs.push(i / tubularSegments, j / radialSegments)
      }
    }

    for (let i = 0; i < tubularSegments; i++) {
      for (let j = 0; j < radialSegments; j++) {
        const a = i * (radialSegments + 1) + j
        const b = a + radialSegments + 1
        
        indices.push(a, b, a + 1)
        indices.push(b, b + 1, a + 1)
      }
    }

    this.setIndex(indices)
    this.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
    this.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
    this.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
  }
}

// 使用:两头细中间粗的管道
const customTube = new CustomTubeGeometry(curve, {
  tubularSegments: 100,
  radialSegments: 20,
  radiusFunction: (t) => {
    // 两端收缩
    const taper = Math.sin(t * Math.PI)
    return 0.2 + 0.5 * taper
  }
})

LatheGeometry 车削几何体

LatheGeometry 通过绕 Y 轴旋转一组 2D 点来生成旋转体(如花瓶、瓶子)。

基本用法

javascript
// 定义轮廓点(X 为半径,Y 为高度)
const points = []
for (let i = 0; i < 20; i++) {
  const t = i / 19
  const angle = t * Math.PI
  // 花瓶形状:底部宽 → 收腰 → 口部展开
  const radius = 0.5 + 0.3 * Math.sin(angle) + 0.1 * Math.sin(angle * 3)
  points.push(new THREE.Vector2(radius, t * 4 - 2))  // (半径, 高度)
}

const latheGeometry = new THREE.LatheGeometry(
  points,       // 轮廓点
  32            // 径向分段数
)

const latheMesh = new THREE.Mesh(
  latheGeometry,
  new THREE.MeshStandardMaterial({
    color: 0xcc9966,
    side: THREE.DoubleSide
  })
)
scene.add(latheMesh)

参数详解

参数类型默认值说明
pointsVector2[]必填轮廓点数组(每个点的 x=半径, y=高度)
segmentsNumber12绕 Y 轴的分段数
phiStartNumber0起始角度(弧度)
phiLengthNumber总旋转角度

常见旋转体示例

javascript
// 杯子
function createCupGeometry() {
  const points = []
  for (let i = 0; i < 30; i++) {
    const t = i / 29
    let r
    if (t < 0.05) {
      r = 0.8 + t * 4  // 底部厚边
    } else if (t > 0.85) {
      r = 0.9 + (t - 0.85) * 3  // 口部外翻
    } else {
      r = 1.0 - 0.15 * Math.sin((t - 0.05) / 0.8 * Math.PI)  // 收腰
    }
    points.push(new THREE.Vector2(r, t * 3))
  }
  return new THREE.LatheGeometry(points, 40)
}

// 灯泡
function createBulbGeometry() {
  const points = []
  for (let i = 0; i < 30; i++) {
    const t = i / 29
    const r = Math.sin(t * Math.PI) * (1 + 0.3 * Math.cos(t * Math.PI * 6))
    points.push(new THREE.Vector2(r * 0.8, t * 2.5))
  }
  return new THREE.LatheGeometry(points, 32)
}

ExtrudeGeometry 拉伸几何体

ExtrudeGeometry 将二维 Shape 沿 Z 轴或自定义路径拉伸为 3D 几何体。

基本拉伸

javascript
import { Shape, ExtrudeGeometry } from 'three'

// 创建 2D 形状
const shape = new Shape()

// 画一个星形
const outerRadius = 1
const innerRadius = 0.5
const points = 5

shape.moveTo(0, outerRadius)
for (let i = 0; i < points * 2; i++) {
  const angle = (i * Math.PI) / points - Math.PI / 2
  const r = i % 2 === 0 ? outerRadius : innerRadius
  shape.lineTo(Math.cos(angle) * r, Math.sin(angle) * r)
}
shape.closePath()

// 拉伸为 3D
const extrudeSettings = {
  depth: 0.5,           // 拉伸深度
  bevelEnabled: true,    // 启用倒角
  bevelThickness: 0.1,   // 倒角厚度
  bevelSize: 0.08,       // 倒角大小
  bevelSegments: 3       // 倒角分段数
}

const extrudeGeometry = new ExtrudeGeometry(shape, extrudeSettings)
const extrudeMesh = new THREE.Mesh(
  extrudeGeometry,
  new THREE.MeshStandardMaterial({ color: 0xffdd44 })
)
scene.add(extrudeMesh)

沿路径拉伸

javascript
const shape = new Shape()
shape.absarc(0, 0, 0.3, 0, Math.PI * 2, false)

const path = new THREE.CatmullRomCurve3([
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(2, 2, 0),
  new THREE.Vector3(4, 1, 2),
  new THREE.Vector3(6, 3, 1)
])

const extrudeOptions = {
  steps: 100,
  bevelEnabled: false,
  extrudePath: path
}

const geometry = new ExtrudeGeometry(shape, extrudeOptions)
const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0x44aaff }))
scene.add(mesh)

ExtrudeGeometry 参数

参数类型默认值说明
depthNumber1Z 轴方向的拉伸深度
bevelEnabledBooleantrue是否启用倒角
bevelThicknessNumber0.2倒角向外的厚度
bevelSizeNumber0.1倒角的宽度
bevelSegmentsNumber3倒角的细分程度
curveSegmentsNumber12形状曲线的细分
stepsNumber1沿深度方向的细分
extrudePathCurvenull自定义拉伸路径

Shape 与形状

Shape 常用方法

javascript
const shape = new THREE.Shape()

// 从点开始
shape.moveTo(10, 10)

// 直线
shape.lineTo(20, 10)
shape.lineTo(20, 20)

// 圆弧
shape.absarc(15, 20, 5, 0, Math.PI * 2, false)

// 闭合
shape.closePath()

// 孔洞(hole)
const hole = new THREE.Path()
hole.moveTo(13, 18)
hole.absarc(15, 18, 2, 0, Math.PI * 2, true)
shape.holes.push(hole)

// 使用 Shape 创建几何体
const geometry = new THREE.ShapeGeometry(shape)
// 或拉伸
const extruded = new THREE.ExtrudeGeometry(shape, { depth: 2 })

文字转 Shape

javascript
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js'
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js'

const loader = new FontLoader()
loader.load('fonts/helvetiker_regular.typeface.json', (font) => {
  const textGeometry = new TextGeometry('Hello Three.js', {
    font: font,
    size: 1,
    height: 0.2,
    curveSegments: 12,
    bevelEnabled: true,
    bevelThickness: 0.03,
    bevelSize: 0.02,
    bevelSegments: 5
  })

  textGeometry.center()

  const textMesh = new THREE.Mesh(
    textGeometry,
    new THREE.MeshStandardMaterial({ color: 0xffffff })
  )
  scene.add(textMesh)
})

相机飞行路径

基础相机飞行

javascript
class CameraFlight {
  constructor(camera, curve, options = {}) {
    this.camera = camera
    this.curve = curve
    this.duration = options.duration || 8000  // 毫秒
    this.easing = options.easing || ((t) => t < 0.5 ? 2*t*t : -1+(4-2*t)*t)
    this.lookAhead = options.lookAhead !== undefined ? options.lookAhead : true
    this.lookAheadDist = options.lookAheadDist || 0.01
    this.autoStart = options.autoStart || false
    this.onComplete = options.onComplete || null
    
    this.startTime = null
    this.isRunning = false
  }

  start() {
    this.startTime = performance.now()
    this.isRunning = true
  }

  stop() {
    this.isRunning = false
  }

  reset() {
    this.startTime = null
    this.isRunning = false
  }

  update() {
    if (!this.isRunning || !this.startTime) return true
    
    const elapsed = performance.now() - this.startTime
    let rawT = elapsed / this.duration
    
    if (rawT >= 1) {
      rawT = 1
      this.isRunning = false
      if (this.onComplete) this.onComplete()
    }
    
    const t = this.easing(rawT)
    
    // 设置相机位置
    const position = this.curve.getPoint(t)
    this.camera.position.copy(position)
    
    // 让相机朝向前方
    if (this.lookAhead && rawT < 1) {
      const lookT = Math.min(t + this.lookAheadDist, 1)
      const lookTarget = this.curve.getPoint(lookT)
      this.camera.lookAt(lookTarget)
    }
    
    return rawT < 1
  }
}

// 使用示例
const flightPath = new THREE.CatmullRomCurve3([
  new THREE.Vector3(0, 5, 20),
  new THREE.Vector3(-10, 8, 10),
  new THREE.Vector3(-5, 3, 0),
  new THREE.Vector3(10, 6, -5),
  new THREE.Vector3(5, 4, 5),
  new THREE.Vector3(0, 2, 0)
])

const flight = new CameraFlight(camera, flightPath, {
  duration: 6000,
  onComplete: () => console.log('飞行完成')
})

flight.start()

function animate() {
  requestAnimationFrame(animate)
  flight.update()
  controls.update()
  renderer.render(scene, camera)
}

带有控制点的相机路径编辑器

javascript
class CameraPathEditor {
  constructor(camera, scene) {
    this.camera = camera
    this.scene = scene
    this.controlPoints = []
    this.curve = null
    this.visualLine = null
    this.spheres = []
    this.isPlaying = false
  }

  addControlPoint(position) {
    const sphere = new THREE.Mesh(
      new THREE.SphereGeometry(0.15, 16, 16),
      new THREE.MeshBasicMaterial({ color: 0xff4444 })
    )
    sphere.position.copy(position)
    sphere.userData.isControlPoint = true
    this.scene.add(sphere)
    this.controlPoints.push(position.clone())
    this.spheres.push(sphere)
    this.updateCurve()
  }

  removeControlPoint(index) {
    if (index >= 0 && index < this.controlPoints.length) {
      this.controlPoints.splice(index, 1)
      this.scene.remove(this.spheres[index])
      this.spheres.splice(index, 1)
      this.updateCurve()
    }
  }

  clearAll() {
    this.controlPoints = []
    this.spheres.forEach(s => this.scene.remove(s))
    this.spheres = []
    if (this.visualLine) {
      this.scene.remove(this.visualLine)
      this.visualLine = null
    }
    this.curve = null
  }

  updateCurve() {
    if (this.controlPoints.length < 2) return
    
    if (this.visualLine) this.scene.remove(this.visualLine)
    
    this.curve = new THREE.CatmullRomCurve3(this.controlPoints.slice())
    
    const points = this.curve.getPoints(200)
    const geo = new THREE.BufferGeometry().setFromPoints(points)
    const mat = new THREE.LineDashedMaterial({
      color: 0xffff00,
      dashSize: 0.3,
      gapSize: 0.15
    })
    this.visualLine = new THREE.Line(geo, mat)
    this.visualLine.computeLineDistances()
    this.scene.add(this.visualLine)
  }

  play(duration = 5000) {
    if (!this.curve || this.controlPoints.length < 2) return
    
    this.isPlaying = true
    const startTime = performance.now()
    
    const animate = () => {
      if (!this.isPlaying) return
      
      const elapsed = performance.now() - startTime
      const t = Math.min(elapsed / duration, 1)
      
      const pos = this.curve.getPoint(t)
      this.camera.position.copy(pos)
      
      if (t < 1) {
        const lookPos = this.curve.getPoint(Math.min(t + 0.01, 1))
        this.camera.lookAt(lookPos)
        requestAnimationFrame(animate)
      } else {
        this.isPlaying = false
      }
    }
    
    animate()
  }

  stop() {
    this.isPlaying = false
  }

  exportPath() {
    return this.controlPoints.map(p => ({ x: p.x, y: p.y, z: p.z }))
  }

  importPath(data) {
    this.clearAll()
    data.forEach(p => {
      this.addControlPoint(new THREE.Vector3(p.x, p.y, p.z))
    })
  }
}

API 参考

Curve 公共方法

方法返回值说明
getPoint(t)Vector3获取参数 t 处的点
getPointAt(u)Vector3按弧长比例 u 获取点
getTangent(t)Vector3获取切线方向
getTangentAt(u)Vector3按弧长获取切线
getPoints(divisions)Vector3[]获取等参数点
getSpacedPoints(divisions)Vector3[]获取近似等距点
getLength()number曲线总长度
getLengths(divisions)number[]各段累计长度
computeFrenetFrames(segments)Object计算法线/副法线
clone()Curve克隆曲线
toJSON()object序列化为 JSON

CatmullRomCurve3 构造参数

参数类型默认值说明
pointsVector3[]必填控制点数组
closedBooleanfalse是否闭合
curveTypeString'catmullrom'插值类型
tensionNumber0.5张力系数

常见问题

Q: 物体沿曲线运动时速度不均匀?

这是因为 getPoint(t) 的参数 t 是均匀的,但曲线各段的弧长不同。解决方法:

javascript
// ❌ 不均匀:直接用 t
object.position.copy(curve.getPoint(progress))

// ✅ 均匀:使用 getPointAt
const totalLength = curve.getLength()
object.position.copy(curve.getPointAt(progress * totalLength))

Q: 如何让物体在曲线转弯时自然倾斜?

使用 Frenet Frames 计算法线和副法线:

javascript
const frames = curve.computeFrenetFrames(segments, false)

const t = progress
const idx = Math.floor(t * segments)
const normal = frames.normals[idx]
const binormal = frames.binormals[idx]

// 用法线和副法线构建物体的朝向矩阵
object.up.copy(binormal)
object.lookAt(curve.getPoint(Math.min(t + 0.01, 1)))

Q: 如何实现路径循环时无缝衔接?

javascript
// 使用闭合曲线
const curve = new THREE.CatmullRomCurve3(points, true)  // true = 闭合

// 或者手动处理衔接
if (progress >= 1) {
  progress -= 1
  // 平滑过渡到起点
}

Q: TubeGeometry 显示黑色/不正确?

检查材质是否设置了正确的面:

javascript
const material = new THREE.MeshStandardMaterial({
  side: THREE.DoubleSide  // 内外壁都渲染
})