Threejs入门(四)变换、坐标系和场景图

146 阅读8分钟

image.png

前言

在前面的几章中,我们创建了一个场景,并且在场景中添加了相机灯光模型,并将他们渲染到页面上。我们通过改变相机的一些参数来改变我们观察模型的角度,通过添加灯光让我们得以看到模型,这些过程中我们为了方便观察模型,更改了相机灯光的位置,这种方式称为平移,是变换的一种,下面我们将介绍另外两种:旋转缩放

转换:平移、旋转和缩放

每当我们在3D空间中移动对象时,我们都会使用称为转换的数学运算来进行,在前面的章节中,我们已经了解了一种转换方式:平移translation,存储在对象.position属性中
还有两种分别是旋转:rotation,存储在.rotation中,与存储在.scale属性中的缩放一起,这些构成了我们将用于场景中移动对象的三个基本变换,简称:TRS;来指代:平移旋转缩放

可以使用scene.add添加到场景中的每个对象都具有这些属性,包括网格灯光相机,而材质几何图形则没有
前面我们使用.position来设置相机灯光的位置

camera.position.set(0,0,10)
light.position.set(0,0,0)
Object3D 基类

不是为每种类型的对象多次重新定义.position.rotation.scale属性,而是在Object3D基类上定义一次这些属性,这样可以添加到场景中的所有其他类都从该基类派生。包括 网格、相机、点、线助手,甚至 场景本身,我们将非正式地派生自Object3D的类称为场景对象

场景图

在介绍转换之前,我们先了解下场景图,因为供我们操作转换目标对象都存在于场景图中,了解了场景图,有助于我们更好的学习转换

scene.add(mesh)

.add方法也是在Object3D中定义并在场景类上被继承,就像.position.rotation.scale,所有其他派生类也继承了这个方法,继承给了我们light.addmesh.addcamera.add等等,这就意味着对象彼此可以互相添加

创建一个顶部是场景的树形结构,称为场景图(如下图)

image.png

我们将A对象添加到B对象中时,我们称B为父对象,A为子对象

场景是顶级父级,上图中场景有三个子对象,两个网格一个灯光,其中一个网格有两个孩子。

每个对象(顶级场景除外)都只有一个父对象,并且可以有任意数量的子对象

renderer.render(scene, camera)

当我们渲染场景时,渲染器遍历场景图,从场景开始,并使用 每个对象相对于其父对象的位置、旋转和缩放来确定在哪里绘制它

访问场景对象的子对象
image.png

所有添加到场景scene中的对象,都会放在children数组之中,可以使用.children来访问场景对象的所有子对象

坐标系:世界空间和局部空间

世界空间局部空间书中讲了很多,总结就是:

  • 世界空间:就是整个场景的空间
  • 局部空间:场景内对象自身的内部空间

如果我们操作的对象的父级是场景,那这个对象就是在世界空间中的转换,否则,则是在局部空间中的转换

顶级场景定义了世界空间,而其他每个对象都定义了自己的局部空间

每个对象都有一个坐标系

第一个转换:平移

三种转换中最简单的就是平移,前面的章节中我们使用平移来改变相机和灯光的位置,我们通过更改对象的.position属性来执行平移

注:每个对象都从其父对象坐标系内的原点开始

平移的方向

注:平移的单位是

  • X轴正向指向屏幕 右侧
  • Y轴正向指向屏幕 上方
  • Z轴正向指向屏幕 外面,也就是屏幕前的你
平移的使用
// 写法一:
mesh.position.x = 1;
mesh.position.x = 2;
mesh.position.x = 3;

// 写法二
mesh.position.set(1,2,3)

// 上面的两种写法的结果是一样的
// src/World/components/cube.js
import { BoxGeometry, Mesh, MeshBasicMaterial, MeshStandardMaterial } from "three";

const createCube = () => {
  const geometry = new BoxGeometry(2, 2, 2)
  const material = new MeshStandardMaterial({color: 'purple'})
  const cube = new Mesh(geometry, material)

  // 沿Y轴正方向移动 2 米
  cube.position.set(0, 2, 0) 
  return cube
}
export {createCube}
image.png

第二个转换 缩放

只要我们在三个轴上缩放相同的数量,缩放对象就会使其变大或变小。如果我们按不同的量缩放轴,对象就会被压扁或拉伸,因此缩放是可以改变对象形状的三个基本变换中唯一的一个

缩放的值是相对于对象的初始大小

缩放是没有单位的,比例值与对象的初始大小成比例关系:

  • 1 表示初始大小的 100%
  • 2 表示初始大小的 200%
  • 0.5 表示初始大小的 50%
统一缩放:对所有三个轴使用相同的值,
  • 一个 mesh.scale.set(1, 1, 1) 的缩放,表示100%的比例缩放X轴、Y轴和Z轴,这是一个默认值
  • 一个 mesh.scale.set(2, 2, 2) 的缩放,表示200%的比例缩放X轴、Y轴和Z轴,该对象增长到其原始的2倍
  • 一个 mesh.scale.set(0.5, 0.5, 0.5) 的缩放,表示50%的比例缩放X轴、Y轴和Z轴,该对象缩小到其原始的一半
非均匀缩放:每个轴上的缩放值不同
// src/World/components/cube.js
const createCube = () => {
  ...
  // x轴不变,Y、Z轴变为原来的2倍
  cube.scale.set(1, 2, 2) 
  ...
}
export {createCube}
image.png
负比例值镜像对象

小于0的缩放值除了使对象变小或变大之外,还会 镜像对象,缩放值-1在任何单轴上都会镜像对象而不影响大小

  • 小于0且大于-1的值将镜像并被 挤压
  • 小于-1的值将镜像和 拉伸对象

注:相机和灯光无法被缩放

最后一个转换:旋转

与平移和缩放相比,旋转要更加的小心,主要是 旋转顺序很重要,不同的旋转顺序会造成不同的结果

表示旋转的类:Euler类(欧拉角)和Quaternions(四元数)
Vector3

前面两种转换类型:平移 和 缩放,他们的位置都是被存储在Vector3类中,这是一个用于表示3D向量的特殊类,这个类有.x.y.z属性和方法.set来帮助我们操作它们。每当我们创建任何场景对象时,例如MeshVector3都会被自动创建并存储在.position

欧拉角

旋转类Euler,与.position.scale一样,当我们创建一个新的场景对象时,会自动创建一个Euler实例并为其赋予默认值

Vector3一样,有.x.y.z属性,以及.set方法:

mesh.rotation.x = 2;
mesh.rotation.y = 2;
mesh.rotation.z = 2;

mesh.rotation.set(2, 2, 2);
旋转单位:弧度

上学的时候我们应该都学过弧度制,圆的一周是360°,即,所以180°,一个直角就是π/2,之前透视相机的视野是用度数指定的,但是其他的角度都是使用 弧度 而不是 度数 指定的

度数转弧度
import { MathUtils } from 'three';
const rads = MathUtils.degToRad(90); // 1.57079... = π/2

注: 因为 π = 180°1° = π/180,旋转 30° 就是 30π/180

四元数

由于书中并没有详细的讲解四元数的使用,我就简单描述一下他们的优缺点吧

  • 欧拉角的缺点:我们不能将两个欧拉角进行相加(更著名的是,它们还存在一种叫做 万向锁的问题)
  • 四元数的缺点:虽然四元数没有欧拉角的缺点,但是它比欧拉角更难使用,所以我们一般还是坚持使用更简单的Euler
两种旋转对象的方法:
  1. 使用欧拉角,使用Euler类表示并存储在.rotation属性中
  2. 使用四元数,使用Quaternion类表示并存储在.quaternion属性中

转换矩阵

前面我们已经将 转换 的相关内容全部说完了,最后还有一个转换矩阵的概念还需要了解下

因为 向量欧拉角 对我们人来说相对容易使用,但是计算机的处理效率并不高,当我们追求 每秒60帧 这一难以捉摸的目标时,我们必须在 易用性效率 之间找到一条平衡线。为此,将对象的 平移、旋转和缩放 组合成一个称为 矩阵数学对象

这是尚未转换的对象的矩阵的样子。

image.png

因为矩阵的使用还是比较复杂的,我学习的也不是很深入,我就不具体讲了😂😂😂,想学习的话还是看书中的讲解吧 矩阵

因为与单独的变换相比,矩阵对CPU和GPU的处理效率要高的多

总结

这一章其实没有什么代码,很多都是一些概念性的东西,也不需要去特别的记忆,因为这些都是一些基础知识,只要写代码,肯定会用到它们,写的多了慢慢地就掌握了

像最后说到的转换矩阵,我觉得要到进阶阶段才会去专门学习吧(毕竟自身现在还是个小白)。不过随着项目的不断迭代,体量逐渐增大,渲染速度和响应速度将会变的很慢,这时候就需要提出一些优化解决方案,使用转换矩阵也许就是一种;当然了,如果有能力提前掌握也是不错的选择😏😏😏

如果文章中有描述不准确或错误的地方,欢迎各位大佬指正😀😀😀