变换和坐标系

平移、旋转和缩放:三个基本转换

平移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.addmesh.addcamera.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定义了三维空间坐标系,场景的中心点是XYZ轴的交点(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属性来进行平移。平移对象会将其移动到其直接父对象坐标系中的新位置。

为了完整地描述一个物体的位置,需要存储三个信息:

  1. 物体在X轴上的位置,我们称之为x。
  2. 物体在Y轴上的位置,我们称之为y。
  3. 物体在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来操作位置。当创建任何场景对象时,例如MeshVector3都会被自动创建并存储在.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.scalelight.scale将没有效果。

旋转

旋转顺序很重要。当在X轴、Y轴和Z轴进行平移或缩放,哪个轴先设置并不重要。以下三个平移方法最终得到的结果是一样的:

  1. 平移X轴,然后Y轴,最后是Z轴。
  2. 平移Y轴,然后X轴,最后是Z轴。
  3. 平移Z轴,然后X轴,最后是Y轴。

下面的三种缩放操作最终结果也是一样的:

  1. 缩放X轴,然后Y轴,最后是Z轴。
  2. 缩放Y轴,然后X轴,最后是Z轴。
  3. 缩放Z轴,然后X轴,最后是Y轴。

但是,这三个旋转可能不会给出相同的结果:

  1. 旋转X轴,然后Y轴,最后是Z轴。
  2. 旋转Y轴,然后X轴,最后是Z轴。
  3. 旋转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)。

欧拉角有几个缺点,在创建动画或进行涉及旋转的数学时会变得很明显。特别是,不能将两个欧拉角相加。四元数没有这些缺点。另一方面,它比欧拉角更难使用。

两种旋转对象的方法:

  1. 使用欧拉角,使用Euler类表示并存储在.rotation属性中。
  2. 使用四元数,使用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

旋转注意事项

  1. 并非所有对象都可以旋转。比如 DirectionalLight就不能旋转。灯光从某个位置照射到目标,灯光的角度是根据目标的位置而不是.rotation属性计算得出的。
  2. three.js中的角度是使用弧度而不是度数指定的。唯一的例外是 PerspectiveCamera.fov属性使用度数。
相关推荐
程序员_三木14 分钟前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
MossGrower2 天前
36. Three.js案例-创建带光照和阴影的球体与平面
3d图形·webgl·three.js·光照与阴影
MossGrower2 天前
34. Three.js案例-创建球体与模糊阴影
webgl·three.js·3d渲染·阴影效果
广东数字化转型2 天前
Three.js相机Camera控件知识梳理
3d·three.js
关山月3 天前
9个学习着色器的GLSL示例
前端·three.js
程序员_三木3 天前
Three.js资源-贴图材质网站推荐
javascript·webgl·three.js·材质·贴图
MossGrower4 天前
37. Three.js案例-绘制部分球体
3d图形·webgl·three.js·球体几何体
关山月4 天前
如何使用Three.js创建3D音频可视化工具
前端·three.js
MossGrower5 天前
35. Three.js案例-创建带阴影的球体与平面
webgl·three.js·阴影·球体
MossGrower6 天前
28. Three.js案例-创建圆角矩形并进行拉伸
3d图形·webgl·three.js·圆角矩形