跳转至

5.2 构建对象

Building Objects

three.js中,一个可见对象是由几何体和材质构建的。我们已经看到了如何创建适用于点和线原语的简单几何体,并遇到了使用GL_TRIANGLES原语的各种标准网格几何体,例如THREE.CylinderGeometryTHREE.IcosahedronGeometry。在这一部分,我们将看到如何从头开始创建新的网格几何体。我们还将看看three.js为处理对象和材质提供的一些其他支持。

In three.js, a visible object is constructed from a geometry and a material. We have seen how to create simple geometries that are suitable for point and line primitives, and we have encountered a variety of standard mesh geometries, such as THREE.CylinderGeometry and THREE.IcosahedronGeometry, that use the GL_TRIANGLES primitive. In this section, we will see how to create new mesh geometries from scratch. We'll also look at some of the other support that three.js provides for working with objects and materials.

5.2.1 多边形网格和 IFS

Polygonal Meshes and IFSs

three.js中,我们所说的网格是指在第3.4节中提到的多边形网格,尽管在three.js的网格中,所有的多边形都必须是三角形。在WebGL中绘制多边形网格有两种方法。一种使用的是函数glDrawArrays(),它只需要一个顶点列表。另一种使用的是我们称之为索引面集(IFS)的表示方法,它是使用函数glDrawElements()绘制的。除了顶点列表,IFS还使用面索引列表来指定三角形。我们将以这个金字塔为例,来查看两种方法:

123

请注意,金字塔的底部是一个正方形,必须被分割成两个三角形,以便将金字塔表示为网格几何体。顶点从0编号到4。一个三角形面可以通过三个数字来指定,这三个数字给出了该三角形顶点的顶点编号。像往常一样,当从前面,即从金字塔的外部观察时,三角形的顶点应该按逆时针顺序指定。这里是我们需要的数据。

    VERTEX COORDINATES:         FACE INDICES:

    Vertex 0:   1, 0,  1        Face 1:  3, 2, 1 
    Vertex 1:   1, 0, -1        Face 2:  3, 1, 0 
    Vertex 2:  -1, 0, -1        Face 3:  3, 0, 4 
    Vertex 3:  -1, 0,  1        Face 4:  0, 1, 4 
    Vertex 4:   0, 1,  0        Face 5:  1, 2, 4 
                                Face 6:  2, 3, 4

一个基本的多边形网格表示不使用面索引。相反,它通过列出顶点的坐标来指定每个三角形。这需要九个数字——每个顶点三个数字——用于三角形的三个顶点。由于一个顶点可以被几个三角形共享,所以存在一些冗余。对于金字塔,一个顶点的坐标将被重复三到四次。

three.js的网格对象需要一个几何体和一个材质。几何体是一个类型为THREE.BufferedGeometry的对象,它有一个“position”属性,该属性保存了网格中使用的顶点的坐标。该属性使用一个类型化数组来保存构成网格的三角形的顶点坐标。金字塔的几何体可以这样创建:

let pyramidVertices = new Float32Array( [
            // 金字塔Geom "position"属性的数据。
            // 包含顶点的x,y,z坐标。
            // 每组三个数字是一个顶点;
            // 每组三个顶点是一个面。
            -1,0,1,  -1,0,-1,  1,0,-1, // 底部的第一个三角形。
            -1,0,1,   1,0,-1,  1,0,1,  // 底部的第二个三角形。
            -1,0,1,   1,0,1,   0,1,0,  // 前面。
            1,0,1,   1,0,-1,  0,1,0,  // 右面。
            1,0,-1, -1,0,-1,  0,1,0,  // 后面。
            -1,0,-1, -1,0,1,   0,1,0   // 左面。
        ] );
let pyramidGeom = new THREE.BufferGeometry();
pyramidGeom.setAttribute("position",
                new THREE.BufferAttribute(pyramidVertices,3) );

当这个几何体与Lambert或Phong材质一起使用时,需要为顶点提供法向量。如果几何体没有法向量,Lambert和Phong材质将显示为黑色。网格的法向量必须存储在BufferedGeometry的另一个属性中。属性的名称是"normal",它为"position"属性中的每个顶点保存一个法向量。它可以以与创建"position"属性相同的方式创建,但BufferedGeometry对象包括一个计算法向量的方法。对于pyramidGeom,我们可以简单地调用

pyramidGeom.computeVertexNormals();

对于一个基本的多边形网格,这将创建垂直于面的法向量。当几个面共享一个顶点时,该顶点将为每个面有一个不同的法向量。这将产生平面看起来的面,这对于多面体是适当的,其侧面实际上是平的。如果多边形网格被用来近似一个平滑的表面,这是不适当的。在这种情况下,我们应该使用垂直于表面的法向量,这意味着通过手工创建"normal"属性。(见4.1.3小节。)

一旦我们有了金字塔的几何体,我们可以通过将它与一个黄色的Lambert材质结合来在three.js网格对象中使用它:

pyramid = new THREE.Mesh( 
            pyramidGeom,
            new THREE.MeshLambertMaterial({ color: "yellow" }) 
        );

但是,如果只有一种颜色,金字塔看起来会有点无聊。可以将不同的材质应用到网格的不同面上。为了实现这一点,几何体中的顶点必须被分成组。BufferedGeometry类的addGroup()方法用于创建这些组。几何体中的顶点根据它们在"position"属性中的顺序被编号为0, 1, 2, ...(这与上面使用的编号不同)。addGroup()方法接受三个参数:组中第一个顶点的编号、组中的顶点数和一个材质索引。材质索引是一个整数,它决定了哪个材质将被应用到该组。如果您正在使用组,重要的是将所有顶点都放入组中。以下是如何为金字塔创建组:

pyramidGeom.addGroup(0,6,0);  // 底部(2个三角形)
pyramidGeom.addGroup(6,3,1);  // 前面。
pyramidGeom.addGroup(9,3,2);  // 右面。
pyramidGeom.addGroup(12,3,3); // 后面。
pyramidGeom.addGroup(15,3,4); // 左面。

要将不同的材质应用到不同的组,应该将材质放入一个数组中。一个组的材质索引是该数组中的索引。

pyramidMaterialArray= [ 
        // 材质数组,用于金字塔的材质。
        new THREE.MeshLambertMaterial( { color: 0xffffff } ),
        new THREE.MeshLambertMaterial( { color: 0x99ffff } ),
        new THREE.MeshLambertMaterial( { color: 0xff99ff } ),
        new THREE.MeshLambertMaterial( { color: 0xffff99 } ),
        new THREE.MeshLambertMaterial( { color: 0xff9999 } )
    ];

这个数组可以作为第二个参数传递给THREE.Mesh构造函数,通常那里会使用单一材质。

pyramid = new THREE.Mesh( pyramidGeom, pyramidMaterialArray );

(但请注意,即使网格几何体使用组,您仍然可以在网格上使用单一材质。)

THREE.BoxGeometry带有组,使得可以为每个面分配不同的材质。示例程序threejs/vertex-groups.html使用本节中的代码创建金字塔,并显示金字塔和立方体,每个对象上使用多种材质。它们看起来像这样:

123

还有另一种方法可以将不同的颜色分配给不同的顶点。BufferedGeometry可以有一个名为"color"的属性,它为每个顶点指定一种颜色。"color"属性使用一个数组,其中包含每个顶点的一组三个RGB分量值。顶点颜色默认被忽略。要使用它们,几何体必须与一个将vertexColors属性设置为true的材质结合使用。以下是如何使用顶点颜色来给金字塔的侧面上色:

pyramidGeom.setAttribute(
        "color",
        new THREE.BufferAttribute( new Float32Array([
                1,1,1,  1,1,1,  1,1,1, // 底部顶点是白色
                1,1,1,  1,1,1,  1,1,1,
                1,0,0,  1,0,0,  1,0,0, // 前面顶点是红色,
                0,1,0,  0,1,0,  0,1,0, // 右面顶点是绿色,
                0,0,1,  0,0,1,  0,0,1, // 后面顶点是蓝色,
                1,1,0,  1,1,0,  1,1,0  // 左面顶点是黄色。
            ]), 3)
    );
pyramid = new THREE.Mesh( 
        pyramidGeom, 
        new THREE.MeshLambertMaterial({
            color: "white",
            vertexColors: true
        }) 
    );

来自几何体的顶点颜色的颜色分量实际上是与Lambert材质中的颜色分量相乘的。将该颜色设置为白色,颜色分量等于一,这是有意义的;在这种情况下,顶点颜色不会被材质颜色修改。

在这个例子中,金字塔的每个面都是一个纯色。金字塔的颜色数组有很多冗余,因为必须为每个顶点指定颜色,即使给定面的顶点颜色都相同。实际上,并不要求一个面的顶点都有相同的颜色。如果它们被分配了不同的颜色,颜色将从顶点插值到面的内部。例如,在下面的演示中,为一个球体的二十面体近似的每个顶点指定了随机的顶点颜色:

该演示可以运行两个有点傻的动画;顶点颜色和顶点位置可以被动画化。


glDrawElements()函数用于避免基本多边形网格表示中的冗余。它使用索引面集(IFS)模式,这需要一个面索引数组来指定网格面的顶点。在该数组中,一个顶点是通过一个数字来指定的,而不是重复所有坐标和其他数据。注意,一个给定的顶点编号指的是该顶点的所有数据:顶点坐标、法向量、顶点颜色以及几何体的属性中提供的任何其他数据。假设两个面共享一个顶点。如果该顶点在两个面中有不同的法向量,或者某个其他属性有不同的值,那么这个向量将需要在属性数组中出现两次。只有当顶点在两个面中具有相同的属性时,这两个出现才能合并。IFS表示最适合用作平滑表面的近似的多边形网格,因为在这种情况下,顶点对所有它出现的顶点都有相同的法向量。它也适用于使用MeshBasicMaterial的对象,因为这种类型的材质不使用法向量。

要在使用BufferedGeometry时使用IFS模式,您需要为几何体提供一个面索引数组。该数组由几何体的setIndex()方法指定。参数可以是一个普通的JavaScript整数数组。对于我们的金字塔示例,几何体的“position”属性将只包含每个顶点一次,面索引数组将通过其在该顶点列表中的位置来引用一个顶点:

pyramidVertices = new Float32Array( [
            1, 0,  1,  // 顶点编号0
            1, 0, -1,  // 顶点编号1
            -1, 0, -1,  // 顶点编号2
            -1, 0,  1,  // 顶点编号3
            0, 1,  0   // 顶点编号4
] );

pyramidFaceIndexArray = [
            3, 2, 1,  // 底部的第一个三角形。
            3, 1, 0,  // 底部的第二个三角形。
            3, 0, 4,  // 前面。
            0, 1, 4,  // 右面。
            1, 2, 4,  // 后面。
            2, 3, 4   // 左面。
];

pyramidGeom = new THREE.BufferGeometry();
pyramidGeom.setAttribute("position",
                new THREE.BufferAttribute(pyramidVertices,3) );
pyramidGeom.setIndex( pyramidFaceIndexArray );

这将与MeshBasicMaterial一起工作。示例程序threejs/vertex-groups-indexed.htmlthreejs/vertex-groups.html的一个变体,它使用了这种方法。

computeVertexNormals()方法仍然可以用于具有索引数组的BufferedGeometry。要计算一个顶点的法向量,它找到该顶点出现的所有的面。对于这些面中的每一个,它计算一个垂直于面的向量。然后它平均这些向量以得到顶点法向量。(我会指出,如果你对我们的金字塔尝试这个,它看起来会非常糟糕。它真的只适用于平滑表面。)

A mesh in three.js is what we called a polygonal mesh in Section 3.4, although in a three.js mesh, all of the polygons must be triangles. There are two ways to draw polygonal meshes in WebGL. One uses the function glDrawArrays(), which requires just a list of vertices. The other uses the representation that we called an indexed face set (IFS), which is drawn using the function glDrawElements(). In addition to a list of vertices, an IFS uses a list of face indices to specify the triangles. We will look at both methods, using this pyramid as an example:

123

Note that the bottom face of the pyramid, which is a square, has to be divided into two triangles in order for the pyramid to be represented as a mesh geometry. The vertices are numbered from 0 to 4. A triangular face can be specified by the three numbers that give the vertex numbers of the vertices of that triangle. As usual, the vertices of a triangle should be specified in counterclockwise order when viewed from the front, that is, from outside the pyramid. Here is the data that we need.

    VERTEX COORDINATES:         FACE INDICES:

    Vertex 0:   1, 0,  1        Face 1:  3, 2, 1 
    Vertex 1:   1, 0, -1        Face 2:  3, 1, 0 
    Vertex 2:  -1, 0, -1        Face 3:  3, 0, 4 
    Vertex 3:  -1, 0,  1        Face 4:  0, 1, 4 
    Vertex 4:   0, 1,  0        Face 5:  1, 2, 4 
                                Face 6:  2, 3, 4

A basic polygonal mesh representation does not use face indices. Instead, it specifies each triangle by listing the coordinates of the vertices. This requires nine numbers—three numbers per vertex—for the three vertices of the triangle. Since a vertex can be shared by several triangles, there is some redundancy. For the pyramid, the coordinates for a vertex will be repeated three or four times.

A three.js mesh object requires a geometry and a material. The geometry is an object of type THREE.BufferedGeometry, which has a "position" attribute that holds the coordinates of the vertices that are used in the mesh. The attribute uses a typed array that holds the coordinates of the vertices of the triangles that make up the mesh. Geometry for the pyramid can be created like this:

let pyramidVertices = new Float32Array( [
            // Data for the pyramidGeom "position" attribute.
            // Contains the x,y,z coordinates for the vertices.
            // Each group of three numbers is a vertex;
            // each group of three vertices is one face.
            -1,0,1,  -1,0,-1,  1,0,-1, // First triangle in the base.
            -1,0,1,   1,0,-1,  1,0,1,  // Second triangle in the base.
            -1,0,1,   1,0,1,   0,1,0,  // Front face.
            1,0,1,   1,0,-1,  0,1,0,  // Right face.
            1,0,-1, -1,0,-1,  0,1,0,  // Back face.
            -1,0,-1, -1,0,1,   0,1,0   // Left face.
        ] );
let pyramidGeom = new THREE.BufferGeometry();
pyramidGeom.setAttribute("position",
                new THREE.BufferAttribute(pyramidVertices,3) );

When this geometry is used with a Lambert or Phong material, normal vectors are required for the vertices. If the geometry has no normal vectors, Lambert and Phong materials will appear black. The normal vectors for a mesh have to be stored in another attribute of the BufferedGeometry. The name of the attribute is "normal", and it holds a normal vector for each vertex in the "position" attribute. It could be created in the same way that the "position" attribute is created, but a BufferedGeometry object includes a method for calculating normal vectors. For the pyramidGeom, we can simply call

pyramidGeom.computeVertexNormals();

For a basic polygonal mesh, this will create normal vectors that are perpendicular to the faces. When several faces share a vertex, that vertex will have a different normal vector for each face. This will produce flat-looking faces, which are appropriate for a polyhedron, whose sides are in fact flat. It is not appropriate if the polygonal mesh is being used to approximate a smooth surface. In that case, we should be using normal vectors that are perpendicular to the surface, which would mean creating the "normal" attribute by hand. (See Subsection 4.1.3.)

Once we have the geometry for our pyramid, we can use it in a three.js mesh object by combining it with, say, a yellow Lambert material:

pyramid = new THREE.Mesh( 
            pyramidGeom,
            new THREE.MeshLambertMaterial({ color: "yellow" }) 
        );

But the pyramid would look a little boring with just one color. It is possible to use different materials on different faces of a mesh. For that to work, the vertices in the geometry must be divided into groups. The addGroup() method in the BufferedGeometry class is used to create the groups. The vertices in the geometry are numbered 0, 1, 2, ..., according their sequence in the "position" attribute. (This is not the same numbering used above.) The addGroup() method takes three parameters: the number of the first vertex in the group, the number of vertices in the group, and a material index. The material index is an integer that determines which material will be applied to the group. If you are using groups, it is important to put all of the vertices into groups. Here is how groups can be created for the pyramid:

pyramidGeom.addGroup(0,6,0);  // The base (2 triangles)
pyramidGeom.addGroup(6,3,1);  // Front face.
pyramidGeom.addGroup(9,3,2);  // Right face.
pyramidGeom.addGroup(12,3,3); // Back face.
pyramidGeom.addGroup(15,3,4); // Left face.

To apply different materials to different groups, the materials should be put into an array. The material index of a group is an index into that array.

pyramidMaterialArray= [ 
        // Array of materials, for use as pyramids's material.
        new THREE.MeshLambertMaterial( { color: 0xffffff } ),
        new THREE.MeshLambertMaterial( { color: 0x99ffff } ),
        new THREE.MeshLambertMaterial( { color: 0xff99ff } ),
        new THREE.MeshLambertMaterial( { color: 0xffff99 } ),
        new THREE.MeshLambertMaterial( { color: 0xff9999 } )
    ];

This array can be passed as the second parameter to the THREE.Mesh constructor, where a single material would ordinarily be used.

pyramid = new THREE.Mesh( pyramidGeom, pyramidMaterialArray );

(But note that you can still use a single material on a mesh, even if the mesh geometry uses groups.)

A THREE.BoxGeometry comes with groups that make it possible to assign a different material to each face. The sample program threejs/vertex-groups.html uses the code from this section to create a pyramid, and it displays both the pyramid and a cube, using multiple materials on each object. Here's what they look like:

123

There is another way to assign different colors to different vertices. A BufferedGeometry can have an attribute named "color" that specifies a color for each vertex. The "color" attribute uses an array containing a set of three RGB component values for each vertex. The vertex colors are ignored by default. To use them, the geometry must be combined with a material in which the vertexColors property is set to true. Here is how vertex colors could be used to color the sides of the pyramid:

pyramidGeom.setAttribute(
        "color",
        new THREE.BufferAttribute( new Float32Array([
                1,1,1,  1,1,1,  1,1,1, // Base vertices are white
                1,1,1,  1,1,1,  1,1,1,
                1,0,0,  1,0,0,  1,0,0, // Front face vertices are red,
                0,1,0,  0,1,0,  0,1,0, // Right face vertices are green,
                0,0,1,  0,0,1,  0,0,1, // Back face vertices are blue,
                1,1,0,  1,1,0,  1,1,0  // Left face vertices are yellow.
            ]), 3)
    );
pyramid = new THREE.Mesh( 
        pyramidGeom, 
        new THREE.MeshLambertMaterial({
            color: "white",
            vertexColors: true
        }) 
    );

The color components of the vertex colors from the geometry are actually multiplied by the color components of the color in the Lambert material. It makes sense for that color to be white, with color components equal to one; in that case the vertex colors are not modified by the material color.

In this example, each face of the pyramid is a solid color. There is a lot of redundancy in the color array for the pyramid, because a color must be specified for every vertex, even if all of the vertex colors for a given face are the same. In fact, it's not required that all of the vertices of a face have the same color. If they are assigned different colors, colors will be interpolated from the vertices to the interior of the face. As an example, in the following demo, a random vertex color was specified for each vertex of an icosahedral approximation for a sphere:

The demo can run two somewhat silly animations; the vertex colors and the vertex positions can be animated.


The glDrawElements() function is used to avoid the redundancy of the basic polygonal mesh representation. It uses the indexed face set pattern, which requires an array of face indices to specify the vertices for the faces of the mesh. In that array, a vertex is specified by a single number, rather than repeating all of the coordinates and other data for that vertex. Note that a given vertex number refers to all of the data for that vertex: vertex coordinates, normal vector, vertex color, and any other data that are provided in attributes of the geometry. Suppose that two faces share a vertex. If that vertex has a different normal vector, or a different value for some other attribute, in the two faces, then that vector will need to occur twice in the attribute arrays. The two occurrences can be combined only if the vertex has identical properties in the two faces. The IFS representation is most suitable for a polygonal mesh that is being used as an approximation for a smooth surface, since in that case a vertex has the same normal vector for all of the vertices in which it occurs. It can also be appropriate for an object that uses a MeshBasicMaterial, since normal vectors are not used with that type of material.

To use the IFS pattern with a BufferedGeometry, you need to provide a face index array for the geometry. The array is specified by the geometry's setIndex() method. The parameter can be an ordinary JavaScript array of integers. For our pyramid example the "position" attribute of the geometry would contain each vertex just once, and the face index array would refer to a vertex by its position in that list of vertices:

pyramidVertices = new Float32Array( [
            1, 0,  1,  // vertex number 0
            1, 0, -1,  // vertex number 1
            -1, 0, -1,  // vertex number 2
            -1, 0,  1,  // vertex number 3
            0, 1,  0   // vertex number 4
] );

pyramidFaceIndexArray = [
            3, 2, 1,  // First triangle in the base.
            3, 1, 0,  // Second Triangle in the base.
            3, 0, 4,  // Front face.
            0, 1, 4,  // Right face.
            1, 2, 4,  // Back face.
            2, 3, 4   // Left face.
];

pyramidGeom = new THREE.BufferGeometry();
pyramidGeom.setAttribute("position",
                new THREE.BufferAttribute(pyramidVertices,3) );
pyramidGeom.setIndex( pyramidFaceIndexArray );

This would work with a MeshBasicMaterial. The sample program threejs/vertex-groups-indexed.html is a variation on threejs/vertex-groups.html that uses this approach.

The computeVertexNormals() method can still be used for a BufferedGeometry that has an index array. To compute a normal vector for a vertex, it finds all of the faces in which that vertex occurs. For each of those faces, it computes a vector perpendicular to the face. Then it averages those vectors to get the vertex normal. (I will note if you tried this for our pyramid, it would look pretty bad. It's really only appropriate for smooth surfaces.)

5.2.2 曲线和曲面

Curves and Surfaces

除了允许你构建索引面集,three.js还支持使用数学定义的曲线和曲面。一些可能性在示例程序threejs/curves-and-surfaces.html中进行了说明,我将在这里讨论其中的一些。

参数化曲面是最容易处理的。它们由一个名为ParametricGeometrythree.js插件表示。作为一个插件,它必须从主要的three.js模块中单独导入。在我的示例程序中,它是这样导入的:

import {ParametricGeometry} from "addons/geometries/ParametricGeometry.js";

参数化曲面由一个数学函数定义,该函数有两个参数(u,v),其中u和v是数字,函数的每个值都是空间中的一个点。曲面由函数在某些指定范围内对u和v的值构成的所有点组成。对于three.js,该函数是一个常规的JavaScript函数,它接受三个参数:u, v和一个类型为THREE.Vector3的对象。该函数必须修改向量以表示对应于uv参数值的空间中的点。通过在(u,v)点的网格上调用函数来创建参数化曲面几何体。这提供了曲面上的一组点,然后这些点被连接起来,以给出曲面的多边形近似。在three.js中,u和v的值总是在0.0到1.0的范围内。几何体是通过构造函数创建的:

new ParametricGeometry( func, slices, stacks )

其中func是定义曲面的JavaScript函数,slicesstacks确定网格中的点数;slices给出了从0到1的区间在u方向上的细分数量,而stacksv方向上。一旦有了几何体,就可以像通常一样使用它来制作网格。这是一个示例,来自示例程序:

123

这个曲面由函数定义:

function surfaceFunction( u, v, vector ) {
    let x,y,z;  // 曲面上的点的坐标,
                // 根据u,v计算,其中u和v的范围从0.0到1.0。
    x = 20 * (u - 0.5);  // x和z的范围从-10到10
    z = 20 * (v - 0.5);
    y = 2*(Math.sin(x/2) * Math.cos(z));
    vector.set( x, y, z );
}

代表曲面的three.js网格是使用创建的:

let surfaceGeometry = new THREE.ParametricGeometry(surfaceFunction, 64, 64);
let surface = new THREE.Mesh( surfaceGeometry, material );

曲线在three.js中更复杂。THREE.Curve类表示二维或三维参数化曲线的抽象概念。(它不表示three.js几何体。)参数化曲线由一个数值变量t的函数定义。函数返回的值类型为THREE.Vector2对于2D曲线或THREE.Vector3对于3D曲线。对于类型为THREE.Curve的对象,曲线,方法curve.getPoint(t)应该返回对应于参数t的曲线上的点。曲线由这个函数生成的点组成,对于t的值范围从0.0到1.0。然而,在Curve类本身中,getPoint()是未定义的。要得到一个实际的曲线,你必须定义它。例如,

let helix = new THREE.Curve();
helix.getPoint = function(t) {
let s = (t - 0.5) * 12*Math.PI;
        // 当t从0到1变化时,s的范围从-6*PI到6*PI
return new THREE.Vector3(
        5*Math.cos(s),
        s,
        5*Math.sin(s)
);
}

一旦getPoint被定义,你就有一个可用的曲线。你可以用它做的一件事是创建一个管状几何体,它定义了一个以圆截面和曲线沿管中心运行的管状表面。示例程序使用上面定义的helix曲线来创建两个管子:

adf

较宽管子的几何体是这样创建的:

tubeGeometry1 = new THREE.TubeGeometry( helix, 128, 2.5, 32 );

构造函数的第二个参数是沿曲线长度的曲面细分数量。第三个是管的圆截面半径,第四个是截面周长上的细分数量。

要制作一个管子,你需要一个3D曲线。也有几种方法可以从2D曲线创建一个曲面。一种方法是围绕一条线旋转曲线,生成一个旋转曲面。曲面由曲线在旋转时经过的所有点组成。这称为车削。这张来自示例程序的图像显示了通过车削余弦曲线生成的曲面。(图像旋转了90度,以便y轴是水平的。)曲线本身显示在曲面上方:

123

three.js中,使用THREE.LatheGeometry对象创建曲面。一个LatheGeometry不是从一个曲线而是从一个位于曲线上的点的数组构建的。这些点是Vector2类型的,曲线位于xy平面内。曲面是通过围绕y轴旋转曲线生成的。LatheGeometry构造函数的形式为:

new THREE.LatheGeometry( points, slices )

第一个参数是Vector2数组。第二个是沿生成的圆圈的曲面细分数量。(曲面的“堆栈”数量由点数组的长度给出。)在示例程序中,我通过调用cosine.getPoints(128),从类型为Curve的对象cosine创建点的数组。这个函数创建了曲线上的128个点,使用的参数值范围从0.0到1.0。

你可以用2D曲线做的另一件事就是简单地填充曲线的内部,给出一个2D填充形状。在three.js中,要这样做,你可以使用类型为THREE.Shape的对象,它是THREE.Curve的一个子类。一个Shape可以像在第2.6节中介绍的2D Canvas API中的路径一样定义。也就是说,一个类型为THREE.Shape的对象shape具有方法shape.moveTo, shape.lineTo, shape.quadraticCurveToshape.bezierCurveTo,可以用来定义路径。有关这些函数如何工作的详细信息,请参阅2.6.2小节。例如,我们可以创建一个泪滴形状:

let path = new THREE.Shape();
path.moveTo(0,10);
path.bezierCurveTo( 0,5, 20,-10, 0,-10 );
path.bezierCurveTo( -20,-10, 0,5, 0,10 );

要使用路径在three.js中创建一个填充形状,我们需要一个ShapeGeometry对象:

let shapeGeom = new THREE.ShapeGeometry( path );

使用此几何体创建的2D形状显示在这张图片的左侧:

123

图片中的其他两个对象是通过拉伸形状创建的。在拉伸中,一个填充的2D形状沿着3D中的路径移动。形状经过的点组成了一个3D实体。在这种情况下,形状沿着垂直于形状的线段拉伸,这是最常见的情况。基本拉伸形状在插图的右侧显示。中间的对象是具有“斜面”边缘的相同形状。有关拉伸的更多详细信息,请参阅THREE.ExtrudeGeometry的文档和示例程序的源代码。

In addition to letting you build indexed face sets, three.js has support for working with curves and surfaces that are defined mathematically. Some of the possibilities are illustrated in the sample program threejs/curves-and-surfaces.html, and I will discuss a few of them here.

Parametric surfaces are the easiest to work with. They are represented by a three.js add-on named ParametricGeometry. As an add-on, it must be imported separately from the main three.js module. In my sample program, it is imported with

import {ParametricGeometry} from "addons/geometries/ParametricGeometry.js";

A parametric surface is defined by a mathematical function of two parameters (u,v), where u and v are numbers, and each value of the function is a point in space. The surface consists of all the points that are values of the function for u and v in some specified ranges. For three.js, the function is a regular JavaScript function that takes three parameters: u, v, and an object of type THREE.Vector3. The function must modify the vector to represent the point in space that corresponds to the values of the u and v parameters. A parametric surface geometry is created by calling the function at a grid of (u,v) points. This gives a collection of points on the surface, which are then connected to give a polygonal approximation of the surface. In three.js, the values of both u and v are always in the range 0.0 to 1.0. The geometry is created by a constructor

new ParametricGeometry( func, slices, stacks )

where func is the JavaScript function that defines the surface, and slices and stacks determine the number of points in the grid; slices gives the number of subdivisions of the interval from 0 to 1 in the u direction, and stacks, in the v direction. Once you have the geometry, you can use it to make a mesh in the usual way. Here is an example, from the sample program:

123

This surface is defined by the function

function surfaceFunction( u, v, vector ) {
    let x,y,z;  // Coordinates for a point on the surface, 
                // calculated from u,v, where u and v
                // range from 0.0 to 1.0.
    x = 20 * (u - 0.5);  // x and z range from -10 to 10
    z = 20 * (v - 0.5);
    y = 2*(Math.sin(x/2) * Math.cos(z));
    vector.set( x, y, z );
}

and the three.js mesh that represents the surface is created using

let surfaceGeometry = new THREE.ParametricGeometry(surfaceFunction, 64, 64);
let surface = new THREE.Mesh( surfaceGeometry, material );

Curves are more complicated in three.js. The class THREE.Curve represents the abstract idea of a parametric curve in two or three dimensions. (It does not represent a three.js geometry.) A parametric curve is defined by a function of one numeric variable t. The value returned by the function is of type THREE.Vector2 for a 2D curve or THREE.Vector3 for a 3D curve. For an object, curve, of type THREE.Curve, the method curve.getPoint(t) should return the point on the curve corresponding to the value of the parameter t. The curve consists of points generated by this function for values of t ranging from 0.0 to 1.0. However, in the Curve class itself, getPoint() is undefined. To get an actual curve, you have to define it. For example,

let helix = new THREE.Curve();
helix.getPoint = function(t) {
let s = (t - 0.5) * 12*Math.PI;
        // As t ranges from 0 to 1, s ranges from -6*PI to 6*PI
return new THREE.Vector3(
        5*Math.cos(s),
        s,
        5*Math.sin(s)
);
}

Once getPoint is defined, you have a usable curve. One thing that you can do with it is create a tube geometry, which defines a surface that is a tube with a circular cross-section and with the curve running along the center of the tube. The sample program uses the helix curve, defined above, to create two tubes:

adf

The geometry for the wider tube is created with

tubeGeometry1 = new THREE.TubeGeometry( helix, 128, 2.5, 32 );

The second parameter to the constructor is the number of subdivisions of the surface along the length of the curve. The third is the radius of the circular cross-section of the tube, and the fourth is the number of subdivisions around the circumference of the cross-section.

To make a tube, you need a 3D curve. There are also several ways to make a surface from a 2D curve. One way is to rotate the curve about a line, generating a surface of rotation. The surface consists of all the points that the curve passes through as it rotates. This is called lathing. This image from the sample program shows the surface generated by lathing a cosine curve. (The image is rotated 90 degrees, so that the y-axis is horizontal.) The curve itself is shown above the surface:

123

The surface is created in three.js using a THREE.LatheGeometry object. A LatheGeometry is constructed not from a curve but from an array of points that lie on the curve. The points are objects of type Vector2, and the curve lies in the xy-plane. The surface is generated by rotating the curve about the y-axis. The LatheGeometry constructor takes the form

new THREE.LatheGeometry( points, slices )

The first parameter is the array of Vector2. The second is the number of subdivisions of the surface along the circle generated when a point is rotated about the axis. (The number of "stacks" for the surface is given by the length of the points array.) In the sample program, I create the array of points from an object, cosine, of type Curve by calling cosine.getPoints(128). This function creates an array of 128 points on the curve, using values of the parameter that range from 0.0 to 1.0.

Another thing that you can do with a 2D curve is simply to fill in the inside of the curve, giving a 2D filled shape. To do that in three.js, you can use an object of type THREE.Shape, which is a subclass of THREE.Curve. A Shape can be defined in the same way as a path in the 2D Canvas API that was covered in Section 2.6. That is, an object shape of type THREE.Shape has methods shape.moveTo, shape.lineTo, shape.quadraticCurveTo and shape.bezierCurveTo that can be used to define the path. See Subsection 2.6.2 for details of how these functions work. As an example, we can create a teardrop shape:

let path = new THREE.Shape();
path.moveTo(0,10);
path.bezierCurveTo( 0,5, 20,-10, 0,-10 );
path.bezierCurveTo( -20,-10, 0,5, 0,10 );

To use the path to create a filled shape in three.js, we need a ShapeGeometry object:

let shapeGeom = new THREE.ShapeGeometry( path );

The 2D shape created with this geometry is shown on the left in this picture:

123

The other two objects in the picture were created by extruding the shape. In extrusion, a filled 2D shape is moved along a path in 3D. The points that the shape passes through make up a 3D solid. In this case, the shape was extruded along a line segment perpendicular to the shape, which is the most common case. The basic extruded shape is shown on the right in the illustration. The middle object is the same shape with "beveled" edges. For more details on extrusion, see the documentation for THREE.ExtrudeGeometry and the source code for the sample program.

5.2.3 纹理

Textures

纹理可以用来为对象添加视觉兴趣和细节。在three.js中,图像纹理由类型为THREE.Texture的对象表示。由于我们谈论的是网页,three.js的图像通常从网络地址加载。图像纹理通常使用类型为THREE.TextureLoader的对象中的load函数创建。该函数接受一个URL(一个网络地址,通常是相对地址)作为参数,并返回一个Texture对象:

let loader = new THREE.TextureLoader();
let texture = loader.load( imageURL );

(建议也设置

tex.colorSpace = THREE.SRGBColorSpace;

以正确显示颜色。three.js文档表示,“包含颜色信息的PNG或JPEG纹理(如.map或.emissiveMap)使用封闭的sRGB颜色空间,并且必须用texture.colorSpace = SRGBColorSpace进行注释。")

three.js中,纹理被视为材质的一部分。要将纹理应用于网格,只需将Texture对象分配给用于网格的网格材质上的map属性:

material.map = texture;

map属性也可以在材质构造函数中设置。所有三种类型的网格材质(Basic、Lambert和Phong)都可以使用纹理。通常,材质基础颜色将是白色,因为材质颜色将与纹理中的颜色相乘。非白色材质颜色将为纹理颜色添加“色彩”。将图像映射到网格所需的纹理坐标是网格几何体的一部分。标准网格几何体,如THREE.SphereGeometry,已经定义了纹理坐标。

这就是基本思想——从图像URL创建纹理对象并将其分配给材质的map属性。然而,也有复杂性。首先,图像加载是“异步的”。也就是说,调用load函数只启动了图像加载的过程,该过程可能在函数返回后的某个时候完成。在图像加载完成之前在对象上使用纹理不会导致错误,但对象将被渲染为完全黑色。一旦图像加载完成,必须重新渲染场景以显示图像纹理。如果动画正在运行,这将自动发生;图像将在加载完成后的第一帧中出现。但是如果没有动画,你需要一种在图像加载完成后渲染场景的方法。实际上,TextureLoader中的load函数有几个可选参数:

loader.load( imageURL, onLoad, undefined, onError );

这里的第三个参数给出为undefined,因为那个参数不再使用。onLoadonError参数是回调函数。如果定义了onLoad函数,它将在图像成功加载后被调用。如果加载图像的尝试失败,将调用onError函数。例如,如果有一个函数render()渲染场景,那么render本身就可以作为onLoad函数使用:

texture = new THREE.TextureLoader().load( "brick.png", render );

onLoad的另一个可能用途是在图像加载完成后延迟将纹理分配给材质。如果你稍后添加纹理,请确保设置

material.needsUpdate = true;

以确保更改在对象重新绘制时生效。(needsUpdate需要在各种对象上设置的确切时间并不总是清楚的。请参阅three.js文档中的更新资源部分。)

Texture有几个可以设置的属性,包括设置纹理的最小化和放大滤镜属性以及控制mipmap生成的属性,mipmap默认会自动生成。你最有可能想要更改的属性是纹理坐标超出范围0到1的包装模式和纹理转换。(有关这些属性的更多信息,请参阅第4.3节。)

对于Texture对象tex,属性tex.wrapStex.wrapT控制s和t纹理坐标超出范围0到1时的处理方式。默认值是“clamp to edge”。你最有可能想要通过将属性值设置为THREE.RepeatWrapping来使纹理在两个方向上重复:

tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;

RepeatWrapping最适合使用“无缝”纹理,其中图像的上边缘与下边缘对齐,左边缘与右边缘对齐。Three.js还提供了一个有趣的变体,称为“镜像重复”,其中重复图像的每个其他副本都被翻转。这消除了图像副本之间的接缝。要进行镜像重复,请使用属性值THREE.MirroredRepeatWrapping

tex.wrapS = THREE.MirroredRepeatWrapping;
tex.wrapT = THREE.MirroredRepeatWrapping;

纹理属性repeatoffsetrotation控制应用于纹理的缩放、平移和旋转作为纹理转换。repeat和offset的值是类型为THREE.Vector2的,因此每个属性都有一个x和y组件。rotation是一个数字,以弧度为单位,给出纹理关于点(0,0)的旋转。(但实际上,旋转中心由另一个名为center的属性给出。)对于Texturetextex.offset的两个组件分别给出水平和垂直方向上的纹理平移。要在水平方向上将纹理偏移0.5,你可以这样说:

tex.offset.x = 0.5;

或者

tex.offset.set( 0.5, 0 );

记住,水平偏移的正值会将纹理向对象的左侧移动,因为偏移是应用于纹理坐标本身而不是纹理图像。

属性tex.repeat的组件在水平和垂直方向上给出纹理缩放。例如,

tex.repeat.set(2,3);

将在水平方向上将纹理坐标缩放2倍,在垂直方向上缩放3倍。同样,对图像的影响是相反的,所以图像在水平方向上缩小2倍,在垂直方向上缩小3倍。结果是,你在水平方向上得到两个图像副本,而在垂直方向上得到三个副本。这解释了“repeat”的名称,但请注意,这些值不仅限于整数。

这个演示让你可以查看一些带有纹理的three.js对象。顺便说一下,演示中的“Pill”对象是一个由一个圆柱体和两个半球体组成的复合对象。


假设我们想在本节开头创建的金字塔上使用图像纹理。为了将纹理图像应用于对象,WebGL需要该对象的纹理坐标。当我们从头开始构建网格时,我们必须将纹理坐标作为网格的几何对象的一部分提供。

让我们看看在我们的金字塔示例上如何做到这一点。像示例中的pyramidGeom这样的BufferedGeometry对象有一个名为"uv"的属性,用于保存纹理坐标。(名称"uv"指的是映射到纹理中的s和t坐标的对象上的坐标。表面的纹理坐标通常被称为"uv坐标。")"uv"属性的BufferAttribute可以从一个包含每个顶点的一对纹理坐标的类型化数组中创建。

我们的金字塔示例有六个三角形面,总共有18个顶点。我们需要一个包含18个顶点的顶点坐标数组。坐标必须被选择以合理的方式将图像映射到面上。我选择的坐标将整个纹理图像映射到金字塔的正方形基座上,并从图像中切出一个三角形应用到每个侧面上。想出正确的坐标需要一些注意。我为金字塔几何体定义纹理坐标如下:

let pyramidUVs = new Float32Array([
        0,0,  0,1,  1,1,   // 底部第一个三角形的uv坐标。
        0,0,  1,1,  1,0,   // 底部第二个三角形的uv坐标。
        0,0,  1,0,  0.5,1, // 前面的uv坐标。
        1,0,  0,0,  0.5,1, // 右面的uv坐标。
        0,0,  1,0,  0.5,1, // 后面的uv坐标。
        1,0,  0,0,  0.5,1  // 左面的uv坐标。
]);
pyramidGeom.setAttribute("uv",
                        new THREE.BufferAttribute(pyramidUVs,2) );

示例程序threejs/textured-pyramid.html显示了一个带有砖纹理的金字塔。这是程序中的一张图像:

123

A texture can be used to add visual interest and detail to an object. In three.js, an image texture is represented by an object of type THREE.Texture. Since we are talking about web pages, the image for a three.js texture is generally loaded from a web address. Image textures are usually created using the load function in an object of type THREE.TextureLoader. The function takes a URL (a web address, usually a relative address) as parameter and returns a Texture object:

let loader = new THREE.TextureLoader();
let texture = loader.load( imageURL );

(It is also advisable to set

tex.colorSpace = THREE.SRGBColorSpace;

to display the colors correctly. The three.js documentation says, "PNG or JPEG Textures containing color information (like .map or .emissiveMap) use the closed domain sRGB color space, and must be annotated with texture.colorSpace = SRGBColorSpace.")

A texture in three.js is considered to be part of a material. To apply a texture to a mesh, just assign the Texture object to the map property of the mesh material that is used on the mesh:

material.map = texture;

The map property can also be set in the material constructor. All three types of mesh material (Basic, Lambert, and Phong) can use a texture. In general, the material base color will be white, since the material color will be multiplied by colors from the texture. A non-white material color will add a "tint" to the texture colors. The texture coordinates that are needed to map the image to a mesh are part of the mesh geometry. The standard mesh geometries such as THREE.SphereGeometry come with texture coordinates already defined.

That's the basic idea—create a texture object from an image URL and assign it to the map property of a material. However, there are complications. First of all, image loading is "asynchronous." That is, calling the load function only starts the process of loading the image, and the process can complete sometime after the function returns. Using a texture on an object before the image has finished loading does not cause an error, but the object will be rendered as completely black. Once the image has been loaded, the scene has to be rendered again to show the image texture. If an animation is running, this will happen automatically; the image will appear in the first frame after it has finished loading. But if there is no animation, you need a way to render the scene once the image has loaded. In fact, the load function in a TextureLoader has several optional parameters:

loader.load( imageURL, onLoad, undefined, onError );

The third parameter here is given as undefined because that parameter is no longer used. The onLoad and onError parameters are callback functions. The onLoad function, if defined, will be called once the image has been successfully loaded. The onError function will be called if the attempt to load the image fails. For example, if there is a function render() that renders the scene, then render itself could be used as the onLoad function:

texture = new THREE.TextureLoader().load( "brick.png", render );

Another possible use of onLoad would be to delay assigning the texture to a material until the image has finished loading. If you do add the texture later, be sure to set

material.needsUpdate = true;

to make sure that the change will take effect when the object is redrawn. (When exactly needsUpdate needs to be set on various objects is not always clear. See the "Updating Resources" section of the three.js documentation.)

A Texture has a number of properties that can be set, including properties to set the minification and magnification filters for the texture and a property to control the generation of mipmaps, which is done automatically by default. The properties that you are most likely to want to change are the wrap mode for texture coordinates outside the range 0 to 1 and the texture transformation. (See Section 4.3 for more information about these properties.)

For a Texture object tex, the properties tex.wrapS and tex.wrapT control how s and t texture coordinates outside the range 0 to 1 are treated. The default is "clamp to edge." You will most likely want to make the texture repeat in both directions by setting the property values to THREE.RepeatWrapping:

tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;

RepeatWrapping works best with "seamless" textures, where the top edge of the image matches up with the bottom edge and the left edge with the right. Three.js also offers an interesting variation called "mirrored repeat" in which every other copy of the repeated image is flipped. This eliminates the seam between copies of the image. For mirrored repetition, use the property value THREE.MirroredRepeatWrapping:

tex.wrapS = THREE.MirroredRepeatWrapping;
tex.wrapT = THREE.MirroredRepeatWrapping;

The texture properties repeat, offset, and rotation control the scaling, translation, and rotation that are applied to the texture as texture transformations. The values of repeat and offset are of type THREE.Vector2, so that each property has an x and a y component. The rotation is a number, measured in radians, giving the rotation of the texture about the point (0,0). (But the center of rotation is actually given by another property named center.) For a Texture, tex, the two components of tex.offset give the texture translation in the horizontal and vertical directions. To offset the texture by 0.5 horizontally, you can say either

tex.offset.x = 0.5;

or

tex.offset.set( 0.5, 0 );

Remember that a positive horizontal offset will move the texture to the left on the objects, because the offset is applied to the texture coordinates not to the texture image itself.

The components of the property tex.repeat give the texture scaling in the horizontal and vertical directions. For example,

tex.repeat.set(2,3);

will scale the texture coordinates by a factor of 2 horizontally and 3 vertically. Again, the effect on the image is the inverse, so that the image is shrunk by a factor of 2 horizontally and 3 vertically. The result is that you get two copies of the image in the horizontal direction where you would have had one, and three vertically. This explains the name "repeat," but note that the values are not limited to be integers.

This demo lets you view some textured three.js objects. The "Pill" object in the demo, by the way, is a compound object consisting of a cylinder and two hemispheres.


Suppose that we want to use an image texture on the pyramid that was created at the beginning of this section. In order to apply a texture image to an object, WebGL needs texture coordinates for that object. When we build a mesh from scratch, we have to supply the texture coordinates as part of the mesh's geometry object.

Let's see how to do this on our pyramid example. A BufferedGeometry object such as pyramidGeom in the example has an attribute named "uv" to hold texture coordinates. (The name "uv" refers to the coordinates on an object that are mapped to the s and t coordinates in a texture. The texture coordinates for a surface are often referred to as "uv coordinates.") The BufferAttribute for a "uv" attribute can be made from a typed array containing a pair of texture coordinates for each vertex.

Our pyramid example has six triangular faces, with a total of 18 vertices. We need an array containing vertex coordinates for 18 vertices. The coordinates have to be chosen to map the image in a reasonable way onto the faces. My choice of coordinates maps the entire texture image onto the square base of the pyramid, and it cuts a triangle out of the image to apply to each of the sides. It takes some care to come up with the correct coordinates. I define the texture coordinates for the pyramid geometry as follows:

let pyramidUVs = new Float32Array([
        0,0,  0,1,  1,1,   // uv coords for first triangle in base.
        0,0,  1,1,  1,0,   // uv coords for second triangle in base.
        0,0,  1,0,  0.5,1, // uv coords for front face.
        1,0,  0,0,  0.5,1, // uv coords for right face.
        0,0,  1,0,  0.5,1, // uv coords for back face.
        1,0,  0,0,  0.5,1  // uv coords for left face.
]);
pyramidGeom.setAttribute("uv",
                        new THREE.BufferAttribute(pyramidUVs,2) );

The sample program threejs/textured-pyramid.html shows the pyramid with a brick texture. Here is an image from the program:

123

5.2.4 变换

Transforms

为了理解如何在three.js中有效地使用对象,了解它如何实现变换是很有用的。我已经解释了Object3D,obj,具有属性obj.positionobj.scaleobj.rotation,这些属性指定了它在自己的局部坐标系中的建模变换。但是,这些属性在渲染对象时并没有直接使用。相反,它们被组合起来计算另一个属性,obj.matrix,它表示变换为一个矩阵。默认情况下,这个矩阵在每次渲染场景时都会自动重新计算。如果变换从不改变,这可能是低效的,所以obj有另一个属性,obj.matrixAutoUpdate,它控制是否自动计算obj.matrix。如果将obj.matrixAutoUpdate设置为false,则不会进行更新。在这种情况下,如果您确实想更改建模变换,可以调用obj.updateMatrix()从当前值计算矩阵obj.positionobj.scaleobj.rotation

我们已经看到了如何通过直接更改属性的值来修改obj的建模变换obj.positionobj.scaleobj.rotation。然而,您也可以通过调用函数obj.translateX(dx)obj.translateY(dy)obj.translateZ(dz)来更改位置,以将对象沿坐标轴的方向移动指定的数量。还有一个函数obj.translateOnAxis(axis,amount),其中axis是一个Vector3,amount是一个数字,给出平移对象的距离。对象沿着向量的方向移动,axis。该向量必须被规范化;也就是说,它必须长度为1。例如,要将obj沿着向量(1,1,1)的方向平移5个单位,可以说

obj.translateOnAxis( new THREE.Vector3(1,1,1).normalize(), 5 );

没有用于更改缩放变换的函数。但是,您可以使用函数obj.rotateX(angle)obj.rotateY(angle)obj.rotateZ(angle)来更改对象的旋转,使对象绕坐标轴旋转。(记住,角度以弧度为单位。)调用obj.rotateX(angle)与将angle加到obj.rotation.x的值上不同,因为它在可能已经应用的其他旋转之上应用了关于x轴的旋转。

还有一个函数obj.rotateOnAxis(axis,angle),其中axis是一个Vector3。这个函数将对象绕向量旋转角度angle(即,绕原点和由axis给出的点之间的线)。该轴必须是一个规范化的向量。

(旋转实际上更加复杂。对象obj的旋转实际上是由属性obj.quaternion表示的,而不是由属性obj.rotation表示的。四元数是数学对象,通常在计算机图形学中作为欧拉角的替代品,用于表示旋转。然而,当您更改属性obj.rotationobj.quaternion之一时,另一个会自动更新,以确保两个属性表示相同的旋转。因此,我们不需要直接使用四元数。)

我需要强调的是,平移和旋转函数修改了对象的位置和旋转属性。也就是说,它们以对象坐标应用,而不是世界坐标,并且在渲染对象时作为对象上的第一个建模变换应用。例如,世界坐标中的旋转可以改变对象的位置,如果它没有定位在原点。然而,更改对象的rotation属性的值永远不会改变其位置。

在渲染对象时实际应用到对象的变换是该对象的建模变换与场景图中其所有祖先的建模变换的组合。在three.js中,该变换存储在对象名为obj.matrixWorld的属性中。

还有一个有用的方法来设置旋转:obj.lookAt(vec),它将对象旋转,使其面向给定的点。参数,vec,是一个Vector3,必须以对象自己的局部坐标系表示。(对于没有父对象或其祖先没有建模变换的对象,这将与世界坐标相同。)对象还被旋转,使其“向上”方向等于属性obj.up的值,默认为(0,1,0)。这个函数可以用于任何对象,但对相机最有用。

In order to understand how to work with objects effectively in three.js, it can be useful to know more about how it implements transforms. I have explained that an Object3D, obj, has properties obj.position, obj.scale, and obj.rotation that specify its modeling transformation in its own local coordinate system. But these properties are not used directly when the object is rendered. Instead, they are combined to compute another property, obj.matrix, that represents the transformation as a matrix. By default, this matrix is recomputed automatically every time the scene is rendered. This can be inefficient if the transformation never changes, so obj has another property, obj.matrixAutoUpdate, that controls whether obj.matrix is computed automatically. If you set obj.matrixAutoUpdate to false, the update is not done. In that case, if you do want to change the modeling transformation, you can call obj.updateMatrix() to compute the matrix from the current values of obj.position, obj.scale, and obj.rotation.

We have seen how to modify obj's modeling transformation by directly changing the values of the properties obj.position, obj.scale, and obj.rotation. However, you can also change the position by calling the function obj.translateX(dx), obj.translateY(dy), or obj.translateZ(dz) to move the object by a specified amount in the direction of a coordinate axis. There is also a function obj.translateOnAxis(axis,amount), where axis is a Vector3 and amount is a number giving the distance to translate the object. The object is moved in the direction of the vector, axis. The vector must be normalized; that is, it must have length 1. For example, to translate obj by 5 units in the direction of the vector (1,1,1), you could say

obj.translateOnAxis( new THREE.Vector3(1,1,1).normalize(), 5 );

There are no functions for changing the scaling transform. But you can change the object's rotation with the functions obj.rotateX(angle), obj.rotateY(angle), and obj.rotateZ(angle) to rotate the object about the coordinate axes. (Remember that angles are measured in radians.) Calling obj.rotateX(angle) is not the same as adding angle onto the value of obj.rotation.x, since it applies a rotation about the x-axis on top of other rotations that might already have been applied.

There is also a function obj.rotateOnAxis(axis,angle), where axis is a Vector3. This function rotates the object through the angle angle about the vector (that is, about the line between the origin and the point given by axis). The axis must be a normalized vector.

(Rotation is actually even more complicated. The rotation of an object, obj, is actually represented by the property obj.quaternion, not by the property obj.rotation. Quaternions are mathematical objects that are often used in computer graphics as an alternative to Euler angles, to represent rotations. However, when you change one of the properties obj.rotation or obj.quaternion, the other is automatically updated to make sure that both properties represent the same rotation. So, we don't need to work directly with the quaternions.)

I should emphasize that the translation and rotation functions modify the position and rotation properties of the object. That is, they apply in object coordinates, not world coordinates, and they are applied as the first modeling transformation on the object when the object is rendered. For example, a rotation in world coordinates can change the position of an object, if it is not positioned at the origin. However, changing the value of the rotation property of an object will never change its position.

The actual transformation that is applied to an object when it is rendered is a combination of the modeling transformation of that object, combined with the modeling transformation on all of its ancestors in the scene graph. In three.js, that transformation is stored in a property of the object named obj.matrixWorld.

There is one more useful method for setting the rotation: obj.lookAt(vec), which rotates the object so that it is facing towards a given point. The parameter, vec, is a Vector3, which must be expressed in the object's own local coordinate system. (For an object that has no parent, or whose ancestors have no modeling transformations, that will be the same as world coordinates.) The object is also rotated so that its "up" direction is equal to the value of the property obj.up, which by default is (0,1,0). This function can be used with any object, but it is most useful for a camera.

5.2.5 加载模型

Loading Models

尽管通过列出网格对象的顶点和面来创建它们是可能的,但除了非常简单的对象外,手工制作是非常困难的。例如,在Blender这样的交互式建模程序中设计对象要容易得多(见附录B)。像Blender这样的建模程序可以使用许多不同的文件格式导出对象。Three.js拥有用于从各种文件格式加载模型的实用函数。这些实用程序不是three.js核心的一部分,但定义它们的JavaScript文件可以在three.js下载包的examples文件夹中找到。

模型文件的首选格式是GLTF。GLTF模型可以存储在扩展名为.gltf的文本文件中,也可以存储在扩展名为.glb的二进制文件中。二进制文件更小、更高效,但不是人类可读的。Three.js用于加载GLTF文件的加载器由GLTFLoader类定义,可以从GLTFLoader.js模块中导入,该模块可在three.js下载包中找到。该脚本的副本以及其他模型加载器的脚本可以在教科书的源文件夹的threejs/script/loaders文件夹中找到,或者在three.js下载包的examples/jsm/loaders文件夹中找到。(注意,GLTFLoader不是对象THREE的一部分。)

如果loader是类型为GLTFLoader的对象,您可以使用它的load()方法开始加载模型的过程:

loader = new GLTFLoader()
loader.load( url, onLoad, onProgress, onError );

只有一个参数是必需的;它是一个包含模型的文件的URL。其他三个参数是回调函数:onLoad将在加载完成时被调用,参数表示来自文件的数据;onProgress在加载过程中定期被调用,参数包含有关模型大小以及已加载多少的信息;如果发生任何错误,将调用onError。(我自己实际上没有使用过onProgress。)请注意,与纹理一样,加载是异步完成的。

GLTF文件可能相当复杂,可以包含整个3D场景,包含多个对象、灯光和其他东西。GLTFLoader返回的数据包含一个three.js Scene。文件中定义的任何对象都将是该场景的场景图的一部分。教科书中使用的所有模型文件都定义了一个Mesh对象,是Scene对象的第一个子对象。此对象带有完整的几何体和材质。onLoad回调函数可以将该对象添加到场景中,可能看起来像这样:

function onLoad(data) { // 参数是加载的模型数据
    let object = data.scene.children[0];
    // 也许修改建模变换或材质...
    scene.add(object);  // 将加载的对象添加到我们的场景中
    render();  // 调用render以显示带有新对象的场景
}

示例程序threejs/model-viewer.html使用GLTFLoader加载了几个模型。它还使用Collada和OBJ两种其他格式的模型的加载器,工作方式非常相似。实际上,加载模型的技术比我在这里描述的更通用。有关详细信息,请参见示例程序的源代码。

我还要提到,GLTF模型可以包括动画。Three.js有几个支持动画的类,包括THREE.AnimationMixerTHREE.AnimationActionTHREE.AnimationClip。我不会在这里讨论动画,但是这三个类用于在这个演示中动画化马和鹳的模型:

Although it is possible to create mesh objects by listing their vertices and faces, it would be difficult to do it by hand for all but very simple objects. It's much easier, for example, to design an object in an interactive modeling program such as Blender (Appendix B). Modeling programs like Blender can export objects using many different file formats. Three.js has utility functions for loading models from files in a variety of file formats. These utilities are not part of the three.js core, but JavaScript files that define them can be found in the examples folder in the three.js download.

The preferred format for model files is GLTF. A GLTF model can be stored in a text file with extension .gltf or in a binary file with extension .glb. Binary files are smaller and more efficient, but not human-readable. A three.js loader for GLTF files is defined by the class GLTFLoader, which can be imported from the module GLTFLoader.js. from the three.js download. Copies of that script, as well as scripts for other model loaders, can be found in the threejs/script/loaders folder in the source folder for this textbook, or in the examples/jsm/loaders folder in the three.js download. (Note that GLTFLoader is not part of the object THREE.)

If loader is an object of type GLTFLoader, you can use its load() method to start the process of loading a model:

loader = new GLTFLoader()
loader.load( url, onLoad, onProgress, onError );

Only the first parameter is required; it is a URL for the file that contains the model. The other three parameters are callback functions: onLoad will be called when the loading is complete, with a parameter that represents the data from the file; onProgress is called periodically during the loading with a parameter that contains information about the size of the model and how much of it has be loaded; and onError is called if any error occurs. (I have not actually used onProgress myself.) Note that, as for textures, the loading is done asynchronously.

A GLTF file can be quite complicated and can contain an entire 3D scene, containing multiple objects, lights, and other things. The data returned by a GLTFLoader contains a three.js Scene. Any objects defined by the file will be part of the scene graph for that scene. All of the model files used in this textbook define a Mesh object that is the first child of the Scene object. This object comes complete with both geometry and material. The onLoad callback function can add that object to the scene and might look something like this:

function onLoad(data) { // the parameter is the loaded model data
    let object = data.scene.children[0];
    // maybe modify the modeling transformation or material...
    scene.add(object);  // add the loaded object to our scene
    render();  // call render to show the scene with the new object
}

The sample program threejs/model-viewer.html uses GLTFLoader to load several models. It also uses loaders for models in two other formats, Collada and OBJ, that work much the same way. The technique for loading the models is actually a little more general that what I've described here. See the source code for the example program for details.

I'll also mention that GLTF models can include animations. Three.js has several classes that support animation, including THREE.AnimationMixer, THREE.AnimationAction, and THREE.AnimationClip. I won't discuss animation here, but these three classes are used to animate the horse and stork models in this demo: