跳转至

5.1 Three.js 基础

Three.js Basics

Three.js 是一个用于 3D 图形的面向对象 JavaScript 库。它是由 Ricardo Cabello(化名 "mr.doob",https://mrdoob.com/)最初创建的开源项目,并且得到了其他程序员的贡献。它似乎是最受欢迎的开源 JavaScript 库,用于 3D Web 应用程序。(另一个受欢迎的选择是 Babylon.js。)Three.js 使用了你已经熟悉的概念,比如几何对象、变换、灯光、材质、纹理和摄像机。但它也有额外的特性,这些特性建立在 WebGL 的强大和灵活性之上。

你可以在它的主网站 https://threejs.org 下载 three.js 并阅读文档。下载的文件相当大,因为它包括了许多示例和支持文件。这本书使用的是 2023 年 7 月发布的版本 154。你应该意识到,你可能在网上找到的关于 three.js 的一些材料并不适用于最新版本。

three.js 的当前版本是一个“模块化”的 JavaScript 库。旧的非模块化形式仍然可用,但它已被弃用,并计划在版本 160 中移除。这本教科书的以前版本使用了非模块化版本。教科书的第 1.4 版已经更新为使用 three.js 模块;除此之外,three.js 材料只针对 three.js 版本之间的一些小差异进行了更改。(值得注意的是,我发现我必须显著增加材质颜色的镜面分量。)

本书中使用的所有 three.js 脚本副本可以在教科书网站源文件夹中的 threejs/script 文件夹中找到。three.js 许可证允许这些文件自由重新分发。但如果你计划认真使用 three.js,你应该阅读其网站上的文档,了解如何使用它以及如何部署它。

three.js 的核心特性定义在一个名为 "three.module.js" 的单一大型 JavaScript 文件中,该文件可以在 three.js 下载中的构建目录中找到。还有一个更小的 "压缩" 版本,three.module.min.js,它包含了相同定义,但格式不适合人类阅读。通常在网页上使用的是压缩版本。除了这个核心之外,three.js 下载还有一个目录,包含许多示例和在示例中使用的各种支持文件。示例使用了许多不是 three.js 核心部分的特性。这些插件可以在 three.js 下载的 examples 文件夹内的 jsm 文件夹中找到。其中一些插件在这本教科书中使用,并包含在 threejs/script 文件夹中。

Three.js is an object-oriented JavaScript library for 3D graphics. It is an open-source project originally created by Ricardo Cabello (who goes by the handle "mr.doob", https://mrdoob.com/), with contributions from other programmers. It seems to be the most popular open-source JavaScript library for 3D web applications. (Another popular option is Babylon.js.) Three.js uses concepts that you are already familiar with, such as geometric objects, transformations, lights, materials, textures, and cameras. But it also has additional features that build on the power and flexibility of WegGL.

You can download three.js and read the documentation at its main web site, https://threejs.org. The download is quite large, since it includes many examples and support files. This book uses Release 154 of the software, from July, 2023. You should be aware that some of the material about three.js that you might find on the Internet does not apply to the most recent release.

The current release of three.js is a "modular" JavaScript library. The older, non-modular form is still available, but it is deprecated and is scheduled to be removed in Release 160. Previous versions of this textbook used the non-modular version. Version 1.4 of the textbook has been updated to use three.js modules; aside from that, the three.js material has been changed only to account for some minor differences between three.js releases. (Notably, I found that I had to significantly increase the specular component of material colors.)

Copies of all three.js scripts that are used in this textbook can be found in the threejs/script folder in the source folder of this textbook's web site. The three.js license allows these files to be freely redistributed. But if you plan to do any serious work with three.js, you should read the documentation on its web site about how to use it and how to deploy it.

The core features of three.js are defined in a single large JavaScript file named "three.module.js", which can be found in a build directory in the three.js download. There is also a smaller "minified" version, three.module.min.js, that contains the same definitions in a format that is not meant to be human-readable. It is the minified version that is usually used on web pages. In addition to this core, the three.js download has a directory containing many examples and a variety of support files that are used in the examples. The examples use many features that are not part of the three.js core. These add-ons can be found in a folder named jsm inside the folder named examples in the three.js download. Several of the add-ons are used in this textbook and are included in the threejs/script folder.

5.1.1 关于 JavaScript 模块

About JavaScript Modules

"模块"一词通常指的是系统中相对独立的组件。模块以有限且明确定义的方式进行交互。它们是构建复杂系统的重要工具。在 JavaScript 中,模块是一个与其他脚本隔离的脚本,除非模块可以“导出”它定义的标识符。一个脚本导出的标识符可以被另一个脚本“导入”。如果一个模块的标识符没有被一个模块显式导出并被另一个模块导入,那么模块只能访问来自另一个模块的标识符。模块还可以访问非模块化脚本的标识符,而无需导入它们。

JavaScript 模块可以通过在其声明上添加 export 修饰符来导出标识符。例如:

export const RED="0xFF0000";
export function setColor(c) { ... }
export class FancyDraw { ... }

或者,它可以在其 export 语句中列出它想要导出的标识符。例如:

export { RED, setColor, FancyDraw };

export 语句有许多其他选项。然而,在这里我们主要感兴趣的是从 three.js 模块导入。

要使用模块化的 three.js,您将需要编写一个模块化脚本。你可以通过在 <script> 元素上添加 type="module" 属性来在网页上实现:

<script type="module">
    .
    .
    .
</script>

然后,脚本可以使用 import 语句来访问其他模块中的标识符。例如,

<script type="module">
import { FancyDraw, setColor } from "./drawutil.js";
.
. // 像平常一样使用 FancyDraw 和 setColor!
.

这假设导出标识符的模块定义在与网页相同目录下名为 drawutil.js 的脚本中。注意,如果脚本路径从当前目录开始,那么脚本名称必须以 "./" 开头。

我的 three.js 示例使用与网页同一目录下名为 script 的目录中的 three.module.min.js 文件。它们可以使用以下方式从该文件导入所有内容:

import * as THREE from "./script/three.module.min.js";

这种形式的 import 语句从 three.module.min.js 获取所有导出,并使它们成为名为 THREE 的新对象的属性。例如,导出的标识符 Mesh 被导入为 THREE.Mesh。再次强调,import 语句有其他形式,这里没有覆盖。


我的许多示例使用不在主要 three.js 脚本中的插件。我已经将使用它们的文件放置在我的 脚本目录 的子目录中。所有文件都来自 three.js 下载中的 examples/jsm 文件夹。我使用了与该文件夹相同的子目录结构,因为一些文件通过名称引用其他子目录中的文件。其中一个插件脚本是 "controls" 子目录中的 "OrbitControls.js"。它导出了一个名为 OrbitControls 的类,可以使用以下方式导入:

import { OrbitControls } from "./script/controls/OrbitControls.js";

插件模块从主要的 three.js 模块导入许多资源。不幸的是,它们不知道在哪里找到该文件。它们依赖于所谓的“import map”来指定其位置。可以通过另一种类型脚本,type="importmap" 来定义 import map。因此,你会看到我的许多示例脚本以这种方式开始:

<script type="importmap">
{
    "imports": {
        "three": "./script/three.module.min.js",
        "addons/": "./script/"
    }
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "addons/controls/OrbitControls.js";
import { GLTFLoader } from "addons/loaders/GLTFLoader.js";

"importmap" 脚本的内容是一个 JSON 对象。这里的 import map 定义 "three" 指向主要的 three.js 文件,并定义 "addons/" 指向脚本目录。插件模块将主要的 three.js 模块引用为 "three",因此该映射是必要的。"addons/" 映射实际上对我的示例是不需要的。


我只给你提供了一个非常简短的 JavaScript 模块概述——足够了,我希望让你理解我的示例程序,并编写一些类似的程序。对于更复杂的项目,你应该看看 three.js 开发者关于设置开发环境的说法。请参阅手册的 "Installation" 部分 https://threejs.org/docs/

The term "module" refers in general to a relatively independent component of a system. Modules interact in limited, well-defined ways. They are an important tool for building complex systems. In JavaScript, a module is a script that is isolated from other scripts, except that a module can "export" identifiers that it defines. Identifiers that are exported by one script can then be "imported" by another script. A module only has access to an identifier from another module if the identifier is explicitly exported by one module and imported by the other. Modules can also access identifiers from non-modular scripts, without having to import them.

A JavaScript module can export an identifier by adding the export modifier to its declaration. For example,

export const RED="0xFF0000";
export function setColor(c) { . . .
export class FancyDraw { . . .

Alternatively, it can list the identifiers that it wants to export in an export statement. For example,

export { RED, setColor, FancyDraw };

The export statement has many other options. However, here we are mostly interested in importing from three.js modules.

To use modular three.js, you will need to write a modular script. You can do that on a web page by adding the attribute type="module" to the <script> element:

<script type="module">
    .
    . 
    .
</script>

The script can then use import statements to access identifiers from other modules. For example,

<script type="module">
import { FancyDraw, setColor } from "./drawutil.js";
.
. // Use FancyDraw and setColor as usual!
.

This assumes that the module that exports the identifiers is defined in a script named drawutil.js in the same directory as the web page. Note that if the path to the script starts in the current directory, then the script name must start with "./".

My three.js examples use the file three.module.min.js from a directory named script in the same directory as the web page. They can import everything from that file using:

import * as THREE from "./script/three.module.min.js";

This form of the import statement gets all the exports from three.module.min.js and makes them properties of a new object named THREE. For example, the exported identifier Mesh is imported as THREE.Mesh. Again, the import statement has other forms, which are not covered here.


Many of my examples use add-ons that are not part of the main three.js script. I have placed the files that use them in subdirectories of my script directory. All of the files come from the examples/jsm folder in the three.js download. I have used the same subdirectory structure as that folder, because some of the files refer to files in other subdirectories by name. One of the add-on scripts is "OrbitControls.js" in the "controls" subdirectory. It exports a class named OrbitControls, which can be imported using

import { Orbitcontrols } from "./script/controls/OrbitControls.js";

The add-on modules import many resources from the main three.js module. Unfortunately, they don't know where to find that file. They rely on something called an "import map" to specify its location. An import map can be defined by another kind of script, with type="importmap". So, you will see that the scripts in many of my examples start something like this:

<script type="importmap">
{
    "imports": {
        "three": "./script/three.module.min.js",
        "addons/": "./script/"
    }
}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "addons/controls/OrbitControls.js";
import { GLTFLoader } from "addons/loaders/GLTFLoader.js";

The content of an "importmap" script is a JSON object. The import map here defines "three" to refer to the main three.js file, and it defines "addons/" to refer to the script directory. The add-on modules refer to the main three.js module as "three", so that mapping is necessary. The "addons/" mapping is actually not needed for my examples.


I have given you only a very brief overview of JavaScript modules—enough, I hope to let you understand my sample programs and write some similar programs of your own. For more complex projects, you should look at what the three.js developers have to say about setting up a development environment. See the "Installation" section of the Manual at https://threejs.org/docs/.

5.1.2 场景、渲染器、相机

Scene, Renderer, Camera

Three.jsHTML <canvas> 元素一起工作,这是我们在 第2.6节 中用于2D图形的相同元素。几乎所有的网络浏览器中,除了其2D图形API外,画布还支持使用 WebGL 进行3D绘图,这是 three.js 使用的,它与2D API的差异非常大。

Three.js 是一个面向对象的场景图API。(见 2.4.2小节。)基本过程是使用 three.js 对象构建场景图,然后渲染它所代表的场景的图像。通过在帧之间修改场景图的属性,可以实现动画。

Three.js 库由大量的类组成。其中最基础的三个是 THREE.SceneTHREE.CameraTHREE.WebGLRenderer。(实际上有几种渲染器类可用。THREE.WebGLRenderer 是迄今为止最常见的。一个用于 WebGPU 的渲染器尚在开发中。)一个 three.js 程序至少需要每种类型一个对象。这些对象通常存储在全局变量中:

let scene, renderer, camera;

注意,我们使用的几乎所有 three.js 类和常量都是一个名为 THREE 的对象的属性,并且它们的名称以 "THREE." 开头。(名称 "THREE" 是在导入 three.js 特性的导入语句中定义的;你可以使用不同的名称。)我有时会在不使用此前缀的情况下引用类,它通常不在使用 three.js 文档中,但在实际程序代码中必须始终包括前缀。

一个 Scene 对象是一个3D世界中构成所有对象的容器,包括灯光、图形对象,可能还有摄像机。它作为场景图的根节点。一个 Camera 是一种特殊类型的对象,代表一个视点,可以从该视点制作3D世界的图像。它代表一个视图变换和投影的组合。一个 WebGLRenderer 是一个可以从场景图中创建图像的对象。

场景是三个对象中最简单的。可以使用不带参数的构造函数将场景创建为 THREE.Scene 类型的对象:

scene = new THREE.Scene();

scene.add(item) 函数可以用来向 scene 添加摄像机、灯光和图形对象。这可能是你唯一需要调用的场景函数。偶尔也有用的是 scene.remove(item) 函数,它从场景中移除一个项目。


有两种摄像机,一种使用正交投影,一种使用透视投影。它们由类 THREE.OrthographicCameraTHREE.PerspectiveCamera 表示,它们是 THREE.Camera 的子类。构造函数使用熟悉的参数来指定投影(见 3.3.3小节):

camera = new THREE.OrthographicCamera( left, right, top, bottom, near, far );

camera = new THREE.PerspectiveCamera( fieldOfViewAngle, aspect, near, far );

正交摄像机的参数指定视体的 x、y 和 z 限制,在眼睛坐标系中——即在坐标系中,摄像机位于 (0,0,0),朝向负 z 轴方向,y 轴指向视图的上方。nearfar 参数以距离摄像机的方式给出 z 限制。对于正交投影,near 可以是负数,将“近”裁剪平面放置在摄像机后面。参数与 OpenGL 函数 glOrtho() 的相同,只是反转了指定顶部和底部裁剪平面的两个参数的顺序。

透视摄像机更常见。透视摄像机的参数来自 OpenGLGLU 库中的 gluPerspective() 函数。第一个参数确定视体的垂直范围,以度为单位的角度给出。aspect 是水平和垂直范围之间的比率;它通常应该设置为画布的宽度除以其高度。nearfar 给出视体的 z 限制,作为距离摄像机的距离。对于透视投影,两者都必须是正数,near 小于 far。创建透视摄像机的典型代码将是:

camera = new THREE.PerspectiveCamera( 45, canvas.width/canvas.height, 1, 100 );

其中 canvas 保存对图像将被渲染的 <canvas> 元素的引用。近和远的值意味着只有位于摄像机前 1 到 100 单位之间的物体被包含在图像中。记住,使用过大的 far 值或过小的 near 值可能会干扰深度测试的准确性。

像其他对象一样,摄像机可以被添加到场景中,但它不必是场景图的一部分才能被使用。如果你想让它成为图中另一个对象的父对象或子对象,你可能会将它添加到场景图。无论如何,你通常希望对摄像机应用建模变换,以设置其在3D空间中的位置和方向。当我更一般地讨论变换时,我将在后面介绍这一点。


渲染器是 THREE.WebGLRenderer 类的一个实例。它的构造函数有一个参数,这是一个包含影响渲染器设置的 JavaScript 对象。你最有可能指定的设置是 canvas(告诉渲染器在哪里绘制)和 antialias(请求渲染器尽可能使用抗锯齿):

renderer = new THREE.WebGLRenderer({
    canvas: theCanvas,
    antialias: true
});

在这里,theCanvas 将是渲染器将显示它产生的图像的 <canvas> 元素的引用。(注意,将 JavaScript 对象作为参数的技术在许多 three.js 函数中使用。它允许支持大量选项,而无需要求一个必须按特定顺序指定的长参数列表。相反,你只需要指定你想要提供非默认值的选项,并且可以按任何顺序通过名称指定这些选项。)

你想要使用渲染器做的主要是渲染图像。为此,你还需要一个场景和一台摄像机。要渲染从给定摄像机的视角看给定场景的图像,请调用:

renderer.render(scene, camera);

这确实是任何 three.js 应用中的中心命令。

(我应该指出,我见过的大多数示例没有向渲染器提供 canvas;相反,它们允许渲染器创建它。然后可以从渲染器获取 canvas 并将其添加到页面。此外,canvas 通常填充整个浏览器窗口。示例程序 threejs/full-window.html 展示了如何做到这一点。然而,我的所有其他示例都使用现有的 canvas,渲染器构造函数如上所示。)

Three.js works with the HTML <canvas> element, the same thing that we used for 2D graphics in Section 2.6. In almost all web browsers, in addition to its 2D Graphics API, a canvas also supports drawing in 3D using WebGL, which is used by three.js and which is about as different as it can be from the 2D API.

Three.js is an object-oriented scene graph API. (See Subsection 2.4.2.) The basic procedure is to build a scene graph out of three.js objects, and then to render an image of the scene it represents. Animation can be implemented by modifying properties of the scene graph between frames.

The three.js library is made up of a large number of classes. Three of the most basic are THREE.Scene, THREE.Camera, and THREE.WebGLRenderer. (There are actually several renderer classes available. THREE.WebGLRenderer is by far the most common. A renderer for WebGPU is available but is still under development.) A three.js program will need at least one object of each type. Those objects are often stored in global variables

let scene, renderer, camera;

Note that almost all of the three.js classes and constants that we will use are properties of an object named THREE, and their names begin with "THREE.". (The name "THREE" is defined in the import statement that imports the three.js features; you can use a different name.) I will sometimes refer to classes without using this prefix, and it is not usually used in the three.js documentation, but the prefix must always be included in actual program code.

A Scene object is a holder for all the objects that make up a 3D world, including lights, graphical objects, and possibly cameras. It acts as a root node for the scene graph. A Camera is a special kind of object that represents a viewpoint from which an image of a 3D world can be made. It represents a combination of a viewing transformation and a projection. A WebGLRenderer is an object that can create an image from a scene graph.

The scene is the simplest of the three objects. A scene can be created as an object of type THREE.Scene using a constructor with no parameters:

scene = new THREE.Scene();

The function scene.add(item) can be used to add cameras, lights, and graphical objects to the scene. It is probably the only scene function that you will need to call. The function scene.remove(item), which removes an item from the scene, is also occasionally useful.


There are two kinds of camera, one using orthographic projection and one using perspective projection. They are represented by classes THREE.OrthographicCamera and THREE.PerspectiveCamera, which are subclasses of THREE.Camera. The constructors specify the projection, using parameters that are familiar from OpenGL (see Subsection 3.3.3):

camera = new THREE.OrthographicCamera( left, right, top, bottom, near, far );

or

camera = new THREE.PerspectiveCamera( fieldOfViewAngle, aspect, near, far );

The parameters for the orthographic camera specify the x, y, and z limits of the view volume, in eye coordinates—that is, in a coordinate system in which the camera is at (0,0,0) looking in the direction of the negative z-axis, with the y-axis pointing up in the view. The near and far parameters give the z-limits in terms of distance from the camera. For an orthographic projection, near can be negative, putting the "near" clipping plane in back of the camera. The parameters are the same as for the OpenGL function glOrtho(), except for reversing the order of the two parameters that specify the top and bottom clipping planes.

Perspective cameras are more common. The parameters for the perspective camera come from the function gluPerspective() in OpenGL's GLU library. The first parameter determines the vertical extent of the view volume, given as an angle measured in degrees. The aspect is the ratio between the horizontal and vertical extents; it should usually be set to the width of the canvas divided by its height. And near and far give the z-limits on the view volume as distances from the camera. For a perspective projection, both must be positive, with near less than far. Typical code for creating a perspective camera would be:

camera = new THREE.PerspectiveCamera( 45, canvas.width/canvas.height, 1, 100 );

where canvas holds a reference to the <canvas> element where the image will be rendered. The near and far values mean that only things between 1 and 100 units in front of the camera are included in the image. Remember that using an unnecessarily large value for far or an unnecessarily small value for near can interfere with the accuracy of the depth test.

A camera, like other objects, can be added to a scene, but it does not have to be part of the scene graph to be used. You might add it to the scene graph if you want it to be a parent or child of another object in the graph. In any case, you will generally want to apply a modeling transformation to the camera to set its position and orientation in 3D space. I will cover that later when I talk about transformations more generally.


A renderer is an instance of the class THREE.WebGLRenderer. Its constructor has one parameter, which is a JavaScript object containing settings that affect the renderer. The settings you are most likely to specify are canvas, which tells the renderer where to draw, and antialias, which asks the renderer to use antialiasing if possible:

renderer = new THREE.WebGLRenderer( {
                        canvas: theCanvas,
                        antialias: true
                    } );

Here, theCanvas would be a reference to the <canvas> element where the renderer will display the images that it produces. (Note that the technique of having a JavaScript object as a parameter is used in many three.js functions. It makes it possible to support a large number of options without requiring a long list of parameters that must all be specified in some particular order. Instead, you only need to specify the options for which you want to provide non-default values, and you can specify those options by name, in any order.)

The main thing that you want to do with a renderer is render an image. For that, you also need a scene and a camera. To render an image of a given scene from the point of view of a given camera, call

renderer.render( scene, camera );

This is really the central command in any three.js application.

(I should note that most of the examples that I have seen do not provide a canvas to the renderer; instead, they allow the renderer to create it. The canvas can then be obtained from the renderer and added to the page. Furthermore, the canvas typically fills the entire browser window. The sample program threejs/full-window.html shows how to do that. However, all of my other examples use an existing canvas, with the renderer constructor shown above.)

5.1.3 THREE.Object3D

THREE.Object3D

three.js 场景图由 THREE.Object3D 类型的对象组成(包括属于该类的子类的物体)。摄像机、灯光和可见物体都由 Object3D 的子类表示。实际上,THREE.Scene 本身也是 Object3D 的一个子类。

任何 Object3D 都包含一个子对象列表,这些子对象也是 Object3D 类型。子列表定义了场景图的结构。如果 node 和 object 是 Object3D 类型,那么方法 node.add(object)object 添加到 node 的子列表中。方法 node.remove(object) 可以用来从列表中移除一个对象。

three.js 场景图实际上是一棵树。也就是说,图中的每个节点都有一个唯一的父节点,除了根节点,它没有父节点。一个 Object3D,obj,有一个属性 obj.parent,指向场景图中 obj 的父节点(如果有的话)。你永远不应该直接设置这个属性。当节点被添加到另一个节点的子列表时,它会自动设置。如果 obj 在被添加为节点的子节点时已经有父节点,那么 obj 首先从当前父节点的子列表中移除,然后被添加到 node 的子列表中。

一个 Object3D,obj,的子节点存储在一个名为 obj.children 的属性中,这是一个普通的 JavaScript 数组。然而,你应该总是使用 obj.add()obj.remove() 方法来添加和移除 obj 的子节点。

为了便于复制场景图结构的一部分,Object3D 定义了一个 clone() 方法。这个方法复制节点,包括递归复制节点的子节点。这使得在场景图中包含相同结构的多个副本变得容易:

let node = THREE.Object3D();
    .
    .  // 向 node 添加子节点。
    .
scene.add(node);
let nodeCopy1 = node.clone();
    .
    .  // 修改 nodeCopy1,可能应用一个变换。
    .
scene.add(nodeCopy1)
let nodeCopy2 = node.clone();
    .
    .  // 修改 nodeCopy2,可能应用一个变换。
    .
scene.add(nodeCopy2);

一个 Object3Dobj,有一个关联的变换,由属性 obj.scaleobj.rotationobj.position 给出。这些属性表示在渲染对象及其子节点时应用到对象上的建模变换。对象首先按比例缩放,然后旋转,然后根据这些属性的值进行平移。(变换实际上比这更复杂,但我们现在先保持简单,稍后将回到这个话题。)

obj.scaleobj.position 的值是 THREE.Vector3 类型的对象。一个 Vector3 表示三维中的向量或点。(还有类似的类 THREE.Vector2THREE.Vector4,用于2维和4维的向量。)可以从三个数字构造一个 Vector3 对象,这些数字给出向量的坐标:

let v = new THREE.Vector3( 17, -3.14159, 42 );

这个对象有属性 v.xv.yv.z 表示坐标。这些属性可以单独设置;例如:v.x = 10。它们也可以一次性设置,使用方法 v.set(x,y,z)Vector3 类还有许多实现向量运算的方法,如加法、点积和叉积。

对于 Object3D属性 obj.scale.xobj.scale.yobj.scale.z 给出对象在 x、y 和 z 方向上的缩放量。默认值当然是 1。调用

obj.scale.set(2,2,2);

意味着在渲染时对象将受到 2 的均匀缩放因子。设置

obj.scale.y = 0.5;

只会在 y 方向上将其缩小一半(假设 obj.scale.xobj.scale.z 仍然有它们的默认值)。

类似地,属性 obj.position.xobj.position.yobj.position.z 给出在渲染时将应用于对象的 x、y 和 z 方向上的平移量。例如,由于摄像机是 Object3D,设置

camera.position.z = 20;

意味着摄像机将从原点的默认位置移动到正 z 轴上的 (0,0,20) 点。当摄像机用于渲染场景时,摄像机上的这种建模变换就成为了视图变换。

对象 obj.rotation 有属性 obj.rotation.x、obj.rotation.y 和 obj.rotation.z,代表关于 x、y 和 z 轴的旋转。角度以弧度为单位。对象首先绕 x 轴旋转,然后绕 y 轴旋转,然后绕 z 轴旋转。(可以改变这个顺序。)obj.rotation 的值不是一个向量。相反,它属于一个类似的类型,THREE.Euler,并且旋转角度被称为 欧拉角

A three.js scene graph is made up of objects of type THREE.Object3D (including objects that belong to subclasses of that class). Cameras, lights, and visible objects are all represented by subclasses of Object3D. In fact, THREE.Scene itself is also a subclass of Object3D.

Any Object3D contains a list of child objects, which are also of type Object3D. The child lists define the structure of the scene graph. If node and object are of type Object3D, then the method node.add(object) adds object to the list of children of node. The method node.remove(object) can be used to remove an object from the list.

A three.js scene graph must, in fact, be a tree. That is, every node in the graph has a unique parent node, except for the root node, which has no parent. An Object3D, obj, has a property obj.parent that points to the parent of obj in the scene graph, if any. You should never set this property directly. It is set automatically when the node is added to the child list of another node. If obj already has a parent when it is added as a child of node, then obj is first removed from the child list of its current parent before it is added to the child list of node.

The children of an Object3D, obj, are stored in a property named obj.children, which is an ordinary JavaScript array. However, you should always add and remove children of obj using the methods obj.add() and obj.remove().

To make it easy to duplicate parts of the structure of a scene graph, Object3D defines a clone() method. This method copies the node, including the recursive copying of the children of that node. This makes it easy to include multiple copies of the same structure in a scene graph:

let node = THREE.Object3D();
    .
    .  // Add children to node.
    .
scene.add(node);
let nodeCopy1 = node.clone();
    .
    .  // Modify nodeCopy1, maybe apply a transformation.
    .
scene.add(nodeCopy1)
let nodeCopy2 = node.clone();
    .
    .  // Modify nodeCopy2, maybe apply a transformation.
    .
scene.add(nodeCopy2);

An Object3D, obj, has an associated transformation, which is given by properties obj.scale, obj.rotation, and obj.position. These properties represent a modeling transformation to be applied to the object and its children when the object is rendered. The object is first scaled, then rotated, then translated according to the values of these properties. (Transformations are actually more complicated than this, but we will keep things simple for now and will return to the topic later.)

The values of obj.scale and obj.position are objects of type THREE.Vector3. A Vector3 represents a vector or point in three dimensions. (There are similar classes THREE.Vector2 and THREE.Vector4 for vectors in 2 and 4 dimensions.) A Vector3 object can be constructed from three numbers that give the coordinates of the vector:

let v = new THREE.Vector3( 17, -3.14159, 42 );

This object has properties v.x, v.y, and v.z representing the coordinates. The properties can be set individually; for example: v.x = 10. They can also be set all at once, using the method v.set(x,y,z). The Vector3 class also has many methods implementing vector operations such as addition, dot product, and cross product.

For an Object3D, the properties obj.scale.x, obj.scale.y, and obj.scale.z give the amount of scaling of the object in the x, y, and z directions. The default values, of course, are 1. Calling

obj.scale.set(2,2,2);

means that the object will be subjected to a uniform scaling factor of 2 when it is rendered. Setting

obj.scale.y = 0.5;

will shrink it to half-size in the y-direction only (assuming that obj.scale.x and obj.scale.z still have their default values).

Similarly, the properties obj.position.x, obj.position.y, and obj.position.z give the translation amounts that will be applied to the object in the x, y, and z directions when it is rendered. For example, since a camera is an Object3D, setting

camera.position.z = 20;

means that the camera will be moved from its default position at the origin to the point (0,0,20) on the positive z-axis. This modeling transformation on the camera becomes a viewing transformation when the camera is used to render a scene.

The object obj.rotation has properties obj.rotation.x, obj.rotation.y, and obj.rotation.z that represent rotations about the x-, y-, and z-axes. The angles are measured in radians. The object is rotated first about the x-axis, then about the y-axis, then about the z-axis. (It is possible to change this order.) The value of obj.rotation is not a vector. Instead, it belongs to a similar type, THREE.Euler, and the angles of rotation are called Euler angles.

5.1.4 物体、几何形状、材料

Object, Geometry, Material

three.js 中,可见对象由点、线或三角形组成。一个单独的对象对应于 OpenGL 基元,如 GL_POINTSGL_LINESGL_TRIANGLES(见 3.1.1小节)。有五个类来表示这些可能性:THREE.Points 用于点,THREE.Mesh 用于三角形,以及三个类用于线:THREE.Line,使用 GL_LINE_STRIP 基元;THREE.LineSegments,使用 GL_LINES 基元;和 THREE.LineLoop,使用 GL_LINE_LOOP 基元。

可见对象由一些几何体和决定该几何体外观的材料组成。在 three.js 中,可见对象的几何体和材料本身由 JavaScriptTHREE.BufferGeometryTHREE.Material 表示。

THREE.BufferGeometry 类型的对象可以存储顶点坐标及其属性。(实际上,顶点坐标也被视为几何体的“属性”。)这些值必须以适合与 OpenGL 函数 glDrawArraysglDrawElements(见 3.4.2小节)一起使用的形式存储。对于 JavaScript,这意味着它们必须存储在类型化数组中。类型化数组类似于普通的 JavaScript 数组,只是它的长度是固定的,只能保存特定类型的数值。例如,Float32Array 保存 32 位浮点数,UInt16Array 保存无符号 16 位整数。类型化数组可以通过指定数组长度的构造函数来创建。例如,

vertexCoords = new Float32Array(300);  // 300个数字的空间。

或者,构造函数可以以一个普通 JavaScript 数组作为其参数。这会创建一个包含从 JavaScript 数组中相同的数字的类型化数组。例如,

data = new Float32Array([1.3, 7, -2.89, 0, 3, 5.5]);

在这种情况下,data 的长度是六,它包含来自 JavaScript 数组的数字副本。

BufferGeometry 指定顶点是一个多步骤过程。您需要创建一个包含顶点坐标的类型化数组。然后,您需要将该数组包装在一个 THREE.BufferAttribute 类型的对象内。最后,您可以将属性添加到几何体中。这里是一个例子:

let vertexCoords = new Float32Array([0,0,0, 1,0,0, 0,1,0]);
let vertexAttrib = new THREE.BufferAttribute(vertexCoords, 3);
let geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", vertexAttrib);

BufferGeometry 构造函数的第二个参数是一个整数,它告诉 three.js 每个顶点的坐标数量。回想一下,一个顶点可以通过 2、3 或 4 个坐标来指定,您需要指定数组为每个顶点提供了多少个数字。转到 setAttribute() 函数,一个 BufferGeometry 可以有属性来指定颜色、法向量和纹理坐标,以及其他自定义属性。 setAttribute() 的第一个参数是属性的名称。这里,“position”是指定顶点坐标或位置的属性名称。

类似地,要为每个顶点指定颜色,您可以将颜色的 RGB 分量放入一个 Float32Array 中,并使用它来为名为“color”的 BufferGeometry 属性指定值。

对于一个具体的例子,假设我们想使用类型为 THREE.Pointsthree.js 对象来表示类型为 GL_POINTS 的原素。假设我们想要在单位球体内部随机放置 10000 个点,每个点都有自己的随机颜色。以下是创建所需 BufferGeometry 的一些代码:

let pointsBuffer = new Float32Array(30000);  // 每个顶点 3 个数字!
let colorBuffer = new Float32Array(30000);
let i = 0;
while (i < 10000) {
    let x = 2 * Math.random() - 1;
    let y = 2 * Math.random() - 1;
    let z = 2 * Math.random() - 1;
    if (x * x + y * y + z * z < 1) {
        // 仅使用单位球体内部的点
        pointsBuffer[3 * i] = x;
        pointsBuffer[3 * i + 1] = y;
        pointsBuffer[3 * i + 2] = z;
        colorBuffer[3 * i] = 0.25 + 0.75 * Math.random();
        colorBuffer[3 * i + 1] = 0.25 + 0.75 * Math.random();
        colorBuffer[3 * i + 2] = 0.25 + 0.75 * Math.random();
        i++;
    }
}
let pointsGeom = new THREE.BufferGeometry();
pointsGeom.setAttribute("position",
                        new THREE.BufferAttribute(pointsBuffer, 3));
pointsGeom.setAttribute("color",
                        new THREE.BufferAttribute(colorBuffer, 3));

three.js 中,要使一些几何体成为可见对象,我们还需要一个适当的材料。例如,对于类型为 THREE.Points 的对象,我们可以使用 THREE.PointsMaterial 类型的材料,它是 Material 的一个子类。材料可以指定点的颜色和大小等属性:

let pointsMat = new THREE.PointsMaterial({
    color: "yellow",
    size: 2,
    sizeAttenuation: false
});

构造函数的参数是一个 JavaScript 对象,其属性用于初始化材料。将 sizeAttenuation 属性设置为 false 时,大小以像素为单位;如果是 true,则 size 表示世界坐标中的大小,并且点会根据与观察者的距离进行缩放。如果省略 color,则使用默认值白色。size 的默认值为 1,sizeAttenuation 的默认值为 true。可以完全省略构造函数的参数,以使用所有默认值。PointsMaterial 不受照明影响;它简单地显示由其 color 属性指定的颜色。

也可以在对象创建后为材料的属性分配值。例如,

let pointsMat = new THREE.PointsMaterial();
pointsMat.color = new THREE.Color("yellow");
pointsMat.size = 2;
pointsMat.sizeAttenuation = false;

请注意,颜色是作为 THREE.Color 类型的值设置的,这是从字符串 "yellow" 构造的。当在材料构造函数中设置颜色属性时,会自动执行相同的从字符串到颜色的转换。

一旦我们有了几何体和材料,我们就可以使用它们来创建类型为 THREE.Points 的可见对象,并将其实加到场景中:

let sphereOfPoints = new THREE.Points(pointsGeom, pointsMat);
scene.add(sphereOfPoints);

这将显示一团黄色的点。但我们希望每个点都有自己的颜色!回想一下,点的颜色存储在几何体中,而不是材料中。我们必须告诉材料使用几何体中的颜色,而不是材料自己的颜色属性。这可以通过将材料属性 vertexColors 的值设置为 true 来完成。所以,我们可以这样创建材料:

let pointsMat = new THREE.PointsMaterial({
    color: "white",
    size: 2,
    sizeAttenuation: false,
    vertexColors: true
});

这里使用白色作为材料颜色,因为顶点颜色实际上是乘以材料颜色的,而不仅仅是替换它。

以下演示显示了一个点云。你可以控制点是全部为黄色还是随机着色。你可以动画化点,并且可以控制点的大小和数量。请注意,点被渲染为正方形。


three.js 中,上述材料的颜色参数是通过字符串 "yellow" 指定的。three.js 中的颜色可以由 THREE.Color* 类型的值表示。THREE.Color 类代表一个 RGB 颜色。一个 Color 对象 c 有属性 c.rc.gc.b,分别给出红色、蓝色和绿色颜色分量,作为范围在 0.0 到 1.0 之间的浮点数。请注意,没有 alpha 分量;three.js 将透明度与颜色分开处理。

有几种方式可以构造一个 *THREE.Color 对象。构造函数可以接收三个参数,给出 RGB 分量,作为范围在 0.0 到 1.0 之间的实数。它可以接受一个字符串参数,以 CSS 颜色字符串的形式给出颜色,如 2D 画布图形 API 中使用的;示例包括 "white"、"red"、"rgb(255,0,0)" 和 "#FF0000"。或者颜色构造函数可以接收一个整数参数,其中每种颜色分量都以整数中的一个八位字段给出。通常,以这种方式表示颜色的整数会写成以 "0x" 开头的十六进制字面量。示例包括 0xff0000 表示红色,0x00ff00 表示绿色,0x0000ff 表示蓝色,0x007050 表示深蓝绿色。以下是使用颜色构造函数的一些示例:

let c1 = new THREE.Color("skyblue");
let c2 = new THREE.Color(1,1,0);  // yellow
let c3 = new THREE.Color(0x98fb98);  // pale green

在许多上下文中,如 THREE.Points 构造函数,three.js* 会接受一个字符串或整数,当需要一个颜色时;字符串或整数将通过 Color 构造函数。作为另一个示例,一个 WebGLRenderer* 对象有一个 "clear color" 属性,当渲染器渲染场景时用作背景颜色。这个属性可以使用以下任何命令设置:

renderer.setClearColor( new THREE.Color(0.6, 0.4, 0.1) );
renderer.setClearColor( "darkgray" );
renderer.setClearColor( 0x99BBEE );

接下来是线条,类型为 THREE.Line 的对象表示一个线带——在 OpenGL 中被称为 GL_LINE_STRIP 类型的原素。要得到相同的连接线段条带,加上一条回到起始顶点的线,我们可以使用类型为 THREE.LineLoop 的对象。例如,对于一个三角形的轮廓,我们可以提供一个包含三个点坐标的 BufferGeometry,并使用一个 LineLoop

我们还需要一个材料。对于线条,材料可以由类型为 THREE.LineBasicMaterial 的对象表示。像往常一样,构造函数的参数是一个 JavaScript 对象,其属性可以包括 colorlinewidth。例如:

let lineMat = new THREE.LineBasicMaterial({
    color:  0xA000A0,  // purple; 默认是白色
    linewidth: 2       // 2 像素;默认是 1
});

(linewidth 属性可能不被尊重。根据规范,WebGL 实现可以将最大线宽设置为 1。)

像点一样,通过在几何体中添加一个 "color" 属性,并将 vertexColors 材料属性的值设置为 true,可以为每个目的指定不同的颜色。以下是一个完整的示例,它制作了一个顶点颜色分别为红色、绿色和蓝色的三角形:

let positionBuffer = new Float32Array([
    -2, -2,   // 第一个顶点的坐标。
    2, -2,   // 第二个顶点的坐标。
    0,  2    // 第三个顶点的坐标。
]);
let colorBuffer = new Float32Array([
    1, 0, 0,  // 第一个顶点的颜色(红色)。
    0, 1, 0,  // 第二个顶点的颜色(绿色)。
    0, 0, 1   // 第三个顶点的颜色(蓝色)。
]);
let lineGeometry = new THREE.BufferGeometry();
lineGeometry.setAttribute(
    "position",
    new THREE.BufferAttribute(positionBuffer,2)
);
lineGeometry.setAttribute(
    "color",
    new THREE.BufferAttribute(colorBuffer,3)
);
let lineMaterial = new THREE.LineBasicMaterial({
    linewidth: 3,
    vertexColors: true
});
let triangle = new THREE.LineLoop(lineGeometry, lineMaterial);
scene.add(triangle);

这产生了以下图像:

Threejs Triangle Vertexcolors

"Basic" 在 LineBasicMaterial 中表示这种材料使用基本颜色,这些颜色不需要照明就能看到,也不会受到照明的影响。这通常是线条想要的。


three.js 中,网格对象对应于 OpenGL 基元 GL_TRIANGLES。网格的几何体对象必须指定哪些顶点是属于哪些三角形的。我们将在下一节中看到如何做到这一点。然而,three.js 提供了一些类来表示常见的网格几何体,例如球体、圆柱体和圆环面。对于这些内置类,您只需要调用构造函数即可创建适当的几何体。例如,类 THREE.CylinderGeometry 表示圆柱体的几何体,其构造函数的形式如下:

new THREE.CylinderGeometry(radiusTop, radiusBottom, height,
        radiusSegments, heightSegments, openEnded, thetaStart, thetaLength)

这个构造函数创建的几何体是对圆柱体的近似表示,其轴线沿着 y 轴。它沿着该轴从 −height/2 延伸到 height/2。其圆形顶部的半径是 radiusTop,底部的半径是 radiusBottom。两个半径不必相同;当它们不同时,您将得到一个截断的圆锥而不是真正的圆柱体。使用 radiusTop 的零值可以制作一个实际的圆锥。参数 radiusSegmentsheightSegments 分别给出圆柱体圆周和长度上的细分数——在 OpenGLGLUT 库中称为切片和堆叠。参数 openEnded 是一个布尔值,指示是否绘制圆柱体的顶部和底部;使用值 true 可以得到一个开放式的管子。最后两个参数允许您制作部分圆柱体。它们的值以弧度为单位,围绕 y 轴测量。仅渲染从 thetaStart 开始到 thetaStart 加上 thetaLength 结束的圆柱体部分。例如,如果 thetaLength 是 Math.PI,您将得到一个半圆柱体。

构造函数的大量参数提供了很多灵活性。所有参数都是可选的。前三个参数的默认值每个都是一。radiusSegments 的默认值是 8,这为平滑圆柱体提供了一个较差的近似。省略最后三个参数将得到一个两端封闭的完整圆柱体。

其他标准网格几何体类似。这里是一些构造函数,列出了所有参数(但请记住,大多数参数是可选的):

new THREE.BoxGeometry(width, height, depth,
                        widthSegments, heightSegments, depthSegments)

new THREE.PlaneGeometry(width, height, widthSegments, heightSegments)

new THREE.RingGeometry(innerRadius, outerRadius, thetaSegments, phiSegments,
                        thetaStart, thetaLength)

new THREE.ConeGeometry(radiusBottom, height, radiusSegments, 
                        heightSegments, openEnded, thetaStart, thetaLength)

new THREE.SphereGeometry(radius, widthSegments, heightSegments,
                        phiStart, phiLength, thetaStart, thetaLength)

new THREE.TorusGeometry(radius, tube, radialSegments, tubularSegments, arc)

BoxGeometry 表示以原点为中心的矩形盒子的几何体。它的构造函数有三个参数,用于给出盒子在每个方向上的大小;默认值为一。最后三个参数给出每个方向上的细分数,默认值为一;大于一的值将导致盒子的面被细分为更小的三角形。

PlaneGeometry 表示位于 xy 平面上、以原点为中心的矩形的几何体。它的参数与立方体类似。RingGeometry 表示一个圆环,即从中心去掉一个较小圆盘的圆盘。圆环位于 xy 平面上,其中心位于原点。您应该总是指定圆环的内外半径。

ConeGeometry 的构造函数与 CylinderGeometry 的构造函数具有完全相同的形式和效果,只是将 radiusTop 设置为零。也就是说,它构建了一个以 y 轴为中心、以原点为中心的圆锥。

对于 SphereGeometry,所有参数都是可选的。构造函数创建了一个以原点为中心、y 轴为轴的球体。第一个参数,给出球体的半径,默认值为一。接下来两个参数给出切片和堆叠的数量,默认值分别为 32 和 16。最后四个参数允许您制作球体的一部分;默认值给出一个完整的球体。这四个参数是以弧度为单位测量的角度。phiStartphiLength 在赤道周围的角度测量,并给出生成的球壳的经度范围。例如:

new THREE.SphereGeometry( 5, 32, 16, 0, Math.PI )

创建了球体“西半球”的几何体。最后两个参数是沿着从球体北极到南极的纬度线测量的角度。例如,要得到球体的“北半球”:

new THREE.SphereGeometry( 5, 32, 16, 0, 2*Math.PI, 0, Math.PI/2 )

对于 TorusGeometry,构造函数创建了一个位于 xy 平面上、以原点为中心、z 轴穿过其孔的圆环面。参数 radius 是圆环中心到圆环管中心的距离,而 tube 是管的半径。接下来的两个参数分别给出每个方向上的细分数。最后一个参数,arc,允许您只制作圆环的一部分。它是一个在 0 到 2*Math.PI 之间的角度,沿着圆环管中心的圆周测量。

还有代表正多面体的几何体类:THREE.TetrahedronGeometryTHREE.OctahedronGeometryTHREE.DodecahedronGeometryTHREE.IcosahedronGeometry。(对于立方体,使用 BoxGeometry。)这四个类的构造函数接受两个参数。第一个指定多面体的大小,默认值为 1。大小以包含多面体的球体的半径给出。第二个参数是一个整数,称为 detail。默认值 0 给出实际的正多面体。较大的值通过添加额外的面来增加细节。随着细节的增加,多面体成为对球体的更好近似。这可以通过下面的插图更容易理解:

Icosahedron Detail

图像显示了使用细节参数等于 0、1、2 和 3 的二十面体几何体的四个网格对象。


要创建一个网格对象,您需要一个材料以及一个几何体。有几种材料适合网格对象,包括 THREE.MeshBasicMaterialTHREE.MeshLambertMaterialTHREE.MeshPhongMaterial。(还有更多网格材料,包括两种较新的材料,THREE.MeshStandardMaterialTHREE.MeshPhysicalMaterial,它们实现了与基于物理的渲染相关的技术,这是一种已成为流行的改进渲染方法。然而,我将不会在这里介绍它们。)

MeshBasicMaterial 表示一种不受照明影响的颜色;无论场景中是否有灯光,它看起来都是一样的,并且它没有阴影,给它一个平坦的而不是3D的外观。其他两个类代表需要被照亮才能看到的材料。它们实现了称为Lambert着色Phong着色的照明模型。主要的区别是 MeshPhongMaterial 有一个镜面颜色,但 *MeshLambertMaterial 没有。它们都可以有散射和自发光颜色。对于所有三种材料类,构造函数有一个参数,一个 JavaScript 对象,它指定了材料的属性值。例如:

let mat = new THREE.MeshPhongMaterial({
    color: 0xbbbb00,     // 散射和环境光的反射率
    emissive: 0,         // 自发光颜色;这是默认值(黑色)
    specular: 0x303030,  // 镜面光的反射率
    shininess: 50        // 控制镜面高光的大小
});

这个例子显示了 Phong 材料的四个颜色参数。这些参数与 OpenGL 中的五个材质属性具有相同的含义(4.1.1小节)。Lambert 材料缺少镜面和光泽度,基本网格材料只有颜色参数。

还有一些其他材料属性,您可能需要在构造函数中设置。除了 flatShading,这些适用于所有三种网格材料:

  • vertexColors — 一个布尔属性,可以设置为 true 以使用几何体中的顶点颜色。默认值为 false。
  • wireframe — 一个布尔值,表示是否应将网格绘制为线框模型,只显示其面的轮廓。默认值为 false。一个 true 值最适合使用 MeshBasicMaterial
  • wireframeLinewidth — 用于绘制线框的线条宽度,以像素为单位。默认值为 1。(非默认值可能不被尊重。)
  • visible — 一个布尔值,控制是否渲染使用它的对象,默认值为 true
  • side — 值为 THREE.FrontSideTHREE.BackSideTHREE.DoubleSide,默认值为 THREE.FrontSide。这决定了是否根据面可见的一侧来绘制网格面。使用默认值 THREE.FrontSide,只有从前面看时才绘制面。THREE.DoubleSide 无论从前面还是从后面看都会绘制它,而 THREE.BackSide 仅从后面看时绘制。对于封闭对象,如立方体或完整球体,只要观察者在对象外部,默认值就讲得通。对于平面、开放管或部分球体,应该将值设置为 THREE.DoubleSide。否则,应该在视图中的对象部分将不会被绘制。
  • flatShading — 一个 boolean 值,默认为 false。这对 MeshBasicMaterial 不起作用。对于应该看起来“有面”的物体,具有平坦侧面,重要的是将此属性设置为 true。例如,对于立方体或边数较少的圆柱体,就是这样。

作为示例,让我们制作一个有光泽的蓝绿色、开放的、五边的管子,侧面平坦:

let mat = new THREE.MeshPhongMaterial({
    color: 0x0088aa,
    specular: 0x003344,
    shininess: 100,
    flatShading: true,  // 使侧面看起来平坦
    side: THREE.DoubleSide  // 绘制管子的内部
});
let geom = new THREE.CylinderGeometry(3, 3, 10, 5, 1, true);
let obj = new THREE.Mesh(geom, mat);
scene.add(obj);

您可以使用以下演示来查看几个 three.js 网格对象,使用各种几何体和材料。在对象上拖动鼠标以旋转它。您还可以探索正多面体几何体的详细程度。

演示可以显示一个线框版本的物体,叠加在实体版本上。在 three.js 中,线框和实体版本实际上是使用相同几何体但不同材料的两个对象。以完全相同的深度绘制两个对象可能是深度测试的问题。您可能记得从 3.4.1小节 OpenGL 使用多边形偏移来解决这个问题。在 three.js 中,您可以将多边形偏移应用于材料。在演示中,这是为同时显示线框材料的实体材料完成的。例如,

mat = new THREE.MeshLambertMaterial({
    polygonOffset: true,
    polygonOffsetUnits: 1,
    polygonOffsetFactor: 1,
    color: "yellow",
    side: THREE.DoubleSide
});

这里显示的 polygonOffsetpolygonOffsetUnitspolygonOffsetFactor 设置将略微增加使用此材料的物体的深度,以便它不与同一物体的线框版本发生干扰。

最后一点:您并不总是需要制作新的材料和几何体来制作新对象。您可以在多个对象中重用相同的材料和几何体。

A visible object in three.js is made up of either points, lines, or triangles. An individual object corresponds to an OpenGL primitive such as GL_POINTS, GL_LINES, or GL_TRIANGLES (see Subsection 3.1.1). There are five classes to represent these possibilities: THREE.Points for points, THREE.Mesh for triangles, and three classes for lines: THREE.Line, which uses the GL_LINE_STRIP primitive; THREE.LineSegments, which uses the GL_LINES primitive; and THREE.LineLoop, which uses the GL_LINE_LOOP primitive.

A visible object is made up of some geometry plus a material that determines the appearance of that geometry. In three.js, the geometry and material of a visible object are themselves represented by JavaScript classes THREE.BufferGeometry and THREE.Material.

An object of type THREE.BufferGeometry can store vertex coordinates and their attributes. (In fact, the vertex coordinates are also considered to be an "attribute" of the geometry.) These values must be stored in a form suitable for use with the OpenGL functions glDrawArrays and glDrawElements (see Subsection 3.4.2). For JavaScript, this means that they must be stored in typed arrays. A typed array is similar to a normal JavaScript array, except that its length is fixed and it can only hold numerical values of a certain type. For example, a Float32Array holds 32-bit floating point numbers, and a UInt16Array holds unsigned 16-bit integers. A typed array can be created with a constructor that specifies the length of the array. For example,

vertexCoords = new Float32Array(300);  // Space for 300 numbers.

Alternatively, the constructor can take an ordinary JavaScript array of numbers as its parameter. This creates a typed array that holds the same numbers. For example,

data = new Float32Array( [ 1.3, 7, -2.89, 0, 3, 5.5 ] );

In this case, the length of data is six, and it contains copies of the numbers from the JavaScript array.

Specifying the vertices for a BufferGeometry is a multistep process. You need to create a typed array containing the coordinates of the vertices. Then you need to wrap that array inside an object of type THREE.BufferAttribute. Finally, you can add the attribute to the geometry. Here is an example:

let vertexCoords = new Float32Array([ 0,0,0, 1,0,0, 0,1,0 ]);
let vertexAttrib = new THREE.BufferAttribute(vertexCoords, 3);
let geometry = new THREE.BufferGeometry();
geometry.setAttribute( "position", vertexAttrib );

The second parameter to the BufferGeometry constructor is an integer that tells three.js the number of coordinates of each vertex. Recall that a vertex can be specified by 2, 3, or 4 coordinates, and you need to specify how many numbers are provided in the array for each vertex. Turning to the setAttribute() function, a BufferGeometry can have attributes specifying color, normal vectors, and texture coordinates, as well as other custom attributes. The first parameter to setAttribute() is the name of the attribute. Here, "position" is the name of the attribute that specifies the coordinates, or position, of the vertices.

Similarly, to specify a color for each vertex, you can put the RGB components of the colors into a Float32Array, and use that to specify a value for the BufferGeometry attribute named "color".

For a specific example, suppose that we want to represent a primitive of type GL_POINTS, using a three.js object of type THREE.Points. Let's say we want 10000 points placed at random inside the unit sphere, where each point can have its own random color. Here is some code that creates the necessary BufferGeometry:

let pointsBuffer = new Float32Array( 30000 );  // 3 numbers per vertex!
let colorBuffer = new Float32Array( 30000 );
let i = 0;
while ( i < 10000 ) {
    let x = 2*Math.random() - 1;
    let y = 2*Math.random() - 1;
    let z = 2*Math.random() - 1;
    if ( x*x + y*y + z*z < 1 ) {  
            // only use points inside the unit sphere
        pointsBuffer[3*i] = x;
        pointsBuffer[3*i+1] = y;
        pointsBuffer[3*i+2] = z;
        colorBuffer[3*i] = 0.25 + 0.75*Math.random();
        colorBuffer[3*i+1] = 0.25 + 0.75*Math.random();
        colorBuffer[3*i+2] = 0.25 + 0.75*Math.random();
        i++;
    }
}
let pointsGeom = new THREE.BufferGeometry();
pointsGeom.setAttribute("position", 
                        new THREE.BufferAttribute(pointsBuffer,3));
pointsGeom.setAttribute("color", 
                        new THREE.BufferAttribute(colorBuffer,3));

In three.js, to make some geometry into a visible object, we also need an appropriate material. For example, for an object of type THREE.Points, we can use a material of type THREE.PointsMaterial, which is a subclass of Material. The material can specify the color and the size of the points, among other properties:

let pointsMat = new THREE.PointsMaterial( {
            color: "yellow",
            size: 2,
            sizeAttenuation: false
        } );

The parameter to the constructor is a JavaScript object whose properties are used to initialize the material. With the sizeAttenuation property set to false, the size is given in pixels; if it is true, then size represents the size in world coordinates and the point is scaled to reflect distance from the viewer. If the color is omitted, a default value of white is used. The default for size is 1 and for sizeAttenuation is true. The parameter to the constructor can be omitted entirely, to use all the defaults. A PointsMaterial is not affected by lighting; it simply shows the color specified by its color property.

It is also possible to assign values to properties of the material after the object has been created. For example,

let pointsMat = new THREE.PointsMaterial();
pointsMat.color = new THREE.Color("yellow");
pointsMat.size = 2;
pointsMat.sizeAttenuation = false;

Note that the color is set as a value of type THREE.Color, which is constructed from a string, "yellow". When the color property is set in the material constructor, the same conversion of string to color is done automatically.

Once we have the geometry and the material, we can use them to create the visible object, of type THREE.Points, and add it to a scene:

let sphereOfPoints = new THREE.Points( pointsGeom, pointsMat );
scene.add( sphereOfPoints );

This will show a cloud of yellow points. But we wanted each point to have its own color! Recall that the colors for the points are stored in the geometry, not in the material. We have to tell the material to use the colors from the geometry, not the material's own color property. This is done by setting the value of the material property vertexColors to true. So, we could create the material using

let pointsMat = new THREE.PointsMaterial( {
            color: "white",
            size: 2,
            sizeAttenuation: false,
            vertexColors: true
        } );

White is used here as the material color because the vertex colors are actually multiplied by the material color, not simply substituted for it.

The following demo shows a point cloud. You can control whether the points are all yellow or are randomly colored. You can animate the points, and you can control the size and number of points. Note that points are rendered as squares.


The color parameter in the above material was specified by the string "yellow". Colors in three.js can be represented by values of type THREE.Color. The class THREE.Color represents an RGB color. A Color object c has properties c.r, c.g, and c.b giving the red, blue, and green color components as floating point numbers in the range from 0.0 to 1.0. Note that there is no alpha component; three.js handles transparency separately from color.

There are several ways to construct a THREE.Color object. The constructor can take three parameters giving the RGB components as real numbers in the range 0.0 to 1.0. It can take a single string parameter giving the color as a CSS color string, like those used in the 2D canvas graphics API; examples include "white", "red", "rgb(255,0,0)", and "#FF0000". Or the color constructor can take a single integer parameter in which each color component is given as an eight-bit field in the integer. Usually, an integer that is used to represent a color in this way is written as a hexadecimal literal, beginning with "0x". Examples include 0xff0000 for red, 0x00ff00 for green, 0x0000ff for blue, and 0x007050 for a dark blue-green. Here are some examples of using color constructors:

let c1 = new THREE.Color("skyblue");
let c2 = new THREE.Color(1,1,0);  // yellow
let c3 = new THREE.Color(0x98fb98);  // pale green

In many contexts, such as the THREE.Points constructor, three.js will accept a string or integer where a color is required; the string or integer will be fed through the Color constructor. As another example, a WebGLRenderer object has a "clear color" property that is used as the background color when the renderer renders a scene. This property could be set using any of the following commands:

renderer.setClearColor( new THREE.Color(0.6, 0.4, 0.1) );
renderer.setClearColor( "darkgray" );
renderer.setClearColor( 0x99BBEE );

Turning next to lines, an object of type THREE.Line represents a line strip—what would be a primitive of the type called GL_LINE_STRIP in OpenGL. To get the same strip of connected line segments, plus a line back to the starting vertex, we can use an object of type THREE.LineLoop. For the outline of a triangle, for example, we could provide a BufferGeometry holding coordinates for three points and use a LineLoop.

We will also need a material. For lines, the material can be represented by an object of type THREE.LineBasicMaterial. As usual, the parameter for the constructor is a JavaScript object, whose properties can include color and linewidth. For example:

let lineMat = new THREE.LineBasicMaterial( {
    color:  0xA000A0,  // purple; the default is white
    linewidth: 2       // 2 pixels; the default is 1
} );

(The linewidth property might not be respected. According to the specification, a WebGL implementation can set the maximum line width to 1.)

As with points, it is possible to specify a different color for each purpose by adding a "color" attribute to the geometry and setting the value of the vertexColors material property to true. Here is a complete example that makes a triangle with vertices colored red, green, and blue:

let positionBuffer = new Float32Array([
        -2, -2,   // Coordinates for first vertex.
        2, -2,   // Coordinates for second vertex.
        0,  2    // Coordinates for third vertex.
    ]);
let colorBuffer = new Float32Array([
        1, 0, 0,  // Color for first vertex (red).
        0, 1, 0,  // Color for second vertex (green).
        0, 0, 1   // Color for third vertex (blue).
]);    
let lineGeometry = new THREE.BufferGeometry();
lineGeometry.setAttribute(
        "position",
        new THREE.BufferAttribute(positionBuffer,2)
    );
lineGeometry.setAttribute(
        "color",
        new THREE.BufferAttribute(colorBuffer,3)
    );
let lineMaterial = new THREE.LineBasicMaterial( {
        linewidth: 3,
        vertexColors: true
    } );
let triangle = new THREE.LineLoop( lineGeometry, lineMaterial );
scene.add(triangle);

This produces the image:

123

The "Basic" in LineBasicMaterial indicates that this material uses basic colors that do not require lighting to be visible and are not affected by lighting. This is generally what you want for lines.


A mesh object in three.js corresponds to the OpenGL primitive GL_TRIANGLES. The geometry object for a mesh must specify which vertices are part of which triangles. We will see how to do that in the next section. However, three.js comes with classes to represent common mesh geometries, such as a sphere, a cylinder, and a torus. For these built-in classes, you just need to call a constructor to create the appropriate geometry. For example, the class THREE.CylinderGeometry represents the geometry for a cylinder, and its constructor takes the form

new THREE.CylinderGeometry(radiusTop, radiusBottom, height,
        radiusSegments, heightSegments, openEnded, thetaStart, thetaLength)

The geometry created by this constructor represents an approximation for a cylinder that has its axis lying along the y-axis. It extends from −height/2 to height/2 along that axis. The radius of its circular top is radiusTop and of its bottom is radiusBottom. The two radii don't have to be the same; when the are different, you get a truncated cone rather than a cylinder as such. Using a value of zero for radiusTop makes an actual cone. The parameters radiusSegments and heightSegments give the number of subdivisions around the circumference of the cylinder and along its length respectively—what are called slices and stacks in the GLUT library for OpenGL. The parameter openEnded is a boolean value that indicates whether the top and bottom of the cylinder are to be drawn; use the value true to get an open-ended tube. Finally, the last two parameters allow you to make a partial cylinder. Their values are given as angles, measured in radians, about the y-axis. Only the part of the cylinder beginning at thetaStart and ending at thetaStart plus thetaLength is rendered. For example, if thetaLength is Math.PI, you will get a half-cylinder.

The large number of parameters to the constructor gives a lot of flexibility. The parameters are all optional. The default value for each of the first three parameters is one. The default for radiusSegments is 8, which gives a poor approximation for a smooth cylinder. Leaving out the last three parameters will give a complete cylinder, closed at both ends.

Other standard mesh geometries are similar. Here are some constructors, listing all parameters (but keep in mind that most of the parameters are optional):

new THREE.BoxGeometry(width, height, depth,
                        widthSegments, heightSegments, depthSegments)

new THREE.PlaneGeometry(width, height, widthSegments, heightSegments)

new THREE.RingGeometry(innerRadius, outerRadius, thetaSegments, phiSegments,
                        thetaStart, thetaLength)

new THREE.ConeGeometry(radiusBottom, height, radiusSegments, 
                        heightSegments, openEnded, thetaStart, thetaLength)

new THREE.SphereGeometry(radius, widthSegments, heightSegments,
                        phiStart, phiLength, thetaStart, thetaLength)

new THREE.TorusGeometry(radius, tube, radialSegments, tubularSegments, arc)

The class BoxGeometry represents the geometry of a rectangular box centered at the origin. Its constructor has three parameters to give the size of the box in each direction; their default value is one. The last three parameters give the number of subdivisions in each direction, with a default of one; values greater than one will cause the faces of the box to be subdivided into smaller triangles.

The class PlaneGeometry represents the geometry of a rectangle lying in the xy-plane, centered at the origin. Its parameters are similar to those for a cube. A RingGeometry represents an annulus, that is, a disk with a smaller disk removed from its center. The ring lies in the xy-plane, with its center at the origin. You should always specify the inner and outer radii of the ring.

The constructor for ConeGeometry has exactly the same form and effect as the constructor for CylinderGeometry, with the radiusTop set to zero. That is, it constructs a cone with axis along the y-axis and centered at the origin.

For SphereGeometry, all parameters are optional. The constructor creates a sphere centered at the origin, with axis along the y-axis. The first parameter, which gives the radius of the sphere, has a default of one. The next two parameters give the numbers of slices and stacks, with default values 32 and 16. The last four parameters allow you to make a piece of a sphere; the default values give a complete sphere. The four parameters are angles measured in radians. phiStart and phiLength are measured in angles around the equator and give the extent in longitude of the spherical shell that is generated. For example,

new THREE.SphereGeometry( 5, 32, 16, 0, Math.PI )

creates the geometry for the "western hemisphere" of a sphere. The last two parameters are angles measured along a line of latitude from the north pole of the sphere to the south pole. For example, to get the sphere's "northern hemisphere":

new THREE.SphereGeometry( 5, 32, 16, 0, 2*Math.PI, 0, Math.PI/2 )

For TorusGeometry, the constructor creates a torus lying in the xy-plane, centered at the origin, with the z-axis passing through its hole. The parameter radius is the distance from the center of the torus to the center of the torus's tube, while tube is the radius of the tube. The next two parameters give the number of subdivisions in each direction. The last parameter, arc, allows you to make just part of a torus. It is an angle between 0 and 2*Math.PI, measured along the circle at the center of the tube.

There are also geometry classes representing the regular polyhedra: THREE.TetrahedronGeometry, THREE.OctahedronGeometry, THREE.DodecahedronGeometry, and THREE.IcosahedronGeometry. (For a cube use a BoxGeometry.) The constructors for these four classes take two parameters. The first specifies the size of the polyhedron, with a default of 1. The size is given as the radius of the sphere that contains the polyhedron. The second parameter is an integer called detail. The default value, 0, gives the actual regular polyhedron. Larger values add detail by adding additional faces. As the detail increases, the polyhedron becomes a better approximation for a sphere. This is easier to understand with an illustration:

123

The image shows four mesh objects that use icosahedral geometries with detail parameter equal to 0, 1, 2, and 3.


To create a mesh object, you need a material as well as a geometry. There are several kinds of material suitable for mesh objects, including THREE.MeshBasicMaterial, THREE.MeshLambertMaterial, and THREE.MeshPhongMaterial. (There are more mesh materials, including two newer ones, THREE.MeshStandardMaterial and THREE.MeshPhysicalMaterial, that implement techniques associated with physically based rendering, an approach to improved rendering that has become popular. However, I will not cover them here.)

A MeshBasicMaterial represents a color that is not affected by lighting; it looks the same whether or not there are lights in the scene, and it is not shaded, giving it a flat rather than 3D appearance. The other two classes represent materials that need to be lit to be seen. They implement models of lighting known as Lambert shading and Phong shading. The major difference is that MeshPhongMaterial has a specular color but MeshLambertMaterial** does not. Both can have diffuse and emissive colors. For all three material classes, the constructor has one parameter, a JavaScript object that specifies values for properties of the material. For example:

let mat = new THREE.MeshPhongMaterial( {
        color: 0xbbbb00,     // reflectivity for diffuse and ambient light
        emissive: 0,         // emission color; this is the default (black)
        specular: 0x303030,  // reflectivity for specular light
        shininess: 50        // controls size of specular highlights
    } );

This example shows the four color parameters for a Phong material. The parameters have the same meaning as the five material properties in OpenGL (Subsection 4.1.1). A Lambert material lacks specular and shininess, and a basic mesh material has only the color parameter.

There are a few other material properties that you might need to set in the constructor. Except for flatShading, these apply to all three kinds of mesh material:

  • vertexColors — a boolean property that can be set to true to use vertex colors from the geometry. The default is false.
  • wireframe — a boolean value that indicates whether the mesh should be drawn as a wireframe model, showing only the outlines of its faces. The default is false. A true value works best with MeshBasicMaterial.
  • wireframeLinewidth — the width of the lines used to draw the wireframe, in pixels. The default is 1. (Non-default values might not be respected.)
  • visible — a boolean value that controls whether the object on which it is used is rendered or not, with a default of true.
  • side — has value THREE.FrontSide, THREE.BackSide, or THREE.DoubleSide, with the default being THREE.FrontSide. This determines whether faces of the mesh are drawn or not, depending on which side of the face is visible. With the default value, THREE.FrontSide, a face is drawn only if it is being viewed from the front. THREE.DoubleSide will draw it whether it is viewed from the front or from the back, and THREE.BackSide only if it is viewed from the back. For closed objects, such as a cube or a complete sphere, the default value makes sense, at least as long as the viewer is outside of the object. For a plane, an open tube, or a partial sphere, the value should be set to THREE.DoubleSide. Otherwise, parts of the object that should be in view won't be drawn.
  • flatShading — a boolean value, with the default being false. This does not work for MeshBasicMaterial. For an object that is supposed to look "faceted," with flat sides, it is important to set this property to true. That would be the case, for example, for a cube or for a cylinder with a small number of sides.

As an example, let's make a shiny, blue-green, open, five-sided tube with flat sides:

let mat = new THREE.MeshPhongMaterial( {
    color: 0x0088aa, 
    specular: 0x003344, 
    shininess: 100,
    flatShading: true,  // for flat-looking sides
    side: THREE.DoubleSide  // for drawing the inside of the tube
    } );
let geom = new THREE.CylinderGeometry(3,3,10,5,1,true);
let obj = new THREE.Mesh(geom,mat);
scene.add(obj);

You can use the following demo to view several three.js mesh objects, using a variety of geometries and materials. Drag your mouse on the object to rotate it. You can also explore the level of detail for the regular polyhedron geometries.

The demo can show a wireframe version of an object overlaid on a solid version. In three.js, the wireframe and solid versions are actually two objects that use the same geometry but different materials. Drawing two objects at exactly the same depth can be a problem for the depth test. You might remember from Subsection 3.4.1 that OpenGL uses polygon offset to solve the problem. In three.js, you can apply polygon offset to a material. In the demos, this is done for the solid materials that are shown at the same time as wireframe materials. For example,

mat = new THREE.MeshLambertMaterial({
    polygonOffset: true,  
    polygonOffsetUnits: 1,
    polygonOffsetFactor: 1,
    color: "yellow",
    side: THREE.DoubleSide
});

The settings shown here for polygonOffset, polygonOffsetUnits, and polygonOffsetFactor will increase the depth of the object that uses this material slightly so that it doesn't interfere with the wireframe version of the same object.

One final note: You don't always need to make new materials and geometries to make new objects. You can reuse the same materials and geometries in multiple objects.

5.1.5 灯光

Lights

与几何体和材料相比,灯光就容易多了!Three.js 有几个类来表示灯光。灯光类是 THREE.Object3D 的子类。一个灯光对象可以被添加到场景中,然后照亮场景中的对象。我们将看看 定向光点光环境光聚光灯

THREE.DirectionalLight 表示从给定方向平行照射的光,就像太阳光一样。定向光的位置属性给出了光线照射的方向。(这是所有场景图对象都有的相同 位置 属性,类型为 Vector3,但对于定向光,其含义不同。)请注意,光线从给定位置向原点照射。默认位置是向量 (0,1,0),它提供了沿 y 轴向下照射的光。这个类的构造函数有两个参数:

new THREE.DirectionalLight(color, intensity)

其中 color 指定光的颜色,可以是 THREE.Color 对象,或十六进制整数,或 CSS 颜色字符串。与 OpenGL 中不同,灯光没有单独的散射和镜面颜色。intensity 是一个非负数,控制光的亮度,较大的值使光更亮。强度为零的光根本不发光。参数是可选的。颜色的默认值为白色 (0xffffff),intensity 的默认值为 1。强度可以大于 1,但通常最好小于 1,以避免场景中的照明过强。

假设我们在正 z 轴上有一个相机,朝向原点,我们希望一个与相机朝向相同方向的光线。我们可以使用位置在正 z 轴上的定向光:

let light = new THREE.DirectionalLight(); // 默认白色光
light.position.set(0, 0, 1);
scene.add(light);

THREE.PointLight 表示从点向所有方向照射的光。该点的位置由光的 position 属性给出。构造函数有三个可选参数:

new THREE.PointLight(color, intensity, cutoff)

前两个参数与定向光相同,默认值也相同。cutoff 是一个非负数。如果值为零(这也是默认值),那么光的照明范围延伸到无限远,并且强度不会随着距离而减少。虽然这在物理上不现实,但通常在实践中效果良好。如果 cutoff 大于零,那么光的强度从光的位置的最大值下降到距离光的 cutoff 距离处的强度为零;光对距离大于 cutoff 的物体没有影响。这种光强度随距离减小被称为光源的 衰减

第三种类型的光是 THREE.AmbientLight。这个类的存在是为了向场景添加环境光。环境光只有颜色:

new THREE.AmbientLight(color)

向场景中添加环境光对象会向场景中添加指定颜色的环境光。环境光的颜色分量应该相当小,以避免冲淡物体的颜色。

例如,假设我们在 (10,30,15) 处想要一个随着距离减小的黄光点光,范围到 100 单位的距离。我们还想向场景中添加一些黄色的环境光:

let light = new THREE.PointLight(0xffffcc, 1, 100);
light.position.set(10, 30, 15);
scene.add(light);
scene.add(new THREE.AmbientLight(0x111100));

第四种类型的光,THREE.SpotLight,对我们来说是新事物。该类型的对象表示一个 聚光灯,它类似于点光,只是它不是向所有方向发光,而是只产生一个光锥。光锥的顶点位于光的位置。默认情况下,光锥的轴线从该位置指向原点(所以除非你改变轴线的方向,否则应该将光的位置从原点移开)。构造函数为点光添加了两个参数:

new THREE.SpotLight(color, intensity, cutoff, coneAngle, exponent)

coneAngle 是一个介于 0 和 Math.PI/2 之间的数字,它决定了光锥的大小。它是光锥轴线与光锥侧面之间的角度。默认值是 Math.PI/3exponent 是一个非负数,它决定了当你从光锥轴线向侧面移动时,光强度减少的速度。默认值 10 给出了合理的结果。exponent 为零则根本没有衰减,以至于与轴线距离不同的物体被均匀照明。

设置 three.js 聚光灯方向的技术有点奇怪,但它确实使控制方向变得容易。类型为 SpotLight 的对象 spot 有一个名为 spot.target 的属性。目标是场景图节点。聚光灯的光锥指向从聚光灯位置到目标位置的方向。当聚光灯首次创建时,其目标是一个新的、空的 Object3D,位置在 (0,0,0)。然而,你可以将目标设置为场景图中的任何对象,这将使聚光灯朝向该对象发光。为了让 three.js 计算聚光灯方向,目标的位置除了原点外,实际上必须是场景图中的节点。例如,假设我们想要一个位于点 (0,0,5) 并指向点 (2,2,0) 的聚光灯:

spotlight = new THREE.SpotLight();
spotlight.position.set(0, 0, 5);
spotlight.target.position.set(2, 2, 0);
scene.add(spotlight);
scene.add(spotlight.target);

Compared to geometries and materials, lights are easy! Three.js has several classes to represent lights. Light classes are subclasses of THREE.Object3D. A light object can be added to a scene and will then illuminate objects in the scene. We'll look at directional lights, point lights, ambient lights, and spotlights.

The class THREE.DirectionalLight represents light that shines in parallel rays from a given direction, like the light from the sun. The position property of a directional light gives the direction from which the light shines. (This is the same position property, of type Vector3, that all scene graph objects have, but the meaning is different for directional lights.) Note that the light shines from the given position towards the origin. The default position is the vector (0,1,0), which gives a light shining down the y-axis. The constructor for this class has two parameters:

new THREE.DirectionalLight( color, intensity )

where color specifies the color of the light, given as a THREE.Color object, or as a hexadecimal integer, or as a CSS color string. Lights do not have separate diffuse and specular colors, as they do in OpenGL. The intensity is a non-negative number that controls the brightness of the light, with larger values making the light brighter. A light with intensity zero gives no light at all. The parameters are optional. The default for color is white (0xffffff) and for intensity is 1. The intensity can be greater than 1, but values less than 1 are usually preferable, to avoid having too much illumination in the scene.

Suppose that we have a camera on the positive z-axis, looking towards the origin, and we would like a light that shines in the same direction that the camera is looking. We can use a directional light whose position is on the positive z-axis:

let light = new THREE.DirectionalLight(); // default white light
light.position.set( 0, 0, 1 );
scene.add(light);

The class THREE.PointLight represents a light that shines in all directions from a point. The location of the point is given by the light's position property. The constructor has three optional parameters:

new THREE.PointLight( color, intensity, cutoff )

The first two parameters are the same as for a directional light, with the same defaults. The cutoff is a non-negative number. If the value is zero—which is the default—then the illumination from the light extends to infinity, and intensity does not decrease with distance. While this is not physically realistic, it generally works well in practice. If cutoff is greater than zero, then the intensity falls from a maximum value at the light's position down to an intensity of zero at a distance of cutoff from the light; the light has no effect on objects that are at a distance greater than cutoff. This falloff of light intensity with distance is referred to as attenuation of the light source.

A third type of light is THREE.AmbientLight. This class exists to add ambient light to a scene. An ambient light has only a color:

new THREE.AmbientLight( color )

Adding an ambient light object to a scene adds ambient light of the specified color to the scene. The color components of an ambient light should be rather small to avoid washing out colors of objects.

For example, suppose that we would like a yellowish point light at (10,30,15) whose illumination falls off with distance from that point, out to a distance of 100 units. We also want to add a bit of yellow ambient light to the scene:

let light = new THREE.PointLight( 0xffffcc, 1, 100 );
light.position.set( 10, 30, 15 );
scene.add(light);
scene.add( new THREE.AmbientLight(0x111100) );

The fourth type of light, THREE.SpotLight, is something new for us. An object of that type represents a spotlight, which is similar to a point light, except that instead of shining in all directions, a spotlight only produces a cone of light. The vertex of the cone is located at the position of the light. By default, the axis of the cone points from that location towards the origin (so unless you change the direction of the axis, you should move the position of the light away from the origin). The constructor adds two parameters to those for a point light:

new THREE.SpotLight( color, intensity, cutoff, coneAngle, exponent )

The coneAngle is a number between 0 and Math.PI/2 that determines the size of the cone of light. It is the angle between the axis of the cone and the side of the cone. The default value is Math.PI/3. The exponent is a non-negative number that determines how fast the intensity of the light decreases as you move from the axis of the cone toward the side. The default value, 10, gives a reasonable result. An exponent of zero gives no falloff at all, so that objects at all distances from the axis are evenly illuminated.

The technique for setting the direction of a three.js spotlight is a little odd, but it does make it easy to control the direction. An object spot of type SpotLight has a property named spot.target. The target is a scene graph node. The cone of light from the spotlight is pointed in the direction from spotlight's position towards the target's position. When a spotlight is first created, its target is a new, empty Object3D, with position at (0,0,0). However, you can set the target to be any object in the scene graph, which will make the spotlight shine towards that object. For three.js to calculate the spotlight direction, a target whose position is anything other than the origin must actually be a node in the scene graph. For example, suppose we want a spotlight located at the point (0,0,5) and pointed towards the point (2,2,0):

spotlight = new THREE.SpotLight();
spotlight.position.set(0,0,5);
spotlight.target.position.set(2,2,0);
scene.add(spotlight);
scene.add(spotlight.target);

5.1.6 建模示例

A Modeling Example

在本章的剩余部分,我们将更深入地了解 three.js,但您已经足够了解如何从基本的几何对象构建 3D 模型。示例程序 threejs/diskworld-1 展示了一个非常简单的模型,一辆汽车在圆柱形基座的边缘行驶。汽车的轮胎是旋转的。磁盘世界在下面的左图中显示。右图显示了汽车的一个轴,每个端点都有一个轮胎。

Diskworld-1

我将讨论一些用于构建这些模型的代码。如果您想尝试用自己的模型进行实验,可以使用程序 threejs/modeling-starter.html 作为起点。

从简单的东西开始,我们来看看如何从棕色圆柱体和绿色圆锥体制作一棵树。我使用 Object3D 来表示整棵树,这样我可以将其作为一个单元处理。这两个几何对象被添加为 Object3D 的子节点。

let tree = new THREE.Object3D();

let trunk = new THREE.Mesh(
    new THREE.CylinderGeometry(0.2,0.2,1,16,1),
    new THREE.MeshLambertMaterial({
        color: 0x885522
    })
);
trunk.position.y = 0.5;  // 将底座向上移动到原点

let leaves = new THREE.Mesh(
    new THREE.ConeGeometry(.7,2,16,3),
    new THREE.MeshPhongMaterial({
        color: 0x00BB00,
        specular: 0x002000,
        shininess: 5
    })
);
leaves.position.y = 2;  // 将圆锥底部移动到树干顶部

tree.add(trunk);
tree.add(leaves);

树干是一个高度等于 1 的圆柱体。它的轴线沿着 y 轴,并且以原点为中心。磁盘世界的平面位于 xz 平面上,所以我想将树干的底部移动到该平面上。这是通过设置 trunk.position.y 的值来完成的,它表示要应用于树干的平移。记住,对象有自己的建模坐标系。指定变换的对象属性,如 trunk.position,在该坐标系中变换对象。在这种情况下,树干是一个更大的复合对象的一部分,代表整棵树。在渲染场景时,树干首先通过它自己的建模变换进行变换。然后,它会被应用于整个树的任何建模变换进一步变换。(这种分层建模首次在 2.4.1小节 中介绍。)

一旦我们有了树对象,就可以将其添加到代表磁盘世界的模型中。在程序中,模型是一个名为 diskworldModelObject3D 类型的对象。模型将包含几棵树,但树不必单独构建。我可以通过克隆已经创建的树来制作更多的树。例如:

tree.position.set(-1.5,0,2);
tree.scale.set(0.7,0.7,0.7);
diskworldModel.add( tree.clone() );

tree.position.set(-1,0,5.2);
tree.scale.set(0.25,0.25,0.25);
diskworldModel.add( tree.clone() );

这将两棵树添加到模型中,具有不同的大小和位置。当树被克隆时,克隆得到它自己的建模变换属性 positionscale 的副本。更改原始树对象中这些属性的值不会影响克隆。

让我们转向一个更复杂的对象,轴和轮子。我开始创建一个轮子,使用圆环面作为轮胎,并使用三个圆柱体的副本作为辐条。在这种情况下,我不是制作一个新的 Object3D 来容纳轮子的所有组件,而是将圆柱体作为圆环的子节点添加。记住,three.js 中的任何屏幕图节点都可以有子节点。

let wheel = new THREE.Mesh(  // 轮胎;辐条将作为子节点添加
    new THREE.TorusGeometry(0.75, 0.25, 16, 32),
    new THREE.MeshLambertMaterial({ color: 0x0000A0 })
);
let yellow = new THREE.MeshPhongMaterial({
        color: 0xffff00,
        specular: 0x101010,
        shininess: 16
    });
let cylinder = new THREE.Mesh(  // 高度为 1,直径为 1 的圆柱体
    new THREE.CylinderGeometry(0.5,0.5,1,32,1),
    yellow
);

cylinder.scale.set(0.15,1.2,0.15); // 使其变细变长,用于辐条。

wheel.add( cylinder.clone() );  // 添加圆柱体的副本。
cylinder.rotation.z = Math.PI/3;  // 旋转它用于第二个辐条。
wheel.add( cylinder.clone() );
cylinder.rotation.z = -Math.PI/3; // 旋转它用于第三个辐条。
wheel.add( cylinder.clone() );

一旦我有了轮子模型,我可以使用它和一个圆柱体来制作轴。对于轴,我使用一个沿着 z 轴的圆柱体。轮子位于 xy 平面上。它面向正确的方向,但它位于轴的中心。要将其移动到轴末端的正确位置,只需沿 z 轴平移即可。

axleModel = new THREE.Object3D(); // 包含两个轮子和一个轴的模型。
cylinder.scale.set(0.2,4.3,0.2);  // 将圆柱体缩放,用作轴。
cylinder.rotation.set(Math.PI/2,0,0); // 将其轴旋转到 z 轴上。
axleModel.add( cylinder );
wheel.position.z = 2;  // 轮子位于轴的两端。
axleModel.add( wheel.clone() );
wheel.position.z = -2;
axleModel.add( wheel );

请注意,对于第二个轮子,我添加了原始轮子模型而不是克隆。没有必要制作额外的副本。有了 axleModel,我可以从两个轴的副本和其他一些组件构建汽车。

磁盘世界可以被动画化。要实现动画,需要在渲染动画的每一帧之前修改适当场景图节点的属性。例如,要使汽车上的轮子旋转,每帧都会增加每个轴围绕其 z 轴的旋转:

carAxle1.rotation.z += 0.05;
carAxle2.rotation.z += 0.05;

这改变了将应用于轴渲染时的建模变换。在它自己的坐标系中,轴的中心轴线沿着 z 轴。围绕 z 轴的旋转旋转了轴及其附着的轮胎,围绕其轴线。

有关示例程序的详细信息,请参见 源代码

In the rest of this chapter, we will go much deeper into three.js, but you already know enough to build 3D models from basic geometric objects. An example is in the sample program threejs/diskworld-1.html, which shows a very simple model of a car driving around the edge of a cylindrical base. The car has rotating tires. The diskworld is shown in the picture on the left below. The picture on the right shows one of the axles from the car, with a tire on each end.

123

I will discuss some of the code that is used to build these models. If you want to experiment with your own models, you can use the program threejs/modeling-starter.html as a starting point.

To start with something simple, let's look at how to make a tree from a brown cylinder and a green cone. I use an Object3D to represent the tree as a whole, so that I can treat it as a unit. The two geometric objects are added as children of the Object3D.

let tree = new THREE.Object3D();

let trunk = new THREE.Mesh(
    new THREE.CylinderGeometry(0.2,0.2,1,16,1),
    new THREE.MeshLambertMaterial({
        color: 0x885522
    })
);
trunk.position.y = 0.5;  // move base up to origin

let leaves = new THREE.Mesh(
    new THREE.ConeGeometry(.7,2,16,3),
    new THREE.MeshPhongMaterial({
        color: 0x00BB00,
        specular: 0x002000,
        shininess: 5
    })
);
leaves.position.y = 2;  // move bottom of cone to top of trunk

tree.add(trunk);
tree.add(leaves);

The trunk is a cylinder with height equal to 1. Its axis lies along the y-axis, and it is centered at the origin. The plane of the diskworld lies in the xz-plane, so I want to move the bottom of the trunk onto that plane. This is done by setting the value of trunk.position.y, which represents a translation to be applied to the trunk. Remember that objects have their own modeling coordinate system. The properties of objects that specify transformations, such as trunk.position, transform the object in that coordinate system. In this case, the trunk is part of a larger, compound object that represents the whole tree. When the scene is rendered, the trunk is first transformed by its own modeling transformation. It is then further transformed by any modeling transformation that is applied to the tree as a whole. (This type of hierarchical modeling was first covered in Subsection 2.4.1.)

Once we have a tree object, it can be added to the model that represents the diskworld. In the program, the model is an object of type Object3D named diskworldModel. The model will contain several trees, but the trees don't have to be constructed individually. I can make additional trees by cloning the one that I have already created. For example:

tree.position.set(-1.5,0,2);
tree.scale.set(0.7,0.7,0.7);
diskworldModel.add( tree.clone() );

tree.position.set(-1,0,5.2);
tree.scale.set(0.25,0.25,0.25);
diskworldModel.add( tree.clone() );

This adds two trees to the model, with different sizes and positions. When the tree is cloned, the clone gets its own copies of the modeling transformation properties, position and scale. Changing the values of those properties in the original tree object does not affect the clone.

Lets turn to a more complicated object, the axle and wheels. I start by creating a wheel, using a torus for the tire and using three copies of a cylinder for the spokes. In this case, instead of making a new Object3D to hold all the components of the wheel, I add the cylinders as children of the torus. Remember that any screen graph node in three.js can have child nodes.

let wheel = new THREE.Mesh(  // the tire; spokes will be added as children
    new THREE.TorusGeometry(0.75, 0.25, 16, 32),
    new THREE.MeshLambertMaterial({ color: 0x0000A0 })
);
let yellow = new THREE.MeshPhongMaterial({
        color: 0xffff00,
        specular: 0x101010,
        shininess: 16
    });
let cylinder = new THREE.Mesh(  // a cylinder with height 1 and diameter 1
    new THREE.CylinderGeometry(0.5,0.5,1,32,1),
    yellow
);

cylinder.scale.set(0.15,1.2,0.15); // Make it thin and tall for use as a spoke.

wheel.add( cylinder.clone() );  // Add a copy of the cylinder.
cylinder.rotation.z = Math.PI/3;  // Rotate it for the second spoke.
wheel.add( cylinder.clone() );
cylinder.rotation.z = -Math.PI/3; // Rotate it for the third spoke.
wheel.add( cylinder.clone() );

Once I have the wheel model, I can use it along with one more cylinder to make the axle. For the axle, I use a cylinder lying along the z-axis. The wheel lies in the xy-plane. It is facing in the correct direction, but it lies in the center of the axle. To get it into its correct position at the end of the axle, it just has to be translated along the z-axis.

axleModel = new THREE.Object3D(); // A model containing two wheels and an axle.
cylinder.scale.set(0.2,4.3,0.2);  // Scale the cylinder for use as an axle.
cylinder.rotation.set(Math.PI/2,0,0); // Rotate its axis onto the z-axis.
axleModel.add( cylinder );
wheel.position.z = 2;  // Wheels are positioned at the two ends of the axle.
axleModel.add( wheel.clone() );
wheel.position.z = -2;
axleModel.add( wheel );

Note that for the second wheel, I add the original wheel model rather than a clone. There is no need to make an extra copy. With the axleModel in hand, I can build the car from two copies of the axle plus some other components.

The diskworld can be animated. To implement the animation, properties of the appropriate scene graph nodes are modified before each frame of the animation is rendered. For example, to make the wheels on the car rotate, the rotation of each axle about its z-axis is increased in each frame:

carAxle1.rotation.z += 0.05;
carAxle2.rotation.z += 0.05;

This changes the modeling transformation that will be applied to the axles when they are rendered. In its own coordinate system, the central axis of an axle lies along the z-axis. The rotation about the z-axis rotates the axle, with its attached tires, about its axis.

For the full details of the sample program, see the source code.