Three-js-学习手册-全-

Three.js 学习手册(全)

原文:zh.annas-archive.org/md5/5001B8D716B9182B26C655FCB6BE8F50

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在过去的几年中,浏览器变得更加强大,并成为交付复杂应用程序和图形的平台。其中大部分是标准的 2D 图形。大多数现代浏览器已经采用了 WebGL,这使您不仅可以在浏览器中创建 2D 应用程序和图形,还可以利用 GPU 的功能创建美丽且性能良好的 3D 应用程序。

然而,直接编程 WebGL 非常复杂。您需要了解 WebGL 的内部细节,并学习复杂的着色器语言,以充分利用 WebGL。Three.js 围绕 WebGL 的功能提供了一个非常易于使用的 JavaScript API,因此您可以在不详细了解 WebGL 的情况下创建美丽的 3D 图形。

Three.js 提供了大量功能和 API,您可以使用它们直接在浏览器中创建 3D 场景。在本书中,您将通过大量互动示例和代码样本学习 Three.js 提供的所有不同 API。

本书涵盖的内容

第一章《使用 Three.js 创建您的第一个 3D 场景》介绍了开始使用 Three.js 所需的基本步骤。您将立即创建您的第一个 Three.js 场景,并在本章结束时,您将能够直接在浏览器中创建和动画化您的第一个 3D 场景。

第二章《构成 Three.js 场景的基本组件》解释了在使用 Three.js 时需要了解的基本组件。您将了解灯光、网格、几何图形、材质和摄像机。在本章中,您还将概述 Three.js 提供的不同光源和您可以在场景中使用的摄像机。

第三章《使用 Three.js 中可用的不同光源》深入探讨了您可以在场景中使用的不同光源。它展示了示例并解释了如何使用聚光灯、方向光、环境光、点光源、半球光和区域光。此外,它还展示了如何在光源上应用镜头眩光效果。

第四章《使用 Three.js 材质》讨论了 Three.js 中可用的可以在网格上使用的材质。它展示了您可以设置的所有属性,以配置用于特定用途的材质,并提供了可供在 Three.js 中使用的材质进行实验的交互式示例。

第五章《学习使用几何图形》是探索 Three.js 提供的所有几何图形的两章中的第一章。在本章中,您将学习如何在 Three.js 中创建和配置几何图形,并可以使用提供的交互式示例来尝试使用几何图形(如平面、圆形、形状、立方体、球体、圆柱体、圆环、圆环结和多面体)。

第六章《高级几何图形和二进制操作》延续了第五章《学习使用几何图形》的内容。它向您展示了如何配置和使用 Three.js 提供的更高级的几何图形,例如凸多边形和车削。在本章中,您还将学习如何从 2D 形状挤出 3D 几何图形,以及如何使用二进制操作组合几何图形来创建新的几何图形。

第七章,“粒子、精灵和点云”,解释了如何使用 Three.js 中的点云。您将学习如何从头开始创建点云以及从现有几何体创建点云。在本章中,您还将学习如何通过使用精灵和点云材质来修改单个点的外观方式。

第八章,“创建和加载高级网格和几何体”,向您展示了如何从外部来源导入网格和几何体。您将学习如何使用 Three.js 的内部 JSON 格式来保存几何体和场景。本章还解释了如何从格式如 OBJ、DAE、STL、CTM、PLY 等加载模型。

第九章,“动画和移动摄像机”,探讨了各种类型的动画,您可以使用它们使您的场景栩栩如生。您将学习如何与 Three.js 一起使用 Tween.js 库,以及如何使用基于变形和骨骼的动画模型。

第十章,“加载和使用纹理”,扩展了第四章,“使用 Three.js 材质”,在那里介绍了材质。在本章中,我们深入了解纹理的细节。本章介绍了各种可用的纹理类型以及如何控制纹理应用到网格上的方式。此外,在本章中,您将学习如何直接使用 HTML5 视频和画布元素的输出作为纹理的输入。

第十一章,“自定义着色器和渲染后处理”,探讨了如何使用 Three.js 对渲染的场景应用后处理效果。通过后处理,您可以对渲染的场景应用模糊、移轴、深褐色等效果。此外,您还将学习如何创建自己的后处理效果,并创建自定义的顶点和片段着色器。

第十二章,“向您的场景添加物理和声音”,解释了如何向 Three.js 场景添加物理效果。通过物理效果,您可以检测物体之间的碰撞,使它们对重力做出反应,并施加摩擦力。本章向您展示了如何使用 Physijs JavaScript 库来实现这一点。此外,本章还向您展示了如何向 Three.js 场景添加定位音频。

您需要为本书做好准备

您只需要一款文本编辑器(例如 Sublime)来玩弄示例,以及一款现代的网络浏览器来访问这些示例。一些示例需要本地网络服务器,但您将在第一章,“使用 Three.js 创建您的第一个 3D 场景”中学习如何设置一个非常轻量级的网络服务器,以便在本书中使用这些示例。

这本书是为谁准备的

这本书非常适合已经了解 JavaScript 并希望开始创建在任何浏览器中运行的 3D 图形的人。您不需要了解任何高级数学或 WebGL;所需的只是对 JavaScript 和 HTML 的一般了解。所需的材料和示例可以免费下载,本书中使用的所有工具都是开源的。因此,如果您想学习如何创建在任何现代浏览器中运行的美丽、交互式的 3D 图形,这本书就是为您准备的。

约定

在本书中,您将找到一些文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄都以以下方式显示:“您可以在这段代码中看到,除了设置map属性外,我们还将bumpMap属性设置为纹理。”

代码块设置如下:

function createMesh(geom, imageFile, bump) {
  var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile)
  var mat = new THREE.MeshPhongMaterial();
  mat.map = texture;
  var bump = THREE.ImageUtils.loadTexture("../assets/textures/general/" + bump)
  mat.bumpMap = bump;
  mat.bumpScale = 0.2;
  var mesh = new THREE.Mesh(geom, mat);
  return mesh;
}

当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:

var effectFilm = new THREE.FilmPass(0.8, 0.325, 256, false);
effectFilm.renderToScreen = true;

var composer4 = new THREE.EffectComposer(webGLRenderer);
**composer4.addPass(renderScene);**
composer4.addPass(effectFilm);

任何命令行输入或输出都以以下方式编写:

**# git clone https://github.com/josdirksen/learning-threejs**

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,会以这样的方式出现在文本中:“您可以通过转到首选项 | 高级并勾选在菜单栏中显示开发菜单来完成此操作。”

注意

警告或重要说明会以这样的方式显示在一个框中。

提示

提示和技巧会以这样的方式出现。

第一章:使用 Three.js 创建您的第一个 3D 场景

现代浏览器正在逐渐获得更强大的功能,可以直接从 JavaScript 中访问。您可以轻松地使用新的 HTML5 标签添加视频和音频,并通过 HTML5 画布创建交互式组件。与 HTML5 一起,现代浏览器还开始支持 WebGL。使用 WebGL,您可以直接利用图形卡的处理资源,并创建高性能的 2D 和 3D 计算机图形。直接从 JavaScript 编程 WebGL 以创建和动画 3D 场景是一个非常复杂和容易出错的过程。Three.js 是一个使这变得更容易的库。以下列表显示了 Three.js 使得易于实现的一些功能:

  • 创建简单和复杂的 3D 几何体

  • 通过 3D 场景中的动画和移动对象

  • 将纹理和材质应用到您的对象上

  • 利用不同的光源照亮场景

  • 从 3D 建模软件加载对象

  • 向您的 3D 场景添加高级后处理效果

  • 使用自定义着色器

  • 创建点云

使用几行 JavaScript 代码,您可以创建从简单的 3D 模型到逼真的实时场景的任何东西,如下图所示(通过在浏览器中打开www.vill.ee/eye/来查看):

使用 Three.js 创建您的第一个 3D 场景

在本章中,我们将直接深入了解 Three.js,并创建一些示例,向您展示 Three.js 的工作原理,并可以用来进行实验。我们不会立即深入所有技术细节;这是您将在接下来的章节中学习的内容。在本章中,我们将涵盖以下内容:

  • 使用 Three.js 所需的工具

  • 下载本书中使用的源代码和示例

  • 创建您的第一个 Three.js 场景

  • 使用材质、光线和动画改进第一个场景

  • 介绍一些辅助库,用于统计和控制场景

我们将从简短介绍 Three.js 开始这本书,然后迅速转向第一个示例和代码样本。在我们开始之前,让我们快速看看最重要的浏览器以及它们对 WebGL 的支持。

在撰写本文时,WebGL 与以下桌面浏览器兼容:

浏览器 支持
Mozilla Firefox 该浏览器自 4.0 版本起就支持 WebGL。
谷歌 Chrome 该浏览器自 9.0 版本起就支持 WebGL。
Safari 安装在 Mac OS X Mountain Lion、Lion 或 Snow Leopard 上的 Safari 版本 5.1 及更高版本支持 WebGL。确保您在 Safari 中启用了 WebGL。您可以通过转到首选项 | 高级并勾选在菜单栏中显示开发菜单来实现这一点。之后,转到开发 | 启用 WebGL
Opera 该浏览器自 12.00 版本起就支持 WebGL。您仍然需要通过打开opera:config并将WebGL启用硬件加速的值设置为1来启用此功能。之后,重新启动浏览器。
互联网浏览器 很长一段时间以来,IE 是唯一不支持 WebGL 的主要浏览器。从 IE11 开始,微软已经添加了对 WebGL 的支持。

基本上,Three.js 可以在任何现代浏览器上运行,除了较旧版本的 IE。因此,如果您想使用较旧版本的 IE,您需要采取额外的步骤。对于 IE 10 及更早版本,有iewebgl插件,您可以从github.com/iewebgl/iewebgl获取。此插件安装在 IE 10 及更早版本中,并为这些浏览器启用了 WebGL 支持。

在移动设备上也可以运行 Three.js;对 WebGL 的支持和性能会有所不同,但两者都在迅速改善:

设备 支持
Android Android 的原生浏览器不支持 WebGL,通常也缺乏对现代 HTML5 功能的支持。如果您想在 Android 上使用 WebGL,可以使用最新版本的 Chrome、Firefox 或 Opera 移动版。
IOS IOS 8 也支持 IOS 设备上的 WebGL。IOS Safari 8 版本具有出色的 WebGL 支持。
Windows mobile Windows 手机自 8.1 版本起支持 WebGL。

使用 WebGL,您可以创建在台式机和移动设备上运行非常流畅的交互式 3D 可视化。

提示

在本书中,我们将主要关注 Three.js 提供的基于 WebGL 的渲染器。然而,还有一个基于 CSS 3D 的渲染器,它提供了一个简单的 API 来创建基于 CSS 3D 的 3D 场景。使用 CSS 3D 的一个重要优势是,这个标准几乎在所有移动和桌面浏览器上都得到支持,并且允许您在 3D 空间中渲染 HTML 元素。我们将展示如何在第七章中使用 CSS 3D 浏览器,粒子、精灵和点云

在本章中,您将直接创建您的第一个 3D 场景,并且可以在之前提到的任何浏览器中运行。我们暂时不会介绍太多复杂的 Three.js 功能,但在本章结束时,您将已经创建了下面截图中可以看到的 Three.js 场景:

使用 Three.js 创建您的第一个 3D 场景

在这个第一个场景中,您将学习 Three.js 的基础知识,并创建您的第一个动画。在开始这个示例之前,在接下来的几节中,我们将首先看一下您需要轻松使用 Three.js 的工具,以及如何下载本书中展示的示例。

使用 Three.js 的要求

Three.js 是一个 JavaScript 库,因此创建 Three.js WebGL 应用程序所需的只是一个文本编辑器和一个支持的浏览器来渲染结果。我想推荐两款 JavaScript 编辑器,这是我在过去几年中开始专门使用的:

  • WebStorm:这个来自 JetBrains 指南的编辑器对编辑 JavaScript 有很好的支持。它支持代码补全、自动部署和直接从编辑器进行 JavaScript 调试。除此之外,WebStorm 还具有出色的 GitHub(和其他版本控制系统)支持。您可以从www.jetbrains.com/webstorm/下载试用版。

  • Notepad++:Notepad++是一个通用的编辑器,支持多种编程语言的代码高亮显示。它可以轻松地布局和格式化 JavaScript。请注意,Notepad++仅适用于 Windows。您可以从notepad-plus-plus.org/下载 Notepad++。

  • Sublime 文本编辑器:Sublime 是一个很棒的编辑器,对编辑 JavaScript 有很好的支持。除此之外,它还提供了许多非常有用的选择(如多行选择)和编辑选项,一旦您习惯了它们,就会提供一个非常好的 JavaScript 编辑环境。Sublime 也可以免费测试,并且可以从www.sublimetext.com/下载。

即使您不使用这些编辑器,也有很多可用的编辑器,开源和商业的,您可以用来编辑 JavaScript 并创建您的 Three.js 项目。您可能想看看的一个有趣的项目是c9.io。这是一个基于云的 JavaScript 编辑器,可以连接到 GitHub 账户。这样,您就可以直接访问本书中的所有源代码和示例,并对其进行实验。

提示

除了这些文本编辑器,您可以使用它们来编辑和实验本书中的源代码,Three.js 目前还提供了一个在线编辑器。

使用这个编辑器,您可以在threejs.org/editor/找到,可以使用图形化的方法创建 Three.js 场景。

我提到大多数现代 Web 浏览器都支持 WebGL,并且可以用于运行 Three.js 示例。我通常在 Chrome 中运行我的代码。原因是大多数情况下,Chrome 对 WebGL 有最好的支持和性能,并且具有非常好的 JavaScript 调试器。通过这个调试器,您可以快速定位问题,例如使用断点和控制台输出。这在下面的截图中有所体现。在本书中,我会给您一些关于调试器使用和其他调试技巧的指导。

使用 Three.js 的要求

现在关于 Three.js 的介绍就到此为止;让我们获取源代码并从第一个场景开始吧。

获取源代码

本书的所有代码都可以从 GitHub (github.com/)访问。GitHub 是一个在线的基于 Git 的存储库,您可以用它来存储、访问和管理源代码的版本。有几种方式可以获取源代码:

  • 克隆 Git 存储库

  • 下载并提取存档

在接下来的两段中,我们将稍微详细地探讨这些选项。

使用 Git 克隆存储库

Git 是一个开源的分布式版本控制系统,我用它来创建和管理本书中的所有示例。为此,我使用了 GitHub,一个免费的在线 Git 存储库。您可以通过github.com/josdirksen/learning-threejs浏览此存储库。

要获取所有示例,您可以使用git命令行工具克隆此存储库。为此,您首先需要为您的操作系统下载一个 Git 客户端。对于大多数现代操作系统,可以从git-scm.com下载客户端,或者您可以使用 GitHub 本身提供的客户端(适用于 Mac 和 Windows)。安装 Git 后,您可以使用它来获取本书存储库的克隆。打开命令提示符并转到您想要下载源代码的目录。在该目录中,运行以下命令:

**# git clone https://github.com/josdirksen/learning-threejs**

这将开始下载所有示例,如下截图所示:

使用 Git 克隆存储库

learning-three.js目录现在将包含本书中使用的所有示例。

下载和提取存档

如果您不想使用 Git 直接从 GitHub 下载源代码,您也可以下载一个存档。在浏览器中打开github.com/josdirksen/learning-threejs,并点击右侧的Download ZIP按钮,如下所示:

下载和提取存档

将其提取到您选择的目录中,您就可以使用所有示例了。

测试示例

现在您已经下载或克隆了源代码,让我们快速检查一下是否一切正常,并让您熟悉目录结构。代码和示例按章节组织。有两种不同的查看示例的方式。您可以直接在浏览器中打开提取或克隆的文件夹,并查看和运行特定示例,或者您可以安装本地 Web 服务器。第一种方法适用于大多数基本示例,但当我们开始加载外部资源,例如模型或纹理图像时,仅仅打开 HTML 文件是不够的。在这种情况下,我们需要一个本地 Web 服务器来确保外部资源被正确加载。在接下来的部分中,我们将解释一些不同的设置简单本地 Web 服务器的方法。如果您无法设置本地 Web 服务器但使用 Chrome 或 Firefox,我们还提供了如何禁用某些安全功能的说明,以便您甚至可以在没有本地 Web 服务器的情况下进行测试。

根据您已经安装了什么,设置本地 Web 服务器非常容易。在这里,我们列举了一些示例。根据您系统上已经安装了什么,有许多不同的方法可以做到这一点。

基于 Python 的 Web 服务器应该在大多数 Unix/Mac 系统上工作

大多数 Unix/Linux/Mac 系统已经安装了 Python。在这些系统上,您可以非常容易地启动本地 Web 服务器:

 **> python -m SimpleHTTPServer**
 **Serving HTTP on 0.0.0.0 port 8000 ...**

在您检出/下载源代码的目录中执行此操作。

如果您已经使用 Node.js,可以使用基于 npm 的 Web 服务器

如果您已经使用 Node.js 做了一些工作,那么您很有可能已经安装了 npm。使用 npm,您有两个简单的选项来设置一个快速的本地 Web 服务器进行测试。第一个选项使用http-server模块,如下所示:

 **> npm install -g http-server**
 **> http-server**
**Starting up http-server, serving ./ on port: 8080**
**Hit CTRL-C to stop the server**

或者,您还可以使用simple-http-server选项,如下所示:

**> npm install -g simple-http-server**
**> nserver**
**simple-http-server Now Serving: /Users/jos/git/Physijs at http://localhost:8000/**

然而,这种第二种方法的缺点是它不会自动显示目录列表,而第一种方法会。

Mac 和/或 Windows 的 Mongoose 便携版

如果您没有安装 Python 或 npm,那么有一个名为 Mongoose 的简单、便携式 Web 服务器可供您使用。首先,从code.google.com/p/mongoose/downloads/list下载您特定平台的二进制文件。如果您使用 Windows,将其复制到包含示例的目录中,并双击可执行文件以启动 Web 浏览器,为其启动的目录提供服务。

对于其他操作系统,您还必须将可执行文件复制到目标目录,但是不是双击可执行文件,而是必须从命令行启动它。在这两种情况下,本地 Web 服务器将在端口8080上启动。以下屏幕截图概括了本段讨论的内容:

Mac 和/或 Windows 的 Mongoose 便携版

通过单击章节,我们可以显示和访问该特定章节的所有示例。如果我在本书中讨论一个示例,我将引用特定的名称和文件夹,以便您可以直接测试和玩耍代码。

在 Firefox 和 Chrome 中禁用安全异常

如果您使用 Chrome 运行示例,有一种方法可以禁用一些安全设置,以便您可以使用 Chrome 查看示例,而无需使用 Web 服务器。要做到这一点,您必须以以下方式启动 Chrome:

  • 对于 Windows,执行以下操作:
**chrome.exe --disable-web-security**

  • 在 Linux 上,执行以下操作:
**google-chrome --disable-web-security**

  • 在 Mac OS 上,通过以下方式禁用设置:
**open -a Google\ Chrome --args --disable-web-security**

以这种方式启动 Chrome,您可以直接从本地文件系统访问所有示例。

对于 Firefox 用户,我们需要采取一些不同的步骤。打开 Firefox,在 URL 栏中键入about:config。这是您将看到的内容:

在 Firefox 和 Chrome 中禁用安全异常

在此屏幕上,点击我会小心,我保证!按钮。这将显示您可以使用的所有可用属性,以便微调 Firefox。在此屏幕上的搜索框中,键入security.fileuri.strict_origin_policy,并将其值更改为false,就像我们在以下屏幕截图中所做的那样:

在 Firefox 和 Chrome 中禁用安全异常

此时,您还可以使用 Firefox 直接运行本书提供的示例。

现在,您已经安装了 Web 服务器,或者禁用了必要的安全设置,是时候开始创建我们的第一个 Three.js 场景了。

创建 HTML 骨架

我们需要做的第一件事是创建一个空的骨架页面,我们可以将其用作所有示例的基础,如下所示:

<!DOCTYPE html>

<html>

  <head>
    <title>Example 01.01 - Basic skeleton</title>
    <script src="../libs/three.js"></script>
    <style>
      body{
        /* set margin to 0 and overflow to hidden, to use the complete page */

        margin: 0;
        overflow: hidden;
      }
    </style>
  </head>
  <body>

    <!-- Div which will hold the Output -->
    <div id="WebGL-output">
    </div>

    <!-- Javascript code that runs our Three.js examples -->
    <script>

      // once everything is loaded, we run our Three.js stuff.
      function init() {
        // here we'll put the Three.js stuff
      };
      window.onload = init;

    </script>
  </body>
</html>

提示

下载示例代码

您可以从www.packtpub.com的帐户中下载示例代码文件,用于您购买的所有 Packt Publishing 图书。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

从此列表中可以看出,骨架是一个非常简单的 HTML 页面,只有几个元素。在<head>元素中,我们加载了我们将在示例中使用的外部 JavaScript 库。对于所有示例,我们至少需要加载 Three.js 库three.js。在<head>元素中,我们还添加了几行 CSS。这些样式元素在创建全屏 Three.js 场景时移除任何滚动条。在此页面的<body>元素中,您可以看到一个单独的<div>元素。当我们编写我们的 Three.js 代码时,我们将把 Three.js 渲染器的输出指向该元素。在此页面的底部,您已经可以看到一些 JavaScript。通过将init函数分配给window.onload属性,我们确保在 HTML 文档加载完成时调用此函数。在init函数中,我们将插入所有特定于 Three.js 的 JavaScript。

Three.js 有两个版本:

  • Three.min.js:这是您在互联网上部署 Three.js 网站时通常使用的库。这是使用UglifyJS创建的 Three.js 的缩小版本,是正常 Three.js 库的四分之一大小。本书中使用的所有示例和代码都基于于 2014 年 10 月发布的 Three.js r69

  • Three.js:这是正常的 Three.js 库。我们在示例中使用这个库,因为当您能够阅读和理解 Three.js 源代码时,调试会更加容易。

如果我们在浏览器中查看此页面,结果并不令人震惊。正如您所期望的那样,您只会看到一个空白页面。

在下一节中,您将学习如何添加前几个 3D 对象并将其渲染到我们在 HTML 骨架中定义的<div>元素中。

渲染和查看 3D 对象

在这一步中,您将创建您的第一个场景,并添加一些对象和一个相机。我们的第一个示例将包含以下对象:

对象 描述
Plane 这是一个作为我们地面区域的二维矩形。在本章的第二个截图中,它被渲染为场景中间的灰色矩形。
Cube 这是一个三维立方体,我们将以红色渲染。
Sphere 这是一个三维球体,我们将以蓝色渲染。
Camera 相机决定了输出中你将看到什么。
Axes 这些是xyz轴。这是一个有用的调试工具,可以看到对象在 3D 空间中的渲染位置。x轴为红色,y轴为绿色,z轴为蓝色。

我将首先向您展示代码中的外观(带有注释的源代码可以在chapter-01/02-first-scene.html中找到),然后我将解释发生了什么:

function init() {
  var scene = new THREE.Scene();
  var camera = new THREE.PerspectiveCamera(45, window.innerWidth /window.innerHeight, 0.1, 1000);

  var renderer = new THREE.WebGLRenderer();
  renderer.setClearColorHex(0xEEEEEE);
  renderer.setSize(window.innerWidth, window.innerHeight);

  var axes = new THREE.AxisHelper(20);
  scene.add(axes);

  var planeGeometry = new THREE.PlaneGeometry(60, 20, 1, 1);
  var planeMaterial = new THREE.MeshBasicMaterial({color: 0xcccccc});
  var plane = new THREE.Mesh(planeGeometry, planeMaterial);

  plane.rotation.x = -0.5 * Math.PI;
  plane.position.x = 15
  plane.position.y = 0
  plane.position.z = 0

  scene.add(plane);

  var cubeGeometry = new THREE.BoxGeometry(4, 4, 4)
  var cubeMaterial = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true});
  var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);

  cube.position.x = -4;
  cube.position.y = 3;
  cube.position.z = 0;

  scene.add(cube);

  var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
  var sphereMaterial = new THREE.MeshBasicMaterial({color: 0x7777ff, wireframe: true});
  var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);

  sphere.position.x = 20;
  sphere.position.y = 4;
  sphere.position.z = 2;

  scene.add(sphere);

  camera.position.x = -30;
  camera.position.y = 40;
  camera.position.z = 30;
  camera.lookAt(scene.position);

  document.getElementById("WebGL-output")
    .appendChild(renderer.domElement);
    renderer.render(scene, camera);
};
window.onload = init;

如果我们在浏览器中打开此示例,我们会看到与我们的目标相似的东西(请参阅本章开头的截图),但仍有很长的路要走,如下所示:

渲染和查看 3D 对象

在我们开始让这个更加美丽之前,我将逐步向您介绍代码,以便您了解代码的作用:

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var renderer = new THREE.WebGLRenderer();
renderer.setClearColorHex()
renderer.setClearColor(new THREE.Color(0xEEEEEE));
renderer.setSize(window.innerWidth, window.innerHeight);

在示例的顶部,我们定义了scenecamerarendererscene对象是一个容器,用于存储和跟踪我们要渲染的所有对象和我们要使用的所有灯光。没有THREE.Scene对象,Three.js 就无法渲染任何东西。关于THREE.Scene对象的更多信息可以在下一章中找到。我们想要渲染的球体和立方体将在示例的后面添加到场景中。在这个第一个片段中,我们还创建了一个camera对象。camera对象定义了我们在渲染场景时会看到什么。在第二章中,Three.js 场景的基本组件,您将了解有关您可以传递给camera对象的参数的更多信息。接下来我们定义rendererrenderer对象负责根据camera对象的角度在浏览器中计算scene对象的外观。在这个示例中,我们创建了一个使用您的图形卡来渲染场景的WebGLRenderer

提示

如果您查看 Three.js 的源代码和文档(您可以在threejs.org/找到),您会注意到除了基于 WebGL 的渲染器之外,还有其他不同的渲染器可用。有一个基于画布的渲染器,甚至还有一个基于 SVG 的渲染器。尽管它们可以工作并且可以渲染简单的场景,但我不建议使用它们。它们非常消耗 CPU,并且缺乏诸如良好的材质支持和阴影等功能。

在这里,我们将renderer的背景颜色设置为接近白色(new THREE.Color(0XEEEEEE)),并使用setClearColor函数告诉renderer需要渲染的场景有多大。

到目前为止,我们有一个基本的空场景,一个渲染器和一个摄像头。然而,还没有要渲染的东西。以下代码添加了辅助轴和平面:

  var axes = new THREE.AxisHelper( 20 );
  scene.add(axes);

  var planeGeometry = new THREE.PlaneGeometry(60,20);
  var planeMaterial = new THREE.MeshBasicMaterial({color: 0xcccccc});
  var plane = new THREE.Mesh(planeGeometry,planeMaterial);

  plane.rotation.x=-0.5*Math.PI;
  plane.position.x=15
  plane.position.y=0
  plane.position.z=0
  scene.add(plane);

正如您所看到的,我们创建了一个axes对象,并使用scene.add函数将这些轴添加到我们的场景中。接下来,我们创建了平面。这是分两步完成的。首先,我们使用新的THREE.PlaneGeometry(60,20)代码定义了平面的外观。在这种情况下,它的宽度为60,高度为20。我们还需要告诉 Three.js 这个平面的外观(例如,它的颜色和透明度)。在 Three.js 中,我们通过创建一个材质对象来实现这一点。对于这个第一个示例,我们将创建一个基本材质(THREE.MeshBasicMaterial),颜色为0xcccccc。接下来,我们将这两者合并成一个名为planeMesh对象。在将plane添加到场景之前,我们需要将其放在正确的位置;我们首先围绕 x 轴旋转它 90 度,然后使用位置属性在场景中定义其位置。如果您已经对此感兴趣,请查看第二章的代码文件夹中的06-mesh-properties.html示例,该示例显示并解释了旋转和定位。然后我们需要做的就是像我们对axes所做的那样将plane添加到scene中。

cubesphere对象以相同的方式添加,但wireframe属性设置为true,告诉 Three.js 渲染线框而不是实心对象。现在,让我们继续进行这个示例的最后部分:

  camera.position.x = -30;
  camera.position.y = 40;
  camera.position.z = 30;
  camera.lookAt(scene.position);

  document.getElementById("WebGL-output")
    .appendChild(renderer.domElement);
    renderer.render(scene, camera);

在这一点上,我们想要渲染的所有元素都已经添加到了正确的位置。我已经提到相机定义了什么将被渲染。在这段代码中,我们使用xyz位置属性来定位相机,使其悬浮在我们的场景上方。为了确保相机看向我们的对象,我们使用lookAt函数将其指向我们场景的中心,默认情况下位于位置(0, 0, 0)。剩下的就是将渲染器的输出附加到我们 HTML 骨架的<div>元素上。我们使用标准的 JavaScript 来选择正确的输出元素,并使用appendChild函数将其附加到我们的div元素上。最后,我们告诉renderer使用提供的camera对象来渲染scene

在接下来的几节中,我们将通过添加光源、阴影、更多材质甚至动画使这个场景更加美观。

添加材质、光源和阴影

在 Three.js 中添加新的材质和光源非常简单,几乎与我们在上一节中解释的方式相同。我们首先通过以下方式向场景添加光源(完整的源代码请查看03-materials-light.html):

  var spotLight = new THREE.SpotLight( 0xffffff );
  spotLight.position.set( -40, 60, -10 );
  scene.add( spotLight );

THREE.SpotLight从其位置(spotLight.position.set(-40, 60, -10))照亮我们的场景。然而,如果这次渲染场景,您不会看到与上一个场景的任何不同。原因是不同的材质对光的反应不同。我们在上一个示例中使用的基本材质(THREE.MeshBasicMaterial)在场景中不会对光源产生任何影响。它们只是以指定的颜色渲染对象。因此,我们必须将planespherecube的材质更改为以下内容:

var planeGeometry = new THREE.PlaneGeometry(60,20);
var planeMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});
var plane = new THREE.Mesh(planeGeometry, planeMaterial);
...
var cubeGeometry = new THREE.BoxGeometry(4,4,4);
var cubeMaterial = new THREE.MeshLambertMaterial({color: 0xff0000});
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
...
var sphereGeometry = new THREE.SphereGeometry(4,20,20);
var sphereMaterial = new THREE.MeshLambertMaterial({color: 0x7777ff});
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);

在这段代码中,我们将对象的材质更改为MeshLambertMaterial。这种材质和MeshPhongMaterial是 Three.js 提供的在渲染时考虑光源的材质。

然而,如下截图所示的结果仍然不是我们要找的:

添加材质、光源和阴影

我们已经接近了,立方体和球看起来好多了。然而,还缺少的是阴影。

渲染阴影需要大量的计算能力,因此在 Three.js 中默认情况下禁用阴影。不过,启用它们非常容易。对于阴影,我们需要在几个地方更改源代码,如下所示:

renderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMapEnabled = true;

我们需要做的第一个更改是告诉renderer我们想要阴影。您可以通过将shadowMapEnabled属性设置为true来实现这一点。如果您查看这个更改的结果,您暂时不会注意到任何不同。这是因为我们需要明确定义哪些对象投射阴影,哪些对象接收阴影。在我们的示例中,我们希望球体和立方体在地面上投射阴影。您可以通过在这些对象上设置相应的属性来实现这一点:

plane.receiveShadow = true;
...
cube.castShadow = true;
...
sphere.castShadow = true;

现在,我们只需要做一件事就可以得到阴影了。我们需要定义我们场景中哪些光源会产生阴影。并非所有的光源都能产生阴影,您将在下一章中了解更多相关信息,但是我们在这个示例中使用的THREE.SpotLight可以。我们只需要设置正确的属性,如下代码行所示,阴影最终将被渲染出来:

spotLight.castShadow = true;

有了这个,我们得到了一个包含来自光源的阴影的场景,如下所示:

添加材质、光源和阴影

我们将添加到这个第一个场景的最后一个特性是一些简单的动画。在第九章动画和移动摄像机中,您将了解更高级的动画选项。

通过动画扩展您的第一个场景

如果我们想要对场景进行动画,我们需要做的第一件事是找到一种在特定间隔重新渲染场景的方法。在 HTML5 和相关的 JavaScript API 出现之前,做到这一点的方法是使用setInterval(function,interval)函数。使用setInterval,我们可以指定一个函数,例如,每 100 毫秒调用一次。这个函数的问题在于它不考虑浏览器中正在发生的事情。如果您正在浏览另一个标签页,这个函数仍然会每隔几毫秒触发一次。此外,setInterval与屏幕重绘不同步。这可能导致更高的 CPU 使用率和性能不佳。

介绍 requestAnimationFrame

现代浏览器幸运地有一个解决方案,使用requestAnimationFrame函数。使用requestAnimationFrame,您可以指定一个由浏览器定义的间隔调用的函数。您可以在提供的函数中进行任何绘图,浏览器将确保尽可能平滑和高效地绘制。使用这个函数非常简单(完整的源代码可以在04-materials-light-animation.html文件中找到),您只需创建一个处理渲染的函数:

function renderScene() {
  requestAnimationFrame(renderScene);
  renderer.render(scene, camera);
}

在这个renderScene函数中,我们再次调用requestAnimationFrame,以保持动画进行。我们需要在代码中改变的唯一一件事是,在我们创建完整的场景后,我们不再调用renderer.render,而是调用renderScene函数一次,以启动动画:

...
document.getElementById("WebGL-output")
  .appendChild(renderer.domElement);
renderScene();

如果您运行这个代码,与之前的例子相比,您不会看到任何变化,因为我们还没有进行任何动画。在添加动画之前,我想介绍一个小的辅助库,它可以为我们提供有关动画运行帧率的信息。这个库来自与 Three.js 相同作者,它渲染了一个小图表,显示了我们为这个动画获得的每秒帧数。

要添加这些统计信息,我们首先需要在 HTML 的<head>元素中包含库,如下所示:

<script src="../libs/stats.js"></script>

我们添加一个<div>元素,用作统计图的输出,如下所示:

<div id="Stats-output"></div>

唯一剩下的事情就是初始化统计信息并将它们添加到这个<div>元素中,如下所示:

function initStats() {
  var stats = new Stats();
  stats.setMode(0);
  stats.domElement.style.position = 'absolute';
  stats.domElement.style.left = '0px';
  stats.domElement.style.top = '0px';
  document.getElementById("Stats-output")
    .appendChild( stats.domElement );
     return stats;
}

这个函数初始化了统计信息。有趣的部分是setMode函数。如果我们将其设置为0,我们将测量每秒帧数(fps),如果我们将其设置为1,我们可以测量渲染时间。对于这个例子,我们对 fps 感兴趣,所以设置为0。在我们的init()函数的开头,我们将调用这个函数,这样我们就启用了stats,如下所示:

function init(){

  var stats = initStats();
  ...
}

唯一剩下的事情就是告诉stats对象我们何时处于新的渲染周期。我们通过在renderScene函数中添加对stats.update函数的调用来实现这一点,如下所示。

function renderScene() {
  stats.update();
  ...
  requestAnimationFrame(renderScene);
  renderer.render(scene, camera);
}

如果您运行带有这些添加的代码,您将在左上角看到统计信息,如下面的截图所示:

介绍 requestAnimationFrame

为立方体添加统计信息

有了requestAnimationFrame和配置好的统计信息,我们有了一个放置动画代码的地方。在本节中,我们将扩展renderScene函数的代码,以使我们的红色立方体围绕所有轴旋转。让我们先向您展示代码:

function renderScene() {
  ...
  cube.rotation.x += 0.02;
  cube.rotation.y += 0.02;
  cube.rotation.z += 0.02;
  ...
  requestAnimationFrame(renderScene);
  renderer.render(scene, camera);
}

看起来很简单,对吧?我们所做的是每次调用renderScene函数时,都增加每个轴的rotation属性 0.02,这样就会显示一个立方体平滑地围绕所有轴旋转。让蓝色的球弹跳并不难。

弹跳球

为了让球弹跳,我们再次向renderScene函数中添加了几行代码,如下所示:

  var step=0;
  function renderScene() {
    ...
    step+=0.04;
    sphere.position.x = 20+( 10*(Math.cos(step)));
    sphere.position.y = 2 +( 10*Math.abs(Math.sin(step)));
    ...
    requestAnimationFrame(renderScene);
    renderer.render(scene, camera);
  }

使用立方体,我们改变了“旋转”属性;对于球体,我们将在场景中改变其“位置”属性。我们希望球体能够从场景中的一个点弹跳到另一个点,并呈现出一个漂亮、平滑的曲线。如下图所示:

弹跳球

为此,我们需要改变它在x轴上的位置和在y轴上的位置。Math.cosMath.sin函数帮助我们使用步长变量创建平滑的轨迹。我不会在这里详细介绍这是如何工作的。现在,你需要知道的是step+=0.04定义了弹跳球的速度。在第八章中,创建和加载高级网格和几何体,我们将更详细地看看这些函数如何用于动画,并且我会解释一切。这是球在弹跳中间的样子:

弹跳球

在结束本章之前,我想在我们的基本场景中再添加一个元素。当处理 3D 场景、动画、颜色和类似属性时,通常需要一些试验来获得正确的颜色或速度。如果你能有一个简单的 GUI,可以让你随时改变这些属性,那就太方便了。幸运的是,有这样的工具!

使用 dat.GUI 使实验更容易

Google 的几名员工创建了一个名为dat.GUI的库(你可以在code.google.com/p/dat-gui/上找到在线文档),它可以让你非常容易地创建一个简单的用户界面组件,可以改变你代码中的变量。在本章的最后部分,我们将使用 dat.GUI 为我们的示例添加一个用户界面,允许我们改变以下内容:

  • 控制弹跳球的速度

  • 控制立方体的旋转

就像我们为统计数据所做的那样,我们首先将这个库添加到我们 HTML 页面的<head>元素中,如下所示:

<script src="../libs/dat.gui.js"></script>

接下来我们需要配置的是一个 JavaScript 对象,它将保存我们想要使用 dat.GUI 改变的属性。在我们的 JavaScript 代码的主要部分,我们添加以下 JavaScript 对象,如下所示:

var controls = new function() {
  this.rotationSpeed = 0.02;
  this.bouncingSpeed = 0.03;
}

在这个 JavaScript 对象中,我们定义了两个属性——this.rotationSpeedthis.bouncingSpeed——以及它们的默认值。接下来,我们将这个对象传递给一个新的 dat.GUI 对象,并为这两个属性定义范围,如下所示:

var gui = new dat.GUI();
gui.add(controls, 'rotationSpeed', 0, 0.5);
gui.add(controls, 'bouncingSpeed', 0, 0.5);

rotationSpeedbouncingSpeed属性都设置为00.5的范围。现在我们所需要做的就是确保在我们的renderScene循环中,直接引用这两个属性,这样当我们通过 dat.GUI 用户界面进行更改时,它立即影响我们对象的旋转和弹跳速度,如下所示:

function renderScene() {
  ...
  cube.rotation.x += controls.rotationSpeed;
  cube.rotation.y += controls.rotationSpeed;
  cube.rotation.z += controls.rotationSpeed;
  step += controls.bouncingSpeed;
  sphere.position.x = 20 +(10 * (Math.cos(step)));
  sphere.position.y = 2 +(10 * Math.abs(Math.sin(step)));
  ...
}

现在,当你运行这个示例(05-control-gui.html),你会看到一个简单的用户界面,你可以用它来控制弹跳和旋转速度。下面是弹跳球和旋转立方体的屏幕截图:

使用 dat.GUI 使实验更容易

如果你在浏览器中查看示例,你可能会注意到当你改变浏览器的大小时,场景并不会自动缩放。在下一节中,我们将把这作为本章的最后一个特性添加进去。

当浏览器大小改变时自动调整输出大小

当浏览器大小改变时改变摄像机可以很简单地完成。我们需要做的第一件事是注册一个事件监听器,就像这样:

window.addEventListener('resize', onResize, false);

现在,每当浏览器窗口大小改变时,我们将调用onResize函数。在这个onResize函数中,我们需要更新摄像机和渲染器,如下所示:

function onResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

对于摄像机,我们需要更新aspect属性,它保存了屏幕的宽高比,对于renderer,我们需要改变它的大小。最后一步是将camerarendererscene的变量定义移到init()函数之外,这样我们就可以从不同的函数(比如onResize函数)中访问它们,如下所示:

var camera;
var scene;
var renderer;

function init() {
  ...
  scene = new THREE.Scene();
  camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
  renderer = new THREE.WebGLRenderer();
  ...
}

要看到这种效果,打开06-screen-size-change.html示例并调整浏览器窗口大小。

总结

第一章就到这里。在本章中,我们向您展示了如何设置开发环境,如何获取代码,以及如何开始使用本书提供的示例。您还学会了,要使用 Three.js 渲染场景,首先必须创建一个THREE.Scene对象,添加相机、光线和要渲染的对象。我们还向您展示了如何通过添加阴影和动画来扩展基本场景。最后,我们添加了一些辅助库。我们使用了 dat.GUI,它允许您快速创建控制用户界面,并添加了stats.js,它提供了有关场景渲染帧率的反馈。

在下一章中,我们将扩展我们在这里创建的示例。您将了解更多关于在 Three.js 中可以使用的最重要的构建模块。

第二章:构成 Three.js 场景的基本组件

在上一章中,您学习了 Three.js 的基础知识。我们展示了一些例子,您创建了您的第一个完整的 Three.js 场景。在本章中,我们将深入了解 Three.js,并解释构成 Three.js 场景的基本组件。在本章中,您将探索以下主题:

  • 在 Three.js 场景中使用的组件

  • 您可以使用THREE.Scene对象做什么

  • 几何体和网格之间的关系

  • 正交相机和透视相机之间的区别

我们首先来看一下如何创建一个场景并添加对象。

创建一个场景

在上一章中,您创建了THREE.Scene,所以您已经了解了 Three.js 的基础知识。我们看到,为了让场景显示任何内容,我们需要三种类型的组件:

组件 描述
相机 这决定了屏幕上的渲染内容。
灯光 这些对材质的显示和创建阴影效果有影响(在第三章中详细讨论,“在 Three.js 中使用不同的光源”)。
对象 这些是从相机的透视角度渲染的主要对象:立方体、球体等。

THREE.Scene作为所有这些不同对象的容器。这个对象本身并没有太多的选项和功能。

注意

THREE.Scene是一种有时也被称为场景图的结构。场景图是一种可以容纳图形场景所有必要信息的结构。在 Three.js 中,这意味着THREE.Scene包含了所有渲染所需的对象、灯光和其他对象。有趣的是,需要注意的是,场景图并不只是对象的数组;场景图由树结构中的一组节点组成。在 Three.js 中,您可以添加到场景中的每个对象,甚至THREE.Scene本身,都是从名为THREE.Object3D的基本对象扩展而来。THREE.Object3D对象也可以有自己的子对象,您可以使用它们来创建一个 Three.js 将解释和渲染的对象树。

场景的基本功能

探索场景功能的最佳方法是查看一个例子。在本章的源代码中,您可以找到01-basic-scene.html的例子。我将使用这个例子来解释场景具有的各种功能和选项。当我们在浏览器中打开这个例子时,输出将看起来有点像下一个截图中显示的内容:

场景的基本功能

这看起来很像我们在上一章中看到的例子。即使场景看起来相当空,它已经包含了一些对象。从下面的源代码中可以看出,我们使用了THREE.Scene对象的scene.add(object)函数来添加THREE.Mesh(您看到的地面平面)、THREE.SpotLightTHREE.AmbientLight。当您渲染场景时,THREE.Camera对象会被 Three.js 自动添加,但是在使用多个相机时,手动将其添加到场景中是一个好的做法。查看下面这个场景的源代码:

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
**scene.add(camera);**
...
var planeGeometry = new THREE.PlaneGeometry(60,40,1,1);
var planeMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});
var plane = new THREE.Mesh(planeGeometry,planeMaterial);
...
**scene.add(plane);**
var ambientLight = new THREE.AmbientLight(0x0c0c0c);
**scene.add(ambientLight);**
...
var spotLight = new THREE.SpotLight( 0xffffff );
...
**scene.add( spotLight );**

在我们深入研究THREE.Scene对象之前,我将首先解释您可以在演示中做什么,之后我们将查看一些代码。在浏览器中打开01-basic-scene.html的例子,并查看右上角的控件,如下截图所示:

场景的基本功能

有了这些控件,你可以向场景中添加一个立方体,移除最后添加到场景中的立方体,并在浏览器的控制台中显示场景当前包含的所有对象。控件部分的最后一个条目显示了场景中对象的当前数量。当你启动场景时,你可能会注意到场景中已经有四个对象。这些是地面平面、环境光、聚光灯以及我们之前提到的摄像机。我们将查看控制部分中的每个功能,并从最简单的addCube开始:

this.addCube = function() {

  var cubeSize = Math.ceil((Math.random() * 3));
  var cubeGeometry = new THREE.BoxGeometry(cubeSize,cubeSize,cubeSize);
  var cubeMaterial = new THREE.MeshLambertMaterial({color: Math.random() * 0xffffff });
  var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
  cube.castShadow = true;
 **cube.name = "cube-" + scene.children.length;**
  cube.position.x=-30 + Math.round(Math.random() * planeGeometry.width));
  cube.position.y= Math.round((Math.random() * 5));
  cube.position.z=-20 + Math.round((Math.random() * planeGeometry.height));

  scene.add(cube);
 **this.numberOfObjects = scene.children.length;**
};

到目前为止,这段代码应该已经很容易阅读了。这里没有引入太多新概念。当你点击addCube按钮时,会创建一个新的THREE.BoxGeometry对象,其宽度、高度和深度设置为 1 到 3 之间的随机值。除了随机大小,立方体还会获得随机颜色和随机位置。

注意

我们在这里引入的一个新元素是,我们还使用其name属性为立方体命名。它的名称设置为cube-,后面跟着当前场景中的对象数量(scene.children.length)。名称对于调试非常有用,但也可以用于直接访问你场景中的对象。如果你使用THREE.Scene.getObjectByName(name)函数,你可以直接检索特定对象,并且例如,改变它的位置,而不必使 JavaScript 对象成为全局变量。你可能会想知道最后一行代码是做什么的。numberOfObjects变量被我们的控制 GUI 用来列出场景中的对象数量。因此,每当我们添加或移除一个对象时,我们都会将这个变量设置为更新后的计数。

我们可以从控制 GUI 中调用的下一个函数是removeCube。顾名思义,点击removeCube按钮会从场景中移除最后添加的立方体。在代码中,它看起来像这样:

  this.removeCube = function() {
    var allChildren = scene.children;
    var lastObject = allChildren[allChildren.length-1];
    if (lastObject instanceof THREE.Mesh) {
      scene.remove(lastObject);
      this.numberOfObjects = scene.children.length;
    }
  }

要向场景中添加对象,我们使用add函数。要从场景中移除对象,我们使用,不太意外地,remove函数。由于 Three.js 将其子对象存储为列表(新对象添加到末尾),我们可以使用children属性,该属性包含场景中所有对象的数组,从THREE.Scene对象中获取最后添加的对象。我们还需要检查该对象是否是THREE.Mesh对象,以避免移除摄像机和灯光。在我们移除对象之后,我们再次更新 GUI 属性numberOfObjects,该属性保存了场景中对象的数量。

我们的 GUI 上的最后一个按钮标有outputObjects。你可能已经点击过这个按钮,但似乎什么也没发生。这个按钮会将当前场景中的所有对象打印到网页浏览器控制台中,如下面的截图所示:

场景的基本功能

将信息输出到控制台日志的代码使用了内置的console对象:

  this.outputObjects = function() {
    console.log(scene.children);
  }

这对于调试非常有用,特别是当你给你的对象命名时,它非常有用,可以帮助你找到场景中特定对象的问题。例如,cube-17的属性看起来像这样(如果你事先知道名称,也可以使用console.log(scene.getObjectByName("cube-17")来仅输出单个对象):

__webglActive: true
__webglInit: true
_listeners: Object
_modelViewMatrix: THREE.Matrix4
_normalMatrix: THREE.Matrix3
castShadow: true
children: Array[0]
eulerOrder: (...)
frustumCulled: true
geometry: THREE.BoxGeometryid: 8
material: THREE.MeshLambertMaterial
matrix: THREE.Matrix4
matrixAutoUpdate: true
matrixWorld: THREE.Matrix4
matrixWorld
NeedsUpdate: false
name: "cube-17"
parent: THREE.Scene
position: THREE.Vector3
quaternion: THREE.Quaternion
receiveShadow: false
renderDepth: null
rotation: THREE.Euler
rotationAutoUpdate: true
scale: THREE.Vector3
type: "Mesh"
up: THREE.Vector3
useQuaternion: (...)
userData: Object
uuid: "DCDC0FD2-6968-44FD-8009-20E9747B8A73"
visible: true

到目前为止,我们已经看到了以下与场景相关的功能:

  • THREE.Scene.Add:向场景中添加对象

  • THREE.Scene.Remove:从场景中移除对象

  • THREE.Scene.children:获取场景中所有子对象的列表

  • THREE.Scene.getObjectByName:通过名称从场景中获取特定对象

这些是最重要的与场景相关的功能,通常情况下,你不会需要更多。然而,还有一些辅助功能可能会派上用场,我想根据处理立方体旋转的代码来展示它们。

正如您在上一章中看到的,我们使用了渲染循环来渲染场景。让我们看看这个示例的循环:

function render() {
  stats.update();
  scene.traverse(function(obj) {
    if (obj instanceof THREE.Mesh && obj != plane ) {
      obj.rotation.x+=controls.rotationSpeed;
      obj.rotation.y+=controls.rotationSpeed;
      obj.rotation.z+=controls.rotationSpeed;
   }
  });

  requestAnimationFrame(render);
  renderer.render(scene, camera);
}

在这里,我们看到了使用THREE.Scene.traverse()函数。我们可以将一个函数传递给traverse()函数,该函数将对场景的每个子对象调用。如果子对象本身有子对象,请记住THREE.Scene对象可以包含一个对象树。traverse()函数也将对该对象的所有子对象调用。您可以遍历整个场景图。

我们使用render()函数来更新每个立方体的旋转(请注意,我们明确忽略了地面平面)。我们也可以通过使用for循环迭代children属性数组来自己完成这个操作,因为我们只是将对象添加到了THREE.Scene中,并没有创建嵌套结构。

在我们深入讨论THREE.MeshTHREE.Geometry的细节之前,我想展示一下可以在THREE.Scene对象上设置的两个有趣的属性:fogoverrideMaterial

向场景添加雾效

使用fog属性可以向整个场景添加雾效果;物体离得越远,就会越隐匿不见,如下面的截图所示:

向场景添加雾效

在 Three.js 中启用雾效果非常简单。只需在定义场景后添加以下代码行:

scene.fog=new THREE.Fog( 0xffffff, 0.015, 100 );

在这里,我们定义了一个白色的雾(0xffffff)。前两个属性可以用来调整雾的外观。0.015值设置了near属性,100值设置了far属性。使用这些属性,您可以确定雾从哪里开始以及它变得多快密。使用THREE.Fog对象,雾是线性增加的。还有一种不同的设置场景雾的方法;为此,请使用以下定义:

scene.fog=new THREE.FogExp2( 0xffffff, 0.01 );

这次,我们不指定nearfar,而只指定颜色(0xffffff)和雾的密度(0.01)。最好稍微尝试一下这些属性,以获得想要的效果。请注意,使用THREE.FogExp2时,雾不是线性增加的,而是随着距离呈指数增长。

使用 overrideMaterial 属性

我们讨论场景的最后一个属性是overrideMaterial。当使用此属性时,场景中的所有对象将使用设置为overrideMaterial属性的材质,并忽略对象本身设置的材质。

像这样使用它:

scene.overrideMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});

在上面的代码中使用overrideMaterial属性后,场景将呈现如下截图所示:

使用 overrideMaterial 属性

在上图中,您可以看到所有的立方体都使用相同的材质和颜色进行渲染。在这个示例中,我们使用了THREE.MeshLambertMaterial对象作为材质。使用这种材质类型,我们可以创建看起来不那么闪亮的对象,这些对象会对场景中存在的灯光做出响应。在第四章中,使用 Three.js 材质,您将了解更多关于这种材质的信息。

在本节中,我们看了 Three.js 的核心概念之一:THREE.Scene。关于场景最重要的一点是,它基本上是一个容器,用于渲染时要使用的所有对象、灯光和相机。以下表格总结了THREE.Scene对象的最重要的函数和属性:

函数/属性 描述
add(object) 用于将对象添加到场景中。您还可以使用此函数,正如我们稍后将看到的,来创建对象组。
children 返回已添加到场景中的所有对象的列表,包括相机和灯光。
getObjectByName(name, recursive) 当您创建一个对象时,可以为其指定一个独特的名称。场景对象具有一个函数,您可以使用它直接返回具有特定名称的对象。如果将 recursive 参数设置为true,Three.js 还将搜索整个对象树以找到具有指定名称的对象。
remove(object) 如果您有场景中对象的引用,也可以使用此函数将其从场景中移除。
traverse(function) children 属性返回场景中所有子对象的列表。使用 traverse 函数,我们也可以访问这些子对象。通过 traverse,所有子对象都会逐个传递给提供的函数。
fog 此属性允许您为场景设置雾。雾会渲染出一个隐藏远处物体的薄雾。
overrideMaterial 使用此属性,您可以强制场景中的所有对象使用相同的材质。

在下一节中,我们将更详细地了解您可以添加到场景中的对象。

几何图形和网格

到目前为止,在每个示例中,您都看到了使用几何图形和网格。例如,要将一个球体添加到场景中,我们做了以下操作:

var sphereGeometry = new THREE.SphereGeometry(4,20,20);
var sphereMaterial = new THREE.MeshBasicMaterial({color: 0x7777ff);
var sphere = new THREE.Mesh(sphereGeometry,sphereMaterial);

我们定义了对象的形状和其几何图形(THREE.SphereGeometry),我们定义了这个对象的外观(THREE.MeshBasicMaterial)和其材质,并将这两者组合成一个网格(THREE.Mesh),可以添加到场景中。在本节中,我们将更详细地了解几何图形和网格是什么。我们将从几何图形开始。

几何图形的属性和函数

Three.js 自带了一大堆可以在 3D 场景中使用的几何图形。只需添加一个材质,创建一个网格,基本上就完成了。以下截图来自示例04-geometries,显示了 Three.js 中可用的一些标准几何图形:

几何图形的属性和函数

在第五章和第六章中,我们将探索 Three.js 提供的所有基本和高级几何图形。现在,我们将更详细地了解几何图形的实际含义。

在 Three.js 中,以及大多数其他 3D 库中,几何图形基本上是 3D 空间中点的集合,也称为顶点,以及连接这些点的一些面。例如,一个立方体:

  • 一个立方体有八个角。每个角可以定义为xyz坐标。因此,每个立方体在 3D 空间中有八个点。在 Three.js 中,这些点被称为顶点,单个点被称为顶点。

  • 一个立方体有六个面,每个角有一个顶点。在 Three.js 中,一个面始终由三个顶点组成一个三角形。因此,在立方体的情况下,每个面由两个三角形组成,以形成完整的面。

当您使用 Three.js 提供的几何图形之一时,您不必自己定义所有顶点和面。对于一个立方体,您只需要定义宽度、高度和深度。Three.js 使用这些信息,在正确的位置创建一个具有八个顶点和正确数量的面(在立方体的情况下为 12 个)的几何图形。即使您通常会使用 Three.js 提供的几何图形或自动生成它们,您仍然可以使用顶点和面完全手工创建几何图形。以下代码行显示了这一点:

var vertices = [
  new THREE.Vector3(1,3,1),
  new THREE.Vector3(1,3,-1),
  new THREE.Vector3(1,-1,1),
  new THREE.Vector3(1,-1,-1),
  new THREE.Vector3(-1,3,-1),
  new THREE.Vector3(-1,3,1),
  new THREE.Vector3(-1,-1,-1),
  new THREE.Vector3(-1,-1,1)
];

var faces = [
  new THREE.Face3(0,2,1),
  new THREE.Face3(2,3,1),
  new THREE.Face3(4,6,5),
  new THREE.Face3(6,7,5),
  new THREE.Face3(4,5,1),
  new THREE.Face3(5,0,1),
  new THREE.Face3(7,6,2),
  new THREE.Face3(6,3,2),
  new THREE.Face3(5,7,0),
  new THREE.Face3(7,2,0),
  new THREE.Face3(1,3,4),
  new THREE.Face3(3,6,4),
];

var geom = new THREE.Geometry();
geom.vertices = vertices;
geom.faces = faces;
geom.computeFaceNormals();

这段代码展示了如何创建一个简单的立方体。我们在vertices数组中定义了构成这个立方体的点。这些点连接在一起形成三角形面,并存储在faces数组中。例如,new THREE.Face3(0,2,1)使用vertices数组中的点021创建了一个三角形面。请注意,你必须注意用于创建THREE.Face的顶点的顺序。定义它们的顺序决定了 Three.js 认为它是一个正面面(面向摄像机的面)还是一个背面面。如果你创建面,应该对正面面使用顺时针顺序,对背面面使用逆时针顺序。

提示

在这个例子中,我们使用了THREE.Face3元素来定义立方体的六个面,每个面有两个三角形。在 Three.js 的早期版本中,你也可以使用四边形而不是三角形。四边形使用四个顶点而不是三个来定义面。在 3D 建模世界中,使用四边形还是三角形更好是一个激烈的争论。基本上,使用四边形在建模过程中通常更受欢迎,因为它们比三角形更容易增强和平滑。然而,在渲染和游戏引擎中,使用三角形通常更容易,因为每个形状都可以被渲染为三角形。

使用这些顶点和面,我们现在可以创建一个THREE.Geometry的新实例,并将顶点分配给vertices属性,将面分配给faces属性。我们需要采取的最后一步是在我们创建的几何形状上调用computeFaceNormals()。当我们调用这个函数时,Three.js 确定了每个面的法向向量。这是 Three.js 用来根据场景中的各种光源确定如何给面上色的信息。

有了这个几何形状,我们现在可以创建一个网格,就像我们之前看到的那样。我创建了一个例子,你可以用来玩弄顶点的位置,并显示各个面。在例子05-custom-geometry中,你可以改变立方体所有顶点的位置,看看面的反应。下面是一个截图(如果控制 GUI 挡住了视线,你可以通过按下H键来隐藏它):

几何形状的属性和函数

这个例子使用了与我们其他例子相同的设置,有一个渲染循环。每当你改变下拉控制框中的属性时,立方体就会根据一个顶点的改变位置进行渲染。这并不是一件轻而易举的事情。出于性能原因,Three.js 假设网格的几何形状在其生命周期内不会改变。对于大多数的几何形状和用例来说,这是一个非常合理的假设。然而,为了让我们的例子工作,我们需要确保以下内容被添加到渲染循环的代码中:

mesh.children.forEach(function(e) {
  e.geometry.vertices=vertices;
  e.geometry.verticesNeedUpdate=true;
  e.geometry.computeFaceNormals();
});

在第一行中,我们将屏幕上看到的网格的顶点指向一个更新后的顶点数组。我们不需要重新配置面,因为它们仍然连接到与之前相同的点。设置更新后的顶点后,我们需要告诉几何形状顶点需要更新。我们通过将几何形状的verticesNeedUpdate属性设置为true来做到这一点。最后,我们通过computeFaceNormals函数对面进行重新计算,以更新完整的模型。

我们将要看的最后一个几何功能是clone()函数。我们提到几何图形定义了对象的形状和形状,结合材质,我们创建了一个可以添加到场景中由 Three.js 渲染的对象。使用clone()函数,正如其名称所示,我们可以复制几何图形,并且例如,使用它来创建一个具有不同材质的不同网格。在相同的示例05-custom-geometry中,你可以在控制 GUI 的顶部看到一个clone按钮,如下面的截图所示:

几何图形的属性和函数

如果你点击这个按钮,将会克隆(复制)当前的几何图形,创建一个新的对象并添加到场景中。这段代码相当简单,但由于我使用的材质而变得有些复杂。让我们退一步,首先看一下立方体的绿色材质是如何创建的,如下面的代码所示:

var materials = [
  new THREE.MeshLambertMaterial( { opacity:0.6, color: 0x44ff44, transparent:true } ),
  new THREE.MeshBasicMaterial( { color: 0x000000, wireframe: true } )
];

正如你所看到的,我并没有使用单一的材质,而是使用了一个包含两种材质的数组。原因是除了显示一个透明的绿色立方体,我还想向你展示线框,因为线框非常清楚地显示了顶点和面的位置。

当创建网格时,Three.js 当然支持使用多种材质。你可以使用SceneUtils.createMultiMaterialObject函数来实现这一点,如下面的代码所示:

var mesh = THREE.SceneUtils.createMultiMaterialObject( geom, materials);

这个函数在这里所做的是不仅创建一个THREE.Mesh对象,而是为你指定的每种材质创建一个,并将这些网格放在一个组中(一个THREE.Object3D对象)。这个组可以像你使用场景对象一样使用。你可以添加网格,通过名称获取对象等。例如,为了确保组的所有子对象都投射阴影,你可以这样做:

mesh.children.forEach(function(e) {e.castShadow=true});

现在,让我们回到我们正在讨论的clone()函数:

this.clone = function() {

  var clonedGeom = mesh.children[0].geometry.clone();
  var materials = [
    new THREE.MeshLambertMaterial( { opacity:0.6, color: 0xff44ff, transparent:true } ),
    new THREE.MeshBasicMaterial({ color: 0x000000, wireframe: true } )
  ];

  var mesh2 = THREE.SceneUtils.createMultiMaterialObject(clonedGeom, materials);
  mesh2.children.forEach(function(e) {e.castShadow=true});
  mesh2.translateX(5);
  mesh2.translateZ(5);
  mesh2.name="clone";
  scene.remove(scene.getObjectByName("clone"));
  scene.add(mesh2);
}

当点击clone按钮时,将调用这段 JavaScript 代码。在这里,我们克隆了我们的立方体的第一个子对象的几何图形。记住,网格变量包含两个子对象;它包含两个网格,一个用于我们指定的每种材质。基于这个克隆的几何图形,我们创建一个新的网格,恰当地命名为mesh2。我们使用平移函数移动这个新的网格(关于这一点我们将在第五章中详细讨论),移除之前的克隆(如果存在),并将克隆添加到场景中。

提示

在前面的部分中,我们使用了THREE.SceneUtils对象的createMultiMaterialObject来为我们创建的几何图形添加线框。Three.js 还提供了另一种使用THREE.WireFrameHelper添加线框的方法。要使用这个辅助程序,首先要像这样实例化辅助程序:

**var helper = new THREE.WireframeHelper(mesh, 0x000000);**

你提供你想要显示线框的网格和线框的颜色。Three.js 现在将创建一个你可以添加到场景中的辅助对象,scene.add(helper)。由于这个辅助对象内部只是一个THREE.Line对象,你可以设置线框的外观。例如,要设置线框线的宽度,使用helper.material.linewidth = 2;

现在关于几何图形的内容就到此为止。

网格的函数和属性

我们已经学会了创建网格时需要一个几何图形和一个或多个材质。一旦我们有了网格,我们将其添加到场景中并进行渲染。有一些属性可以用来改变这个网格在场景中的位置和外观。在这个第一个示例中,我们将看到以下一组属性和函数:

功能/属性 描述
position 这确定了这个对象相对于其父对象位置的位置。大多数情况下,对象的父对象是一个THREE.Scene对象或一个THREE.Object3D对象。
rotation 通过这个属性,您可以设置对象围绕任意轴的旋转。Three.js 还提供了围绕轴旋转的特定函数:rotateX()rotateY()rotateZ()
scale 这个属性允许您沿着xyz轴缩放对象。
translateX(amount) 这个属性将对象沿着x轴移动指定的距离。
translateY(amount) 这个属性将对象沿着y轴移动指定的距离。
translateZ(amount) 这个属性将对象沿着z轴移动指定的距离。对于平移函数,您还可以使用translateOnAxis(axis, distance)函数,它允许您沿着特定轴平移网格一定距离。
visible 如果将此属性设置为falseTHREE.Mesh将不会被 Three.js 渲染。

和往常一样,我们为您准备了一个示例,让您可以尝试这些属性。如果您在浏览器中打开06-mesh-properties.html,您将获得一个下拉菜单,您可以在其中改变所有这些属性,并直接看到结果,如下面的截图所示:

网格的函数和属性

让我带您了解一下,我将从位置属性开始。我们已经看到这个属性几次了,所以让我们快速解决这个问题。通过这个属性,您可以设置对象的xyz坐标。这个位置是相对于其父对象的,通常是您将对象添加到的场景,但也可以是THREE.Object3D对象或另一个THREE.Mesh对象。当我们查看分组对象时,我们将在第五章中回到这一点,学习使用几何图形。我们可以以三种不同的方式设置对象的位置属性。我们可以直接设置每个坐标:

cube.position.x=10;
cube.position.y=3;
cube.position.z=1;

但是,我们也可以一次性设置它们所有,如下所示:

cube.position.set(10,3,1);

还有第三个选项。position属性是一个THREE.Vector3对象。这意味着,我们也可以这样设置这个对象:

cube.postion=new THREE.Vector3(10,3,1)

在查看此网格的其他属性之前,我想快速地侧重一下。我提到这个位置是相对于其父级的位置。在上一节关于THREE.Geometry的部分中,我们使用了THREE.SceneUtils.createMultiMaterialObject来创建一个多材质对象。我解释说,这实际上并不返回一个单一的网格,而是一个包含基于相同几何形状的每种材质的网格的组合;在我们的情况下,它是一个包含两个网格的组合。如果我们改变其中一个创建的网格的位置,您可以清楚地看到它实际上是两个不同的THREE.Mesh对象。然而,如果我们现在移动这个组合,偏移量将保持不变,如下面的截图所示。在第五章中,学习使用几何图形,我们将更深入地研究父子关系以及分组如何影响缩放、旋转和平移等变换。

网格的函数和属性

好的,接下来列表中的是rotation属性。在本章和上一章中,您已经看到了这个属性被使用了几次。通过这个属性,您可以设置对象围绕其中一个轴的旋转。您可以以与设置位置相同的方式设置这个值。完整的旋转,您可能还记得数学课上学过,是2 x π。您可以在 Three.js 中以几种不同的方式配置这个属性:

cube.rotation.x = 0.5*Math.PI;
cube.rotation.set(0.5*Math.PI, 0, 0);
cube.rotation = new THREE.Vector3(0.5*Math.PI,0,0);

如果您想使用度数(从 0 到 360)而不是弧度,我们需要将其转换为弧度。可以像这样轻松地完成这个转换:

Var degrees = 45;
Var inRadians = degrees * (Math.PI / 180);

您可以使用06-mesh-properties.html示例来尝试这个属性。

我们列表中的下一个属性是我们还没有讨论过的:scale。名称基本上总结了您可以使用此属性做什么。您可以沿着特定轴缩放物体。如果将缩放设置为小于 1 的值,物体将缩小,如下面的屏幕截图所示:

网格的函数和属性

当您使用大于 1 的值时,物体将变大,如下面的屏幕截图所示:

网格的函数和属性

本章我们将要看的网格的下一个部分是translate功能。使用 translate,您也可以改变物体的位置,但是不是定义物体应该在的绝对位置,而是定义物体相对于当前位置应该移动到哪里。例如,我们有一个添加到场景中的球体,其位置已设置为(1,2,3)。接下来,我们沿着x轴平移物体:translateX(4)。它的位置现在将是(5,2,3)。如果我们想将物体恢复到原始位置,我们可以这样做:translateX(-4)。在06-mesh-properties.html示例中,有一个名为translate的菜单选项。从那里,您可以尝试这个功能。只需设置xyz的平移值,然后点击translate按钮。您将看到物体根据这三个值被移动到一个新的位置。

我们在右上角菜单中可以使用的最后一个属性是visible属性。如果单击visible菜单项,您会看到立方体变得不可见,如下所示:

网格的函数和属性

当您再次单击它时,立方体将再次可见。有关网格、几何体以及您可以对这些对象进行的操作的更多信息,请参阅第五章、学习使用几何体和第七章、粒子、精灵和点云

不同用途的不同摄像机

Three.js 中有两种不同的摄像机类型:正交摄像机和透视摄像机。在第三章、使用 Three.js 中可用的不同光源中,我们将更详细地了解如何使用这些摄像机,因此在本章中,我将坚持基础知识。解释这些摄像机之间的区别的最佳方法是通过几个示例来看。

正交摄像机与透视摄像机

在本章的示例中,您可以找到一个名为07-both-cameras.html的演示。当您打开此示例时,您将看到类似于这样的东西:

正交摄像机与透视摄像机

这被称为透视视图,是最自然的视图。从这个图中可以看出,物体距离摄像机越远,呈现的越小。

如果我们将摄像机更改为 Three.js 支持的另一种类型,即正交摄像机,您将看到相同场景的以下视图:

正交摄像机与透视摄像机

使用正交摄像机,所有的立方体都以相同的大小呈现;物体与摄像机之间的距离并不重要。这在 2D 游戏中经常使用,比如模拟城市 4文明的旧版本。

正交摄像机与透视摄像机

在我们的示例中,我们将最常使用透视摄像机,因为它最接近现实世界。切换摄像机非常容易。每当您在07-both-cameras示例上点击切换摄像机按钮时,都会调用以下代码片段:

this.switchCamera = function() {
  if (camera instanceof THREE.PerspectiveCamera) {
    camera = new THREE.OrthographicCamera( window.innerWidth / - 16, window.innerWidth / 16, window.innerHeight / 16, window.innerHeight / - 16, -200, 500 );
    camera.position.x = 120;
    camera.position.y = 60;
    camera.position.z = 180;
    camera.lookAt(scene.position);
    this.perspective = "Orthographic";
  } else {
    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

    camera.position.x = 120;
    camera.position.y = 60;
    camera.position.z = 180;

    camera.lookAt(scene.position);
    this.perspective = "Perspective";
  }
};

在这个表中,你可以看到我们创建相机的方式有所不同。让我们先看一下THREE.PerspectiveCamera。这个相机接受以下参数:

参数 描述
fov FOV代表视野。这是从相机位置可以看到的场景的一部分。例如,人类几乎有 180 度的视野,而一些鸟类甚至可能有完整的 360 度视野。但由于普通电脑屏幕无法完全填满我们的视野,通常会选择一个较小的值。大多数情况下,游戏中选择的 FOV 在 60 到 90 度之间。良好的默认值:50
aspect 这是我们要渲染输出的区域的水平和垂直尺寸之间的纵横比。在我们的情况下,由于我们使用整个窗口,我们只使用该比率。纵横比决定了水平 FOV 和垂直 FOV 之间的差异,如你可以在下图中看到的那样。良好的默认值:window.innerWidth / window.innerHeight
near near属性定义了 Three.js 应该从相机位置渲染场景的距离。通常情况下,我们将其设置为一个非常小的值,直接从相机位置渲染所有东西。良好的默认值:0.1
far far属性定义了相机从相机位置能看到的距离。如果我们设置得太低,可能会导致我们的场景的一部分不被渲染,如果设置得太高,在某些情况下可能会影响渲染性能。良好的默认值:1000
zoom zoom属性允许你放大或缩小场景。当你使用小于1的数字时,你会缩小场景,如果你使用大于1的数字,你会放大。请注意,如果你指定一个负值,场景将被倒置渲染。良好的默认值:1

以下图像很好地概述了这些属性如何共同确定你所看到的内容:

正交相机与透视相机

相机的fov属性确定了水平 FOV。基于aspect属性,确定了垂直 FOV。near属性用于确定近平面的位置,far属性确定了远平面的位置。在近平面和远平面之间的区域将被渲染。

要配置正交相机,我们需要使用其他属性。正交投影对使用的纵横比或我们观察场景的 FOV 都不感兴趣,因为所有的物体都以相同的大小渲染。当你定义一个正交相机时,你所做的就是定义需要被渲染的长方体区域。正交相机的属性反映了这一点,如下所示:

参数 描述
left 这在 Three.js 文档中被描述为相机截头锥左平面。你应该把它看作是将要被渲染的左边界。如果你将这个值设置为-100,你就看不到任何在左侧更远处的物体。
right right属性的工作方式类似于left属性,但这次是在屏幕的另一侧。任何更远的右侧都不会被渲染。
top 这是要渲染的顶部位置。
bottom 这是要渲染的底部位置。
near 从这一点开始,基于相机的位置,场景将被渲染。
far 到这一点,基于相机的位置,场景将被渲染。
zoom 这允许你放大或缩小场景。当你使用小于1的数字时,你会缩小场景;如果你使用大于1的数字,你会放大。请注意,如果你指定一个负值,场景将被倒置渲染。默认值为1

所有这些属性可以总结在下图中:

正交相机与透视相机

观察特定点

到目前为止,您已经了解了如何创建摄像机以及各种参数的含义。在上一章中,您还看到需要将摄像机定位在场景中的某个位置,并且从摄像机的视角进行渲染。通常,摄像机指向场景的中心:位置(0,0,0)。然而,我们可以很容易地改变摄像机的观察对象,如下所示:

camera.lookAt(new THREE.Vector3(x,y,z));

我添加了一个示例,其中摄像机移动,它所看的点用红点标记如下:

观察特定点

如果您打开08-cameras-lookat示例,您将看到场景从左向右移动。实际上场景并没有移动。摄像机正在看不同的点(请参见中心的红点),这会产生场景从左向右移动的效果。在这个示例中,您还可以切换到正交摄像机。在那里,您会发现改变摄像机观察的点几乎与THREE.PerspectiveCamera具有相同的效果。然而,值得注意的是,使用THREE.OrthographicCamera,您可以清楚地看到无论摄像机看向何处,所有立方体的大小都保持不变。

观察特定点

提示

当您使用lookAt函数时,您将摄像机对准特定位置。您还可以使用它来使摄像机在场景中跟随物体移动。由于每个THREE.Mesh对象都有一个THREE.Vector3对象的位置,您可以使用lookAt函数指向场景中的特定网格。您只需要这样做:camera.lookAt(mesh.position)。如果您在渲染循环中调用此函数,您将使摄像机跟随物体在场景中移动。

总结

我们在这第二个介绍章节中讨论了很多内容。我们展示了THREE.Scene的所有函数和属性,并解释了如何使用这些属性来配置您的主场景。我们还向您展示了如何创建几何体。您可以使用THREE.Geometry对象从头开始创建它们,也可以使用 Three.js 提供的任何内置几何体。最后,我们向您展示了如何配置 Three.js 提供的两个摄像机。THREE.PerspectiveCamera使用真实世界的透视渲染场景,而THREE.OrthographicCamera提供了在游戏中经常看到的虚假 3D 效果。我们还介绍了 Three.js 中几何体的工作原理。您现在可以轻松地创建自己的几何体。

在下一章中,我们将看看 Three.js 中可用的各种光源。您将了解各种光源的行为,如何创建和配置它们以及它们如何影响特定材质。

第三章:使用 Three.js 中可用的不同光源

在第一章中,您学习了 Three.js 的基础知识,在上一章中,我们更深入地了解了场景中最重要的部分:几何体、网格和相机。您可能已经注意到,在那一章中我们跳过了灯光,尽管它们构成了每个 Three.js 场景的重要部分。没有灯光,我们将看不到任何渲染。由于 Three.js 包含大量的灯光,每种灯光都有特定的用途,我们将用整个章节来解释灯光的各种细节,并为下一章关于材质使用做好准备。

WebGL 本身并不直接支持照明。如果没有 Three.js,您将不得不编写特定的 WebGL 着色器程序来模拟这些类型的灯光。您可以在developer.mozilla.org/en-US/docs/Web/WebGL/Lighting_in_WebGL找到有关在 WebGL 中模拟照明的良好介绍。

在这一章中,您将学习以下主题:

  • 在 Three.js 中可用的光源

  • 何时应该使用特定的光源

  • 您如何调整和配置所有这些光源的行为

  • 作为奖励,我们还将快速看一下如何创建镜头眩光

与所有章节一样,我们有很多示例供您用来实验灯光的行为。本章中展示的示例可以在提供的源代码的chapter-03文件夹中找到。

Three.js 提供的不同类型的照明

Three.js 中有许多不同的灯光可用,它们都具有特定的行为和用途。在这一章中,我们将讨论以下一组灯光:

名称 描述
THREE.AmbientLight 这是一种基本的光,其颜色被添加到场景中对象的当前颜色中。
THREE.PointLight 这是空间中的一个单点,光从这个点向所有方向扩散。这种光不能用来创建阴影。
THREE.SpotLight 这种光源具有类似台灯、天花板上的聚光灯或火炬的锥形效果。这种光可以投射阴影。
THREE.DirectionalLight 这也被称为无限光。这种光的光线可以被视为平行的,就像太阳的光一样。这种光也可以用来创建阴影。
THREE.HemisphereLight 这是一种特殊的光,可以用来通过模拟反射表面和微弱照亮的天空来创建更自然的室外照明。这种光也不提供任何与阴影相关的功能。
THREE.AreaLight 使用这种光源,您可以指定一个区域,而不是空间中的单个点,从这个区域发出光。THREE.AreaLight不会投射任何阴影。
THREE.LensFlare 这不是一个光源,但使用THREE.LensFlare,您可以为场景中的灯光添加镜头眩光效果。

这一章分为两个主要部分。首先,我们将看一下基本的灯光:THREE.AmbientLightTHREE.PointLightTHREE.SpotLightTHREE.DirectionalLight。所有这些灯光都扩展了基本的THREE.Light对象,提供了共享功能。这里提到的灯光都是简单的灯光,需要很少的设置,并且可以用来重新创建大部分所需的照明场景。在第二部分中,我们将看一下一些特殊用途的灯光和效果:THREE.HemisphereLightTHREE.AreaLightTHREE.LensFlare。您可能只在非常特殊的情况下需要这些灯光。

基本灯光

我们将从最基本的灯光开始:THREE.AmbientLight

THREE.AmbientLight

当你创建THREE.AmbientLight时,颜色是全局应用的。这种光没有特定的方向,THREE.AmbientLight不会对任何阴影产生影响。你通常不会将THREE.AmbientLight作为场景中唯一的光源,因为它会使所有的物体都呈现相同的颜色,而不考虑形状。你会将它与其他光源一起使用,比如THREE.SpotLightTHREE.DirectionalLight,来软化阴影或为场景增加一些额外的颜色。最容易理解的方法是查看chapter-03文件夹中的01-ambient-light.html示例。通过这个示例,你可以得到一个简单的用户界面,用于修改这个场景中可用的THREE.AmbientLight。请注意,在这个场景中,我们还有THREE.SpotLight,它提供了额外的照明并产生阴影。

在下面的截图中,你可以看到我们使用了第一章的场景,并使THREE.AmbientLight的颜色可配置。在这个示例中,你还可以关闭聚光灯,看看THREE.AmbientLight单独的效果:

THREE.AmbientLight

我们在这个场景中使用的标准颜色是#0c0c0c。这是颜色的十六进制表示。前两个值指定颜色的红色部分,接下来的两个值指定绿色部分,最后两个值指定蓝色部分。

在这个示例中,我们使用了一个非常昏暗的浅灰色,主要用于使我们的网格投射到地面平面上的硬阴影变得柔和。你可以通过右上角的菜单将颜色更改为更显眼的黄/橙色(#523318),然后物体将在上面产生太阳般的光芒。这在下面的截图中显示:

THREE.AmbientLight

正如前面的图片所示,黄/橙色应用到了所有的物体,并在整个场景上投射出绿色的光晕。在使用这种光时,你应该记住的是,你应该非常谨慎地选择颜色。如果你选择的颜色太亮,你很快就会得到一个完全过饱和的图像。

现在我们已经看到它的作用,让我们看看如何创建和使用THREE.AmbientLight。接下来的几行代码向你展示了如何创建THREE.AmbientLight,并展示了如何将其连接到 GUI 控制菜单,我们将在第十一章中介绍,自定义着色器和渲染后处理

var ambiColor = "#0c0c0c";
var ambientLight = new THREE.AmbientLight(ambiColor);
scene.add(ambientLight);
...

var controls = new function() {
  this.ambientColor = ambiColor  ;
}

var gui = new dat.GUI();
gui.addColor(controls, 'ambientColor').onChange(function(e) {
  ambientLight.color = new THREE.Color(e);
});

创建THREE.AmbientLight非常简单,只需要几个步骤。THREE.AmbientLight没有位置,是全局应用的,所以我们只需要指定颜色(十六进制),new THREE.AmbientLight(ambiColor),并将这个光添加到场景中,scene.add(ambientLight)。在示例中,我们将THREE.AmbientLight的颜色绑定到控制菜单。要做到这一点,可以使用我们在前两章中使用的相同类型的配置。唯一的变化是,我们使用gui.addColor(...)函数,而不是使用gui.add(...)函数。这在控制菜单中创建一个选项,我们可以直接改变传入变量的颜色。在代码中,你可以看到我们使用了 dat.GUI 的onChange特性:gui.addColor(...).onChange(function(e){...})。通过这个函数,我们告诉dat.GUI每次颜色改变时调用传入的函数。在这种特定情况下,我们将THREE.AmbientLight的颜色设置为一个新值。

使用 THREE.Color 对象

在我们继续下一个光源之前,这里有一个关于使用THREE.Color对象的快速说明。在 Three.js 中,当您构造一个对象时,通常可以将颜色指定为十六进制字符串("#0c0c0c")或十六进制值(0x0c0c0c),这是首选的方法,或者通过指定 0 到 1 的范围上的单独的 RGB 值(0.30.50.6)。如果您想在构造后更改颜色,您将不得不创建一个新的THREE.Color对象或修改当前THREE.Color对象的内部属性。THREE.Color对象具有以下函数来设置和获取有关当前对象的信息:

名称 描述
set(value) 将此颜色的值设置为提供的十六进制值。此十六进制值可以是字符串、数字或现有的THREE.Color实例。
setHex(value) 将此颜色的值设置为提供的数值十六进制值。
setRGB(r,g,b) 根据提供的 RGB 值设置此颜色的值。值的范围从 0 到 1。
setHSL(h,s,l) 根据提供的 HSL 值设置此颜色的值。值的范围从 0 到 1。有关如何使用 HSL 配置颜色的良好解释可以在en.wikibooks.org/wiki/Color_Models:_RGB,_HSV,_HSL找到。
setStyle(style) 根据指定颜色的 CSS 方式设置此颜色的值。例如,您可以使用"rgb(255,0,0)""#ff0000""#f00",甚至"red"
copy(color) 将提供的THREE.Color实例的颜色值复制到此颜色。
copyGammaToLinear(color) 这主要是在内部使用。根据提供的THREE.Color实例设置此对象的颜色。首先将颜色从伽马颜色空间转换为线性颜色空间。伽马颜色空间也使用 RGB 值,但使用的是指数比例而不是线性比例。
copyLinearToGamma(color) 这主要是在内部使用。根据提供的THREE.Color实例设置此对象的颜色。首先将颜色从线性颜色空间转换为伽马颜色空间。
convertGammaToLinear() 将当前颜色从伽马颜色空间转换为线性颜色空间。
convertLinearToGamma() 将当前颜色从线性颜色空间转换为伽马颜色空间。
getHex() 以数字形式返回此颜色对象的值:435241
getHexString() 以十六进制字符串形式返回此颜色对象的值:"0c0c0c"
getStyle() 以基于 CSS 的值返回此颜色对象的值:"rgb(112,0,0)"
getHSL(optionalTarget) 以 HSL 值的形式返回此颜色对象的值。如果提供optionalTarget对象,Three.js 将在该对象上设置hsl属性。
offsetHSL(h, s, l) 将提供的hsl值添加到当前颜色的hsl值中。
add(color) 将提供的颜色的rgb值添加到当前颜色。
addColors(color1, color2) 这主要是在内部使用。添加color1color2,并将当前颜色的值设置为结果。
addScalar(s) 这主要是在内部使用。将一个值添加到当前颜色的 RGB 分量中。请记住,内部值使用 0 到 1 的范围。
multiply(color) 这主要是在内部使用。将当前 RGB 值与THREE.Color的 RGB 值相乘。
multiplyScalar(s) 这主要是在内部使用。将当前 RGB 值与提供的值相乘。请记住,内部值使用 0 到 1 的范围。
lerp(color, alpha) 这主要是在内部使用。找到介于此对象颜色和提供的颜色之间的颜色。alpha 属性定义了你希望结果在当前颜色和提供的颜色之间的距离。
equals(color) 如果提供的THREE.Color实例的 RGB 值与当前颜色的值匹配,则返回true
fromArray(array) 这与setRGB具有相同的功能,但现在 RGB 值可以作为数字数组提供。
toArray 这将返回一个包含三个元素的数组,[r, g, b]
clone() 这将创建这种颜色的精确副本。

在这个表中,你可以看到有很多种方法可以改变当前的颜色。很多这些函数在 Three.js 内部被使用,但它们也提供了一个很好的方式来轻松改变光和材质的颜色。

在我们继续讨论THREE.PointLightTHREE.SpotLightTHREE.DirectionalLight之前,让我们首先强调它们的主要区别,即它们如何发光。以下图表显示了这三种光源是如何发光的:

使用 THREE.Color 对象

你可以从这个图表中看到以下内容:

  • THREE.PointLight从一个特定点向所有方向发光

  • THREE.SpotLight从一个特定点发射出锥形的光

  • THREE.DirectionalLight不是从单一点发光,而是从一个二维平面发射光线,光线是平行的

我们将在接下来的几段中更详细地看这些光源;让我们从THREE.Pointlight开始。

THREE.PointLight

在 Three.js 中,THREE.PointLight是一个从单一点发出的照射所有方向的光源。一个很好的例子是夜空中发射的信号弹。就像所有的光源一样,我们有一个具体的例子可以用来玩THREE.PointLight。如果你在chapter-03文件夹中查看02-point-light.html,你可以找到一个例子,其中THREE.PointLight在一个简单的 Three.js 场景中移动。以下截图显示了这个例子:

THREE.PointLight

在这个例子中,THREE.PointLight在我们已经在第一章中看到的场景中移动,使用 Three.js 创建你的第一个 3D 场景。为了更清楚地看到THREE.PointLight在哪里,我们沿着相同的路径移动一个小橙色的球体。当这个光源移动时,你会看到红色的立方体和蓝色的球体在不同的侧面被这个光源照亮。

提示

你可能会注意到在这个例子中我们没有看到任何阴影。在 Three.js 中,THREE.PointLight不会投射阴影。由于THREE.PointLight向所有方向发光,计算阴影对于 GPU 来说是一个非常繁重的过程。

与我们之前看到的THREE.AmbientLight不同,你只需要提供THREE.Color并将光源添加到场景中。然而,对于THREE.PointLight,我们有一些额外的配置选项:

属性 描述
color 这是光的颜色。
distance 这是光照射的距离。默认值为0,这意味着光的强度不会根据距离而减少。
intensity 这是光的强度。默认值为1
position 这是THREE.Scene中光的位置。
visible 如果将此属性设置为true(默认值),则此光源将打开,如果设置为false,则光源将关闭。

在接下来的几个例子和截图中,我们将解释这些属性。首先,让我们看看如何创建THREE.PointLight

var pointColor = "#ccffcc";
var pointLight = new THREE.PointLight(pointColor);
pointLight.position.set(10,10,10);
scene.add(pointLight);

我们创建了一个具有特定color属性的光(这里我们使用了一个字符串值;我们也可以使用一个数字或THREE.Color),设置了它的position属性,并将其添加到场景中。

我们首先要看的属性是 intensity。通过这个属性,你可以设置光的亮度。如果你将其设置为 0,你将看不到任何东西;将其设置为 1,你将得到默认的亮度;将其设置为 2,你将得到两倍亮度的光;依此类推。例如,在下面的截图中,我们将光的强度设置为 2.4

THREE.PointLight

要改变光的强度,你只需要使用 THREE.PointLightintensity 属性,如下所示:

pointLight.intensity = 2.4;

或者你可以使用 dat.GUI 监听器,像这样:

var controls = new function() {
  this.intensity = 1;
}
var gui = new dat.GUI();
  gui.add(controls, 'intensity', 0, 3).onChange(function (e) {
    pointLight.intensity = e;
  });

PointLightdistance 属性非常有趣,最好通过一个例子来解释。在下面的截图中,你会看到同样的场景,但这次是一个非常高的 intensity 属性(我们有一个非常明亮的光),但是有一个很小的 distance

THREE.PointLight

SpotLightdistance 属性确定了光从光源传播到其强度属性为 0 的距离。你可以像这样设置这个属性:pointLight.distance = 14。在前面的截图中,光的亮度在距离为 14 时慢慢减弱到 0。这就是为什么在例子中,你仍然可以看到一个明亮的立方体,但光无法到达蓝色的球体。distance 属性的默认值是 0,这意味着光不会随着距离的增加而减弱。

THREE.SpotLight

THREE.SpotLight 是你经常会使用的灯光之一(特别是如果你想要使用阴影)。THREE.SpotLight 是一个具有锥形效果的光源。你可以把它比作手电筒或灯笼。这种光源有一个方向和一个产生光的角度。以下表格列出了适用于 THREE.SpotLight 的所有属性:

属性 描述
angle 这决定了从这个光源发出的光束有多宽。这是用弧度来衡量的,默认值为 Math.PI/3
castShadow 如果设置为 true,这个光将投射阴影。
color 这是光的颜色。
distance 这是光照的距离。默认值为 0,这意味着光的强度不会根据距离而减弱。
exponent 对于 THREE.SpotLight,从光源越远,发出的光的强度就会减弱。exponent 属性确定了这种强度减弱的速度。值越低,从这个光源发出的光就会到达更远的物体,而值越高,它只会到达非常接近 THREE.SpotLight 的物体。
intensity 这是光的强度。默认值为 1。
onlyShadow 如果将此属性设置为 true,这个光将只投射阴影,不会为场景增加任何光。
position 这是光在 THREE.Scene 中的位置。
shadowBias 阴影偏移将投射的阴影远离或靠近投射阴影的物体。你可以使用这个来解决一些在处理非常薄的物体时出现的奇怪效果(一个很好的例子可以在 www.3dbuzz.com/training/view/unity-fundamentals/lights/8-shadows-bias 找到)。如果你看到奇怪的阴影效果,这个属性的小值(例如 0.01)通常可以解决问题。这个属性的默认值是 0
shadowCameraFar 这确定了从光源创建阴影的距离。默认值为 5,000
shadowCameraFov 这确定了用于创建阴影的视场有多大(参见第二章中的 不同用途的不同相机 部分,三.js 场景的基本组件)。默认值为 50
shadowCameraNear 这决定了从光源到阴影应该创建的距离。默认值为50
shadowCameraVisible 如果设置为true,您可以看到这个光源是如何投射阴影的(请参见下一节的示例)。默认值为false
shadowDarkness 这定义了阴影的深度。这在场景渲染后无法更改。默认值为0.5
shadowMapWidthshadowMapHeight 这决定了用多少像素来创建阴影。当阴影边缘有锯齿状或看起来不平滑时,增加这个值。这在场景渲染后无法更改。两者的默认值都是512
target 对于THREE.SpotLight,它指向的方向很重要。使用target属性,您可以指定THREE.SpotLight瞄准场景中的特定对象或位置。请注意,此属性需要一个THREE.Object3D对象(如THREE.Mesh)。这与我们在上一章中看到的相机不同,相机在其lookAt函数中使用THREE.Vector3
visible 如果设置为true(默认值),则此光源打开,如果设置为false,则关闭。

创建THREE.SpotLight非常简单。只需指定颜色,设置您想要的属性,并将其添加到场景中,如下所示:

var pointColor = "#ffffff";
var spotLight = new THREE.SpotLight(pointColor);
spotLight.position.set(-40, 60, -10);
spotLight.castShadow = true;
spotLight.target = plane;
scene.add(spotLight);

THREE.SpotLightTHREE.PointLight并没有太大的区别。唯一的区别是我们将castShadow属性设置为true,因为我们想要阴影,并且我们需要为这个SpotLight设置target属性。target属性确定了光的瞄准位置。在这种情况下,我们将其指向了名为plane的对象。当您运行示例(03-spot-light.html)时,您将看到如下截图所示的场景:

THREE.SpotLight

在这个示例中,您可以设置一些特定于THREE.SpotLight的属性。其中之一是target属性。如果我们将此属性设置为蓝色的球体,光将聚焦在球体的中心,即使它在场景中移动。当我们创建光时,我们将其瞄准地面平面,在我们的示例中,我们也可以将其瞄准其他两个对象。但是,如果您不想将光瞄准到特定对象,而是瞄准到空间中的任意点,您可以通过创建一个THREE.Object3D()对象来实现:

var target = new THREE.Object3D();
target.position = new THREE.Vector3(5, 0, 0);

然后,设置THREE.SpotLighttarget属性:

spotlight.target = target

在本节开始时的表格中,我们展示了一些可以用来控制THREE.SpotLight发出光线的属性。distanceangle属性定义了光锥的形状。angle属性定义了光锥的宽度,而distance属性则设置了光锥的长度。下图解释了这两个值如何共同定义将从THREE.SpotLight接收光线的区域。

THREE.SpotLight

通常情况下,您不需要设置这些值,因为它们已经有合理的默认值,但是您可以使用这些属性,例如,创建一个具有非常窄的光束或快速减少光强度的THREE.SpotLight。您可以使用最后一个属性来改变THREE.SpotLight产生光线的方式,即exponent属性。使用这个属性,您可以设置光强度从光锥的中心向边缘迅速减少的速度。在下面的图像中,您可以看到exponent属性的效果。我们有一个非常明亮的光(高intensity),随着它从中心向锥体的边缘移动,光强度迅速减弱(高exponent):

THREE.SpotLight

您可以使用此功能来突出显示特定对象或模拟小手电筒。我们还可以使用小的exponent值和angle创建相同的聚焦光束效果。在对这种第二种方法进行谨慎说明时,请记住,非常小的角度可能会很快导致各种渲染伪影(伪影是图形中用于不需要的失真和奇怪渲染部分的术语)。

在继续下一个光源之前,我们将快速查看THREE.SpotLight可用的与阴影相关的属性。您已经学会了通过将THREE.SpotLightcastShadow属性设置为true来获得阴影(当然,还要确保我们为应该投射阴影的对象设置castShadow属性,并且在我们场景中的THREE.Mesh对象上设置receiveShadow属性,以显示阴影)。Three.js 还允许您对阴影的渲染进行非常精细的控制。这是通过我们在本节开头的表中解释的一些属性完成的。通过shadowCameraNearshadowCameraFarshadowCameraFov,您可以控制这种光如何在何处投射阴影。这与我们在前一章中解释的透视相机的视野工作方式相同。查看此操作的最简单方法是将shadowCameraVisible设置为true;您可以通过选中菜单中的调试复选框来执行此操作。如下截图所示,这显示了用于确定此光的阴影的区域:

THREE.SpotLight

在结束本节之前,我将给出一些建议,以防您在阴影方面遇到问题:

  • 启用shadowCameraVisible属性。这显示了受此光影响的阴影区域。

  • 如果阴影看起来很块状,您可以增加shadowMapWidthshadowMapHeight属性,或者确保用于计算阴影的区域紧密包裹您的对象。您可以使用shadowCameraNearshadowCameraFarshadowCameraFov属性来配置此区域。

  • 请记住,您不仅需要告诉光源投射阴影,还需要通过设置castShadowreceiveShadow属性告诉每个几何体是否会接收和/或投射阴影。

  • 如果您在场景中使用薄物体,渲染阴影时可能会出现奇怪的伪影。您可以使用shadowBias属性轻微偏移阴影,这通常可以解决这类问题。

  • 您可以通过设置shadowDarkness属性来改变阴影的深浅。如果您的阴影太暗或不够暗,更改此属性可以让您微调阴影的渲染方式。

  • 如果您想要更柔和的阴影,可以在THREE.WebGLRenderer上设置不同的shadowMapType值。默认情况下,此属性设置为THREE.PCFShadowMap;如果将此属性设置为PCFSoftShadowMap,则可以获得更柔和的阴影。

THREE.DirectionalLight

THREE.DirectionalLight是我们将要看的基本灯光中的最后一个。这种类型的光可以被认为是非常遥远的光。它发出的所有光线都是平行的。一个很好的例子是太阳。太阳离我们如此遥远,以至于我们在地球上接收到的光线(几乎)是平行的。THREE.DirectionalLight和我们在上一节中看到的THREE.SpotLight之间的主要区别是,这种光不会像THREE.SpotLight那样随着距离THREE.DirectionalLight的目标越来越远而减弱(您可以使用distanceexponent参数来微调这一点)。THREE.DirectionalLight照亮的完整区域接收到相同强度的光。

要查看此操作,请查看此处显示的04-directional-light示例:

THREE.DirectionalLight

正如你在上面的图像中所看到的,没有一个光锥应用到了场景中。一切都接收到了相同数量的光。只有光的方向、颜色和强度被用来计算颜色和阴影。

就像THREE.SpotLight一样,你可以设置一些控制光强度和投射阴影方式的属性。THREE.DirectionalLight有很多与THREE.SpotLight相同的属性:positiontargetintensitydistancecastShadowonlyShadowshadowCameraNearshadowCameraFarshadowDarknessshadowCameraVisibleshadowMapWidthshadowMapHeightshadowBias。关于这些属性的信息,你可以查看前面关于THREE.SpotLight的部分。接下来的几段将讨论一些额外的属性。

如果你回顾一下THREE.SpotLight的例子,你会发现我们必须定义光锥,阴影应用的范围。因为对于THREE.DirectionalLight,所有的光线都是平行的,我们没有光锥,而是一个长方体区域,就像你在下面的截图中看到的一样(如果你想亲自看到这个,请将摄像机远离场景):

THREE.DirectionalLight

落入这个立方体范围内的一切都可以从光中投射和接收阴影。就像对于THREE.SpotLight一样,你定义这个范围越紧密,你的阴影看起来就越好。使用以下属性定义这个立方体:

directionalLight.shadowCameraNear = 2;
directionalLight.shadowCameraFar = 200;
directionalLight.shadowCameraLeft = -50;
directionalLight.shadowCameraRight = 50;
directionalLight.shadowCameraTop = 50;
directionalLight.shadowCameraBottom = -50;

你可以将这与我们在第二章中关于摄像机的部分中配置正交相机的方式进行比较。

注意

有一个THREE.DirectionalLight可用的属性我们还没有讨论:shadowCascade。当你想在THREE.DirectionalLight上使用阴影时,这个属性可以用来创建更好的阴影。如果你将属性设置为true,Three.js 将使用另一种方法来生成阴影。它将阴影生成分割到由shadowCascadeCount指定的值。这将导致在相机视点附近更详细的阴影,而在远处更少详细的阴影。要使用这个功能,你需要尝试不同的设置,如shadowCascadeCountshadowCascadeBiasshadowCascadeWidthshadowCascadeHeightshadowCascadeNearZshadowCascadeFarZ。你可以在alteredqualia.com/three/examples/webgl_road.html找到一个使用了这种设置的示例。

特殊灯光

在这个特殊灯光的部分,我们将讨论 Three.js 提供的另外两种灯光。首先,我们将讨论THREE.HemisphereLight,它有助于为室外场景创建更自然的光照,然后我们将看看THREE.AreaLight,它从一个大区域发出光,而不是从一个单一点发出光,最后,我们将向您展示如何在场景中添加镜头眩光效果。

THREE.HemisphereLight

我们要看的第一个特殊灯光是THREE.HemisphereLight。使用THREE.HemisphereLight,我们可以创建更自然的室外光照。没有这种光,我们可以通过创建THREE.DirectionalLight来模拟室外,它模拟太阳,也许添加额外的THREE.AmbientLight来为场景提供一些一般的颜色。然而,这看起来并不真实。当你在室外时,不是所有的光都直接来自上方:大部分是被大气层散射和地面以及其他物体反射的。Three.js 中的THREE.HemisphereLight就是为这种情况而创建的。这是一个更自然的室外光照的简单方法。要查看一个示例,请看05-hemisphere-light.html

THREE.HemisphereLight

注意

请注意,这是第一个加载额外资源的示例,无法直接从本地文件系统运行。因此,如果您还没有这样做,请查看第一章,“使用 Three.js 创建您的第一个 3D 场景”,了解如何设置本地 Web 服务器或禁用浏览器中的安全设置以使加载外部资源正常工作。

在此示例中,您可以打开和关闭THREE.HemisphereLight并设置颜色和强度。创建半球光与创建任何其他光一样简单:

var hemiLight = new THREE.HemisphereLight(0x0000ff, 0x00ff00, 0.6);
hemiLight.position.set(0, 500, 0);
scene.add(hemiLight);

您只需指定从天空接收到的颜色、从地面接收到的颜色以及这些光的强度。如果以后想要更改这些值,可以通过以下属性访问它们:

属性 描述
groundColor 这是从地面发出的颜色
color 这是从天空发出的颜色
intensity 这是光线照射的强度

THREE.AreaLight

我们将要看的最后一个真实光源是THREE.AreaLight。使用THREE.AreaLight,我们可以定义一个发光的矩形区域。THREE.AreaLight不包含在标准的 Three.js 库中,而是在其扩展中,因此在使用此光源之前,我们必须采取一些额外的步骤。在查看细节之前,让我们先看一下我们的目标结果(06-area-light.html打开此示例);以下屏幕截图概括了我们想要看到的结果:

THREE.AreaLight

在此屏幕截图中,我们定义了三个THREE.AreaLight对象,每个对象都有自己的颜色。您还可以看到这些灯如何影响整个区域。

当我们想要使用THREE.AreaLight时,不能使用我们之前示例中使用的THREE.WebGLRenderer。原因是THREE.AreaLight是一个非常复杂的光源,会导致普通的THREE.WebGLRenderer对象严重影响性能。它在渲染场景时使用了不同的方法(将其分解为多个步骤),并且可以比标准的THREE.WebGLRenderer对象更好地处理复杂的光(或者说非常多的光源)。

要使用THREE.WebGLDeferredRenderer,我们必须包含 Three.js 提供的一些额外的 JavaScript 源。在 HTML 骨架的头部,确保您定义了以下一组<script>源:

<head>
  <script type="text/javascript" src="../libs/three.js"></script>
  <script type="text/javascript" src="../libs/stats.js"></script>
  <script type="text/javascript" src="../libs/dat.gui.js"></script>

  <script type="text/javascript" src="../libs/WebGLDeferredRenderer.js"></script>
  <script type="text/javascript" src="../libs/ShaderDeferred.js"></script>
  <script type="text/javascript" src="../libs/RenderPass.js"></script>
  <script type="text/javascript" src="../libs/EffectComposer.js"></script>
  <script type="text/javascript" src="../libs/CopyShader.js"></script>
  <script type="text/javascript" src="../libs/ShaderPass.js"></script>
  <script type="text/javascript" src="../libs/FXAAShader.js"></script>
  <script type="text/javascript" src="../libs/MaskPass.js"></script>
</head>

包括这些库后,我们可以使用THREE.WebGLDeferredRenderer。我们可以以与我们在其他示例中讨论的方式使用此渲染器。只需要一些额外的参数:

var renderer = new THREE.WebGLDeferredRenderer({width: window.innerWidth,height: window.innerHeight,scale: 1, antialias: true,tonemapping: THREE.FilmicOperator, brightness: 2.5 });

目前不要太担心这些属性的含义。在第十章,“加载和使用纹理”中,我们将深入探讨THREE.WebGLDeferredRenderer并向您解释它们。有了正确的 JavaScript 库和不同的渲染器,我们就可以开始添加Three.AreaLight

我们几乎以与所有其他光源相同的方式执行此操作:

var areaLight1 = new THREE.AreaLight(0xff0000, 3);
areaLight1.position.set(-10, 10, -35);
areaLight1.rotation.set(-Math.PI / 2, 0, 0);
areaLight1.width = 4;
areaLight1.height = 9.9;
scene.add(areaLight1);

在这个例子中,我们创建了一个新的THREE.AreaLight。这个光源的颜色值为0xff0000,强度值为3。和其他光源一样,我们可以使用position属性来设置它在场景中的位置。当你创建THREE.AreaLight时,它将被创建为一个水平平面。在我们的例子中,我们创建了三个垂直放置的THREE.AreaLight对象,所以我们需要围绕它们的x轴旋转-Math.PI/2。最后,我们使用widthheight属性设置了THREE.AreaLight的大小,并将它们添加到了场景中。如果你第一次尝试这样做,你可能会想知道为什么在你放置光源的地方看不到任何东西。这是因为你看不到光源本身,只能看到它发出的光,只有当它接触到物体时才能看到。如果你想重现我在例子中展示的效果,你可以在相同的位置(areaLight1.position)添加THREE.PlaneGeometryTHREE.BoxGeometry来模拟发光的区域,如下所示:

var planeGeometry1 = new THREE.BoxGeometry(4, 10, 0);
var planeGeometry1Mat = new THREE.MeshBasicMaterial({color: 0xff0000})
var plane = new THREE.Mesh(planeGeometry1, planeGeometry1Mat);
plane.position = areaLight1.position;
scene.add(plane);

你可以使用THREE.AreaLight创建非常漂亮的效果,但可能需要进行一些实验来获得期望的效果。如果你从右上角拉下控制面板,你可以调整三个灯的颜色和强度,立即看到效果,如下所示:

THREE.AreaLight

镜头耀斑

本章最后要探讨的主题是镜头耀斑。你可能已经熟悉镜头耀斑了。例如,当你直接对着太阳或其他强光拍照时,它们会出现。在大多数情况下,你可能会想避免这种情况,但对于游戏和 3D 生成的图像来说,它提供了一个很好的效果,使场景看起来更加真实。

Three.js 也支持镜头耀斑,并且非常容易将它们添加到你的场景中。在最后一节中,我们将向场景添加一个镜头耀斑,并创建输出,你可以通过打开07-lensflares.html来看到:

LensFlare

我们可以通过实例化THREE.LensFlare对象来创建镜头耀斑。我们需要做的第一件事就是创建这个对象。THREE.LensFlare接受以下参数:

flare = new THREE.LensFlare(texture, size, distance, blending, color, opacity);

这些参数在下表中有解释:

参数 描述
texture 纹理是决定耀斑形状的图像。
size 我们可以指定耀斑的大小。这是以像素为单位的大小。如果指定为-1,则使用纹理本身的大小。
distance 这是从光源(0)到相机(1)的距离。使用这个来将镜头耀斑定位在正确的位置。
blending 我们可以为耀斑指定多个纹理。混合模式决定了这些纹理如何混合在一起。在LensFlare中默认使用的是THREE.AdditiveBlending。关于混合的更多内容在下一章中有介绍。
color 这是耀斑的颜色。

让我们来看看用于创建这个对象的代码(参见07-lensflares.html):

var textureFlare0 = THREE.ImageUtils.loadTexture
      ("../assets/textures/lensflare/lensflare0.png");

var flareColor = new THREE.Color(0xffaacc);
var lensFlare = new THREE.LensFlare(textureFlare0, 350, 0.0, THREE.AdditiveBlending, flareColor);

lensFlare.position = spotLight.position;
scene.add(lensFlare);

我们首先加载一个纹理。在这个例子中,我使用了 Three.js 示例提供的镜头耀斑纹理,如下所示:

LensFlare

如果你将这个图像与本节开头的截图进行比较,你会发现它定义了镜头耀斑的外观。接下来,我们使用new THREE.Color( 0xffaacc );来定义镜头耀斑的颜色,这会使镜头耀斑呈现红色的光晕。有了这两个对象,我们就可以创建THREE.LensFlare对象。在这个例子中,我们将耀斑的大小设置为350,距离设置为0.0(直接在光源处)。

在创建了LensFlare对象之后,我们将它定位在光源的位置并将其添加到场景中,如下截图所示:

LensFlare

这已经看起来不错,但是如果你把这个与本章开头的图像进行比较,你会注意到我们缺少页面中间的小圆形伪影。我们创建这些伪影的方式与主要的光晕几乎相同,如下所示:

var textureFlare3 = THREE.ImageUtils.loadTexture
      ("../assets/textures/lensflare/lensflare3.png");

lensFlare.add(textureFlare3, 60, 0.6, THREE.AdditiveBlending);
lensFlare.add(textureFlare3, 70, 0.7, THREE.AdditiveBlending);
lensFlare.add(textureFlare3, 120, 0.9, THREE.AdditiveBlending);
lensFlare.add(textureFlare3, 70, 1.0, THREE.AdditiveBlending);

不过,这一次我们不创建一个新的THREE.LensFlare,而是使用刚刚创建的LensFlare提供的add函数。在这个方法中,我们需要指定纹理、大小、距离和混合模式,就这样。请注意,add函数可以接受两个额外的参数。你还可以将新的眩光的coloropacity属性设置为add。我们用于这些新眩光的纹理是一个非常轻的圆形,如下面的截图所示:

LensFlare

如果你再次观察场景,你会看到伪影出现在你用distance参数指定的位置。

总结

在本章中,我们涵盖了关于 Three.js 中可用的不同类型的光的大量信息。在本章中,你学到了配置光线、颜色和阴影并不是一门精确的科学。为了得到正确的结果,你应该尝试不同的设置,并使用 dat.GUI 控件来微调你的配置。不同的光有不同的行为方式。THREE.AmbientLight颜色被添加到场景中的每一个颜色中,通常用于平滑硬色和阴影。THREE.PointLight在所有方向上发光,但不能用于创建阴影。THREE.SpotLight是一种类似手电筒的灯光。它呈圆锥形,可以配置为随距离衰减,并且能够投射阴影。我们还看了THREE.DirectionalLight。这种光可以与远处的光进行比较,比如太阳,它的光线是平行的,强度不会随着离配置的目标越远而减弱。除了标准的光,我们还看了一些更专业的光。为了获得更自然的室外效果,你可以使用THREE.HemisphereLight,它考虑了地面和天空的反射;THREE.AreaLight不是从单一点发光,而是从一个大面积发光。我们向你展示了如何使用THREE.LensFlare对象添加摄影镜头眩光。

到目前为止的章节中,我们已经介绍了一些不同的材质,而在本章中,你看到并不是所有的材质对可用的光都有相同的反应。在下一章中,我们将概述 Three.js 中可用的材质。

第四章:使用 Three.js 材质

在之前的章节中,我们稍微谈到了材质。您已经了解到,材质与THREE.Geometry一起形成THREE.Mesh。材质就像物体的皮肤,定义了几何体外观的外部。例如,皮肤定义了几何体是金属外观、透明还是显示为线框。然后,生成的THREE.Mesh对象可以添加到场景中,由 Three.js 渲染。到目前为止,我们还没有真正详细地研究过材质。在本章中,我们将深入探讨 Three.js 提供的所有材质,并学习如何使用这些材质来创建好看的 3D 物体。我们将在本章中探讨的材质如下表所示:

名称 描述
MeshBasicMaterial 这是一种基本材质,您可以使用它来给您的几何体一个简单的颜色或显示几何体的线框。
MeshDepthMaterial 这是一种使用从相机到网格的距离来确定如何着色的材质。
MeshNormalMaterial 这是一种简单的材质,它基于法向量确定面的颜色。
MeshFacematerial 这是一个容器,允许您为几何体的每个面指定一个独特的材质。
MeshLambertMaterial 这是一种考虑光照的材质,用于创建暗淡的非光亮外观的物体。
MeshPhongMaterial 这是一种考虑光照的材质,可用于创建光亮的物体。
ShaderMaterial 这种材质允许您指定自己的着色器程序,直接控制顶点的位置和像素的颜色。
LineBasicMaterial 这是一种可以用在THREE.Line几何体上创建彩色线条的材质。
LineDashMaterial 这与LineBasicMaterial相同,但这种材质还允许您创建虚线效果。

如果您查看 Three.js 的源代码,您可能会遇到THREE.RawShaderMaterial。这是一种专门的材质,只能与THREE.BufferedGeometry一起使用。这种几何体是一种针对静态几何体进行优化的特殊形式(例如,顶点和面不会改变)。我们不会在本章中探讨这种材质,但在第十一章中,自定义着色器和渲染后处理,当我们讨论创建自定义着色器时,我们将使用它。在代码中,您还可以找到THREE.SpriteCanvasMaterialTHREE.SpriteMaterialTHREE.PointCloudMaterial。这些是您在为个别点设置样式时使用的材质。我们不会在本章中讨论这些,但我们将在第七章中探讨它们,粒子、精灵和点云

材质有许多共同的属性,因此在我们查看第一个材质MeshBasicMaterial之前,我们将先看一下所有材质共享的属性。

理解共同的材质属性

您可以快速看到所有材质之间共享的属性。Three.js 提供了一个材质基类THREE.Material,列出了所有共同的属性。我们将这些共同的材质属性分为以下三类:

  • 基本属性:这些是您经常使用的属性。使用这些属性,您可以控制物体的不透明度,它是否可见,以及如何引用它(通过 ID 或自定义名称)。

  • 混合属性:每个物体都有一组混合属性。这些属性定义了物体如何与其背景相结合。

  • 高级属性:有许多高级属性控制着低级的 WebGL 上下文如何渲染物体。在大多数情况下,您不需要去处理这些属性。

请注意,在本章中,我们跳过了与纹理和贴图相关的任何属性。大多数材质允许您使用图像作为纹理(例如,类似木头或石头的纹理)。在第十章中,加载和使用纹理,我们将深入探讨各种可用的纹理和映射选项。一些材质还具有与动画相关的特定属性(皮肤和morphTargets);我们也会跳过这些属性。这些将在第九章中进行讨论,动画和移动摄像机

我们从列表中的第一个开始:基本属性。

基本属性

THREE.Material对象的基本属性列在下表中(您可以在THREE.BasicMeshMaterial部分中看到这些属性的实际应用):

属性 描述
id 用于标识材质的属性,在创建材质时分配。第一个材质从0开始,每创建一个额外的材质,增加1
uuid 这是一个唯一生成的 ID,用于内部使用。
name 您可以使用此属性为材质分配一个名称。这可用于调试目的。
opacity 这定义了对象的透明度。与transparent属性一起使用。此属性的范围是从01
transparent 如果将其设置为true,Three.js 将以设置的不透明度渲染此对象。如果将其设置为false,对象将不透明,只是颜色更浅。如果使用使用 alpha(透明度)通道的纹理,则还应将此属性设置为true
overdraw 当使用THREE.CanvasRenderer时,多边形会被渲染得更大一些。当使用此渲染器时看到间隙时,将其设置为true
visible 这定义了此材质是否可见。如果将其设置为false,则在场景中看不到对象。
Side 使用此属性,您可以定义材质应用于几何体的哪一侧。默认值为THREE.Frontside,将材质应用于对象的前面(外部)。您还可以将其设置为THREE.BackSide,将其应用于后面(内部),或THREE.DoubleSide,将其应用于两侧。
needsUpdate 对于材质的一些更新,您需要告诉 Three.js 材质已更改。如果此属性设置为true,Three.js 将使用新的材质属性更新其缓存。

对于每种材质,您还可以设置一些混合属性。

混合属性

材质具有一些通用的与混合相关的属性。混合确定我们渲染的颜色如何与它们后面的颜色相互作用。当我们谈论组合材质时,我们会稍微涉及这个主题。混合属性列在下表中:

名称 描述
blending 这决定了此对象上的材质与背景的混合方式。正常模式是THREE.NormalBlending,只显示顶层。
blendsrc 除了使用标准混合模式,您还可以通过设置blendsrcblenddstblendequation来创建自定义混合模式。此属性定义了对象(源)如何混合到背景(目标)中。默认的THREE.SrcAlphaFactor设置使用 alpha(透明度)通道进行混合。
blenddst 此属性定义了背景(目标)在混合中的使用方式,默认为THREE.OneMinusSrcAlphaFactor,这意味着此属性也使用源的 alpha 通道进行混合,但使用1(源的 alpha 通道)作为值。
blendequation 这定义了如何使用blendsrcblenddst值。默认是将它们相加(AddEquation)。使用这三个属性,您可以创建自定义混合模式。

最后一组属性主要用于内部使用,控制了如何使用 WebGL 来渲染场景的具体细节。

高级属性

我们不会详细介绍这些属性。这些与 WebGL 内部工作方式有关。如果您确实想了解有关这些属性的更多信息,OpenGL 规范是一个很好的起点。您可以在www.khronos.org/registry/gles/specs/2.0/es_full_spec_2.0.25.pdf找到此规范。以下表格提供了这些高级属性的简要描述:

名称 描述
depthTest 这是一个高级的 WebGL 属性。使用此属性,您可以启用或禁用GL_DEPTH_TEST参数。此参数控制是否使用深度来确定新像素的值。通常情况下,您不需要更改此设置。有关更多信息,请参阅我们之前提到的 OpenGL 规范。
depthWrite 这是另一个内部属性。此属性可用于确定此材质是否影响 WebGL 深度缓冲区。如果您使用 2D 叠加对象(例如中心),则应将此属性设置为false。通常情况下,您不需要更改此属性。
polygonOffsetpolygonOffsetFactorpolygonOffsetUnits 使用这些属性,您可以控制POLYGON_OFFSET_FILL WebGL 特性。通常不需要这些。要详细了解它们的作用,可以查看 OpenGL 规范。
alphatest 可以设置为特定值(01)。每当像素的 alpha 值小于此值时,它将不会被绘制。您可以使用此属性来消除一些与透明度相关的伪影。

现在,让我们看看所有可用的材质,以便您可以看到这些属性对呈现输出的影响。

从一个简单的网格开始

在本节中,我们将看一些简单的材质:MeshBasicMaterialMeshDepthMaterialMeshNormalMaterialMeshFaceMaterial。我们从MeshBasicMaterial开始。

在我们查看这些材质的属性之前,这里有一个关于如何传递属性以配置材质的快速说明。有两个选项:

  • 您可以将参数作为参数对象传递给构造函数,就像这样:
var material = new THREE.MeshBasicMaterial(
{
  color: 0xff0000, name: 'material-1', opacity: 0.5, transparency: true, ...
});
  • 或者,您还可以创建一个实例并单独设置属性,就像这样:
var material = new THREE.MeshBasicMaterial();
material.color = new THREE.Color(0xff0000);
material.name = 'material-1';
material.opacity = 0.5;
material.transparency = true;

通常,最好的方法是在创建材质时知道所有属性的值时使用构造函数。这两种风格使用的参数格式相同。唯一的例外是color属性。在第一种风格中,我们可以直接传入十六进制值,Three.js 会自己创建一个THREE.Color对象。在第二种风格中,我们必须显式创建一个THREE.Color对象。在本书中,我们将使用这两种风格。

THREE.MeshBasicMaterial

MeshBasicMaterial是一个非常简单的材质,不考虑场景中可用的光源。使用此材质的网格将呈现为简单的平面多边形,您还可以选择显示几何的线框。除了我们在此材质的早期部分看到的常见属性之外,我们还可以设置以下属性:

名称 描述
color 此属性允许您设置材质的颜色。
wireframe 这允许您将材质呈现为线框。这对于调试很有用。
Wireframelinewidth 如果启用线框,此属性定义线框的宽度。
Wireframelinecap 此属性定义线框模式下线条端点的外观。可能的值为buttroundsquare。默认值为round。实际上,更改此属性的结果非常难以看到。此属性不受WebGLRenderer支持。
wireframeLinejoin 这定义了线条连接点的可视化方式。可能的值为roundbevelmiter。默认值为round。如果您仔细观察,可以在低opacity和非常大的wireframeLinewidth值的示例中看到这一点。此属性不受WebGLRenderer支持。
Shading 这定义了如何应用着色。可能的值为THREE.SmoothShadingTHREE.NoShadingTHREE.FlatShading。默认值为THREE.SmoothShading,这会产生一个平滑的对象,您看不到单独的面。此属性在此材质的示例中未启用。例如,请查看MeshNormalMaterial部分。
vertexColors 您可以使用此属性为每个顶点定义单独的颜色。默认值为THREE.NoColors。如果将此值设置为THREE.VertexColors,渲染器将考虑THREE.Geometrycolors属性上设置的颜色。此属性在CanvasRenderer上不起作用,但在WebGLRenderer上起作用。查看LineBasicMaterial示例,我们在其中使用此属性为线条的各个部分着色。您还可以使用此属性为此材质类型创建渐变效果。
fog 此属性确定此材质是否受全局雾设置的影响。这在实际中没有显示,但如果将其设置为false,我们在第二章中看到的全局雾不会影响对象的渲染方式。

在前几章中,我们看到了如何创建材质并将其分配给对象。对于THREE.MeshBasicMaterial,我们可以这样做:

var meshMaterial = new THREE.MeshBasicMaterial({color: 0x7777ff});

这将创建一个新的THREE.MeshBasicMaterial并将color属性初始化为0x7777ff(紫色)。

我添加了一个示例,您可以使用它来玩转THREE.MeshBasicMaterial属性和我们在上一节中讨论的基本属性。如果您在chapter-04文件夹中打开01-basic-mesh-material.html示例,您将看到一个如下截图所示的旋转立方体:

THREE.MeshBasicMaterial

这是一个非常简单的对象。在右上角的菜单中,您可以玩转属性并选择不同的网格(还可以更改渲染器)。例如,一个球体,opacity0.2transparent设置为truewireframe设置为truewireframeLinewidth9,并使用CanvasRenderer渲染如下:

THREE.MeshBasicMaterial

在此示例中,您可以设置的一个属性是side属性。使用此属性,您可以定义材质应用到THREE.Geometry的哪一侧。当您选择平面网格时,您可以测试此属性的工作原理。由于通常材质仅应用于材质的正面,因此旋转平面将在一半时间内不可见(当它向您展示背面时)。如果将side属性设置为double,则平面将始终可见,因为材质应用于几何体的两侧。但请注意,当side属性设置为double时,渲染器将需要做更多的工作,因此这可能会影响场景的性能。

THREE.MeshDepthMaterial

列表中的下一个材料是THREE.MeshDepthMaterial。使用这种材料,物体的外观不是由灯光或特定的材料属性定义的;而是由物体到摄像机的距离定义的。您可以将其与其他材料结合使用,轻松创建淡出效果。这种材料具有的唯一相关属性是以下两个控制是否要显示线框的属性:

名称 描述
wireframe 这决定是否显示线框。
wireframeLineWidth 这决定线框的宽度。

为了演示这一点,我们修改了来自第二章的立方体示例(chapter-04文件夹中的02-depth-material)。请记住,您必须单击addCube按钮才能填充场景。以下屏幕截图显示了修改后的示例:

THREE.MeshDepthMaterial

尽管该材料没有许多额外的属性来控制物体的渲染方式,但我们仍然可以控制物体颜色淡出的速度。在本例中,我们暴露了摄像机的nearfar属性。您可能还记得来自第二章的内容,构成 Three.js 场景的基本组件,通过这两个属性,我们设置了摄像机的可见区域。比near属性更接近摄像机的任何对象都不会显示出来,而比far属性更远的任何对象也会超出摄像机的可见区域。

摄像机的nearfar属性之间的距离定义了物体淡出的亮度和速度。如果距离非常大,物体远离摄像机时只会稍微淡出。如果距离很小,淡出效果将更加明显(如下面的屏幕截图所示):

THREE.MeshDepthMaterial

创建THREE.MeshDepthMaterial非常简单,对象不需要任何参数。在本例中,我们使用了scene.overrideMaterial属性,以确保场景中的所有对象都使用这种材料,而无需为每个THREE.Mesh对象显式指定它:

var scene = new THREE.Scene();
scene.overrideMaterial = new THREE.MeshDepthMaterial();

本章的下一部分实际上并不是关于特定材料,而是展示了如何将多种材料组合在一起的方法。

组合材料

如果您回顾一下THREE.MeshDepthMaterial的属性,您会发现没有选项来设置立方体的颜色。一切都是由材料的默认属性为您决定的。然而,Three.js 有将材料组合在一起创建新效果的选项(这也是混合发挥作用的地方)。以下代码显示了我们如何将材料组合在一起:

var cubeMaterial = new THREE.MeshDepthMaterial();
var colorMaterial = new THREE.MeshBasicMaterial({color: 0x00ff00, transparent: true, blending: THREE.MultiplyBlending})
var cube = new THREE.SceneUtils.createMultiMaterialObject(cubeGeometry, [colorMaterial, cubeMaterial]);
cube.children[1].scale.set(0.99, 0.99, 0.99);

我们得到了以下使用THREE.MeshDepthMaterial的亮度和THREE.MeshBasicMaterial的颜色的绿色立方体(打开03-combined-material.html查看此示例)。以下屏幕截图显示了示例:

组合材料

让我们看看您需要采取哪些步骤才能获得这个特定的结果。

首先,我们需要创建两种材质。对于THREE.MeshDepthMaterial,我们不需要做任何特殊处理;但是,对于THREE.MeshBasicMaterial,我们将transparent设置为true并定义一个blending模式。如果我们不将transparent属性设置为true,我们将只得到实心的绿色物体,因为 Three.js 不知道考虑已渲染的颜色。将transparent设置为true后,Three.js 将检查blending属性,以查看绿色的THREE.MeshBasicMaterial对象应如何与背景交互。在这种情况下,背景是用THREE.MeshDepthMaterial渲染的立方体。在第九章中,动画和移动相机,我们将更详细地讨论可用的各种混合模式。

然而,对于这个例子,我们使用了THREE.MultiplyBlending。这种混合模式将前景颜色与背景颜色相乘,并给出所需的效果。这个代码片段中的最后一行也很重要。当我们使用THREE.SceneUtils.createMultiMaterialObject()函数创建一个网格时,几何图形会被复制,并且会返回两个完全相同的网格组。如果我们在没有最后一行的情况下渲染这些网格,您应该会看到闪烁效果。当对象被渲染在另一个对象的上方并且其中一个对象是透明的时,有时会发生这种情况。通过缩小使用THREE.MeshDepthMaterial创建的网格,我们可以避免这种情况。为此,请使用以下代码:

cube.children[1].scale.set(0.99, 0.99, 0.99);

下一个材质也是一个我们无法影响渲染中使用的颜色的材质。

THREE.MeshNormalMaterial

理解这种材质如何渲染的最简单方法是先看一个例子。打开chapter-04文件夹中的04-mesh-normal-material.html示例。如果您选择球体作为网格,您将看到类似于这样的东西:

THREE.MeshNormalMaterial

正如你所看到的,网格的每个面都以稍微不同的颜色呈现,即使球体旋转,颜色也基本保持不变。这是因为每个面的颜色是基于面外指向的法线。这个法线是垂直于面的向量。法线向量在 Three.js 的许多不同部分中都有用到。它用于确定光的反射,帮助将纹理映射到 3D 模型上,并提供有关如何照亮、着色和着色表面像素的信息。幸运的是,Three.js 处理这些向量的计算并在内部使用它们,因此您不必自己计算它们。以下屏幕截图显示了THREE.SphereGeometry的所有法线向量:

THREE.MeshNormalMaterial

这个法线指向的方向决定了使用THREE.MeshNormalMaterial时面的颜色。由于球体的所有面的法线都指向不同的方向,我们得到了您在示例中看到的多彩球体。作为一个快速的旁注,要添加这些法线箭头,您可以像这样使用THREE.ArrowHelper

for (var f = 0, fl = sphere.geometry.faces.length; f < fl; f++) {
  var face = sphere.geometry.faces[ f ];
  var centroid = new THREE.Vector3(0, 0, 0);
  centroid.add(sphere.geometry.vertices[face.a]);
  centroid.add(sphere.geometry.vertices[face.b]);
  centroid.add(sphere.geometry.vertices[face.c]);
  centroid.divideScalar(3);

  var arrow = new THREE.ArrowHelper(face.normal, centroid, 2, 0x3333FF, 0.5, 0.5);
  sphere.add(arrow);
}

在这段代码片段中,我们遍历了THREE.SphereGeometry的所有面。对于每个THREE.Face3对象,我们通过添加构成该面的顶点并将结果除以 3 来计算中心(质心)。我们使用这个质心和面的法线向量来绘制一个箭头。THREE.ArrowHelper接受以下参数:directionoriginlengthcolorheadLengthheadWidth

您可以在THREE.MeshNormalMaterial上设置的其他一些属性:

名称 描述
wireframe 这决定是否显示线框。
wireframeLineWidth 这决定线框的宽度。
shading 这配置了平面着色和平滑着色。

我们已经看到了wireframewireframeLinewidth,但在我们的THREE.MeshBasicMaterial示例中跳过了shading属性。使用shading属性,我们可以告诉 Three.js 如何渲染我们的对象。如果使用THREE.FlatShading,每个面将按原样呈现(正如您在前面的几个屏幕截图中看到的),或者您可以使用THREE.SmoothShading,它会使我们对象的面变得更加平滑。例如,如果我们使用THREE.SmoothShading来渲染球体,结果看起来像这样:

THREE.MeshNormalMaterial

我们几乎完成了简单的材料。最后一个是THREE.MeshFaceMaterial

THREE.MeshFaceMaterial

基本材料中的最后一个实际上不是一个材料,而是其他材料的容器。THREE.MeshFaceMaterial允许您为几何体的每个面分配不同的材料。例如,如果您有一个立方体,它有 12 个面(请记住,Three.js 只使用三角形),您可以使用这种材料为立方体的每一面分配不同的材料(例如,不同的颜色)。使用这种材料非常简单,如下面的代码所示:

var matArray = [];
matArray.push(new THREE.MeshBasicMaterial( { color: 0x009e60 }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0x009e60 }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0x0051ba }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0x0051ba }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0xffd500 }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0xffd500 }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0xff5800 }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0xff5800 }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0xC41E3A }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0xC41E3A }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0xffffff }));
matArray.push(new THREE.MeshBasicMaterial( { color: 0xffffff }));

var faceMaterial = new THREE.MeshFaceMaterial(matArray);

var cubeGeom = new THREE.BoxGeometry(3,3,3);
var cube = new THREE.Mesh(cubeGeom, faceMaterial);

我们首先创建一个名为matArray的数组来保存所有的材料。接下来,我们创建一个新的材料,在这个例子中是THREE.MeshBasicMaterial,每个面的颜色都不同。有了这个数组,我们实例化THREE.MeshFaceMaterial,并将它与立方体几何一起使用来创建网格。让我们深入了解一下代码,并看看您需要做什么才能重新创建以下示例:一个简单的 3D 魔方。您可以在05-mesh-face-material.html中找到此示例。以下屏幕截图显示了此示例:

THREE.MeshFaceMaterial

这个魔方由许多小立方体组成:沿着x轴有三个立方体,沿着y轴有三个立方体,沿着z轴有三个立方体。这是如何完成的:

var group = new THREE.Mesh();
// add all the rubik cube elements
var mats = [];
mats.push(new THREE.MeshBasicMaterial({ color: 0x009e60 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0x009e60 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0x0051ba }));
mats.push(new THREE.MeshBasicMaterial({ color: 0x0051ba }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xffd500 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xffd500 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xff5800 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xff5800 }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xC41E3A }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xC41E3A }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xffffff }));
mats.push(new THREE.MeshBasicMaterial({ color: 0xffffff }));

var faceMaterial = new THREE.MeshFaceMaterial(mats);

for (var x = 0; x < 3; x++) {
  for (var y = 0; y < 3; y++) {
    for (var z = 0; z < 3; z++) {
      var cubeGeom = new THREE.BoxGeometry(2.9, 2.9, 2.9);
      var cube = new THREE.Mesh(cubeGeom, faceMaterial);
      cube.position.set(x * 3 - 3, y * 3, z * 3 - 3);

      group.add(cube);
    }
  }
}

在这段代码中,我们首先创建THREE.Mesh,它将容纳所有的单独立方体(group);接下来,我们为每个面创建材料并将它们推送到mats数组中。请记住,立方体的每一面都由两个面组成,所以我们需要 12 种材料。从这些材料中,我们创建THREE.MeshFaceMaterial。然后,我们创建三个循环,以确保我们创建了正确数量的立方体。在这个循环中,我们创建每个单独的立方体,分配材料,定位它们,并将它们添加到组中。您应该记住的是,立方体的位置是相对于这个组的位置的。如果我们移动或旋转组,所有的立方体都会随之移动和旋转。有关如何使用组的更多信息,请参阅第八章创建和加载高级网格和几何体

如果您在浏览器中打开了示例,您会看到整个魔方立方体旋转,而不是单独的立方体。这是因为我们在渲染循环中使用了以下内容:

group.rotation.y=step+=0.01;

这导致完整的组围绕其中心(0,0,0)旋转。当我们定位单独的立方体时,我们确保它们位于这个中心点周围。这就是为什么在前面的代码行中看到cube.position.set(x * 3 - 3, y * 3, z * 3 - 3);中的-3 偏移量。

提示

如果您查看这段代码,您可能会想知道 Three.js 如何确定要为特定面使用哪种材料。为此,Three.js 使用materialIndex属性,您可以在geometry.faces数组的每个单独的面上设置它。该属性指向我们在THREE.FaceMaterial对象的构造函数中添加的材料的数组索引。当您使用标准的 Three.js 几何体之一创建几何体时,Three.js 会提供合理的默认值。如果您想要其他行为,您可以为每个面自己设置materialIndex属性,以指向提供的材料之一。

THREE.MeshFaceMaterial是我们基本材质中的最后一个。在下一节中,我们将看一下 Three.js 中提供的一些更高级的材质。

高级材质

在这一部分,我们将看一下 Three.js 提供的更高级的材质。我们首先会看一下THREE.MeshPhongMaterialTHREE.MeshLambertMaterial。这两种材质对光源有反应,分别可以用来创建有光泽和无光泽的材质。在这一部分,我们还将看一下最多才多艺,但最难使用的材质之一:THREE.ShaderMaterial。使用THREE.ShaderMaterial,你可以创建自己的着色器程序,定义材质和物体的显示方式。

THREE.MeshLambertMaterial

这种材质可以用来创建无光泽的表面。这是一种非常易于使用的材质,可以响应场景中的光源。这种材质可以配置许多我们之前见过的属性:coloropacityshadingblendingdepthTestdepthWritewireframewireframeLinewidthwireframeLinecapwireframeLineJoinvertexColorsfog。我们不会详细讨论这些属性,而是专注于这种材质特有的属性。这样我们就只剩下以下四个属性了:

名称 描述
ambient 这是材质的环境颜色。这与我们在上一章看到的环境光一起使用。这种颜色与环境光提供的颜色相乘。默认为白色。
emissive 这是材质发出的颜色。它并不真正作为光源,但这是一个不受其他光照影响的纯色。默认为黑色。
wrapAround 如果将此属性设置为true,则启用半兰伯特光照技术。使用半兰伯特光照,光的衰减更加温和。如果有一个有严重阴影的网格,启用此属性将软化阴影并更均匀地分布光线。
wrapRGB wrapAround设置为 true 时,你可以使用THREE.Vector3来控制光的衰减速度。

这种材质的创建方式和其他材质一样。下面是它的创建方式:

var meshMaterial = new THREE.MeshLambertMaterial({color: 0x7777ff});

有关此材质的示例,请查看06-mesh-lambert-material.html。以下截图显示了此示例:

THREE.MeshLambertMaterial

正如你在前面的截图中看到的,这种材质看起来相当无光泽。我们还可以使用另一种材质来创建有光泽的表面。

THREE.MeshPhongMaterial

使用THREE.MeshPhongMaterial,我们可以创建一个有光泽的材质。你可以用于此的属性基本上与无光泽的THREE.MeshLambertMaterial对象相同。我们再次跳过基本属性和已经讨论过的属性:coloropacityshadingblendingdepthTestdepthWritewireframewireframeLinewidthwireframeLinecapwireframelineJoinvertexColors

这种材质的有趣属性如下表所示:

名称 描述
ambient 这是材质的环境颜色。这与我们在上一章看到的环境光一起使用。这种颜色与环境光提供的颜色相乘。默认为白色。
emissive 这是材质发出的颜色。它并不真正作为光源,但这是一个不受其他光照影响的纯色。默认为黑色。
specular 此属性定义材质有多光泽,以及以什么颜色发光。如果设置为与color属性相同的颜色,你会得到一个更金属质感的材质。如果设置为灰色,会得到一个更塑料质感的材质。
shininess 此属性定义镜面高光的光泽程度。光泽的默认值为30
metal 当此属性设置为true时,Three.js 使用略有不同的方式来计算像素的颜色,使对象看起来更像金属。请注意,效果非常微小。
wrapAround 如果将此属性设置为true,则启用半兰伯特光照技术。使用半兰伯特光照,光线的衰减更加微妙。如果网格有严重的黑暗区域,启用此属性将软化阴影并更均匀地分布光线。
wrapRGB wrapAround设置为true时,您可以使用THREE.Vector3来控制光线衰减的速度。

初始化THREE.MeshPhongMaterial对象的方式与我们已经看到的所有其他材质的方式相同,并且显示在以下代码行中:

var meshMaterial = new THREE.MeshPhongMaterial({color: 0x7777ff});

为了给你最好的比较,我们为这种材质创建了与THREE.MeshLambertMaterial相同的示例。您可以使用控制 GUI 来尝试这种材质。例如,以下设置会创建一个看起来像塑料的材质。您可以在07-mesh-phong-material.html中找到这个示例。以下屏幕截图显示了这个示例:

THREE.MeshPhongMaterial

我们将探讨的高级材质中的最后一个是THREE.ShaderMaterial

使用 THREE.ShaderMaterial 创建自己的着色器

THREE.ShaderMaterial是 Three.js 中最多功能和复杂的材质之一。使用这种材质,您可以传递自己的自定义着色器,直接在 WebGL 上下文中运行。着色器将 Three.js JavaScript 网格转换为屏幕上的像素。使用这些自定义着色器,您可以精确定义对象的渲染方式,以及如何覆盖或更改 Three.js 的默认设置。在本节中,我们暂时不会详细介绍如何编写自定义着色器。有关更多信息,请参阅第十一章, 自定义着色器和渲染后处理。现在,我们只会看一个非常基本的示例,展示如何配置这种材质。

THREE.ShaderMaterial有许多可以设置的属性,我们已经看到了。使用THREE.ShaderMaterial,Three.js 传递了有关这些属性的所有信息,但是您仍然必须在自己的着色器程序中处理这些信息。以下是我们已经看到的THREE.ShaderMaterial的属性:

名称 描述
wireframe 这将材质呈现为线框。这对于调试目的非常有用。
Wireframelinewidth 如果启用线框,此属性定义了线框的线宽。
linewidth 这定义了要绘制的线的宽度。
Shading 这定义了如何应用着色。可能的值是THREE.SmoothShadingTHREE.FlatShading。此属性在此材质的示例中未启用。例如,查看MeshNormalMaterial部分。
vertexColors 您可以使用此属性定义应用于每个顶点的单独颜色。此属性在CanvasRenderer上不起作用,但在WebGLRenderer上起作用。查看LineBasicMaterial示例,我们在该示例中使用此属性来给线的各个部分上色。
fog 这决定了这种材质是否受全局雾设置的影响。这并没有展示出来。如果设置为false,我们在第二章, 组成 Three.js 场景的基本组件中看到的全局雾不会影响对象的渲染方式。

除了这些传递到着色器的属性之外,THREE.ShaderMaterial还提供了一些特定属性,您可以使用这些属性将附加信息传递到自定义着色器中(它们目前可能看起来有点晦涩;有关更多详细信息,请参见第十一章自定义着色器和渲染后处理),如下所示:

名称 描述
fragmentShader 此着色器定义了传入的每个像素的颜色。在这里,您需要传递片段着色器程序的字符串值。
vertexShader 此着色器允许您更改传入的每个顶点的位置。在这里,您需要传递顶点着色器程序的字符串值。
uniforms 这允许您向着色器发送信息。相同的信息被发送到每个顶点和片段。
defines 转换为#define 代码片段。使用这些片段,您可以在着色器程序中设置一些额外的全局变量。
attributes 这些可以在每个顶点和片段之间改变。它们通常用于传递位置和与法线相关的数据。如果要使用这个,您需要为几何图形的所有顶点提供信息。
lights 这决定了是否应该将光数据传递到着色器中。默认值为false

在我们看一个例子之前,我们将简要解释ShaderMaterial的最重要部分。要使用这种材质,我们必须传入两种不同的着色器:

  • vertexShader:这在几何图形的每个顶点上运行。您可以使用此着色器通过移动顶点的位置来转换几何图形。

  • fragmentShader:这在几何图形的每个片段上运行。在vertexShader中,我们返回应该显示在这个特定片段上的颜色。

到目前为止,在本章中我们讨论的所有材质,Three.js 都提供了fragmentShadervertexShader,所以你不必担心这些。

在本节中,我们将看一个简单的例子,该例子使用了一个非常简单的vertexShader程序,该程序改变了立方体顶点的xyz坐标,以及一个fragmentShader程序,该程序使用了来自glslsandbox.com/的着色器创建了一个动画材质。

接下来,您可以看到我们将使用的vertexShader的完整代码。请注意,编写着色器不是在 JavaScript 中完成的。您需要使用一种称为GLSL的类似 C 的语言来编写着色器(WebGL 支持 OpenGL ES 着色语言 1.0——有关 GLSL 的更多信息,请参见www.khronos.org/webgl/):

<script id="vertex-shader" type="x-shader/x-vertex">
  uniform float time;

  void main()
  {
    vec3 posChanged = position;
    posChanged.x = posChanged.x*(abs(sin(time*1.0)));
    posChanged.y = posChanged.y*(abs(cos(time*1.0)));
    posChanged.z = posChanged.z*(abs(sin(time*1.0)));

    gl_Position = projectionMatrix * modelViewMatrix * vec4(posChanged,1.0);
  }
</script>

我们不会在这里详细讨论,只关注这段代码的最重要部分。要从 JavaScript 与着色器通信,我们使用一种称为 uniforms 的东西。在这个例子中,我们使用uniform float time;语句来传递外部值。根据这个值,我们改变传入顶点的xyz坐标(作为 position 变量传入):

vec3 posChanged = position;
posChanged.x = posChanged.x*(abs(sin(time*1.0)));
posChanged.y = posChanged.y*(abs(cos(time*1.0)));
posChanged.z = posChanged.z*(abs(sin(time*1.0)));

posChanged向量现在包含了基于传入的时间变量的这个顶点的新坐标。我们需要执行的最后一步是将这个新位置传递回 Three.js,这总是这样完成的:

gl_Position = projectionMatrix * modelViewMatrix * vec4(posChanged,1.0);

gl_Position变量是一个特殊变量,用于返回最终位置。接下来,我们需要创建shaderMaterial并传入vertexShader。为此,我们创建了一个简单的辅助函数,我们可以像这样使用:var meshMaterial1 = createMaterial("vertex-shader","fragment-shader-1");在下面的代码中:

function createMaterial(vertexShader, fragmentShader) {
  var vertShader = document.getElementById(vertexShader).innerHTML;
  var fragShader = document.getElementById(fragmentShader).innerHTML;

  var attributes = {};
  var uniforms = {
    time: {type: 'f', value: 0.2},
    scale: {type: 'f', value: 0.2},
    alpha: {type: 'f', value: 0.6},
    resolution: { type: "v2", value: new THREE.Vector2() }
  };

  uniforms.resolution.value.x = window.innerWidth;
  uniforms.resolution.value.y = window.innerHeight;

  var meshMaterial = new THREE.ShaderMaterial({
    uniforms: uniforms,
    attributes: attributes,
    vertexShader: vertShader,
    fragmentShader: fragShader,
    transparent: true

  });
  return meshMaterial;
}

参数指向 HTML 页面中script元素的 ID。在这里,您还可以看到我们设置了一个 uniforms 变量。这个变量用于将信息从我们的渲染器传递到我们的着色器。我们这个例子的完整渲染循环如下代码片段所示:

function render() {
  stats.update();

  cube.rotation.y = step += 0.01;
  cube.rotation.x = step;
  cube.rotation.z = step;

  cube.material.materials.forEach(function (e) {
    e.uniforms.time.value += 0.01;
  });

  // render using requestAnimationFrame
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}

您可以看到,我们每次运行渲染循环时都会将时间变量增加 0.01。此信息传递到vertexShader中,并用于计算我们立方体顶点的新位置。现在打开08-shader-material.html示例,您会看到立方体围绕其轴收缩和增长。以下屏幕截图显示了此示例的静态图像:

使用 THREE.ShaderMaterial 创建自定义着色器

在此示例中,您可以看到立方体的每个面都具有动画图案。分配给立方体每个面的片段着色器创建了这些图案。正如您可能已经猜到的那样,我们为此使用了THREE.MeshFaceMaterial(以及我们之前解释的createMaterial函数):

var cubeGeometry = new THREE.CubeGeometry(20, 20, 20);

var meshMaterial1 = createMaterial("vertex-shader", "fragment-shader-1");
var meshMaterial2 = createMaterial("vertex-shader", "fragment-shader-2");
var meshMaterial3 = createMaterial("vertex-shader", "fragment-shader-3");
var meshMaterial4 = createMaterial("vertex-shader", "fragment-shader-4");
var meshMaterial5 = createMaterial("vertex-shader", "fragment-shader-5");
var meshMaterial6 = createMaterial("vertex-shader", "fragment-shader-6");

var material = new THREE.MeshFaceMaterial([meshMaterial1, meshMaterial2, meshMaterial3, meshMaterial4, meshMaterial5, meshMaterial6]);

var cube = new THREE.Mesh(cubeGeometry, material);

我们尚未解释的部分是关于fragmentShader。在此示例中,所有fragmentShader对象都是从glslsandbox.com/复制的。该网站提供了一个实验性的游乐场,您可以在其中编写和共享fragmentShader对象。我不会在这里详细介绍,但在此示例中使用的fragment-shader-6如下所示:

<script id="fragment-shader-6" type="x-shader/x-fragment">
  #ifdef GL_ES
  precision mediump float;
  #endif

  uniform float time;
  uniform vec2 resolution;

  void main( void )
  {

    vec2 uPos = ( gl_FragCoord.xy / resolution.xy );

    uPos.x -= 1.0;
    uPos.y -= 0.5;

    vec3 color = vec3(0.0);
    float vertColor = 2.0;
    for( float i = 0.0; i < 15.0; ++i ) {
      float t = time * (0.9);

      uPos.y += sin( uPos.x*i + t+i/2.0 ) * 0.1;
      float fTemp = abs(1.0 / uPos.y / 100.0);
      vertColor += fTemp;
      color += vec3( fTemp*(10.0-i)/10.0, fTemp*i/10.0, pow(fTemp,1.5)*1.5 );
    }

    vec4 color_final = vec4(color, 1.0);
    gl_FragColor = color_final;
  }
</script>

最终传递给 Three.js 的颜色是使用gl_FragColor = color_final设置的颜色。更多了解fragmentShader的方法是探索glslsandbox.com/提供的内容,并使用代码创建自己的对象。在我们转移到下一组材质之前,这里是一个使用自定义vertexShader程序的更多示例(www.shadertoy.com/view/4dXGR4):

使用 THREE.ShaderMaterial 创建自定义着色器

有关片段和顶点着色器的更多内容,请参阅第十一章,“自定义着色器和渲染后处理”。

您可以用于线几何体的材质

我们将要查看的最后几种材质只能用于特定几何体:THREE.Line。顾名思义,这只是一个仅由顶点组成且不包含任何面的单条线。Three.js 提供了两种不同的材质,您可以用于线,如下所示:

  • THREE.LineBasicMaterial:线的基本材质允许您设置colorslinewidthlinecaplinejoin属性

  • THREE.LineDashedMaterial:具有与THREE.LineBasicMaterial相同的属性,但允许您通过指定虚线和间距大小来创建虚线效果

我们将从基本变体开始,然后再看虚线变体。

THREE.LineBasicMaterial

对于THREE.Line几何体可用的材质非常简单。以下表格显示了此材质可用的属性:

名称 描述
color 这确定了线的颜色。如果指定了vertexColors,则忽略此属性。
linewidth 这确定了线的宽度。
linecap 此属性定义了线框模式下线条末端的外观。可能的值是buttroundsquare。默认值是round。在实践中,更改此属性的结果很难看到。此属性不受WebGLRenderer支持。
linejoin 定义线接头的可视化方式。可能的值是roundbevelmiter。默认值是round。如果仔细观察,可以在使用低opacity和非常大的wireframeLinewidth的示例中看到这一点。此属性不受WebGLRenderer支持。
vertexColors 通过将此属性设置为THREE.VertexColors值,可以为每个顶点提供特定的颜色。
fog 这确定了这个对象是否受全局雾化属性的影响。

在我们查看 LineBasicMaterial 的示例之前,让我们先快速看一下如何从一组顶点创建 THREE.Line 网格,并将其与 LineMaterial 结合起来创建网格,如下所示的代码:

var points = gosper(4, 60);
var lines = new THREE.Geometry();
var colors = [];
var i = 0;
points.forEach(function (e) {
  lines.vertices.push(new THREE.Vector3(e.x, e.z, e.y));
  colors[ i ] = new THREE.Color(0xffffff);
  colors[ i ].setHSL(e.x / 100 + 0.5, (  e.y * 20 ) / 300, 0.8);
  i++;
});

lines.colors = colors;
var material = new THREE.LineBasicMaterial({
  opacity: 1.0,
  linewidth: 1,
  vertexColors: THREE.VertexColors });

var line = new THREE.Line(lines, material);

这段代码片段的第一部分 var points = gosper(4, 60); 用作获取一组 xy 坐标的示例。这个函数返回一个 gosper 曲线(更多信息,请查看 en.wikipedia.org/wiki/Gosper_curve),这是一个填充 2D 空间的简单算法。接下来我们创建一个 THREE.Geometry 实例,对于每个坐标,我们创建一个新的顶点,并将其推入该实例的 lines 属性中。对于每个坐标,我们还计算一个颜色值,用于设置 colors 属性。

提示

在这个例子中,我们使用 setHSL() 方法设置颜色。与提供红色、绿色和蓝色的值不同,使用 HSL,我们提供色调、饱和度和亮度。使用 HSL 比 RGB 更直观,更容易创建匹配的颜色集。关于 HSL 的非常好的解释可以在 CSS 规范中找到:www.w3.org/TR/2003/CR-css3-color-20030514/#hsl-color

现在我们有了几何体,我们可以创建 THREE.LineBasicMaterial,并将其与几何体一起使用,创建一个 THREE.Line 网格。你可以在 09-line-material.html 示例中看到结果。以下截图显示了这个示例:

THREE.LineBasicMaterial

我们将在本章讨论的下一个和最后一个材质,与 THREE.LineBasicMaterial 仅略有不同。使用 THREE.LineDashedMaterial,我们不仅可以给线条上色,还可以添加虚线效果。

THREE.LineDashedMaterial

这种材质与 THREE.LineBasicMaterial 具有相同的属性,还有两个额外的属性,可以用来定义虚线的宽度和虚线之间的间隙,如下所示:

名称 描述
scale 这会缩放 dashSizegapSize。如果比例小于 1dashSizegapSize 会增加,如果比例大于 1dashSizegapSize 会减少。
dashSize 这是虚线的大小。
gapSize 这是间隙的大小。

这种材质几乎与 THREE.LineBasicMaterial 完全相同。它的工作原理如下:

lines.computeLineDistances();
var material = new THREE.LineDashedMaterial({ vertexColors: true, color: 0xffffff, dashSize: 10, gapSize: 1, scale: 0.1 });

唯一的区别是你必须调用 computeLineDistances()(用于确定构成一条线的顶点之间的距离)。如果不这样做,间隙将无法正确显示。这种材质的示例可以在 10-line-material-dashed.html 中找到,并且看起来像以下截图:

THREE.LineDashedMaterial

总结

Three.js 提供了许多可以用来渲染几何体的材质。这些材质从非常简单的 (THREE.MeshBasicMaterial) 到复杂的 (THREE.ShaderMaterial) 都有,其中你可以提供自己的 vertexShaderfragmentShader 程序。材质共享许多基本属性。如果你知道如何使用单个材质,你可能也知道如何使用其他材质。请注意,并非所有材质都会对场景中的光源做出反应。如果你想要一个考虑光照效果的材质,可以使用 THREE.MeshPhongMaterialTHREE.MeshLamberMaterial。仅仅通过代码来确定某些材质属性的效果是非常困难的。通常,一个好主意是使用 dat.GUI 方法来尝试这些属性。

另外,请记住大部分材质的属性都可以在运行时修改。但有些属性(例如 side)是不能在运行时修改的。如果你改变了这样的值,你需要将 needsUpdate 属性设置为 true。关于在运行时可以和不可以改变的完整概述,请参考以下页面:github.com/mrdoob/three.js/wiki/Updates

在这一章和前面的章节中,我们谈到了几何体。我们在示例中使用了它们并探索了其中一些。在下一章中,你将学习关于几何体的一切,以及如何与它们一起工作。

第五章:学习使用几何图形

在之前的章节中,你学到了很多关于如何使用 Three.js 的知识。你知道如何创建一个基本场景,添加光照,并为你的网格配置材质。在第二章中,组成 Three.js 场景的基本组件,我们提到了 Three.js 提供的可用几何图形,但并没有详细讨论,你可以使用这些几何图形来创建你的 3D 对象。在本章和下一章中,我们将带你了解所有 Three.js 提供的几何图形(除了我们在上一章中讨论的THREE.Line)。在本章中,我们将看一下以下几何图形:

  • THREE.CircleGeometry

  • THREE.RingGeometry

  • THREE.PlaneGeometry

  • THREE.ShapeGeometry

  • THREE.BoxGeometry

  • THREE.SphereGeometry

  • THREE.CylinderGeometry

  • THREE.TorusGeometry

  • THREE.TorusKnotGeometry

  • THREE.PolyhedronGeometry

  • THREE.IcosahedronGeometry

  • THREE.OctahedronGeometry

  • THREE.TetraHedronGeometry

  • THREE.DodecahedronGeometry

在下一章中,我们将看一下以下复杂的几何图形:

  • THREE.ConvexGeometry

  • THREE.LatheGeometry

  • THREE.ExtrudeGeometry

  • THREE.TubeGeometry

  • THREE.ParametricGeometry

  • THREE.TextGeometry

让我们来看看 Three.js 提供的所有基本几何图形。

Three.js 提供的基本几何图形

在 Three.js 中,我们有一些几何图形会产生二维网格,还有更多的几何图形会创建三维网格。在本节中,我们首先看一下 2D 几何图形:THREE.CircleGeometryTHREE.RingGeometryTHREE.PlaneGeometryTHREE.ShapeGeometry。之后,我们将探索所有可用的基本 3D 几何图形。

二维几何图形

二维对象看起来像平面对象,正如其名称所示,只有两个维度。列表中的第一个二维几何图形是THREE.PlaneGeometry

THREE.PlaneGeometry

PlaneGeometry对象可用于创建一个非常简单的二维矩形。有关此几何图形的示例,请参阅本章源代码中的01-basic-2d-geometries-plane.html示例。使用PlaneGeometry创建的矩形如下截图所示:

THREE.PlaneGeometry

创建这个几何图形非常简单,如下所示:

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

THREE.PlaneGeometry的示例中,你可以更改这些属性,直接看到它对生成的 3D 对象的影响。这些属性的解释如下表所示:

属性 必填 描述
width 这是矩形的宽度。
height 这是矩形的高度。
widthSegments 这是宽度应该分成的段数。默认为1
heightSegments 这是高度应该分成的段数。默认为1

正如你所看到的,这不是一个非常复杂的几何图形。你只需指定大小,就完成了。如果你想创建更多的面(例如,当你想创建一个棋盘格图案时),你可以使用widthSegmentsheightSegments属性将几何图形分成更小的面。

在我们继续下一个几何图形之前,这里有一个关于本示例使用的材质的快速说明,我们在本章的大多数其他示例中也使用这种材质。我们使用以下方法基于几何图形创建网格:

function createMesh(geometry) {

  // assign two materials
  var meshMaterial = new THREE.MeshNormalMaterial();
  meshMaterial.side = THREE.DoubleSide;
  var wireframeMaterial = new THREE.MeshBasicMaterial();
  wireFrameMaterial.wireframe = true;

  // create a multimaterial
  var mesh = THREE.SceneUtils.createMultiMaterialObject(geometry,[meshMaterial,wireframeMaterial]);
  return mesh;
}

在这个函数中,我们基于提供的网格创建了一个多材质网格。首先使用的材质是THREE.MeshNormalMaterial。正如你在上一章中学到的,THREE.MeshNormalMaterial根据其法向量(面的方向)创建了彩色的面。我们还将这种材质设置为双面的(THREE.DoubleSide)。如果不这样做,当对象的背面对着摄像机时,我们就看不到它了。除了THREE.MeshNormalMaterial,我们还添加了THREE.MeshBasicMaterial,并启用了它的线框属性。这样,我们可以很好地看到对象的 3D 形状和为特定几何体创建的面。

提示

如果你想在创建后访问几何体的属性,你不能简单地说plane.width。要访问几何体的属性,你必须使用对象的parameters属性。因此,要获取本节中创建的plane对象的width属性,你必须使用plane.parameters.width

THREE.CircleGeometry

你可能已经猜到了THREE.CircleGeometry创建了什么。使用这个几何体,你可以创建一个非常简单的二维圆(或部分圆)。让我们先看看这个几何体的例子,02-basic-2d-geometries-circle.html。在下面的截图中,你可以找到一个例子,我们在其中使用了一个小于2 * PIthetaLength值:

THREE.CircleGeometry

注意,2 * PI代表弧度中的一个完整圆。如果你更喜欢使用度而不是弧度,那么在它们之间进行转换非常容易。以下两个函数可以帮助你在弧度和度之间进行转换,如下所示:

function deg2rad(degrees) {
  return degrees * Math.PI / 180;
}

function rad2deg(radians) {
  return radians * 180 / Math.PI;
}

在这个例子中,你可以看到并控制使用THREE.CircleGeometry创建的网格。当你创建THREE.CircleGeometry时,你可以指定一些属性来定义圆的外观,如下所示:

属性 必需 描述
radius 圆的半径定义了它的大小。半径是从圆心到边缘的距离。默认值为50
segments 此属性定义用于创建圆的面的数量。最小数量为3,如果未指定,则默认为8。较高的值意味着更平滑的圆形。
thetaStart 此属性定义从哪里开始绘制圆。这个值可以从02 * PI,默认值为0
thetaLength 此属性定义圆完成的程度。如果未指定,它默认为2 * PI(完整圆)。例如,如果你为这个值指定了0.5 * PI,你将得到一个四分之一圆。使用这个属性和thetaStart属性一起来定义圆的形状。

你可以使用以下代码片段创建一个完整的圆:

new THREE.CircleGeometry(3, 12);

如果你想从这个几何体创建半个圆,你可以使用类似于这样的代码:

new THREE.CircleGeometry(3, 12, 0, Math.PI);

在继续下一个几何体之前,快速说明一下 Three.js 在创建这些二维形状(THREE.PlaneGeometryTHREE.CircleGeometryTHREE.ShapeGeometry)时使用的方向:Three.js 创建这些对象时是竖立的,所以它们位于x-y平面上。这是非常合乎逻辑的,因为它们是二维形状。然而,通常情况下,特别是对于THREE.PlaneGeometry,你可能希望将网格放在地面上(x-z平面)——一些可以放置其余对象的地面区域。创建一个水平定向的二维对象的最简单方法是将网格围绕其x轴向后旋转一四分之一圈(-PI/2),如下所示:

mesh.rotation.x =- Math.PI/2;

这就是关于THREE.CircleGeometry的全部内容。下一个几何体THREE.RingGeometry看起来很像THREE.CircleGeometry

THREE.RingGeometry

使用THREE.RingGeometry,您可以创建一个二维对象,不仅与THREE.CircleGeometry非常相似,而且还允许您在中心定义一个孔(请参阅03-basic-3d-geometries-ring.html):

THREE.RingGeometry

THREE.RingGeometry没有任何必需的属性(请参阅下一个表格以获取默认值),因此要创建此几何体,您只需指定以下内容:

Var ring = new THREE.RingGeometry();

您可以通过将以下参数传递到构造函数来进一步自定义环形几何的外观:

属性 强制 描述
innerRadius 圆的内半径定义了中心孔的大小。如果将此属性设置为0,则不会显示孔。默认值为0
outerRadius 圆的外半径定义了其大小。半径是从圆的中心到其边缘的距离。默认值为50
thetaSegments 这是用于创建圆的对角线段数。较高的值意味着更平滑的环。默认值为8
phiSegments 这是沿着环的长度所需使用的段数。默认值为8。这实际上不影响圆的平滑度,但增加了面的数量。
thetaStart 这定义了从哪里开始绘制圆的位置。此值可以范围从02 * PI,默认值为0
thetaLength 这定义了圆完成的程度。当未指定时,默认为2 * PI(完整圆)。例如,如果为此值指定0.5 * PI,则将获得一个四分之一圆。将此属性与thetaStart属性一起使用以定义圆的形状。

在下一节中,我们将看一下二维形状的最后一个:THREE.ShapeGeometry

THREE.ShapeGeometry

THREE.PlaneGeometryTHREE.CircleGeometry在自定义外观方面的方式有限。如果要创建自定义的二维形状,可以使用THREE.ShapeGeometry。使用THREE.ShapeGeometry,您可以调用一些函数来创建自己的形状。您可以将此功能与 HTML 画布元素和 SVG 中也可用的<path>元素功能进行比较。让我们从一个示例开始,然后我们将向您展示如何使用各种函数来绘制自己的形状。本章的源代码中可以找到04-basic-2d-geometries-shape.html示例。以下屏幕截图显示了此示例:

THREE.ShapeGeometry

在此示例中,您可以看到一个自定义创建的二维形状。在描述属性之前,让我们首先看一下用于创建此形状的代码。在创建THREE.ShapeGeometry之前,我们首先必须创建THREE.Shape。您可以通过查看先前的屏幕截图来追踪这些步骤,从底部右侧开始。以下是我们创建THREE.Shape的方法:

function drawShape() {
  // create a basic shape
  var shape = new THREE.Shape();

  // startpoint
  shape.moveTo(10, 10);

  // straight line upwards
  shape.lineTo(10, 40);

  // the top of the figure, curve to the right
  shape.bezierCurveTo(15, 25, 25, 25, 30, 40);

  // spline back down
  shape.splineThru(
    [new THREE.Vector2(32, 30),
      new THREE.Vector2(28, 20),
      new THREE.Vector2(30, 10),
    ])

  // curve at the bottom
  shape.quadraticCurveTo(20, 15, 10, 10);

  // add 'eye' hole one
  var hole1 = new THREE.Path();
  hole1.absellipse(16, 24, 2, 3, 0, Math.PI * 2, true);
  shape.holes.push(hole1);

  // add 'eye hole 2'
  var hole2 = new THREE.Path();
  hole2.absellipse(23, 24, 2, 3, 0, Math.PI * 2, true);
  shape.holes.push(hole2);

  // add 'mouth'
  var hole3 = new THREE.Path();
  hole3.absarc(20, 16, 2, 0, Math.PI, true);
  shape.holes.push(hole3);

  // return the shape
  return shape;
}

在此代码片段中,您可以看到我们使用线条、曲线和样条创建了此形状的轮廓。之后,我们使用THREE.Shapeholes属性在此形状中打了一些孔。不过,在本节中,我们谈论的是THREE.ShapeGeometry而不是THREE.Shape。要从THREE.Shape创建几何体,我们需要将THREE.Shape(在我们的情况下从drawShape()函数返回)作为参数传递给THREE.ShapeGeometry,如下所示:

new THREE.ShapeGeometry(drawShape());

此函数的结果是一个可用于创建网格的几何体。当您已经拥有一个形状时,还有一种创建THREE.ShapeGeometry的替代方法。您可以调用shape.makeGeometry(options),它将返回THREE.ShapeGeometry的一个实例(有关选项的解释,请参见下一个表格)。

让我们首先看一下您可以传递给THREE.ShapeGeometry的参数:

属性 强制 描述
shapes 这些是用于创建THREE.Geometry的一个或多个THREE.Shape对象。您可以传入单个THREE.Shape对象或THREE.Shape对象的数组。

| options | 否 | 您还可以传入一些应用于使用shapes参数传入的所有形状的options。这些选项的解释在这里给出:

  • curveSegments:此属性确定从形状创建的曲线有多光滑。默认值为12

  • material:这是用于指定形状创建的面的materialIndex属性。当您将THREE.MeshFaceMaterial与此几何体一起使用时,materialIndex属性确定用于传入形状的面的材料。

  • UVGenerator:当您在材质中使用纹理时,UV 映射确定纹理的哪一部分用于特定的面。使用UVGenerator属性,您可以传入自己的对象,用于为传入的形状创建面的 UV 设置。有关 UV 设置的更多信息,请参阅第十章加载和使用纹理。如果没有指定,将使用THREE.ExtrudeGeometry.WorldUVGenerator

|

THREE.ShapeGeometry最重要的部分是THREE.Shape,您可以使用它来创建形状,因此让我们看一下您可以使用的绘制函数列表来创建THREE.Shape(请注意,这些实际上是THREE.Path对象的函数,THREE.Shape是从中扩展的)。

名称 描述
moveTo(x,y) 将绘图位置移动到指定的xy坐标。
lineTo(x,y) 从当前位置(例如,由moveTo函数设置)画一条线到提供的xy坐标。
quadraticCurveTo(aCPx, aCPy, x, y) 您可以使用两种不同的方式来指定曲线。您可以使用此quadraticCurveTo函数,也可以使用bezierCurveTo函数(请参阅下一行表)。这两个函数之间的区别在于您如何指定曲线的曲率。以下图解释了这两个选项之间的区别:THREE.ShapeGeometry对于二次曲线,我们需要指定一个额外的点(使用aCPxaCPy参数),曲线仅基于该点和,当然,指定的终点(xy参数)。对于三次曲线(由bezierCurveTo函数使用),您需要指定两个额外的点来定义曲线。起点是路径的当前位置。
bezierCurveTo(aCPx1, aCPy1, aCPx2, aCPy2, x, y) 根据提供的参数绘制曲线。有关说明,请参见上一个表条目。曲线是基于定义曲线的两个坐标(aCPx1aCPy1aCPx2aCPy2)和结束坐标(xy)绘制的。起点是路径的当前位置。
splineThru(pts) 此函数通过提供的坐标集(pts)绘制流线。此参数应该是THREE.Vector2对象的数组。起点是路径的当前位置。
arc(aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise) 这绘制一个圆(或圆的一部分)。圆从路径的当前位置开始。在这里,aXaY被用作从当前位置的偏移量。请注意,aRadius设置圆的大小,aStartAngleaEndAngle定义了绘制圆的一部分的大小。布尔属性aClockwise确定圆是顺时针绘制还是逆时针绘制。
absArc(aX, aY, aRadius, aStartAngle, aEndAngle, AClockwise) 参见arc的描述。位置是绝对的,而不是相对于当前位置。
ellipse(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise) 查看arc的描述。另外,使用ellipse函数,我们可以分别设置x半径和y半径。
absEllipse(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise) 查看ellipse的描述。位置是绝对的,而不是相对于当前位置。
fromPoints(vectors) 如果您将一个THREE.Vector2(或THREE.Vector3)对象数组传递给此函数,Three.js 将使用从提供的向量到直线创建路径。
holes holes属性包含一个THREE.Shape对象数组。数组中的每个对象都作为一个孔渲染。这个部分开头我们看到的示例就是一个很好的例子。在那个代码片段中,我们将三个THREE.Shape对象添加到了这个数组中。一个是主THREE.Shape对象的左眼,一个是右眼,一个是嘴巴。

在这个示例中,我们使用新的THREE.ShapeGeometry(drawShape()))构造函数从THREE.Shape对象创建了THREE.ShapeGeometryTHREE.Shape对象本身还有一些辅助函数,您可以用来创建几何体。它们如下:

名称 描述
makeGeometry(options) 这从THREE.Shape返回THREE.ShapeGeometry。有关可用选项的更多信息,请查看我们之前讨论的THREE.ShapeGeometry的属性。
createPointsGeometry(divisions) 这将形状转换为一组点。divisions属性定义了返回多少个点。如果这个值更高,将返回更多的点,生成的线条将更平滑。分割应用于路径的每个部分。
createSpacedPointsGeometry(divisions) 即使这将形状转换为一组点,但这次将分割应用于整个路径。

当您创建一组点时,使用createPointsGeometrycreateSpacedPointsGeometry;您可以使用创建的点来绘制一条线,如下所示:

new THREE.Line( shape.createPointsGeometry(10), new THREE.LineBasicMaterial( { color: 0xff3333, linewidth: 2 } ) );

当您在示例中点击asPointsasSpacedPoints按钮时,您将看到类似于这样的东西:

THREE.ShapeGeometry

这就是关于二维形状的全部内容。下一部分将展示和解释基本的三维形状。

三维几何体

在这个关于基本三维几何体的部分,我们将从我们已经看过几次的几何体THREE.BoxGeometry开始。

THREE.BoxGeometry

THREE.BoxGeometry是一个非常简单的 3D 几何体,允许您通过指定其宽度、高度和深度来创建一个立方体。我们添加了一个示例05-basic-3d-geometries-cube.html,您可以在其中尝试这些属性。以下屏幕截图显示了这个几何体:

THREE.BoxGeometry

正如您在此示例中所看到的,通过更改THREE.BoxGeometrywidthheightdepth属性,您可以控制生成网格的大小。当您创建一个新的立方体时,这三个属性也是必需的,如下所示:

new THREE.BoxGeometry(10,10,10);

在示例中,您还可以看到一些您可以在立方体上定义的其他属性。以下表格解释了所有这些属性:

属性 必需 描述
宽度 这是立方体的宽度。这是立方体顶点沿x轴的长度。
height 这是立方体的高度。这是立方体顶点沿y轴的长度。
depth 这是立方体的深度。这是立方体顶点沿z轴的长度。
widthSegments 这是我们沿着立方体x轴将一个面分成的段数。默认值为1
heightSegments 这是我们沿着立方体y轴将一个面分成的段数。默认值为1
depthSegments 这是我们沿着立方体的z轴将一个面分成的段数。默认值为1

通过增加各种段属性,您可以将立方体的六个主要面分成更小的面。如果您想要在立方体的部分上设置特定的材质属性,使用THREE.MeshFaceMaterial是很有用的。THREE.BoxGeometry是一个非常简单的几何体。另一个简单的是THREE.SphereGeometry

THREE.SphereGeometry

使用SphereGeometry,您可以创建一个三维球体。让我们直接进入示例06-basic-3d-geometries-sphere.html

THREE.SphereGeometry

在上一张截图中,我们向您展示了基于THREE.SphereGeometry创建的半开放球体。这种几何体非常灵活,可以用来创建各种与球体相关的几何体。然而,一个基本的THREE.SphereGeometry可以像这样轻松创建:new THREE.SphereGeometry()。以下属性可用于调整结果网格的外观:

属性 强制 描述
--- --- ---
半径 用于设置球体的半径。这定义了结果网格的大小。默认值为50
widthSegments 这是垂直使用的段数。更多的段意味着更光滑的表面。默认值为8,最小值为3
heightSegments 这是水平使用的段数。段数越多,球体表面越光滑。默认值为6,最小值为2
phiStart 这确定了沿着其x轴开始绘制球体的位置。这可以从02 * PI范围内,并且默认值为0
phiLength 这确定了球体从phiStart处绘制的距离。2 * PI将绘制一个完整的球体,0.5 * PI将绘制一个开放的四分之一球体。默认值为2 * PI
thetaStart 这确定了沿着其x轴开始绘制球体的位置。这可以从0PI范围内,并且默认值为0
thetaLength 这确定了从phiStart处绘制球体的距离。PI值是一个完整的球体,而0.5 * PI将只绘制球体的顶半部分。默认值为PI

radiuswidthSegmentsheightSegments属性应该很清楚。我们已经在其他示例中看到了这些类型的属性。phiStartphiLengththetaStartthetaLength属性在没有示例的情况下有点难以理解。不过幸运的是,您可以从06-basic-3d-geometries-sphere.html示例的菜单中尝试这些属性,并创建出有趣的几何体,比如这些:

THREE.SphereGeometry

列表中的下一个是THREE.CylinderGeometry

THREE.CylinderGeometry

使用这个几何体,我们可以创建圆柱体和类似圆柱体的对象。对于所有其他几何体,我们也有一个示例(07-basic-3d-geometries-cylinder.html),让您可以尝试这个几何体的属性,其截图如下:

THREE.CylinderGeometry

当您创建THREE.CylinderGeometry时,没有任何强制参数。因此,您可以通过调用new THREE.CylinderGeometry()来创建一个圆柱体。您可以传递多个属性,就像在示例中看到的那样,以改变这个圆柱体的外观。这些属性在下表中解释:

属性 强制 描述
--- --- ---
radiusTop 这设置了圆柱体顶部的大小。默认值为20
radiusBottom 这设置了圆柱体底部的大小。默认值为20
height 此属性设置圆柱体的高度。默认高度为100
radialSegments 这确定了圆柱体半径上的段数。默认值为8。更多的段数意味着更平滑的圆柱体。
heightSegments 这确定了圆柱体高度上的段数。默认值为1。更多的段数意味着更多的面。
openEnded 这确定网格在顶部和底部是否封闭。默认值为false

这些都是您可以用来配置圆柱体的非常基本的属性。然而,一个有趣的方面是当您为顶部(或底部)使用负半径时。如果这样做,您可以使用这个几何体来创建类似沙漏的形状,如下面的截图所示。需要注意的一点是,正如您从颜色中看到的那样,这种情况下的上半部分是里面翻转的。如果您使用的材质没有配置THREE.DoubleSide,您将看不到上半部分。

THREE.CylinderGeometry

下一个几何体是THREE.TorusGeometry,你可以用它来创建类似甜甜圈的形状。

THREE.TorusGeometry

圆环是一个简单的形状,看起来像一个甜甜圈。以下截图显示了THREE.TorusGeometry的示例:

THREE.TorusGeometry

就像大多数简单的几何体一样,在创建THREE.TorusGeometry时没有任何强制性参数。下表列出了创建此几何体时可以指定的参数:

属性 强制性 描述
radius 这设置了完整圆环的大小。默认值为100
tube 这设置了管道(实际甜甜圈)的半径。此属性的默认值为40
radialSegments 这确定了沿着圆环长度使用的段数。默认值为8。在演示中查看更改此值的效果。
tubularSegments 这确定了沿着圆环宽度使用的段数。默认值为6。在演示中查看更改此值的效果。
arc 通过这个属性,您可以控制圆环是否绘制完整圆。这个值的默认值是2 * PI(一个完整的圆)。

其中大多数都是非常基本的属性,arc属性是一个非常有趣的属性。通过这个属性,您可以定义甜甜圈是完整的圆还是部分圆。通过调整这个属性,您可以创建非常有趣的网格,比如下面这个弧度设置为0.5 * PI的网格:

THREE.TorusGeometry

THREE.TorusGeometry是一个非常直接的几何体。在下一节中,我们将看一个几何体,它几乎与它的名字相同,但要复杂得多:THREE.TorusKnotGeometry

THREE.TorusKnotGeometry

使用THREE.TorusKnotGeometry,您可以创建一个圆环结。圆环结是一种特殊的结,看起来像一个围绕自身缠绕了几次的管子。最好的解释方法是看09-basic-3d-geometries-torus-knot.html示例。下面的截图显示了这个几何体:

THREE.TorusKnotGeometry

如果您打开这个示例并尝试调整pq属性,您可以创建各种美丽的几何体。p属性定义了结的轴向绕组次数,q定义了结在其内部绕组的程度。如果这听起来有点模糊,不用担心。您不需要理解这些属性就可以创建美丽的结,比如下面截图中显示的一个(对于那些对细节感兴趣的人,维基百科在这个主题上有一篇很好的文章en.wikipedia.org/wiki/Torus_knot):

THREE.TorusKnotGeometry

通过这个几何体的示例,你可以尝试不同的pq的组合,看看它们对这个几何体的影响:

属性 强制性 描述
radius 这设置了整个环面的大小。默认值为100
tube 这设置了管道(实际甜甜圈)的半径。这个属性的默认值为40
radialSegments 这确定了沿着环面结的长度使用的段数。默认值为64。在演示中查看更改此值的效果。
tubularSegments 这确定了沿着环面结的宽度使用的段数。默认值为8。在演示中查看更改此值的效果。
p 这定义了结的形状,默认值为2
q 这定义了结的形状,默认值为3
heightScale 使用这个属性,你可以拉伸环面结。默认值为1

列表中的下一个几何体是基本几何体中的最后一个:THREE.PolyhedronGeometry

THREE.PolyhedronGeometry

使用这个几何体,你可以轻松创建多面体。多面体是一个只有平面面和直边的几何体。不过,大多数情况下,你不会直接使用这个几何体。Three.js 提供了许多特定的多面体,你可以直接使用,而不需要指定THREE.PolyhedronGeometry的顶点和面。我们将在本节的后面讨论这些多面体。如果你确实想直接使用THREE.PolyhedronGeometry,你必须指定顶点和面(就像我们在第三章中为立方体所做的那样,在 Three.js 中使用不同的光源)。例如,我们可以像这样创建一个简单的四面体(也可以参见本章中的THREE.TetrahedronGeometry):

var vertices = [
  1,  1,  1, 
  -1, -1,  1, 
  -1,  1, -1, 
  1, -1, -1
];

var indices = [
  2, 1, 0, 
  0, 3, 2, 
  1, 3, 0, 
  2, 3, 1
];

polyhedron = createMesh(new THREE.PolyhedronGeometry(vertices, indices, controls.radius, controls.detail));

要构建THREE.PolyhedronGeometry,我们传入verticesindicesradiusdetail属性。生成的THREE.PolyhedronGeometry对象显示在10-basic-3d-geometries-polyhedron.html示例中(在右上角的菜单中选择类型为:自定义):

THREE.PolyhedronGeometry

当你创建一个多面体时,你可以传入以下四个属性:

属性 强制性 描述
vertices 这些是组成多面体的点。
indices 这些是需要从顶点创建的面。
radius 这是多面体的大小。默认为1
detail 使用这个属性,你可以给多面体添加额外的细节。如果将其设置为1,多面体中的每个三角形将分成四个更小的三角形。如果将其设置为2,这四个更小的三角形将再次分成四个更小的三角形,依此类推。

在本节的开头,我们提到 Three.js 自带了一些多面体。在接下来的小节中,我们将快速展示这些多面体。

所有这些多面体类型都可以通过查看09-basic-3d-geometries-polyhedron.html示例来查看。

THREE.IcosahedronGeometry

THREE.IcosahedronGeometry 创建了一个由 12 个顶点创建的 20 个相同三角形面的多面体。创建这个多面体时,你只需要指定radiusdetail级别。这个截图显示了使用THREE.IcosahedronGeometry创建的多面体:

THREE.IcosahedronGeometry

THREE.TetrahedronGeometry

四面体是最简单的多面体之一。这个多面体只包含了由四个顶点创建的四个三角形面。你可以像创建 Three.js 提供的其他多面体一样创建THREE.TetrahedronGeometry,通过指定radiusdetail级别。下面是一个使用THREE.TetrahedronGeometry创建的四面体的截图:

THREE.TetrahedronGeometry

THREE.Octahedron Geometry

Three.js 还提供了一个八面体的实现。顾名思义,这个多面体有 8 个面。这些面是由 6 个顶点创建的。以下截图显示了这个几何图形:

THREE.Octahedron Geometry

THREE.DodecahedronGeometry

Three.js 提供的最后一个多面体几何图形是THREE.DodecahedronGeometry。这个多面体有 12 个面。以下截图显示了这个几何图形:

THREE.DodecahedronGeometry

这是关于 Three.js 提供的基本二维和三维几何图形的章节的结束。

总结

在本章中,我们讨论了 Three.js 提供的所有标准几何图形。正如你所看到的,有很多几何图形可以直接使用。为了更好地学习如何使用这些几何图形,尝试使用这些几何图形。使用本章的示例来了解你可以用来自定义 Three.js 提供的标准几何图形的属性。当你开始使用几何图形时,最好选择一个基本的材质;不要直接选择复杂的材质,而是从THREE.MeshBasicMaterial开始,将线框设置为true,或者使用THREE.MeshNormalMaterial。这样,你将更好地了解几何图形的真实形状。对于二维形状,重要的是要记住它们是放置在x-y平面上的。如果你想要水平放置一个二维形状,你需要围绕x轴旋转网格为-0.5 * PI。最后,要注意,如果你旋转一个二维形状,或者一个开放的三维形状(例如圆柱体或管道),记得将材质设置为THREE.DoubleSide。如果不这样做,你的几何图形的内部或背面将不会显示出来。

在本章中,我们专注于简单直接的网格。Three.js 还提供了创建复杂几何图形的方法。在下一章中,你将学习如何创建这些复杂几何图形。

第六章:高级几何形状和二进制操作

在上一章中,我们向你展示了 Three.js 提供的所有基本几何形状。除了这些基本几何形状,Three.js 还提供了一组更高级和专业化的对象。在本章中,我们将向你展示这些高级几何形状,并涵盖以下主题:

  • 如何使用高级几何形状,比如THREE.ConvexGeometryTHREE.LatheGeometryTHREE.TubeGeometry

  • 如何使用THREE.ExtrudeGeometry从 2D 形状创建 3D 形状。我们将根据使用 Three.js 提供的功能绘制的 2D 形状来做这个,我们将展示一个例子,其中我们基于外部加载的 SVG 图像创建 3D 形状。

  • 如果你想自己创建自定义形状,你可以很容易地修改我们在前几章中讨论的形状。然而,Three.js 还提供了一个THREE.ParamtericGeometry对象。使用这个对象,你可以基于一组方程创建几何形状。

  • 最后,我们将看看如何使用THREE.TextGeometry创建 3D 文本效果。

  • 此外,我们还将向你展示如何使用 Three.js 扩展 ThreeBSP 提供的二进制操作从现有的几何形状创建新的几何形状。

我们将从列表中的第一个开始,THREE.ConvexGeometry

THREE.ConvexGeometry

使用THREE.ConvexGeometry,我们可以围绕一组点创建凸包。凸包是包围所有这些点的最小形状。最容易理解的方法是通过一个例子来看。如果你打开01-advanced-3d-geometries-convex.html的例子,你会看到一个随机点集的凸包。以下截图显示了这个几何形状:

THREE.ConvexGeometry

在这个例子中,我们生成一组随机点,并基于这些点创建THREE.ConvexGeometry。在例子中,你可以点击redraw,这将生成 20 个新点并绘制凸包。我们还将每个点添加为一个小的THREE.SphereGeometry对象,以清楚地展示凸包的工作原理。THREE.ConvexGeometry没有包含在标准的 Three.js 发行版中,所以你必须包含一个额外的 JavaScript 文件来使用这个几何形状。在你的 HTML 页面顶部,添加以下内容:

<script src="../libs/ConvexGeometry.js"></script>

以下代码片段显示了这些点是如何创建并添加到场景中的:

function generatePoints() {
  // add 10 random spheres
  var points = [];
  for (var i = 0; i < 20; i++) {
    var randomX = -15 + Math.round(Math.random() * 30);
    var randomY = -15 + Math.round(Math.random() * 30);
    var randomZ = -15 + Math.round(Math.random() * 30);
    points.push(new THREE.Vector3(randomX, randomY, randomZ));
  }

  var group = new THREE.Object3D();
  var material = new THREE.MeshBasicMaterial({color: 0xff0000, transparent: false});
  points.forEach(function (point) {
    var geom = new THREE.SphereGeometry(0.2);
    var mesh = new THREE.Mesh(geom, material);
    mesh.position.clone(point);
    group.add(mesh);
  });

  // add the points as a group to the scene
  scene.add(group);
}

正如你在这段代码片段中看到的,我们创建了 20 个随机点(THREE.Vector3),并将它们推入一个数组中。接下来,我们遍历这个数组,并创建THREE.SphereGeometry,其位置设置为这些点之一(position.clone(point))。所有点都被添加到一个组中(更多内容请参阅第七章),所以我们可以通过旋转组来轻松旋转它们。

一旦你有了这组点,创建THREE.ConvexGeometry就非常容易,如下面的代码片段所示:

// use the same points to create a convexgeometry
var convexGeometry = new THREE.ConvexGeometry(points);
convexMesh = createMesh(convexGeometry);
scene.add(convexMesh);

包含顶点(THREE.Vector3类型)的数组是THREE.ConvexGeometry的唯一参数。关于createMesh()函数(这是我们在第五章中自己创建的函数)我们在这里调用。在上一章中,我们使用这种方法使用THREE.MeshNormalMaterial创建网格。对于这个例子,我们将其更改为半透明的绿色THREE.MeshBasicMaterial,以更好地显示我们创建的凸包和构成这个几何形状的单个点。

下一个复杂的几何形状是THREE.LatheGeometry,它可以用来创建类似花瓶的形状。

THREE.LatheGeometry

THREE.LatheGeometry允许您从平滑曲线创建形状。这条曲线由许多点(也称为节点)定义,通常称为样条。这个样条围绕对象的中心z轴旋转,产生类似花瓶和钟形的形状。再次,理解THREE.LatheGeometry的最简单方法是看一个例子。这个几何图形显示在02-advanced-3d-geometries-lathe.html中。以下来自示例的截图显示了这个几何图形:

THREE.LatheGeometry

在前面的截图中,您可以看到样条作为一组小红色球体。这些球体的位置与其他参数一起传递给THREE.LatheGeometry。在这个例子中,我们将这个样条旋转了半圈,基于这个样条,我们提取了您可以看到的形状。在我们查看所有参数之前,让我们看一下用于创建样条的代码以及THREE.LatheGeometry如何使用这个样条:

function generatePoints(segments, phiStart, phiLength) {
  // add 10 random spheres
  var points = [];
  var height = 5;
  var count = 30;
  for (var i = 0; i < count; i++) {
    points.push(new THREE.Vector3((Math.sin(i * 0.2) + Math.cos(i * 0.3)) * height + 12, 0, ( i - count ) + count / 2));
  }

  ...

  // use the same points to create a LatheGeometry
  var latheGeometry = new THREE.LatheGeometry (points, segments, phiStart, phiLength);
  latheMesh = createMesh(latheGeometry);
  scene.add(latheMesh);
}

在这段 JavaScript 中,您可以看到我们生成了 30 个点,它们的x坐标是基于正弦和余弦函数的组合,而z坐标是基于icount变量的。这创建了在前面截图中以红点可视化的样条。

基于这些要点,我们可以创建THREE.LatheGeometry。除了顶点数组之外,THREE.LatheGeometry还需要一些其他参数。以下表格列出了所有的参数:

属性 强制 描述
points 这些是用于生成钟形/花瓶形状的样条的点。
segments 这是在创建形状时使用的段数。这个数字越高,结果形状就越圆润。这个默认值是12
phiStart 这确定在生成形状时在圆上从哪里开始。这可以从02*PI。默认值是0
phiLength 这定义了形状生成的完整程度。例如,一个四分之一的形状将是0.5*PI。默认值是完整的360度或2*PI

在下一节中,我们将看一种通过从 2D 形状中提取 3D 几何图形的替代方法。

通过挤出创建几何图形

Three.js 提供了几种方法,可以将 2D 形状挤出为 3D 形状。通过挤出,我们指的是沿着它的z轴拉伸 2D 形状以将其转换为 3D。例如,如果我们挤出THREE.CircleGeometry,我们得到一个看起来像圆柱体的形状,如果我们挤出THREE.PlaneGeometry,我们得到一个类似立方体的形状。

挤出形状的最通用方法是使用THREE.ExtrudeGeometry对象。

THREE.ExtrudeGeometry

使用THREE.ExtrudeGeometry,您可以从 2D 形状创建 3D 对象。在我们深入了解这个几何图形的细节之前,让我们先看一个例子:03-extrude-geometry.html。以下来自示例的截图显示了这个几何图形:

THREE.ExtrudeGeometry

在这个例子中,我们取出了在上一章中创建的 2D 形状,并使用THREE.ExtrudeGeometry将其转换为 3D。正如您在这个截图中所看到的,形状沿着z轴被挤出,从而得到一个 3D 形状。创建THREE.ExtrudeGeometry的代码非常简单:

var options = {
  amount: 10,
  bevelThickness: 2,
  bevelSize: 1,
  bevelSegments: 3,
  bevelEnabled: true,
  curveSegments: 12,
  steps: 1
};

shape = createMesh(new THREE.ExtrudeGeometry(drawShape(), options));

在这段代码中,我们使用drawShape()函数创建了形状,就像在上一章中所做的那样。这个形状与一个options对象一起传递给THREE.ExtrudeGeometry构造函数。使用options对象,您可以精确地定义形状应该如何被挤出。以下表格解释了您可以传递给THREE.ExtrudeGeometry的选项。

属性 强制 描述
shapes 需要一个或多个形状(THREE.Shape对象)来从中挤出几何图形。请参阅前一章关于如何创建这样的形状。
amount 这确定形状应该被挤出的距离(深度)。默认值为100
bevelThickness 这确定倒角的深度。倒角是前后面和挤出之间的圆角。该值定义了倒角进入形状的深度。默认值为6
bevelSize 这确定倒角的高度。这加到形状的正常高度上。默认值为bevelThickness - 2
bevelSegments 这定义了用于倒角的段数。使用的段数越多,倒角看起来就越平滑。默认值为3
bevelEnabled 如果设置为true,则添加倒角。默认值为true
curveSegments 这确定在挤出形状的曲线时将使用多少段。使用的段数越多,曲线看起来就越平滑。默认值为12
steps 这定义了沿着深度将挤出分成多少段。默认值为1。较高的值将导致更多的单独面。
extrudePath 这是沿着形状应该被挤出的路径(THREE.CurvePath)。如果未指定,则形状沿着 z 轴被挤出。
material 这是用于正面和背面的材质的索引。如果要为正面和背面使用不同的材料,可以使用THREE.SceneUtils.createMultiMaterialObject函数创建网格。
extrudeMaterial 这是用于倒角和挤出的材料的索引。如果要为正面和背面使用不同的材料,可以使用THREE.SceneUtils.createMultiMaterialObject函数创建网格。
uvGenerator 当您在材质中使用纹理时,UV 映射确定了纹理的哪一部分用于特定的面。使用uvGenerator属性,您可以传入自己的对象,为传入的形状创建 UV 设置。有关 UV 设置的更多信息,请参阅第十章加载和使用纹理。如果未指定,将使用THREE.ExtrudeGeometry.WorldUVGenerator
frames 弗雷内框架用于计算样条的切线、法线和副法线。这在沿着extrudePath挤出时使用。您不需要指定这个,因为 Three.js 提供了自己的实现,THREE.TubeGeometry.FrenetFrames,这也是默认值。有关弗雷内框架的更多信息,请参阅en.wikipedia.org/wiki/Differential_geometry_of_curves#Frenet_frame

您可以使用03-extrude-geometry.html示例中的菜单来尝试这些选项。

在这个例子中,我们沿着 z 轴挤出了形状。正如您在选项中所看到的,您还可以使用extrudePath选项沿着路径挤出形状。在下面的几何图形THREE.TubeGeometry中,我们将这样做。

THREE.TubeGeometry

THREE.TubeGeometry创建沿着 3D 样条线挤出的管道。您可以使用一些顶点指定路径,THREE.TubeGeometry将创建管道。您可以在本章的源代码中找到一个可以尝试的示例(04-extrude-tube.html)。以下屏幕截图显示了这个示例:

THREE.TubeGeometry

正如您在这个例子中所看到的,我们生成了一些随机点,并使用这些点来绘制管道。通过右上角的控件,我们可以定义管道的外观,或者通过单击newPoints按钮生成新的管道。创建管道所需的代码非常简单,如下所示:

var points = [];
for (var i = 0 ; i < controls.numberOfPoints ; i++) {
  var randomX = -20 + Math.round(Math.random() * 50);
  var randomY = -15 + Math.round(Math.random() * 40);
  var randomZ = -20 + Math.round(Math.random() * 40);

  points.push(new THREE.Vector3(randomX, randomY, randomZ));
}

var tubeGeometry = new THREE.TubeGeometry(new THREE.SplineCurve3(points), segments, radius, radiusSegments, closed);

var tubeMesh = createMesh(tubeGeometry);
scene.add(tubeMesh);

我们首先需要获取一组THREE.Vector3类型的顶点,就像我们为THREE.ConvexGeometryTHREE.LatheGeometry所做的那样。然而,在我们可以使用这些点来创建管道之前,我们首先需要将这些点转换为THREE.SplineCurve3。换句话说,我们需要通过我们定义的点定义一个平滑的曲线。我们可以通过将顶点数组简单地传递给THREE.SplineCurve3的构造函数来实现这一点。有了这个样条和其他参数(我们稍后会解释),我们就可以创建管道并将其添加到场景中。

THREE.TubeGeometry除了THREE.SplineCurve3之外还需要一些其他参数。下表列出了THREE.TubeGeometry的所有参数:

属性 强制性 描述
path 这是描述管道应该遵循的路径的THREE.SplineCurve3
segments 这些是用于构建管道的段。默认值为64。路径越长,您应该指定的段数就越多。
radius 这是管道的半径。默认值为1
radiusSegments 这是沿着管道长度使用的段数。默认值为8。使用的越多,管道看起来就越
closed 如果设置为true,管道的起点和终点将连接在一起。默认值为false

我们将在本章中展示的最后一个挤出示例并不是真正不同的几何形状。在下一节中,我们将向您展示如何使用THREE.ExtrudeGeometry从现有的 SVG 路径创建挤出。

从 SVG 挤出

当我们讨论THREE.ShapeGeometry时,我们提到 SVG 基本上遵循绘制形状的相同方法。SVG 与 Three.js 处理形状的方式非常接近。在本节中,我们将看看如何使用来自github.com/asutherland/d3-threeD的一个小库,将 SVG 路径转换为 Three.js 形状。

对于05-extrude-svg.html示例,我使用了蝙蝠侠标志的 SVG 图形,并使用ExtrudeGeometry将其转换为 3D,如下面的屏幕截图所示:

从 SVG 挤出

首先,让我们看看原始的 SVG 代码是什么样的(当您查看此示例的源代码时,也可以自行查看):

<svg version="1.0"   x="0px" y="0px" width="1152px" height="1152px" xml:space="preserve">
  <g>
  <path  id="batman-path" style="fill:rgb(0,0,0);" d="M 261.135 114.535 C 254.906 116.662 247.491 118.825 244.659 119.344 C 229.433 122.131 177.907 142.565 151.973 156.101 C 111.417 177.269 78.9808 203.399 49.2992 238.815 C 41.0479 248.66 26.5057 277.248 21.0148 294.418 C 14.873 313.624 15.3588 357.341 21.9304 376.806 C 29.244 398.469 39.6107 416.935 52.0865 430.524 C 58.2431 437.23 63.3085 443.321 63.3431 444.06 ... 261.135 114.535 "/>
  </g>
</svg>

除非你是 SVG 大师,否则这对你来说可能毫无意义。但基本上,你在这里看到的是一组绘图指令。例如,C 277.987 119.348 279.673 116.786 279.673 115.867告诉浏览器绘制三次贝塞尔曲线,而L 489.242 111.787告诉我们应该画一条线到特定位置。幸运的是,我们不必自己编写代码来解释这些。使用 d3-threeD 库,我们可以自动转换这些。这个库最初是为了与优秀的D3.js库一起使用而创建的,但通过一些小的调整,我们也可以单独使用这个特定的功能。

提示

SVG代表可缩放矢量图形。这是一种基于 XML 的标准,可用于创建 Web 的基于矢量的 2D 图像。这是一种开放标准,受到所有现代浏览器的支持。然而,直接使用 SVG 并从 JavaScript 进行操作并不是非常直接的。幸运的是,有几个开源的 JavaScript 库可以使处理 SVG 变得更加容易。Paper.jsSnap.jsD3.jsRaphael.js是其中一些最好的。

以下代码片段显示了我们如何加载之前看到的 SVG,将其转换为THREE.ExtrudeGeometry,并显示在屏幕上:

function drawShape() {
  var svgString = document.querySelector("#batman-path").getAttribute("d");
  var shape = transformSVGPathExposed(svgString);
  return shape;
}

var options = {
  amount: 10,
  bevelThickness: 2,
  bevelSize: 1,
  bevelSegments: 3,
  bevelEnabled: true,
  curveSegments: 12,
  steps: 1
};

shape = createMesh(new THREE.ExtrudeGeometry(drawShape(), options));

在此代码片段中,您将看到对transformSVGPathExposed函数的调用。此函数由 d3-threeD 库提供,并将 SVG 字符串作为参数。我们直接从 SVG 元素获取此 SVG 字符串,方法是使用以下表达式:document.querySelector("#batman-path").getAttribute("d")。在 SVG 中,d属性包含用于绘制形状的路径语句。添加一个漂亮的闪亮材质和聚光灯,您就重新创建了此示例。

本节我们将讨论的最后一个几何图形是THREE.ParametricGeometry。使用此几何图形,您可以指定一些用于以编程方式创建几何图形的函数。

THREE.ParametricGeometry

使用THREE.ParametricGeometry,您可以基于方程创建几何图形。在深入研究我们自己的示例之前,一个好的开始是查看 Three.js 已经提供的示例。下载 Three.js 分发时,您会得到examples/js/ParametricGeometries.js文件。在此文件中,您可以找到几个示例方程,您可以与THREE.ParametricGeometry一起使用。最基本的示例是创建平面的函数:

function plane(u, v) {	
  var x = u * width;
  var y = 0;
  var z = v * depth;
  return new THREE.Vector3(x, y, z);
}

此函数由THREE.ParametricGeometry调用。uv值将从01范围,并且将针对从01的所有值调用大量次数。在此示例中,u值用于确定向量的x坐标,而v值用于确定z坐标。运行时,您将获得宽度为width和深度为depth的基本平面。

在我们的示例中,我们做了类似的事情。但是,我们不是创建一个平面,而是创建了一种波浪般的图案,就像您在06-parametric-geometries.html示例中看到的那样。以下屏幕截图显示了此示例:

THREE.ParametricGeometry

要创建此形状,我们将以下函数传递给THREE.ParametricGeometry

radialWave = function (u, v) {
  var r = 50;

  var x = Math.sin(u) * r;
  var z = Math.sin(v / 2) * 2 * r;
  var y = (Math.sin(u * 4 * Math.PI) + Math.cos(v * 2 * Math.PI)) * 2.8;

  return new THREE.Vector3(x, y, z);
}

var mesh = createMesh(new THREE.ParametricGeometry(radialWave, 120, 120, false));

正如您在此示例中所看到的,只需几行代码,我们就可以创建非常有趣的几何图形。在此示例中,您还可以看到我们可以传递给THREE.ParametricGeometry的参数。这些参数在下表中有解释:

属性 强制 描述
function 这是根据提供的uv值定义每个顶点位置的函数
slices 这定义了应将u值分成的部分数
stacks 这定义了应将v值分成的部分数

在转到本章的最后一部分之前,我想最后说明一下如何使用slicesstacks属性。我们提到uv属性被传递给提供的function参数,并且这两个属性的值范围从01。使用slicesstacks属性,我们可以定义传入函数的调用频率。例如,如果我们将slices设置为5stacks设置为4,则函数将使用以下值进行调用:

u:0/5, v:0/4
u:1/5, v:0/4
u:2/5, v:0/4
u:3/5, v:0/4
u:4/5, v:0/4
u:5/5, v:0/4
u:0/5, v:1/4
u:1/5, v:1/4
...
u:5/5, v:3/4
u:5/5, v:4/4

因此,此值越高,您就可以指定更多的顶点,并且创建的几何图形将更加平滑。您可以使用06-parametric-geometries.html示例右上角的菜单来查看此效果。

要查看更多示例,您可以查看 Three.js 分发中的examples/js/ParametricGeometries.js文件。该文件包含创建以下几何图形的函数:

  • 克莱因瓶

  • 平面

  • 平坦的莫比乌斯带

  • 3D 莫比乌斯带

  • Torus knot

  • 球体

本章的最后一部分涉及创建 3D 文本对象。

创建 3D 文本

在本章的最后一部分,我们将快速了解如何创建 3D 文本效果。首先,我们将看看如何使用 Three.js 提供的字体来渲染文本,然后我们将快速了解如何使用自己的字体来实现这一点。

渲染文本

在 Three.js 中渲染文本非常容易。你所要做的就是定义你想要使用的字体和我们在讨论THREE.ExtrudeGeometry时看到的基本挤出属性。以下截图显示了在 Three.js 中渲染文本的07-text-geometry.html示例:

渲染文本

创建这个 3D 文本所需的代码如下:

var options = {
  size: 90,
  height: 90,
  weight: 'normal',
  font: 'helvetiker',
  style: 'normal',
  bevelThickness: 2,
  bevelSize: 4,
  bevelSegments: 3,
  bevelEnabled: true,
  curveSegments: 12,
  steps: 1
};

// the createMesh is the same function we saw earlier
text1 = createMesh(new THREE.TextGeometry("Learning", options));
text1.position.z = -100;
text1.position.y = 100;
scene.add(text1);

text2 = createMesh(new THREE.TextGeometry("Three.js", options));
scene.add(text2);
};

让我们看看我们可以为THREE.TextGeometry指定的所有选项:

属性 强制性 描述
size No 这是文本的大小。默认值为100
height No 这是挤出的长度(深度)。默认值为50
weight No 这是字体的粗细。可能的值是normalbold。默认值是normal
font No 这是要使用的字体的名称。默认值是helvetiker
style No 这是字体的粗细。可能的值是normalitalic。默认值是normal
bevelThickness No 这是斜角的深度。斜角是正面和背面以及挤出之间的圆角。默认值为10
bevelSize No 这是斜角的高度。默认值为8
bevelSegments No 这定义了斜角使用的段数。段数越多,斜角看起来越平滑。默认值为3
bevelEnabled No 如果设置为true,则添加斜角。默认值为false
curveSegments No 这定义了在挤出形状的曲线时使用的段数。段数越多,曲线看起来越平滑。默认值为4
steps No 这定义了挤出物将被分成的段数。默认值为1
extrudePath No 这是形状应该沿着的路径。如果没有指定,形状将沿着z轴挤出。
material No 这是要用于正面和背面的材质的索引。使用THREE.SceneUtils.createMultiMaterialObject函数来创建网格。
extrudeMaterial No 这是用于斜角和挤出的材质的索引。使用THREE.SceneUtils.createMultiMaterialObject函数来创建网格。
uvGenerator No 当你在材质中使用纹理时,UV 映射决定了纹理的哪一部分用于特定的面。使用UVGenerator属性,你可以传入自己的对象,用于为传入的形状创建面的 UV 设置。有关 UV 设置的更多信息可以在第十章中找到,加载和使用纹理。如果没有指定,将使用THREE.ExtrudeGeometry.WorldUVGenerator
frames No 弗雷内框架用于计算样条的切线、法线和副法线。这在沿着extrudePath挤出时使用。你不需要指定这个,因为 Three.js 提供了自己的实现,THREE.TubeGeometry.FrenetFrames,它也被用作默认值。有关弗雷内框架的更多信息可以在en.wikipedia.org/wiki/Differential_geometry_of_curves#Frenet_frame找到。

Three.js 中包含的字体也被添加到了本书的资源中。你可以在assets/fonts文件夹中找到它们。

提示

如果你想在 2D 中渲染字体,例如将它们用作材质的纹理,你不应该使用THREE.TextGeometryTHREE.TextGeometry内部使用THREE.ExtrudeGeometry来构建 3D 文本,而 JavaScript 字体引入了很多开销。渲染简单的 2D 字体比仅仅使用 HTML5 画布更好。使用context.font,你可以设置要使用的字体,使用context.fillText,你可以将文本输出到画布上。然后你可以使用这个画布作为纹理的输入。我们将在第十章加载和使用纹理中向你展示如何做到这一点。

也可以使用其他字体与这个几何图形,但是你首先需要将它们转换为 JavaScript。如何做到这一点将在下一节中展示。

添加自定义字体

Three.js 提供了一些字体,你可以在场景中使用。这些字体基于typeface.js提供的字体(typeface.neocracy.org:81/)。Typeface.js 是一个可以将 TrueType 和 OpenType 字体转换为 JavaScript 的库。生成的 JavaScript 文件可以包含在你的页面中,然后可以在 Three.js 中使用该字体。

要转换现有的 OpenType 或 TrueType 字体,可以使用typeface.neocracy.org:81/fonts.html上的网页。在这个页面上,你可以上传一个字体,它将被转换为 JavaScript。请注意,这并不适用于所有类型的字体。字体越简单(更直线),在 Three.js 中使用时渲染正确的机会就越大。

要包含该字体,只需在你的 HTML 页面顶部添加以下行:

<script type="text/javascript" src="../assets/fonts/bitstream_vera_sans_mono_roman.typeface.js">
</script>

这将加载字体并使其可用于 Three.js。如果你想知道字体的名称(用于font属性),你可以使用以下一行 JavaScript 代码将字体缓存打印到控制台上:

console.log(THREE.FontUtils.faces);

这将打印出类似以下的内容:

添加自定义字体

在这里,你可以看到我们可以使用helvetiker字体,weightboldnormal,以及bitstream vera sans mono字体,weightnormal。请注意,每种字体重量都有单独的 JavaScript 文件,并且需要单独加载。确定字体名称的另一种方法是查看字体的 JavaScript 源文件。在文件的末尾,你会找到一个名为familyName的属性,如下面的代码所示。这个属性也包含了字体的名称:

"familyName":"Bitstream Vera Sans Mono"

在本章的下一部分中,我们将介绍 ThreeBSP 库,使用二进制操作intersectsubtractunion创建非常有趣的几何图形。

使用二进制操作来合并网格

在本节中,我们将看一种不同的创建几何图形的方法。到目前为止,在本章和上一章中,我们使用了 Three.js 提供的默认几何图形来创建有趣的几何图形。使用默认属性集,你可以创建美丽的模型,但是你受限于 Three.js 提供的内容。在本节中,我们将向你展示如何组合这些标准几何图形来创建新的几何图形——一种称为构造实体几何CSG)的技术。为此,我们使用了 Three.js 扩展 ThreeBSP,你可以在github.com/skalnik/ThreeBSP上找到。这个额外的库提供了以下三个函数:

名称 描述
intersect 此函数允许你基于两个现有几何图形的交集创建一个新的几何图形。两个几何图形重叠的区域将定义这个新几何图形的形状。
union union 函数可用于合并两个几何体并创建一个新的几何体。您可以将其与我们将在第八章中查看的mergeGeometry功能进行比较,创建和加载高级网格和几何体
subtract 减法函数是 union 函数的相反。您可以通过从第一个几何体中去除重叠区域来创建一个新的几何体。

在接下来的几节中,我们将更详细地查看每个函数。以下截图显示了仅使用unionsubtract功能后可以创建的示例。

使用二进制操作合并网格

要使用这个库,我们需要在页面中包含它。这个库是用 CoffeeScript 编写的,这是 JavaScript 的一个更用户友好的变体。要使其工作,我们有两个选项。我们可以添加 CoffeeScript 文件并即时编译它,或者我们可以预编译为 JavaScript 并直接包含它。对于第一种方法,我们需要执行以下操作:

<script type="text/javascript" src="../libs/coffee-script.js"></script>
<script type="text/coffeescript" src="../libs/ThreeBSP.coffee"></script>

ThreeBSP.coffee文件包含了我们在这个示例中需要的功能,coffee-script.js可以解释用于 ThreeBSP 的 Coffee 语言。我们需要采取的最后一步是确保ThreeBSP.coffee文件在我们开始使用 ThreeBSP 功能之前已经被完全解析。为此,我们在文件底部添加以下内容:

<script type="text/coffeescript">
  onReady();
</script>

我们将初始的onload函数重命名为onReady,如下所示:

function onReady() {
  // Three.js code
}

如果我们使用 CoffeeScript 命令行工具将 CoffeeScript 预编译为 JavaScript,我们可以直接包含生成的 JavaScript 文件。不过,在这之前,我们需要安装 CoffeeScript。您可以在 CoffeeScript 网站上按照安装说明进行安装coffeescript.org/。安装完 CoffeeScript 后,您可以使用以下命令行将 CoffeeScript ThreeBSP 文件转换为 JavaScript:

coffee --compile ThreeBSP.coffee

这个命令创建了一个ThreeBSP.js文件,我们可以像其他 JavaScript 文件一样在我们的示例中包含它。在我们的示例中,我们使用了第二种方法,因为它比每次加载页面时编译 CoffeeScript 要快。为此,我们只需要在我们的 HTML 页面顶部添加以下内容:

<script type="text/javascript" src="../libs/ThreeBSP.js"></script>

现在 ThreeBSP 库已加载,我们可以使用它提供的功能。

减法函数

在我们开始使用subtract函数之前,有一个重要的步骤需要记住。这三个函数使用网格的绝对位置进行计算。因此,如果您在应用这些函数之前将网格分组在一起或使用多个材质,可能会得到奇怪的结果。为了获得最佳和最可预测的结果,请确保您正在使用未分组的网格。

让我们首先演示“减法”功能。为此,我们提供了一个示例08-binary-operations.html。通过这个示例,您可以尝试这三种操作。当您首次打开二进制操作示例时,您会看到以下启动屏幕:

减法功能

有三个线框:一个立方体和两个球体。Sphere1,中心球体,是执行所有操作的对象,Sphere2位于右侧,Cube位于左侧。在Sphere2Cube上,您可以定义四种操作之一:subtractunionintersectnone。这些操作是从Sphere1的视角应用的。当我们将Sphere2设置为 subtract 并选择showResult(并隐藏线框)时,结果将显示Sphere1减去Sphere1Sphere2重叠的区域。请注意,这些操作中的一些可能需要几秒钟才能在您按下showResult按钮后完成,因此在busy指示器可见时请耐心等待。

下面的截图显示了减去另一个球体后的球体的结果动作:

减去函数

在这个示例中,首先执行了“Sphere2”定义的操作,然后执行了“Cube”的操作。因此,如果我们减去“Sphere2”和“Cube”(我们沿着 x 轴稍微缩放),我们会得到以下结果:

减去函数

理解“减去”功能的最佳方法就是玩弄一下示例。在这个示例中,ThreeBSP 代码非常简单,并且在redrawResult函数中实现,我们在示例中点击“showResult”按钮时调用该函数:

function redrawResult() {
  scene.remove(result);
  var sphere1BSP = new ThreeBSP(sphere1);
  var sphere2BSP = new ThreeBSP(sphere2);
  var cube2BSP = new ThreeBSP(cube);

  var resultBSP;

  // first do the sphere
  switch (controls.actionSphere) {
    case "subtract":
      resultBSP = sphere1BSP.subtract(sphere2BSP);
    break;
    case "intersect":
      resultBSP = sphere1BSP.intersect(sphere2BSP);
    break;
    case "union":
      resultBSP = sphere1BSP.union(sphere2BSP);
    break;
    case "none": // noop;
  }

  // next do the cube
  if (!resultBSP) resultBSP = sphere1BSP;
  switch (controls.actionCube) {
    case "subtract":
      resultBSP = resultBSP.subtract(cube2BSP);
    break;
    case "intersect":
      resultBSP = resultBSP.intersect(cube2BSP);
    break;
    case "union":
      resultBSP = resultBSP.union(cube2BSP);
    break;
    case "none": // noop;
  }

  if (controls.actionCube === "none" && controls.actionSphere === "none") {
  // do nothing
  } else {
    result = resultBSP.toMesh();
    result.geometry.computeFaceNormals();
    result.geometry.computeVertexNormals();
    scene.add(result);
  }
}

在这段代码中,我们首先将我们的网格(你可以看到的线框)包装在一个ThreeBSP对象中。这使我们能够在这些对象上应用“减去”、“交集”和“联合”功能。现在,我们可以在包装在中心球体周围的ThreeBSP对象上调用我们想要的特定功能,这个函数的结果将包含我们创建新网格所需的所有信息。要创建这个网格,我们只需在sphere1BSP对象上调用toMesh()函数。在结果对象上,我们必须确保所有的法线都通过首先调用computeFaceNormals然后调用computeVertexNormals()来正确计算。这些计算函数需要被调用,因为通过运行二进制操作之一,几何体的顶点和面会发生变化,这会影响面的法线。显式地重新计算它们将确保你的新对象被平滑地着色(当材质上的着色设置为THREE.SmoothShading时)并正确渲染。最后,我们将结果添加到场景中。

对于“交集”和“联合”,我们使用完全相同的方法。

交集函数

在前一节中我们解释的一切,对于“交集”功能来说并没有太多需要解释的了。使用这个功能,只有重叠的部分是留下来的网格。下面的截图是一个示例,其中球体和立方体都设置为相交:

交集函数

如果你看一下示例并玩弄一下设置,你会发现很容易创建这些类型的对象。记住,这可以应用于你可以创建的每一个网格,甚至是我们在本章中看到的复杂网格,比如THREE.ParametricGeometryTHREE.TextGeometry

“减去”和“交集”功能一起运行得很好。我们在本节开头展示的示例是通过首先减去一个较小的球体来创建一个空心球体。之后,我们使用立方体与这个空心球体相交,得到以下结果(带有圆角的空心立方体):

交集函数

ThreeBSP 提供的最后一个功能是“联合”功能。

联合函数

ThreeBSP 提供的最后一个功能是最不有趣的。使用这个功能,我们可以将两个网格组合在一起创建一个新的网格。因此,当我们将这个应用于两个球体和立方体时,我们将得到一个单一的对象——联合函数的结果:

联合函数

这并不是真的很有用,因为 Three.js 也提供了这个功能(参见第八章,“创建和加载高级网格和几何体”,在那里我们解释了如何使用THREE.Geometry.merge),而且性能稍微更好。如果启用旋转,你会发现这个联合是从中心球体的角度应用的,因为它是围绕那个球体的中心旋转的。其他两个操作也是一样的。

摘要

在本章中,我们看到了很多内容。我们介绍了一些高级几何图形,甚至向你展示了如何使用一些简单的二进制操作来创建有趣的几何图形。我们向你展示了如何使用高级几何图形,比如THREE.ConvexGeometryTHREE.TubeGeometryTHREE.LatheGeometry来创建非常漂亮的形状,并且可以尝试这些几何图形来获得你想要的结果。一个非常好的特性是,我们还可以将现有的 SVG 路径转换为 Three.js。不过,请记住,你可能仍然需要使用诸如 GIMP、Adobe Illustrator 或 Inkscape 等工具来微调路径。

如果你想创建 3D 文本,你需要指定要使用的字体。Three.js 自带了一些你可以使用的字体,但你也可以创建自己的字体。然而,请记住,复杂的字体通常不会正确转换。最后,使用 ThreeBSP,你可以访问三种二进制操作,可以应用到你的网格上:联合、减去和相交。使用联合,你可以将两个网格组合在一起;使用减去,你可以从源网格中移除重叠部分的网格;使用相交,只有重叠部分被保留。

到目前为止,我们看到了实体(或线框)几何图形,其中顶点相互连接形成面。在接下来的章节中,我们将看一种用称为粒子的东西来可视化几何图形的替代方法。使用粒子,我们不渲染完整的几何图形——我们只将顶点渲染为空间中的点。这使你能够创建外观出色且性能良好的 3D 效果。

第七章:粒子、精灵和点云

在之前的章节中,我们讨论了 Three.js 提供的最重要的概念、对象和 API。在本章中,我们将研究到目前为止我们跳过的唯一概念:粒子。使用粒子(有时也称为精灵),非常容易创建许多小对象,你可以用来模拟雨、雪、烟雾和其他有趣的效果。例如,你可以将单个几何体渲染为一组粒子,并分别控制这些粒子。在本章中,我们将探索 Three.js 提供的各种粒子特性。更具体地说,在本章中,我们将研究以下主题:

  • 使用THREE.SpriteMaterial创建和设置粒子的样式

  • 使用点云创建一组分组的粒子

  • 从现有几何体创建点云

  • 动画粒子和粒子系统

  • 使用纹理来设置粒子的样式

  • 使用THREE.SpriteCanvasMaterial使用画布设置粒子的样式

让我们先来探讨一下什么是粒子,以及如何创建一个。不过,在我们开始之前,关于本章中使用的一些名称,有一个快速说明。在最近的 Three.js 版本中,与粒子相关的对象的名称已经发生了变化。我们在本章中使用的THREE.PointCloud,以前被称为THREE.ParticleSystemTHREE.Sprite以前被称为THREE.Particle,材质也经历了一些名称的变化。因此,如果你看到使用这些旧名称的在线示例,请记住它们谈论的是相同的概念。在本章中,我们使用了最新版本 Three.js 引入的新命名约定。

理解粒子

就像我们对大多数新概念一样,我们将从一个例子开始。在本章的源代码中,你会找到一个名为01-particles.html的例子。打开这个例子,你会看到一个非常不起眼的白色立方体网格,如下面的截图所示:

理解粒子

在这个截图中,你看到的是 100 个精灵。精灵是一个始终面向摄像机的二维平面。如果你创建一个没有任何属性的精灵,它们会被渲染为小的、白色的、二维的正方形。这些精灵是用以下代码创建的:

function createSprites() {
  var material = new THREE.SpriteMaterial();
  for (var x = -5; x < 5; x++) {
    for (var y = -5; y < 5; y++) {
      var sprite = new THREE.Sprite(material);
      sprite.position.set(x * 10, y * 10, 0);
      scene.add(sprite);
    }
  }
}

在这个例子中,我们使用THREE.Sprite(material)构造函数手动创建精灵。我们传入的唯一项是一个材质。这必须是THREE.SpriteMaterialTHREE.SpriteCanvasMaterial。我们将在本章的其余部分更深入地研究这两种材质。

在我们继续研究更有趣的粒子之前,让我们更仔细地看一看THREE.Sprite对象。THREE.Sprite对象扩展自THREE.Object3D对象,就像THREE.Mesh一样。这意味着你可以使用大多数从THREE.Mesh中了解的属性和函数在THREE.Sprite上。你可以使用position属性设置其位置,使用scale属性缩放它,并使用translate属性相对移动它。

提示

请注意,在较旧版本的 Three.js 中,你无法使用THREE.Sprite对象与THREE.WebGLRenderer一起使用,只能与THREE.CanvasRenderer一起使用。在当前版本中,THREE.Sprite对象可以与两种渲染器一起使用。

使用THREE.Sprite,你可以非常容易地创建一组对象并在场景中移动它们。当你使用少量对象时,这很有效,但是当你想要使用大量THREE.Sprite对象时,你很快就会遇到性能问题,因为每个对象都需要被 Three.js 单独管理。Three.js 提供了另一种处理大量精灵(或粒子)的方法,使用THREE.PointCloud。使用THREE.PointCloud,Three.js 不需要单独管理许多个THREE.Sprite对象,而只需要管理THREE.PointCloud实例。

要获得与之前看到的屏幕截图相同的结果,但这次使用THREE.PointCloud,我们执行以下操作:

function createParticles() {

  var geom = new THREE.Geometry();
  var material = new THREE.PointCloudMaterial({size: 4, vertexColors: true, color: 0xffffff});

  for (var x = -5; x < 5; x++) {
    for (var y = -5; y < 5; y++) {
      var particle = new THREE.Vector3(x * 10, y * 10, 0);
      geom.vertices.push(particle);
      geom.colors.push(new THREE.Color(Math.random() * 0x00ffff));
    }
  }

  var cloud = new THREE.PointCloud(geom, material);
  scene.add(cloud);
}

正如您所看到的,对于每个粒子(云中的每个点),我们需要创建一个顶点(由THREE.Vector3表示),将其添加到THREE.Geometry中,使用THREE.GeometryTHREE.PointCloudMaterial创建THREE.PointCloud,并将云添加到场景中。THREE.PointCloud的示例(带有彩色方块)可以在02-particles-webgl.html示例中找到。以下屏幕截图显示了此示例:

理解粒子

在接下来的几节中,我们将进一步探讨THREE.PointCloud

粒子,THREE.PointCloud 和 THREE.PointCloudMaterial

在上一节的最后,我们快速介绍了THREE.PointCloudTHREE.PointCloud的构造函数接受两个属性:几何体和材质。材质用于着色和纹理粒子(稍后我们将看到),几何体定义了单个粒子的位置。用于定义几何体的每个顶点和每个点都显示为一个粒子。当我们基于THREE.BoxGeometry创建THREE.PointCloud时,我们会得到 8 个粒子,每个粒子代表立方体的每个角落。不过,通常情况下,您不会从标准的 Three.js 几何体之一创建THREE.PointCloud,而是手动将顶点添加到从头创建的几何体中(或使用外部加载的模型),就像我们在上一节的最后所做的那样。在本节中,我们将深入探讨这种方法,并查看如何使用THREE.PointCloudMaterial来设置粒子的样式。我们将使用03-basic-point-cloud.html示例来探索这一点。以下屏幕截图显示了此示例:

粒子,THREE.PointCloud 和 THREE.PointCloudMaterial

在此示例中,我们创建了THREE.PointCloud,并用 15000 个粒子填充它。所有粒子都使用THREE.PointCloudMaterial进行样式设置。要创建THREE.PointCloud,我们使用了以下代码:

function createParticles(size, transparent, opacity, vertexColors, sizeAttenuation, color) {

  var geom = new THREE.Geometry();
  var material = new THREE.PointCloudMaterial({size: size, transparent: transparent, opacity: opacity, vertexColors: vertexColors, sizeAttenuation: sizeAttenuation, color: color});

  var range = 500;
  for (var i = 0; i < 15000; i++) {
    var particle = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range - range / 2, Math.random() * range - range / 2);
    geom.vertices.push(particle);
    var color = new THREE.Color(0x00ff00);
    color.setHSL(color.getHSL().h, color.getHSL().s, Math.random() * color.getHSL().l);
    geom.colors.push(color);
  }

  cloud = new THREE.PointCloud(geom, material);
  scene.add(cloud);
}

在此列表中,我们首先创建THREE.Geometry。我们将粒子表示为THREE.Vector3添加到此几何体中。为此,我们创建了一个简单的循环,以随机位置创建THREE.Vector3并将其添加。在同一个循环中,我们还指定了颜色数组geom.colors,当我们将THREE.PointCloudMaterialvertexColors属性设置为true时使用。最后要做的是创建THREE.PointCloudMaterial并将其添加到场景中。

下表解释了您可以在THREE.PointCloudMaterial上设置的所有属性:

名称 描述
color 这是ParticleSystem中所有粒子的颜色。将vertexColors属性设置为 true,并使用几何体的颜色属性指定颜色会覆盖此属性(更准确地说,顶点的颜色将与此值相乘以确定最终颜色)。默认值为0xFFFFFF
map 使用此属性,您可以将纹理应用于粒子。例如,您可以使它们看起来像雪花。此属性在此示例中未显示,但在本章后面会有解释。
size 这是粒子的大小。默认值为1
sizeAnnutation 如果将其设置为 false,则所有粒子的大小都将相同,而不管它们距离摄像机的位置有多远。如果将其设置为 true,则大小基于距离摄像机的距离。默认值为true
vertexColors 通常,THREE.PointCloud中的所有粒子都具有相同的颜色。如果将此属性设置为THREE.VertexColors并且填充了几何体中的颜色数组,则将使用该数组中的颜色(还请参阅此表中的颜色条目)。默认值为THREE.NoColors
opacity 这与 transparent 属性一起设置了粒子的不透明度。默认值为1(不透明)。
透明 如果设置为 true,则粒子将以不透明度属性设置的不透明度进行渲染。默认值为false
混合 这是渲染粒子时使用的混合模式。有关混合模式的更多信息,请参见第九章动画和移动摄像机
这决定了粒子是否受到添加到场景中的雾的影响。默认值为true

上一个示例提供了一个简单的控制菜单,您可以使用它来实验特定于THREE.ParticleCloudMaterial的属性。

到目前为止,我们只将粒子呈现为小立方体,这是默认行为。然而,您还有一些其他方式可以用来设置粒子的样式:

  • 我们可以应用THREE.SpriteCanvasMaterial(仅适用于THREE.CanvasRenderer)来使用 HTML 画布元素的结果作为纹理

  • 使用THREE.SpriteMaterial和基于 HTML5 的纹理在使用THREE.WebGLRenderer时使用 HTML 画布的输出

  • 使用THREE.PointCloudMaterialmap属性加载外部图像文件(或使用 HTML5 画布)来为THREE.ParticleCloud的所有粒子设置样式

在下一节中,我们将看看如何做到这一点。

使用 HTML5 画布对粒子进行样式设置

Three.js 提供了三种不同的方式,您可以使用 HTML5 画布来设置粒子的样式。如果您使用THREE.CanvasRenderer,您可以直接从THREE.SpriteCanvasMaterial引用 HTML5 画布。当您使用THREE.WebGLRenderer时,您需要采取一些额外的步骤来使用 HTML5 画布来设置粒子的样式。在接下来的两节中,我们将向您展示不同的方法。

使用 HTML5 画布与 THREE.CanvasRenderer

使用THREE.SpriteCanvasMaterial,您可以使用 HTML5 画布的输出作为粒子的纹理。这种材质是专门为THREE.CanvasRenderer创建的,并且只在使用这个特定的渲染器时才有效。在我们看如何使用这种材质之前,让我们先看看您可以在这种材质上设置的属性:

名称 描述
颜色 这是粒子的颜色。根据指定的混合模式,这会影响画布图像的颜色。
program 这是一个以画布上下文作为参数的函数。当粒子被渲染时,将调用此函数。对这个 2D 绘图上下文的调用的输出显示为粒子。
不透明度 这决定了粒子的不透明度。默认值为1,即不透明。
透明 这决定了粒子是否是透明的。这与不透明度属性一起使用。
混合 这是要使用的混合模式。有关更多详细信息,请参见第九章动画和移动摄像机
旋转 这个属性允许您旋转画布的内容。通常需要将其设置为 PI,以正确对齐画布的内容。请注意,这个属性不能传递给材质的构造函数,而需要显式设置。

要查看THREE.SpriteCanvasMaterial的实际效果,您可以打开04-program-based-sprites.html示例。以下屏幕截图显示了这个例子:

使用 HTML5 画布与 THREE.CanvasRenderer

在这个例子中,粒子是在createSprites函数中创建的:

function createSprites() {

  var material = new THREE.SpriteCanvasMaterial({
    program: draw,
    color: 0xffffff});
   material.rotation = Math.PI;

  var range = 500;
  for (var i = 0; i < 1000; i++) {
    var sprite = new THREE.Sprite(material);
    sprite.position = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range - range / 2, Math.random() * range - range / 2);
    sprite.scale.set(0.1, 0.1, 0.1);
    scene.add(sprite);
  }
}

这段代码看起来很像我们在上一节中看到的代码。主要变化是,因为我们正在使用THREE.CanvasRenderer,我们直接创建THREE.Sprite对象,而不是使用THREE.PointCloud。在这段代码中,我们还使用program属性定义了THREE.SpriteCanvasMaterial,该属性指向draw函数。这个draw函数定义了粒子的外观(在我们的例子中,是Pac-Man中的幽灵):

var draw = function(ctx) {
  ctx.fillStyle = "orange";
  ...
  // lots of other ctx drawing calls
  ...
  ctx.beginPath();
  ctx.fill();
}

我们不会深入讨论绘制形状所需的实际画布代码。这里重要的是我们定义了一个接受 2D 画布上下文(ctx)作为参数的函数。在该上下文中绘制的一切都被用作THREE.Sprite的形状。

使用 HTML5 画布与 WebGLRenderer

如果我们想要在THREE.WebGLRenderer中使用 HTML5 画布,我们可以采取两种不同的方法。我们可以使用THREE.PointCloudMaterial并创建THREE.PointCloud,或者我们可以使用THREE.SpriteTHREE.SpriteMaterialmap属性。

让我们从第一种方法开始创建THREE.PointCloud。在THREE.PointCloudMaterial的属性中,我们提到了map属性。使用map属性,我们可以为粒子加载纹理。在 Three.js 中,这个纹理也可以是来自 HTML5 画布的输出。一个展示这个概念的例子是05a-program-based-point-cloud-webgl.html。以下截图显示了这个例子:

使用 HTML5 画布与 WebGLRenderer

让我们来看看我们编写的代码来实现这个效果。大部分代码与我们之前的 WebGL 示例相同,所以我们不会详细介绍。为了得到这个例子所做的重要代码更改如下所示:

var getTexture = function() {
  var canvas = document.createElement('canvas');
  canvas.width = 32;
  canvas.height = 32;

  var ctx = canvas.getContext('2d');
  ...
  // draw the ghost
  ...
  ctx.fill();
  var texture = new THREE.Texture(canvas);
  texture.needsUpdate = true;
  return texture;
}

function createPointCloud(size, transparent, opacity, sizeAttenuation, color) {

  var geom = new THREE.Geometry();

  var material = new THREE.PointCloudMaterial ({size: size, transparent: transparent, opacity: opacity, map: getTexture(), sizeAttenuation: sizeAttenuation, color: color});

  var range = 500;
  for (var i = 0; i < 5000; i++) {
    var particle = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range - range / 2, Math.random() * range - range / 2);
    geom.vertices.push(particle);
  }

  cloud = new THREE.PointCloud(geom, material);
  cloud.sortParticles = true;
  scene.add(cloud);
}

getTexture中,这两个 JavaScript 函数中的第一个,我们基于 HTML5 画布元素创建了THREE.Texture。在第二个函数createPointCloud中,我们将这个纹理分配给了THREE.PointCloudMaterialmap属性。在这个函数中,您还可以看到我们将THREE.PointCloudsortParticles属性设置为true。这个属性确保在粒子被渲染之前,它们根据屏幕上的z位置进行排序。如果您看到部分重叠的粒子或不正确的透明度,将此属性设置为true(在大多数情况下)可以解决这个问题。不过,您应该注意,将此属性设置为true会影响场景的性能。当这个属性设置为 true 时,Three.js 将不得不确定每个单独粒子到相机的距离。对于一个非常大的THREE.PointCloud对象,这可能会对性能产生很大的影响。

当我们谈论THREE.PointCloud的属性时,还有一个额外的属性可以设置在THREE.PointCloud上:FrustumCulled。如果将此属性设置为 true,这意味着如果粒子超出可见相机范围,它们将不会被渲染。这可以用来提高性能和帧速率。

这样做的结果是,我们在getTexture()方法中绘制到画布上的一切都用于THREE.PointCloud中的粒子。在接下来的部分中,我们将更深入地了解从外部文件加载的纹理是如何工作的。请注意,在这个例子中,我们只看到了纹理可能实现的一小部分。在第十章中,加载和使用纹理,我们将深入了解纹理的可能性。

在本节的开头,我们提到我们也可以使用THREE.Spritemap属性一起创建基于画布的粒子。为此,我们使用了与前面示例中相同的方法创建THREE.Texture。然而,这一次,我们将它分配给THREE.Sprite,如下所示:

function createSprites() {
  var material = new THREE.SpriteMaterial({
    map: getTexture(),
    color: 0xffffff
  });

  var range = 500;
  for (var i = 0; i < 1500; i++) {
    var sprite = new THREE.Sprite(material);
    sprite.position.set(Math.random() * range - range / 2, Math.random() * range - range / 2, Math.random() * range - range / 2);
    sprite.scale.set(4,4,4);
    scene.add(sprite);
  }
}

在这里,你可以看到我们使用了一个标准的THREE.SpriteMaterial对象,并将画布的输出作为THREE.Texture分配给了材质的map属性。您可以通过在浏览器中打开05b-program-based-sprites-webgl.html来查看这个例子。这两种方法各有优缺点。使用THREE.Sprite,您可以更好地控制每个粒子,但当您处理大量粒子时,性能会降低,复杂性会增加。使用THREE.PointCloud,您可以轻松管理大量粒子,但对每个单独的粒子的控制较少。

使用纹理来设置粒子的样式

在之前的例子中,我们看到了如何使用 HTML5 画布来设置THREE.PointCloud和单个THREE.Sprite对象的样式。由于您可以绘制任何您想要的东西,甚至加载外部图像,您可以使用这种方法向粒子系统添加各种样式。然而,有一种更直接的方法可以使用图像来设置您的粒子的样式。您可以使用THREE.ImageUtils.loadTexture()函数将图像加载为THREE.Texture。然后可以将THREE.Texture分配给材质的map属性。

在本节中,我们将向您展示两个示例并解释如何创建它们。这两个示例都使用图像作为粒子的纹理。在第一个示例中,我们创建了一个模拟雨的场景,06-rainy-scene.html。以下屏幕截图显示了这个示例:

使用纹理来设置粒子的样式

我们需要做的第一件事是获取一个代表雨滴的纹理。您可以在assets/textures/particles文件夹中找到一些示例。在第九章动画和移动相机中,我们将解释纹理的所有细节和要求。现在,您需要知道的是纹理应该是正方形的,最好是 2 的幂(例如,64 x 64,128 x 128,256 x 256)。对于这个例子,我们将使用这个纹理:

使用纹理来设置粒子的样式

这个图像使用了黑色背景(需要正确混合)并显示了雨滴的形状和颜色。在我们可以在THREE.PointCloudMaterial中使用这个纹理之前,我们首先需要加载它。可以用以下代码行来完成:

var texture = THREE.ImageUtils.loadTexture("../assets/textures/particles/raindrop-2.png");

有了这行代码,Three.js 将加载纹理,我们可以在我们的材质中使用它。对于这个例子,我们定义了这样的材质:

var material = new THREE.PointCloudMaterial({size: 3, transparent: true, opacity: true, map: texture, blending: THREE.AdditiveBlending, sizeAttenuation: true, color: 0xffffff});

在本章中,我们已经讨论了所有这些属性。这里需要理解的主要是map属性指向我们使用THREE.ImageUtils.loadTexture()函数加载的纹理,并且我们将THREE.AdditiveBlending指定为blending模式。这种blending模式意味着当绘制新像素时,背景像素的颜色会添加到这个新像素的颜色中。对于我们的雨滴纹理,这意味着黑色背景不会显示出来。一个合理的替代方案是用透明背景替换我们纹理中的黑色,但遗憾的是这在粒子和 WebGL 中不起作用。

这样就处理了THREE.PointCloud的样式。当您打开这个例子时,您还会看到粒子本身在移动。在之前的例子中,我们移动了整个粒子系统;这一次,我们在THREE.PointCloud内部定位了单个粒子。这样做实际上非常简单。每个粒子都表示为构成用于创建THREE.PointCloud的几何体的顶点。让我们看看如何为THREE.PointCloud添加粒子:

var range = 40;
for (var i = 0; i < 1500; i++) {
  var particle = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range * 1.5, Math.random() * range - range / 2);

  particle.velocityX = (Math.random() - 0.5) / 3;
  particle.velocityY = 0.1 + (Math.random() / 5);
  geom.vertices.push(particle);
}

这与我们之前看到的例子并没有太大不同。在这里,我们为每个粒子(THREE.Vector3)添加了两个额外的属性:velocityXvelocityY。第一个定义了粒子(雨滴)水平移动的方式,第二个定义了雨滴下落的速度。水平速度范围从-0.16 到+0.16,垂直速度范围从 0.1 到 0.3。现在每个雨滴都有自己的速度,我们可以在渲染循环中移动单个粒子:

var vertices = system2.geometry.vertices;
vertices.forEach(function (v) {
  v.x = v.x - (v.velocityX);
  v.y = v.y - (v.velocityY);

  if (v.x <= -20 || v.x >= 20) v.velocityX = v.velocityX * -1;
  if (v.y <= 0) v.y = 60;
});

在这段代码中,我们从用于创建THREE.PointCloud的几何体中获取所有vertices(粒子)。对于每个粒子,我们取velocityXvelocityY并使用它们来改变粒子的当前位置。最后两行确保粒子保持在我们定义的范围内。如果v.y位置低于零,我们将雨滴添加回顶部,如果v.x位置达到任何边缘,我们通过反转水平速度使其反弹回来。

让我们看另一个例子。这一次,我们不会下雨,而是下雪。此外,我们不仅使用单一纹理,还将使用五个单独的图像(来自 Three.js 示例)。让我们首先再次看一下结果(参见07-snowy-scene.html):

使用纹理来设置粒子样式

在前面的截图中,您可以看到我们不仅使用单个图像作为纹理,还使用了多个图像。您可能想知道我们是如何做到这一点的。您可能还记得,我们只能为THREE.PointCloud有一个单一的材质。如果我们想要有多个材质,我们只需要创建多个粒子系统,如下所示:

function createPointClouds(size, transparent, opacity, sizeAttenuation, color) {

  var texture1 = THREE.ImageUtils.loadTexture("../assets/textures/particles/snowflake1.png");
  var texture2 = THREE.ImageUtils.loadTexture("../assets/textures/particles/snowflake2.png");
  var texture3 = THREE.ImageUtils.loadTexture("../assets/textures/particles/snowflake3.png");
  var texture4 = THREE.ImageUtils.loadTexture("../assets/textures/particles/snowflake5.png");

  scene.add(createPointCloud("system1", texture1, size, transparent, opacity, sizeAttenuation, color));
  scene.add(createPointCloud ("system2", texture2, size, transparent, opacity, sizeAttenuation, color));
  scene.add(createPointCloud ("system3", texture3, size, transparent, opacity, sizeAttenuation, color));
  scene.add(createPointCloud ("system4", texture4, size, transparent, opacity, sizeAttenuation, color));
}

在这里,您可以看到我们分别加载纹理,并将创建THREE.PointCloud的所有信息传递给createPointCloud函数。这个函数看起来像这样:

function createPointCloud(name, texture, size, transparent, opacity, sizeAttenuation, color) {
  var geom = new THREE.Geometry();

  var color = new THREE.Color(color);
  color.setHSL(color.getHSL().h, color.getHSL().s, (Math.random()) * color.getHSL().l);

  var material = new THREE.PointCloudMaterial({size: size, transparent: transparent, opacity: opacity, map: texture, blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: sizeAttenuation, color: color});

  var range = 40;
  for (var i = 0; i < 50; i++) {
    var particle = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range * 1.5, Math.random() * range - range / 2);
    particle.velocityY = 0.1 + Math.random() / 5;
    particle.velocityX = (Math.random() - 0.5) / 3;
    particle.velocityZ = (Math.random() - 0.5) / 3;
    geom.vertices.push(particle);
  }

  var cloud = new THREE.ParticleCloud(geom, material);
  cloud.name = name;
  cloud.sortParticles = true;
  return cloud;
}

在这个函数中,我们首先定义了应该渲染为特定纹理的粒子的颜色。这是通过随机改变传入颜色的亮度来完成的。接下来,材质以与之前相同的方式创建。这里唯一的变化是depthWrite属性设置为false。这个属性定义了这个对象是否影响 WebGL 深度缓冲区。通过将其设置为false,我们确保各种点云不会相互干扰。如果这个属性没有设置为false,您会看到当一个粒子在另一个THREE.PointCloud对象的粒子前面时,有时会显示纹理的黑色背景。这段代码的最后一步是随机放置粒子并为每个粒子添加随机速度。在渲染循环中,我们现在可以像这样更新每个THREE.PointCloud对象的所有粒子的位置:

scene.children.forEach(function (child) {
  if (child instanceof THREE.ParticleSystem) {
    var vertices = child.geometry.vertices;
    vertices.forEach(function (v) {
      v.y = v.y - (v.velocityY);
      v.x = v.x - (v.velocityX);
      v.z = v.z - (v.velocityZ);

      if (v.y <= 0) v.y = 60;
      if (v.x <= -20 || v.x >= 20) v.velocityX = v.velocityX * -1;
      if (v.z <= -20 || v.z >= 20) v.velocityZ = v.velocityZ * -1;
    });
  }
});

通过这种方法,我们可以拥有不同纹理的粒子。然而,这种方法有点受限。我们想要的不同纹理越多,我们就必须创建和管理更多的点云。如果您有一组不同样式的有限粒子,最好使用我们在本章开头展示的THREE.Sprite对象。

使用精灵地图

在本章的开头,我们使用了THREE.Sprite对象来使用THREE.CanvasRendererTHREE.WebGLRenderer渲染单个粒子。这些精灵被放置在 3D 世界的某个地方,并且它们的大小是基于与摄像机的距离(有时也称为billboarding)。在本节中,我们将展示THREE.Sprite对象的另一种用法。我们将向您展示如何使用THREE.Sprite创建类似于抬头显示HUD)的 3D 内容的图层,使用额外的THREE.OrthographicCamera实例。我们还将向您展示如何使用精灵地图为THREE.Sprite对象选择图像。

作为示例,我们将创建一个简单的THREE.Sprite对象,它从屏幕左侧移动到右侧。在背景中,我们将渲染一个带有移动摄像机的 3D 场景,以说明THREE.Sprite独立于摄像机移动。以下截图显示了我们将为第一个示例创建的内容(08-sprites.html):

使用精灵地图

如果您在浏览器中打开此示例,您将看到类似 Pac-Man 幽灵的精灵在屏幕上移动,并且每当它碰到右边缘时,颜色和形状都会发生变化。我们首先要做的是看一下我们如何创建THREE.OrthographicCamera和一个单独的场景来渲染THREE.Sprite

var sceneOrtho = new THREE.Scene();
var cameraOrtho = new THREE.OrthographicCamera( 0, window.innerWidth, window.innerHeight, 0, -10, 10 );

接下来,让我们看看THREE.Sprite的构造以及精灵可以采用的各种形状是如何加载的:

function getTexture() {
  var texture = new THREE.ImageUtils.loadTexture("../assets/textures/particles/sprite-sheet.png");
  return texture;
}

function createSprite(size, transparent, opacity, color, spriteNumber) {
  var spriteMaterial = new THREE.SpriteMaterial({
    opacity: opacity,
    color: color,
    transparent: transparent,
    map: getTexture()});

  // we have 1 row, with five sprites
  spriteMaterial.map.offset = new THREE.Vector2(1/5 * spriteNumber, 0);
  spriteMaterial.map.repeat = new THREE.Vector2(1/5, 1);
  spriteMaterial.blending = THREE.AdditiveBlending;

  // makes sure the object is always rendered at the front
  spriteMaterial.depthTest = false;
  var sprite = new THREE.Sprite(spriteMaterial);
  sprite.scale.set(size, size, size);
  sprite.position.set(100, 50, 0);
  sprite.velocityX = 5;

  sceneOrtho.add(sprite);
}

getTexture()函数中,我们加载了一个纹理。但是,我们加载的不是每个ghost的五个不同图像,而是加载了一个包含所有精灵的单个纹理。纹理看起来像这样:

使用精灵地图

通过map.offsetmap.repeat属性,我们选择要在屏幕上显示的正确精灵。使用map.offset属性,我们确定了我们加载的纹理在x轴(u)和y轴(v)上的偏移量。这些属性的比例范围从 0 到 1。在我们的示例中,如果我们想选择第三个幽灵,我们将 u 偏移(x轴)设置为 0.4,因为我们只有一行,所以我们不需要改变 v 偏移(y轴)。如果我们只设置这个属性,纹理会在屏幕上压缩显示第三、第四和第五个幽灵。要只显示一个幽灵,我们需要放大。我们通过将 u 值的map.repeat属性设置为 1/5 来实现这一点。这意味着我们放大(仅对x轴)以仅显示纹理的 20%,这正好是一个幽灵。

我们需要采取的最后一步是更新render函数:

webGLRenderer.render(scene, camera);
webGLRenderer.autoClear = false;
webGLRenderer.render(sceneOrtho, cameraOrtho);

我们首先使用普通相机和移动的球体渲染场景,然后再渲染包含我们的精灵的场景。请注意,我们需要将 WebGLRenderer 的autoClear属性设置为false。如果不这样做,Three.js 将在渲染精灵之前清除场景,并且球体将不会显示出来。

以下表格显示了我们在前面示例中使用的THREE.SpriteMaterial的所有属性的概述:

名称 描述
color 这是精灵的颜色。
map 这是要用于此精灵的纹理。这可以是一个精灵表,就像本节示例中所示的那样。
sizeAnnutation 如果设置为false,精灵的大小不会受到其与相机的距离影响。默认值为true
opacity 这设置了精灵的透明度。默认值为1(不透明)。
blending 这定义了在渲染精灵时要使用的混合模式。有关混合模式的更多信息,请参见第九章动画和移动相机
fog 这确定精灵是否受到添加到场景中的雾的影响。默认为true

您还可以在此材质上设置depthTestdepthWrite属性。有关这些属性的更多信息,请参见第四章使用 Three.js 材质

当然,在 3D 中定位THREE.Sprites时,我们也可以使用精灵地图(就像本章开头所做的那样)。以下是一个示例(09-sprites-3D.html)的屏幕截图:

使用精灵地图

通过前表中的属性,我们可以很容易地创建前面屏幕截图中看到的效果:

function createSprites() {

  group = new THREE.Object3D();
  var range = 200;
  for (var i = 0; i < 400; i++) {
    group.add(createSprite(10, false, 0.6, 0xffffff, i % 5, range));
  }
  scene.add(group);
}

function createSprite(size, transparent, opacity, color, spriteNumber, range) {

  var spriteMaterial = new THREE.SpriteMaterial({
    opacity: opacity,
    color: color,
    transparent: transparent,
    map: getTexture()}
  );

  // we have 1 row, with five sprites
  spriteMaterial.map.offset = new THREE.Vector2(0.2*spriteNumber, 0);
  spriteMaterial.map.repeat = new THREE.Vector2(1/5, 1);
  spriteMaterial.depthTest = false;

  spriteMaterial.blending = THREE.AdditiveBlending;

  var sprite = new THREE.Sprite(spriteMaterial);
  sprite.scale.set(size, size, size);
  sprite.position.set(Math.random() * range - range / 2, Math.random() * range - range / 2, Math.random() * range - range / 2);
  sprite.velocityX = 5;

  return sprite;
}

在这个示例中,我们基于我们之前展示的精灵表创建了 400 个精灵。您可能已经了解并理解了这里显示的大多数属性和概念。由于我们已经将单独的精灵添加到了一个组中,因此旋转它们非常容易,可以像这样完成:

group.rotation.x+=0.1;

到目前为止,在本章中,我们主要是从头开始创建精灵和点云。不过,一个有趣的选择是从现有几何体创建THREE.PointCloud

从高级几何体创建 THREE.PointCloud

正如您记得的那样,THREE.PointCloud根据提供的几何体的顶点渲染每个粒子。这意味着,如果我们提供一个复杂的几何体(例如环结或管道),我们可以基于该特定几何体的顶点创建THREE.PointCloud。在本章的最后一节中,我们将创建一个环结,就像我们在上一章中看到的那样,并将其渲染为THREE.PointCloud

我们已经在上一章中解释了环结,所以在这里我们不会详细介绍。我们使用了上一章的确切代码,并添加了一个单一的菜单选项,您可以使用它将渲染的网格转换为THREE.PointCloud。您可以在本章的源代码中找到示例(10-create-particle-system-from-model.html)。以下截图显示了示例:

从高级几何创建 THREE.PointCloud

在前面的截图中,您可以看到用于生成环结的每个顶点都被用作粒子。在这个例子中,我们添加了一个漂亮的材质,基于 HTML 画布,以创建这种发光效果。我们只会看一下创建材质和粒子系统的代码,因为在本章中我们已经讨论了其他属性:

function generateSprite() {

  var canvas = document.createElement('canvas');
  canvas.width = 16;
  canvas.height = 16;

  var context = canvas.getContext('2d');
  var gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2);

  gradient.addColorStop(0, 'rgba(255,255,255,1)');
  gradient.addColorStop(0.2, 'rgba(0,255,255,1)');
  gradient.addColorStop(0.4, 'rgba(0,0,64,1)');
  gradient.addColorStop(1, 'rgba(0,0,0,1)');

  context.fillStyle = gradient;
  context.fillRect(0, 0, canvas.width, canvas.height);

  var texture = new THREE.Texture(canvas);
  texture.needsUpdate = true;
  return texture;
}

function createPointCloud(geom) {
  var material = new THREE.PointCloudMaterial({
    color: 0xffffff,
    size: 3,
    transparent: true,
    blending: THREE.AdditiveBlending,
    map: generateSprite()
  });

  var cloud = new THREE.PointCloud(geom, material);
  cloud.sortParticles = true;
  return cloud;
}

// use it like this
var geom = new THREE.TorusKnotGeometry(...);
var knot = createPointCloud(geom);

在这段代码片段中,您可以看到两个函数:createPointCloud()generateSprite()。在第一个函数中,我们直接从提供的几何体(在本例中是一个环结)创建了一个简单的THREE.PointCloud对象,并使用generateSprite()函数设置了纹理(map属性)为一个发光的点(在 HTML5 画布元素上生成)。generateSprite()函数如下:

从高级几何创建 THREE.PointCloud

总结

这一章就到这里了。我们解释了粒子、精灵和粒子系统是什么,以及如何使用可用的材质来设计这些对象。在本章中,您看到了如何直接在THREE.CanvasRendererTHREE.WebGLRenderer中使用THREE.Sprite。然而,如果您想创建大量的粒子,您应该使用THREE.PointCloud。使用THREE.PointCloud,所有粒子共享相同的材质,您可以通过将材质的vertexColors属性设置为THREE.VertexColors,并在用于创建THREE.PointCloudTHREE.Geometrycolors数组中提供颜色值来更改单个粒子的颜色。我们还展示了如何通过改变它们的位置轻松地对粒子进行动画。这对于单个THREE.Sprite实例和用于创建THREE.PointCloud的几何体的顶点是一样的。

到目前为止,我们已经根据 Three.js 提供的几何创建了网格。这对于简单的模型如球体和立方体非常有效,但当您想要创建复杂的 3D 模型时,并不是最佳方法。对于这些模型,通常会使用 3D 建模应用程序,如 Blender 或 3D Studio Max。在下一章中,您将学习如何加载和显示这些 3D 建模应用程序创建的模型。

第八章:创建和加载高级网格和几何

在这一章中,我们将看一下创建高级和复杂几何和网格的几种不同方法。在第五章,“学习使用几何”,和第六章,“高级几何和二进制操作”中,我们向您展示了如何使用 Three.js 的内置对象创建一些高级几何。在这一章中,我们将使用以下两种方法来创建高级几何和网格:

  • 组合和合并:我们解释的第一种方法使用了 Three.js 的内置功能来组合和合并现有的几何。这样可以从现有对象创建新的网格和几何。

  • 从外部加载:在本节中,我们将解释如何从外部来源加载网格和几何。例如,我们将向您展示如何使用 Blender 以 Three.js 支持的格式导出网格。

我们从组合和合并方法开始。使用这种方法,我们使用标准的 Three.js 分组和THREE.Geometry.merge()函数来创建新对象。

几何组合和合并

在本节中,我们将介绍 Three.js 的两个基本功能:将对象组合在一起和将多个网格合并成单个网格。我们将从将对象组合在一起开始。

将对象组合在一起

在之前的一些章节中,当您使用多个材质时已经看到了这一点。当您使用多个材质从几何创建网格时,Three.js 会创建一个组。您的几何的多个副本被添加到这个组中,每个副本都有自己特定的材质。这个组被返回,所以看起来像是使用多个材质的网格。然而,实际上,它是一个包含多个网格的组。

创建组非常容易。您创建的每个网格都可以包含子元素,可以使用 add 函数添加子元素。将子对象添加到组中的效果是,您可以移动、缩放、旋转和平移父对象,所有子对象也会受到影响。让我们看一个例子(01-grouping.html)。以下屏幕截图显示了这个例子:

将对象组合在一起

在这个例子中,您可以使用菜单来移动球体和立方体。如果您勾选旋转选项,您会看到这两个网格围绕它们的中心旋转。这并不是什么新鲜事,也不是很令人兴奋。然而,这两个对象并没有直接添加到场景中,而是作为一个组添加的。以下代码概括了这个讨论:

sphere = createMesh(new THREE.SphereGeometry(5, 10, 10));
cube = createMesh(new THREE.BoxGeometry(6, 6, 6));

group = new THREE.Object3D();
group.add(sphere);
group.add(cube);

scene.add(group);

在这段代码片段中,您可以看到我们创建了THREE.Object3D。这是THREE.MeshTHREE.Scene的基类,但本身并不包含任何内容或导致任何内容被渲染。请注意,在最新版本的 Three.js 中,引入了一个名为THREE.Group的新对象来支持分组。这个对象与THREE.Object3D对象完全相同,您可以用new THREE.Group()替换前面代码中的new THREE.Object3D()以获得相同的效果。在这个例子中,我们使用add函数将spherecube添加到这个对象,然后将它添加到scene中。如果您查看这个例子,您仍然可以移动立方体和球体,以及缩放和旋转这两个对象。您也可以在它们所在的组上进行这些操作。如果您查看组菜单,您会看到位置和缩放选项。您可以使用这些选项来缩放和移动整个组。这个组内部对象的缩放和位置是相对于组本身的缩放和位置的。

缩放和定位非常简单。但需要记住的一点是,当您旋转一个组时,它不会单独旋转其中的对象;它会围绕自己的中心旋转(在我们的示例中,您会围绕group对象的中心旋转整个组)。在这个示例中,我们使用THREE.ArrowHelper对象在组的中心放置了一个箭头,以指示旋转点:

var arrow = new THREE.ArrowHelper(new THREE.Vector3(0, 1, 0), group.position, 10, 0x0000ff);
scene.add(arrow);

如果您同时选中分组旋转复选框,组将会旋转。您会看到球体和立方体围绕组的中心旋转(由箭头指示),如下所示:

将对象分组

在使用组时,您仍然可以引用、修改和定位单个几何体。您需要记住的是,所有位置、旋转和平移都是相对于父对象进行的。在下一节中,我们将看看合并,您将合并多个单独的几何体,并最终得到一个单个的THREE.Geometry对象。

将多个网格合并成单个网格

在大多数情况下,使用组可以让您轻松操作和管理大量网格。然而,当您处理大量对象时,性能将成为一个问题。使用组时,您仍然在处理需要单独处理和渲染的单个对象。使用THREE.Geometry.merge(),您可以合并几何体并创建一个组合的几何体。在下面的示例中,您可以看到这是如何工作的,以及它对性能的影响。如果您打开02-merging.html示例,您会看到一个场景,其中有一组随机分布的半透明立方体。在菜单中使用滑块,您可以设置场景中立方体的数量,并通过单击重绘按钮重新绘制场景。根据您运行的硬件,随着立方体数量的增加,您会看到性能下降。在我们的案例中,如下截图所示,在大约 4,000 个对象时,刷新率会从正常的 60 fps 降至约 40 fps:

将多个网格合并成单个网格

如您所见,您可以向场景中添加的网格数量存在一定限制。不过,通常情况下,您可能不需要那么多网格,但是在创建特定游戏(例如像Minecraft这样的游戏)或高级可视化时,您可能需要管理大量单独的网格。使用THREE.Geometry.merge(),您可以解决这个问题。在查看代码之前,让我们运行相同的示例,但这次选中合并框。通过标记此选项,我们将所有立方体合并为单个THREE.Geometry,并添加该对象,如下截图所示:

将多个网格合并成单个网格

如您所见,我们可以轻松渲染 20,000 个立方体而不会出现性能下降。为此,我们使用以下几行代码:

var geometry = new THREE.Geometry();
for (var i = 0; i < controls.numberOfObjects; i++) {
  var cubeMesh = addcube();
  cubeMesh.updateMatrix();
  geometry.merge(cubeMesh.geometry,cubeMesh.matrix);
}
scene.add(new THREE.Mesh(geometry, cubeMaterial));

在此代码片段中,addCube()函数返回THREE.Mesh。在较早版本的 Three.js 中,我们可以使用THREE.GeometryUtils.merge函数将THREE.Mesh对象合并到THREE.Geometry对象中。但在最新版本中,这个功能已经被弃用,取而代之的是THREE.Geometry.merge函数。为了确保合并的THREE.Geometry对象被正确定位和旋转,我们不仅向merge函数提供THREE.Geometry,还提供其变换矩阵。当我们将这个矩阵添加到merge函数时,我们合并的立方体将被正确定位。

我们这样做了 20,000 次,最后得到一个单一的几何图形,我们将其添加到场景中。如果您查看代码,您可能会看到这种方法的一些缺点。由于您得到了一个单一的几何图形,您无法为每个单独的立方体应用材质。然而,这可以通过使用THREE.MeshFaceMaterial来解决。然而,最大的缺点是您失去了对单个立方体的控制。如果您想要移动、旋转或缩放单个立方体,您无法做到(除非您搜索正确的面和顶点并单独定位它们)。

通过分组和合并方法,您可以使用 Three.js 提供的基本几何图形创建大型和复杂的几何图形。如果您想创建更高级的几何图形,那么使用 Three.js 提供的编程方法并不总是最佳和最简单的选择。幸运的是,Three.js 还提供了其他几种选项来创建几何图形。在下一节中,我们将看看如何从外部资源加载几何图形和网格。

从外部资源加载几何图形

Three.js 可以读取多种 3D 文件格式,并导入这些文件中定义的几何图形和网格。以下表格显示了 Three.js 支持的文件格式:

格式 描述
JSON Three.js 有自己的 JSON 格式,您可以用它来声明性地定义几何图形或场景。尽管这不是官方格式,但在想要重用复杂几何图形或场景时,它非常易于使用并非常方便。
OBJ 或 MTL OBJ 是由Wavefront Technologies首次开发的简单 3D 格式。它是最广泛采用的 3D 文件格式之一,用于定义对象的几何形状。MTL 是 OBJ 的伴随格式。在 MTL 文件中,指定了 OBJ 文件中对象的材质。Three.js 还有一个名为 OBJExporter.js 的自定义 OBJ 导出器,如果您想要从 Three.js 导出模型到 OBJ,可以使用它。
Collada Collada 是一种用于以基于 XML 的格式定义数字资产的格式。这也是一种被几乎所有 3D 应用程序和渲染引擎支持的广泛使用的格式。
STL STL代表STereoLithography,广泛用于快速原型制作。例如,3D 打印机的模型通常定义为 STL 文件。Three.js 还有一个名为 STLExporter.js 的自定义 STL 导出器,如果您想要从 Three.js 导出模型到 STL,可以使用它。
CTM CTM 是由openCTM创建的文件格式。它用作以紧凑格式存储 3D 三角形网格的格式。
VTK VTK 是由可视化工具包定义的文件格式,用于指定顶点和面。有两种可用格式:二进制格式和基于文本的 ASCII 格式。Three.js 仅支持基于 ASCII 的格式。
AWD AWD 是用于 3D 场景的二进制格式,最常与away3d.com/引擎一起使用。请注意,此加载器不支持压缩的 AWD 文件。
Assimp 开放资产导入库(也称为Assimp)是导入各种 3D 模型格式的标准方式。使用此加载器,您可以导入使用assimp2json转换的大量 3D 格式的模型,有关详细信息,请访问github.com/acgessler/assimp2json
VRML VRML代表虚拟现实建模语言。这是一种基于文本的格式,允许您指定 3D 对象和世界。它已被 X3D 文件格式取代。Three.js 不支持加载 X3D 模型,但这些模型可以很容易地转换为其他格式。更多信息可以在www.x3dom.org/?page_id=532#找到。
Babylon Babylon 是一个 3D JavaScript 游戏库。它以自己的内部格式存储模型。有关此内容的更多信息,请访问www.babylonjs.com/
PDB 这是一种非常专业的格式,由蛋白质数据银行创建,用于指定蛋白质的外观。Three.js 可以加载和可视化以这种格式指定的蛋白质。
PLY 这种格式被称为多边形文件格式。这通常用于存储来自 3D 扫描仪的信息。

在下一章中,当我们讨论动画时,我们将重新访问其中一些格式(并查看另外两种格式,MD2 和 glTF)。现在,我们从 Three.js 的内部格式开始。

以 Three.js JSON 格式保存和加载

你可以在 Three.js 中的两种不同场景中使用 Three.js 的 JSON 格式。你可以用它来保存和加载单个THREE.Mesh,或者你可以用它来保存和加载完整的场景。

保存和加载 THREE.Mesh

为了演示保存和加载,我们创建了一个基于THREE.TorusKnotGeometry的简单示例。通过这个示例,你可以创建一个环结,就像我们在第五章 学习使用几何图形中所做的那样,并使用保存按钮从保存和加载菜单中保存当前几何图形。对于这个例子,我们使用 HTML5 本地存储 API 进行保存。这个 API 允许我们在客户端的浏览器中轻松存储持久信息,并在以后的时间检索它(即使浏览器已关闭并重新启动)。

我们将查看03-load-save-json-object.html示例。以下截图显示了这个例子:

保存和加载 THREE.Mesh

从 Three.js 中以 JSON 格式导出非常容易,不需要你包含任何额外的库。要将THREE.Mesh导出为 JSON,你需要做的唯一的事情是:

var result = knot.toJSON();
localStorage.setItem("json", JSON.stringify(result));

在保存之前,我们首先将toJSON函数的结果,一个 JavaScript 对象,使用JSON.stringify函数转换为字符串。这将产生一个看起来像这样的 JSON 字符串(大部分顶点和面都被省略了):

{
  "metadata": {
    "version": 4.3,
    "type": "Object",
    "generator": "ObjectExporter"
  },
  "geometries": [{
    "uuid": "53E1B290-3EF3-4574-BD68-E65DFC618BA7",
    "type": "TorusKnotGeometry",
    "radius": 10,
    "tube": 1,
    "radialSegments": 64,
    "tubularSegments": 8,
    "p": 2,
    "q": 3,
    "heightScale": 1
  }],
  ...
}

正如你所看到的,Three.js 保存了关于THREE.Mesh的所有信息。要使用 HTML5 本地存储 API 保存这些信息,我们只需要调用localStorage.setItem函数。第一个参数是键值(json),我们稍后可以使用它来检索我们作为第二个参数传递的信息。

从本地存储中加载THREE.Mesh回到 Three.js 也只需要几行代码,如下所示:

var json = localStorage.getItem("json");

if (json) {
  var loadedGeometry = JSON.parse(json);
  var loader = new THREE.ObjectLoader();

  loadedMesh = loader.parse(loadedGeometry);
  loadedMesh.position.x -= 50;
  scene.add(loadedMesh);
}

在这里,我们首先使用我们保存的名称(在本例中为json)从本地存储中获取 JSON。为此,我们使用 HTML5 本地存储 API 提供的localStorage.getItem函数。接下来,我们需要将字符串转换回 JavaScript 对象(JSON.parse),并将 JSON 对象转换回THREE.Mesh。Three.js 提供了一个名为THREE.ObjectLoader的辅助对象,你可以使用它将 JSON 转换为THREE.Mesh。在这个例子中,我们使用加载器上的parse方法直接解析 JSON 字符串。加载器还提供了一个load函数,你可以传入包含 JSON 定义的文件的 URL。

正如你在这里看到的,我们只保存了THREE.Mesh。我们失去了其他一切。如果你想保存完整的场景,包括灯光和相机,你可以使用THREE.SceneExporter

保存和加载场景

如果你想保存完整的场景,你可以使用与我们在前一节中看到的相同方法来保存几何图形。04-load-save-json-scene.html是一个展示这一点的工作示例。以下截图显示了这个例子:

保存和加载场景

在这个例子中,您有三个选项:exportSceneclearSceneimportScene。使用exportScene,场景的当前状态将保存在浏览器的本地存储中。要测试导入功能,您可以通过单击clearScene按钮来删除场景,并使用importScene按钮从本地存储加载它。执行所有这些操作的代码非常简单,但在使用之前,您必须从 Three.js 分发中导入所需的导出器和加载器(查看examples/js/exportersexamples/js/loaders目录):

<script type="text/javascript" src="../libs/SceneLoader.js"></script>
<script type="text/javascript" src="../libs/SceneExporter.js"></script>

在页面中包含这些 JavaScript 导入后,您可以使用以下代码导出一个场景:

var exporter = new THREE.SceneExporter();
var sceneJson = JSON.stringify(exporter.parse(scene));
localStorage.setItem('scene', sceneJson);

这种方法与我们在上一节中使用的方法完全相同,只是这次我们使用THREE.SceneExporter()来导出完整的场景。生成的 JSON 如下:

{
  "metadata": {
    "formatVersion": 3.2,
    "type": "scene",
    "generatedBy": "SceneExporter",
    "objects": 5,
    "geometries": 3,
    "materials": 3,
    "textures": 0
  },
  "urlBaseType": "relativeToScene", "objects": {
    "Object_78B22F27-C5D8-46BF-A539-A42207DDDCA8": {
      "geometry": "Geometry_5",
      "material": "Material_1",
      "position": [15, 0, 0],
      "rotation": [-1.5707963267948966, 0, 0],
      "scale": [1, 1, 1],
      "visible": true
    }
    ... // removed all the other objects for legibility
  },
  "geometries": {
    "Geometry_8235FC68-64F0-45E9-917F-5981B082D5BC": {
      "type": "cube",
      "width": 4,
      "height": 4,
      "depth": 4,
      "widthSegments": 1,
      "heightSegments": 1,
      "depthSegments": 1
    }
    ... // removed all the other objects for legibility
  }
  ... other scene information like textures

当您再次加载此 JSON 时,Three.js 会按原样重新创建对象。加载场景的方法如下:

var json = (localStorage.getItem('scene'));
var sceneLoader = new THREE.SceneLoader();
sceneLoader.parse(JSON.parse(json), function(e) {
  scene = e.scene;
}, '.');

传递给加载程序的最后一个参数('.')定义了相对 URL。例如,如果您有使用纹理的材质(例如,外部图像),那么这些材质将使用此相对 URL 进行检索。在这个例子中,我们不使用纹理,所以只需传入当前目录。与THREE.ObjectLoader一样,您也可以使用load函数从 URL 加载 JSON 文件。

有许多不同的 3D 程序可以用来创建复杂的网格。一个流行的开源程序是 Blender(www.blender.org)。Three.js 有一个针对 Blender(以及 Maya 和 3D Studio Max)的导出器,直接导出到 Three.js 的 JSON 格式。在接下来的部分中,我们将指导您配置 Blender 以使用此导出器,并向您展示如何在 Blender 中导出复杂模型并在 Three.js 中显示它。

使用 Blender

在开始配置之前,我们将展示我们将要实现的结果。在下面的截图中,您可以看到一个简单的 Blender 模型,我们使用 Three.js 插件导出,并在 Three.js 中使用THREE.JSONLoader导入:

使用 Blender

在 Blender 中安装 Three.js 导出器

要让 Blender 导出 Three.js 模型,我们首先需要将 Three.js 导出器添加到 Blender 中。以下步骤适用于 Mac OS X,但在 Windows 和 Linux 上基本相同。您可以从www.blender.org下载 Blender,并按照特定于平台的安装说明进行操作。安装后,您可以添加 Three.js 插件。首先,使用终端窗口找到 Blender 安装的addons目录:

在 Blender 中安装 Three.js 导出器

在我的 Mac 上,它位于这里:./blender.app/Contents/MacOS/2.70/scripts/addons。对于 Windows,该目录可以在以下位置找到:C:\Users\USERNAME\AppData\Roaming\Blender Foundation\Blender\2.7X\scripts\addons。对于 Linux,您可以在此处找到此目录:/home/USERNAME/.config/blender/2.7X/scripts/addons

接下来,您需要获取 Three.js 分发并在本地解压缩。在此分发中,您可以找到以下文件夹:utils/exporters/blender/2.65/scripts/addons/。在此目录中,有一个名为io_mesh_threejs的单个子目录。将此目录复制到您的 Blender 安装的addons文件夹中。

现在,我们只需要启动 Blender 并启用导出器。在 Blender 中,打开Blender 用户首选项文件 | 用户首选项)。在打开的窗口中,选择插件选项卡,并在搜索框中输入three。这将显示以下屏幕:

在 Blender 中安装 Three.js 导出器

此时,找到了 Three.js 插件,但它仍然被禁用。勾选右侧的小复选框,Three.js 导出器将被启用。最后,为了检查一切是否正常工作,打开文件 | 导出菜单选项,您将看到 Three.js 列为导出选项。如下截图所示:

在 Blender 中安装 Three.js 导出器

安装了插件后,我们可以加载我们的第一个模型。

从 Blender 加载和导出模型

例如,我们在assets/models文件夹中添加了一个名为misc_chair01.blend的简单 Blender 模型,您可以在本书的源文件中找到。在本节中,我们将加载此模型,并展示将此模型导出到 Three.js 所需的最小步骤。

首先,我们需要在 Blender 中加载此模型。使用文件 | 打开并导航到包含misc_chair01.blend文件的文件夹。选择此文件,然后单击打开。这将显示一个看起来有点像这样的屏幕:

从 Blender 加载和导出模型

将此模型导出到 Three.js JSON 格式非常简单。从文件菜单中,打开导出 | Three.js,输入导出文件的名称,然后选择导出 Three.js。这将创建一个 Three.js 理解的 JSON 文件。此文件的部分内容如下所示:

{

  "metadata" :
  {
    "formatVersion" : 3.1,
    "generatedBy"   : "Blender 2.7 Exporter",
    "vertices"      : 208,
    "faces"         : 124,
    "normals"       : 115,
    "colors"        : 0,
    "uvs"           : [270,151],
    "materials"     : 1,
    "morphTargets"  : 0,
    "bones"         : 0
  },
...

然而,我们还没有完全完成。在前面的截图中,您可以看到椅子包含木纹理。如果您查看 JSON 导出,您会看到椅子的导出也指定了一个材质,如下所示:

"materials": [{
  "DbgColor": 15658734,
  "DbgIndex": 0,
  "DbgName": "misc_chair01",
  "blending": "NormalBlending",
  "colorAmbient": [0.53132, 0.25074, 0.147919],
  "colorDiffuse": [0.53132, 0.25074, 0.147919],
  "colorSpecular": [0.0, 0.0, 0.0],
  "depthTest": true,
  "depthWrite": true,
  "mapDiffuse": "misc_chair01_col.jpg",
  "mapDiffuseWrap": ["repeat", "repeat"],
  "shading": "Lambert",
  "specularCoef": 50,
  "transparency": 1.0,
  "transparent": false,
  "vertexColors": false
}],

此材质为mapDiffuse属性指定了一个名为misc_chair01_col.jpg的纹理。因此,除了导出模型,我们还需要确保 Three.js 也可以使用纹理文件。幸运的是,我们可以直接从 Blender 保存这个纹理。

在 Blender 中,打开UV/Image Editor视图。您可以从文件菜单选项的左侧下拉菜单中选择此视图。这将用以下内容替换顶部菜单:

从 Blender 加载和导出模型

确保选择要导出的纹理,我们的情况下是misc_chair_01_col.jpg(您可以使用小图标选择不同的纹理)。接下来,单击图像菜单,使用另存为图像菜单选项保存图像。将其保存在与模型相同的文件夹中,使用 JSON 导出文件中指定的名称。此时,我们已经准备好将模型加载到 Three.js 中。

此时将加载到 Three.js 中的代码如下:

var loader = new THREE.JSONLoader();
loader.load('../assets/models/misc_chair01.js', function (geometry, mat) {
  mesh = new THREE.Mesh(geometry, mat[0]);

  mesh.scale.x = 15;
  mesh.scale.y = 15;
  mesh.scale.z = 15;

  scene.add(mesh);

}, '../assets/models/');

我们之前已经见过JSONLoader,但这次我们使用load函数而不是parse函数。在此函数中,我们指定要加载的 URL(指向导出的 JSON 文件),一个在对象加载时调用的回调,以及纹理所在的位置../assets/models/(相对于页面)。此回调接受两个参数:geometrymatgeometry参数包含模型,mat参数包含材质对象的数组。我们知道只有一个材质,因此当我们创建THREE.Mesh时,我们直接引用该材质。如果您打开05-blender-from-json.html示例,您可以看到我们刚刚从 Blender 导出的椅子。

使用 Three.js 导出器并不是从 Blender 加载模型到 Three.js 的唯一方法。Three.js 理解许多 3D 文件格式,而 Blender 可以导出其中的一些格式。然而,使用 Three.js 格式非常简单,如果出现问题,通常可以很快找到。

在下一节中,我们将看一下 Three.js 支持的一些格式,并展示一个基于 Blender 的 OBJ 和 MTL 文件格式的示例。

从 3D 文件格式导入

在本章的开头,我们列出了 Three.js 支持的一些格式。在本节中,我们将快速浏览一些这些格式的例子。请注意,对于所有这些格式,都需要包含一个额外的 JavaScript 文件。您可以在 Three.js 分发的examples/js/loaders目录中找到所有这些文件。

OBJ 和 MTL 格式

OBJ 和 MTL 是配套格式,经常一起使用。OBJ 文件定义了几何图形,而 MTL 文件定义了所使用的材质。OBJ 和 MTL 都是基于文本的格式。OBJ 文件的一部分看起来像这样:

v -0.032442 0.010796 0.025935
v -0.028519 0.013697 0.026201
v -0.029086 0.014533 0.021409
usemtl Material
s 1
f 2731 2735 2736 2732
f 2732 2736 3043 3044

MTL 文件定义了材质,如下所示:

newmtl Material
Ns 56.862745
Ka 0.000000 0.000000 0.000000
Kd 0.360725 0.227524 0.127497
Ks 0.010000 0.010000 0.010000
Ni 1.000000
d 1.000000
illum 2

Three.js 对 OBJ 和 MTL 格式有很好的理解,并且也受到 Blender 的支持。因此,作为一种替代方案,您可以选择以 OBJ/MTL 格式而不是 Three.js JSON 格式从 Blender 中导出模型。Three.js 有两种不同的加载器可供使用。如果您只想加载几何图形,可以使用OBJLoader。我们在我们的例子(06-load-obj.html)中使用了这个加载器。以下截图显示了这个例子:

OBJ 和 MTL 格式

要在 Three.js 中导入这个模型,您必须添加 OBJLoader JavaScript 文件:

<script type="text/javascript" src="../libs/OBJLoader.js"></script>

像这样导入模型:

var loader = new THREE.OBJLoader();
loader.load('../assets/models/pinecone.obj', function (loadedMesh) {
  var material = new THREE.MeshLambertMaterial({color: 0x5C3A21});

  // loadedMesh is a group of meshes. For
  // each mesh set the material, and compute the information
  // three.js needs for rendering.
  loadedMesh.children.forEach(function (child) {
    child.material = material;
    child.geometry.computeFaceNormals();
    child.geometry.computeVertexNormals();
  });

  mesh = loadedMesh;
  loadedMesh.scale.set(100, 100, 100);
  loadedMesh.rotation.x = -0.3;
  scene.add(loadedMesh);
});

在这段代码中,我们使用OBJLoader从 URL 加载模型。一旦模型加载完成,我们提供的回调就会被调用,并且我们将模型添加到场景中。

提示

通常,一个很好的第一步是将回调的响应打印到控制台上,以了解加载的对象是如何构建的。通常情况下,使用这些加载器,几何图形或网格会作为一组组的层次结构返回。了解这一点会使得更容易放置和应用正确的材质,并采取任何其他额外的步骤。此外,查看一些顶点的位置来确定是否需要缩放模型的大小以及摄像机的位置。在这个例子中,我们还调用了computeFaceNormalscomputeVertexNormals。这是为了确保所使用的材质(THREE.MeshLambertMaterial)能够正确渲染。

下一个例子(07-load-obj-mtl.html)使用OBJMTLLoader加载模型并直接分配材质。以下截图显示了这个例子:

OBJ 和 MTL 格式

首先,我们需要将正确的加载器添加到页面上:

<script type="text/javascript" src="../libs/OBJLoader.js"></script>
<script type="text/javascript" src="../libs/MTLLoader.js"></script>
<script type="text/javascript" src="../libs/OBJMTLLoader.js"></script>

我们可以这样从 OBJ 和 MTL 文件加载模型:

var loader = new THREE.OBJMTLLoader();
loader.load('../assets/models/butterfly.obj', '../assets/models/butterfly.mtl', function(object) {
  // configure the wings
  var wing2 = object.children[5].children[0];
  var wing1 = object.children[4].children[0];

  wing1.material.opacity = 0.6;
  wing1.material.transparent = true;
  wing1.material.depthTest = false;
  wing1.material.side = THREE.DoubleSide;

  wing2.material.opacity = 0.6;
  wing2.material.depthTest = false;
  wing2.material.transparent = true;
  wing2.material.side = THREE.DoubleSide;

  object.scale.set(140, 140, 140);
  mesh = object;
  scene.add(mesh);

  mesh.rotation.x = 0.2;
  mesh.rotation.y = -1.3;
});

在查看代码之前,首先要提到的是,如果您收到了一个 OBJ 文件、一个 MTL 文件和所需的纹理文件,您需要检查 MTL 文件如何引用纹理。这些应该是相对于 MTL 文件的引用,而不是绝对路径。代码本身与我们为THREE.ObjLoader看到的代码并没有太大的不同。我们指定了 OBJ 文件的位置、MTL 文件的位置以及在加载模型时要调用的函数。在这种情况下,我们使用的模型是一个复杂的模型。因此,我们在回调中设置了一些特定的属性来修复一些渲染问题,如下所示:

  • 源文件中的不透明度设置不正确,导致翅膀不可见。因此,为了解决这个问题,我们自己设置了opacitytransparent属性。

  • 默认情况下,Three.js 只渲染对象的一面。由于我们从两个方向观察翅膀,我们需要将side属性设置为THREE.DoubleSide值。

  • 当需要将翅膀渲染在彼此之上时,会导致一些不必要的伪影。我们通过将depthTest属性设置为false来解决这个问题。这对性能有轻微影响,但通常可以解决一些奇怪的渲染伪影。

但是,正如您所看到的,您可以轻松地直接将复杂的模型加载到 Three.js 中,并在浏览器中实时渲染它们。不过,您可能需要微调一些材质属性。

加载 Collada 模型

Collada 模型(扩展名为.dae)是另一种非常常见的格式,用于定义场景和模型(以及我们将在下一章中看到的动画)。在 Collada 模型中,不仅定义了几何形状,还定义了材料。甚至可以定义光源。

要加载 Collada 模型,您必须采取与 OBJ 和 MTL 模型几乎相同的步骤。首先要包括正确的加载程序:

<script type="text/javascript" src="../libs/ColladaLoader.js"></script>

在这个例子中,我们将加载以下模型:

加载 Collada 模型

再次加载卡车模型非常简单:

var mesh;
loader.load("../assets/models/dae/Truck_dae.dae", function (result) {
  mesh = result.scene.children[0].children[0].clone();
  mesh.scale.set(4, 4, 4);
  scene.add(mesh);
});

这里的主要区别是返回给回调的对象的结果。result对象具有以下结构:

var result = {

  scene: scene,
  morphs: morphs,
  skins: skins,
  animations: animData,
  dae: {
    ...
  }
};

在本章中,我们对scene参数中的对象感兴趣。我首先将场景打印到控制台上,看看我感兴趣的网格在哪里,即result.scene.children[0].children[0]。剩下的就是将其缩放到合理的大小并添加到场景中。对于这个特定的例子,最后需要注意的是,当我第一次加载这个模型时,材料没有正确渲染。原因是纹理使用了.tga格式,这在 WebGL 中不受支持。为了解决这个问题,我不得不将.tga文件转换为.png并编辑.dae模型的 XML,指向这些.png文件。

正如你所看到的,对于大多数复杂模型,包括材料,通常需要采取一些额外的步骤才能获得所需的结果。通过仔细观察材料的配置(使用console.log())或用测试材料替换它们,问题通常很容易发现。

加载 STL、CTM、VTK、AWD、Assimp、VRML 和 Babylon 模型

我们将快速浏览这些文件格式,因为它们都遵循相同的原则:

  1. 在网页中包括[NameOfFormat]Loader.js

  2. 使用[NameOfFormat]Loader.load()加载 URL。

  3. 检查回调的响应格式是什么样的,并渲染结果。

我们已经为所有这些格式包含了一个示例:

名称 示例 截图
STL 08-load-STL.html 加载 STL、CTM、VTK、AWD、Assimp、VRML 和 Babylon 模型
CTM 09-load-CTM.html 加载 STL、CTM、VTK、AWD、Assimp、VRML 和 Babylon 模型
VTK 10-load-vtk.html 加载 STL、CTM、VTK、AWD、Assimp、VRML 和 Babylon 模型
AWD 11-load-awd.html 加载 STL、CTM、VTK、AWD、Assimp、VRML 和 Babylon 模型
Assimp 12-load-assimp.html 加载 STL、CTM、VTK、AWD、Assimp、VRML 和 Babylon 模型
VRML 13-load-vrml.html 加载 STL、CTM、VTK、AWD、Assimp、VRML 和 Babylon 模型
Babylon 巴比伦加载程序与表中的其他加载程序略有不同。使用此加载程序,您不会加载单个THREE.MeshTHREE.Geometry实例,而是加载一个完整的场景,包括灯光。14-load-babylon.html 加载 STL、CTM、VTK、AWD、Assimp、VRML 和 Babylon 模型

如果您查看这些示例的源代码,您可能会发现,对于其中一些示例,我们需要在模型正确渲染之前更改一些材料属性或进行一些缩放。我们之所以需要这样做,是因为模型是在其外部应用程序中创建的方式不同,给它不同的尺寸和分组,而不是我们在 Three.js 中通常使用的。

我们几乎展示了所有支持的文件格式。在接下来的两个部分中,我们将采取不同的方法。首先,我们将看看如何从蛋白质数据银行(PDB 格式)渲染蛋白质,最后我们将使用 PLY 格式中定义的模型创建一个粒子系统。

显示来自蛋白质数据银行的蛋白质

蛋白质数据银行(www.rcsb.org)包含许多不同分子和蛋白质的详细信息。除了这些蛋白质的解释外,它们还提供了以 PDB 格式下载这些分子结构的方法。Three.js 提供了一个用于 PDB 格式文件的加载器。在本节中,我们将举例说明如何解析 PDB 文件并使用 Three.js 进行可视化。

加载新文件格式时,我们总是需要在 Three.js 中包含正确的加载器,如下所示:

<script type="text/javascript" src="../libs/PDBLoader.js"></script>

包含此加载器后,我们将创建以下分子描述的 3D 模型(请参阅15-load-ptb.html示例):

显示来自蛋白质数据银行的蛋白质

加载 PDB 文件的方式与之前的格式相同,如下所示:

var loader = new THREE.PDBLoader();
var group = new THREE.Object3D();
loader.load("../assets/models/diamond.pdb", function (geometry, geometryBonds) {
  var i = 0;

  geometry.vertices.forEach(function (position) {
    var sphere = new THREE.SphereGeometry(0.2);
    var material = new THREE.MeshPhongMaterial({color: geometry.colors[i++]});
    var mesh = new THREE.Mesh(sphere, material);
    mesh.position.copy(position);
    group.add(mesh);
  });

  for (var j = 0; j < geometryBonds.vertices.length; j += 2) {
    var path = new THREE.SplineCurve3([geometryBonds.vertices[j], geometryBonds.vertices[j + 1]]);
    var tube = new THREE.TubeGeometry(path, 1, 0.04)
    var material = new THREE.MeshPhongMaterial({color: 0xcccccc});
    var mesh = new THREE.Mesh(tube, material);
    group.add(mesh);
  }
  console.log(geometry);
  console.log(geometryBonds);

  scene.add(group);
});

如您从此示例中所见,我们实例化THREE.PDBLoader,传入我们想要加载的模型文件,并提供一个在加载模型时调用的回调函数。对于这个特定的加载器,回调函数被调用时带有两个参数:geometrygeometryBondsgeometry参数提供的顶点包含了单个原子的位置,而geometryBounds用于原子之间的连接。

对于每个顶点,我们创建一个颜色也由模型提供的球体:

var sphere = new THREE.SphereGeometry(0.2);
var material = new THREE.MeshPhongMaterial({color: geometry.colors[i++]});
var mesh = new THREE.Mesh(sphere, material);
mesh.position.copy(position);
group.add(mesh)

每个连接都是这样定义的:

var path = new THREE.SplineCurve3([geometryBonds.vertices[j], geometryBonds.vertices[j + 1]]);
var tube = new THREE.TubeGeometry(path, 1, 0.04)
var material = new THREE.MeshPhongMaterial({color: 0xcccccc});
var mesh = new THREE.Mesh(tube, material);
group.add(mesh);

对于连接,我们首先使用THREE.SplineCurve3对象创建一个 3D 路径。这个路径被用作THREE.Tube的输入,并用于创建原子之间的连接。所有连接和原子都被添加到一个组中,然后将该组添加到场景中。您可以从蛋白质数据银行下载许多模型。

以下图片显示了一颗钻石的结构:

显示来自蛋白质数据银行的蛋白质

从 PLY 模型创建粒子系统

与其他格式相比,使用 PLY 格式并没有太大的不同。您需要包含加载器,提供回调函数,并可视化模型。然而,在最后一个示例中,我们将做一些不同的事情。我们将使用此模型的信息创建一个粒子系统,而不是将模型呈现为网格(请参阅15-load-ply.html示例)。以下截图显示了这个示例:

从 PLY 模型创建粒子系统

渲染上述截图的 JavaScript 代码实际上非常简单,如下所示:

var loader = new THREE.PLYLoader();
var group = new THREE.Object3D();
loader.load("../assets/models/test.ply", function (geometry) {
  var material = new THREE.PointCloudMaterial({
    color: 0xffffff,
    size: 0.4,
    opacity: 0.6,
    transparent: true,
    blending: THREE.AdditiveBlending,
    map: generateSprite()
  });

  group = new THREE.PointCloud(geometry, material);
  group.sortParticles = true;

  scene.add(group);
});

如您所见,我们使用THREE.PLYLoader来加载模型。回调函数返回geometry,我们将这个几何体作为THREE.PointCloud的输入。我们使用的材质与上一章中最后一个示例中使用的材质相同。如您所见,使用 Three.js,很容易将来自各种来源的模型组合在一起,并以不同的方式呈现它们,只需几行代码。

总结

在 Three.js 中使用外部来源的模型并不难。特别是对于简单的模型,您只需要采取几个简单的步骤。在处理外部模型或使用分组和合并创建模型时,有几件事情需要记住。首先,您需要记住的是,当您将对象分组时,它们仍然作为单独的对象可用。应用于父对象的变换也会影响子对象,但您仍然可以单独变换子对象。除了分组,您还可以将几何体合并在一起。通过这种方法,您会失去单独的几何体,并获得一个新的单一几何体。当您需要渲染成千上万个几何体并且遇到性能问题时,这种方法尤其有用。

Three.js 支持大量外部格式。在使用这些格式加载器时,最好查看源代码并记录回调中收到的信息。这将帮助您了解您需要采取的步骤,以获得正确的网格并将其设置到正确的位置和比例。通常,当模型显示不正确时,这是由其材质设置引起的。可能是使用了不兼容的纹理格式,不正确地定义了不透明度,或者格式包含了不正确的链接到纹理图像。通常最好使用测试材质来确定模型本身是否被正确加载,并记录加载的材质到 JavaScript 控制台以检查意外的值。还可以导出网格和场景,但请记住,Three.js 的GeometryExporterSceneExporterSceneLoader仍在进行中。

在本章和前几章中使用的模型大多是静态模型。它们不是动画的,不会四处移动,也不会改变形状。在下一章中,您将学习如何为模型添加动画,使其栩栩如生。除了动画,下一章还将解释 Three.js 提供的各种摄像机控制。通过摄像机控制,您可以在场景中移动、平移和旋转摄像机。

第九章:动画和移动相机

在前几章中,我们看到了一些简单的动画,但没有太复杂的。在第一章中,使用 Three.js 创建您的第一个 3D 场景,我们介绍了基本的渲染循环,在接下来的章节中,我们使用它来旋转一些简单的对象,并展示了一些其他基本的动画概念。在本章中,我们将更详细地了解 Three.js 如何支持动画。我们将详细讨论以下四个主题:

  • 基本动画

  • 移动相机

  • 变形和皮肤

  • 加载外部动画

我们从动画背后的基本概念开始。

基本动画

在我们看例子之前,让我们快速回顾一下在第一章中展示的渲染循环。为了支持动画,我们需要告诉 Three.js 每隔一段时间渲染一次场景。为此,我们使用标准的 HTML5 requestAnimationFrame功能,如下所示:

render();

function render() {

  // render the scene
  renderer.render(scene, camera);
  // schedule the next rendering using requestAnimationFrame
  requestAnimationFrame(render);
}

使用这段代码,我们只需要在初始化场景完成后一次调用render()函数。在render()函数本身中,我们使用requestAnimationFrame来安排下一次渲染。这样,浏览器会确保render()函数以正确的间隔被调用(通常大约每秒 60 次)。在requestAnimationFrame添加到浏览器之前,使用setInterval(function, interval)setTimeout(function, interval)。这些会在每个设置的间隔调用指定的函数。这种方法的问题在于它不考虑其他正在进行的事情。即使您的动画没有显示或在隐藏的标签中,它仍然被调用并且仍在使用资源。另一个问题是,这些函数在被调用时更新屏幕,而不是在浏览器最佳时机,这意味着更高的 CPU 使用率。使用requestAnimationFrame,我们不告诉浏览器何时需要更新屏幕;我们要求浏览器在最合适的时机运行提供的函数。通常,这会导致大约 60fps 的帧速率。使用requestAnimationFrame,您的动画将运行得更顺畅,对 CPU 和 GPU 更友好,而且您不必担心自己的时间问题。

简单动画

使用这种方法,我们可以通过改变它们的旋转、缩放、位置、材质、顶点、面和您能想象到的任何其他东西来非常容易地对对象进行动画处理。在下一个渲染循环中,Three.js 将渲染更改的属性。一个非常简单的例子,基于我们在第一章中已经看到的一个例子,可以在01-basic-animation.html中找到。以下截图显示了这个例子:

简单动画

这个渲染循环非常简单。只需改变相关网格的属性,Three.js 会处理其余的。我们是这样做的:

function render() {
  cube.rotation.x += controls.rotationSpeed;
  cube.rotation.y += controls.rotationSpeed;
  cube.rotation.z += controls.rotationSpeed;

  step += controls.bouncingSpeed;
  sphere.position.x = 20 + ( 10 * (Math.cos(step)));
  sphere.position.y = 2 + ( 10 * Math.abs(Math.sin(step)));

  scalingStep += controls.scalingSpeed;
  var scaleX = Math.abs(Math.sin(scalingStep / 4));
  var scaleY = Math.abs(Math.cos(scalingStep / 5));
  var scaleZ = Math.abs(Math.sin(scalingStep / 7));
  cylinder.scale.set(scaleX, scaleY, scaleZ);

  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

这里没有什么特别的,但它很好地展示了我们在本书中讨论的基本动画背后的概念。在下一节中,我们将快速地进行一个侧步。除了动画,另一个重要的方面是,当在更复杂的场景中使用 Three.js 时,您将很快遇到的一个方面是使用鼠标在屏幕上选择对象的能力。

选择对象

尽管与动画没有直接关系,但由于我们将在本章中研究相机和动画,这是对本章中解释的主题的一个很好的补充。我们将展示如何使用鼠标从场景中选择对象。在我们查看示例之前,我们将首先看看所需的代码:

var projector = new THREE.Projector();

function onDocumentMouseDown(event) {
  var vector = new THREE.Vector3(event.clientX / window.innerWidth ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5);
  vector = vector.unproject(camera);

  var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());

  var intersects = raycaster.intersectObjects([sphere, cylinder, cube]);

  if (intersects.length > 0) {
    intersects[ 0 ].object.material.transparent = true;
    intersects[ 0 ].object.material.opacity = 0.1;
  }
}

在这段代码中,我们使用THREE.ProjectorTHREE.Raycaster来确定我们是否点击了特定的对象。当我们点击屏幕时会发生以下情况:

  1. 首先,根据我们在屏幕上点击的位置创建了THREE.Vector3

  2. 接下来,使用vector.unproject函数,我们将屏幕上的点击位置转换为我们 Three.js 场景中的坐标。换句话说,我们从屏幕坐标转换为世界坐标。

  3. 接下来,我们创建THREE.Raycaster。使用THREE.Raycaster,我们可以在场景中发射射线。在这种情况下,我们从相机的位置(camera.position)发射射线到我们在场景中点击的位置。

  4. 最后,我们使用raycaster.intersectObjects函数来确定射线是否击中了提供的任何对象。

最终步骤的结果包含了被射线击中的任何对象的信息。提供了以下信息:

distance: 49.9047088522448
face: THREE.Face3
faceIndex: 4
object: THREE.Mesh
point: THREE.Vector3

被点击的网格是对象,facefaceIndex指向被选中的网格的面。distance值是从相机到点击对象的距离,point是点击网格的确切位置。您可以在02-selecting-objects.html示例中测试这一点。您点击的任何对象都将变为透明,并且选择的详细信息将被打印到控制台。

如果您想看到发射的射线路径,可以从菜单中启用showRay属性。以下屏幕截图显示了用于选择蓝色球体的射线:

选择对象

现在我们完成了这个小插曲,让我们回到我们的动画中。到目前为止,我们已经在渲染循环中改变了属性以使对象动画化。在下一节中,我们将看一下一个小型库,它可以更轻松地定义动画。

使用 Tween.js 进行动画

Tween.js 是一个小型的 JavaScript 库,您可以从github.com/sole/tween.js/下载,并且您可以使用它来轻松定义属性在两个值之间的过渡。所有开始和结束值之间的中间点都为您计算。这个过程称为缓动

例如,您可以使用这个库来在 10 秒内将网格的x位置从 10 改变为 3,如下所示:

var tween = new TWEEN.Tween({x: 10}).to({x: 3}, 10000).easing(TWEEN.Easing.Elastic.InOut).onUpdate( function () {
  // update the mesh
})

在这个例子中,我们创建了TWEEN.Tween。这个缓动将确保x属性在 10,000 毫秒的时间内从 10 改变为 3。Tween.js 还允许您定义属性随时间如何改变。这可以使用线性、二次或其他任何可能性来完成(请参阅sole.github.io/tween.js/examples/03_graphs.html获取完整的概述)。随时间值的变化方式称为缓动。使用 Tween.js,您可以使用easing()函数进行配置。

从 Three.js 中使用这个库非常简单。如果您打开03-animation-tween.html示例,您可以看到 Tween.js 库的实际效果。以下屏幕截图显示了示例的静态图像:

使用 Tween.js 进行动画

在这个例子中,我们从第七章中取了一个粒子云,粒子、精灵和点云,并将所有粒子动画化到地面上。这些粒子的位置是基于使用 Tween.js 库创建的缓动动画,如下所示:

// first create the tweens
var posSrc = {pos: 1}
var tween = new TWEEN.Tween(posSrc).to({pos: 0}, 5000);
tween.easing(TWEEN.Easing.Sinusoidal.InOut);

var tweenBack = new TWEEN.Tween(posSrc).to({pos: 1}, 5000);
tweenBack.easing(TWEEN.Easing.Sinusoidal.InOut);

tween.chain(tweenBack);
tweenBack.chain(tween);

var onUpdate = function () {
  var count = 0;
  var pos = this.pos;

  loadedGeometry.vertices.forEach(function (e) {
    var newY = ((e.y + 3.22544) * pos) - 3.22544;
    particleCloud.geometry.vertices[count++].set(e.x, newY, e.z);
  });

  particleCloud.sortParticles = true;
};

tween.onUpdate(onUpdate);
tweenBack.onUpdate(onUpdate);

通过这段代码,我们创建了两个缓动:tweentweenBack。第一个定义了位置属性从 1 过渡到 0 的方式,第二个则相反。通过chain()函数,我们将这两个缓动链接在一起,因此这些缓动在启动时将开始循环。我们在这里定义的最后一件事是onUpdate方法。在这个方法中,我们遍历粒子系统的所有顶点,并根据缓动提供的位置(this.pos)来改变它们的位置。

我们在模型加载时启动缓动,因此在以下函数的末尾,我们调用了tween.start()函数:

var loader = new THREE.PLYLoader();
loader.load( "../assets/models/test.ply", function (geometry) {
  ...
  tween.start()
  ...
});

当缓动开始时,我们需要告诉 Tween.js 库何时更新它所知道的所有缓动。我们通过调用TWEEN.update()函数来实现这一点:

function render() {
  TWEEN.update();
  webGLRenderer.render(scene, camera);
  requestAnimationFrame(render);
}

有了这些步骤,缓动库将负责定位点云的各个点。正如你所看到的,使用这个库比自己管理过渡要容易得多。

除了通过动画和更改对象来动画场景,我们还可以通过移动相机来动画场景。在前几章中,我们已经多次通过手动更新相机的位置来实现这一点。Three.js 还提供了许多其他更新相机的方法。

与相机一起工作

Three.js 有许多相机控件可供您在整个场景中控制相机。这些控件位于 Three.js 发行版中,可以在examples/js/controls目录中找到。在本节中,我们将更详细地查看以下控件:

名称 描述
FirstPersonControls 这些控件的行为类似于第一人称射击游戏中的控件。使用键盘四处移动,用鼠标四处张望。
FlyControls 这些是类似飞行模拟器的控件。使用键盘和鼠标进行移动和转向。
RollControls 这是FlyControls的简化版本。允许您在z轴周围移动和翻滚。
TrackBallControls 这是最常用的控件,允许您使用鼠标(或轨迹球)轻松地在场景中移动、平移和缩放。
OrbitControls 这模拟了围绕特定场景轨道上的卫星。这允许您使用鼠标和键盘四处移动。

这些控件是最有用的控件。除此之外,Three.js 还提供了许多其他控件可供使用(但本书中未进行解释)。但是,使用这些控件的方式与前表中解释的方式相同:

名称 描述
DeviceOrientationControls 根据设备的方向控制摄像机的移动。它内部使用 HTML 设备方向 API (www.w3.org/TR/orientation-event/)。
EditorControls 这些控件是专门为在线 3D 编辑器创建的。这是由 Three.js 在线编辑器使用的,您可以在threejs.org/editor/找到。
OculusControls 这些是允许您使用 Oculus Rift 设备在场景中四处张望的控件。
OrthographicTrackballControls 这与TrackBallControls相同,但专门用于与THREE.OrthographicCamera一起使用。
PointerLockControls 这是一个简单的控制,可以使用渲染场景的 DOM 元素锁定鼠标。这为简单的 3D 游戏提供了基本功能。
TransformControls 这是 Three.js 编辑器使用的内部控件。
VRControls 这是一个使用PositionSensorVRDevice API 来控制场景的控制器。有关此标准的更多信息,请访问developer.mozilla.org/en-US/docs/Web/API/Navigator.getVRDevices

除了使用这些相机控制,您当然也可以通过设置position来自行移动相机,并使用lookAt()函数更改其指向的位置。

提示

如果您曾经使用过较旧版本的 Three.js,您可能会错过一个名为THREE.PathControls的特定相机控件。使用此控件,可以定义路径(例如使用THREE.Spline)并沿该路径移动相机。在最新版本的 Three.js 中,由于代码复杂性,此控件已被移除。Three.js 背后的人目前正在开发替代方案,但目前还没有可用的替代方案。

我们将首先看一下TrackballControls控件。

TrackballControls

在使用TrackballControls之前,您首先需要将正确的 JavaScript 文件包含到您的页面中:

<script type="text/javascript" src="../libs/TrackballControls.js"></script>

包括这些内容后,我们可以创建控件并将其附加到相机上,如下所示:

var trackballControls = new THREE.TrackballControls(camera);
trackballControls.rotateSpeed = 1.0;
trackballControls.zoomSpeed = 1.0;
trackballControls.panSpeed = 1.0;

更新相机的位置是我们在渲染循环中做的事情,如下所示:

var clock = new THREE.Clock();
function render() {
  var delta = clock.getDelta();
  trackballControls.update(delta);
  requestAnimationFrame(render);
  webGLRenderer.render(scene, camera);
}

在上面的代码片段中,我们看到了一个新的 Three.js 对象,THREE.ClockTHREE.Clock对象可用于精确计算特定调用或渲染循环完成所需的经过时间。您可以通过调用clock.getDelta()函数来实现这一点。此函数将返回此调用和上一次调用getDelta()之间的经过时间。要更新相机的位置,我们调用trackballControls.update()函数。在此函数中,我们需要提供自上次调用此更新函数以来经过的时间。为此,我们使用THREE.Clock对象的getDelta()函数。您可能想知道为什么我们不只是将帧速率(1/60 秒)传递给update函数。原因是,使用requestAnimationFrame,我们可以期望 60 fps,但这并不是保证的。根据各种外部因素,帧速率可能会发生变化。为了确保相机平稳转动和旋转,我们需要传递确切的经过时间。

此示例的工作示例可以在04-trackball-controls-camera.html中找到。以下截图显示了此示例的静态图像:

TrackballControls

您可以以以下方式控制相机:

控制 动作
左键并移动 围绕场景旋转和滚动相机
滚轮 放大和缩小
中键并移动 放大和缩小
右键并移动 在场景中移动

有一些属性可以用来微调相机的行为。例如,您可以通过设置rotateSpeed属性来设置相机旋转的速度,并通过将noZoom属性设置为true来禁用缩放。在本章中,我们不会详细介绍每个属性的作用,因为它们几乎是不言自明的。要了解可能性的完整概述,请查看TrackballControls.js文件的源代码,其中列出了这些属性。

FlyControls

我们将要看的下一个控件是FlyControls。使用FlyControls,您可以使用在飞行模拟器中找到的控件在场景中飞行。示例可以在05-fly-controls-camera.html中找到。以下截图显示了此示例的静态图像:

FlyControls

启用FlyControls的方式与TrackballControls相同。首先,加载正确的 JavaScript 文件:

<script type="text/javascript" src="../libs/FlyControls.js"></script>

接下来,我们配置控件并将其附加到相机上,如下所示:

var flyControls = new THREE.FlyControls(camera);
flyControls.movementSpeed = 25;
flyControls.domElement = document.querySelector('#WebGL-output');
flyControls.rollSpeed = Math.PI / 24;
flyControls.autoForward = true;
flyControls.dragToLook = false;

再次,我们不会深入研究所有具体的属性。查看FlyControls.js文件的源代码。让我们只挑选出需要配置的属性来使控制器工作。需要正确设置的属性是domElement属性。该属性应指向我们渲染场景的元素。在本书的示例中,我们使用以下元素作为输出:

<div id="WebGL-output"></div>

我们设置属性如下:

flyControls.domElement = document.querySelector('#WebGL-output');

如果我们没有正确设置这个属性,鼠标移动会导致奇怪的行为。

你可以用THREE.FlyControls来控制相机:

控制 动作
左键和中键 开始向前移动
右鼠标按钮 向后移动
鼠标移动 四处看看
W 开始向前移动
S 向后移动
A 向左移动
D 向右移动
R 向上移动
F 向下移动
左、右、上、下箭头 向左、向右、向上、向下看
G 向左翻滚
E 向右翻滚

我们将要看的下一个控制是THREE.RollControls

RollControls

RollControls的行为与FlyControls基本相同,所以我们在这里不会详细介绍。RollControls可以这样创建:

var rollControls = new THREE.RollControls(camera);
rollControls.movementSpeed = 25;
rollControls.lookSpeed = 3;

如果你想玩玩这个控制,看看06-roll-controls-camera.html的例子。请注意,如果你只看到一个黑屏,把鼠标移到浏览器底部,城市景观就会出现。这个相机可以用以下控制移动:

控制 动作
左鼠标按钮 向前移动
右鼠标按钮 向后移动
左、右、上、下箭头 向左、向右、向前、向后移动
W 向前移动
A 向左移动
S 向后移动
D 向右移动
Q 向左翻滚
E 向右翻滚
R 向上移动
F 向下移动

我们将要看的基本控制的最后一个是FirstPersonControls

FirstPersonControls

正如其名称所示,FirstPersonControls允许你像第一人称射击游戏一样控制相机。鼠标用来四处看看,键盘用来四处走动。你可以在07-first-person-camera.html中找到一个例子。以下截图显示了这个例子的静态图像:

FirstPersonControls

创建这些控制遵循了我们到目前为止看到的其他控制所遵循的相同原则。我们刚刚展示的例子使用了以下配置:

var camControls = new THREE.FirstPersonControls(camera);
camControls.lookSpeed = 0.4;
camControls.movementSpeed = 20;
camControls.noFly = true;
camControls.lookVertical = true;
camControls.constrainVertical = true;
camControls.verticalMin = 1.0;
camControls.verticalMax = 2.0;
camControls.lon = -150;
camControls.lat = 120;

当你自己使用这个控制时,你应该仔细看看的唯一属性是最后两个:lonlat属性。这两个属性定义了当场景第一次渲染时相机指向的位置。

这个控制的控制非常简单:

控制 动作
鼠标移动 四处看看
左、右、上、下箭头 向左、向右、向前、向后移动
W 向前移动
A 向左移动
S 向后移动
D 向右移动
R 向上移动
F 向下移动
Q 停止所有移动

对于下一个控制,我们将从第一人称视角转向太空视角。

OrbitControl

OrbitControl控制是围绕场景中心的物体旋转和平移的好方法。在08-controls-orbit.html中,我们包含了一个展示这个控制如何工作的例子。以下截图显示了这个例子的静态图像:

OrbitControl

使用OrbitControl和使用其他控制一样简单。包括正确的 JavaScript 文件,设置控制与相机,再次使用THREE.Clock来更新控制:

<script type="text/javascript" src="../libs/OrbitControls.js"></script>
...
var orbitControls = new THREE.OrbitControls(camera);
orbitControls.autoRotate = true;
var clock = new THREE.Clock();
...
var delta = clock.getDelta();
orbitControls.update(delta);

THREE.OrbitControls的控制集中在使用鼠标上,如下表所示:

控制 动作
左鼠标点击+移动 围绕场景中心旋转相机
滚轮或中键点击+移动 放大和缩小
右鼠标点击+移动 在场景中四处移动
左、右、上、下箭头 在场景中四处移动

摄像机和移动已经介绍完了。在这一部分,我们已经看到了许多控制,可以让你创建有趣的摄像机动作。在下一节中,我们将看一下更高级的动画方式:变形和蒙皮。

变形和骨骼动画

当您在外部程序(例如 Blender)中创建动画时,通常有两种主要选项来定义动画:

  • 变形目标:使用变形目标,您定义了网格的变形版本,即关键位置。对于这个变形目标,存储了所有顶点位置。要使形状动画化,您需要将所有顶点从一个位置移动到另一个关键位置并重复该过程。以下屏幕截图显示了用于显示面部表情的各种变形目标(以下图像由 Blender 基金会提供):变形和骨骼动画

  • 骨骼动画:另一种选择是使用骨骼动画。使用骨骼动画,您定义网格的骨架,即骨骼,并将顶点附加到特定的骨骼上。现在,当您移动一个骨骼时,任何连接的骨骼也会适当移动,并且根据骨骼的位置、移动和缩放移动和变形附加的顶点。下面再次由 Blender 基金会提供的屏幕截图显示了骨骼如何用于移动和变形对象的示例:变形和骨骼动画

Three.js 支持这两种模式,但通常使用变形目标可能会获得更好的结果。骨骼动画的主要问题是从 Blender 等 3D 程序中获得可以在 Three.js 中进行动画处理的良好导出。使用变形目标比使用骨骼和皮肤更容易获得良好的工作模型。

在本节中,我们将研究这两种选项,并另外查看 Three.js 支持的一些外部格式,其中可以定义动画。

使用变形目标的动画

变形目标是定义动画的最直接方式。您为每个重要位置(也称为关键帧)定义所有顶点,并告诉 Three.js 将顶点从一个位置移动到另一个位置。然而,这种方法的缺点是,对于大型网格和大型动画,模型文件将变得非常庞大。原因是对于每个关键位置,所有顶点位置都会重复。

我们将向您展示如何使用两个示例处理变形目标。在第一个示例中,我们将让 Three.js 处理各个关键帧(或我们从现在开始称之为变形目标)之间的过渡,在第二个示例中,我们将手动完成这个过程。

使用 MorphAnimMesh 的动画

在我们的第一个变形示例中,我们将使用 Three.js 分发的模型之一——马。了解基于变形目标的动画如何工作的最简单方法是打开10-morph-targets.html示例。以下屏幕截图显示了此示例的静态图像:

使用 MorphAnimMesh 的动画

在此示例中,右侧的马正在进行动画和奔跑,而左侧的马站在原地。左侧的这匹马是从基本模型即原始顶点集渲染的。在右上角的菜单中,您可以浏览所有可用的变形目标,并查看左侧马可以采取的不同位置。

Three.js 提供了一种从一个位置移动到另一个位置的方法,但这意味着我们必须手动跟踪我们所在的当前位置和我们想要变形成的目标,并且一旦达到目标位置,就要重复这个过程以达到其他位置。幸运的是,Three.js 还提供了一个特定的网格,即THREE.MorphAnimMesh,它会为我们处理这些细节。在我们继续之前,这里有关于 Three.js 提供的另一个与动画相关的网格THREE.MorphBlendMesh的快速说明。如果您浏览 Three.js 提供的对象,您可能会注意到这个对象。使用这个特定的网格,您可以做的事情几乎与THREE.MorphAnimMesh一样多,当您查看源代码时,甚至可以看到这两个对象之间的许多内容是重复的。然而,THREE.MorphBlendMesh似乎已经被弃用,并且在任何官方的 Three.js 示例中都没有使用。您可以使用THREE.MorphAnimMesh来完成THREE.MorhpBlendMesh可以完成的所有功能,因此请使用THREE.MorphAnimMesh来进行这种功能。以下代码片段显示了如何从模型加载并创建THREE.MorphAnimMesh

var loader = new THREE.JSONLoader();
loader.load('../assets/models/horse.js', function(geometry, mat) {

  var mat = new THREE.MeshLambertMaterial({ morphTargets: true, vertexColors: THREE.FaceColors});

  morphColorsToFaceColors(geometry);
  geometry.computeMorphNormals();
  meshAnim = new THREE.MorphAnimMesh(geometry, mat );
  scene.add(meshAnim);

},'../assets/models' );

function morphColorsToFaceColors(geometry) {

  if (geometry.morphColors && geometry.morphColors.length) {

    var colorMap = geometry.morphColors[ 0 ];
    for (var i = 0; i < colorMap.colors.length; i++) {
      geometry.faces[ i ].color = colorMap.colors[ i ];
      geometry.faces[ i ].color.offsetHSL(0, 0.3, 0);
    }
  }
}

这与我们加载其他模型时看到的方法相同。然而,这次外部模型还包含了变形目标。我们创建THREE.MorphAnimMesh而不是创建普通的THREE.Mesh对象。加载动画时需要考虑几件事情:

  • 确保您使用的材质将THREE.morphTargets设置为true。如果没有设置,您的网格将不会动画。

  • 在创建THREE.MorphAnimMesh之前,请确保在几何体上调用computeMorphNormals,以便计算所有变形目标的法线向量。这对于正确的光照和阴影效果是必需的。

  • 还可以为特定变形目标的面定义颜色。这些可以从morphColors属性中获得。您可以使用这个来变形几何体的形状,也可以变形各个面的颜色。使用morphColorsToFaceColors辅助方法,我们只需将面的颜色固定为morphColors数组中的第一组颜色。

  • 默认设置是一次性播放完整的动画。如果为同一几何体定义了多个动画,您可以使用parseAnimations()函数和playAnimation(name,fps)来播放其中一个定义的动画。我们将在本章的最后一节中使用这种方法,从 MD2 模型中加载动画。

剩下的就是在渲染循环中更新动画。为此,我们再次使用THREE.Clock来计算增量,并用它来更新动画,如下所示:

function render() {

  var delta = clock.getDelta();
  webGLRenderer.clear();
  if (meshAnim) {
    meshAnim.updateAnimation(delta *1000);
    meshAnim.rotation.y += 0.01;
  }

  // render using requestAnimationFrame
  requestAnimationFrame(render);
  webGLRenderer.render(scene, camera);
}

这种方法是最简单的,可以让您快速设置来自具有定义变形目标的模型的动画。另一种方法是手动设置动画,我们将在下一节中展示。

通过设置 morphTargetInfluence 属性创建动画

我们将创建一个非常简单的示例,其中我们将一个立方体从一个形状变形为另一个形状。这一次,我们将手动控制我们将变形到哪个目标。您可以在11-morph-targets-manually.html中找到这个示例。以下截图显示了这个示例的静态图像:

通过设置 morphTargetInfluence 属性创建动画

在这个例子中,我们手动为一个简单的立方体创建了两个变形目标,如下所示:

// create a cube
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
var cubeMaterial = new THREE.MeshLambertMaterial({morphTargets: true, color: 0xff0000});

// define morphtargets, we'll use the vertices from these geometries
var cubeTarget1 = new THREE.CubeGeometry(2, 10, 2);
var cubeTarget2 = new THREE.CubeGeometry(8, 2, 8);

// define morphtargets and compute the morphnormal
cubeGeometry.morphTargets[0] = {name: 'mt1', vertices: cubeTarget2.vertices};
cubeGeometry.morphTargets[1] = {name: 'mt2', vertices: cubeTarget1.vertices};
cubeGeometry.computeMorphNormals();

var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);

当您打开这个示例时,您会看到一个简单的立方体。在右上角的滑块中,您可以设置morphTargetInfluences。换句话说,您可以确定初始立方体应该变形成指定为mt1的立方体的程度,以及它应该变形成mt2的程度。当您手动创建变形目标时,您需要考虑到变形目标与源几何体具有相同数量的顶点。您可以使用网格的morphTargetInfluences属性来设置影响:

var controls = new function () {
  // set to 0.01 to make sure dat.gui shows correct output
  this.influence1 = 0.01;
  this.influence2 = 0.01;

  this.update = function () {
    cube.morphTargetInfluences[0] = controls.influence1;
    cube.morphTargetInfluences[1] = controls.influence2;
  };
}

请注意,初始几何图形可以同时受多个形态目标的影响。这两个例子展示了形态目标动画背后的最重要的概念。在下一节中,我们将快速查看使用骨骼和蒙皮进行动画。

使用骨骼和蒙皮进行动画

形态动画非常直接。Three.js 知道所有目标顶点位置,只需要将每个顶点从一个位置过渡到下一个位置。对于骨骼和蒙皮,情况会变得有点复杂。当您使用骨骼进行动画时,您移动骨骼,Three.js 必须确定如何相应地转换附加的皮肤(一组顶点)。在这个例子中,我们使用从 Blender 导出到 Three.js 格式(models文件夹中的hand-1.js)的模型。这是一个手的模型,包括一组骨骼。通过移动骨骼,我们可以对整个模型进行动画。让我们首先看一下我们如何加载模型:

var loader = new THREE.JSONLoader();
loader.load('../assets/models/hand-1.js', function (geometry, mat) {
  var mat = new THREE.MeshLambertMaterial({color: 0xF0C8C9, skinning: true});
  mesh = new THREE.SkinnedMesh(geometry, mat);

  // rotate the complete hand
  mesh.rotation.x = 0.5 * Math.PI;
  mesh.rotation.z = 0.7 * Math.PI;

  // add the mesh
  scene.add(mesh);

  // and start the animation
  tween.start();

}, '../assets/models');

加载用于骨骼动画的模型与加载任何其他模型并无太大不同。我们只需指定包含顶点、面和骨骼定义的模型文件,然后基于该几何图形创建一个网格。Three.js 还提供了一个特定的用于这样的蒙皮几何的网格,称为THREE.SkinnedMesh。确保模型得到更新的唯一一件事是将您使用的材质的skinning属性设置为true。如果不将其设置为true,则不会看到任何骨骼移动。我们在这里做的最后一件事是将所有骨骼的useQuaternion属性设置为false。在这个例子中,我们将使用一个tween对象来处理动画。这个tween实例定义如下:

var tween = new TWEEN.Tween({pos: -1}).to({pos: 0}, 3000).easing(TWEEN.Easing.Cubic.InOut).yoyo(true).repeat(Infinity).onUpdate(onUpdate);

通过这个 Tween,我们将pos变量从-1过渡到0。我们还将yoyo属性设置为true,这会导致我们的动画在下一次运行时以相反的方式运行。为了确保我们的动画保持运行,我们将repeat设置为Infinity。您还可以看到我们指定了一个onUpdate方法。这个方法用于定位各个骨骼,接下来我们将看一下这个方法。

在我们移动骨骼之前,让我们先看一下12-bones-manually.html示例。以下屏幕截图显示了这个示例的静态图像:

使用骨骼和蒙皮进行动画

当您打开此示例时,您会看到手做出抓取的动作。我们通过在从我们的 Tween 动画调用的onUpdate方法中设置手指骨骼的z旋转来实现这一点,如下所示:

var onUpdate = function () {
  var pos = this.pos;

  // rotate the fingers
  mesh.skeleton.bones[5].rotation.set(0, 0, pos);
  mesh.skeleton.bones[6].rotation.set(0, 0, pos);
  mesh.skeleton.bones[10].rotation.set(0, 0, pos);
  mesh.skeleton.bones[11].rotation.set(0, 0, pos);
  mesh.skeleton.bones[15].rotation.set(0, 0, pos);
  mesh.skeleton.bones[16].rotation.set(0, 0, pos);
  mesh.skeleton.bones[20].rotation.set(0, 0, pos);
  mesh.skeleton.bones[21].rotation.set(0, 0, pos);

  // rotate the wrist
  mesh.skeleton.bones[1].rotation.set(pos, 0, 0);
};

每当调用此更新方法时,相关的骨骼都设置为pos位置。要确定需要移动哪根骨骼,最好打印出mesh.skeleton属性到控制台。这将列出所有骨骼及其名称。

提示

Three.js 提供了一个简单的辅助工具,您可以用它来显示模型的骨骼。将以下内容添加到代码中:

helper = new THREE.SkeletonHelper( mesh );
helper.material.linewidth = 2;
helper.visible = false;
scene.add( helper );

骨骼被突出显示。您可以通过启用12-bones-manually.html示例中显示的showHelper属性来查看此示例。

正如您所看到的,使用骨骼需要更多的工作,但比固定的形态目标更灵活。在这个例子中,我们只移动了骨骼的旋转;您还可以移动位置或更改比例。在下一节中,我们将看一下从外部模型加载动画。在该部分,我们将重新访问这个例子,但现在,我们将从模型中运行预定义的动画,而不是手动移动骨骼。

使用外部模型创建动画

在第八章创建和加载高级网格和几何体中,我们看了一些 Three.js 支持的 3D 格式。其中一些格式也支持动画。在本章中,我们将看一下以下示例:

  • 使用 JSON 导出器的 Blender:我们将从 Blender 中创建的动画开始,并将其导出到 Three.js JSON 格式。

  • Collada 模型:Collada 格式支持动画。在此示例中,我们将从 Collada 文件加载动画,并在 Three.js 中呈现它。

  • MD2 模型:MD2 模型是旧版 Quake 引擎中使用的简单格式。尽管该格式有点过时,但仍然是存储角色动画的非常好的格式。

我们将从 Blender 模型开始。

使用 Blender 创建骨骼动画

要开始使用 Blender 中的动画,您可以加载我们在 models 文件夹中包含的示例。您可以在那里找到hand.blend文件,然后将其加载到 Blender 中。以下截图显示了这个示例的静态图像:

使用 Blender 创建骨骼动画

在本书中,没有足够的空间详细介绍如何在 Blender 中创建动画,但有一些事情需要记住:

  • 您的模型中的每个顶点至少必须分配给一个顶点组。

  • 您在 Blender 中使用的顶点组的名称必须对应于控制它的骨骼的名称。这样,Three.js 可以确定移动骨骼时需要修改哪些顶点。

  • 只有第一个“动作”被导出。因此,请确保要导出的动画是第一个。

  • 在创建关键帧时,最好选择所有骨骼,即使它们没有改变。

  • 在导出模型时,请确保模型处于静止姿势。如果不是这种情况,您将看到一个非常畸形的动画。

有关在 Blender 中创建和导出动画以及上述要点的原因的更多信息,您可以查看以下优秀资源:devmatrix.wordpress.com/2013/02/27/creating-skeletal-animation-in-blender-and-exporting-it-to-three-js/

当您在 Blender 中创建动画时,可以使用我们在上一章中使用的 Three.js 导出器导出文件。在使用 Three.js 导出器导出文件时,您必须确保检查以下属性:

使用 Blender 创建骨骼动画

这将导出您在 Blender 中指定的动画作为骨骼动画,而不是形变动画。使用骨骼动画,骨骼的移动被导出,我们可以在 Three.js 中重放。

在 Three.js 中加载模型与我们之前的示例相同;但是,现在模型加载后,我们还将创建一个动画,如下所示:

var loader = new THREE.JSONLoader();
loader.load('../assets/models/hand-2.js', function (model, mat) {

  var mat = new THREE.MeshLambertMaterial({color: 0xF0C8C9, skinning: true});
  mesh = new THREE.SkinnedMesh(model, mat);

  var animation = new THREE.Animation(mesh, model.animation);

  mesh.rotation.x = 0.5 * Math.PI;
  mesh.rotation.z = 0.7 * Math.PI;
  scene.add(mesh);

  // start the animation
  animation.play();

}, '../assets/models');

要运行此动画,我们只需创建一个THREE.Animation实例,并在此动画上调用play方法。在看到动画之前,我们还需要执行一个额外的步骤。在我们的渲染循环中,我们调用THREE.AnimationHandler.update(clock.getDelta())函数来更新动画,Three.js 将使用骨骼来设置模型的正确位置。这个示例(13-animation-from-blender.html)的结果是一个简单的挥手。

以下截图显示了这个示例的静态图像:

使用 Blender 创建骨骼动画

除了 Three.js 自己的格式,我们还可以使用其他几种格式来定义动画。我们将首先看一下加载 Collada 模型。

从 Collada 模型加载动画

从 Collada 文件加载模型的工作方式与其他格式相同。首先,您必须包含正确的加载器 JavaScript 文件:

<script type="text/javascript" src="../libs/ColladaLoader.js"></script>

接下来,我们创建一个加载器并使用它来加载模型文件:

var loader = new THREE.ColladaLoader();
loader.load('../assets/models/monster.dae', function (collada) {

  var child = collada.skins[0];
  scene.add(child);

  var animation = new THREE.Animation(child, child.geometry.animation);
  animation.play();

  // position the mesh
  child.scale.set(0.15, 0.15, 0.15);
  child.rotation.x = -0.5 * Math.PI;
  child.position.x = -100;
  child.position.y = -60;
});

Collada 文件不仅可以包含单个模型,还可以存储完整的场景,包括摄像机、灯光、动画等。使用 Collada 模型的一个好方法是将loader.load函数的结果打印到控制台,并确定要使用的组件。在这种情况下,场景中只有一个THREE.SkinnedMeshchild)。要渲染和动画化这个模型,我们所要做的就是设置动画,就像我们为基于 Blender 的模型所做的那样;甚至渲染循环保持不变。以下是我们如何渲染和动画化模型的方法:

function render() {
  ...
  meshAnim.updateAnimation( delta *1000 );
  ...
}

而这个特定 Collada 文件的结果看起来像这样:

从 Collada 模型加载动画

另一个使用变形目标的外部模型的例子是 MD2 文件格式。

从 Quake 模型加载的动画

MD2 格式是为了对 Quake 中的角色进行建模而创建的,Quake 是一款 1996 年的伟大游戏。尽管新的引擎使用了不同的格式,但你仍然可以在 MD2 格式中找到许多有趣的模型。要使用这种格式的文件,我们首先必须将它们转换为 Three.js 的 JavaScript 格式。你可以在以下网站上在线进行转换:

oos.moxiecode.com/js_webgl/md2_converter/

转换后,你会得到一个以 Three.js 格式的 JavaScript 文件,你可以使用MorphAnimMesh加载和渲染。由于我们已经在前面的章节中看到了如何做到这一点,我们将跳过加载模型的代码。不过代码中有一件有趣的事情。我们不是播放完整的动画,而是提供需要播放的动画的名称:

mesh.playAnimation('crattack', 10);

原因是 MD2 文件通常包含许多不同的角色动画。不过,Three.js 提供了功能来确定可用的动画并使用playAnimation函数播放它们。我们需要做的第一件事是告诉 Three.js 解析动画:

mesh.parseAnimations();

这将导致一个动画名称列表,可以使用playAnimation函数播放。在我们的例子中,你可以在右上角的菜单中选择动画的名称。可用的动画是这样确定的:

mesh.parseAnimations();

var animLabels = [];
for (var key in mesh.geometry.animations) {
  if (key === 'length' || !mesh.geometry.animations.hasOwnProperty(key)) continue;
  animLabels.push(key);
}

gui.add(controls,'animations',animLabels).onChange(function(e) {
  mesh.playAnimation(controls.animations,controls.fps);
});

每当从菜单中选择一个动画时,都会使用指定的动画名称调用mesh.playAnimation函数。演示这一点的例子可以在15-animation-from-md2.html中找到。以下截图显示了这个例子的静态图像:

从 Quake 模型加载的动画

摘要

在本章中,我们看了一些不同的方法,你可以为你的场景添加动画。我们从一些基本的动画技巧开始,然后转移到摄像机的移动和控制,最后使用变形目标和骨骼/骨骼动画来动画模型。当你有了渲染循环后,添加动画就变得非常容易。只需改变网格的属性,在下一个渲染步骤中,Three.js 将渲染更新后的网格。

在之前的章节中,我们看了一下你可以用来皮肤化你的物体的各种材料。例如,我们看到了如何改变这些材料的颜色、光泽和不透明度。然而,我们还没有详细讨论过的是如何使用外部图像(也称为纹理)与这些材料一起。使用纹理,你可以轻松地创建看起来像是由木头、金属、石头等制成的物体。在下一章中,我们将探讨纹理的各个方面以及它们在 Three.js 中的使用方式。

第十章:加载和使用纹理

在第四章中,使用 Three.js 材质,我们向您介绍了 Three.js 中可用的各种材质。然而,在那一章中,我们没有讨论如何将纹理应用到网格上。在本章中,我们将讨论这个主题。更具体地说,在本章中,我们将讨论以下主题:

  • 在 Three.js 中加载纹理并将其应用于网格

  • 使用凹凸和法线贴图为网格应用深度和细节

  • 使用光照图创建假阴影

  • 使用环境贴图为材质添加详细的反射

  • 使用高光贴图来设置网格特定部分的光泽

  • 微调和自定义网格的 UV 映射

  • 使用 HTML5 画布和视频元素作为纹理的输入

让我们从最基本的示例开始,向您展示如何加载和应用纹理。

在材质中使用纹理

在 Three.js 中有不同的纹理使用方式。您可以使用它们来定义网格的颜色,但也可以使用它们来定义光泽、凹凸和反射。我们首先看的例子是最基本的方法,即使用纹理来定义网格的每个像素的颜色。

加载纹理并将其应用于网格

纹理的最基本用法是将其设置为材质上的映射。当您使用此材质创建网格时,网格的颜色将基于提供的纹理着色。

加载纹理并在网格上使用它可以通过以下方式完成:

function createMesh(geom, imageFile) {
  var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile)

  var mat = new THREE.MeshPhongMaterial();
  mat.map = texture;

  var mesh = new THREE.Mesh(geom, mat);
  return mesh;
}

在这个代码示例中,我们使用THREE.ImageUtils.loadTexture函数从特定位置加载图像文件。您可以使用 PNG、GIF 或 JPEG 图像作为纹理的输入。请注意,加载纹理是异步完成的。在我们的场景中,这不是一个问题,因为我们有一个render循环,每秒渲染大约 60 次。如果您想要等待直到纹理加载完成,您可以使用以下方法:

texture = THREE.ImageUtils.loadTexture('texture.png', {}, function() { renderer.render(scene); });

在这个示例中,我们向loadTexture提供了一个回调函数。当纹理加载时,将调用此回调。在我们的示例中,我们不使用回调,而是依赖于render循环最终在加载纹理时显示纹理。

您几乎可以使用任何您喜欢的图像作为纹理。然而,最好的结果是当您使用一个边长是 2 的幂的正方形纹理时。因此,边长为 256 x 256、512 x 512、1024 x 1024 等尺寸效果最好。以下图像是一个正方形纹理的示例:

加载纹理并将其应用于网格

由于纹理的像素(也称为texels)通常不是一对一地映射到面的像素上,因此需要对纹理进行放大或缩小。为此,WebGL 和 Three.js 提供了一些不同的选项。您可以通过设置magFilter属性来指定纹理的放大方式,通过设置minFilter属性来指定缩小方式。这些属性可以设置为以下两个基本值:

名称 描述
THREE.NearestFilter 此滤镜使用它能找到的最近的像素的颜色。当用于放大时,这将导致块状,当用于缩小时,结果将丢失很多细节。
THREE.LinearFilter 此滤镜更先进,使用四个相邻像素的颜色值来确定正确的颜色。在缩小时仍会丢失很多细节,但放大会更加平滑,不那么块状。

除了这些基本值,我们还可以使用 mipmap。Mipmap是一组纹理图像,每个图像的尺寸都是前一个的一半。当加载纹理时会创建这些图像,并允许更平滑的过滤。因此,当您有一个正方形纹理(作为 2 的幂),您可以使用一些额外的方法来获得更好的过滤效果。这些属性可以使用以下值进行设置:

名称 描述
THREE.NearestMipMapNearestFilter 此属性选择最佳映射所需分辨率的 mipmap,并应用我们在前表中讨论的最近过滤原则。放大仍然很粗糙,但缩小看起来好多了。
THREE.NearestMipMapLinearFilter 此属性不仅选择单个 mipmap,还选择两个最接近的 mipmap 级别。在这两个级别上,应用最近的过滤器以获得两个中间结果。这两个结果通过线性过滤器传递以获得最终结果。
THREE.LinearMipMapNearestFilter 此属性选择最佳映射所需分辨率的 mipmap,并应用我们在前表中讨论的线性过滤原则。
THREE.LinearMipMapLinearFilter 此属性不仅选择单个 mipmap,还选择两个最接近的 mipmap 级别。在这两个级别上,应用线性过滤器以获得两个中间结果。这两个结果通过线性过滤器传递以获得最终结果。

如果您没有明确指定magFilterminFilter属性,Three.js 将使用THREE.LinearFilter作为magFilter属性的默认值,并使用THREE.LinearMipMapLinearFilter作为minFilter属性的默认值。在我们的示例中,我们将使用这些默认属性。基本纹理的示例可以在01-basic-texture.html中找到。以下屏幕截图显示了此示例:

加载纹理并将其应用于网格

在此示例中,我们加载了一些纹理(使用您之前看到的代码)并将它们应用于各种形状。在此示例中,您可以看到纹理很好地包裹在形状周围。在 Three.js 中创建几何图形时,它会确保正确应用任何使用的纹理。这是通过一种称为UV 映射的东西完成的(本章后面将详细介绍)。通过 UV 映射,我们告诉渲染器应将纹理的哪一部分应用于特定的面。最简单的示例是立方体。其中一个面的 UV 映射如下所示:

(0,1),(0,0),(1,0),(1,1)

这意味着我们对这个面使用完整的纹理(UV 值范围从 0 到 1)。

除了我们可以使用THREE.ImageUtils.loadTexture加载的标准图像格式之外,Three.js 还提供了一些自定义加载程序,您可以使用这些加载程序加载以不同格式提供的纹理。以下表格显示了您可以使用的其他加载程序:

名称 描述

| THREE.DDSLoader | 使用此加载程序,您可以加载以 DirectDraw Surface 格式提供的纹理。这种格式是一种专有的微软格式,用于存储压缩纹理。使用此加载程序非常简单。首先,在 HTML 页面中包含DDSLoader.js文件,然后使用以下内容使用纹理:

var loader = new THREE.DDSLoader();
var texture = loader.load( '../assets/textures/  seafloor.dds' );

var mat = new THREE.MeshPhongMaterial();
mat.map = texture;

您可以在本章的源代码中看到此加载程序的示例:01-basic-texture-dds.html。在内部,此加载程序使用THREE.CompressedTextureLoader。|

| THREE.PVRLoader | Power VR 是另一种专有文件格式,用于存储压缩纹理。Three.js 支持 Power VR 3.0 文件格式,并可以使用以此格式提供的纹理。要使用此加载程序,请在 HTML 页面中包含PVRLoader.js文件,然后使用以下内容使用纹理:

var loader = new THREE.DDSLoader();
var texture = loader.load( '../assets/textures/ seafloor.dds' );

var mat = new THREE.MeshPhongMaterial();
mat.map = texture;

您可以在本章的源代码中看到此加载程序的示例:01-basic-texture-pvr.html。请注意,并非所有的 WebGL 实现都支持此格式的纹理。因此,当您使用此格式但未看到纹理时,请检查控制台以查看错误。在内部,此加载程序还使用THREE.CompressedTextureLoader。|

| THREE.TGALoader | Targa 是一种光栅图形文件格式,仍然被大量 3D 软件程序使用。使用THREE.TGALoader对象,您可以在 3D 模型中使用以此格式提供的纹理。要使用这些图像文件,您首先必须在 HTML 中包含TGALoader.js文件,然后可以使用以下内容加载 TGA 纹理:

var loader = new THREE.TGALoader();
var texture = loader.load( '../assets/textures/crate_color8.tga' );

var mat = new THREE.MeshPhongMaterial();
mat.map = texture;

本章的源代码中提供了此加载器的示例。您可以通过在浏览器中打开01-basic-texture-tga.html来查看此示例。|

在这些示例中,我们使用纹理来定义网格像素的颜色。我们还可以将纹理用于其他目的。以下两个示例用于定义如何应用阴影到材质上。您可以使用这个来在网格表面创建凸起和皱纹。

使用凸起贴图创建皱纹

凸起贴图用于增加材质的深度。您可以通过打开02-bump-map.html示例来看到其效果。请参考以下截图查看示例:

使用凸起贴图创建皱纹

在此示例中,您可以看到左侧墙看起来比右侧墙更详细,并且在比较时似乎具有更多的深度。这是通过在材质上设置额外的纹理,所谓的凸起贴图来实现的:

function createMesh(geom, imageFile, bump) {
  var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile)
  var mat = new THREE.MeshPhongMaterial();
  mat.map = texture;

  var bump = THREE.ImageUtils.loadTexture(
    "../assets/textures/general/" + bump)
  mat.bumpMap = bump;
  mat.bumpScale = 0.2;

  var mesh = new THREE.Mesh(geom, mat);
  return mesh;
}

您可以在此代码中看到,除了设置map属性之外,我们还将bumpMap属性设置为纹理。另外,通过bumpScale属性,我们可以设置凸起的高度(或如果设置为负值则为深度)。此示例中使用的纹理如下所示:

使用凸起贴图创建皱纹

凸起贴图是灰度图像,但您也可以使用彩色图像。像素的强度定义了凸起的高度。凸起贴图只包含像素的相对高度。它并不表示坡度的方向。因此,使用凸起贴图可以达到的细节水平和深度感知是有限的。要获得更多细节,您可以使用法线贴图。

使用法线贴图实现更详细的凸起和皱纹

在法线贴图中,高度(位移)不会被存储,而是存储了每个图像的法线方向。不详细介绍,使用法线贴图,您可以创建看起来非常详细的模型,而仍然只使用少量的顶点和面。例如,查看03-normal-map.html示例。以下截图显示了此示例:

使用法线贴图实现更详细的凸起和皱纹

在此截图中,您可以看到左侧有一个非常详细的抹灰立方体。光源在立方体周围移动,您可以看到纹理对光源的自然响应。这提供了一个非常逼真的模型,只需要一个非常简单的模型和几个纹理。以下代码片段显示了如何在 Three.js 中使用法线贴图:

function createMesh(geom, imageFile, normal) {
  var t = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile);
  var m = THREE.ImageUtils.loadTexture("../assets/textures/general/" + normal);

  var mat2 = new THREE.MeshPhongMaterial();
  mat2.map = t;
  mat2.normalMap = m;

  var mesh = new THREE.Mesh(geom, mat2);
  return mesh;
}

这里使用的方法与凸起贴图相同。不过这次,我们将normalMap属性设置为法线纹理。我们还可以通过设置normalScale属性mat.normalScale.set(1,1)来定义凸起的外观。通过这两个属性,您可以沿着xy轴进行缩放。不过,最好的方法是保持这些值相同以获得最佳效果。请注意,再次强调,当这些值低于零时,高度会反转。以下截图显示了纹理(左侧)和法线贴图(右侧):

使用法线贴图实现更详细的凸起和皱纹

然而,法线贴图的问题在于它们不太容易创建。您需要使用专门的工具,如 Blender 或 Photoshop。它们可以使用高分辨率渲染或纹理作为输入,并从中创建法线贴图。

Three.js 还提供了一种在运行时执行此操作的方法。THREE.ImageUtils对象有一个名为getNormalMap的函数,它接受 JavaScript/DOMImage作为输入,并将其转换为法线贴图。

使用光照贴图创建虚假阴影

在之前的示例中,我们使用特定的贴图来创建看起来真实的阴影,这些阴影会对房间中的光照做出反应。还有另一种选择可以创建假阴影。在本节中,我们将使用光照贴图。光照贴图是一个预渲染的阴影(也称为预烘烤阴影),您可以使用它来营造真实阴影的错觉。以下截图来自04-light-map.html示例,展示了这个效果:

使用光照贴图创建假阴影

如果您看一下之前的示例,会发现有两个非常漂亮的阴影,似乎是由两个立方体投射出来的。然而,这些阴影是基于一个看起来像下面这样的光照贴图的:

使用光照贴图创建假阴影

正如您所看到的,光照贴图中指定的阴影也显示在地面上,营造出真实阴影的错觉。您可以使用这种技术创建高分辨率的阴影,而不会产生沉重的渲染惩罚。当然,这仅适用于静态场景。使用光照贴图与使用其他纹理基本相同,只有一些小差异。以下是我们使用光照贴图的方法:

var lm = THREE.ImageUtils.loadTexture('../assets/textures/lightmap/lm-1.png');
var wood = THREE.ImageUtils.loadTexture('../assets/textures/general/floor-wood.jpg');
var groundMaterial = new THREE.MeshBasicMaterial({lightMap: lm, map: wood});
groundGeom.faceVertexUvs[1] = groundGeom.faceVertexUvs[0];

要应用光照贴图,我们只需要将材质的lightMap属性设置为我们刚刚展示的光照贴图。然而,还需要额外的步骤才能让光照贴图显示出来。我们需要明确定义 UV 映射(纹理在面上的哪一部分)以便独立应用和映射光照贴图。在我们的示例中,我们只使用了基本的 UV 映射,这是在创建地面时由 Three.js 自动创建的。更多信息和为什么需要明确定义 UV 映射的背景可以在stackoverflow.com/questions/15137695/three-js-lightmap-causes-an-error-webglrenderingcontext-gl-error-gl-invalid-op找到。

当阴影贴图正确放置后,我们需要将立方体放置在正确的位置,以便看起来阴影是由它们投射出来的。

Three.js 提供了另一种纹理,您可以使用它来模拟高级的 3D 效果。在下一节中,我们将看看如何使用环境贴图来模拟反射。

使用环境贴图创建假反射

计算环境反射非常消耗 CPU,并且通常需要使用光线追踪器方法。如果您想在 Three.js 中使用反射,仍然可以做到,但您需要模拟它。您可以通过创建对象所在环境的纹理并将其应用于特定对象来实现这一点。首先,我们将展示我们的目标结果(请参阅05-env-map-static.html,也显示在以下截图中):

使用环境贴图创建假反射

在这个截图中,您可以看到球体和立方体反射了环境。如果您移动鼠标,还可以看到反射与您在城市环境中的相机角度相对应。为了创建这个示例,我们执行以下步骤:

  1. 创建 CubeMap 对象:我们需要做的第一件事是创建一个CubeMap对象。CubeMap是一组可以应用于立方体每一面的六个纹理。

  2. 使用这个 CubeMap 对象创建一个盒子:带有CubeMap的盒子是您在移动相机时看到的环境。它给人一种错觉,好像您站在一个可以四处看的环境中。实际上,您是在一个立方体内部,内部渲染了纹理,给人一种空间的错觉。

  3. 将 CubeMap 对象应用为纹理:我们用来模拟环境的CubeMap对象也可以作为网格的纹理。Three.js 会确保它看起来像环境的反射。

一旦您获得了源材料,创建CubeMap就非常简单。您需要的是六张图片,它们共同组成一个完整的环境。因此,您需要以下图片:向前看(posz)、向后看(negz)、向上看(posy)、向下看(negy)、向右看(posx)和向左看(negx)。Three.js 将这些拼接在一起,以创建一个无缝的环境映射。有几个网站可以下载这些图片。本例中使用的图片来自www.humus.name/index.php?page=Textures

一旦您获得了六张单独的图片,您可以按照以下代码片段中所示的方式加载它们:

function createCubeMap() {

  var path = "../assets/textures/cubemap/parliament/";
  var format = '.jpg';
  var urls = [
    path + 'posx' + format, path + 'negx' + format,
    path + 'posy' + format, path + 'negy' + format,
    path + 'posz' + format, path + 'negz' + format
  ];

  var textureCube = THREE.ImageUtils.loadTextureCube( urls );
  return textureCube;
}

我们再次使用THREE.ImageUtils JavaScript 对象,但这次,我们传入一个纹理数组,并使用loadTextureCube函数创建CubeMap对象。如果您已经有了 360 度全景图像,您也可以将其转换为一组图像,以便创建CubeMap。只需转到gonchar.me/panorama/来转换图像,您最终会得到六张带有名称如right.pngleft.pngtop.pngbottom.pngfront.pngback.png的图像。您可以通过创建以下方式的urls变量来使用这些图像:

var urls = [
  'right.png',
  'left.png',
  'top.png',
  'bottom.png',
  'front.png',
  'back.png'
];

或者,您还可以在加载场景时让 Three.js 处理转换,方法是创建textureCube,如下所示:

var textureCube = THREE.ImageUtils.loadTexture("360-degrees.png", new THREE.UVMapping());

使用CubeMap,我们首先创建一个盒子,可以这样创建:

var textureCube = createCubeMap();
var shader = THREE.ShaderLib[ "cube" ];
shader.uniforms[ "tCube" ].value = textureCube;
var material = new THREE.ShaderMaterial( {
  fragmentShader: shader.fragmentShader,
  vertexShader: shader.vertexShader,
  uniforms: shader.uniforms,
  depthWrite: false,
  side: THREE.BackSide
});
cubeMesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100), material);

Three.js 提供了一个特定的着色器,我们可以使用THREE.ShaderMaterial来基于CubeMap创建一个环境(var shader = THREE.ShaderLib[ "cube" ];)。我们使用CubeMap配置此着色器,创建一个网格,并将其添加到场景中。如果从内部看,这个网格代表我们所处的虚假环境。

这个相同的CubeMap对象应该应用于我们想要渲染的网格,以创建虚假的反射:

var sphere1 = createMesh(new THREE.SphereGeometry(10, 15, 15), "plaster.jpg");
sphere1.material.envMap = textureCube;
sphere1.rotation.y = -0.5;
sphere1.position.x = 12;
sphere1.position.y = 5;
scene.add(sphere1);

var cube = createMesh(new THREE.CubeGeometry(10, 15, 15), "plaster.jpg","plaster-normal.jpg");
sphere2.material.envMap = textureCube;
sphere2.rotation.y = 0.5;
sphere2.position.x = -12;
sphere2.position.y = 5;
scene.add(cube);

如您所见,我们将材质的envMap属性设置为我们创建的cubeMap对象。结果是一个场景,看起来我们站在一个宽阔的室外环境中,网格反映了这个环境。如果您使用滑块,可以设置材质的reflectivity属性,正如其名称所示,这决定了材质反射了多少环境。

除了反射,Three.js 还允许您为折射(类似玻璃的对象)使用CubeMap对象。以下屏幕截图显示了这一点:

使用环境映射创建虚假反射

要获得这种效果,我们只需要将纹理加载更改为以下内容:

var textureCube = THREE.ImageUtils.loadTextureCube( urls, new THREE.CubeRefractionMapping());

您可以使用材质上的refraction属性来控制refraction比例,就像使用reflection属性一样。在本例中,我们为网格使用了静态环境映射。换句话说,我们只看到了环境反射,而没有看到环境中的其他网格。在下面的屏幕截图中(您可以通过在浏览器中打开05-env-map-dynamic.html来查看),我们将向您展示如何创建一个反射,同时还显示场景中的其他对象:

使用环境映射创建虚假反射

要显示场景中其他对象的反射,我们需要使用一些其他 Three.js 组件。我们需要的第一件事是一个名为THREE.CubeCamera的额外相机:

Var cubeCamera = new THREE.CubeCamera(0.1, 20000, 256);
scene.add(cubeCamera);

我们将使用THREE.CubeCamera来拍摄包含所有渲染对象的场景快照,并使用它来设置CubeMap。您需要确保将此相机定位在您想要显示动态反射的THREE.Mesh的确切位置上。在本例中,我们将仅在中心球上显示反射(如前一个屏幕截图中所示)。该球位于位置 0, 0, 0,因此在本例中,我们不需要显式定位THREE.CubeCamera

我们只将动态反射应用于球体,因此我们需要两种不同的材质:

var dynamicEnvMaterial = new THREE.MeshBasicMaterial({envMap: cubeCamera.renderTarget });
var envMaterial = new THREE.MeshBasicMaterial({envMap: textureCube });

与我们之前的例子的主要区别是,对于动态反射,我们将envMap属性设置为cubeCamera.renderTarget,而不是我们之前创建的textureCube。对于这个例子,我们在中心球体上使用dynamicEnvMaterial,在其他两个对象上使用envMaterial

sphere = new THREE.Mesh(sphereGeometry, dynamicEnvMaterial);
sphere.name = 'sphere';
scene.add(sphere);

var cylinder = new THREE.Mesh(cylinderGeometry, envMaterial);
cylinder.name = 'cylinder';
scene.add(cylinder);
cylinder.position.set(10, 0, 0);

var cube = new THREE.Mesh(boxGeometry, envMaterial);
cube.name = 'cube';
scene.add(cube);
cube.position.set(-10, 0, 0);

现在剩下的就是确保cubeCamera渲染场景,这样我们就可以将其输出用作中心球体的输入。为此,我们更新render循环如下:

function render() {
  sphere.visible = false;
  cubeCamera.updateCubeMap( renderer, scene );
  sphere.visible = true;
  renderer.render(scene, camera);
  ...
  requestAnimationFrame(render);
}

正如你所看到的,我们首先禁用了sphere的可见性。我们这样做是因为我们只想看到来自其他两个对象的反射。接下来,我们通过调用updateCubeMap函数使用cubeCamera渲染场景。之后,我们再次使sphere可见,并像平常一样渲染场景。结果是,在球体的反射中,你可以看到立方体和圆柱的反射。

我们将看到的基本材质的最后一个是高光贴图。

高光贴图

使用高光贴图,你可以指定一个定义材质光泽度和高光颜色的贴图。例如,在下面的截图中,我们使用了高光贴图和法线贴图来渲染一个地球。你可以在浏览器中打开06-specular-map.html来查看这个例子。其结果也显示在下面的截图中:

高光贴图

在这个截图中,你可以看到海洋被突出显示并反射光线。另一方面,大陆非常黑暗,不反射(太多)光线。为了达到这种效果,我们没有使用任何特定的法线纹理,而只使用了法线贴图来显示高度和以下高光贴图来突出显示海洋:

高光贴图

基本上,像素的值越高(从黑色到白色),表面看起来就越有光泽。高光贴图通常与specular属性一起使用,你可以用它来确定反射的颜色。在这种情况下,它被设置为红色:

var specularTexture=THREE.ImageUtils.loadTexture("../assets/textures/planets/EarthSpec.png");
var normalTexture=THREE.ImageUtils.loadTexture("../assets/textures/planets/EarthNormal.png");

var planetMaterial = new THREE.MeshPhongMaterial();
planetMaterial.specularMap = specularTexture;
planetMaterial.specular = new THREE.Color( 0xff0000 );
planetMaterial.shininess = 1;

planetMaterial.normalMap = normalTexture;

还要注意的是,通常使用低光泽度可以实现最佳效果,但根据光照和你使用的高光贴图,你可能需要进行实验以获得期望的效果。

纹理的高级用法

在上一节中,我们看到了一些基本的纹理用法。Three.js 还提供了更高级纹理用法的选项。在本节中,我们将看一下 Three.js 提供的一些选项。

自定义 UV 映射

我们将从更深入地了解 UV 映射开始。我们之前解释过,使用 UV 映射,你可以指定纹理的哪一部分显示在特定的面上。当你在 Three.js 中创建几何体时,这些映射也会根据你创建的几何体类型自动创建。在大多数情况下,你不需要真正改变这个默认的 UV 映射。理解 UV 映射工作原理的一个好方法是看一个来自 Blender 的例子,如下面的截图所示:

自定义 UV 映射

在这个例子中,你可以看到两个窗口。左侧的窗口包含一个立方体几何体。右侧的窗口是 UV 映射,我们加载了一个示例纹理来展示映射的方式。在这个例子中,我们选择了左侧窗口的一个单独面,并且右侧窗口显示了这个面的 UV 映射。你可以看到,面的每个顶点都位于右侧 UV 映射的一个角落(小圆圈)。这意味着完整的纹理将被用于这个面。这个立方体的所有其他面也以相同的方式映射,因此结果将显示一个每个面都显示完整纹理的立方体;参见07-uv-mapping.html,也显示在下面的截图中:

自定义 UV 映射

这是 Blender 中(也是 Three.js 中)立方体的默认设置。让我们通过只选择纹理的三分之二来改变 UV(在下面的截图中看到所选区域):

自定义 UV 映射

如果我们现在在 Three.js 中展示这个,你会看到纹理被应用的方式不同,如下截图所示:

自定义 UV 映射

自定义 UV 映射通常是从诸如 Blender 之类的程序中完成的,特别是当模型变得更加复杂时。这里最重要的部分是记住 UV 映射在两个维度上运行,从 0 到 1。要自定义 UV 映射,你需要为每个面定义应该显示纹理的部分。你需要通过定义组成面的每个顶点的uv坐标来实现这一点。你可以使用以下代码来设置uv值:

geom.faceVertexUvs[0][0][0].x = 0.5;
geom.faceVertexUvs[0][0][0].y = 0.7;
geom.faceVertexUvs[0][0][1].x = 0.4;
geom.faceVertexUvs[0][0][1].y = 0.1;
geom.faceVertexUvs[0][0][2].x = 0.4;
geom.faceVertexUvs[0][0][2].y = 0.5;

这段代码将把第一个面的uv属性设置为指定的值。记住每个面由三个顶点定义,所以要设置一个面的所有uv值,我们需要设置六个属性。如果你打开07-uv-mapping-manual.html例子,你可以看到当你手动改变uv映射时会发生什么。以下截图展示了这个例子:

自定义 UV 映射

接下来,我们将看一下纹理如何通过一些内部 UV 映射技巧来重复。

重复包装

当你在 Three.js 中应用纹理到一个几何体上时,Three.js 会尽可能地优化应用纹理。例如,对于立方体,这意味着每一面都会显示完整的纹理,对于球体,完整的纹理会被包裹在球体周围。然而,有些情况下你可能不希望纹理在整个面或整个几何体上展开,而是希望纹理重复出现。Three.js 提供了详细的功能来控制这一点。一个可以用来调整重复属性的例子在08-repeat-wrapping.html中提供。以下截图展示了这个例子:

重复包装

在这个例子中,你可以设置控制纹理重复的属性。

在这个属性产生期望效果之前,你需要确保你将纹理的包装设置为THREE.RepeatWrapping,如下代码片段所示:

cube.material.map.wrapS = THREE.RepeatWrapping;
cube.material.map.wrapT = THREE.RepeatWrapping;

wrapS属性定义了你希望纹理在x轴上的行为,wrapT属性定义了纹理在y轴上的行为。Three.js 为此提供了两个选项,如下所示:

  • THREE.RepeatWrapping允许纹理重复出现。

  • THREE.ClampToEdgeWrapping是默认设置。使用THREE.ClampToEdgeWrapping,纹理不会整体重复,而只有边缘的像素会重复。

如果你禁用了repeatWrapping菜单选项,将会使用THREE.ClampToEdgeWrapping选项,如下所示:

重复包装

如果我们使用THREE.RepeatWrapping,我们可以设置repeat属性,如下代码片段所示:

cube.material.map.repeat.set(repeatX, repeatY);

repeatX变量定义了纹理在x轴上重复的次数,repeatY变量定义了在y轴上的重复次数。如果这些值设置为1,纹理就不会重复;如果设置为更高的值,你会看到纹理开始重复。你也可以使用小于 1 的值。在这种情况下,你会看到你会放大纹理。如果你将重复值设置为负值,纹理会被镜像。

当你改变repeat属性时,Three.js 会自动更新纹理并使用新的设置进行渲染。如果你从THREE.RepeatWrapping改变到THREE.ClampToEdgeWrapping,你需要显式地更新纹理:

cube.material.map.needsUpdate = true;

到目前为止,我们只使用了静态图像作为纹理。然而,Three.js 也有选项可以使用 HTML5 画布作为纹理。

渲染到画布并将其用作纹理

在本节中,我们将看两个不同的示例。首先,我们将看一下如何使用画布创建一个简单的纹理并将其应用于网格,然后,我们将进一步创建一个可以用作凹凸贴图的画布,使用随机生成的图案。

使用画布作为纹理

在第一个示例中,我们将使用Literally库(来自literallycanvas.com/)创建一个交互式画布,您可以在其上绘制;请参见以下截图的左下角。您可以在09-canvas-texture中查看此示例。随后的截图显示了此示例:

使用画布作为纹理

您在此画布上绘制的任何内容都会直接呈现在立方体上作为纹理。在 Three.js 中实现这一点非常简单,只需要几个步骤。我们需要做的第一件事是创建一个画布元素,并且对于这个特定的示例,配置它以便与Literally库一起使用,如下所示:

<div class="fs-container">
  <div id="canvas-output" style="float:left">
  </div>
</div>
...
var canvas = document.createElement("canvas");
$('#canvas-output')[0].appendChild(canvas);
$('#canvas-output').literallycanvas(
  {imageURLPrefix: '../libs/literally/img'});

我们只需从 JavaScript 中创建一个canvas元素,并将其添加到特定的div元素中。通过literallycanvas调用,我们可以创建绘图工具,您可以直接在画布上绘制。接下来,我们需要创建一个使用画布绘制作为其输入的纹理:

function createMesh(geom) {

  var canvasMap = new THREE.Texture(canvas);
  var mat = new THREE.MeshPhongMaterial();
  mat.map = canvasMap;
  var mesh = new THREE.Mesh(geom,mat);

  return mesh;
}

正如代码所示,您在创建新纹理时所需做的唯一事情就是在传入画布元素的引用时,new THREE.Texture(canvas)。这将创建一个使用画布元素作为其材质的纹理。剩下的就是在每次渲染时更新材质,以便在立方体上显示画布绘制的最新版本,如下所示:

function render() {
  stats.update();

  cube.rotation.y += 0.01;
  cube.rotation.x += 0.01;

  cube.material.map.needsUpdate = true;
  requestAnimationFrame(render);
  webGLRenderer.render(scene, camera);
}

为了通知 Three.js 我们想要更新纹理,我们只需将纹理的needsUpdate属性设置为true。在这个示例中,我们已经将画布元素用作最简单的纹理输入。当然,我们可以使用相同的思路来处理到目前为止看到的所有不同类型的地图。在下一个示例中,我们将把它用作凹凸贴图。

使用画布作为凹凸贴图

正如我们在本章前面看到的,我们可以使用凹凸贴图创建一个简单的皱纹纹理。在这个地图中,像素的强度越高,皱纹越深。由于凹凸贴图只是一个简单的黑白图像,所以我们可以在画布上创建这个图像,并将该画布用作凹凸贴图的输入。

在下一个示例中,我们使用画布生成一个随机灰度图像,并将该图像用作我们应用于立方体的凹凸贴图的输入。请参见09-canvas-texture-bumpmap.html示例。以下截图显示了此示例:

使用画布作为凹凸贴图

这需要的 JavaScript 代码与我们之前解释的示例并没有太大不同。我们需要创建一个画布元素,并用一些随机噪声填充这个画布。对于噪声,我们使用Perlin noise。Perlin noise (en.wikipedia.org/wiki/Perlin_noise) 生成一个非常自然的随机纹理,正如您在前面的截图中所看到的。我们使用来自github.com/wwwtyro/perlin.js的 Perlin noise 函数来实现这一点:

var ctx = canvas.getContext("2d");
function fillWithPerlin(perlin, ctx) {

  for (var x = 0; x < 512; x++) {
    for (var y = 0; y < 512; y++) {
      var base = new THREE.Color(0xffffff);
      var value = perlin.noise(x / 10, y / 10, 0);
      base.multiplyScalar(value);
      ctx.fillStyle = "#" + base.getHexString();
      ctx.fillRect(x, y, 1, 1);
    }
  }
}

我们使用perlin.noise函数根据画布元素的xy坐标创建一个从 0 到 1 的值。这个值用于在画布元素上绘制一个单个像素。对所有像素执行此操作会创建一个随机地图,您也可以在上一张截图的左下角看到。然后可以轻松地将此地图用作凹凸贴图。以下是创建随机地图的方法:

var bumpMap = new THREE.Texture(canvas);

var mat = new THREE.MeshPhongMaterial();
mat.color = new THREE.Color(0x77ff77);
mat.bumpMap = bumpMap;
bumpMap.needsUpdate = true;

var mesh = new THREE.Mesh(geom, mat);
return mesh;

提示

在这个例子中,我们使用 HTML 画布元素渲染了 Perlin 噪声。Three.js 还提供了一种动态创建纹理的替代方法。THREE.ImageUtils对象有一个generateDataTexture函数,你可以使用它来创建特定大小的THREE.DataTexture纹理。这个纹理包含在image.data属性中的Uint8Array,你可以直接使用它来设置这个纹理的 RGB 值。

我们用于纹理的最终输入是另一个 HTML 元素:HTML5 视频元素。

使用视频输出作为纹理

如果你读过前面关于渲染到画布的段落,你可能会考虑将视频渲染到画布并将其用作纹理的输入。这是一个选择,但是 Three.js(通过 WebGL)已经直接支持使用 HTML5 视频元素。查看11-video-texture.html。参考以下截图,了解这个例子的静态图像:

使用视频输出作为纹理

使用视频作为纹理的输入,就像使用画布元素一样,非常容易。首先,我们需要有一个视频元素来播放视频:

<video  id="video"
  style="display: none;
  position: absolute; left: 15px; top: 75px;"
  src="../assets/movies/Big_Buck_Bunny_small.ogv"
  controls="true" autoplay="true">
</video>

这只是一个基本的 HTML5 视频元素,我们设置为自动播放。接下来,我们可以配置 Three.js 以将此视频用作纹理的输入,如下所示:

var video  = document.getElementById('video');
texture = new THREE.Texture(video);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = false;

由于我们的视频不是正方形的,我们需要确保在材质上禁用 mipmap 生成。我们还设置了一些简单的高性能滤镜,因为材质经常变化。现在剩下的就是创建一个网格并设置纹理。在这个例子中,我们使用了MeshFaceMaterialMeshBasicMaterial

var materialArray = [];
materialArray.push(new THREE.MeshBasicMaterial({color: 0x0051ba}));
materialArray.push(new THREE.MeshBasicMaterial({color: 0x0051ba}));
materialArray.push(new THREE.MeshBasicMaterial({color: 0x0051ba}));
materialArray.push(new THREE.MeshBasicMaterial({color: 0x0051ba}));
materialArray.push(new THREE.MeshBasicMaterial({map: texture }));
materialArray.push(new THREE.MeshBasicMaterial({color: 0xff51ba}));

var faceMaterial = new THREE.MeshFaceMaterial(materialArray);
var mesh = new THREE.Mesh(geom,faceMaterial);

现在剩下的就是确保在我们的render循环中更新纹理,如下所示:

if ( video.readyState === video.HAVE_ENOUGH_DATA ) {
  if (texture) texture.needsUpdate = true;
}

在这个例子中,我们只是将视频渲染到立方体的一侧,但由于这是一个普通的纹理,我们可以随心所欲地使用它。例如,我们可以使用自定义 UV 映射沿着立方体的边缘分割它,或者甚至将视频输入用作凹凸贴图或法线贴图的输入。

在 Three.js 版本 r69 中,引入了一个专门用于处理视频的纹理。这个纹理(THREE.VideoTexture)包装了你在本节中看到的代码,你可以使用THREE.VideoTexture方法作为一种替代方法。以下代码片段显示了如何使用THREE.VideoTexture创建纹理(你可以通过查看11-video-texture.html示例来查看这个过程):

var video = document.getElementById('video');
texture = new THREE.VideoTexture(video);

总结

因此,我们结束了关于纹理的这一章。正如你所看到的,Three.js 中有许多不同类型的纹理,每种都有不同的用途。你可以使用 PNG、JPG、GIF、TGA、DDS 或 PVR 格式的任何图像作为纹理。加载这些图像是异步进行的,所以记得要么使用渲染循环,要么在加载纹理时添加回调。使用纹理,你可以从低多边形模型创建出色的对象,甚至可以使用凹凸贴图和法线贴图添加虚假的详细深度。使用 Three.js,还可以使用 HTML5 画布元素或视频元素轻松创建动态纹理。只需定义一个以这些元素为输入的纹理,并在需要更新纹理时将needsUpdate属性设置为true

通过这一章,我们基本上涵盖了 Three.js 的所有重要概念。然而,我们还没有看到 Three.js 提供的一个有趣的功能——后期处理。通过后期处理,你可以在场景渲染后添加效果。例如,你可以模糊或着色你的场景,或者使用扫描线添加类似电视的效果。在下一章中,我们将看看后期处理以及如何将其应用到你的场景中。

第十一章:自定义着色器和渲染后期处理

我们即将结束这本书,在本章中,我们将看一下我们尚未涉及的 Three.js 的主要特性:渲染后期处理。除此之外,在本章中,我们还将介绍如何创建自定义着色器。本章我们将讨论的主要内容如下:

  • 为后期处理设置 Three.js

  • 讨论 Three.js 提供的基本后期处理通道,比如THREE.BloomPassTHREE.FilmPass

  • 使用蒙版将效果应用于场景的一部分

  • 使用THREE.TexturePass来存储渲染结果

  • 使用THREE.ShaderPass添加更基本的后期处理效果,比如棕褐色滤镜,镜像效果和颜色调整

  • 使用THREE.ShaderPass进行各种模糊效果和更高级的滤镜

  • 通过编写一个简单的着色器创建自定义后期处理效果

在第一章的介绍 requestAnimationFrame部分,使用 Three.js 创建您的第一个 3D 场景,我们设置了一个渲染循环,我们在整本书中都用来渲染和动画我们的场景。对于后期处理,我们需要对这个设置进行一些更改,以允许 Three.js 对最终渲染进行后期处理。在第一部分中,我们将看看如何做到这一点。

为后期处理设置 Three.js

为了为后期处理设置 Three.js,我们需要对我们当前的设置进行一些更改。我们需要采取以下步骤:

  1. 创建THREE.EffectComposer,我们可以用来添加后期处理通道。

  2. 配置THREE.EffectComposer,使其渲染我们的场景并应用任何额外的后期处理步骤。

  3. 在渲染循环中,使用THREE.EffectComposer来渲染场景,应用通道,并显示输出。

和往常一样,我们有一个可以用来实验并用于自己用途的例子。本章的第一个例子可以从01-basic-effect-composer.html中访问。您可以使用右上角的菜单修改此示例中使用的后期处理步骤的属性。在这个例子中,我们渲染了一个简单的地球,并为其添加了类似旧电视的效果。这个电视效果是在使用THREE.EffectComposer渲染场景之后添加的。以下截图显示了这个例子:

为后期处理设置 Three.js

创建 THREE.EffectComposer

让我们首先看一下您需要包含的额外 JavaScript 文件。这些文件可以在 Three.js 分发的examples/js/postprocessingexamples/js/shaders目录中找到。

使THREE.EffectComposer工作所需的最小设置如下:

<script type="text/javascript" src="../libs/postprocessing/EffectComposer.js"></script>
<script type="text/javascript" src="../libs/postprocessing/MaskPass.js"></script>
<script type="text/javascript" src="../libs/postprocessing/RenderPass.js"></script>
<script type="text/javascript" src="../libs/shaders/CopyShader.js"></script>
<script type="text/javascript" src="../libs/postprocessing/ShaderPass.js"></script>

EffectComposer.js文件提供了THREE.EffectComposer对象,允许我们添加后期处理步骤。MaskPass.jsShaderPass.jsCopyShader.jsTHREE.EffectComposer内部使用,RenderPass.js允许我们向THREE.EffectComposer添加渲染通道。没有这个通道,我们的场景将根本不会被渲染。

在这个例子中,我们添加了两个额外的 JavaScript 文件,为我们的场景添加了类似电影的效果:

<script type="text/javascript" src="../libs/postprocessing/FilmPass.js"></script>
<script type="text/javascript" src="../libs/shaders/FilmShader.js"></script>

我们需要做的第一件事是创建THREE.EffectComposer。您可以通过将THREE.WebGLRenderer传递给它的构造函数来实现:

var webGLRenderer = new THREE.WebGLRenderer();
var composer = new THREE.EffectComposer(webGLRenderer);

接下来,我们向这个合成器添加各种通道

为后期处理配置 THREE.EffectComposer

每个通道按照添加到THREE.EffectComposer的顺序执行。我们添加的第一个通道是THREE.RenderPass。接下来的通道渲染了我们的场景,但还没有输出到屏幕上:

var renderPass = new THREE.RenderPass(scene, camera);
composer.addPass(renderPass);

要创建THREE.RenderPass,我们传入要渲染的场景和要使用的相机。使用addPass函数,我们将THREE.RenderPass添加到THREE.EffectComposer中。下一步是添加另一个通行证,将其结果输出到屏幕上。并非所有可用的通行证都允许这样做——稍后会详细介绍——但是在这个例子中使用的THREE.FilmPass允许我们将其通行证的结果输出到屏幕上。要添加THREE.FilmPass,我们首先需要创建它并将其添加到 composer 中。生成的代码如下:

var renderPass = new THREE.RenderPass(scene,camera);
var effectFilm = new THREE.FilmPass(0.8, 0.325, 256, false);
effectFilm.renderToScreen = true;

var composer = new THREE.EffectComposer(webGLRenderer);
composer.addPass(renderPass);
composer.addPass(effectFilm);

正如您所看到的,我们创建了THREE.FilmPass并将renderToScreen属性设置为true。这个通行证被添加到THREE.EffectComposer之后的renderPass之后,所以当使用这个 composer 时,首先渲染场景,通过THREE.FilmPass,我们也可以在屏幕上看到输出。

更新渲染循环

现在我们只需要对渲染循环进行一点修改,以使用 composer 而不是THREE.WebGLRenderer

var clock = new THREE.Clock();
function render() {
  stats.update();

  var delta = clock.getDelta();
  orbitControls.update(delta);

  sphere.rotation.y += 0.002;

  requestAnimationFrame(render);
  composer.render(delta);
}

我们唯一的修改是删除了webGLRenderer.render(scene, camera),并用composer.render(delta)替换它。这将在EffectComposer上调用渲染函数,而EffectComposer又使用传入的THREE.WebGLRenderer,由于我们将FilmPassrenderToScreen设置为true,因此FilmPass的结果显示在屏幕上。

有了这个基本设置,我们将在接下来的几节中看看可用的后期处理通行证。

后期处理通行证

Three.js 提供了许多后期处理通行证,您可以直接在THREE.EffectComposer中使用。请注意,最好尝试本章中的示例,以查看这些通行证的结果并理解发生了什么。以下表格概述了可用的通行证:

通行证名称 描述
THREE.BloomPass 这是一种效果,使光亮区域渗入较暗区域。这模拟了相机被极其明亮的光所淹没的效果。
THREE.DotScreenPass 这在屏幕上应用了一层代表原始图像的黑点。
THREE.FilmPass 这通过应用扫描线和失真来模拟电视屏幕。
THREE.GlitchPass 这在屏幕上显示一个电子故障,以随机时间间隔。
THREE.MaskPass 这允许您对当前图像应用蒙版。后续通行证仅应用于蒙版区域。
THREE.RenderPass 这根据提供的场景和相机渲染场景。
THREE.SavePass 当执行此通行证时,它会复制当前的渲染步骤,以便以后使用。这个通行证在实践中并不那么有用,我们不会在任何示例中使用它。
THREE.ShaderPass 这允许您为高级或自定义后期处理通行证传递自定义着色器。
THREE.TexturePass 这将当前 composer 的状态存储在一个纹理中,您可以将其用作其他EffectComposer实例的输入。

让我们从一些简单的通行证开始。

简单的后期处理通行证

对于简单的通行证,我们将看看我们可以用THREE.FilmPassTHREE.BloomPassTHREE.DotScreenPass做些什么。对于这些通行证,有一个例子可用,02-post-processing-simple,允许您尝试这些通行证,并查看它们如何以不同的方式影响原始输出。以下屏幕截图显示了这个例子:

Simple postprocessing passes

在这个例子中,我们同时显示了四个场景,并且在每个场景中,添加了不同的后期处理通行证。左上角的一个显示了THREE.BloomPass,右上角的一个显示了THREE.FilmPass,左下角的一个显示了THREE.DotScreenPass,右下角的一个显示了原始渲染。

在这个例子中,我们还使用THREE.ShaderPassTHREE.TexturePass来重用原始渲染的输出作为其他三个场景的输入。因此,在我们查看各个 pass 之前,让我们先看看这两个 pass:

var renderPass = new THREE.RenderPass(scene, camera);
var effectCopy = new THREE.ShaderPass(THREE.CopyShader);
effectCopy.renderToScreen = true;

var composer = new THREE.EffectComposer(webGLRenderer);
composer.addPass(renderPass);
composer.addPass(effectCopy);

var renderScene = new THREE.TexturePass(composer.renderTarget2);

在这段代码中,我们设置了THREE.EffectComposer,它将输出默认场景(右下角的场景)。这个 composer 有两个 passes。THREE.RenderPass渲染场景,而THREE.ShaderPass在配置为THREE.CopyShader时,如果将renderToScreen属性设置为true,则将输出渲染到屏幕上。如果你看例子,你会发现我们展示了同一个场景四次,但每次都应用了不同的效果。我们可以使用THREE.RenderPass从头开始渲染场景四次,但这样有点浪费,因为我们可以重用第一个 composer 的输出。为此,我们创建了THREE.TexturePass并传入了composer.renderTarget2的值。现在我们可以使用renderScene变量作为其他 composer 的输入,而无需从头开始渲染场景。让我们首先重新审视THREE.FilmPass,看看我们如何使用THREE.TexturePass作为输入。

使用 THREE.FilmPass 创建类似电视的效果

在本章的第一部分,我们已经看过如何创建THREE.FilmPass,现在让我们看看如何将这个效果与上一节的THREE.TexturePass一起使用:

var effectFilm = new THREE.FilmPass(0.8, 0.325, 256, false);
effectFilm.renderToScreen = true;

var composer4 = new THREE.EffectComposer(webGLRenderer);
**composer4.addPass(renderScene);**
composer4.addPass(effectFilm);

使用THREE.TexturePass的唯一步骤是将它作为你的 composer 中的第一个 pass 添加。接下来,我们只需添加THREE.FilmPass,效果就会应用上。THREE.FilmPass本身有四个参数:

属性 描述
noiseIntensity 这个属性允许你控制场景看起来有多粗糙。
scanlinesIntensity THREE.FilmPass向场景添加了一些扫描线。使用这个属性,你可以定义这些扫描线的显示程度。
scanLinesCount 可以使用这个属性控制显示的扫描线数量。
grayscale 如果设置为true,输出将被转换为灰度。

实际上,你可以有两种方式传入这些参数。在这个例子中,我们将它们作为构造函数的参数传入,但你也可以直接设置它们,如下所示:

effectFilm.uniforms.grayscale.value = controls.grayscale;
effectFilm.uniforms.nIntensity.value = controls.noiseIntensity;
effectFilm.uniforms.sIntensity.value = controls.scanlinesIntensity;
effectFilm.uniforms.sCount.value = controls.scanlinesCount;

在这种方法中,我们使用了uniforms属性,它用于直接与 WebGL 通信。在本章稍后讨论创建自定义着色器时,我们将更深入地了解uniforms;现在你只需要知道,通过这种方式,你可以直接更新后处理 passes 和着色器的配置,并直接看到结果。

使用 THREE.BloomPass 向场景添加泛光效果

你在左上角看到的效果称为泛光效果。当应用泛光效果时,场景的亮区域会更加突出,并且“渗透”到暗区域。创建THREE.BloomPass的代码如下所示:

var effectCopy = new THREE.ShaderPass(THREE.CopyShader);
effectCopy.renderToScreen = true;
...
var bloomPass = new THREE.BloomPass(3, 25, 5, 256);
var composer3 = new THREE.EffectComposer(webGLRenderer);
composer3.addPass(renderScene);
composer3.addPass(bloomPass);
composer3.addPass(effectCopy);

如果你将这个与我们用THREE.FilmPass使用的THREE.EffectComposer进行比较,你会注意到我们添加了一个额外的 pass,effectCopy。这一步,我们也用于正常的输出,不会添加任何特殊效果,只是将最后一个 pass 的输出复制到屏幕上。我们需要添加这一步,因为THREE.BloomPass不能直接渲染到屏幕上。

下表列出了你可以在THREE.BloomPass上设置的属性:

属性 描述
Strength 这是泛光效果的强度。数值越高,亮区域就会更亮,而且会“渗透”到暗区域。
kernelSize 这个属性控制泛光效果的偏移量。
sigma 使用sigma属性,你可以控制泛光效果的锐度。数值越高,泛光效果看起来就越模糊。
分辨率 分辨率 属性定义了绽放效果的创建精度。如果设置得太低,结果会显得有点方块。

更好地理解这些属性的方法就是使用之前提到的例子02-post-processing-simple进行实验。以下截图显示了具有高内核和 sigma 大小以及低强度的绽放效果:

使用 THREE.BloomPass 为场景添加绽放效果

我们将要看的最后一个简单效果是 THREE.DotScreenPass

将场景输出为一组点

使用 THREE.DotScreenPass 与使用 THREE.BloomPass 非常相似。我们刚刚看到了 THREE.BloomPass 的效果。现在让我们看看 THREE.DotScreenPass 的代码:

var dotScreenPass = new THREE.DotScreenPass();
var composer1 = new THREE.EffectComposer(webGLRenderer);
composer1.addPass(renderScene);
composer1.addPass(dotScreenPass);
composer1.addPass(effectCopy);

通过这种效果,我们再次必须添加 effectCopy 将结果输出到屏幕。THREE.DotScreenPass 也可以通过一些属性进行配置,如下所示:

属性 描述
中心 通过 中心 属性,你可以微调点的偏移方式。
角度 点是以一定的方式对齐的。通过 角度 属性,你可以改变这种对齐方式。
缩放 通过这个,我们可以设置要使用的点的大小。缩放越低,点就越大。

对其他着色器适用于这个着色器。通过实验,更容易找到正确的设置。

将场景输出为一组点

在同一屏幕上显示多个渲染器的输出

本节不涉及如何使用后期处理效果的细节,而是解释如何在同一屏幕上获取所有四个 THREE.EffectComposer 实例的输出。首先,让我们看看用于此示例的渲染循环:

function render() {
  stats.update();

  var delta = clock.getDelta();
  orbitControls.update(delta);

  sphere.rotation.y += 0.002;

  requestAnimationFrame(render);

  webGLRenderer.autoClear = false;
  webGLRenderer.clear();

  webGLRenderer.setViewport(0, 0, 2 * halfWidth, 2 * halfHeight);
  composer.render(delta);

  webGLRenderer.setViewport(0, 0, halfWidth, halfHeight);
  composer1.render(delta);

  webGLRenderer.setViewport(halfWidth, 0, halfWidth, halfHeight);
  composer2.render(delta);

  webGLRenderer.setViewport(0, halfHeight, halfWidth, halfHeight);
  composer3.render(delta);

  webGLRenderer.setViewport(halfWidth, halfHeight, halfWidth, halfHeight);
  composer4.render(delta);
}

这里要注意的第一件事是,我们将 webGLRenderer.autoClear 属性设置为 false,然后显式调用 clear() 函数。如果我们不在每次在 composer 上调用 render() 函数时这样做,之前渲染的场景将被清除。通过这种方法,我们只在渲染循环的开始清除一切。

为了避免所有的 composer 在同一空间渲染,我们将webGLRenderer的视口设置为屏幕的不同部分。这个函数接受四个参数:xy宽度高度。正如你在代码示例中看到的,我们使用这个函数将屏幕分成四个区域,并让 composer 分别渲染到它们的个别区域。请注意,如果需要,你也可以在多个场景、相机和WebGLRenderer上使用这种方法。

在本节开始的表格中,我们还提到了 THREE.GlitchPass。使用这个渲染通道,你可以为你的场景添加一种电子故障效果。这种效果和你之前看到的其他效果一样容易使用。要使用它,首先在你的 HTML 页面中包含以下两个文件:

<script type="text/javascript" src="../libs/postprocessing/GlitchPass.js"></script>
<script type="text/javascript" src="../libs/postprocessing/DigitalGlitch.js"></script>

然后,创建 THREE.GlitchPass 对象,如下所示:

var effectGlitch = new THREE.GlitchPass(64);
effectGlitch.renderToScreen = true;

结果是一个场景,其中结果被正常渲染,只是在随机间隔发生故障,如下截图所示:

在同一屏幕上显示多个渲染器的输出

到目前为止,我们只链接了一些简单的通道。在下一个例子中,我们将配置一个更复杂的 THREE.EffectComposer 并使用蒙版将效果应用到屏幕的一部分。

使用蒙版创建高级 EffectComposer 流

在之前的例子中,我们将后期处理通道应用到了整个屏幕上。然而,Three.js 也有能力只将通道应用到特定区域。在本节中,我们将执行以下步骤:

  1. 创建一个用作背景图像的场景。

  2. 创建一个看起来像地球的球体的场景。

  3. 创建一个看起来像火星的球体的场景。

  4. 创建 EffectComposer,将这三个场景渲染成一个单一的图像。

  5. colorify 效果应用到渲染为火星的球体上。

  6. 对渲染为地球的球体应用棕褐色效果。

这可能听起来很复杂,但实际上实现起来非常容易。首先,让我们来看看我们在03-post-processing-masks.html示例中的目标结果。以下截图显示了这些步骤的结果:

使用蒙版的高级 EffectComposer 流程

首先,我们需要做的是设置我们将渲染的各种场景,如下所示:

var sceneEarth = new THREE.Scene();
var sceneMars = new THREE.Scene();
var sceneBG = new THREE.Scene();

要创建地球和火星球体,我们只需使用正确的材质和纹理创建球体,并将它们添加到各自的场景中,如下面的代码所示:

var sphere = createEarthMesh(new THREE.SphereGeometry(10, 40, 40));
sphere.position.x = -10;
var sphere2 = createMarshMesh(new THREE.SphereGeometry(5, 40, 40));
sphere2.position.x = 10;
sceneEarth.add(sphere);
sceneMars.add(sphere2);

我们还需要像对待普通场景一样向场景中添加一些灯光,但我们不会在这里展示(有关更多详细信息,请参见第三章,“Three.js 中可用的不同光源”,)。唯一需要记住的是,灯光不能添加到不同的场景,因此您需要为两个场景创建单独的灯光。这就是我们需要为这两个场景做的所有设置。

对于背景图像,我们创建THREE.OrthoGraphicCamera。请记住,从第二章,“Three.js 场景的基本组件”中,正交投影中对象的大小不取决于距离,因此这也是创建固定背景的好方法。以下是我们创建THREE.OrthoGraphicCamera的方法:

var cameraBG = new THREE.OrthographicCamera(-window.innerWidth, window.innerWidth, window.innerHeight, -window.innerHeight, -10000, 10000);
cameraBG.position.z = 50;

var materialColor = new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture("../assets/textures/starry-deep-outer-space-galaxy.jpg"), depthTest: false });
var bgPlane = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), materialColor);
bgPlane.position.z = -100;
bgPlane.scale.set(window.innerWidth * 2, window.innerHeight * 2, 1);
sceneBG.add(bgPlane);

我们不会对这部分详细说明,但我们必须采取一些步骤来创建背景图像。首先,我们从背景图像创建材质,并将此材质应用于简单的平面。接下来,我们将此平面添加到场景中,并将其缩放以完全填满整个屏幕。因此,当我们使用这个相机渲染这个场景时,我们的背景图像会被拉伸到屏幕的宽度。

现在我们有了三个场景,我们可以开始设置我们的通道和THREE.EffectComposer。让我们首先看一下完整的通道链,之后我们再看看各个通道:

var composer = new THREE.EffectComposer(webGLRenderer);
composer.renderTarget1.stencilBuffer = true;
composer.renderTarget2.stencilBuffer = true;

composer.addPass(bgPass);
composer.addPass(renderPass);
composer.addPass(renderPass2);

composer.addPass(marsMask);
composer.addPass(effectColorify1);
composer.addPass(clearMask);

composer.addPass(earthMask);
composer.addPass(effectSepia);
composer.addPass(clearMask);

composer.addPass(effectCopy);

要使用蒙版,我们需要以不同的方式创建THREE.EffectComposer。在这种情况下,我们需要创建一个新的THREE.WebGLRenderTarget,并将内部使用的渲染目标的stencilBuffer属性设置为true。模板缓冲区是一种特殊类型的缓冲区,用于限制渲染区域。因此,通过启用模板缓冲区,我们可以使用我们的蒙版。首先,让我们来看一下添加的前三个通道。这三个通道分别渲染背景、地球场景和火星场景,如下所示:

var bgPass = new THREE.RenderPass(sceneBG, cameraBG);
var renderPass = new THREE.RenderPass(sceneEarth, camera);
renderPass.clear = false;
var renderPass2 = new THREE.RenderPass(sceneMars, camera);
renderPass2.clear = false;

这里没有什么新的,除了我们将两个通道的clear属性设置为false。如果我们不这样做,我们只会看到renderPass2的输出,因为它会在开始渲染之前清除一切。如果你回顾一下THREE.EffectComposer的代码,接下来的三个通道是marsMaskeffectColorifyclearMask。首先,我们来看一下这三个通道是如何定义的:

var marsMask = new THREE.MaskPass(sceneMars, camera );
var clearMask = new THREE.ClearMaskPass();
var effectColorify = new THREE.ShaderPass(THREE.ColorifyShader );
effectColorify.uniforms['color'].value.setRGB(0.5, 0.5, 1);

这三个通道中的第一个是THREE.MaskPass。创建THREE.MaskPass时,您需要像为THREE.RenderPass一样传入一个场景和一个相机。THREE.MaskPass将在内部渲染此场景,但不会在屏幕上显示,而是使用此信息创建蒙版。当THREE.MaskPass添加到THREE.EffectComposer时,所有后续通道将仅应用于THREE.MaskPass定义的蒙版,直到遇到THREE.ClearMaskPass。在这个例子中,这意味着effectColorify通道,它添加了蓝色的发光效果,仅应用于sceneMars中渲染的对象。

我们使用相同的方法在地球对象上应用了一个棕褐色滤镜。我们首先基于地球场景创建一个蒙版,并在THREE.EffectComposer中使用这个蒙版。在THREE.MaskPass之后,我们添加我们想要应用的效果(在这种情况下是effectSepia),一旦完成,我们添加THREE.ClearMaskPass来移除蒙版。对于这个特定的THREE.EffectComposer的最后一步是我们已经看到的。我们需要将最终结果复制到屏幕上,我们再次使用effectCopy通道来实现。

在使用THREE.MaskPass时还有一个有趣的额外属性,那就是inverse属性。如果将此属性设置为true,则蒙版将被反转。换句话说,效果将应用于除传入THREE.MaskPass的场景之外的所有内容。这在下面的截图中显示出来:

使用蒙版的高级 EffectComposer 流程

到目前为止,我们已经使用了 Three.js 提供的标准通道来实现我们的效果。Three.js 还提供了THREE.ShaderPass,可以用于自定义效果,并带有大量可以使用和实验的着色器。

使用 THREE.ShaderPass 进行自定义效果

使用THREE.ShaderPass,我们可以通过传入自定义着色器为我们的场景应用大量额外的效果。这一部分分为三个部分。首先,我们将看一下以下一组简单着色器:

名称 描述
THREE.MirrorShader 这会为屏幕的一部分创建一个镜像效果。
THREE.HueSaturationShader 这允许你改变颜色的色调饱和度
THREE.VignetteShader 这应用了一个晕影效果。这个效果在图像中心周围显示出暗色边框。
THREE.ColorCorrectionShader 使用这个着色器,你可以改变颜色分布。
THREE.RGBShiftShader 这个着色器分离了颜色的红色、绿色和蓝色分量。
THREE.BrightnessContrastShader 这改变了图像的亮度和对比度。
THREE.ColorifyShader 这将在屏幕上应用颜色叠加。
THREE.SepiaShader 这在屏幕上创建了一个棕褐色的效果。
THREE.KaleidoShader 这为场景添加了一个万花筒效果,围绕场景中心提供了径向反射。
THREE.LuminosityShader 这提供了一个亮度效果,显示了场景的亮度。
THREE.TechnicolorShader 这模拟了旧电影中可以看到的双色技术色彩效果。

接下来,我们将看一些提供一些模糊相关效果的着色器:

名称 描述
THREE.HorizontalBlurShaderTHREE.VerticalBlurShader 这些将模糊效果应用到整个场景。
THREE.HorizontalTiltShiftShaderTHREE.VerticalTiltShiftShader 这些重新创建了移轴效果。使用移轴效果,可以确保只有图像的一部分是清晰的,从而创建看起来像微缩的场景。
THREE.TriangleBlurShader 这使用基于三角形的方法应用了模糊效果。

最后,我们将看一些提供高级效果的着色器:

名称 描述
THREE.BleachBypassShader 这会创建一个漂白副本效果。使用这个效果,图像上会应用一个类似银色的叠加。
THREE.EdgeShader 这个着色器可以用来检测图像中的锐利边缘并突出显示它们。
THREE.FXAAShader 这个着色器在后期处理阶段应用了抗锯齿效果。如果在渲染过程中应用抗锯齿效果太昂贵,可以使用这个。
THREE.FocusShader 这是一个简单的着色器,可以使中心区域清晰渲染,边缘模糊。

我们不会详细介绍所有的着色器,因为如果您了解了一个着色器的工作原理,您基本上就知道了其他着色器的工作原理。在接下来的章节中,我们将重点介绍一些有趣的着色器。您可以使用每个章节提供的交互式示例来尝试其他着色器。

提示

Three.js 还提供了两种高级的后期处理效果,允许您在场景中应用bokeh效果。Bokeh 效果可以使场景的一部分产生模糊效果,同时使主要主题非常清晰。Three.js 提供了THREE.BrokerPass,您可以使用它来实现这一点,或者使用THREE.BokehShader2THREE.DOFMipMapShader,您可以与THREE.ShaderPass一起使用。这些着色器的示例可以在 Three.js 网站上找到,网址为threejs.org/examples/webgl_postprocessing_dof2.htmlthreejs.org/examples/webgl_postprocessing_dof.html

我们先从一些简单的着色器开始。

简单着色器

为了尝试基本的着色器,我们创建了一个示例,您可以在其中玩耍着色器,并直接在场景中看到效果。您可以在04-shaderpass-simple.html中找到这个示例。以下截图显示了这个示例:

简单着色器

通过右上角的菜单,您可以选择要应用的特定着色器,并通过各种下拉菜单设置所选着色器的属性。例如,下面的截图显示了RGBShiftShader的效果:

简单着色器

当您改变着色器的属性之一时,结果会直接更新。对于这个例子,我们直接在着色器上设置了改变的值。例如,当RGBShiftShader的值发生变化时,我们会像这样更新着色器:

this.changeRGBShifter = function() {
  rgbShift.uniforms.amount.value = controls.rgbAmount;
  rgbShift.uniforms.angle.value = controls.angle;
}

让我们来看看其他一些着色器。以下图像显示了VignetteShader的结果:

简单着色器

MirrorShader有以下效果:

简单着色器

通过后期处理,我们还可以应用极端的效果。THREE.KaleidoShader就是一个很好的例子。如果您从右上角的菜单中选择这个着色器,您会看到以下效果:

简单着色器

简单着色器就介绍到这里。正如您所看到的,它们非常多才多艺,可以创造出非常有趣的效果。在这个例子中,我们每次应用了一个着色器,但您可以向THREE.EffectComposer添加尽可能多的THREE.ShaderPass步骤。

模糊着色器

在这一部分,我们不会深入代码;我们只会展示各种模糊着色器的结果。您可以使用05-shaderpass-blur.html示例来进行实验。以下场景使用HorizontalBlurShaderVerticalBlurShader进行了模糊处理,您将在接下来的段落中了解到这两种着色器:

模糊着色器

前面的图像显示了THREE.HorizontalBlurShaderTHREE.VerticalBlurShader。您可以看到效果是一个模糊的场景。除了这两种模糊效果,Three.js 还提供了另一个着色器来模糊图像,即THREE.TriangleShader,如下所示。例如,您可以使用这个着色器来描绘运动模糊,就像下面的截图所示:

模糊着色器

最后一个类似模糊的效果是由THREE.HorizontalTiltShiftShaderTHREE.VerticalTiltShiftShader提供的。这个着色器不会使整个场景模糊,而只会模糊一个小区域。这提供了一种称为tilt shift的效果。这经常用于从普通照片中创建微缩场景。以下图像显示了这种效果:

模糊着色器

高级着色器

对于高级着色器,我们将做与之前的模糊着色器相同的事情。我们只会展示着色器的输出。有关如何配置它们的详细信息,请查看06-shaderpass-advanced.html示例。以下截图显示了这个示例:

高级着色器

前面的例子展示了THREE.EdgeShader。使用这个着色器,您可以检测场景中物体的边缘。

下一个着色器是THREE.FocusShader。这个着色器只在屏幕中心呈现焦点,如下截图所示:

高级着色器

到目前为止,我们只使用了 Three.js 提供的着色器。但是,自己创建着色器也非常容易。

创建自定义后期处理着色器

在本节中,您将学习如何创建一个自定义着色器,可以在后期处理中使用。我们将创建两种不同的着色器。第一个将把当前图像转换为灰度图像,第二个将通过减少可用颜色的数量将图像转换为 8 位图像。请注意,创建顶点和片段着色器是一个非常广泛的主题。在本节中,我们只是触及了这些着色器可以做什么以及它们是如何工作的表面。有关更深入的信息,您可以在www.khronos.org/webgl/找到 WebGL 规范。一个充满示例的额外好资源是 Shadertoy,网址为www.shadertoy.com/

自定义灰度着色器

要为 Three.js(以及其他 WebGL 库)创建自定义着色器,您需要实现两个组件:顶点着色器和片段着色器。顶点着色器可用于更改单个顶点的位置,片段着色器用于确定单个像素的颜色。对于后期处理着色器,我们只需要实现片段着色器,并且可以保留 Three.js 提供的默认顶点着色器。在查看代码之前要强调的一个重要点是,GPU 通常支持多个着色器管线。这意味着在顶点着色器步骤中,多个着色器可以并行运行,这也适用于片段着色器步骤。

让我们首先看一下应用灰度效果到我们的图像的着色器的完整源代码(custom-shader.js):

THREE.CustomGrayScaleShader = {

  uniforms: {

    "tDiffuse": { type: "t", value: null },
    "rPower":  { type: "f", value: 0.2126 },
    "gPower":  { type: "f", value: 0.7152 },
    "bPower":  { type: "f", value: 0.0722 }

  },

  vertexShader: [
    "varying vec2 vUv;",
    "void main() {",
      "vUv = uv;",
      "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
    "}"
  ].join("\n"),

  fragmentShader: [

    "uniform float rPower;",
    "uniform float gPower;",
    "uniform float bPower;",
    "uniform sampler2D tDiffuse;",

    "varying vec2 vUv;",

    "void main() {",
      "vec4 texel = texture2D( tDiffuse, vUv );",
      "float gray = texel.r*rPower + texel.g*gPower+ texel.b*bPower;",
      "gl_FragColor = vec4( vec3(gray), texel.w );",
    "}"
  ].join("\n")
};

从代码中可以看出,这不是 JavaScript。当您编写着色器时,您会用OpenGL 着色语言GLSL)编写它们,它看起来很像 C 编程语言。有关 GLSL 的更多信息,请访问www.khronos.org/opengles/sdk/docs/manglsl/

让我们首先看一下这个顶点着色器:

"varying vec2 vUv;","void main() {",
  "vUv = uv;",
  "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
  "}"

对于后期处理,这个着色器实际上不需要做任何事情。您在上面看到的代码是 Three.js 实现顶点着色器的标准方式。它使用projectionMatrix,这是从相机的投影,以及modelViewMatrix,它将对象的位置映射到世界位置,来确定在屏幕上渲染对象的位置。

对于后期处理,这段代码中唯一有趣的事情是uv值,它指示从纹理中读取的 texel,通过"varying vec2 vUv"变量传递到片段着色器。我们将使用vUV值在片段着色器中获取正确的像素进行处理。让我们看看片段着色器并了解代码在做什么。我们从以下变量声明开始:

"uniform float rPower;",
"uniform float gPower;",
"uniform float bPower;",
"uniform sampler2D tDiffuse;",

"varying vec2 vUv;",

在这里,我们看到uniforms属性的四个实例。uniforms属性的实例具有从 JavaScript 传递到着色器的值,并且对于处理的每个片段都是相同的。在这种情况下,我们传递了三个浮点数,由f类型标识(用于确定要包含在最终灰度图像中的颜色的比例),以及一个纹理(tDiffuse),由t类型标识。此纹理包含来自THREE.EffectComposer的上一次传递的图像。Three.js 确保它正确地传递给此着色器,我们可以从 JavaScript 自己设置uniforms属性的其他实例。在我们可以从 JavaScript 使用这些 uniforms 之前,我们必须定义此着色器可用的uniforms属性。这是在着色器文件的顶部完成的:

uniforms: {

  "tDiffuse": { type: "t", value: null },
  "rPower":  { type: "f", value: 0.2126 },
  "gPower":  { type: "f", value: 0.7152 },
  "bPower":  { type: "f", value: 0.0722 }

},

此时,我们可以从 Three.js 接收配置参数,并已经接收到我们想要修改的图像。让我们来看一下将每个像素转换为灰色像素的代码:

"void main() {",
  "vec4 texel = texture2D( tDiffuse, vUv );",
  "float gray = texel.r*rPower + texel.g*gPower + texel.b*bPower;",
  "gl_FragColor = vec4( vec3(gray), texel.w );"

这里发生的是,我们从传入的纹理中获取正确的像素。我们通过使用texture2D函数来实现这一点,其中我们传入我们当前的图像(tDiffuse)和我们想要分析的像素(vUv)的位置。结果是一个包含颜色和不透明度(texel.w)的纹素(纹理中的像素)。

接下来,我们使用此texelrgb属性来计算灰度值。此灰度值设置为gl_FragColor变量,最终显示在屏幕上。有了这个,我们就有了自己的自定义着色器。使用此着色器就像使用其他着色器一样。首先,我们只需要设置THREE.EffectComposer

var renderPass = new THREE.RenderPass(scene, camera);

var effectCopy = new THREE.ShaderPass(THREE.CopyShader);
effectCopy.renderToScreen = true;

var shaderPass = new THREE.ShaderPass(THREE.CustomGrayScaleShader);

var composer = new THREE.EffectComposer(webGLRenderer);
composer.addPass(renderPass);
composer.addPass(shaderPass);
composer.addPass(effectCopy);

在渲染循环中调用composer.render(delta)。如果我们想在运行时更改此着色器的属性,我们只需更新我们定义的uniforms属性:

shaderPass.enabled = controls.grayScale;
shaderPass.uniforms.rPower.value = controls.rPower;
shaderPass.uniforms.gPower.value = controls.gPower;
shaderPass.uniforms.bPower.value = controls.bPower;

结果可以在07-shaderpass-custom.html中看到。以下屏幕截图显示了此示例:

自定义灰度着色器

让我们创建另一个自定义着色器。这次,我们将把 24 位输出减少到较低的位数。

创建自定义位着色器

通常,颜色表示为 24 位值,给我们大约 1600 万种不同的颜色。在计算机的早期,这是不可能的,颜色通常表示为 8 位或 16 位颜色。使用此着色器,我们将自动将我们的 24 位输出转换为 8 位的颜色深度(或任何您想要的)。

由于它与我们之前的示例没有变化,我们将跳过顶点着色器,直接列出uniforms属性的实例:

uniforms: {

  "tDiffuse": { type: "t", value: null },
  "bitSize":  { type: "i", value: 4 }

}

以下是片段着色器本身:

fragmentShader: [

  "uniform int bitSize;",

  "uniform sampler2D tDiffuse;",

  "varying vec2 vUv;",

  "void main() {",

    "vec4 texel = texture2D( tDiffuse, vUv );",
    "float n = pow(float(bitSize),2.0);",
    "float newR = floor(texel.r*n)/n;",
    "float newG = floor(texel.g*n)/n;",
    "float newB = floor(texel.b*n)/n;",

    "gl_FragColor = vec4(newR, newG, newB, texel.w );",

  "}"

].join("\n")

我们定义了两个uniforms属性的实例,用于配置此着色器。第一个是 Three.js 用于传递当前屏幕的实例,第二个是我们自己定义的整数(type: "i"),用作我们希望以颜色深度渲染结果的实例。代码本身非常简单:

  • 我们首先从纹理和基于传入的vUv像素位置的tDiffuse中获取texel

  • 通过计算bitSize属性的 2 的bitSize次幂(pow(float(bitSize),2.0))来计算我们可以拥有的颜色数量。

  • 接下来,我们通过将值乘以n,四舍五入,(floor(texel.r*n)),然后再除以n,来计算texel的颜色的新值。

  • 结果设置为gl_FragColor(红色、绿色和蓝色值以及不透明度),并显示在屏幕上。

您可以在与我们之前的自定义着色器相同的示例中查看此自定义着色器的结果,即07-shaderpass-custom.html。以下屏幕截图显示了此示例:

创建自定义位着色器

这就是关于后期处理的章节。

总结

在本章中,我们讨论了许多不同的后期处理选项。正如你所看到的,创建THREE.EffectComposer并将通道链接在一起实际上非常容易。你只需要记住一些事情。并非所有的通道都会输出到屏幕上。如果你想要输出到屏幕,你可以始终使用THREE.ShaderPassTHREE.CopyShader。向 composer 添加通道的顺序很重要。效果是按照这个顺序应用的。如果你想要重用来自特定THREE.EffectComposer实例的结果,你可以使用THREE.TexturePass。当你的THREE.EffectComposer中有多个THREE.RenderPass时,确保将clear属性设置为false。如果不这样做,你只会看到最后一个THREE.RenderPass步骤的输出。如果你只想对特定对象应用效果,你可以使用THREE.MaskPass。当你完成遮罩后,用THREE.ClearMaskPass清除遮罩。除了 Three.js 提供的标准通道之外,还有大量的标准着色器可用。你可以将这些与THREE.ShaderPass一起使用。使用 Three.js 的标准方法非常容易创建用于后期处理的自定义着色器。你只需要创建一个片段着色器。

到目前为止,我们基本上涵盖了关于 Three.js 的所有知识。在下一章,也就是最后一章,我们将看一看一个名为Physijs的库,你可以用它来扩展 Three.js 的物理功能,并应用碰撞、重力和约束。

第十二章:将物理和声音添加到您的场景

在这最后一章中,我们将看看 Physijs,这是另一个您可以用来扩展 Three.js 基本功能的库。Physijs 是一个允许您在 3D 场景中引入物理的库。通过物理,我们指的是您的对象受到重力的影响,它们可以相互碰撞,可以通过施加冲量移动,并且可以通过铰链和滑块约束其运动。这个库内部使用另一个著名的物理引擎ammo.js。除了物理,我们还将看看 Three.js 如何帮助您向场景添加空间声音。

在本章中,我们将讨论以下主题:

  • 创建一个 Physijs 场景,其中您的对象受到重力的影响,并且可以相互碰撞。

  • 展示如何改变场景中对象的摩擦和恢复(弹性)

  • 解释 Physijs 支持的各种形状以及如何使用它们

  • 展示如何通过组合简单形状来创建复合形状

  • 展示高度场如何允许您模拟复杂的形状

  • 通过应用点、铰链、滑块和锥扭转以及“自由度”约束来限制对象的移动

  • 向场景添加声音源,其声音音量和方向基于它们与摄像机的距离。

我们要做的第一件事是创建一个可以与 Physijs 一起使用的 Three.js 场景。我们将在我们的第一个示例中这样做。

创建一个基本的 Three.js 场景

为 Physijs 设置一个 Three.js 场景非常简单,只需要几个步骤。我们需要做的第一件事是包含正确的 JavaScript 文件,您可以从 GitHub 存储库chandlerprall.github.io/Physijs/获取。像这样将 Physijs 库添加到您的 HTML 页面中:

<script type="text/javascript" src="../libs/physi.js"></script>

模拟场景需要相当多的处理器。如果我们在渲染线程上运行所有模拟计算(因为 JavaScript 的本质是单线程),它将严重影响场景的帧速率。为了补偿这一点,Physijs 在后台线程中进行计算。这个后台线程是通过大多数现代浏览器实现的“web workers”规范提供的。使用这个规范,您可以在单独的线程中运行 CPU 密集型任务,从而不影响渲染。有关 web workers 的更多信息可以在www.w3.org/TR/workers/找到。

对于 Physijs,这意味着我们必须配置包含这个工作任务的 JavaScript 文件,并告诉 Physijs 它在哪里可以找到需要模拟我们场景的 ammo.js 文件。我们需要包含 ammo.js 文件的原因是,Physijs 是一个围绕 ammo.js 的包装器,使其易于使用。Ammo.js(您可以在github.com/kripken/ammo.js/找到)是实现物理引擎的库;Physijs 只是为这个物理库提供了一个易于使用的接口。由于 Physijs 只是一个包装器,我们也可以将其他物理引擎与 Physijs 一起使用。在 Physijs 存储库上,您还可以找到一个使用不同物理引擎 Cannon.js 的分支。

要配置 Physijs,我们必须设置以下两个属性:

Physijs.scripts.worker = '../libs/physijs_worker.js';
Physijs.scripts.ammo = '../libs/ammo.js';

第一个属性指向我们要执行的工作任务,第二个属性指向内部使用的 ammo.js 库。我们需要执行的下一步是创建一个场景。Physijs 提供了一个围绕 Three.js 普通场景的包装器,因此在您的代码中,您可以这样做来创建一个场景:

var scene = new Physijs.Scene();
scene.setGravity(new THREE.Vector3(0, -10, 0));

这将创建一个新的场景,应用了物理,并设置了重力。在这种情况下,我们将* y *轴上的重力设置为-10。换句话说,物体直下落。您可以为各个轴设置或在运行时更改重力为任何您认为合适的值,场景将相应地做出响应。

在场景中开始模拟物理之前,我们需要添加一些对象。为此,我们可以使用 Three.js 指定对象的常规方式,但我们必须将它们包装在特定的 Physijs 对象中,以便它们可以被 Physijs 库管理,如下面的代码片段所示:

var stoneGeom = new THREE.BoxGeometry(0.6,6,2);
var stone = new Physijs.BoxMesh(stoneGeom, new THREE.MeshPhongMaterial({color: 0xff0000}));
scene.add(stone);

在这个例子中,我们创建一个简单的THREE.BoxGeometry对象。我们不是创建THREE.Mesh,而是创建Physijs.BoxMesh,这告诉 Physijs 在模拟物理和检测碰撞时将几何形状视为盒子。Physijs 提供了许多不同形状的网格供您使用。有关可用形状的更多信息可以在本章后面找到。

现在THREE.BoxMesh已经添加到场景中,我们已经具备了第一个 Physijs 场景的所有要素。剩下的就是告诉 Physijs 模拟物理并更新场景中对象的位置和旋转。我们可以通过在我们刚刚创建的场景上调用 simulate 方法来实现这一点。因此,为此,我们将基本的渲染循环更改为以下内容:

render = function() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
  scene.simulate();
}

通过调用scene.simulate()这一最后一步,我们就完成了 Physijs 场景的基本设置。不过,如果我们运行这个例子,我们不会看到太多。我们只会看到屏幕中央的一个立方体,当场景渲染时开始下落。因此,让我们看一个更复杂的例子,模拟多米诺骨牌倒下的情况。

对于这个例子,我们将创建以下场景:

创建基本的 Three.js 场景

如果您在浏览器中打开01-basic-scene.html示例,您会看到一组多米诺石,当场景加载时开始倒下。第一个会推倒第二个,依此类推。这个场景的完整物理效果由 Physijs 管理。我们启动此动画的唯一操作是推倒第一个多米诺。实际上,创建这个场景非常简单,只需要几个步骤,如下所示:

  1. 定义一个 Physijs 场景。

  2. 定义容纳石头的地面区域。

  3. 放置石头。

  4. 把第一块石头推倒。

让我们跳过这一步,因为我们已经知道如何做,直接进入第二步,定义包含所有石头的沙盒。这个沙盒由几个箱子组成。以下是完成此操作所需的代码:

function createGround() {
  var ground_material = Physijs.createMaterial(new THREE.MeshPhongMaterial({ map: THREE.ImageUtils.loadTexture( '../assets/textures/general/wood-2.jpg' )}),0.9,0.3);

  var ground = new Physijs.BoxMesh(new THREE.BoxGeometry(60, 1, 60), ground_material, 0);

  var borderLeft = new Physijs.BoxMesh(new THREE.BoxGeometry (2, 3, 60), ground_material, 0);
  borderLeft.position.x=-31;
  borderLeft.position.y=2;
  ground.add(borderLeft);

  var borderRight = new Physijs.BoxMesh(new THREE. BoxGeometry (2, 3, 60), ground_material, 0);
  borderRight.position.x=31;
  borderRight.position.y=2;
  ground.add(borderRight);

  var borderBottom = new Physijs.BoxMesh(new THREE. BoxGeometry (64, 3, 2), ground_material, 0);
  borderBottom.position.z=30;
  borderBottom.position.y=2;
  ground.add(borderBottom);

  var borderTop = new Physijs.BoxMesh(new THREE.BoxGeometry (64, 3, 2), ground_material, 0);
  borderTop.position.z=-30;
  borderTop.position.y=2;
  ground.add(borderTop);

  scene.add(ground);
}

这段代码并不复杂。首先,我们创建一个简单的盒子作为地面平面,然后我们添加一些边界以防止物体从这个地面平面上掉下来。我们将这些边界添加到地面对象上,以创建一个复合对象。这是 Physijs 将作为单个对象处理的对象。在这段代码中还有一些其他新内容,我们将在接下来的章节中更深入地解释。第一个是我们创建的ground_material。我们使用Physijs.createMaterial函数来创建这种材料。这个函数包装了一个标准的 Three.js 材料,但允许我们设置材料的摩擦弹性(弹性)属性。关于这一点的更多信息可以在下一节中找到。另一个新方面是我们添加到Physijs.BoxMesh构造函数的最后一个参数。对于我们在本节中创建的所有BoxMesh对象,我们将0作为最后一个参数添加。通过这个参数,我们设置了对象的重量。我们这样做是为了防止地面受到场景中的重力影响,以免它下落。

现在我们有了地面,我们可以放置多米诺骨牌。为此,我们创建简单的Three.BoxGeometry实例,将它们包裹在BoxMesh中,并将它们放置在地面网格的特定位置上,如下所示:

var stoneGeom = new THREE.BoxGeometry(0.6,6,2);
var stone = new Physijs.BoxMesh(stoneGeom, Physijs.createMaterial(new THREE.MeshPhongMaterial(color: scale(Math.random()).hex(),transparent:true, opacity:0.8})));
stone.position.copy(point);
stone.lookAt(scene.position);
stone.__dirtyRotation = true;
stone.position.y=3.5;
scene.add(stone);

我们不展示计算每个多米诺骨牌位置的代码(请参阅示例源代码中的getPoints()函数);这段代码只是展示了多米诺骨牌的位置。您可以在这里看到,我们再次创建了BoxMesh,它包装了THREE.BoxGeometry。为了确保多米诺骨牌正确对齐,我们使用lookAt函数来设置它们正确的旋转。如果我们不这样做,它们将面向同一个方向,不会倒下。我们必须确保在手动更新 Physijs 包装对象的旋转(或位置)之后,告诉 Physijs 发生了变化,以便 Physijs 可以更新场景中所有对象的内部表示。对于旋转,我们可以使用内部的__dirtyRotation属性,对于位置,我们将__dirtyPosition设置为true

现在剩下要做的就是推第一个多米诺骨牌。我们只需将x轴上的旋转设置为 0.2,稍微倾斜它。场景中的重力会完成剩下的工作,完全推倒第一个多米诺骨牌。以下是我们如何推第一个多米诺骨牌:

stones[0].rotation.x=0.2;
stones[0].__dirtyRotation = true;

这完成了第一个示例,其中已经展示了 Physijs 的许多功能。如果您想要调整重力,可以通过右上角的菜单进行更改。当您按下resetScene按钮时,重力的更改将被应用:

创建基本的 Three.js 场景

在下一节中,我们将更仔细地看一下 Physijs 材质属性如何影响对象。

材质属性

让我们从示例的解释开始。当您打开02-material-properties.html示例时,您会看到一个空盒子,与之前的示例有些相似。这个盒子围绕其x轴上下旋转。在右上角的菜单中,您有几个滑块,可以用来改变 Physijs 的一些材质属性。这些属性适用于您可以使用addCubesaddSpheres按钮添加的立方体和球体。当您按下addSpheres按钮时,场景中将添加五个球体,当您按下addCubes按钮时,将添加五个立方体。以下是一个演示摩擦和恢复的示例:

材质属性

这个示例允许您玩弄在创建 Physijs 材质时可以设置的restitution(弹性)和friction属性。例如,如果您将cubeFriction设置为1并添加一些立方体,您会发现,即使地面在移动,立方体几乎不会移动。如果您将cubeFriction设置为0,您会注意到立方体在地面停止水平时立即滑动。以下截图显示了高摩擦力允许立方体抵抗重力:

材质属性

您可以在此示例中设置的另一个属性是restitution属性。restitution属性定义了对象具有的能量在碰撞时有多少被恢复。换句话说,高恢复会创建一个有弹性的对象,低恢复会导致对象在碰撞时立即停止。

提示

当您使用物理引擎时,通常不必担心检测碰撞。引擎会处理这个问题。然而,有时候在两个对象发生碰撞时得到通知是非常有用的。例如,您可能想要创建一个声音效果,或者在创建游戏时扣除一条生命。

使用 Physijs,您可以向 Physijs 网格添加事件侦听器,如下面的代码所示:

mesh.addEventListener( 'collision', function( other_object, relative_velocity, relative_rotation, contact_normal ) {
});

这样,当此网格与 Physijs 处理的其他网格发生碰撞时,您将得到通知。

一个很好的演示方法是使用球体,将恢复设置为1,然后点击addSpheres按钮几次。这将创建一些到处弹跳的球体。

在我们继续下一节之前,让我们先看一下这个示例中使用的一点代码:

sphere = new Physijs.SphereMesh(new THREE.SphereGeometry( 2, 20 ), Physijs.createMaterial(new THREE.MeshPhongMaterial({color: colorSphere, opacity: 0.8, transparent: true}), controls.sphereFriction, controls.sphereRestitution));
box.position.set(Math.random() * 50 -25, 20 + Math.random() * 5, Math.random() * 50 -25);
scene.add( sphere );

这是当我们向场景添加球体时执行的代码。这一次,我们使用了不同的 Physijs 网格:Physijs.SphereMesh。我们创建了THREE.SphereGeometry,而提供的最佳匹配逻辑上是Physijs.SphereMesh(在下一节中会详细介绍)。当我们创建Physijs.SphereMesh时,我们传入我们的几何图形,并使用Physijs.createMaterial来创建一个 Physijs 特定的材质。我们这样做是为了可以为这个对象设置摩擦弹性

到目前为止,我们已经看到了BoxMeshSphereMesh。在下一节中,我们将解释并展示 Physijs 提供的不同类型的网格,你可以用它们来包装你的几何图形。

基本支持的形状

Physijs 提供了一些形状,你可以用它们来包装你的几何图形。在本节中,我们将向你介绍所有可用的 Physijs 网格,并通过一个示例演示这些网格。记住,你只需要用这些网格之一替换THREE.Mesh构造函数即可使用这些网格。

以下表格提供了 Physijs 中可用的网格的概述:

名称 描述
--- ---
Physijs.PlaneMesh 这个网格可以用来创建一个零厚度的平面。你也可以使用BoxMeshTHREE.BoxGeometry以及低高度一起使用。
Physijs.BoxMesh 如果你有类似立方体的几何图形,使用这个网格。例如,这是THREE.BoxGeometry的一个很好的匹配。
Physijs.SphereMesh 对于球形,使用这个几何图形。这个几何图形是THREE.SphereGeometry的一个很好的匹配。
Physijs.CylinderMesh 使用THREE.Cylinder,你可以创建各种类似圆柱体的形状。根据圆柱体的形状,Physijs 提供了多个网格。Physijs.CylinderMesh应该用于具有相同顶部半径和底部半径的普通圆柱体。
Physijs.ConeMesh 如果你将顶部半径指定为0,并使用底部半径的正值,你可以使用THREE.Cylinder来创建一个圆锥。如果你想对这样的对象应用物理效果,Physijs 中最合适的选择是ConeMesh
Physijs.CapsuleMesh 胶囊体就像THREE.Cylinder,但顶部和底部都是圆形的。我们稍后将向你展示如何在 Three.js 中创建一个胶囊体。
Physijs.ConvexMesh hysijs.ConvexMesh是一个粗糙的形状,你可以用它来创建更复杂的对象。它创建一个凸面(就像THREE.ConvexGeometry)来近似复杂对象的形状。
Physijs.ConcaveMesh 虽然ConvexMesh是一个粗糙的形状,ConcaveMesh是你复杂几何图形的更详细的表示。请注意,使用ConcaveMesh会有很高的性能惩罚。通常,最好是要么创建具有自己特定 Physijs 网格的单独几何图形,要么将它们组合在一起(就像我们在前面的示例中所做的那样)。
Physijs.HeightfieldMesh 这个网格是一个非常专业化的网格。使用这个网格,你可以从THREE.PlaneGeometry创建一个高度场。查看03-shapes.html示例以了解这个网格。

我们将快速浏览这些形状,使用03-shapes.html作为参考。我们不会进一步解释Physijs.ConcaveMesh,因为它的使用非常有限。

在我们看示例之前,让我们先快速看一下Physijs.PlaneMesh。这个网格基于THREE.PlaneGeometry创建一个简单的平面,如下所示:

var plane = new Physijs.PlaneMesh(new THREE.PlaneGeometry(5,5,10,10), material);

scene.add( plane );

在这个函数中,您可以看到我们只是传入一个简单的THREE.PlaneGeometry来创建这个网格。如果您将其添加到场景中,您会注意到一些奇怪的事情。您刚刚创建的网格不会对重力产生反应。原因是Physijs.PlaneMesh的固定重量为0,因此它不会对重力产生反应,也不会被其他对象的碰撞移动。除了这个网格,所有其他网格都会对重力和碰撞产生反应,就像您所期望的那样。以下截图显示了一个高度场,您可以在其中放置各种支持的形状:

基本支持的形状

前面的图像显示了03-shapes.html示例。在这个示例中,我们创建了一个随机高度场(稍后会详细介绍),并在右上角有一个菜单,您可以使用它来放置各种形状的对象。如果您尝试这个示例,您会看到不同的形状如何对高度图和与其他对象的碰撞产生不同的响应。

让我们来看看一些这些形状的构造:

new Physijs.SphereMesh(new THREE.SphereGeometry(3,20),mat);
new Physijs.BoxMesh(new THREE.BoxGeometry(4,2,6),mat);
new Physijs.CylinderMesh(new THREE.CylinderGeometry(2,2,6),mat);
new Physijs.ConeMesh(new THREE.CylinderGeometry(0,3,7,20,10),mat);

这里没有什么特别的;我们创建一个几何体,并使用 Physijs 中最匹配的网格来创建我们添加到场景中的对象。然而,如果我们想要使用Physijs.CapsuleMesh呢?Three.js 中没有类似胶囊的几何体,所以我们必须自己创建一个。以下是为此目的编写的代码:

var merged = new THREE.Geometry();
var cyl = new THREE.CylinderGeometry(2, 2, 6);
var top = new THREE.SphereGeometry(2);
var bot = new THREE.SphereGeometry(2);

var matrix = new THREE.Matrix4();
matrix.makeTranslation(0, 3, 0);
top.applyMatrix(matrix);

var matrix = new THREE.Matrix4();
matrix.makeTranslation(0, -3, 0);
bot.applyMatrix(matrix);

// merge to create a capsule
merged.merge(top);
merged.merge(bot);
merged.merge(cyl);

// create a physijs capsule mesh
var capsule = new Physijs.CapsuleMesh(merged, getMaterial());

Physijs.CapsuleMesh看起来像一个圆柱体,但顶部和底部是圆角的。我们可以通过创建一个圆柱体(cyl)和两个球体(topbot),然后使用merge()函数将它们合并在一起来轻松地在 Three.js 中重新创建这个形状。以下截图显示了一些胶囊体沿着高度图滚动:

基本支持的形状

在我们查看高度图之前,让我们来看看您可以添加到这个示例中的最后一个形状,Physijs.ConvexMesh。凸包是包裹几何体所有顶点的最小形状。结果形状将只有小于 180 度的角。您可以将此网格用于复杂的形状,如环面结,如下所示:

var convex = new Physijs.ConvexMesh(new THREE.TorusKnotGeometry(0.5,0.3,64,8,2,3,10), material);

在这种情况下,对于物理模拟和碰撞,将使用环面结的凸包。这是一种非常好的方法,可以应用物理效果并检测复杂对象的碰撞,同时最小化性能影响。

Physijs 中讨论的最后一个网格是Physijs.HeightMap。以下截图显示了使用 Physijs 创建的高度图:

基本支持的形状

使用高度图,您可以非常容易地创建一个包含凸起和浅滩的地形。使用Physijs.Heightmap,我们确保所有对象对这个地形的高度差作出正确的响应。让我们来看看完成这个任务所需的代码:

var date = new Date();
var pn = new Perlin('rnd' + date.getTime());

function createHeightMap(pn) {

  var ground_material = Physijs.createMaterial(
    new THREE.MeshLambertMaterial({
      map: THREE.ImageUtils.loadTexture('../assets/textures/ground/grasslight-big.jpg')
    }),
    0.3, // high friction
    0.8 // low restitution
  );

  var ground_geometry = new THREE.PlaneGeometry(120, 100, 100, 100);
  for (var i = 0; i < ground_geometry.vertices.length; i++) {
    var vertex = ground_geometry.vertices[i];
    var value = pn.noise(vertex.x / 10, vertex.y / 10, 0);
    vertex.z = value * 10;
  }
  ground_geometry.computeFaceNormals();
  ground_geometry.computeVertexNormals();

  var ground = new Physijs.HeightfieldMesh(
    ground_geometry,
    ground_material,
    0, // mass
    100,
    100
  );
  ground.rotation.x = Math.PI / -2;
  ground.rotation.y = 0.4;
  ground.receiveShadow = true;

  return ground;
}

在这段代码片段中,我们采取了几个步骤来创建您在示例中看到的高度图。首先,我们创建了 Physijs 材质和一个简单的PlaneGeometry对象。为了从PlaneGeometry创建一个崎岖的地形,我们遍历这个几何体的每个顶点,并随机设置z属性。为此,我们使用 Perlin 噪声生成器来创建一个凸起地图,就像我们在第十章的使用画布作为凸起地图部分中使用的那样,加载和使用纹理。我们需要调用computeFaceNormalscomputeVertexNormals来确保纹理、光照和阴影被正确渲染。在这一点上,我们有了包含正确高度信息的PlaneGeometry。使用PlaneGeometry,我们可以创建Physijs.HeightFieldMesh。构造函数的最后两个参数取PlaneGeometry的水平和垂直段数,并应与用于构造PlaneGeometry的最后两个属性匹配。最后,我们将HeightFieldMesh旋转到我们想要的位置,并将其添加到场景中。所有其他 Physijs 对象现在将正确地与这个高度图交互。

使用约束来限制对象的移动

到目前为止,我们已经看到了一些基本的物理现象。我们已经看到了各种形状如何对重力、摩擦和恢复力作出反应,以及它们如何影响碰撞。Physijs 还提供了高级构造,允许您限制对象的运动。在 Physijs 中,这些对象被称为约束。以下表格概述了 Physijs 中可用的约束:

约束 描述
PointConstraint 这允许您将一个对象的位置固定到另一个对象的位置。如果一个对象移动,另一个对象也会随之移动,保持它们之间的距离和方向不变。
HingeConstraint HingeConstraint允许您限制物体的运动,就像它是在铰链上一样,比如门。
SliderConstraint 这个约束,正如其名称所示,允许您限制物体在一个单一轴上的运动,比如滑动门。
ConeTwistConstraint 使用这个约束,您可以限制一个对象相对于另一个对象的旋转和运动。这个约束的功能类似于球和插座连接,比如您的手臂在肩膀插座中的移动方式。
DOFConstraint DOFConstraint允许您指定围绕任意三个轴的运动限制,并允许您设置允许的最小和最大角度。这是可用的约束中最通用的一个。

理解这些约束的最简单方法是看到它们在实际中的作用并与它们一起玩耍。为此,我们提供了一个使用所有这些约束的例子,04-physijs-constraints.js。以下截图显示了这个例子:

使用约束来限制物体的移动

基于这个例子,我们将为您介绍这五个约束中的四个。对于DOFConstraint,我们创建了一个单独的例子。我们首先看的是PointConstraint

使用 PointConstraint 来限制两个点之间的移动

如果您打开这个例子,您会看到两个红色的球体。这两个球体使用PointConstraint连接在一起。在左上角的菜单中,您可以移动绿色的滑块。一旦其中一个滑块碰到一个红色的球体,您会看到它们两个以相同的方式移动,并且它们保持相同的距离,同时仍然遵守重量、重力、摩擦和其他物理方面的规则。

在这个例子中,PointConstraint是这样创建的:

function createPointToPoint() {
  var obj1 = new THREE.SphereGeometry(2);
  var obj2 = new THREE.SphereGeometry(2);

  var objectOne = new Physijs.SphereMesh(obj1, Physijs.createMaterial(new THREE.MeshPhongMaterial({color: 0xff4444, transparent: true, opacity:0.7}),0,0));

  objectOne.position.x = -10;
  objectOne.position.y = 2;
  objectOne.position.z = -18;

  scene.add(objectOne);

  var objectTwo = new Physijs.SphereMesh(obj2,Physijs.createMaterial(new THREE.MeshPhongMaterial({color: 0xff4444, transparent: true, opacity:0.7}),0,0));

  objectTwo.position.x = -20;
  objectTwo.position.y = 2;
  objectTwo.position.z = -5;

  scene.add(objectTwo);

  var constraint = new Physijs.PointConstraint(objectOne, objectTwo, objectTwo.position);
  scene.addConstraint(constraint);
}

在这里,您可以看到我们使用了 Physijs 特定的网格(在这种情况下是SphereMesh)来创建对象,并将它们添加到场景中。我们使用Physijs.PointConstraint构造函数来创建约束。这个约束需要三个参数:

  • 前两个参数定义了您想要连接在一起的对象。在这种情况下,我们将两个球体连接在一起。

  • 第三个参数定义了约束绑定的位置。例如,如果您将第一个对象绑定到一个非常大的对象,您可以将这个位置设置为该对象的右侧。通常,如果您只想连接两个对象在一起,一个很好的选择就是将它设置为第二个对象的位置。

如果您不想将一个对象固定到另一个对象,而是固定到场景中的一个静态位置,您可以省略第二个参数。在这种情况下,第一个对象保持与您指定的位置相同的距离,当然也遵守重力和其他物理方面的规则。

一旦约束被创建,我们可以通过使用addConstraint函数将其添加到场景中来启用它。当您开始尝试使用约束时,您可能会遇到一些奇怪的问题。为了使调试更容易,您可以向addConstraint函数传递true。如果这样做,约束点和方向将显示在场景中。这可以帮助您正确获取约束的旋转和位置。

使用 HingeConstraint 创建类似门的约束

HingeConstraint,顾名思义,允许您创建一个行为类似铰链的对象。它围绕特定轴旋转,将运动限制在指定的角度内。在我们的示例中,HingeConstraint显示为场景中心的两个白色弹簧板。这些弹簧板受约束于小的棕色立方体,并可以围绕它们旋转。如果您想要玩弄这些铰链,您可以通过在铰链菜单中勾选enableMotor框来启用它们。这将加速弹簧板到general菜单中指定的速度。负速度将使铰链向下移动,正速度将使其向上移动。以下屏幕截图显示了铰链处于上升位置和下降位置:

使用 HingeConstraint 创建类似门的约束

让我们更仔细地看看我们是如何创建其中一个弹簧板的:

var constraint = new Physijs.HingeConstraint(flipperLeft, flipperLeftPivot, flipperLeftPivot.position, new THREE.Vector3(0,1,0));
scene.addConstraint(constraint);
constraint.setLimits(-2.2, -0.6, 0.1, 0);

此约束接受四个参数。让我们更详细地看看每个参数:

参数 描述
mesh_a 传递到函数中的第一个对象是要约束的对象。在本例中,第一个对象是作为弹簧板的白色立方体。这是受约束的对象在其运动中受到约束的对象。
mesh_b 第二个对象定义了mesh_a约束到哪个对象。在本例中,mesh_a受约束于小的棕色立方体。如果我们移动此网格,mesh_a将跟随它移动,仍然保持HingeConstraint在原位。您会发现所有约束都有这个选项。例如,如果您创建了一个四处移动的汽车,并希望创建一个打开门的约束,您可以使用此选项。如果省略了第二个参数,铰链将受到场景的约束(永远无法移动)。
position 这是应用约束的点。在本例中,它是mesh_a围绕其旋转的铰链点。如果您指定了mesh_b,则此铰链点将随着mesh_b的位置和旋转而移动。
axis 这是铰链应该围绕其旋转的轴。在本例中,我们将铰链水平设置为(0,1,0)。

向场景添加HingeConstraint的方式与我们在PointConstraint中看到的方式相同。您使用addConstraint方法,指定要添加的约束,并可选择添加true以显示约束的确切位置和方向,以进行调试。然而,对于HingeConstraint,我们还需要定义允许的运动范围。我们使用setLimits函数来实现这一点。

此函数接受以下四个参数:

参数 描述
low 运动的最小角度,以弧度表示。
high 运动的最大角度,以弧度表示。
bias_factor 此属性定义约束在位置错误后纠正自身的速率。例如,当铰链被不同对象推出其约束时,它将移动到正确的位置。此值越高,它纠正位置的速度就越快。最好将其保持在0.5以下。
relaxation_factor 这定义了约束改变速度的速率。如果设置为较高的值,当达到运动的最小或最大角度时,对象将会弹跳。

您可以在运行时更改这些属性。如果您使用这些属性添加HingeConstraint,您将看不到太多移动。只有当网格被另一个对象击中或基于重力时,网格才会移动。然而,许多其他约束也可以通过内部电机移动。当您在我们的示例中检查铰链子菜单中的enableMotor框时,您会看到这一点。以下代码用于启用此电机:

constraint.enableAngularMotor( controls.velocity, controls.acceleration );

这将使用提供的加速度将网格(在我们的例子中是弹簧板)加速到指定的速度。如果我们想让弹簧板向另一个方向移动,我们只需指定一个负速度。如果我们没有任何限制,这将导致我们的弹簧板只要电机持续运行就会旋转。要禁用电机,我们只需调用以下代码:

flipperLeftConstraint.disableMotor();

现在网格会根据摩擦、碰撞、重力和其他物理方面减速。

使用 SliderConstraint 限制运动到单一轴

下一个约束是SliderConstraint。使用这个约束,你可以限制物体沿着任意一个轴的运动。在04-constraints.html示例中,绿色的滑块可以从滑块子菜单中控制。以下截图显示了这个例子:

使用 SliderConstraint 限制运动到单一轴

使用SlidersLeft按钮,滑块将移动到左侧(它们的下限),使用SlidersRight按钮,它们将移动到右侧(它们的上限)。从代码中创建这些约束非常容易:

var constraint = new Physijs.SliderConstraint(sliderMesh, new THREE.Vector3(0, 2, 0), new THREE.Vector3(0, 1, 0));

scene.addConstraint(constraint);
constraint.setLimits(-10, 10, 0, 0);
constraint.setRestitution(0.1, 0.1);

从代码中可以看出,这个约束需要三个参数(如果你想将一个对象约束到另一个对象,则需要四个参数)。以下表格解释了这个约束的参数:

参数 描述
mesh_a 传入函数的第一个对象是要约束的对象。在这个例子中,第一个对象是作为滑块的绿色立方体。这是将在其运动中受到约束的对象。
mesh_b 这是第二个对象,定义了mesh_a被约束到哪个对象。这是一个可选参数,在这个例子中被省略了。如果省略,网格将被约束到场景。如果指定了,当这个网格移动或其方向改变时,滑块将随之移动。
position 这是约束应用的点。当你将mesh_a约束到mesh_b时,这一点尤为重要。

| axis | 这是mesh_a将要滑动的轴。请注意,如果指定了mesh_b,这是相对于mesh_b的方向。在当前版本的 Physijs 中,使用线性电机和线性限制时,这个轴似乎有一个奇怪的偏移。如果你想沿着这个方向滑动,以下内容适用于这个版本:

  • x轴:new THREE.Vector3(0,1,0)

  • y轴:new THREE.Vector3(0,0,Math.PI/2)

  • z轴:new THREE.Vector3(Math.PI/2,0,0)

|

在创建约束并使用scene.addConstraint将其添加到场景后,你可以设置constraint.setLimits(-10, 10, 0, 0)来指定这个约束的限制,以确定滑块可以滑动多远。你可以在SliderConstraint上设置以下限制:

参数 描述
linear_lower 这是物体的线性下限
linear_upper 这是物体的线性上限
angular_lower 这是物体的角度下限
angular_higher 这是物体的角度上限

最后,当你击中这些限制时,你可以设置恢复(弹跳)的大小。你可以使用constraint.setRestitution(res_linear, res_angular)来设置,第一个参数设置线性限制时的弹跳量,第二个参数设置角限制时的弹跳量。

现在,完整的约束已经配置好了,我们可以等待碰撞发生,使物体滑动,或者使用电机。对于SlideConstraint,我们有两个选择:我们可以使用角度电机沿着我们指定的轴加速,遵守我们设置的角度限制,或者使用线性电机沿着我们指定的轴加速,遵守我们设置的线性限制。在这个例子中,我们使用了线性电机。要使用角度电机,请看后面在本章中解释的DOFConstraint

使用ConeTwistConstraint创建类似球和插座的约束

使用ConeTwistConstraint,可以创建一个受限于一组角度的约束。我们可以指定从一个对象到另一个对象的xyz轴的最小和最大角度。以下截图显示了ConeTwistConstraint允许您以一定角度围绕参考点移动对象:

使用 ConeTwistConstraint 创建类似球和插座的约束

要理解ConeTwistConstraint最简单的方法是看一下创建它所需的代码。实现这一点所需的代码如下:

var baseMesh = new THREE.SphereGeometry(1);
var armMesh = new THREE.BoxGeometry(2, 12, 3);

var objectOne = new Physijs.BoxMesh(baseMesh,Physijs.createMaterial(new THREE.MeshPhongMaterial({color: 0x4444ff, transparent: true, opacity:0.7}), 0, 0), 0);
objectOne.position.z = 0;
objectOne.position.x = 20;
objectOne.position.y = 15.5;
objectOne.castShadow = true;
scene.add(objectOne);

var objectTwo = new Physijs.SphereMesh(armMesh,Physijs.createMaterial(new THREE.MeshPhongMaterial({color: 0x4444ff, transparent: true, opacity:0.7}), 0, 0), 10);
objectTwo.position.z = 0;
objectTwo.position.x = 20;
objectTwo.position.y = 7.5;
scene.add(objectTwo);
objectTwo.castShadow = true;

var constraint = new Physijs.ConeTwistConstraint(objectOne, objectTwo, objectOne.position);

scene.addConstraint(constraint);

constraint.setLimit(0.5*Math.PI, 0.5*Math.PI, 0.5*Math.PI);
constraint.setMaxMotorImpulse(1);
constraint.setMotorTarget(new THREE.Vector3(0, 0, 0));

在这段 JavaScript 代码中,你可能已经认识到了我们之前讨论过的许多概念。我们首先创建了两个对象,并用约束将它们连接起来:objectOne(一个球体)和objectTwo(一个立方体)。我们将这些对象定位,使得objectTwo悬挂在objectOne下方。现在我们可以创建ConeTwistConstraint。如果你已经看过其他约束的话,那么这个约束所需的参数并不陌生。第一个参数是要约束的对象,第二个参数是第一个对象要被约束到的对象,最后一个参数是约束被构建的位置(在这种情况下,是objectOne围绕其旋转的点)。在将约束添加到场景后,我们可以使用setLimit函数设置其限制。这个函数接受三个弧度值,用来指定每个轴的最大角度。

就像大多数其他约束一样,我们可以使用约束提供的马达来移动objectOne。对于ConeTwistConstraint,我们设置了MaxMotorImpulse(马达可以施加的力量),并设置了马达应该将objectOne移动到的目标角度。在这个例子中,我们将其移动到其直接在球体下方的静止位置。您可以通过设置这个目标值来玩弄这个例子,如下面的截图所示:

使用 ConeTwistConstraint 创建类似球和插座的约束

我们将要看的最后一个约束也是最通用的——DOFConstraint

使用 DOFConstraint 创建详细控制

DOFConstraint,也称为自由度约束,允许您精确控制对象的线性和角运动。我们将通过创建一个示例来展示如何使用这个约束,您可以在这个示例中驾驶一个简单的类似汽车的形状。这个形状由一个作为车身的矩形和四个作为车轮的球体组成。让我们从创建车轮开始:

function createWheel(position) {
  var wheel_material = Physijs.createMaterial(
   new THREE.MeshLambertMaterial({
     color: 0x444444,
     opacity: 0.9,
     transparent: true
    }),
    1.0, // high friction
    0.5 // medium restitution
  );

  var wheel_geometry = new THREE.CylinderGeometry(4, 4, 2, 10);
  var wheel = new Physijs.CylinderMesh(
    wheel_geometry,
    wheel_material,
    100
  );

  wheel.rotation.x = Math.PI / 2;
  wheel.castShadow = true;
  wheel.position = position;
  return wheel;
}

在这段代码中,我们只是创建了一个简单的CylinderGeometryCylinderMesh对象,它们可以作为我们汽车的车轮。以下截图展示了前面代码的结果:

使用 DOFConstraint 创建详细控制

接下来,我们需要创建汽车的车身并将所有东西添加到场景中:

var car = {};
var car_material = Physijs.createMaterial(new THREE.MeshLambertMaterial({
    color: 0xff4444,
    opacity: 0.9,  transparent: true
  }),   0.5, 0.5 
);

var geom = new THREE.BoxGeometry(15, 4, 4);
var body = new Physijs.BoxMesh(geom, car_material, 500);
body.position.set(5, 5, 5);
body.castShadow = true;
scene.add(body);

var fr = createWheel(new THREE.Vector3(0, 4, 10));
var fl = createWheel(new THREE.Vector3(0, 4, 0));
var rr = createWheel(new THREE.Vector3(10, 4, 10));
var rl = createWheel(new THREE.Vector3(10, 4, 0));

scene.add(fr);
scene.add(fl);
scene.add(rr);
scene.add(rl);

到目前为止,我们只是创建了组成汽车的各个部件。为了将所有东西联系在一起,我们将创建约束。每个车轮都将被约束到body上。约束的创建如下:

var frConstraint = new Physijs.DOFConstraint(fr,body, new THREE.Vector3(0,4,8));
scene.addConstraint(frConstraint);
var flConstraint = new Physijs.DOFConstraint (fl,body, new THREE.Vector3(0,4,2));
scene.addConstraint(flConstraint);
var rrConstraint = new Physijs.DOFConstraint (rr,body, new THREE.Vector3(10,4,8));
scene.addConstraint(rrConstraint);
var rlConstraint = new Physijs.DOFConstraint (rl,body, new THREE.Vector3(10,4,2));
scene.addConstraint(rlConstraint);

每个车轮(第一个参数)都有自己的约束,并且它附加到汽车的位置(第二个参数)是用最后一个参数指定的。如果我们按照这个配置运行,我们会看到四个车轮支撑着汽车的车身。为了让汽车移动,我们还需要做两件事:我们需要为车轮设置约束(它们可以沿着哪个轴移动),并且我们需要配置正确的马达。首先,我们为两个前轮设置约束;对于这些前轮,我们希望它们只能沿着z轴旋转,以便它们可以驱动汽车,并且不允许它们沿着其他轴移动。

实现这一点所需的代码如下:

frConstraint.setAngularLowerLimit({ x: 0, y: 0, z: 0 });
frConstraint.setAngularUpperLimit({ x: 0, y: 0, z: 0 });
flConstraint.setAngularLowerLimit({ x: 0, y: 0, z: 0 });
flConstraint.setAngularUpperLimit({ x: 0, y: 0, z: 0 });

乍一看,这可能看起来很奇怪。通过将下限和上限设置为相同的值,我们确保在指定的方向上不能进行旋转。这也意味着车轮不能围绕其z轴旋转。我们之所以这样指定它,是因为当您为特定轴启用马达时,这些限制将被忽略。因此,在这一点上设置z轴上的限制对我们的前轮没有任何影响。

我们将使用后轮来转向,并确保它们不会倒下,我们需要固定x轴。使用以下代码,我们固定x轴(将上限和下限设置为0),固定y轴,以便这些车轮最初已经转向,并且禁用z轴上的任何限制:

rrConstraint.setAngularLowerLimit({ x: 0, y: 0.5, z: 0.1 });
rrConstraint.setAngularUpperLimit({ x: 0, y: 0.5, z: 0 });
rlConstraint.setAngularLowerLimit({ x: 0, y: 0.5, z: 0.1 });
rlConstraint.setAngularUpperLimit({ x: 0, y: 0.5, z: 0 });

如您所见,要禁用限制,我们必须将特定轴的下限设置得比上限高。这将允许围绕该轴的自由旋转。如果我们不为z轴设置这个,这两个车轮将只是被拖着走。在这种情况下,它们将因与地面的摩擦而与其他车轮一起转动。

剩下的就是为前轮设置马达,可以这样做:

flConstraint.configureAngularMotor(2, 0.1, 0, -2, 1500);
frConstraint.conAngularMotor(2, 0.1, 0, -2, 1500);

由于我们可以为三个轴创建一个马达,我们需要指定马达工作的轴:0 是x轴,1 是y轴,2 是z轴。第二个和第三个参数定义了马达的角度限制。在这里,我们再次将下限(0.1)设置得比上限(0)高,以允许自由旋转。第三个参数指定我们要达到的速度,最后一个参数指定这个马达可以施加的力。如果这最后一个参数太小,汽车就不会移动;如果太大,后轮就会离开地面。

使用以下代码启用它们:

flConstraint.enableAngularMotor(2);
frConstraint.enableAngularMotor(2);

如果您打开05-dof-constraint.html示例,您可以玩弄各种约束和马达,并驾驶汽车四处走动。以下截图显示了这个例子:

使用 DOFConstraint 创建详细控制

在下一节中,我们将看看这本书中我们将讨论的最后一个主题,那就是如何将声音添加到您的 Three.js 场景中。

将声源添加到您的场景中

到目前为止,我们已经讨论了很多内容,可以创建美丽的场景、游戏和其他 3D 可视化。然而,我们还没有展示如何将声音添加到您的 Three.js 场景中。在本节中,我们将看看两个 Three.js 对象,它们允许您向场景中添加声音源。这是特别有趣的,因为这些声源会响应摄像机的位置:

  • 声源与摄像机之间的距离决定了声源的音量。

  • 摄像机左侧和右侧的位置决定了左侧扬声器和右侧扬声器的声音音量。

最好的解释方法是看到它的实际效果。在浏览器中打开06-audio.html示例,您会看到三个带有动物图片的立方体。以下截图显示了这个例子:

向您的场景添加声源

这个例子使用了我们在第九章中看到的第一人称控制,动画和移动摄像机,所以您可以使用箭头键与鼠标结合来在场景中移动。您会发现,您越接近特定的立方体,该特定动物的声音就会越大。如果您将摄像机位置放在狗和奶牛之间,您将会从右侧听到奶牛的声音,从左侧听到狗的声音。

提示

在这个例子中,我们使用了一个特定的辅助工具THREE.GridHelper,从 Three.js 中创建了立方体下面的网格:

var helper = new THREE.GridHelper( 500, 10 );
helper.color1.setHex( 0x444444 );
helper.color2.setHex( 0x444444 );
scene.add( helper );

创建网格时,您需要指定网格的大小(在本例中为 500)和单个网格元素的大小(我们在这里使用了 10)。如果您愿意,还可以通过指定color1color2属性来设置水平线的颜色。

只需少量代码即可完成此操作。我们需要做的第一件事是定义THREE.AudioListener并将其添加到THREE.PerspectiveCamera中,如下所示:

var listener1 = new THREE.AudioListener();
camera.add( listener1 );

接下来,我们需要创建THREE.Mesh并将THREE.Audio对象添加到该网格中,如下所示:

var cube = new THREE.BoxGeometry(40, 40, 40);

var material_1 = new THREE.MeshBasicMaterial({
  color: 0xffffff,
  map: THREE.ImageUtils.loadTexture("../assets/textures/animals/cow.png")
});

var mesh1 = new THREE.Mesh(cube, material_1);
mesh1.position.set(0, 20, 100);

var sound1 = new THREE.Audio(listener1);
sound1.load('../assets/audio/cow.ogg');
sound1.setRefDistance(20);
sound1.setLoop(true);
sound1.setRolloffFactor(2);

mesh1.add(sound1);

从这段代码片段中可以看出,我们首先创建了一个标准的THREE.Mesh实例。接下来,我们创建了一个THREE.Audio对象,将其连接到之前创建的THREE.AudioListener对象上。最后,我们将THREE.Audio对象添加到我们创建的网格中,完成了整个过程。

有一些属性可以在THREE.Audio对象上设置以配置其行为:

  • load:这允许我们加载要播放的音频文件。

  • setRefDistance:这确定了从对象到声音减小的距离。

  • setLoop:默认情况下,声音只播放一次。通过将此属性设置为true,声音将循环播放。

  • setRolloffFactor:这确定了音量随着远离声源而减小的速度。

在内部,Three.js 使用 Web Audio API(webaudio.github.io/web-audio-api/)来播放声音并确定正确的音量。并非所有浏览器都支持此规范。目前最好的支持来自 Chrome 和 Firefox。

总结

在本章的最后,我们探讨了如何通过添加物理功能来扩展 Three.js 的基本 3D 功能。为此,我们使用了 Physijs 库,它允许您添加重力、碰撞、约束等等。我们还展示了如何在场景中添加定位声音使用THREE.AudioTHREE.AudioListener对象。通过这些主题,我们已经完成了关于 Three.js 的这本书。在这些章节中,我们涵盖了许多不同的主题,并探索了 Three.js 所提供的几乎所有内容。在前几章中,我们解释了 Three.js 背后的核心概念和思想;之后,我们研究了可用的灯光以及材质如何影响对象的渲染方式。在掌握基础知识之后,我们探索了 Three.js 提供的各种几何图形以及如何组合几何图形来创建新的图形。

在书的第二部分,我们研究了一些更高级的主题。您学会了如何创建粒子系统,如何从外部来源加载模型,以及如何创建动画。最后,在最后几章中,我们研究了您可以在皮肤中使用的高级纹理以及在场景渲染后可以应用的后处理效果。我们以这一章关于物理的书结束,除了解释如何将物理添加到 Three.js 场景中,还展示了围绕 Three.js 的活跃社区项目,您可以使用这些项目为已经很棒的库添加更多功能。

希望您喜欢阅读本书并且和我写作一样喜欢玩弄示例!

posted @ 2024-05-22 12:07  绝不原创的飞龙  阅读(57)  评论(0编辑  收藏  举报