07.Cameras相机
介绍
我们已经创建过了一个PerspectiveCamera
,但还有其他类型的相机,如您在文档中所见。
相机 Camera
Camera
类就是我们所说的抽象类。你不应该直接使用它,但你可以继承它来访问公共属性和方法。以下一些类继承自Camera类。
阵列相机 ArrayCamera
ArrayCamera 用于通过使用多个相机来多次渲染您的场景。每个相机将渲染画布的特定区域。你可以想象这看起来像老式的多人游戏控制台,我们必须共享一个分屏。
立体相机 StereoCamera
StereoCamera用于通过两个模仿眼睛的相机渲染场景,以创建我们所说的视差效果,从而诱使您的大脑认为存在深度。您必须拥有足够的设备,例如 VR 耳机或红色和蓝色眼镜才能看到结果。
立方相机 CubeCamera
CubeCamera用于获取面向每个方向(向前、向后、向左、向右、向上和向下)的渲染,以创建周围环境的渲染。您可以使用它来创建用于反射的环境贴图或阴影贴图。我们稍后会谈到这些。
正交相机 OrthographicCamera
OrthographicCamera用于在没有透视的情况下创建场景的正交渲染。如果您制作像帝国时代这样的 RTS 游戏,它会很有用。无论元素与相机的距离如何,它们在屏幕上的大小都是相同的。
透视相机 PerspectiveCamera
PerspectiveCamera是我们已经使用过的,它模拟了具有透视功能的真实相机。
我们将重点关注OrthographicCamera和PerspectiveCamera。
透视相机 PerspectiveCamera
正如我们之前看到的,PerspectiveCamera类需要一些参数来实例化,但我们并没有使用所有可能的参数。添加第三个和第四个参数:
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 1, 100)
您应该得到相同的结果,但让我们详细讨论这些参数。
视野
第一个参数称为视野,对应于您的相机视图的垂直振幅角度(以度为单位)。如果你使用小角度,你最终会得到一个长范围的效果,如果你使用广角,你最终会得到一个鱼眼效果,因为最终,相机看到的东西会被拉伸或挤压以适合画布。
至于选择正确的视野,您必须尝试一下。我通常使用45~75之间的视野。
纵横比
第二个参数称为宽高比,对应于宽度除以高度。虽然您可能认为这显然是画布宽度乘以画布高度并且 Three.js 应该自己计算它,但如果您以非常谨慎的方式开始使用 Three.js,那么情况可能并不总是这样。在我们的例子中,我们可以简单地使用画布宽度和画布高度。
我建议将这些值保存在一个对象中,因为我们将多次需要它们:
const sizes = {
width: 800,
height: 600
}
近与远 near/far
第三个和第四个参数称为near和far,对应于相机可以看到多近和多远。任何对象或对象的一部分比该near
值更靠近相机或比far
该值更远离相机,那么这个物体将不会显示在可视的范围上。
你可以想象,就像在那些古老的赛车游戏中一样,你可以看到远处的树木突然冒出来,又从背后消失不见。
虽然您可能很想使用非常小和非常大的值,例如,0.0001您9999999,但是可能会遇到一个称为 z-fighting 的错误,其中两个面似乎在叠合在一起了,因为其中一个将渲染在另一个之上。
https://twitter.com/FreyaHolmer/status/799602767081848832
https://twitter.com/Snapman_I_Am/status/800567120765616128
尝试使用合理的值并仅在需要时增加这些值。在我们的例子中,我们可以使用0.1和100。
正交相机 OrthographicCamera
虽然我们不会在课程的其余部分使用这种类型的相机,但它对特定项目很有用。
OrthographicCamera与PerspectiveCamera的不同之处在于它没有透视,这意味着无论对象与相机的距离如何,它们都将具有相同的大小。
您必须提供的参数与PerspectiveCamera有很大不同。
您必须提供相机在每个方向( left
、right
和top
)可以看到的距离,而不是视野bottom
。然后您可以像我们为PerspectiveCamera所做的那样提供near
和far
值。
在项目中我们注释PerspectiveCamera并添加OrthographicCamera。保持position
更新并lookAt(...)
:
const camera = new THREE.OrthographicCamera(- 1, 1, 1, - 1, 0.1, 100)
如您所见,没有透视图,立方体的边看起来是平行的。问题是我们的立方体看起来不是立方体。
这是由于我们为left
、right
、top
和bottom
提供的值是1或- 1,这意味着我们渲染一个正方形区域,但该正方形区域将被拉伸以适合我们的矩形画布,而我们的画布不是正方形。
我们需要使用画布比例(宽度乘以高度)。让我们创建一个名为aspectRatio
(就像 PerspectiveCamera)的变量并将该比率存储在其中:
const aspectRatio = sizes.width / sizes.height
const camera = new THREE.OrthographicCamera(- 1 * aspectRatio, 1 * aspectRatio, 1, - 1, 0.1, 100)
这导致渲染区域宽度大于渲染区域高度,因为我们的画布宽度大于其高度。
现在我们有一个看起来像立方体的立方体。
自定义控件
让我们回到我们的PerspectiveCamera。注释OrthographicCamera,取消注释PerspectiveCamera,移动camera
使其面向立方体,并删除函数中的网格旋转tick:
// Camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 1, 1000)
// const aspectRatio = sizes.width / sizes.height
// const camera = new THREE.OrthographicCamera(- 1 * aspectRatio, 1 * aspectRatio, 1, - 1, 0.1, 100)
// camera.position.x = 2
// camera.position.y = 2
camera.position.z = 3
camera.lookAt(mesh.position)
scene.add(camera)
我们现在要做的是用鼠标控制相机。首先,我们要知道鼠标坐标。我们可以通过使用原生 JavaScript
addEventListener
监听mousemove
事件来做到这一点。
坐标将位于回调函数的参数中,如event.clientX
和event.clientY
:
// Cursor
window.addEventListener('mousemove', (event) =>
{
console.log(event.clientX, event.clientY)
})
我们可以使用这些值,但我建议再调整一下它们。通过调整,我的意思是有一个振幅1的浮动,这个值可以是正的也可以是负的。
如果我们只关注x
,那就意味着:
- 如果你的光标在画布的最左边,你应该得到- 0.5
- 如果你的光标在画布的中心,你应该得到0
- 如果你的光标在画布的最右边,你应该得到0.5
虽然这不是强制性的,但做好规范这样的习惯,它有助于提高你的代码水平。
就像我们规定的size
变量一样,我们将创建一个具有默认值x
和y
属性的变量cursor
,然后在mousemove
回调中更新这些属性:
// Cursor
const cursor = {
x: 0,
y: 0
}
window.addEventListener('mousemove', (event) =>
{
cursor.x = event.clientX / sizes.width - 0.5
cursor.y = event.clientY / sizes.height - 0.5
console.log(cursor.x, cursor.y)
})
event.clientX除以sizes.width会给我们一个介于0
和1
之间的值 (如果我们将光标保持在画布上方),而减法- 0.5
会给我们一个介于- 0.5
和0.5
之间的值。
您现在已将鼠标位置存储在cursor
对象变量中,您可以在函数中更新相机的位置tick
:
const tick = () =>
{
// ...
// Update camera
camera.position.x = cursor.x
camera.position.y = cursor.y
// ...
}
轴的运动似乎有点不对劲。这是因为position.y
在 Three.js
中向上时轴为正,而clientY
在网页中向下时轴为正。
您可以通过在整个公式前面cursor.y
添加一个来简单地反转更新它(不要忘记括号):-
window.addEventListener('mousemove', (event) =>
{
cursor.x = event.clientX / sizes.width - 0.5
cursor.y = - (event.clientY / sizes.height - 0.5)
})
最后,您可以通过乘以cursor.x
和来增加振幅cursor.y
,并使用以下方法让相机观察网格lookAt(...)
:
const tick = () =>
{
// ...
// Update camera
camera.position.x = cursor.x * 5
camera.position.y = cursor.y * 5
camera.lookAt(mesh.position)
// ...
}
我们可以更进一步,使用Math.sin(...)
和Math.cos(...)
使相机围绕网格进行完整旋转。
sin
和cos
,当以相同的角度组合使用时,我们可以将东西放在一个圆圈上。要进行完整旋转,该角度的振幅必须是 π 的 2 倍(称为“pi”)。就像你知道的那样,一个完整的旋转被称为“tau”,但我们无法在 JavaScript 中访问这个值,我们必须使用 π 代替。
您可以使用本机 JavaScript 访问 π 的近似值Math.PI
。
要增加该圆的半径,您可以简单地将Math.sin(...)
和的结果相乘Math.cos(...)
:
const tick = () =>
{
// ...
// Update camera
camera.position.x = Math.sin(cursor.x * Math.PI * 2) * 2
camera.position.z = Math.cos(cursor.x * Math.PI * 2) * 2
camera.position.y = cursor.y * 3
camera.lookAt(mesh.position)
// ...
}
tick()
虽然这是控制相机的良好开端,但 Three.js 集成了多个称为控件的类来帮助您做同样的事情以及更多。
内置控件
如果您在Three.js 文档中键入“控件” ,您会看到有很多预制控件。我们课程案例将只使用其中一个,但了解它们的作用可能会很有趣。
设备方向控件 DeviceOrientationControls
如果您的设备、操作系统和浏览器允许, DeviceOrientationControls将自动检索设备方向并相应地旋转相机。如果您拥有合适的设备,您可以使用它来创建身临其境的宇宙或 VR 体验。
飞控 FlyControls
FlyControls可以像在宇宙飞船上一样移动相机。您可以在所有 3 个轴上旋转,前进和后退。
第一人称控制 FirstPersonControls
FirstPersonControls就像FlyControls一样,但有一个固定的向上轴。你可以看到的视图就像飞翔的鸟,鸟不能滚动。虽然 FirstPersonControls 包含“FirstPerson”,但它不像在 FPS 游戏中那样工作。
指针锁定控件 PointerLockControls
PointerLockControls使用指针锁定 JavaScript API。此 API 隐藏光标,使其居中,并在mousemove事件回调中不断发送坐标位置移动。使用此 API,您可以直接在浏览器内创建 FPS 游戏。虽然如果您想创建那种交互,这个类很合适,但它只会在指针锁定时处理相机旋转。您必须自己处理摄像机位置和游戏物理。
轨道控制 OrbitControls
OrbitControls与我们在上一课中制作的控件非常相似。您可以使用鼠标左键围绕一个点旋转,使用鼠标右键横向平移,并使用滚轮放大或缩小。
轨迹球控件 TrackballControls
TrackballControls就像OrbitControls,但在垂直角度方面没有限制。即使场景颠倒,您也可以继续旋转并旋转相机。
变换控件 TransformControls
TransformControls与相机无关。您可以使用它向对象添加小控件以移动该对象。
拖动控件 DragControls
就像TransformControls一样,DragControls与相机无关。您可以使用它通过拖放来移动面向相机的平面上的对象。
我们将只使用OrbitControls,但可以随意测试其他类。
轨道控制 OrbitControls
我们在tick
函数中更新camera
的部分。
实例化
首先,我们需要使用OrbitControls类实例化一个变量。虽然您可能认为THREE.OrbitControls
可以使用,但不幸的是您错了,``在THREE
变量中没有包含OrbitControls
该类;
OrbitControls 类属于是THREE变量中包含的默认不可用的类的。该决定有助于减轻库的重量。这就是我们的 Vite 模板的用武之地。
它仍然位于依赖项文件夹中。要导入它,您必须提供文件夹内部的路径/node_modules/
,即/three/examples/jsm/controls/OrbitControls.js
:
import {
OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
您现在可以使用类OrbitControls
(不带THREE.
)实例化一个变量,并确保在创建相机后执行此操作。
要使其工作,您必须在页面中提供相机和将要添加鼠标事件的元素作为参数:
// Controls
const controls = new OrbitControls(camera, canvas)
您现在可以使用鼠标左键或右键拖放来移动相机,并且可以向上或向下滚动以放大或缩小。
它比我们的自定义代码要容易得多,而且它带有更多的控件。但让我们更进一步。
目标 target
默认情况下,相机正在注视场景的中心。我们可以用target
属性改变它。
这个属性是一个Vector3,意味着我们可以改变它的x,y和z属性。
如果我们希望 OrbitControls默认在立方体上方查看,我们只需增加属性y:
controls.target.y = 2
但这不会像那样工作,因为我们需要告诉它OrbitControl
自己更新。我们可以通过update
在之后立即调用方法来做到这一点:
controls.target.y = 2
controls.update()
减震 enableDamping
如果您阅读OrbitControls的文档,会提到damping
. 阻尼将通过添加某种加速度和摩擦力公式来平滑动画。
要启用阻尼感,请将controls
的属性enableDamping
切换为true
。
为了正常工作,还需要通过调用在每一帧上更新控件controls.update()
。您可以在tick
函数上执行此操作:
// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
// ...
const tick = () =>
{
// ...
// Update controls
controls.update()
// ...
}
您会发现控件现在更加流畅。
您可以使用许多其他方法和属性来自定义您的控件,例如旋转速度、缩放速度、缩放限制、角度限制、阻尼强度和键绑定(您也可以使用键盘控制)。
何时使用内置控件
虽然这些控件很方便,但它们也有局限性。如果您过于依赖它们,您可能最终不得不以意想不到的方式改变代码实现逻辑。
首先,确保列出您需要的这些控件的所有功能,然后检查您将要使用的类是否可以处理所有这些功能。
如果没有,你将不得不自己封装一个。