HyG Front-end Dev Engineer

Three.js 之 2 Camera 相机

2022-05-16
HyG

本系列为 Three.js journey 教程学习笔记。

Camera 相机

查看 Three.js 的文档,可以看到 Camera 是一个抽象类,一般不直接使用,其他类型的 Camera 实现了这个抽象类。有

  • ArrayCamera 包含着一组子摄像机,常用于多人同屏的渲染,更好地提升VR场景的渲染性能
  • StereoCamera 双透视摄像机(立体相机),常用于创建 3D 立体影像,比如 3D 电影之类或 VR
  • CubeCamera 有6个渲染,分别是立方体的6个面,常用于渲染环境、反光等
  • OrthographicCamera 正交相机,在这种投影模式下,无论物体距离相机距离远或者近,在最终渲染的图片中物体的大小都保持不变。这对于渲染2D场景或者UI元素是非常有用的。
  • PerspectiveCamera 透视相机,这一投影模式被用来模拟人眼所看到的景象,它是3D场景的渲染中使用得最普遍的投影模式。

PerspectiveCamera 透视相机

PerspectiveCamera(fov : Number, aspect : Number, near : Number, far : Number)
  • fov — 摄像机视锥体垂直视野角度
  • aspect — 摄像机视锥体长宽比
  • near — 摄像机视锥体近端面
  • far — 摄像机视锥体远端面

这些参数一起定义了摄像机的 viewing frustum(视锥体)。

透视图中,灰色的部分是视锥体,是可能被渲染的物体所在的区域。fov 是视锥体竖直方向上的张角(是角度制而非弧度制),如侧视图所示。

aspect 等于 width / height,是照相机水平方向和竖直方向长度的比值,通常设为 Canvas 的横纵比例。

near 和 far 分别是照相机到视锥体最近、最远的距离,均为正值,且 fa r应大于 near。

但请注意,不要将 near 和 far 设置为比较极端的数值,如 0.0001 和 99999,这可能引起 bug,让 threejs 无法分辨物体的前后,导致闪动

import * as THREE from 'three'
// Canvas
const canvas = document.querySelector('#mainCanvas') as HTMLCanvasElement

// Scene
const scene = new THREE.Scene()

// Object
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({
    color: 0x607d8b,
  }),
)
scene.add(cube)

// Camera
const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 1, 100)
camera.position.set(1, 1, 3)
camera.lookAt(cube.position)

// Renderer
const renderer = new THREE.WebGLRenderer({
  canvas,
})
renderer.setSize(canvas.clientWidth, canvas.clientHeight)
renderer.render(scene, camera)

OrthographicCamera 正交相机

构造函数

OrthographicCamera( left : Number, right : Number, top : Number, bottom : Number, near : Number, far : Number )
  • left — 摄像机视锥体左侧面。
  • right — 摄像机视锥体右侧面。
  • top — 摄像机视锥体上侧面。
  • bottom — 摄像机视锥体下侧面。
  • near — 摄像机视锥体近端面。
  • far — 摄像机视锥体远端面。

这些参数一起定义了摄像机的 viewing frustum(视锥体),下图灰色部分为 frustum,只有在视景体内部(下图中的灰色部分)的物体才可能显示在屏幕上,而视景体外的物体会在显示之前被裁减掉。

为了保持照相机的横竖比例,需要保证 (right - left)(top - bottom) 的比例与 Canvas 宽度与高度的比例一致。

import * as THREE from 'three'
import stats from '../common/stats'

// Canvas
const canvas = document.querySelector('#mainCanvas') as HTMLCanvasElement

// Scene
const scene = new THREE.Scene()

// Object
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({
    color: 0x607d8b,
  }),
)
scene.add(cube)

// Camera
const aspectRatio = canvas.clientWidth / canvas.clientHeight
const camera = new THREE.OrthographicCamera(-1 * aspectRatio, 1 * aspectRatio, 1, -1, 1, 100)
camera.position.set(2, 2, 2)
camera.lookAt(cube.position)

// Renderer
const renderer = new THREE.WebGLRenderer({
  canvas,
})
renderer.setSize(canvas.clientWidth, canvas.clientHeight)

// Clock
const clock = new THREE.Clock()

// Animations
const tick = () => {
  stats.begin()

  const delta = clock.getDelta()

  cube.rotation.y += 1 * delta

  // Render
  renderer.render(scene, camera)
  stats.end()
  requestAnimationFrame(tick)
}

tick()

效果如下:

相机控制

尝试用鼠标控制相机的位置,首先创建一个监听 mousemove 的事件工具函数

export const captureMouse = (element: HTMLElement) => {
  const mouse: {
    x: number
    y: number
    event: MouseEvent | null
  } = {
    x: 0,
    y: 0,
    event: null,
  }
  const { offsetLeft, offsetTop } = element

  element.addEventListener('mousemove', (e) => {
    let x
    let y
    x = e.pageX
    y = e.pageY
    x -= offsetLeft
    y -= offsetTop
    mouse.x = x
    mouse.y = y
    mouse.event = e
  })
  return mouse
}

引入到画布中

import * as THREE from 'three'
import stats from '../common/stats'
import { captureMouse } from '../common/utils'

// Canvas
const canvas = document.querySelector('#mainCanvas') as HTMLCanvasElement

// Scene
const scene = new THREE.Scene()

// Object
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({
    color: 0x607d8b,
  })
)
scene.add(cube)

// Camera
const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 1, 100)
camera.position.set(0, 0, 3)

// Renderer
const renderer = new THREE.WebGLRenderer({
  canvas,
})
renderer.setSize(canvas.clientWidth, canvas.clientHeight)

// mouse postion
const mouse = captureMouse(canvas)

// Animations
const tick = () => {
  stats.begin()

  // Uppdate camera
  camera.position.x = (mouse.x / canvas.clientWidth - 0.5) * 4
  camera.position.y = -(mouse.y / canvas.clientWidth - 0.5) * 4

  // Render
  renderer.render(scene, camera)
  stats.end()
  requestAnimationFrame(tick)
}

tick()

效果如下

如果设置了 camera.lookAt(cube.position) 则效果如下:

// Animations
const tick = () => {
  stats.begin()

  // Uppdate camera
  camera.position.x = (mouse.x / canvas.clientWidth - 0.5) * 4
  camera.position.y = -(mouse.y / canvas.clientWidth - 0.5) * 4
  camera.lookAt(cube.position)

  // Render
  renderer.render(scene, camera)
  stats.end()
  requestAnimationFrame(tick)
}

看起来有点复杂,幸运的是 Three.js 内置了一些控制器

Three.js 内置的控制器

  • FlyControls 启用了一种类似于数字内容创建工具(例如Blender)中飞行模式的导航方式。 你可以在3D空间中任意变换摄像机,并且无任何限制(例如,专注于一个特定的目标)。
  • FirstPersonControls 该类是 FlyControls 的另一个实现。
  • PointerLockControls 该类的实现是基于Pointer Lock API的。 对于第一人称3D游戏来说, PointerLockControls 是一个非常完美的选择。
  • OrbitControls (轨道控制器)可以使得相机围绕目标进行轨道运动。
  • TrackballControls 与 OrbitControls 相类似。然而,它不能恒定保持摄像机的up向量。 这意味着,如果摄像机绕过“北极”和“南极”,则不会翻转以保持“右侧朝上”。
  • TransformControls 该类可提供一种类似于在数字内容创建工具(例如Blender)中对模型进行交互的方式,来在3D空间中变换物体。 和其他控制器不同的是,变换控制器不倾向于对场景摄像机的变换进行改变。
  • DragControls 该类被用于提供一个拖放交互。

接下来我们使用 OrbitControls

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import stats from '../common/stats'

// Canvas
const canvas = document.querySelector('#mainCanvas') as HTMLCanvasElement

// Scene
const scene = new THREE.Scene()

// Object
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshBasicMaterial({
    color: 0x607d8b,
  })
)
scene.add(cube)

// Camera
const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 1, 100)
camera.position.set(0, 0, 3)

const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

// Renderer
const renderer = new THREE.WebGLRenderer({
  canvas,
})
renderer.setSize(canvas.clientWidth, canvas.clientHeight)

// Animations
const tick = () => {
  stats.begin()

  controls.update()
  // Render
  renderer.render(scene, camera)
  stats.end()
  requestAnimationFrame(tick)
}

tick()

其中设置了 enableDamping 开启阻尼,需要在 requestAnimationFrame update controls

const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

在线 demo 链接

demo 源码

小结

我们已经学习了相机的概念和相机的一些控制器,什么时候使用内置控制器也取决于你的项目场景,不过内置的控制器已经能满足绝大部分场景了。

下一节将讲讲全屏和窗口大小。


Similar Posts

Comments