裁剪平面、选择高亮与辅助几何体
本篇涵盖 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
| 特性 | Sprite | CSS2DObject |
|---|---|---|
| 内容 | 图片/Canvas | HTML 元素 |
| 样式 | 受限于纹理 | 完整 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
})