关于 camera
用于构建游戏摄像机的技能,介绍 viverse 内置角色摄像机及自定义第一或第三人称方案,也涵盖屏幕震动和随速度变化的视野效果。
主要功能
- 01角色环绕、碰撞与缩放
- 02自定义第一及第三人称摄像机
- 03空闲时返回静止姿态
- 04屏幕震动效果
- 05根据速度调整视野
适用场景
- 01设置玩家角色摄像机
- 02构建自定义第一人称或环绕摄像机
- 03空闲时将摄像机角度恢复至静止姿态
- 04为游戏摄像机添加震动或速度效果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ drawcall-ai/ skills --skill camera安装后在 Claude Code 中输入/camera确认已加载。
详细介绍
摄像机
游戏摄像机跟随目标,以受限的俯仰角和偏航角环绕,并支持碰撞检测和缩放。首先需要做一个选择:对于玩家角色,直接使用 viverse 提供的现成 CharacterCameraBehavior;只有在需要完全自定义的摄像机 rig 时,才从头构建摄像机。手动实现角色摄像机往往会重现相同的问题——偏航角锁定(无法上下瞄准)或穿墙,而内置实现已经解决了这些问题。
所有自定义 rig 都遵循同一约定:camera.rotation.set(pitch, yaw, 0, 'YXZ'),其中正俯仰角表示向上看,正偏航角表示向左看。'YXZ' 欧拉角顺序可确保俯仰角和偏航角正确组合,因此这里只说明一次,后文均默认遵循。
以下各节分别是:推荐:CharacterCameraBehavior(角色的内置实现)、从头构建摄像机(以 ECS 系统形式实现第一人称、第三人称和静止位置变体),以及效果(屏幕震动和 FOV 速度)。交叉参考:@pmndrs/viverse,以及 acta、physics、entity-component-system 和 math 技能。
推荐:CharacterCameraBehavior(用于角色)
对于玩家角色,优先使用此方案,而不是手动实现。CharacterCameraBehavior 会绕两个轴(俯仰角和偏航角,均有限制)旋转,检测摄像机与世界的碰撞,并处理缩放。
import { CharacterCameraBehavior, FirstPersonCharacterCameraBehavior } from '@pmndrs/viverse'
const cameraBehavior = new CharacterCameraBehavior() // third-person orbit + collision + zoom
// each frame, after moving the character:
cameraBehavior.update(camera, characterModel, delta, (ray, far) => world.raycast(ray, far)?.distance)
每帧在移动角色后调用 update。碰撞检测通过射线回调 (ray, far) => world.raycast(ray, far)?.distance 防止摄像机进入墙体。
update(camera, target, deltaTime, raycast?, options?) 就是完整的 API——没有 setOptions 方法。通过第 5 个 options 参数调整行为,每帧传入相同结构的参数:
options.rotation:minPitch/maxPitch/minYaw/maxYaw/speedoptions.zoom:minDistance/maxDistance/speedoptions.collisionoptions.characterBaseOffset
实例公开了 rotationPitch、rotationYaw 和 zoomDistance。在首次调用 update 之前设置这些属性,即可设定初始朝向和缩放距离。
FirstPersonCharacterCameraBehavior 并非独立类,而是一个现成的选项对象。将其作为第 5 个参数传入,即可切换到第一人称视角:
cameraBehavior.update(camera, characterModel, delta, raycast, FirstPersonCharacterCameraBehavior)
此方案可与 BvhCharacterPhysics(移动)和 Acta(动画)配合使用——参见 acta 和 physics 技能。移动和瞄准方向根据摄像机朝向,通过 camera.getWorldDirection 推导(正确包含俯仰角和横向移动)。信息流向为:输入 → Acta → physics。
characterBaseOffset 是相对于摄像机的:其水平部分会随摄像机偏航角旋转。因此,[0.5, 1.5, 0] 这样的越肩偏移量会在环绕时始终相对于视角保持在肩部上方,而纯垂直偏移(默认值以及第一人称情况)不受偏航角影响。角色无需面向摄像机,也能保持稳定的构图。
从头构建摄像机
对于完全自定义的 rig,可将第一人称和第三人称摄像机构建为精简的 ECS 系统——参见 entity-component-system 技能。
第一人称摄像机
第一人称摄像机会将自身定位在玩家头部,并根据累积的俯仰角和偏航角旋转。
export class FirstPersonCameraSystem extends createSystem({}) {
private pitch = 0
private yaw = 0
private headOffset = new Vector3(0, 1.7, 0)
init() {
RotatePitchAction.subscribe(
(delta) => {
this.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.pitch + delta))
},
{ signal },
)
RotateYawAction.subscribe(
(delta) => {
this.yaw += delta
},
{ signal },
)
}
update() {
// TODO: retrieve player position
camera.position.copy(playerPosition).add(this.headOffset)
camera.rotation.set(this.pitch, this.yaw, 0, 'YXZ')
}
}
俯仰角限制在 [-PI/2, PI/2];偏航角不受限制(this.yaw += delta)。订阅时传入 { signal } 以便清理。
第三人称摄像机
第三人称摄像机使用 Spherical 环绕玩家——参见 math 技能。
export class ThirdPersonCameraSystem extends createSystem({}) {
private pitch = 0.3
private yaw = 0
private radius = 5
private headOffset = new Vector3(0, 1.7, 0)
private spherical = new Spherical()
init() {
RotatePitchAction.subscribe(
(delta) => {
this.pitch = Math.max(0.1, Math.min(Math.PI / 2, this.pitch + delta))
},
{ signal },
)
RotateYawAction.subscribe(
(delta) => {
this.yaw += delta
},
{ signal },
)
}
update() {
// TODO: retrieve player position
this.spherical.set(this.radius, this.pitch, this.yaw)
camera.position.setFromSpherical(this.spherical).add(playerPosition).add(headOffset)
camera.lookAt(target)
}
}
俯仰角限制在 [0.1, PI/2]——请注意,其下限不同于第一人称的 -PI/2,以防摄像机绕到地面以下。偏航角不受限制,订阅时也使用相同的 { signal } 模式。
静止位置
玩家空闲时,摄像机可以平滑插值回静止姿势。由于 pitch 和 yaw 是摄像机系统的私有状态,静止姿势插值应放在拥有这些状态的系统内部——单独的系统无法访问并修改它们。
// add these fields to the FirstPerson/ThirdPerson camera system:
private restPitch = 0
private restYaw = 0
update() {
// ... position the camera from this.pitch / this.yaw as above ...
if (playerIsIdle) { // TODO: derive idle from input or velocity
this.pitch = MathUtils.lerp(this.pitch, this.restPitch, 0.05)
this.yaw = MathUtils.lerp(this.yaw, this.restYaw, 0.05)
}
}
效果
两种效果可叠加到任意摄像机上:屏幕震动和 FOV 速度。
屏幕震动
向摄像机位置应用一阵逐渐衰减的随机偏移。
export class ScreenShakeSystem extends createSystem({}) {
private intensity = 0
private remaining = 0
shake(intensity: number, duration: number) {
this.intensity = intensity
this.remaining = duration
}
update(delta: number) {
if (this.remaining <= 0) return
this.remaining -= delta * 1000
const t = this.remaining / 1000
camera.position.x += (Math.random() - 0.5) * this.intensity * t
camera.position.y += (Math.random() - 0.5) * this.intensity * t
}
}
shake(intensity, duration) 会启动一阵震动。注意单位处理:delta 的单位是秒,而 remaining 以毫秒为单位跟踪,因此 update 会减去 delta * 1000。系数 t = remaining / 1000 会同时缩放 x 和 y 方向的偏移,使震动在其持续期间逐渐衰减。
FOV 速度效果
玩家移动越快,视野范围越大,以传达速度感。
export class FOVSpeedSystem extends createSystem({}) {
private baseFOV = 75
private maxFOV = 100
private maxSpeed = 50
update() {
// TODO: compute/retrieve player speed
camera.fov = MathUtils.lerp(this.baseFOV, this.maxFOV, Math.min(speed / this.maxSpeed, 1))
camera.updateProjectionMatrix()
}
}
更改 fov 后,务必调用 camera.updateProjectionMatrix(),否则新值不会生效。