曲线与路径动画
Three.js 提供了丰富的曲线(Curve)和路径(Path)类,用于创建平滑的曲线轨迹、沿路径移动物体、生成管道几何体等。路径动画是 3D 场景中非常常见的需求,如相机飞行、物体轨道运动、粒子轨迹等。
系统架构
plaintext
┌─────────────────────────────────────────────────────────────────────────┐
│ 曲线体系结构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 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)来表示曲线上的位置:
plaintext
曲线上的点分布:
起点 ●━━━━━━━━━━━━━━━● 中点 ━━━━━━━━━━━━━━━ ● 终点
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)参数详解
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
path | Curve | 必填 | 沿其生成管道的曲线 |
tubularSegments | Number | 64 | 管道方向的分段数 |
radius | Number | 1 | 管道半径 |
radialSegments | Number | 8 | 截面的分段数(圆形精度) |
closed | Boolean | false | 是否闭合管道 |
高级用法:变截面管道
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)参数详解
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
points | Vector2[] | 必填 | 轮廓点数组(每个点的 x=半径, y=高度) |
segments | Number | 12 | 绕 Y 轴的分段数 |
phiStart | Number | 0 | 起始角度(弧度) |
phiLength | Number | 2π | 总旋转角度 |
常见旋转体示例
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 参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
depth | Number | 1 | Z 轴方向的拉伸深度 |
bevelEnabled | Boolean | true | 是否启用倒角 |
bevelThickness | Number | 0.2 | 倒角向外的厚度 |
bevelSize | Number | 0.1 | 倒角的宽度 |
bevelSegments | Number | 3 | 倒角的细分程度 |
curveSegments | Number | 12 | 形状曲线的细分 |
steps | Number | 1 | 沿深度方向的细分 |
extrudePath | Curve | null | 自定义拉伸路径 |
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 构造参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
points | Vector3[] | 必填 | 控制点数组 |
closed | Boolean | false | 是否闭合 |
curveType | String | 'catmullrom' | 插值类型 |
tension | Number | 0.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 // 内外壁都渲染
})