| name | vtj-camera-system |
| description | Configures third-person camera systems with collision avoidance. Use when adjusting follow behavior, implementing camera modes, or handling cave/obstacle detection. |
vite-threejs Camera System
Overview
本项目采用 第三人称相机系统,核心组件:
- Camera:主相机类,管理模式切换
- CameraRig:第三人称跟随逻辑,处理平滑跟随、避障、bobbing 效果
核心原则:相机通过锚点跟随玩家,使用 lerp 平滑过渡,通过方块检测避免穿模。
When to Use
- 修改相机跟随行为
- 调整相机偏移和平滑度
- 实现新的相机模式
- 处理相机与地形的碰撞
相机架构
┌─────────────────────────────────────────────────────────────┐
│ Camera │
│ - mode: 'third-person' | 'bird-perspective' │
│ - perspectiveCamera: THREE.PerspectiveCamera │
│ - orbitControls: OrbitControls (鸟瞰模式用) │
│ - rig: CameraRig (第三人称用) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CameraRig │
│ - group: THREE.Group (跟随玩家位置) │
│ - cameraAnchor: THREE.Object3D (相机位置锚点) │
│ - targetAnchor: THREE.Object3D (看向目标锚点) │
│ - _smoothedPosition: 平滑后的位置 │
│ - _smoothedLookAtTarget: 平滑后的看向点 │
└─────────────────────────────────────────────────────────────┘
CameraRig 工作原理
锚点系统
this.group = new THREE.Group()
this.cameraAnchor = new THREE.Object3D()
this.targetAnchor = new THREE.Object3D()
this.cameraAnchor.position.copy(this.config.follow.offset)
this.targetAnchor.position.copy(this.config.follow.targetOffset)
this.group.add(this.cameraAnchor)
this.group.add(this.targetAnchor)
平滑跟随
update() {
const playerPos = this.target.position
const facingAngle = this.target.facingAngle
this._smoothedPosition.lerp(playerPos, this.config.follow.smoothSpeed)
this.group.position.copy(this._smoothedPosition)
this.group.rotation.y = facingAngle
const cameraPos = this.cameraAnchor.getWorldPosition(new THREE.Vector3())
const targetPos = this.targetAnchor.getWorldPosition(new THREE.Vector3())
this._smoothedLookAtTarget.lerp(targetPos, this.config.follow.lookAtSmoothSpeed)
return { cameraPos, targetPos: this._smoothedLookAtTarget, fov: this._currentFov }
}
配置参数
export const CAMERA_RIG_CONFIG = {
follow: {
offset: new THREE.Vector3(2, 1.5, 3.0),
targetOffset: new THREE.Vector3(0, 1.5, -5.5),
smoothSpeed: 0.1,
lookAtSmoothSpeed: 0.45,
mouseTargetY: {
sensitivity: 0.030,
maxOffset: 4.5,
returnSpeed: 1.5,
damping: 3.5,
},
},
trackingShot: {
fov: { baseFov: 55, maxFov: 85, speedThreshold: 3.0 },
bobbing: {
verticalFrequency: 4.0,
verticalAmplitude: 0.025,
horizontalFrequency: 4.0,
horizontalAmplitude: 0.015,
rollFrequency: 4.0,
rollAmplitude: 0.005,
idleBreathing: { enabled: true, frequency: 0.7, amplitude: 0.015 },
},
},
}
洞穴避障(非射线方式)
本项目使用 方块检测 而非射线检测来处理相机穿模:
_checkBlockAbovePlayer(playerPos) {
const checkHeights = [2, 3]
const checkRange = [-1, 0, 1]
let blockCount = 0
for (const heightOffset of checkHeights) {
for (const dx of checkRange) {
for (const dz of checkRange) {
const block = terrainManager.getBlockWorld(
playerPos.x + dx,
playerPos.y + heightOffset,
playerPos.z + dz
)
if (block && block.id !== 0) blockCount++
if (blockCount >= 4) return true
}
}
}
return false
}
洞穴模式处理
if (isInCave) {
this._caveOffset = new THREE.Vector3(0.0, 1.5, 1.0)
this._caveTargetOffset = new THREE.Vector3(0, 1.5, -1.5)
this.target.setOpacity(0.1)
} else {
this.target.setOpacity(1.0)
}
肩膀切换(左右视角)
toggleSide() {
this._currentSide *= -1
gsap.to(this, {
_sideFactor: this._currentSide,
duration: 0.6,
ease: 'power2.inOut',
})
}
this.config.follow.offset.x = this._targetOffset.x * this._sideFactor
触发方式:
emitter.on('input:toggle_camera_side', () => this.toggleSide())
动态 FOV(速度感)
_updateDynamicFov(speed) {
const { baseFov, maxFov, speedThreshold } = this.config.trackingShot.fov
const speedRatio = Math.min(speed / speedThreshold, 1.0)
const targetFov = baseFov + (maxFov - baseFov) * speedRatio
this._currentFov += (targetFov - this._currentFov) * smoothSpeed
}
事件监听
CameraRig 监听以下事件:
constructor() {
emitter.on('input:mouse_move', ({ movementY }) => {
this.mouseYVelocity += movementY * this.config.sensitivity
})
emitter.on('input:wheel', ({ deltaY }) => {
this._normalOffset.y += deltaY * sensitivity
})
emitter.on('input:toggle_camera_side', () => this.toggleSide())
emitter.on('settings:camera-rig-changed', ({ fov, bobbing }) => {
})
}
访问相机
this.camera = this.experience.camera
this.cameraInstance = this.experience.camera.instance
this.experience.camera.switchMode('bird-perspective')
this.experience.camera.attachRig(player)
添加新相机模式
Step 1: 定义模式
this.cameraModes = {
THIRD_PERSON: 'third-person',
BIRD_PERSPECTIVE: 'bird-perspective',
FIRST_PERSON: 'first-person',
}
Step 2: 实现切换逻辑
switchMode(mode) {
this.currentMode = mode
switch (mode) {
case 'first-person':
this.rig?.disable()
this.orbitControls.enabled = false
break
}
}
Common Mistakes
❌ 直接操作相机位置
this.camera.instance.position.set(0, 5, 10)
this.camera.rig.setOffset(new THREE.Vector3(0, 5, 10))
❌ 忘记平滑过渡
this.group.position.copy(playerPos)
this._smoothedPosition.lerp(playerPos, smoothSpeed)
this.group.position.copy(this._smoothedPosition)
❌ 使用射线检测碰撞
this.raycaster.set(cameraPos, direction)
const intersects = this.raycaster.intersectObjects(terrain)
const isInCave = this._checkBlockAbovePlayer(playerPos)
Quick Reference
| 需求 | 方法 |
|---|
| 获取相机实例 | this.experience.camera.instance |
| 切换相机模式 | this.experience.camera.switchMode('mode') |
| 调整跟随平滑度 | 修改 config.follow.smoothSpeed |
| 切换左右肩膀 | rig.toggleSide() |
| 响应设置变化 | 监听 settings:camera-rig-changed |
| 配置项 | 说明 |
|---|
follow.offset | 相机相对玩家的位置偏移 |
follow.targetOffset | 看向点相对玩家的偏移 |
follow.smoothSpeed | 位置平滑系数 (0-1) |
trackingShot.fov | 动态 FOV 配置 |
trackingShot.bobbing | 行走晃动配置 |