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.
A mesh in three.js is what we called an indexed face set in Subsection 3.4.1. In a three.js mesh, all the polygons are triangles. A geometry in three.js is an object of type THREE.Geometry. Any geometry object contains an array of vertices, represented as objects of type THREE.Vector3. For a mesh geometry, it also contains an array of faces, represented as objects of type THREE.Face3. Each object of type Face3 specifies one of the triangular faces of the geometry. The three vertices of the triangle are specified by three integers. Each integer is an index into the geometry's vertex array. The three integers can be specified as parameters to the THREE.Face3 constructor. For example,
var f = new THREE.Face3( 0, 7, 2 );
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:
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.
var pyramidGeom = new THREE.Geometry();
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:
Note that the order of the vertices on a face is not completely arbitrary: They should be listed in counterclockwise order as seen from in front of the face, that is, looking at the face from the outside of the pyramid.
This pyramid geometry as given will work with a MeshBasicMaterial, but to work with lit materials such as MeshLambertMaterial or MeshPhongMaterial, the geometry needs normal vectors. If the geometry has no normal vectors, Lambert and Phong materials will appear black. It is possible to assign the normal vectors by hand, but you can also have three.js compute them for you by calling methods in the geometry class. For the pyramid, this would be done by calling
pyramidGeom.computeFaceNormals();
This method computes one normal vector for each face, where the normal is perpendicular to the face. This is sufficient if the material is using flat shading; that is, if the material's flatShading property is set to true. The flatShading property was discussed in Subsection 5.1.3.
Flat shading is appropriate for the pyramid. But when an object is supposed to look smooth rather than faceted, it needs a normal vector for each vertex rather than one for each face. A Face3 has an array of three vertex normals. They can be set by hand, or Three.js can compute reasonable vertex normals for a smooth surface by averaging the face normals of all faces that share a vertex. Just call
geom.computeVertexNormals();
where geom is the geometry object. Note that the face normals must already exist before computeVertexNormals is called, so that usually you will call geom.computeVertexNormals() immediately after calling geom.computeFaceNormals(). A geometry that has face normals but not vertex normals will not work with a material whose flatShading property has the default value, false. To make it possible to use smooth shading on a surface like the pyramid, all of the vertex normals of each face should be set equal to its face normal. In that case, even with smooth shading, the pyramid's side will look flat. Standard three.js geometries such as BoxGeometry come with correct face and vertex normals.
The face normal for an object, face, of type THREE.Face3 is stored in the property face.normal. The vertex normals are stored in face.vertexNormals, which is an array of three Vector3.
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:
This code is from the sample program threejs/MeshFaceMaterial.html. The program displays a cube and a pyramid using multiple materials on each object. Here's what they look like:
There is another way to assign a different color to each face of a mesh object: It is possible to store the colors as properties of the face objects in the geometry. You can then use an ordinary material on the object, instead of an array of materials. But you also have to tell the material to use the colors from the geometry in place of the material's color property.
There are several ways that color might be assigned to faces in a mesh. One is to simply make each face a different solid color. Each face object has a color property that can be used to implement this idea. The value of the color property is an object of type THREE.Color, representing a color for the entire face. For example, we can set the face colors of the pyramid with
pyramidGeom.faces[0].color = new THREE.Color(0xCCCCCC); pyramidGeom.faces[1].color = new THREE.Color(0xCCCCCC); pyramidGeom.faces[2].color = new THREE.Color("green"); pyramidGeom.faces[3].color = new THREE.Color("blue"); pyramidGeom.faces[4].color = new THREE.Color("yellow"); pyramidGeom.faces[5].color = new THREE.Color("red");
To use these colors, the vertexColors property of the material must be set to the value THREE.FaceColors; for example:
material = new THREE.MeshLambertMaterial({ vertexColors: THREE.FaceColors, shading: THREE.FlatShading });
The default value of the property is THREE.NoColors, which tells the renderer to use the material's color property for every face.
A second way to apply color to a face is to apply a different color to each vertex of the face. WebGL will then interpolate the vertex colors to compute colors for pixels inside the face. Each face object has a property named vertexColors whose value should be an array of three THREE.Color objects, one for each vertex of the face. To use these colors, the vertexColors property of the material has to be set to THREE.VertexColors.
The following demo uses vertex colors and face colors on an icosahedral approximation for a sphere. The colors can be animated. In the color animation, each of the colors that is used on the object cycles through the set of possible hues. The positions of the vertices can also 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.)
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. 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 THREE.ParametricGeometry( func, slices, stacks )
where func is the JavaScript function 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:
This surface is defined by the function
function surfaceFunction( u, v ) { var x,y,z; // A point on the surface, calculated from u,v. // u and v range from 0 to 1. 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)); return new THREE.Vector3( x, y, z ); }
and the three.js mesh that represents the surface is created using
var surfaceGeometry = new THREE.ParametricGeometry(surfaceFunction, 64, 64); var 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,
var helix = new THREE.Curve(); helix.getPoint = function(t) { var 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:
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:
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:
var 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:
var shapeGeom = new THREE.ShapeGeometry( path );
The 2D shape created with this geometry is shown on the left in this picture:
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 segement 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.
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:
var loader = new THREE.TextureLoader(); var texture = loader.load( imageURL );
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:
var 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 change the value of material.map, be sure to set
material.needsUpdate = true;
to make sure that the change will take effect when the object is redrawn.
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:
pyramidGeometry.faceVertexUvs = [[ [ new THREE.Vector2(0,0), new THREE.Vector2(0,1), new THREE.Vector2(1,1) ], [ new THREE.Vector2(0,0), new THREE.Vector2(1,1), new THREE.Vector2(1,0) ], [ new THREE.Vector2(0,0), new THREE.Vector2(1,0), new THREE.Vector2(0.5,1) ], [ new THREE.Vector2(1,0), new THREE.Vector2(0,0), new THREE.Vector2(0.5,1) ], [ new THREE.Vector2(0,0), new THREE.Vector2(1,0), new THREE.Vector2(0.5,1) ], [ new THREE.Vector2(1,0), new THREE.Vector2(0,0), new THREE.Vector2(0.5,1) ], ]];
Note that this is a three-dimensional array.
The sample program threejs/textured-pyramid.html shows the pyramid with a brick texture. Here is an image from the program:
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.quaterion, 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.
(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.quaterion, 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.)
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.
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 file 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 folder in the source folder for this textbook.
If loader is an object of type THREE.GTLFLoader, you can use its load() method to start the process of loading a model:
loader = new THREE.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.
(If you try to run the sample program from a download of the web site version of this textbook, it probably won't work because of security restrictions in most web browsers. See the section on running things locally in the three.js documentation. The same applies to the demo below.)
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: