很多东西汇集在一起构成一个美丽的3D场景,例如光照、材质、模型、纹理、相机设置、后期处理、粒子效果、交互性等等,但无论我们创建什么样的场景,没有比这更多的了比组成它的乐曲的排列和运动更重要。 要创建建筑效果图,我们必须成为建筑师和室内装饰师。我们必须考虑建筑物和里面房间的比例,巧妙地放置家具和灯具。在自然场景中,无论是一朵花的特写,还是广阔的山景,我们都需要以自然而令人信服的方式安排树木和岩石,或叶子和花瓣。也许一群入侵的机器人会扫过大地,眼睛闪闪发光,手臂和脚摆动,齐声前进,火箭冲向天空,在它们着陆的地方造成巨大的爆炸在这种情况下,我们必须同时成为机器人设计师和弹道学专家。 即使是纯粹的抽象场景也需要了解如何在3D空间中四处移动对象。 推荐:使用NSDT场景设计器快速搭建可编程的3D场景。 最后,我们还必须成为导演,将摄影机摆好位置,对每个镜头进行艺术构图。创建3D场景时,唯一的限制是您的想象力以及您的技术知识深度。 在3D空间中四处移动对象是学习three。js的一项基本技能。我们将这项技能分为两部分:首先,我们将探索用于描述3D空间的坐标系,然后我们将探索称为变换的数学运算,用于在坐标系内移动对象。 在此过程中,我们会遇到几个数学对象,例如场景图,一种用于描述构成场景的对象层次结构的结构,向量,用于描述3D空间中的位置(以及许多其他事物),以及不少于两种描述旋转的方式:欧拉角和四元数。我们将通过向您介绍变换矩阵来结束本章,变换矩阵用于存储对象的完整变换状态。1、平移、旋转和缩放 每当我们在3D空间中四处移动对象时,我们都会使用称为变换的数学运算来进行此操作。我们已经看到了两种转换:平移,存储在对象的。position属性中,旋转,存储在。rotation属性中。与存储在。scale属性中的缩放一起,这些构成了我们将用来在场景中四处移动对象的三个基本变换。我们有时会使用它们的首字母TRS来指代变换、旋转和缩放。 我们可以使用scene。add添加到场景中的每个对象都具有这些属性,包括网格、灯光和相机,而材质和几何体则没有。我们之前使用。position来设置相机的位置:camera。position。set(0,0,10); 以及设置有向光的位置:light。position。set(10,10,10); 之前我们也使用。rotation来更好地查看我们的立方体:cube。rotation。set(0。5,0。1,0。8); 到目前为止,我们唯一没有遇到的根本性转变是。scale。2、Object3D基类 不是为每种类型的对象多次重新定义。position、。rotation和。scale属性,而是在Object3D基类上定义一次这些属性,然后可以添加到场景中的所有其他类都派生自该基类。这包括网格、相机、灯光、点、线、助手,甚至场景本身。我们将非正式地将派生自Object3D的类称为场景对象。 Object3D除了这三个之外还有很多属性和方法,被每个场景对象继承。这意味着定位和设置相机或网格的工作方式与设置灯光或场景的方式大致相同。然后根据需要将其他属性添加到场景对象,因此灯光获得颜色和强度设置,场景获得背景颜色,网格获得材质和几何形状,等等。3、场景图 回想一下我们是如何将网格添加到场景中的:scene。add(mesh); 。add方法也是在Object3D上定义的,和。position、。rotation、。scale一样被场景类继承。所有其他派生类也继承此方法,为我们提供light。add、mesh。add、camera。add等。这意味着我们可以将对象相互添加以创建树结构,场景位于顶部。这种树结构被称为场景图。 当我们将一个对象添加到另一个对象时,我们将一个对象称为父对象,将另一个对象称为子对象。parent。add(child); 场景是顶级父级。上图中的场景有三个孩子:一个灯光和两个网格。其中一个网格也有两个孩子。但是,每个对象(顶级场景除外)只有一个父对象。 场景图中的每个对象(顶级场景除外)只有一个父对象,并且可以有任意数量的子对象。 当我们渲染场景时:renderer。render(scene,camera); 渲染器遍历场景图,从场景开始,并使用每个对象相对于其父对象的位置、旋转和比例来确定将其绘制在何处。4、访问场景的子对象 可以使用。children数组访问场景对象的所有子对象:scene。add(mesh);thechildrenarraycontainsthemeshweaddedscene。children;〔mesh〕now,addalight:scene。add(light);thechildrenarraynowcontainsboththemeshandthelightscene。children;〔mesh,light〕;nowyoucanaccessthemeshandlightusingarrayindicesscene。children〔0〕;meshscene。children〔1〕;light 有更复杂的方法来访问特定的子对象,例如Object3d。getObjectByName方法。但是,当你不知道对象的名称或对象没有名称时,直接访问。children数组很有用。5、坐标系:世界空间和局部空间 Three。js使用3D笛卡尔坐标系描述3D空间。 3D笛卡尔坐标系使用在点(0,0,0)(称为原点)处交叉的X、Y和Z轴表示。二维坐标系类似,但只有X和Y轴。 每个3D图形系统都使用这样的坐标系,从Unity和Unreal等游戏引擎,到Pixar用来制作电影的软件,再到3DSMax、Maya和Blender等专业动画和建模软件。即使是用于在网页上定位对象的语言CSS,也使用笛卡尔坐标系。但是,这些系统之间可能存在细微的技术差异,例如轴的标记不同或指向不同的方向。 在使用three。js时,我们会遇到几个2D和3D坐标系。在这里,我们将介绍其中最重要的两个:世界空间和局部空间。6、世界空间 我们的场景定义了世界空间坐标系,系统的中心是X、Y和Z轴相交的点。 还记得之前当我们第一次介绍Scene类时,我们称它为小宇宙吗?这个微小的宇宙就是世界空间。 当我们在场景中布置物体时无论是在房间中放置家具、在森林中放置树木,还是在战场上狂暴的机器人我们在屏幕上看到的是每个物体在世界空间中的位置。 当我们将一个对象直接添加到场景中然后对其进行平移、旋转或缩放时,该对象将相对于世界空间移动即相对于场景的中心。addacubetoourscenescene。add(cube);movethecuberelativetoworldspacecube。position。x5; 这两个语句是等价的,只要对象是场景的直接子对象:相对于世界空间变换对象。在场景中四处移动对象。 每当我们尝试在3D中可视化一些棘手的东西时,降低维度并考虑2D类比会很有用。所以,让我们考虑一个棋盘。当我们安排棋子开始新游戏时,我们将它们放在棋盘上的特定位置。这意味着棋盘是场景,棋子是我们放置在场景中的对象。 接下来,当我们向某人解释为什么我们这样排列棋子时,一侧是白色,另一侧是黑色,第二行是棋子,等等,我们这样做是相对于棋盘本身的。该板定义了一个坐标系,Y轴为行,X轴为列。这是棋盘的世界空间,我们解释每个棋子相对于这个坐标系的位置。 现在游戏开始了,我们开始移动棋子。当我们这样做时,我们遵循国际象棋的规则。当我们在three。js场景中移动对象时,我们遵循笛卡尔坐标系的规则。这里的类比有点不合理,因为棋盘上的每一块棋子都有自己的移动方式,而在笛卡尔坐标系中,平移、旋转和缩放对于任何类型的对象都是一样的。7、局部空间 现在,考虑其中一个棋子。如果要求描述一个棋子的形状,你不会描述它相对于棋盘的样子,因为它可以放在棋盘上的任何地方,而且实际上,即使根本不在棋盘上也能保持它的形状。相反,你将在脑海中创建一个新的坐标系并描述该作品在那里的外观。 就像棋盘上的棋子一样,我们可以添加到场景中的每一个物体也都有一个局部坐标系,物体的形状(几何)就是在这个局部坐标系内描述的。当我们创建网格或灯光时,我们还创建了一个新的局部坐标系,网格或灯光位于其中心。这个局部坐标系有X、Y和Z轴,就像世界空间一样。对象的局部坐标系称为局部空间(有时也称为对象空间)。 当我们创建一个222的BoxBufferGeometry,然后使用该几何体创建一个网格时,几何体的大小是网格局部空间中每边的两个单位:constgeometrynewBoxBufferGeometry(2,2,2);constmeshnewMesh(geometry,material); 正如我们将在下面看到的,我们可以使用。scale拉伸或缩小网格,并且屏幕上绘制的网格大小将会改变。但是,当我们缩放网格时,几何体的大小不会改变。当渲染器来渲染网格时,它会看到它已经缩放,然后以不同的大小绘制网格。8、每个对象都有一个坐标系 回顾一下:顶级场景定义了世界空间,每个其他对象定义了自己的局部空间。creatingthescenecreatestheworldspacecoordinatesystemconstscenenewScene();meshAhasitsownlocalcoordinatesystemconstmeshAnewMesh();meshBalsohasitsownlocalcoordinatesystemconstmeshBnewMesh(); 通过以上三行代码,我们创建了三个坐标系。这三个坐标系在数学上没有区别。我们可以在世界空间中进行的任何数学运算在任何对象的局部空间中都将以相同的方式工作。 人们很容易将坐标系视为复杂的大事物,但是,在3D中工作时,你会发现周围有很多坐标系。每个对象至少有一个,有的有几个。渲染场景还涉及另一套完整的坐标系,即将对象从3D世界空间转换为在屏幕的平面2D表面上看起来不错的东西。每个纹理甚至都有一个2D坐标系。最后,它们并没有那么复杂,而且制造起来非常便宜。9、使用场景图 使用每个对象的。add和。remove方法,我们可以创建和操作场景图。 当我们使用scene。add将一个对象添加到我们的场景时,我们将这个对象嵌入到场景的坐标系,世界空间中。当我们四处移动对象时,它会相对于世界空间(或等效地,相对于场景)移动。 当我们将一个对象添加到场景图中更深处的另一个对象时,我们将子对象嵌入到父对象的局部空间中。当我们移动子对象时,它会相对于父对象的坐标系移动。坐标系像俄罗斯套娃一样相互嵌套。 让我们看一些代码。首先,我们将添加一个对象A作为场景的子对象:scene。add(meshA); 现在,场景是A的父级,或者等效地,A是场景的子级。接下来,我们将移动A:meshA。position。x5; 现在,A已在世界空间内沿正X轴平移五个单位。每当我们变换一个对象时,我们都是相对于其父坐标系进行的。接下来,让我们看看当我们添加第二个对象B作为A的子对象时会发生什么:meshA。add(meshB); A仍然是场景的子对象,所以我们有关系SceneAB。所以,A是场景的子对象,B是A的子对象。或者,等价地,A现在生活在世界空间中,B现在生活在A的本地空间中。当我们移动A时,它会在世界空间中四处移动,而当我们移动B时,它会在A的局部空间中四处移动。 接下来,我们将平移B:meshB。position。x3; 你认为B最终会去哪里?10、我们看到的是世界空间 当我们调用。render时,渲染器会计算每个对象的世界空间位置。为此,它从场景图的底部开始向上工作,结合每个父对象和子对象的变换,计算每个对象相对于世界空间的最终位置。我们最终在屏幕上看到的是世界空间。在这里,我们将为A和B手动执行此计算。请记住,每个对象都从相对于其父对象的(0,0,0)开始。Astartsat(0,0,0)inworldspacescene。add(meshA);Bstartsat(0,0,0)inAslocalspacemeshA。add(meshB);meshA。position。x5;meshB。position。x3; 计算A的位置很容易,因为它是场景的直接子对象。我们将A沿X轴向右移动了五个单位,因此它的最终位置为x5,y0,z0,即(5,0,0)。 当我们移动A时,它的局部坐标系也随之移动,在计算B的世界空间位置时我们必须考虑到这一点。因为B是A的子级,这意味着它现在开始于(5,0,0)相对于世界空间。接下来,我们将B相对于A沿X轴移动了三个单位,所以B在X轴上的最终位置为538。这给了我们B在世界空间中的最终位置:(8,0,0)。11、在坐标系之间移动对象 如果我们将一个对象从一个坐标系移动到另一个坐标系会发生什么?换句话说,如果我们使用网格B,在不改变其位置的情况下,将其从A中移除并直接添加到场景中,会发生什么情况?我们可以在一行中完成:scene。add(meshB); 一个对象只能有一个父对象,因此B(在本例中为网格A)的任何先前父对象都将被删除。 以下陈述仍然成立:B已在其父坐标系内沿正X轴平移了三个单位。然而,B的父级现在是场景而不是A,所以现在我们必须重新计算B在世界空间而不是A的局部空间中的位置,这将为我们提供(3,0,0)。 这就是坐标系。在本章的其余部分,我们将更深入地了解三种基本变换中的每一种:平移、旋转和缩放。12、第一个变换:平移 三个基本变换中最简单的是平移(Translate)。我们已经在几个示例中使用了它,并且还设置了我们场景中相机和灯光的位置。我们通过更改对象的。position属性来执行平移。平移对象会将其移动到其直接父级坐标系内的新位置。 为了完整描述一个物体的位置,我们需要存储三个信息:对象在X轴上的位置,我们称之为x。对象在Y轴上的位置,我们称之为y。对象在Z轴上的位置,我们称之为z。 我们可以将这三个位置写成一个有序的数字列表:(x,y,z)。 所有三个轴上的零都写为(0,0,0),正如我们之前提到的,该点称为原点。每个对象都从其父对象坐标系中的原点开始。 沿X轴向右一个单位,沿Y轴向上两个单位,沿Z轴向外三个单位的位置被写为(1,2,3)。沿X轴向左两个单位、沿Y轴向下四个单位、沿Z轴向后八个单位的位置被写为(2,4,8)。 我们称这样的有序数字列表为向量,因为有三个数字,所以它是一个3D向量。13、平移一个对象 我们可以沿X、Y和Z轴一一平移,或者我们可以使用position。set同时沿所有三个轴平移。两种情况下的最终结果将是相同的。translateoneaxisatatimemesh。position。x1;mesh。position。y2;mesh。position。z3;translateallthreeaxesatoncemesh。position。set(1,2,3); 当我们执行平移(1,2,3)时,我们正在执行数学运算:(0,0,0)(1,2,3) 这意味着:从点(0,0,0)移动到点(1,2,3)。14、平移单位米 当我们执行平移mesh。position。x2时,我们将对象沿X轴向右移动两个three。js单位,正如我们之前提到的,我们总是取一个three。js单位相等一米。15、世界空间中的方向 上面我们提到了将对象在X轴上向左或向右移动,在Y轴上向上或向下移动,以及在Z轴上向内或向外移动。这些方向是相对于你的屏幕的,并且假设你没有旋转相机。在这种情况下,以下指示成立:正X轴指向屏幕右侧。正Y轴指向上方,指向屏幕顶部。正Z轴指向屏幕外 然后,当你移动一个对象时:X轴上的正平移会将对象移动到屏幕右侧。Y轴上的正平移会将对象向上移动到屏幕顶部Z轴上的正平移会将对象移向你 当我们在平移中使用负号时,我们颠倒了这些方向:X轴上的负平移会将对象移动到屏幕左侧Y轴上的负平移会将对象向下移动到屏幕底部Z轴上的负平移会将对象移入远离你的位置。 但当然,你可以向任何方向旋转相机,在这种情况下,这些方向将不再适用。毕竟,你在屏幕上看到的是相机的视点。然而,能够使用正常语言描述世界空间中的方向是很有用的,因此我们将把这个相机位置视为默认视图并继续使用这个术语描述方向,无论相机恰好在哪里。16、位置存储在Vector3类中 Three。js有一个用于表示3D向量的特殊类,称为Vector3。这个类有。x,。y和。z属性和类似。set的方法来帮助我们操作它们。每当我们创建任何场景对象(例如网格)时,都会自动创建一个Vector3并存储在。position中:whenwecreateamesh。。。constmeshnewMesh();。。。internally,three。jscreatesaVector3forus:mesh。positionnewVector3(); 我们也可以自己创建Vector3实例:import{Vector3}fromthree;constvectornewVector3(1,2,3); 我们可以直接访问和更新。x、。y和。z属性,或者我们可以使用。set一次更改所有三个属性:vector。x;1vector。y;2vector。z;3vector。x5;vector。x;5vector。set(7,7,7);vector。x;7vector。y;7vector。z;7 与几乎所有three。js类一样,我们可以省略参数以使用默认值。如果我们省略所有三个参数,则创建的Vector3将代表原点,所有值为零:constoriginnewVector3();origin。x;0origin。y;0origin。z;0mesh。positionnewVector3();mesh。position。x;0mesh。position。y;0mesh。position。z;0 three。js也有表示2D向量和4D向量的类,但是,3D向量是迄今为止我们会遇到的最常见的向量类型。17、向量是通用的数学对象 向量可以表示各种事物,而不仅仅是平移。任何可以表示为两个、三个或四个数字的有序列表的数据通常存储在其中一个向量类中。这些数据类型分为三类:空间中的一个点。坐标系内的长度和方向。没有更深的数学意义的数字列表。 第二类是向量的数学定义,平移属于这一类。第一类和第三类在技术上不是向量。然而,在向量类中重用代码是很有用的,所以我们对此视而不见。18、第二个变换:缩放 缩放对象会使它变大或变小,只要我们在所有三个轴上缩放相同的量即可。如果我们按不同的量缩放轴,对象将被压扁或拉伸。因此,缩放是唯一的可以改变对象形状的基本变换。 与。position一样,。scale存储在一个Vector3中,对象的初始比例为(1,1,1):whenwecreateamesh。。。constmeshnewMesh();。。。internally,three。jscreatesaVector3forus:mesh。scalenewVector3(1,1,1);19、比例值是相对于对象的初始大小 由于。scale和。position都存储在Vector3中,因此缩放对象的工作方式与平移它的方式大致相同。然而,虽然translation使用three。js单位,但scale不使用任何单位。相反,缩放值与对象的初始大小成正比:1表示初始大小的100,2表示初始大小的200,0。5表示初始大小的50,依此类推。20、均匀缩放 当我们将所有三个轴缩放相同的量时,对象将扩大或缩小,但保持其比例。这称为均匀缩放。默认值为(1,1,1)的比例,表示X轴、Y轴和Z轴上的比例为100:mesh。scale。set(1,1,1); (2,2,2)的比例表示X轴、Y轴和Z轴上的比例为200。该对象将增长到其初始大小的两倍:mesh。scale。set(2,2,2); (0。5,0。5,0。5)的比例表示X轴、Y轴和Z轴上的比例为50。该对象将缩小到其初始大小的一半:mesh。scale。set(0。5,0。5,0。5);21、非均匀缩放 如果我们缩放单个轴,对象将失去其比例并被压扁或拉伸。这称为非均匀缩放。如果我们只缩放X轴,对象将变得更宽或更窄:doubletheinitialwidthmesh。scale。x2;halvetheinitialwidthmesh。scale。x0。5; 在Y轴上缩放将使对象变高或变矮:squashthemeshtoonequarterheightmesh。scale。y0。25;stretchthemeshtoatoweringonethousandtimesitsinitialheightmesh。scale。y1000; 最后,如果我们在Z轴上缩放,对象的深度将受到影响:stretchtheobjecttoeighttimesitsinitialdepthmesh。scale。z8;squashtheobjecttoonetenthofitsinitialdepthmesh。scale。z0。1; 再一次,我们可以使用。set同时在所有三个轴上进行缩放:mesh。scale。set(2,0。5,6);22、负比例值镜像对象 小于零的比例值除了使对象变小或变大之外,还会镜像对象。任何单个轴上的比例值为1将镜像对象而不影响大小:mirrorthemeshacrosstheXaxismesh。scale。x1;mirrorthemeshacrosstheYaxismesh。scale。y1;mirrorthemeshacrosstheZaxismesh。scale。z1; 小于零和大于1的值将镜像和挤压对象:mirrorandsquashmeshtohalfwidthmesh。scale。x0。5; 小于1的值将镜像并拉伸对象:mirrorandstretchmeshtodoubleheightmesh。scale。y2;23、均匀缩放和镜像 要在保持比例的同时镜像一个对象,请对所有三个轴使用相同的值,但将其中一个设为负值。例如,要将对象的大小加倍并在Y轴上进行镜像,请使用(2,2,2)的比例值:mesh。scale。set(2,2,2); 或者,要将对象缩小到十分之一大小并在X轴上镜像,请使用(0。1,0。1,0。1)的比例值:mesh。scale。set(0。1,0。1,0。1);24、相机和灯光无法缩放 并非所有对象都可以缩放。例如,相机和灯光(RectAreaLight除外)没有尺寸,因此缩放它们没有意义。更改camera。scale或light。scale不会有任何效果。25、第三个变换:旋转 旋转比平移或缩放需要更多的注意。这有几个原因,但最主要的是轮换顺序。如果我们在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类不足以存储旋转数据。相反,three。js没有一个,而是两个用于存储旋转数据的数学类。我们将在这里查看其中更简单的一个:欧拉角。幸运的是,它类似于Vector3类。26、表示旋转:欧拉角 欧拉角在three。js中使用Euler类表示。与。position和。scale一样,当我们创建一个新的场景对象时,会自动创建一个Euler实例并赋予默认值。whenwecreateamesh。。。constmeshnewMesh();。。。internally,three。jscreatesanEulerforus:mesh。rotationnewEuler(); 与Vector3一样,有。x、。y和。z属性以及一个。set方法:mesh。rotation。x2;mesh。rotation。y2;mesh。rotation。z2;mesh。rotation。set(2,2,2); 再一次,我们可以自己创建Euler实例:import{Euler}fromthree;consteulernewEuler(1,2,3); 也像Vector3一样,我们可以省略参数以使用默认值,同样,所有轴上的默认值为零:consteulernewEuler();euler。x;0euler。y;0euler。z;027、欧拉旋转顺序 默认情况下,three。js将在对象的局部空间中执行绕X轴的旋转,然后绕Y轴,最后绕Z轴。我们可以使用Euler。order属性更改它。默认顺序称为XYZ,但也可以是YZX、ZXY、XZY、YXZ和ZYX。 我们不会在这里进一步讨论轮换顺序。通常,你唯一需要更改顺序的时间是在处理来自另一个应用程序的旋转数据时。即便如此,这通常由three。js加载程序处理。现在,如果愿意,你可以简单地将Euler视为Vector3。在你开始创建动画或执行涉及旋转的复杂数学运算之前,这样做不太可能会遇到任何问题。28、旋转单位是弧度 你可能熟悉使用度数表示旋转。圆是360,直角是90,依此类推。我们之前遇到的透视相机的视野以度为单位指定。 但是,three。js中的所有其他角度都使用弧度而不是度数指定。圆的弧度是2,直角弧度是2。如果你习惯使用弧度,那就太好了!至于我们其他人,我们可以使用。degToRad实用程序将度数转换为弧度。import{MathUtils}fromthree;constradsMathUtils。degToRad(90);1。57079。。。2 在这里,我们可以看到90等于1。57079。。。,或2弧度。29、另一个旋转表示:四元数 上面我们提到three。js有两个类来表示旋转。第二个是Quaternion类,我们只是在这里顺便提一下。与Euler一起,每当我们创建一个新的场景对象(例如网格)时,都会为我们创建一个四元数并将其存储在。quaternion属性中:whenwecreateameshconstmeshnewMesh();。。。internally,three。jscreatesanEulerforus:mesh。rotationnewEuler();。。ANDaQuaternion:mesh。quaternionnewQuaternion(); 我们可以互换使用四元数和欧拉角。当我们更改mesh。rotation时,mesh。quaternion属性会自动更新,反之亦然。这意味着我们可以在适合我们的时候使用欧拉角,并在适合我们的时候切换到四元数。 欧拉角有几个缺点,在创建动画或进行涉及旋转的数学运算时会变得很明显。特别是,我们不能将两个欧拉角相加(更著名的是,它们还遭受称为万向节锁定的问题)。四元数没有这些缺点。另一方面,它们比欧拉角更难使用,所以现在我们将坚持使用更简单的欧拉角。 现在,记下这两种旋转对象的方法:使用欧拉角,使用欧拉类表示并存储在。rotation属性中。使用四元数,使用Quaternion类表示并存储在。quaternion属性中。30、关于旋转对象的重要事项 尽管我们在本节中强调了一些问题,但旋转对象通常是直观的。以下是一些需要注意的重要事项:并非所有对象都可以旋转。比如我们上一章介绍的DirectionalLight是不能旋转的。光线从一个位置照射到一个目标,光线的角度是根据目标的位置计算的,而不是。rotation属性。three。js中的角度使用弧度指定,而不是度数。唯一的例外是PerspectiveCamera。fov属性,它使用度数来匹配真实世界的摄影惯例。31、变换矩阵 我们在本文中介绍了很多内容。我们介绍了笛卡尔坐标系、世界空间和局部空间、场景图、平移、旋转和缩放以及相关的。position、。rotation和。scale属性,以及用于存储变换的三个数学类:Vector3、Euler和四元数。我们肯定不能塞进其他东西吗? 好吧,还有一件事。我们不能在不讨论变换矩阵的情况下结束关于变换的一章。虽然向量和欧拉角对我们人类来说(相对)容易处理,但它们对计算机的处理效率不高。当我们追求每秒60帧的难以捉摸的目标时,我们必须在易用性和效率之间保持平衡。为此,对象的平移、旋转和缩放被组合到一个称为矩阵的数学对象中。这是未转换的对象的矩阵的样子。 它有四行四列,所以它是一个44矩阵,它存储了一个对象的完整变换,这就是我们将其称为变换矩阵的原因。再一次,有一个three。js类来处理这种类型的数学对象,称为Matrix4。还有一个用于33矩阵的类,称为Matrix3。当矩阵的主对角线上全为1,其他地方都为0时,如上图所示,我们称其为单位矩阵I。 与单独的变换相比,矩阵对于CPU和GPU的处理效率要高得多,并且代表了一种让我们两全其美的折衷方案。我们人类可以使用更简单的。position、。rotation和。scale属性,然后,每当我们调用。render时,渲染器将更新每个对象的矩阵并将它们用于内部计算。 我们将在这里花一些时间来了解变换矩阵的工作原理,但如果你对数学过敏,那么跳过这一部分(暂时)绝对没问题。你不需要深入了解矩阵的工作原理即可使用three。js。你可以坚持使用。position、。rotation和。scale,让three。js处理矩阵。另一方面,如果你是一位数学奇才,直接使用变换矩阵会打开一个全新的机会范围。32、局部矩阵 事实上,每个对象都有两个变换矩阵,而不是一个。第一个是局部矩阵,它包含对象的组合。position、。rotation和。scale。局部矩阵存储在Object3D。matrix属性中。从Object3D继承的每个对象都具有此属性。whenwecreateameshconstmeshnewMesh();。。。internally,three。jscreatesaMatrix4forus:mesh。matrixnewMatrix4(); 此时,矩阵将类似于上面的单位矩阵,主对角线上为1,其他各处为0。如果我们改变物体的位置,然后强制矩阵更新:mesh。position。x5;mesh。updateMatrix(); 现在,网格的局部矩阵将如下所示: 通常,我们不需要手动调用。updateMatrix,因为渲染器会在渲染之前更新每个对象的矩阵。但是,在这里,我们希望立即看到矩阵中的变化,因此我们必须强制更新。 如果我们改变所有三个轴上的位置并再次更新矩阵:mesh。position。x2;mesh。position。y4;mesh。position。z6;mesh。updateMatrix(); 。。。现在我们可以看到平移存储在矩阵最后一列的前三行中。 接下来,让我们对缩放做同样的事情:mesh。scale。x5;mesh。scale。y7;mesh。scale。z9;mesh。updateMatrix(); 我们会看到比例值存储在主对角线上。 太棒了!这意味着我们可以编写一个公式来在变换矩阵中存储平移和缩放比例。mesh。position。xTx;mesh。position。yTy;mesh。position。zTz;mesh。scale。xSx;mesh。scale。ySy;mesh。scale。zSz; 现在转换矩阵如下所示: 最后,让我们看看旋转是如何存储的。首先,让我们重新设置位置和比例:mesh。position。set(0,0,0);mesh。scale。set(1,1,1);mesh。updateMatrix(); 现在矩阵看起来又像单位矩阵了,主对角线上是1,其他地方都是0。接下来,让我们尝试绕X轴旋转30度:mesh。rotation。xMathUtils。degToRad(30);mesh。updateMatrix(); 。。。那么矩阵将如下所示: 嗯很奇怪。然而,当我们看到以下等式时,这更有意义:cos(30)0。866。。。sin(30)0。5 所以,这个矩阵实际上是: 不幸的是,这并不像上面的转换和缩放示例那样直观。但是,我们再次使用它来编写公式。如果我们将绕X轴的旋转写为Rx,这是绕X轴旋转的公式: 最后,绕Z轴旋转Rz的转换矩阵是: 33、世界矩阵 正如我们多次提到的,对我们来说重要的是对象在世界空间中的最终位置,因为这是我们在渲染对象后看到的。为了帮助计算这一点,每个对象都有第二个变换矩阵,即世界矩阵,存储在Object3D。matrixWorld中。这两个矩阵在数学上没有区别。它们都是44变换矩阵,当我们创建网格或任何其他场景对象时,局部矩阵和世界矩阵都会自动创建。whenwecreateameshconstmeshnewMesh();。。。internally,three。jscreatesthelocalmatrixandtheworldmatrixmesh。matrixnewMatrix4();mesh。matrixWorldnewMatrix4(); 世界矩阵存储对象在世界空间中的位置。如果对象是场景的直接子对象,则这两个矩阵将相同,但如果对象位于场景图下方的某个位置,则局部矩阵和世界矩阵很可能会不同。 为了帮助我们理解这一点,让我们再次看一下之前的对象A和B:constscenenewScene();constmeshAnewMesh();constmeshBnewMesh();Astartsat(0,0,0)inworldspacescene。add(meshA);Bstartsat(0,0,0)inAslocalspacemeshA。add(meshB);moveArelativetoitsparentthescenemeshA。position。x5;moveBrelativetoitsparentAmeshB。position。x3;meshA。updateMatrix();meshA。updateMatrixWorld();meshB。updateMatrix();meshB。updateMatrixWorld(); 再一次,我们必须强制更新矩阵。或者,你可以调用。render并且场景中所有对象的矩阵将自动更新。 回想一下,我们计算了A和B在世界空间中的最终位置,发现A位于(5,0,0),而B最终位于(8,0,0)。让我们来看看这对每个对象的本地和世界矩阵是如何工作的。首先是A的局部矩阵。 正如我们在上面看到的,对象在X轴上的位置存储在其局部矩阵顶行的最后一列中。现在,让我们看看A的世界矩阵: 由于A是场景的直接子节点,因此局部矩阵和世界矩阵是相同的。现在,让我们看一下B。首先,局部矩阵: 最后,这是B的世界矩阵: 这一次,本地矩阵和世界矩阵不同,因为B不是场景的直接子对象。34、直接使用矩阵 希望这个简短的介绍已经揭开了矩阵工作原理的一些神秘面纱。它们并不像看起来那么复杂,相反,它们只是一种存储大量数字的紧凑方式。然而,记住所有这些数字需要一些练习,并且手动进行涉及矩阵的计算是乏味的。幸运的是,three。js带有许多函数,可以让我们轻松地处理矩阵。有明显的函数,如加法、乘法、减法,以及设置和获取矩阵的平移、旋转或缩放分量的函数,等等。 几乎从来不需要直接使用矩阵,而不是分别设置。position、。rotation和。scale,但它确实允许对对象的变换进行强大的操作。把它想象成一个超级大国,一旦你的three。js技能达到足够的水平,你就会解锁。 当一起使用时,我们在本章中遇到的所有属性。position、。rotation、。scale、。quaternion、。matrix和。matrixWorld都具有巨大的表现力,使你能够像艺术家一样创建场景画笔。whenwecreateamesh,oranyotherobjectderivedfromObject3Dsuchaslights,camera,oreventhesceneitselfconstmeshnewMesh();。。。internally,three。jscreatesmanydifferentobjectstohelpustransformtheobjectmesh。positionnewVector3();mesh。scalenewVector3();mesh。rotationnewEuler();mesh。quaternionnewQuaternion();mesh。matrixnewMatrix4();mesh。matrixWorldnewMatrix4(); 学习如何使用。position、。rotation和。scale是使用three。js所需的一项基本技能。然而,学习使用。quaternion和变换矩阵是一项高级技能,你不需要立即掌握。 原文链接:http:www。bimant。comblogthreejscoordinatesandtransforms