12.Three.js 基础之镜头
12 Three.js 基础之镜头
在之前所有的示例中,关于镜头,我们使用的都是
再次强调一下,我个人偏好是喜欢将
Camera 称为 “镜头”,但是Three.js 官方或其他教程中称呼其为 “相机”
在
镜头类型 |
镜头名称 | 解释说明 |
---|---|---|
ArrayCamera | 镜头阵列 | 一组已预定义的镜头 |
CubeCamera | 立方镜头 | |
OrthographicCamera | 正交镜头 | 无论物体距离镜头远近,最终渲染出的大小不变 |
PerspectiveCamera | 透视镜头 | 像人眼睛一样的镜头,远大近小,最常用的镜头 |
StereoCamera | 立体镜头 | 双透视镜头,常用于创建 |
所有镜头的辅助对象都是:Three.CameraHelper
由于 透视镜头
镜头的一些知识
我们通过透视镜头,来讲解一些镜头的知识
透视镜头(PerspectiveCamera) 所呈现出的效果,和我们用有眼睛观察世界是一模一样的。
平截面
无论所观察的物体是什么类型,例如球体、立方体、椎体等,在我们的视野中都会形成
- 远截面
(far) :物体最远处的截面 - 近截面
(near) :物体最近处的截面
若以某个特定角度,当镜头
例如我们在一个立方体的正前方,此时近截面完全遮挡住远截面,此时我们观察到的立方体更像是一个平面。
但是由于可能存在阴影,我们依然能够感知到这是一个 “
3D 立体物体”。
视椎
想象一下,假设把我们的镜头
如果我们眼睛不是与物体,而是与 “场景
请注意,上面提到的 场景的远截面和近截面 是加了引号,事实上没有办法直接设置场景的远近截面,场景的近远视椎是由镜头的以下几个参数最终计算出的:
- 镜头的观察角度
(fov) - 镜头画面的宽高比
(aspect) - 镜头的最近可见距离
(far) - 镜头的最远可见距离
(near) - 一个隐含因素:镜头本身的位置
(camera.position)
近截面和远截面决定了物体是否在镜头内可见
物体与镜头的距离决定物体在视觉上的大小
当我们初始化一个透视镜头时,构造函数需要传递的
透视镜头默认参数值:fov=50、aspect=1、near=0.1、far=2000
补充说明:
在
- fov:摄像机视椎体垂直视野角度
- aspect:摄像机视椎体长宽比
( 宽高比) - near:摄像机视椎体近端面
- far:摄像机视椎体远端面
虽然我的描述和官方描述文字上存在差异,但意思相同。我认为我的用词更加口语化,容易理解,所以在本系列文章中,我会继续使用我的描述语言。
因为受到自己对
Three.js 的理解程度,或许偏个人化语言描述或许是不正确的。
关于镜头近截面与远截面的补充说明:计算量与性能
我们知道透视镜头默认参数值:fov=50、aspect=1、near=0.1、far=2000,而我们之前文章中的示例,通常镜头设置参数为:new Three.PerspectiveCamera(45,2,0.1,1000)
也就是说,近截面
( 镜头最近可见距离) 通常设置为0.1 、远截面( 镜头最远可见距离) 通常为1000
若超出这个范围内的物体或物体局部则都将不可见。
思考一下
假设我们直接将
答案是肯定的,但场景越大,可见度越微观,渲染所需计算量也越大。
请记得:当计算量大到一定程度后,就会出现渲染异常,画面会出现一些意外的、不符合预期结果。
通常表现为物体表面像素紊乱、破碎、闪烁、像素前后失调,这是因为
我十分确信此刻我是在讲解
Three.js ,而不是 大姨妈。
解决办法( 并不推荐) :将渲染器的logarithmicDepthBuffer 设置为true
logarighmicDepthBuffer:对数深度缓存器
const renderer = new Three.WebGLRenderer({
canvas:xxxx,
logarithmicDepthBuffer:true
})
注意事项:
- 通常电脑浏览器都已支持
logarithmicDepthBuffer 属性,但很多手机目前还不支持。 logarithmicDepthBuffer 为true 只是在一定程度上能够缓解问题,但若near 足够小、far 足够大时,依然会出现GPU 计算精度不够,造成画面渲染紊乱。
解决办法( 推荐做法) :不解决
请记得:你本就不应该把
near 和far 正确的设定原则
尽可能让
注意,这里的 “更接近镜头不远的位置” 是指 “合适、适当的位置”,并不是指小数点后精确多少位。
在保证精度的前提下,尽可能设置合适的 近截面和远截面,这样让 镜头与物体产生的视椎 “更小、更接近”,以节省渲染所需计算量和性能。
举一个例子:
假设你需要渲染出一个 足球 的特写,那么把足球放置在一个 比较小的平台或地面即可,而不是创建一个城市一样大小的场景,却只渲染出一个 足球的近距离特写。
但是假设你的场景确确实实需要非常大,此时就需要多参考网上其他人是如何处理类似场景的。
或许后续文章中也会有讲解,此时此刻你只需知道near 和far 设置合适即可,没必要过于精细或巨大。
镜头示例1 :使用CameraHelper 来观察镜头
关于 透视镜头
本示例主要演示 通过 镜头辅助对象
思考一下:
在正常情况下,一个镜头只能看到别的物体但无法看到自己。
就好像我们的眼睛可以看到这个世界,但是眼睛本身自己没法看到自己
当然除非对着镜子,不过对着镜子的本质依然是眼睛去看别的物体。
更加直白一点:就算你眼睛长得再大,你也做不到左眼直接看到自己右眼。
我们人虽然是
2 个眼球,但是在Three.js 的相关举例中,是将 左右两个眼睛当成 是1 个镜头来阐述的。
HelloCamera 示例目标
- 创建一个包含物体的场景
- 创建
2 个镜头,镜头A 和镜头B - 将网页画面一分为二
- 左侧显示 镜头
A 所看到的场景 - 右侧使用使用 镜头
B 来观察 镜头A
补充说明:
- 事实上是 镜头
B 观察并显示 镜头A 对应的辅助对象(CameraHelper) - 为了省事,我们直接使用前文讲解灯光时编写的
create-scene.ts 来创建场景
代码实现思路
关键点1 :同一个场景渲染出2 个不同的画面
const leftCamera = new Three.PerspectiveCamera(45, 2, 5, 100)
leftCamera.position.set(0, 10, 20)
const rightCamera = new Three.PerspectiveCamera(60, 2, 0.1, 200)
rightCamera.position.set(40, 10, 30)
rightCamera.lookAt(0, 5, 0)
const leftControls = new OrbitControls(leftCamera, leftViewRef.current)
leftControls.target.set(0, 5, 0)
leftControls.update()
const rightControls = new OrbitControls(rightCamera, rightViewRef.current)
rightControls.target.set(0, 5, 0)
rightControls.update()
...
<div className='full-screen'>
<div className='split'>
<div ref={leftViewRef}></div>
<div ref={rightViewRef}></div>
</div>
<canvas ref={canvasRef} />
</div>
渲染器裁减功能,涉及到的方法有
我们之前示例中,为了适应浏览器窗口大小的改变,我们使用过 渲染器的
setSize()
setScissor ( x : Integer, y : Integer, width : Integer, height : Integer ) : null
将剪裁区域设为
setScissorTest ( boolean : Boolean ) : null
启用或禁用剪裁检测
setViewport ( x : Integer, y : Integer, width : Integer, height : Integer ) : null
将视口大小设置为
请额外留意在后面实际代码中,我们定义的
多敲几遍,记住
setScissorForElement() 函数中获得裁减区域的代码套路
关键点2 :镜头辅助对象
镜头辅助对象为
const helper = new THREE.CameraHelper( leftCamera )
scene.add( helper )
关键点3 :如何渲染
在之前所有的示例代码中,渲染代码都为:
renderer.render(scene, camera)
但本示例中,我们是同一个
需要依次分别渲染出 左侧镜头视角 和 右侧镜头视角。
//leftCamera一些更新操作
...
renderer.render(sceneRef.current, leftCamera)
//rightCamera一些更新操作
...
renderer.render(sceneRef.current, rightCamera)
具体的代码
create-scene.ts
我们直接使用之前 “
index.scss
.full-screen,canvas {
display: block;
height: inherit;
width: inherit;
}
.split {
position: fixed;
display: flex;
width: inherit;
height: inherit;
}
.split div {
width: inherit;
height: inherit;
}
index.tsx
import { useRef, useEffect } from 'react'
import * as Three from 'three'
import createScene from '@/components/hello-light/create-scene'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import './index.scss'
const HelloCamera = () => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const sceneRef = useRef<Three.Scene | null>(null)
const leftViewRef = useRef<HTMLDivElement>(null)
const rightViewRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (canvasRef.current === null || leftViewRef.current === null || rightViewRef.current === null) {
return
}
const renderer = new Three.WebGLRenderer({ canvas: canvasRef.current as HTMLCanvasElement })
renderer.setScissorTest(true)
const scene = createScene()
scene.background = new Three.Color(0x000000)
sceneRef.current = scene
const light = new Three.DirectionalLight(0xFFFFFF, 1)
light.position.set(0, 10, 0)
light.target.position.set(5, 0, 0)
scene.add(light)
scene.add(light.target)
const leftCamera = new Three.PerspectiveCamera(45, 2, 5, 100)
leftCamera.position.set(0, 10, 20)
const helper = new Three.CameraHelper(leftCamera)
scene.add(helper)
const leftControls = new OrbitControls(leftCamera, leftViewRef.current)
leftControls.target.set(0, 5, 0)
leftControls.update()
const rightCamera = new Three.PerspectiveCamera(60, 2, 0.1, 200)
rightCamera.position.set(40, 10, 30) //为了能够看清、看全镜头,所以将右侧镜头的位置设置稍远一些
rightCamera.lookAt(0, 5, 0)
const rightControls = new OrbitControls(rightCamera, rightViewRef.current)
rightControls.target.set(0, 5, 0)
rightControls.update()
const setScissorForElement = (div: HTMLDivElement) => {
if (canvasRef.current === null) {
return
}
//获得 canvas 和 div 的矩形框尺寸和位置
const canvasRect = canvasRef.current.getBoundingClientRect()
const divRect = div.getBoundingClientRect()
//计算出裁切框的尺寸和位置
const right = Math.min(divRect.right, canvasRect.right) - canvasRect.left
const left = Math.max(0, divRect.left - canvasRect.left)
const bottom = Math.min(divRect.bottom, canvasRect.bottom) - canvasRect.top
const top = Math.max(0, divRect.top - canvasRect.top)
const width = Math.min(canvasRect.width, right - left)
const height = Math.min(canvasRect.height, bottom - top)
//将剪刀设置为仅渲染到画布的该部分
const positiveYUpBottom = canvasRect.height - bottom
renderer.setScissor(left, positiveYUpBottom, width, height)
renderer.setViewport(left, positiveYUpBottom, width, height)
//返回外观
return width / height
}
const render = () => {
if (leftCamera === null || rightCamera === null || sceneRef.current === null) {
return
}
const sceneBackground = sceneRef.current.background as Three.Color
//渲染 左侧 镜头
const leftAspect = setScissorForElement(leftViewRef.current as HTMLDivElement)
leftCamera.aspect = leftAspect as number
leftCamera.updateProjectionMatrix()
helper.update()
helper.visible = false
sceneBackground.set(0x000000)
renderer.render(sceneRef.current, leftCamera)
//渲染 右侧 个镜头
const rightAspect = setScissorForElement(rightViewRef.current as HTMLDivElement)
rightCamera.aspect = rightAspect as number
rightCamera.updateProjectionMatrix()
helper.visible = true
sceneBackground.set(0x000040)
renderer.render(sceneRef.current, rightCamera)
window.requestAnimationFrame(render)
}
window.requestAnimationFrame(render)
const handleResize = () => {
if (canvasRef.current === null) {
return
}
const width = canvasRef.current.clientWidth
const height = canvasRef.current.clientHeight
renderer.setSize(width, height, false)
}
handleResize()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [canvasRef])
return (
<div className='full-screen'>
<div className='split'>
<div ref={leftViewRef}></div>
<div ref={rightViewRef}></div>
</div>
<canvas ref={canvasRef} />
</div>
)
}
export default HelloCamera
执行以后,就会看到浏览器中左右
镜头示例2 :OrthographicCamera
第二个比较经常用的镜头是
正交镜头与透视镜头最大的区别点在于:
-
正交镜头的视椎体不是 椎体,而是立方体
-
正交镜头看到的都是一个“面”
-
因此,正交镜头没有 “透视
( 近大远小) ”这个概念我对于以上
2 点理解并不深,先记住以后再慢慢研究
OrthographicCamera 的用途
- 用途
1 :作为2D 画布 - 作为
3D 建模程序 的 上、下、左、右、前、后 视图。
OrthographicCamera 基本用法
const camera = new Three.OrthographicCamera(-1, 1, 1, -1, 5, 50)
camera.zoom = 0.2
camera.position.set(0,10,20)
初始化时,构造函数内的参数依次是:
OrthographicCamera( left : Number, right : Number, top : Number, bottom : Number, near : Number, far : Number )
- left:视椎体左侧面
- right:视椎体右侧面
- top:视椎体顶面
- bottom:视椎体底面
- near:视椎体近端面
- far:视椎体远端面
从实际的角度来看,一定要注意以下几点:
left 的值不能大于right ,同理bottom 的值不能大于top 。 如果没有按照这个约定,例如bottom 大于top 相当于颠倒了相机。near 设置越小,投影的映像越大left 与right 之间的距离、top 与bottom 之间的距离的比例一定要和canvas 比例相同,否则会导致投影的物体形状变形
正交镜头与透视镜头的几点不同地方
渲染时,对应的设置不同。
透视镜头渲染时,需要修改的是
正交镜头渲染时,需要修改的是
假设我们在HelloCamera 示例中使用OrthographicCamera
需要修改的地方为:
- const leftCamera = new Three.PerspectiveCamera(45, 2, 5, 100)
- leftCamera.position.set(0, 10, 20)
+ const leftCamera = new Three.OrthographicCamera(-1, 1, 1, -1, 5, 50)
+ leftCamera.zoom = 0.2
leftCamera.position.set(0,10,20)
...
const leftAspect = setScissorForElement(leftViewRef.current as HTMLDivElement)
- leftCamera.aspect = leftAspect as number
+ leftCamera.left = -(leftAspect as number)
+ leftCamera.right = leftAspect as number
leftCamera.updateProjectionMatrix()
其他代码无需修改,发布调试,即可看到正交镜头辅助对象,此时的视椎不再是椎体,而是一个立方体。
关于其他镜头:ArrayCamera、CubeCamera、
具体的用法,可查阅:https://threejs.org/docs/index.html#api/zh/cameras/Camera
至此,关于镜头的基础知识讲解完毕。
虽然本文是在讲镜头,但本文的核心知识点却是在讲 渲染器的裁切 功能。
一定要多多复习,熟练掌握 渲染器的
setScissor() 、setScissorTest()、setViewport() 方法。
下一节,我们将讲解