平移、旋转和缩放:三个基本转换
平移translation ,存储在可见对象的 .position
属性中,以及旋转rotation ,存储在 .rotation
属性中。与存储在 .scale
属性中的缩放一起,这些构成了将用于在场景中移动可见对象的三个基本变换。
只要可以使用scene.add
方法添加到场景中的每个可见对象都具有转换属性,包括网格、灯光和相机,而材质和几何图形则没有。
js
// 设置相机的位置
camera.position.set(0, 0, 10);
// 设置定向光的位置
light.position.set(10, 10, 10);
// 旋转立方体
cube.rotation.set(-0.5, -0.1, 0.8);
Object3D基类
不是为每种类型的可见对象多次重新定义.position
、.rotation
和.scale
属性,而是在 Object3D
基类上定义一次这些属性,这样可以添加到场景中的所有其他类都从该基类派生。这些包括网格、相机、灯光、点、线、助手,甚至场景本身。
Object3D
类还有许多属性和方法,由每个场景对象继承。这意味着定位和设置相机或网格的工作方式与设置灯光或场景的方式大致相同。然后根据需要将其他属性添加到场景对象,以便灯光获得颜色和强度设置,场景获得背景颜色,网格获得材质和几何体,等等。
场景图
js
// 网格添加到场景中
scene.add(mesh);
.add
方法也是在Object3D
中定义并在场景类上被继承,就像.position
,.rotation
和.scale
。所有其他派生类也继承了这个方法,比如light.add
、mesh.add
、camera.add
等等。这意味着可以将对象彼此互相添加,以创建一个顶部有场景的树结构。这种树状结构称为场景图。
当一个对象添加到另一个对象时,称一个对象为父对象 ,另一个对象为子对象。
js
// 场景图中的对象具有父子关系
parent.add(child);
场景是顶级父级 。上图中的场景有三个孩子:一个灯光和两个网格。其中一个网格也有两个孩子。每个对象(顶级场景除外)都只有一个父对象,每个对象可以有任意数量的子对象。
js
// 渲染场景
renderer.render(scene, camera);
渲染器遍历场景图,从场景开始,并使用每个对象相对于其父对象的位置、旋转和缩放来确定在哪里绘制它。
访问场景对象的子对象
可以使用 .children
数组访问场景对象的所有子对象:
js
// 访问场景的子对象
scene.add(mesh);
// 子对象数组现在只有网格对象
scene.children; // -> [mesh]
// 添加灯光对象
scene.add(light);
// 子对象数组现在有网格对象和灯光对象
scene.children; // -> [mesh, light];
// 也可以使用数组索引下标进行访问
scene.children[0]; // -> mesh
scene.children[1]; // -> light
坐标系
scene
定义了三维空间坐标系,场景的中心点是X
、Y
和Z
轴的交点(0,0,0)。
只要对象是场景的直接子对象,并把它添加到场景中时,然后平移、旋转或缩放它,该对象将相对于场景的中心移动。
每个对象都有一个坐标系
顶级场景定义了场景的中心点,而其他每个对象都定义了自己的局部空间。
js
// 创建一个场景,场景定义了系统的中心点
const scene = new Scene();
// 创建meshA网格,它也有自己的局部空间
const meshA = new Mesh();
// 创建meshB网格,它也有自己的局部空间
const meshB = new Mesh();
以上代码创建了三个坐标系。这三个坐标系在数学上没有区别。围绕场景进行的任何数学运算都将在任何对象的局部空间中以相同的方式进行。
使用场景图
使用每个对象的.add
方法.remove
方法,就可以操作场景图。
当使用scene.add
向场景添加对象时,会将这个对象嵌入到场景的坐标系中。当移动对象时,它将相对于相对于场景移动。
将一个对象添加到场景图中更深的另一个对象时,就将子对象嵌入到了父对象的本地局部空间中。当移动子对象时,它会相对于父对象的坐标系移动。坐标系可以相互嵌套,每当变换一个对象时,都是相对于它的父坐标系进行的。
js
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import {
BoxGeometry,
Color,
Mesh,
MeshStandardMaterial,
PerspectiveCamera,
Scene,
DirectionalLight,
WebGLRenderer,
} from 'three';
const canvas = ref<HTMLElement | null>(null);
onMounted(() => {
// 创建场景
const scene = new Scene();
// 设置场景的背景颜色
scene.background = new Color('gold');
// 创建相机并设置其位置
const fov = 35;
const aspect = canvas.value?.clientWidth / canvas.value?.clientHeight;
const near = 0.1;
const far = 100;
const camera = new PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 0, 10);
// 创建2*2*2盒子形状的几何体
const geometry = new BoxGeometry(1, 1, 1);
// 创建默认材质
const materialA = new MeshStandardMaterial({color:'white'});
const materialB = new MeshStandardMaterial({color:'blue'});
// 创建网格
const meshA = new Mesh(geometry, materialA);
meshA.position.x = 2;
// 创建网格
const meshB = new Mesh(geometry, materialB);
meshB.position.x = 1;
meshA.add(meshB);
// 创建灯光
const light = new DirectionalLight('red', 8);
// 灯光从(10,10,10)照向(0,0,0)
light.position.set(10, 10, 100);
// 将网格和灯光添加到场景中
scene.add(meshA,light);
// 创建渲染器
const renderer = new WebGLRenderer();
// 设置渲染器的大小
renderer.setSize(canvas.value?.clientWidth, canvas.value?.clientHeight);
// 设置设备像素比(DPR)
renderer.setPixelRatio(window.devicePixelRatio);
// 添加canvas元素在页面中
canvas.value?.append(renderer.domElement);
// 渲染场景
renderer.render(scene, camera);
});
</script>
<template>
<div ref="canvas" id="canvas"></div>
</template>
<style scoped lang="scss">
#canvas {
width: 100vw;
height: 100vh;
}
</style>
通过以上代码得出现有关系Scene⟶A⟶B。 所以,A是场景的子对象,然后B是A的子对象。当移动A对象时,它将在场景的坐标体系中移动,当移动B时, 它会在A的局部坐标体系空间中移动。A对象已沿场景坐标体系内的X轴正向平移了2个单位,B对象在A对象的坐标基础之上X轴正向平移了1个单位
js
// 添加对象A到场景中
scene.add(meshA);
// 在场景的坐标系内移动A
meshA.position.x = 2;
// 添加B对象到A对象中
meshA.add(meshB);
// 在A对象的局部坐标系空间移动B对象
meshB.position.x = 1;
当调用.render
时,渲染器会计算每个对象的位置。为此,它从场景图的底部开始并向上移动,结合每个父子节点的变换,计算每个对象相对于场景的最终位置。每个对象最开始的位置都是相对于它的父对象的中心(0,0,0)。
计算A的位置很简单,因为它是场景的直接子元素。我们沿着X轴的右侧平移了A五个单位,所以它的最终位置是x=5,y=0,z=0, 或者(5,0,0)。
当平移A时,它的局部坐标系也随之移动,计算B的最终位置时必须考虑到这一点。因为,B是A的子对象,这意味着A现在相对于场景空间在(5,0,0)的位置移动。接下来,A沿着X轴平移了B三个单位, 所以最终B在X轴的位置是5+3=8。 最终B的位置是:(8,0,0)。
平移
通过更改对象的 .position
属性来进行平移。平移对象会将其移动到其直接父对象坐标系中的新位置。
为了完整地描述一个物体的位置,需要存储三个信息:
- 物体在X轴上的位置,我们称之为x。
- 物体在Y轴上的位置,我们称之为y。
- 物体在Z轴上的位置,我们称之为z。
可以将这三个位置写成一个有序的数字列表:(x,y,z)。
所有三个轴上都是零写作(0,0,0),这个点被称为原点 。每个对象都从其父对象坐标系内的原点开始。
一个点沿X轴往 右侧 移动1个单位,沿Y轴往 上方 移动2个单位,沿Z轴往 外侧 移动3个单位被写作(1,2,3)。 一个点沿X轴往 左侧 移动2个单位,沿Y轴往 下方 移动4个单位,沿Z轴往 内侧 移动8个单位被写作(−2,−4,−8)。
可以沿着X、Y和Z轴一个接一个的平移对象,或者可以使用position.set
一次沿所有三个轴平移对象。两种情况下的最终结果将是相同的。
js
mesh.position.x = 1;
mesh.position.y = 2;
mesh.position.z = 3;
mesh.position.set(1,2,3);
在three.js中1个单位表示1米。
平移的方向
在X轴是向左或向右移动,在Y轴是向上或向下移动,在Z轴是向内或向外移动。
- X轴正向指向屏幕 右侧。
- Y轴正向指向屏幕 上方,即屏幕顶部。
- Z轴正向指向屏幕 外面,即指向屏幕前的用户。
当在平移中加入减号时,就会反转这些方向:
- 沿X轴负向移动一个对象将会使对象移动到屏幕 左侧。
- 沿Y轴负向移动一个对象将会使对象移动到屏幕 底部。
- 沿Z轴负向移动一个对象将会使对象往屏幕 内侧 移动,即背向用户移动。
位置被存储在Vector3类中
three.js有一个用于表示3D向量的特殊类,称为 Vector3
。 这个类有.x
、.y
和.z
属性和方法.set
来操作位置。当创建任何场景对象时,例如Mesh
,Vector3
都会被自动创建并存储在.position
中:
js
const mesh = new Mesh();
mesh.position = new Vector3();
// 也可以自己创建Vector3实例对象
import { Vector3 } from 'three';
const vector = new Vector3(1, 2, 3);
可以直接访问和更新.x
、.y
和.z
属性,或者我们可以使用.set
一次更改所有这三个属性:
js
vector.x; // 1
vector.y; // 2
vector.z; // 3
vector.x = 5;
vector.x; // 5
vector.set(7, 7, 7);
vector.x; // 7
vector.y; // 7
vector.z; // 7
Vector3
可以省略参数以使用默认值。如果省略所有三个参数,则创建的Vector3
将表示原点,即所有值为零:
js
const origin = new Vector3();
origin.x; // 0
origin.y; // 0
origin.z; // 0
mesh.position = new Vector3();
mesh.position.x; // 0
mesh.position.y; // 0
mesh.position.z; // 0
缩放
在所有三个轴上缩放相同的数量,缩放对象就会变大或变小。如果按不同的数量缩放,对象将被压扁或拉伸。因此,缩放是可以改变对象形状的三个基本变换中唯一的一个。
像.position
一样,.scale
也是存储在Vector3
中的, 对象的初始缩放比例是(1,1,1):
js
const mesh = new Mesh();
mesh.scale = new Vector3(1, 1, 1);
缩放的值是相对于对象的初始大小。由于.scale
和.position
都存储在于Vector3
中,因此缩放对象的方式与平移对象的方式相同。但是,虽然平移使用了three.js单位,但缩放不使用任何单位。比例值与对象的初始大小成比例:1表示初始大小的100%,2表示初始大小的200%,0.5表示初始大小的50%,依此类推。
三个轴同比缩放
以相同的量缩放所有三个轴时,对象将扩大或缩小,但保持相同比例进行缩放。一个(1,1,1)的缩放, 表示100%的比例缩放X轴、Y轴和Z轴,它是一个默认值:
js
// 将对象重置为其初始比例
mesh.scale.set(1, 1, 1);
一个(2,2,2)的缩放表示200%的比例放大X轴、Y轴和Z轴。该对象将增长到其初始大小的两倍:
js
// 将对象的大小放大1倍
mesh.scale.set(2, 2, 2);
一个(0.5,0.5,0.5)的缩放表示50%的比例缩小X轴、Y轴和Z轴。对象将缩小到其初始大小的一半:
js
// 将对象缩小到一半大小
mesh.scale.set(0.5, 0.5, 0.5);
每个轴上的缩放值不同
缩放单个轴,对象将失去其比例并被压扁或拉伸。
只缩放X轴,物体会变宽或变窄:
js
// 物体变宽
mesh.scale.x = 2;
// 物体变窄
mesh.scale.x = 0.5;
只缩放Y轴将使对象更高或更短:
js
// 高度压缩四分之一
mesh.scale.y = 0.25;
// 高度拉伸1000倍
mesh.scale.y = 1000;
只缩放Z轴,对象的深度会受到影响:
js
// 深度拉伸8倍
mesh.scale.z = 8;
// 深度压缩0.1倍
mesh.scale.z = 0.1;
同样的,可以使用.set
一次性在所有三个轴上进行缩放:
js
mesh.scale.set(2, 0.5, 6);
负比例缩放
小于零的缩放值除了使对象变小或变大之外,还会镜像对象。缩放值为−1 在任何单轴上 都会镜像对象而不影响大小:
js
mesh.scale.x = -1;
mesh.scale.y = -1;
mesh.scale.z = -1;
小于0且大于−1的值将镜像并挤压对象:
js
mesh.scale.x = -0.5;
值小于−1将镜像和拉伸对象:
js
mesh.scale.y = -2;
保持其缩放比例的同时镜像对象,需要对三个轴使用相同的值,但将其中一个设为负值。例如,将对象的大小放大一倍并在Y轴镜像,使用缩放值(2,−2,2):
js
mesh.scale.set(2, -2, 2);
将对象缩小到十分之一大小并在X轴镜像,使用缩放比例值(−0.1,0.1,0.1):
js
mesh.scale.set(-0.1, 0.1, 0.1);
相机和灯光无法缩放
并非所有对象都可以缩放。比如,相机和灯光(灯光除了RectAreaLight
)没有大小,因此缩放它们没有意义。更改camera.scale
或light.scale
将没有效果。
旋转
旋转顺序很重要。当在X轴、Y轴和Z轴进行平移或缩放,哪个轴先设置并不重要。以下三个平移方法最终得到的结果是一样的:
- 平移X轴,然后Y轴,最后是Z轴。
- 平移Y轴,然后X轴,最后是Z轴。
- 平移Z轴,然后X轴,最后是Y轴。
下面的三种缩放操作最终结果也是一样的:
- 缩放X轴,然后Y轴,最后是Z轴。
- 缩放Y轴,然后X轴,最后是Z轴。
- 缩放Z轴,然后X轴,最后是Y轴。
但是,这三个旋转可能不会给出相同的结果:
- 旋转X轴,然后Y轴,最后是Z轴。
- 旋转Y轴,然后X轴,最后是Z轴。
- 旋转Z轴,然后X轴,最后是Y轴。
.position
和.scale
的存储在Vector3
类,但该类不足以存储旋转数据。
Euler类和Quaternion类
在three.js中使用类Euler
和类Quaternion
表示旋转 。与.position
和.scale
一样,当创建一个新的场景对象时,会自动创建一个euler
实例对象并为其赋予默认值,并存储在场景对象属性.rotation
中。当创建新的场景对象(例如网格)时,也会创建一个Quaternion
并存储在场景对象属性.quaternion
中。
js
const mesh = new Mesh();
mesh.rotation = new Euler();
mesh.quaternion = new Quaternion();
与Vector3
一样,有.x
、.y
和.z
属性,以及.set
方法:
js
mesh.rotation.x = 2;
mesh.rotation.y = 2;
mesh.rotation.z = 2;
mesh.rotation.set(2, 2, 2);
同样的,也可以自己创建euler
实例:
js
import { Euler } from 'three';
const euler = new Euler(1, 2, 3);
与Vector3
一样,可以省略参数以使用默认值,所有轴的默认值为零:
js
const euler = new Euler();
euler.x; // 0
euler.y; // 0
euler.z; // 0
可以互换使用四元数和欧拉角。当更改mesh.rotation
时,mesh.quaternion
属性会自动更新,反之亦然。这意味着可以在欧拉角(Euler)适用时使用欧拉角,并在四元数适用时切换到四元数(Quaternion)。
欧拉角有几个缺点,在创建动画或进行涉及旋转的数学时会变得很明显。特别是,不能将两个欧拉角相加。四元数没有这些缺点。另一方面,它比欧拉角更难使用。
两种旋转对象的方法:
- 使用欧拉角,使用
Euler
类表示并存储在.rotation
属性中。 - 使用四元数,使用
Quaternion
类表示并存储在.quaternion
属性中。
旋转顺序
默认情况下,three.js将在对象的局部空间中围绕X轴,然后围绕Y轴,最后围绕Z轴旋转。可以使用 Euler.order
属性来改变它。默认顺序称为"XYZ",但也可以使用"YZX"、"ZXY"、"XZY"、"YXZ"和"ZYX"。通常,需要更改旋转顺序的唯一时候是在处理来自另一个应用程序的旋转数据。
旋转单位
透视相机的视野是用度数指定的。但是,three.js中的所有其他角度都是使用弧度而不是度数指定的 。360°是一个圆圈,与之对应的是2π弧度。90°是一个直角,与之对应的是2π弧度。可以使用.degToRad
将度数转换为弧度。
js
// 将度数转换为弧度
import { MathUtils } from 'three';
const rads = MathUtils.degToRad(90); // 1.57079... = π/2
旋转注意事项
- 并非所有对象都可以旋转。比如
DirectionalLight
就不能旋转。灯光从某个位置照射到目标,灯光的角度是根据目标的位置而不是.rotation
属性计算得出的。 - three.js中的角度是使用弧度而不是度数指定的。唯一的例外是
PerspectiveCamera.fov
属性使用度数。