下面介绍three.js相机:
一、简介
为了在二维平面呈现三维效果, 我们需要一个观察者, 在二维平面上绘制出观察者看到的三维世界这个观察者就是相机。
相机用于决定场景中哪些部分能被看到,以及如何呈现它们。它类似于真实摄像机的存在。
相机有很多属性,比如位置、目标(或注视点)、视角(FOV)、近面和远面等。这些属性共同决定了相机如何看待场景中的物体,并形成最终的渲染结果。
为了描述一个观察者, 我们需要考虑这样两个问题
·观察方向: 观察者在哪里, 他沿着哪个方向在观察, 看向什么
·可视范围: 太近, 太远, 太偏的物体都看不到
1) 如何描述观察方向
为了描述观察方向, 有几个概念需要理解
·视点(eye point): 观察点的位置
·目标点(lookat point): 观察目标所在的位置
·观察方向(viewing direction): 从视点出发到目标点的射线, 也叫视线
为了在二维平面上绘制, 还需要确定上方向(up direction)
如果我们调整上方向, 最终看到的效果就不同
2) 如何描述可视范围
那如何描述可视范围呢?
·视角(fov): 可以看到的水平, 垂直的角度
·近裁截面(near): 可以看到的最近距离
·远裁截面(far): 可以看到的最远距离
由视角近面远面构成了可视空间(view volume).
常见的可视空间有两种
·长方体可视空间, 由正射投影(orthographic projection)产生
·四棱锥可视空间, 由透视投影(perspective projection)产生
二、透视相机详解
透视相机主要模拟人的视角, 一般观察物体, 会呈现近大远小的特征
1) 相机参数
在初始化创建相机时, 可以设置4个参数
·fov: 垂直方向的视角
·aspect: 宽高比, 水平方向的视角
·near: 近截面
·far: 远截面
下面通过一个示例, 可视化调整相机的参数帮助大家理解每个参数的作用
const camera = new THREE.PerspectiveCamera(45, 2, 10, 20)
// 设置视点
camera.position.set(0, 0, 5)
// 设置观察点
camera.lookAt(0, 0, 0)
使用相机可视化工具动态调整参数, 观察相机的变化
import * as THREE from 'three'
import { BaseGui } from './BaseGui'
function cameraHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.updateProjectionMatrix()
parent.update()
}
const cameraConfig = {
fov: {
name: '可视角度',
extend: [0, 180],
handler: cameraHandler,
},
aspect: {
name: '宽高比',
extend: [1, 3],
step: 0.1,
handler: cameraHandler,
},
near: {
name: '近截面',
extend: [0.1, 20],
handler: cameraHandler,
},
far: {
name: '远截面',
extend: [10, 50],
handler: cameraHandler,
},
}
export class CameraGui extends BaseGui {
constructor(options = {}) {
if (!options.target.isCamera) {
console.error('target must be an instance of Camera')
return
}
if (!options.scene.isScene) {
console.log('scene is required')
return
}
super()
this.init(options)
}
init(options) {
this.helper = new THREE.CameraHelper(options.target)
this.scene = options.scene
this.scene.add(this.helper)
this.camera = this.helper.camera
options.camera !== false ? this.initCamera() : ''
}
initCamera() {
this.initGuiFolder(this.camera, this.camera.type, this.helper, cameraConfig)
}
}
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { MeshGui } from '../gui'
import { CameraGui } from '../gui'
// 一. 创建场景
const scene = new THREE.Scene()
// 二. 创建渲染相机(上帝视角, 观察整个场景)
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight
)
camera.position.set(200, 200, 200)
camera.lookAt(0, 0, 0)
// 创建透视相机, 借助可视化工具观察
const pCamera = new THREE.PerspectiveCamera(45, 1, 10, 20)
pCamera.position.set(0, 0, 5)
pCamera.updateMatrixWorld()
new CameraGui({
target: pCamera,
scene,
})
// 三. 创建物体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2)
const cubeMaterial = new THREE.MeshNormalMaterial()
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
scene.add(cube)
// 集成Gui
new MeshGui({
target: cube,
})
// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
// 将渲染的canvas添加到body元素中
document.body.appendChild(renderer.domElement)
// 五. 辅助工具
const control = new OrbitControls(camera, renderer.domElement)
const axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)
const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)
function animation() {
renderer.render(scene, camera)
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
效果
示例代码
import * as THREE from 'three'
import { BaseGui } from './BaseGui'
function positionHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.updateMatrixWorld()
parent.update()
}
const positionConfig = {
x: {
handler: positionHandler,
},
y: {
handler: positionHandler,
},
z: {
handler: positionHandler,
},
}
function cameraHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.updateProjectionMatrix()
parent.update()
}
const cameraConfig = {
fov: {
name: '可视角度',
extend: [0, 180],
handler: cameraHandler,
},
aspect: {
name: '宽高比',
extend: [1, 3],
step: 0.1,
handler: cameraHandler,
},
near: {
name: '近截面',
extend: [0.1, 20],
handler: cameraHandler,
},
far: {
name: '远截面',
extend: [10, 50],
handler: cameraHandler,
},
}
export class CameraGui extends BaseGui {
constructor(options = {}) {
if (!options.target.isCamera) {
console.error('target must be an instance of Camera')
return
}
if (!options.scene.isScene) {
console.log('scene is required')
return
}
super()
this.init(options)
}
init(options) {
this.helper = new THREE.CameraHelper(options.target)
this.scene = options.scene
this.scene.add(this.helper)
this.camera = this.helper.camera
this.position = this.camera.position
options.camera !== false ? this.initCamera() : ''
options.position !== false ? this.initPosition() : ''
}
initCamera() {
this.initGuiFolder(this.camera, this.camera.type, this.helper, cameraConfig)
}
initPosition() {
this.initGuiFolder(this.position, '相机坐标', this.helper, positionConfig)
}
}
注意
当调整相机参数时, 需要更新相机的投影矩阵, 否则不会生效
2) 调整视点
示例
import * as THREE from 'three'
import { BaseGui } from './BaseGui'
function positionHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.updateMatrixWorld()
parent.update()
}
const positionConfig = {
x: {
handler: positionHandler,
},
y: {
handler: positionHandler,
},
z: {
handler: positionHandler,
},
}
function cameraHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.updateProjectionMatrix()
parent.update()
}
const cameraConfig = {
fov: {
name: '可视角度',
extend: [0, 180],
handler: cameraHandler,
},
aspect: {
name: '宽高比',
extend: [1, 3],
step: 0.1,
handler: cameraHandler,
},
near: {
name: '近截面',
extend: [0.1, 20],
handler: cameraHandler,
},
far: {
name: '远截面',
extend: [10, 50],
handler: cameraHandler,
},
}
export class CameraGui extends BaseGui {
constructor(options = {}) {
if (!options.target.isCamera) {
console.error('target must be an instance of Camera')
return
}
if (!options.scene.isScene) {
console.log('scene is required')
return
}
super()
this.init(options)
}
init(options) {
this.helper = new THREE.CameraHelper(options.target)
this.scene = options.scene
this.scene.add(this.helper)
this.camera = this.helper.camera
this.position = this.camera.position
options.camera !== false ? this.initCamera() : ''
options.position !== false ? this.initPosition() : ''
}
initCamera() {
this.initGuiFolder(this.camera, this.camera.type, this.helper, cameraConfig)
}
initPosition() {
this.initGuiFolder(this.position, '相机坐标', this.helper, positionConfig)
}
}
⚠️ 特别注意
当调整相机在世界中的位置时, 需要更新世界矩阵
效果
3) 调整观察点
示例
import * as THREE from 'three'
import { BaseGui } from './BaseGui'
function positionHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.updateMatrixWorld()
parent.update()
}
const positionConfig = {
x: {
handler: positionHandler,
},
y: {
handler: positionHandler,
},
z: {
handler: positionHandler,
},
}
function lookatHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.lookAt(...Object.values(item))
camera.updateMatrixWorld()
parent.update()
}
const lookatConfig = {
x: {
handler: lookatHandler,
},
y: {
handler: lookatHandler,
},
z: {
handler: lookatHandler,
},
}
function cameraHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.updateProjectionMatrix()
parent.update()
}
const cameraConfig = {
fov: {
name: '可视角度',
extend: [0, 180],
handler: cameraHandler,
},
aspect: {
name: '宽高比',
extend: [1, 3],
step: 0.1,
handler: cameraHandler,
},
near: {
name: '近截面',
extend: [0.1, 20],
handler: cameraHandler,
},
far: {
name: '远截面',
extend: [10, 50],
handler: cameraHandler,
},
}
export class CameraGui extends BaseGui {
constructor(options = {}) {
if (!options.target.isCamera) {
console.error('target must be an instance of Camera')
return
}
if (!options.scene.isScene) {
console.log('scene is required')
return
}
super()
this.init(options)
}
init(options) {
this.helper = new THREE.CameraHelper(options.target)
this.scene = options.scene
this.scene.add(this.helper)
this.camera = this.helper.camera
this.position = this.camera.position
options.camera !== false ? this.initCamera() : ''
options.position !== false ? this.initPosition() : ''
options.lookAt !== false ? this.initLookAt() : ''
}
initCamera() {
this.initGuiFolder(this.camera, this.camera.type, this.helper, cameraConfig)
}
initPosition() {
this.initGuiFolder(this.position, '相机坐标', this.helper, positionConfig)
}
initLookAt() {
const lookAt = {
x: 0,
y: 0,
z: 0,
}
this.initGuiFolder(lookAt, '观察点坐标', this.helper, lookatConfig)
}
}
注意
每次调整观察点时, 需要重新调用lookAt()方法更新
效果
4) 实时渲染
为了能看到添加到场景中的透视相机拍出的图片, 我们可以使用透视相机进行拍照并渲染
效果
我们将渲染的画布分成两个部分
·左边: 实时显示透视相机的渲染结果
·右边: 调整相机参数
示例
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { MeshGui } from '../gui'
import { CameraGui } from '../gui'
// 一. 创建场景
const scene = new THREE.Scene()
// 二. 创建渲染相机(上帝视角, 观察整个场景)
const camera = new THREE.PerspectiveCamera(
45,
(window.innerWidth / window.innerHeight) * 0.5
)
camera.position.set(20, 20, 20)
camera.lookAt(0, 0, 0)
// 创建透视相机, 借助可视化工具观察
const pCamera = new THREE.PerspectiveCamera(
45,
(window.innerWidth / window.innerHeight) * 0.5,
10,
20
)
pCamera.position.set(0, 0, 5)
pCamera.updateMatrixWorld()
new CameraGui({
target: pCamera,
scene,
})
// 三. 创建物体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2)
const cubeMaterial = new THREE.MeshNormalMaterial()
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
scene.add(cube)
// 集成Gui
new MeshGui({
target: cube,
})
// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
renderer.autoClear = false
// 将渲染的canvas添加到body元素中
document.body.appendChild(renderer.domElement)
// 五. 辅助工具
const control = new OrbitControls(camera, renderer.domElement)
const axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)
const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)
function animation() {
renderer.clear()
renderer.setViewport(0, 0, window.innerWidth / 2, window.innerHeight)
renderer.render(scene, pCamera)
renderer.setViewport(
window.innerWidth / 2,
0,
window.innerWidth / 2,
window.innerHeight
)
renderer.render(scene, camera)
}
window.addEventListener('resize', () => {
camera.aspect = (window.innerWidth / window.innerHeight) * 0.5
camera.updateProjectionMatrix()
pCamera.aspect = (window.innerWidth / window.innerHeight) * 0.5
pCamera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
三、正交相机详解
正交相机使用正视投影, 不管从什么距离, 哪个角度渲染的结果都是一样的. 这种相机主要被用于游戏中
1) 相机参数
在初始化创建相机时, 可以设置4个参数
·left: 左面
·right: 右面
·top: 上面
·bottom: 下面
·near: 近面
·far: 远面
下面通过一个示例, 可视化调整相机的参数帮助大家理解每个参数的作用
效果
示例
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { MeshGui } from '../gui'
import { CameraGui } from '../gui'
// 一. 创建场景
const scene = new THREE.Scene()
// 二. 创建渲染相机(上帝视角, 观察整个场景)
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight
)
camera.position.set(20, 20, 20)
camera.lookAt(0, 0, 0)
// 创建正交相机, 借助可视化工具观察
const oCamera = new THREE.OrthographicCamera(-5, 5, 5, -5, 5, 15)
oCamera.position.set(0, 0, 5)
oCamera.updateMatrixWorld()
new CameraGui({
target: oCamera,
scene,
})
// 三. 创建物体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2)
const cubeMaterial = new THREE.MeshNormalMaterial()
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
scene.add(cube)
// 集成Gui
new MeshGui({
target: cube,
})
// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
// 将渲染的canvas添加到body元素中
document.body.appendChild(renderer.domElement)
// 五. 辅助工具
const control = new OrbitControls(camera, renderer.domElement)
const axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)
const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)
function animation() {
renderer.render(scene, camera)
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
import * as THREE from 'three'
import { BaseGui } from './BaseGui'
function positionHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.updateMatrixWorld()
parent.update()
}
const positionConfig = {
x: {
handler: positionHandler,
},
y: {
handler: positionHandler,
},
z: {
handler: positionHandler,
},
}
function lookatHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.lookAt(...Object.values(item))
camera.updateMatrixWorld()
parent.update()
}
const lookatConfig = {
x: {
handler: lookatHandler,
},
y: {
handler: lookatHandler,
},
z: {
handler: lookatHandler,
},
}
function cameraHandler(item, key, value, parent) {
const camera = parent.camera
item[key] = value
camera.updateProjectionMatrix()
parent.update()
}
const cameraMapping = {
PerspectiveCamera: {
fov: {
name: '可视角度',
extend: [0, 180],
handler: cameraHandler,
},
aspect: {
name: '宽高比',
extend: [1, 3],
step: 0.1,
handler: cameraHandler,
},
near: {
name: '近截面',
extend: [0.1, 20],
handler: cameraHandler,
},
far: {
name: '远截面',
extend: [10, 50],
handler: cameraHandler,
},
},
OrthographicCamera: {
left: {
name: '左面',
extend: [-10, 10],
handler: cameraHandler,
},
right: {
name: '右面',
extend: [-10, 10],
handler: cameraHandler,
},
top: {
name: '上面',
extend: [-10, 10],
handler: cameraHandler,
},
bottom: {
name: '底面',
extend: [-10, 10],
handler: cameraHandler,
},
near: {
name: '近面',
extend: [1, 10],
handler: cameraHandler,
},
far: {
name: '近面',
extend: [10, 20],
handler: cameraHandler,
},
},
}
export class CameraGui extends BaseGui {
constructor(options = {}) {
if (!options.target.isCamera) {
console.error('target must be an instance of Camera')
return
}
if (!options.scene.isScene) {
console.log('scene is required')
return
}
super()
this.init(options)
}
init(options) {
this.helper = new THREE.CameraHelper(options.target)
this.scene = options.scene
this.scene.add(this.helper)
console.log(this.scene)
this.camera = this.helper.camera
this.position = this.camera.position
options.camera !== false ? this.initCamera() : ''
options.position !== false ? this.initPosition() : ''
options.lookAt !== false ? this.initLookAt() : ''
}
initCamera() {
this.initGuiFolder(
this.camera,
this.camera.type,
this.helper,
cameraMapping[this.camera.type]
)
}
initPosition() {
this.initGuiFolder(this.position, '相机坐标', this.helper, positionConfig)
}
initLookAt() {
const lookAt = {
x: 0,
y: 0,
z: 0,
}
this.initGuiFolder(lookAt, '观察点坐标', this.helper, lookatConfig)
}
}
2) 实时渲染
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { MeshGui } from '../gui'
import { CameraGui } from '../gui'
// 一. 创建场景
const scene = new THREE.Scene()
// 二. 创建渲染相机(上帝视角, 观察整个场景)
const camera = new THREE.PerspectiveCamera(
45,
(window.innerWidth / window.innerHeight) * 0.5
)
camera.position.set(20, 20, 20)
camera.lookAt(0, 0, 0)
// 创建透视相机, 借助可视化工具观察
// 创建正交相机, 借助可视化工具观察
const oCamera = new THREE.OrthographicCamera(-5, 5, 5, -5, 5, 15)
oCamera.position.set(0, 0, 5)
oCamera.updateMatrixWorld()
new CameraGui({
target: oCamera,
scene,
})
// 三. 创建物体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2)
const cubeMaterial = new THREE.MeshNormalMaterial()
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
scene.add(cube)
// 集成Gui
new MeshGui({
target: cube,
})
// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
renderer.autoClear = false
// 将渲染的canvas添加到body元素中
document.body.appendChild(renderer.domElement)
// 五. 辅助工具
const control = new OrbitControls(camera, renderer.domElement)
const axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)
const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)
function animation() {
renderer.clear()
renderer.setViewport(0, 0, window.innerWidth / 2, window.innerHeight)
renderer.render(scene, oCamera)
renderer.setViewport(
window.innerWidth / 2,
0,
window.innerWidth / 2,
window.innerHeight
)
renderer.render(scene, camera)
}
window.addEventListener('resize', () => {
camera.aspect = (window.innerWidth / window.innerHeight) * 0.5
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})