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 提供了一个易于使用的 JavaScript API,围绕 WebGL 的功能,因此您可以在不详细了解 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 提供的更高级的几何体,例如凸面和车削。在本章中,你还将学习如何从二维形状中挤出三维几何体,以及如何通过使用二进制运算组合几何体来创建新的几何体。

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

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

第九章, 动画和移动相机,探讨了你可以使用的各种类型的动画,让你的场景栩栩如生。你将学习如何结合使用 Tween.js 库和 Three.js,以及如何处理基于变形和骨骼的动画模型。

第十章, 加载和使用纹理,在第四章,使用 Three.js 材质,中介绍了材质的基础上进行了扩展。在本章中,我们深入探讨了纹理的细节。本章介绍了可用的各种纹理类型以及如何控制纹理如何应用于你的网格。此外,在本章中,你还将了解到如何直接使用 HTML5 视频和 canvas 元素的输出作为纹理的输入。

第十一章, 自定义着色器和渲染后处理,探讨了如何使用 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

新术语重要词汇将以粗体显示。屏幕上显示的词汇,例如在菜单或对话框中,将以如下方式显示:“您可以通过前往首选项 | 高级并检查在菜单栏中显示开发菜单”来完成此操作。”

注意

警告或重要提示将以这样的框显示。

提示

技巧和窍门将如下所示。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

如要向我们发送一般反馈,请简单地将电子邮件发送至 <feedback@packtpub.com>,并在邮件主题中提及书籍标题。

如果您在某个领域有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南 www.packtpub.com/authors

客户支持

现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

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

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出中的变化。您可以从 www.packtpub.com/sites/default/files/downloads/2215OS_Graphics.pdf 下载此文件。

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中找到一个错误——可能是文本或代码中的错误——如果您能向我们报告这个错误,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进此书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击错误清单提交表单链接,并输入您的错误清单详情来报告它们。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误清单部分。

要查看之前提交的错误清单,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在错误清单部分。

盗版

互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过链接将疑似盗版材料发送至 <copyright@packtpub.com> 与我们联系。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。

问题和建议

如果您对本书的任何方面有问题,请通过 <questions@packtpub.com> 与我们联系,我们将尽力解决问题。

第一章. 使用 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。
Google Chrome 该浏览器自版本 9 开始支持 WebGL。
Safari 安装在 Mac OS X Mountain Lion、Lion 或 Snow Leopard 上的 Safari 版本 5.1 及更高版本支持 WebGL。请确保您已启用 Safari 中的 WebGL。您可以通过转到首选项 | 高级并勾选在菜单栏中显示开发菜单来完成此操作。之后,转到开发 | 启用 WebGL
Opera 自 12.00 版本起,此浏览器已支持 WebGL。您仍然需要通过打开 opera:config 并将 WebGLEnable Hardware Acceleration 的值设置为 1 来启用此功能。之后,重新启动浏览器。
Internet Explorer Internet Explorer 一直是有史以来唯一一个不支持 WebGL 的重要浏览器。从 IE11 开始,微软添加了对 WebGL 的支持。

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

三.js 也可能在移动设备上运行;对 WebGL 的支持和您将获得的表现将有所不同,但两者都在迅速提高:

设备 支持
Android Android 的原生浏览器没有 WebGL 支持,并且通常也缺乏对现代 HTML5 功能的支持。如果您想在 Android 上使用 WebGL,您可以使用最新的 Chrome、Firefox 或 Opera 移动版本。
iOS 在 iOS 8 中,iOS 设备也支持 WebGL。iOS Safari 版本 8 对 WebGL 有很好的支持。
Windows mobile Windows mobile 自 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 库,因此你只需要一个文本编辑器和其中一个支持的浏览器来渲染结果。我想推荐两个 JavaScript 编辑器,我在过去几年里开始专门使用它们:

  • WebStorm:来自 JetBrains 的这款编辑器对编辑 JavaScript 提供了极大的支持。它支持代码补全、自动部署和直接从编辑器中进行 JavaScript 调试。除此之外,WebStorm 对 GitHub(和其他版本控制系统)有极好的支持。你可以从www.jetbrains.com/webstorm/下载试用版。

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

  • Sublime Text 编辑器:Sublime 是一个非常棒的编辑器,它对编辑 JavaScript 提供了非常好的支持。除此之外,它提供了许多非常有用的选择(如多行选择)和编辑选项,一旦你习惯了它们,就能提供一个真正优秀的 JavaScript 编辑环境。Sublime 也可以免费试用,可以从www.sublimetext.com/下载。

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

小贴士

除了这些你可以用来编辑和实验本书源代码的文本编辑器之外,Three.js 目前也提供了一个在线编辑器。

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

我提到过,大多数现代网络浏览器都支持 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,然后点击右侧的下载 ZIP按钮,如下所示:

下载并提取存档

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

测试示例

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

根据您已经安装的软件,设置本地网络服务器非常简单。在这里,我们列出了一些如何进行此操作的示例。根据您系统上已安装的软件,有多种不同的方法可以做到这一点。

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

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

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

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

如果您使用过 Node.js,那么基于 npm 的网络服务器是一个不错的选择

如果您已经使用过 Node.js 做了一些工作,那么您很可能已经安装了 npm。使用 npm,您有两个简单的选项来快速设置一个用于测试的本地网络服务器。第一个选项使用 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,您可以使用。首先,从 code.google.com/p/mongoose/downloads/list 下载您特定平台的二进制文件。如果您使用 Windows,将其复制到包含示例的目录中,然后双击可执行文件以启动一个在启动目录中提供服务的网络浏览器。

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

适用于 Mac 和/或 Windows 的便携式 Mongoose 版本

只需点击一个章节,我们就可以展示和访问该特定章节的所有示例。如果我在这本书中讨论一个示例,我会引用具体的名称和文件夹,这样你就可以直接测试并玩转代码。

在 Firefox 和 Chrome 中禁用安全异常

如果你使用 Chrome 运行示例,有一种方法可以禁用一些安全设置,这样你就可以使用 Chrome 查看示例,而无需 Web 服务器。为此,你必须以以下方式启动 Chrome:

  • 对于 Windows 系统,你可以调用以下命令:

    chrome.exe --disable-web-security
    
    
  • 在 Linux 上,请执行以下操作:

    google-chrome --disable-web-security
    
    
  • 在 Mac OS 上,你可以通过以下方式启动 Chrome 来禁用设置:

    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="img/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对象的参数。

提示

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

在这里,我们使用setClearColor函数将renderer的背景颜色设置为几乎白色(new THREE.Color(0XEEEEEE)),并使用setSize函数告诉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示例,构成 Three.js 场景的基本组件,它展示了旋转和定位的说明。然后我们需要做的就是将plane添加到scene中,就像我们添加axes一样。

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="img/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);
  }

对于立方体,我们更改了rotation属性;对于球体,我们将更改场景中其position属性。我们希望球体以一个漂亮的、平滑的曲线从场景中的一个点到另一个点弹跳。这如图所示:

弹跳球体

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

弹跳球体

在结束本章之前,我想在我们的基本场景中添加一个额外的元素。当与 3D 场景、动画、颜色和属性等工作时,通常需要一些实验来得到正确的颜色或速度。如果能够有一个简单的 GUI,允许你即时更改这些属性,那就太容易了。幸运的是,有这样的工具!

使用 dat.GUI 使实验更容易

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

  • 控制弹跳球体的速度

  • 控制立方体的旋转

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

<script src="img/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.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.AmbientLightTHREE.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) 当你创建一个对象时,你可以给它一个独特的名称。场景对象有一个函数,你可以使用它来直接返回具有特定名称的对象。如果你将递归参数设置为true,Three.js 也会搜索完整的对象树以找到具有指定名称的对象。
remove(object) 如果你有一个场景中对象的引用,你也可以使用此函数将其从场景中删除。
traverse(function) 子属性返回场景中所有子对象的列表。使用遍历函数,我们也可以访问这些子对象。使用遍历,所有子对象将逐个传递给提供的函数。
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 库中,几何体基本上是三维空间中点的集合,也称为顶点,以及连接这些点的多个面。以一个立方体为例:

  • 一个立方体有八个角。每个角都可以定义为xyz坐标。因此,每个立方体在三维空间中有八个点。在 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 渲染的场景中的对象。正如其名所示,我们可以复制几何体,例如,使用它来创建具有不同材料的不同网格。在相同的示例 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.js 所做的不是创建一个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);
}

这段 JavaScript 代码在点击克隆按钮时被调用。在这里,我们克隆了立方体第一个子元素的几何形状。记住,mesh 变量包含两个子元素;它包含两个网格,一个对应于我们指定的每种材质。基于这个克隆的几何形状,我们创建了一个新的网格,命名为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,2,3)。接下来,我们沿着物体的x轴进行平移:translateX(4)。它的位置现在将是(5,2,3)。如果我们想将物体恢复到原始位置,我们这样做:translateX(-4)。在06-mesh-properties.html示例中,有一个名为平移的菜单标签。从那里,你可以尝试这个功能。只需设置xyz的平移值,然后点击平移按钮。你会看到物体根据这三个值移动到新的位置。

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

网格的函数和属性

当你再次点击它时,立方体再次变得可见。有关网格、几何体以及你可以用这些对象做什么的更多信息,请参阅第五章,学习与几何体一起工作,以及第七章,粒子、精灵和点云

不同的用途需要不同的相机

在 Three.js 中有两种不同的摄像机类型:正交摄像机和透视摄像机。在第三章《在 Three.js 中使用不同的光源》中,我们将更详细地探讨如何使用这些摄像机,因此在本章中,我将专注于基础知识。解释这两种摄像机之间差异的最佳方式是通过查看几个示例。

正交摄像机与透视摄像机

在本章的示例中,你可以找到一个名为 07-both-cameras.html 的演示。当你打开这个示例时,你会看到如下内容:

正交摄像机与透视摄像机

这被称为透视视图,是最自然的视图。正如你可以从这张图中看到的,立方体离摄像机越远,渲染得越小。

如果我们将摄像机切换到 Three.js 支持的另一种类型——正交摄像机,你将看到相同场景的以下视图:

正交摄像机与透视摄像机

使用正交摄像机时,所有立方体都以相同的大小渲染;物体与摄像机之间的距离并不重要。这通常用于像 SimCity 4 和旧版本的 Civilization 这样的 2D 游戏。

正交摄像机与透视摄像机

在我们的示例中,我们将最常使用透视摄像机,因为它最接近现实世界。切换摄像机非常简单。以下代码块在点击 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 度。但由于普通计算机屏幕并不能完全填满我们的视野,通常会选择较小的值。对于游戏来说,通常选择 60 到 90 度之间的 FOV。好的默认值:50
aspect 这是我们将要渲染输出的区域水平尺寸和垂直尺寸之间的宽高比。在我们的例子中,因为我们使用整个窗口,所以我们只使用那个比例。宽高比决定了水平视场和垂直视场之间的差异,正如你可以在以下图像中看到。好的默认值: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 着色器程序来模拟这些类型的灯光。有关从头开始模拟 WebGL 中灯光的详细介绍,请参阅developer.mozilla.org/en-US/docs/Web/WebGL/Lighting_in_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 控制菜单,我们将在第十一章(Chapter 11)自定义着色器和渲染后处理中介绍:

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.add(...) 函数,而是使用 gui.addColor(...) 函数。这会在控制菜单中创建一个选项,我们可以直接更改传入变量的颜色。在代码中,你可以看到我们使用了 dat.GUI 的 onChange 功能:gui.addColor(...).onChange(function(e){...})。使用这个函数,我们告诉 dat.GUI 每次颜色改变时调用传入的函数。在这个特定的情况下,我们将 THREE.AmbientLight 的颜色设置为新的值。

使用 THREE.Color 对象

在我们继续到下一个光源之前,这里有一个关于使用 THREE.Color 对象的简要说明。在 Three.js 中,当你构建一个对象时,你可以(通常)指定颜色为十六进制字符串("#0c0c0c")或十六进制值(0x0c0c0c),这是首选的方法,或者通过指定 0 到 1 比例上的单个 RGB 值(0.3, 0.5, 0.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.PointLight 的强度属性,如下所示:

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 这决定了用于创建阴影的视野大小(请参阅 第二章 中 不同用途的不同相机 小节,构成 Three.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.SpotLight 的目标属性:

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.DirectionalLightTHREE.SpotLight(我们在上一节中已经讨论过)的主要区别在于,这种光不会随着距离 THREE.DirectionalLight 目标越来越远而减弱,就像 THREE.SpotLight 那样(你可以通过 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.js 场景的基本组件

注意

对于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="img/three.js"></script>
  <script type="text/javascript" src="img/stats.js"></script>
  <script type="text/javascript" src="img/dat.gui.js"></script>

  <script type="text/javascript" src="img/WebGLDeferredRenderer.js"></script>
  <script type="text/javascript" src="img/ShaderDeferred.js"></script>
  <script type="text/javascript" src="img/RenderPass.js"></script>
  <script type="text/javascript" src="img/EffectComposer.js"></script>
  <script type="text/javascript" src="img/CopyShader.js"></script>
  <script type="text/javascript" src="img/ShaderPass.js"></script>
  <script type="text/javascript" src="img/FXAAShader.js"></script>
  <script type="text/javascript" src="img/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来亲自查看:

镜头光晕

我们可以通过实例化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.LenseFlare对象添加摄影镜头光晕。

在前面的章节中,我们已经介绍了几种不同的材料,在本章中,你看到并不是所有材料对可用的灯光都有相同的反应。在下一章中,我们将概述 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.Geometry 的颜色属性上设置的颜色。此属性在 CanvasRenderer 上不起作用,但在 WebGLRenderer 上起作用。查看 LineBasicMaterial 示例,我们使用此属性为线条的各个部分着色。您还可以使用此属性为此材质类型创建渐变效果。
fog 此属性确定此材质是否受全局雾设置的影响。此效果在动作中未显示,但如果将其设置为 false,我们在第二章中看到的全局雾构成 Three.js 场景的基本组件不会影响此对象的渲染。

在前面的章节中,我们看到了如何创建材质并将它们分配给对象。对于 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 这决定了线框的宽度。

为了演示这一点,我们修改了第二章,构成 Three.js 场景的基本组件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,我们设置 transparenttrue 并定义一个 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 这配置了以 THREE.FlatShading 的平面着色和 THREE.SmoothShading 的平滑着色形式进行的着色。

我们已经看到了 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

这种材料可以用来创建看起来平淡无光、不反光的表面。这是一种非常易于使用的材料,能够对场景中的光源做出反应。这种材料可以配置我们之前见过的多种属性:颜色不透明度阴影混合深度测试深度写入线框线框线宽线框线帽线框线连接顶点颜色雾效。我们不会深入探讨这些属性的细节,但会专注于与这种材料相关的特定属性。这仅剩下以下四个属性:

名称 描述
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对象相同。我们再次跳过基本属性和已经讨论过的属性:颜色不透明度阴影混合深度测试深度写入线框线框线宽线框线帽线框线连接顶点颜色

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

名称 描述
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 坐标,以及一个使用来自 glslsandbox.com/ 的着色器创建动画材料的 fragmentShader 程序。

接下来,你可以看到我们将使用的 vertexShader 的完整代码。请注意,编写着色器不是在 JavaScript 中完成的。你使用一种类似于 C 的语言编写着色器,称为 GLSL(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 坐标:

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 实例,并为每个坐标创建一个新的顶点,然后将它推入此实例的线条属性中。对于每个坐标,我们还计算一个颜色值,我们使用它来设置 colors 属性。

小贴士

在此示例中,我们使用 setHSL() 方法设置了颜色。与提供红色、绿色和蓝色的值不同,使用 HSL,我们提供色调、饱和度和亮度。使用 HSL 比使用 RGB 更直观,并且更容易创建匹配的颜色集。CSS 规范中可以找到关于 HSL 的非常好的解释: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。如果缩放小于 1,则 dashSizegapSize 增加,如果缩放大于 1,则 dashSizegapSize 减少。
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 中,我们有一些几何体可以生成二维网格,还有更多几何体可以创建三维网格。在本节中,我们将首先查看二维几何体:THREE.CircleGeometryTHREE.RingGeometryTHREE.PlaneGeometryTHREE.ShapeGeometry。之后,我们将探索所有可用的基本三维几何体。

二维几何体

二维对象看起来像平面对象,正如其名所示,只有两个维度。列表中的第一个二维几何体是 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。在下面的屏幕截图中,你可以找到一个示例,其中我们使用 THREE.CircleGeometry 并将 thetaLength 值设置为小于 2 * PI

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 canvas 元素和 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 Yes 这些是用于创建 THREE.Geometry 的一或多个 THREE.Shape 对象。您可以传入单个 THREE.Shape 对象或 THREE.Shape 对象的数组。

| options | No | 您也可以传递一些应用于所有通过 shapes 参数传入的形状的 options。这些选项的解释如下:

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

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

  • UVGenerator:当您使用材质与纹理一起使用时,UV 映射确定用于特定面的纹理的哪个部分。使用 UVGenerator 属性,您可以传递自己的对象,该对象将为为传入的形状创建的面的 UV 设置。有关 UV 设置的更多信息,请参阅第十章 Chapter 10,加载和使用纹理。如果没有指定,则使用 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);

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

属性 必需 描述
Width 这是立方体的宽度。这是立方体顶点沿 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()。以下属性可以用来调整最终网格的外观:

属性 必需 描述
radius 这用于设置球体的半径。这定义了最终网格的大小。默认值是 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 的实际应用,您可以通过打开 08-basic-3d-geometries-torus.html 示例来获取此截图:

THREE.TorusGeometry

就像大多数简单几何形状一样,创建 THREE.TorusGeometry 时没有必填参数。以下表格列出了您在创建此几何形状时可以指定的参数:

属性 必需 描述
radius 这设置了完整扭曲面的尺寸。默认值是 100
tube 这设置了管子的半径(实际的甜甜圈)。此属性的默认值是 40
radialSegments 这决定了扭曲面长度方向上要使用的分段数。默认值是 8。在演示中查看更改此值的效果。
tubularSegments 这决定了扭曲面宽度方向上要使用的分段数。默认值是 6。在演示中查看更改此值的效果。
arc 使用此属性,您可以控制扭曲面是否绘制成完整的圆圈。此值的默认值是 2 * PI(一个完整的圆圈)。

大多数这些都是非常基础的属性,你已经见过。然而,arc 属性却非常有趣。使用这个属性,你定义甜甜圈是形成一个完整的圆还是只有部分圆。通过实验这个属性,你可以创建非常有趣的网格,如下面设置 arc0.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,您必须指定顶点和面(就像我们在第三章 Chapter 3 中处理立方体时做的那样,在 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 示例中展示(在右上角的菜单中选择 type 为:Custom):

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

四面体是最简单的多面体之一。这个多面体只包含由四个顶点创建的四个三角形面。您可以通过指定 radiusdetail 级别来创建 THREE.TetrahedronGeometry,就像 Three.js 提供的其他多面体一样。以下是使用 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.ParametricGeometry 对象。使用此对象,你可以根据一组方程创建一个几何形状。

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

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

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

THREE.ConvexGeometry

使用 THREE.ConvexGeometry,我们可以围绕一组点创建凸包。凸包是包含所有这些点的最小形状。理解这一点最简单的方法是查看一个示例。如果您打开 01-advanced-3d-geometries-convex.html 示例,您将看到一组随机点的凸包。以下截图显示了此几何形状:

THREE.ConvexGeometry

在这个示例中,我们生成一组随机点,并根据这些点创建 THREE.ConvexGeometry。在示例中,您可以点击重绘,这将生成 20 个新的点并绘制凸包。我们还添加了每个点作为一个小型的 THREE.SphereGeometry 对象,以清楚地显示凸包的工作原理。THREE.ConvexGeometry 不包含在标准的 Three.js 分发中,因此您必须包含一个额外的 JavaScript 文件才能使用此几何形状。在您的 HTML 页面顶部添加以下内容:

<script src="img/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.ConvexGeometry只接受一个包含顶点(THREE.Vector3类型)的数组作为参数。这里有一个关于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() 函数创建了形状,就像我们在上一章中所做的那样。这个形状被传递给 THREE.ExtrudeGeometry 构造函数,同时传递一个 options 对象。通过 options 对象,你可以精确地定义形状应该如何拉伸。以下表格解释了你可以传递给 THREE.ExtrudeGeometry 的选项。

属性 必需 描述
shapes 需要一个或多个形状(THREE.Shape 对象)来从其中拉伸几何体。请参阅前面的章节了解如何创建这样的形状。
amount 这决定了形状应该拉伸多远(深度)。默认值是 100
bevelThickness 这决定了斜面的深度。斜面是前后面和拉伸之间的圆角。此值定义斜面进入形状的深度。默认值是 6
bevelSize 这决定了斜面的高度。这被添加到形状的正常高度上。默认值是 bevelThickness - 2
bevelSegments 这定义了斜面将使用的段数。使用的段数越多,斜面看起来越平滑。默认值是 3
bevelEnabled 如果设置为 true,则添加斜面。默认值是 true
curveSegments 这决定了在拉伸形状的曲线时将使用多少段。使用的段数越多,曲线看起来越平滑。默认值是 12
steps 这定义了沿着拉伸的深度将分割成多少段。默认值是 1。更高的值将导致更多的单独面。
extrudePath No 这是形状应该拉伸的路径(THREE.CurvePath)。如果没有指定,形状将沿着z轴拉伸。
material No 这是用于前表面和后表面的材质索引。如果你想为前后表面使用不同的材质,请使用THREE.SceneUtils.createMultiMaterialObject函数来创建网格。
extrudeMaterial No 这是用于斜面和拉伸的材质索引。如果你想为前后表面使用不同的材质,请使用THREE.SceneUtils.createMultiMaterialObject函数来创建网格。
uvGenerator No 当你使用材质与纹理结合时,UV 贴图决定了纹理的哪一部分用于特定的面。通过uvGenerator属性,你可以传入自己的对象,该对象将为传入的形状创建面的 UV 设置。有关 UV 设置的更多信息,请参阅第十章,加载和使用纹理。如果没有指定,将使用THREE.ExtrudeGeometry.WorldUVGenerator
frames No Frenet 框架用于计算样条的切线、法线和双法线。当沿着extrudePath拉伸时使用。无需指定此参数,因为 Three.js 提供了自己的实现,即THREE.TubeGeometry.FrenetFrames,这也是默认值。有关 Frenet 框架的更多信息,请参阅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 的标准,可以用来创建用于网络的矢量 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 字符串: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设置为5,将stacks设置为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 莫比乌斯带

  • 管道

  • 扭结

  • 球体

本章的最后部分处理创建 3D 文本对象。

创建 3D 文本

在本章的最后部分,我们将快速浏览如何创建 3D 文本效果。首先,我们将查看如何使用 Three.js 提供的字体渲染文本,之后,我们将简要了解如何使用自己的字体进行此操作。

渲染文本

在 Three.js 中渲染文本非常简单。您只需定义要使用的字体和我们在讨论THREE.ExtrudeGeometry时看到的基 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 这是文本的大小。默认值是100
height 这是拉伸的长度(深度)。默认值是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 Frenet 坐标系用于计算样条的切线、法线和双法线。当沿着 extrudePath 挤压时使用。您不需要指定此值,因为 Three.js 提供了自己的实现,THREE.TubeGeometry.FrenetFrames,这也是默认值。有关 Frenet 坐标系的更多信息,请参阅 en.wikipedia.org/wiki/Differential_geometry_of_curves#Frenet_frame

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

提示

如果您想在 2D 中渲染字体,例如,将其用作材质的纹理,您不应该使用THREE.TextGeometryTHREE.TextGeometry内部使用THREE.ExtrudeGeometry构建 3D 文本,而 JavaScript 字体引入了很多开销。渲染简单的 2D 字体比仅使用 HTML5 canvas 更好。使用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="img/bitstream_vera_sans_mono_roman.typeface.js">
</script>

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

console.log(THREE.FontUtils.faces);

这将打印出如下内容:

添加自定义字体

在这里,您可以看到我们可以使用helvetiker字体,其weighteither boldnormal,以及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 联合函数可以用来合并两个几何体并创建一个新的几何体。你可以将此与我们在第八章中将要讨论的mergeGeometry功能进行比较,即第八章中的创建和加载高级网格和几何体
subtract 减去函数是联合函数的对立面。你可以通过从第一个几何体中移除重叠区域来创建一个新的几何体。

在接下来的几节中,我们将更详细地探讨这些函数。以下截图显示了仅使用unionsubtract功能依次创建的示例。

使用二进制操作合并网格

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

<script type="text/javascript" src="img/coffee-script.js"></script>
<script type="text/coffeescript" src="img/ThreeBSP.coffee"></script>

ThreeBSP.coffee文件包含我们在这个例子中需要的功能,而coffee-script.js可以解释 ThreeBSP 使用的 Coffee 语言。我们需要采取的最后一步是确保在开始使用 ThreeBSP 功能之前,ThreeBSP.coffee文件已经被完全解析。为此,我们在文件的底部添加以下内容:

<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="img/ThreeBSP.js"></script>

现在 ThreeBSP 库已加载,我们可以使用它提供的函数。

减法函数

在我们开始使用subtract函数之前,有一个重要的步骤您需要记住。这三个函数使用网格的绝对位置进行计算。因此,如果您在应用这些函数之前将网格分组或使用多种材质,您可能会得到奇怪的结果。为了获得最佳和最可预测的结果,请确保您正在处理未分组的网格。

让我们从演示subtract功能开始。为此,我们提供了一个示例,08-binary-operations.html。使用此示例,您可以尝试三种操作。当您第一次打开二进制操作示例时,您将看到以下类似的开屏:

减法函数

有三个线框:一个立方体和两个球体。Sphere1,中心球体,是所有操作执行的对象,Sphere2位于右侧,Cube位于左侧。在Sphere2Cube上,您可以定义四种操作之一:subtract(减法)、union(并集)、intersect(交集)和none(无)。这些操作是从Sphere1的角度应用的。当我们把Sphere2设置为减法,并选择showResult(并隐藏线框)时,结果将显示Sphere1减去Sphere1Sphere2重叠的区域。请注意,在您点击showResult按钮后,一些操作可能需要几秒钟才能完成,所以当可见的忙碌指示器时,请耐心等待。

以下截图显示了在减去另一个球体后球体的结果操作:

减法函数

在这个示例中,首先执行Sphere2定义的操作,然后执行Cube的操作。因此,如果我们减去Sphere2Cube(我们在x轴上稍微缩放),我们将得到以下结果:

减法函数

理解subtract功能性的最佳方式就是直接在示例中尝试。完成此操作所需的 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对象中。这使得我们可以对这些对象应用subtractintersectunion函数。现在,我们只需在围绕中心球体(sphere1BSP)包裹的ThreeBSP对象上调用我们想要的特定函数,这个函数的结果将包含我们创建新网格所需的所有信息。要创建这个网格,我们只需在sphere1BSP对象上调用toMesh()函数。在生成的对象上,我们必须确保通过首先调用computeFaceNormals然后调用computeVertexNormals()来正确计算所有法线。由于运行二进制操作之一会改变几何体的顶点和面,这会影响面的法线,因此需要调用这些计算函数。显式地重新计算它们将确保你的新对象在设置材质的着色为THREE.SmoothShading时能够平滑着色(正确渲染)。最后,我们将结果添加到场景中。

对于intersectunion,我们使用完全相同的方法。

相交函数

在上一节中解释了所有内容之后,对于intersect函数就没有太多可以解释的了。使用这个函数,只留下网格重叠的部分。以下截图是一个示例,其中球体和立方体都被设置为相交:

相交函数

如果你查看这个例子并调整设置,你会发现创建这类对象非常容易。而且记住,这可以应用于你创建的任何网格,即使是本章中看到的复杂网格,例如THREE.ParametricGeometryTHREE.TextGeometry

subtractintersect函数配合得很好。本节开头我们展示的例子是通过首先减去一个较小的球体来创建一个空心球体。之后,我们使用立方体与这个空心球体相交以得到以下结果(一个带有圆角的空心立方体):

相交函数

ThreeBSP 提供的最后一个函数是union函数。

并集函数

最后一个函数是 ThreeBSP 提供的函数中最不有趣的一个。使用这个函数,我们可以将两个网格组合在一起以创建一个新的网格。因此,当我们将其应用于两个球体和立方体时,我们将得到一个单一的对象——并集函数的结果:

并集函数

这实际上并不那么有用,因为 Three.js 也提供了这个功能(参见第八章 Chapter 8,创建和加载高级网格和几何形状,其中我们解释了如何使用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创建和样式化粒子

  • 使用点云创建一组分组的粒子

  • 从现有几何体创建点云

  • 粒子和粒子系统的动画

  • 使用纹理样式化粒子

  • 使用 canvas 通过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.Mesh 一样扩展自 THREE.Object3D 对象。这意味着你可以使用 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,并用 15,000 个粒子填充它。所有粒子都使用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并将其添加。在这个相同的循环中,我们还指定了用于设置THREE.PointCloudMaterialvertexColors属性为true时使用的颜色数组geom.colors。最后要做的事情是创建THREE.PointCloudMaterial并将其添加到场景中。

以下表格解释了你可以设置在THREE.PointCloudMaterial上的所有属性:

名称 描述
color 这是ParticleSystem中所有粒子的颜色。将vertexColors属性设置为 true 并使用几何体的颜色属性指定颜色将覆盖此属性(更准确地说,顶点的颜色将与该值相乘以确定最终颜色)。默认值是0xFFFFFF
map 使用此属性,你可以将纹理应用到粒子上。例如,你可以使它们看起来像雪花。此属性在本示例中没有显示,但将在本章稍后进行解释。
size 这是粒子的尺寸。默认值是1
sizeAnnutation 如果设置为 false,所有粒子的大小将相同,无论它们距离相机有多远。如果设置为 true,大小基于距离相机的距离。默认值是true
顶点颜色 通常,THREE.PointCloud 中的所有粒子具有相同的颜色。如果此属性设置为 THREE.VertexColors 并且几何体的颜色数组已被填充,则将使用该数组的颜色(也请参阅此表中的颜色条目)。默认值为 THREE.NoColors
透明度 与透明属性一起,此属性设置粒子的透明度。默认值为 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 创建,并且仅在您使用此特定渲染器时才有效。在我们探讨如何使用这种材料之前,让我们首先看看您可以在此材料上设置的属性:

名称 描述
颜色 这是粒子的颜色。根据指定的 混合模式,这会影响画布图像的颜色。
程序 这是一个接受画布上下文作为参数的函数。当粒子被渲染时,将调用此函数。2D 绘图上下文的输出将显示为粒子。
opacity 这决定了粒子的不透明度。默认值是 1,没有不透明度。
transparent 这决定了粒子是否透明。这和 opacity 属性一起工作。
blending 这是将要使用的混合模式。有关更多详细信息,请参阅第九章 Animations and Moving the Camera。
rotation 这个属性允许你旋转画布的内容。你通常需要将其设置为 PI 以正确对齐画布的内容。请注意,这个属性不能传递给材料的构造函数,而需要显式设置。

要查看 THREE.SpriteCanvasMaterial 的实际效果,你可以打开 04-program-based-sprites.html 示例。以下截图显示了此示例:

使用 THREE.CanvasRenderer 与 HTML5 canvas

在这个例子中,粒子是在 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。在这段代码中,我们还定义了具有指向 draw 函数的 program 属性的 THREE.SpriteCanvasMaterial。这个 draw 函数定义了粒子将看起来像什么(在我们的例子中,是一个来自 Pac-Man 的幽灵):

var draw = function(ctx) {
  ctx.fillStyle = "orange";
  ...
  // lots of other ctx drawing calls
  ...
  ctx.beginPath();
  ctx.fill();
}

我们不会深入绘制我们形状所需的实际 canvas 代码。这里重要的是我们定义了一个接受 2D canvas 上下文(ctx)作为其参数的函数。在上下文中绘制的一切都用作 THREE.Sprite 的形状。

使用 WebGLRenderer 与 HTML5 canvas

如果我们想在 THREE.WebGLRenderer 中使用 HTML5 canvas,我们可以采取两种不同的方法。我们可以使用 THREE.PointCloudMaterial 并创建 THREE.PointCloud,或者我们可以使用 THREE.SpriteTHREE.SpriteMaterialmap 属性。

让我们从第一种方法开始,创建 THREE.PointCloud。在 THREE.PointCloudMaterial 的属性中,我们提到了 map 属性。通过 map 属性,我们可以为粒子加载一个纹理。使用 Three.js,这个纹理也可以是 HTML5 canvas 的输出。展示这个概念的例子是 05a-program-based-point-cloud-webgl.html。以下截图显示了此示例:

使用 WebGLRenderer 与 HTML5 canvas

让我们看看我们编写的代码来实现这个效果。大部分代码与我们的上一个 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 canvas 来样式化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.CanvasRenderer 以及 THREE.WebGLRenderer 来渲染单个粒子。这些精灵被放置在 3D 世界中的某个位置,它们的大小基于与摄像机的距离(这有时也被称为 billboarding)。在本节中,我们将展示 THREE.Sprite 对象的另一种用途。我们将向您展示如何使用额外的 THREE.OrthographicCamera 实例使用 THREE.Sprite 创建一个类似于 抬头显示HUD)的层,用于您的 3D 内容。我们还将向您展示如何使用精灵贴图选择 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() 函数中,我们加载一个纹理。然而,我们不是为每个 幽灵 加载五张不同的图片,而是加载一个包含所有精灵的单个纹理。纹理看起来像这样:

处理精灵贴图

使用 map.offsetmap.repeat 属性,我们可以选择屏幕上显示的正确精灵。通过 map.offset 属性,我们确定加载的纹理在 x 轴(u)和 y 轴(v)上的偏移量。这些属性的缩放范围从 0 到 1。在我们的例子中,如果我们想选择第三个幽灵,我们将 u 偏移量(x 轴)设置为 0.4,因为我们只有一行,所以我们不需要改变 v 偏移量(y 轴)。如果我们只设置这个属性,纹理会在屏幕上显示第三个、第四个和第五个幽灵压缩在一起。如果我们只想显示一个幽灵,我们需要放大。我们通过将 map.repeat 属性的 u 值设置为 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 画布元素上生成),该函数如下所示:

从高级几何形状创建 THREE.PointCloud

摘要

本章内容到此结束。我们解释了粒子、精灵和粒子系统是什么,以及如何使用可用的材料来样式化这些对象。在本章中,您看到了如何直接使用 THREE.SpriteTHREE.CanvasRendererTHREE.WebGLRenderer 一起使用。然而,如果您想创建大量粒子,则应使用 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.Object3D()替换为new THREE.Group()以获得相同的效果。在这个例子中,我们使用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 个对象时,刷新率下降到大约 40 fps,而不是正常的 60 fps:

将多个网格合并成一个网格

如您所见,您可以添加到场景中的网格数量有一定的限制。不过,通常情况下,你可能不需要那么多网格,但在创建特定游戏(例如,类似于我的世界)或高级可视化时,你可能需要管理大量单独的网格。通过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 是一种简单的 3D 格式,最初由Wavefront Technologies开发。它是被广泛采用的 3D 文件格式之一,用于定义对象的几何形状。MTL 是 OBJ 的配套格式。在 MTL 文件中,OBJ 文件中对象的材质被指定。如果需要从 Three.js 导出模型到 OBJ 格式,Three.js 也提供了一个自定义的 OBJ 导出器,称为 OBJExporter.js。
Collada Collada 是一种基于 XML 格式的定义数字资产的格式。这也是一个广泛使用的格式,几乎所有的 3D 应用程序和渲染引擎都支持它。
STL STL代表立体光刻,广泛用于快速原型制作。例如,3D 打印机的模型通常定义为 STL 文件。Three.js 也提供了一个自定义的 STL 导出器,称为 STLExporter.js,如果您想从 Three.js 导出模型到 STL 格式。
CTM CTM 是由openCTM创建的文件格式。它用作以紧凑格式存储基于 3D 三角形的网格的格式。
VTK VTK 是由Visualization Toolkit定义的文件格式,用于指定顶点和面。有两种格式可供选择:一种是基于二进制的,另一种是基于文本的 ASCII 格式。Three.js 仅支持基于 ASCII 的格式。
AWD AWD 是 3D 场景的二进制格式,通常与away3d.com/引擎一起使用。请注意,此加载器不支持压缩的 AWD 文件。
Assimp Open asset import library(也称为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 中,你可以使用其 JSON 格式处理两种不同的场景。你可以用它来保存和加载单个 THREE.Mesh,或者用它来保存和加载一个完整的场景。

保存和加载 THREE.Mesh

为了演示保存和加载,我们创建了一个基于 THREE.TorusKnotGeometry 的简单示例。通过这个示例,你可以创建一个环面结,就像我们在 第五章 中所做的那样,学习与几何体一起工作,并使用 Save & Load 菜单中的 save 按钮保存当前几何体。对于这个示例,我们使用 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));

在保存之前,我们首先使用 JSON.stringify 函数将 toJSON 函数的结果,一个 JavaScript 对象,转换为字符串。这会产生一个看起来像这样的 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="img/SceneLoader.js"></script>
<script type="text/javascript" src="img/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.JSONLoader导入到 Three.js 中:

使用 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文件夹中添加了一个简单的 Blender 模型misc_chair01.blend,你可以在本书的源代码中找到它。在本节中,我们将加载此模型,并展示将此模型导出到 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/图像编辑器视图。你可以从文件菜单选项左侧的下拉菜单中选择此视图。这将替换顶部菜单,如下所示:

从 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 也支持这些格式。因此,作为替代方案,你可以选择将 Blender 中的模型导出为 OBJ/MTL 格式,而不是 Three.js 的 JSON 格式。Three.js 有两个不同的加载器你可以使用。如果你只想加载几何形状,你可以使用OBJLoader。我们在这个示例(06-load-obj.html)中使用了这个加载器。以下截图显示了此示例:

OBJ 和 MTL 格式

要在 Three.js 中导入此文件,你必须添加 OBJLoader JavaScript 文件:

<script type="text/javascript" src="img/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="img/OBJLoader.js"></script>
<script type="text/javascript" src="img/MTLLoader.js"></script>
<script type="text/javascript" src="img/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="img/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 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="img/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 创建您的第一个 3D 场景,关于渲染循环。为了支持动画,我们需要告诉 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,我们不是告诉浏览器何时更新屏幕;我们请求浏览器在最适合的时候运行提供的函数。通常,这会导致大约 60 fps 的帧率。使用requestAnimationFrame,你的动画将运行得更平滑,并且对 CPU 和 GPU 更友好,你也不必担心自己处理时间问题。

简单动画

使用这种方法,我们可以通过改变对象的旋转、缩放、位置、材质、顶点、面以及你能想象到的任何其他属性来非常容易地动画化对象。在下一个渲染循环中,Three.js 将渲染这些更改的属性。一个基于我们已经在第一章中看到的简单示例,使用 Three.js 创建您的第一个 3D 场景,可以在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/下载,并可以使用它轻松地定义两个值之间属性的过渡。所有起始值和结束值之间的中间点都会为你计算。这个过程被称为tweening

例如,你可以使用这个库在 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。这个 tween 将确保x属性在 10,000 毫秒内从 10 变为 3。Tween.js 还允许你定义这个属性随时间如何变化。这可以通过线性、二次或其他任何可能性(参见sole.github.io/tween.js/examples/03_graphs.html以获取完整概述)。随时间改变值的方式被称为easing。使用 Tween.js,你可以通过easing()函数来配置这个设置。

使用这个 Three.js 库非常简单。如果你打开03-animation-tween.html示例,你可以看到 Tween.js 库在行动。以下截图显示了示例的静态图像:

使用 Tween.js 进行动画

在这个例子中,我们从第七章,粒子、精灵和点云,取了一个粒子云,并动画化了所有粒子到地面。这些粒子的位置是基于使用 Tween.js 库创建的 tween,如下所示:

// 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);

使用这段代码,我们创建了两个 tween:tweentweenBack。第一个定义了位置属性如何从 1 过渡到 0,第二个则相反。通过chain()函数,我们将这两个 tween 链接在一起,所以这些 tween 在启动时将开始循环。在这里我们定义的最后一件事是onUpdate方法。在这个方法中,我们遍历粒子系统的所有顶点,并根据 tween 提供的位置(this.pos)改变它们的位置。

我们在模型加载时开始 tween,所以在以下函数的末尾,我们调用tween.start()函数:

var loader = new THREE.PLYLoader();
loader.load( "../assets/models/test.ply", function (geometry) {
  ...
  tween.start()
  ...
});

当 tween 开始时,我们需要告诉 Tween.js 库我们希望它在何时更新它所知道的所有 tween。我们通过调用TWEEN.update()函数来完成这个操作:

function render() {
  TWEEN.update();
  webGLRenderer.render(scene, camera);
  requestAnimationFrame(render);
}

在这些步骤到位后,tween 库将负责定位点云的各个点。正如你所看到的,使用这个库比自行管理过渡要容易得多。

除了动画化和改变对象,我们还可以通过移动相机来动画化场景。在前几章中,我们已经手动更新相机位置来这样做了几次。Three.js 还提供了一些更新相机的方法。

与相机一起工作

Three.js 提供了一些相机控制,您可以使用这些控制来在场景中控制相机。这些控制位于 Three.js 发行版中,可以在examples/js/controls目录中找到。在本节中,我们将更详细地查看以下控制:

Name 描述
FirstPersonControls 这些控制的行为类似于第一人称射击游戏中的控制。使用键盘移动,使用鼠标四处查看。
FlyControls 这些是类似飞行模拟器的控制。使用键盘和鼠标移动和转向。
RollControls 这是FlyControls的一个简化版本。允许您在z轴周围移动和滚动。
TrackBallControls 这些是最常用的控制,允许您使用鼠标(或轨迹球)轻松地在场景中移动、平移和缩放。
OrbitControls 这模拟了一个围绕特定场景运行的卫星。这允许您使用鼠标和键盘移动。

这些控制是最有用的控制。除了这些,Three.js 还提供了一些额外的控制,您可以使用(但本书中未解释)。然而,使用这些控制的方式与前面表格中解释的方式相同:

Name 描述
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="img/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

启用FlyControlsTrackballControls的方式相同。首先,加载正确的 JavaScript 文件:

<script type="text/javascript" src="img/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="img/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 支持的一些外部格式,在这些格式中可以定义动画。

带有形态目标的动画

形态目标(Morph targets)是定义动画最直接的方法。你为每个重要的位置(也称为关键帧)定义所有顶点,并告诉 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.Mesh 对象,而是创建 THREE.MorphAnimMesh。在加载动画时,您需要考虑以下几点:

  • 确保您使用的材质已将 THREE.morphTargets 设置为 true。如果没有设置,您的网格将不会动画化。

  • 在创建 THREE.MorphAnimMesh 之前,请确保对几何体调用 computeMorphNormals,以便计算所有形态目标的所有法向量。这对于正确的光照和阴影效果是必需的。

  • 还可以为特定形态目标的表面定义颜色。这些颜色可以通过 morphColors 属性访问。您可以使用它来变形几何体的形状,以及单个面的颜色。使用 morphColorsToFaceColors 辅助方法,我们只需将面的颜色固定在 morphColors 数组中的第一组颜色上。

  • 默认设置是一次性播放完整动画。如果有多个动画定义了相同的几何体,您可以使用 parseAnimations() 函数与 playAnimation(name,fps) 一起使用来播放定义的动画之一。我们将在本章的最后部分使用这种方法,其中我们将从 MD2 模型中加载动画。

剩下的工作是在渲染循环中更新动画。为此,我们再次使用 THREE.Clock 来计算 delta 并使用它来更新动画,如下所示:

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);

使用这个缓动效果,我们将pos变量从-1过渡到0。我们还设置了yoyo属性为true,这会导致动画在下次运行时反向播放。为了确保动画持续运行,我们将repeat设置为Infinity。您还可以看到我们指定了一个onUpdate方法。此方法用于定位单个骨骼,我们将在下一节中探讨这一点。

在我们移动骨骼之前,让我们看看12-bones-manually.html的示例。下面的截图显示了该示例的静态图像:

使用骨骼和蒙皮动画

当您打开此示例时,您会看到手部做出抓取动作。我们通过在从我们的缓动动画中调用的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 格式。其中一些格式也支持动画。在本章中,我们将查看以下示例:

  • Blender 与 JSON 导出器:我们将从一个在 Blender 中创建并导出为 Three.js JSON 格式的动画开始。

  • Collada 模型:Collada 格式支持动画。对于此示例,我们将从 Collada 文件中加载一个动画,并使用 Three.js 进行渲染。

  • MD2 模型:MD2 模型是一种在较老的 Quake 引擎中使用的简单格式。尽管格式有些过时,但它仍然是一个用于存储角色动画的非常好的格式。

我们将从 Blender 模型开始。

使用 Blender 创建骨骼动画

要从 Blender 开始动画制作,您可以加载我们包含在模型文件夹中的示例。您可以在那里找到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="img/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.SkinnedMesh(子节点)。要渲染和动画化这个模型,我们只需像处理基于 Blender 的模型一样设置动画;甚至渲染循环也保持不变。以下是渲染和动画化模型的步骤:

function render() {
  ...
  meshAnim.updateAnimation( delta *1000 );
  ...
}

对于这个特定的 Collada 文件,结果如下所示:

从 Collada 模型加载动画

另一个使用形态目标的外部模型示例是 MD2 文件格式。

从 Quake 模型加载的动画

MD2 格式是为了模拟 1996 年伟大的游戏《Quake》中的角色而创建的。尽管较新的引擎使用不同的格式,但您仍然可以在 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 图像作为纹理的输入。请注意,加载纹理是异步进行的。在我们的场景中,这不是问题,因为我们有一个每秒渲染场景约 60 次的render循环。如果您想等待纹理加载完成,可以使用以下方法:

texture = THREE.ImageUtils.loadTexture('texture.png', {}, function() { renderer.render(scene); });

在此示例中,我们向loadTexture提供了一个回调函数。当纹理加载时,会调用此回调函数。在我们的示例中,我们不使用回调,而是依赖于render循环在纹理加载后最终显示纹理。

您几乎可以使用任何图像作为纹理。然而,最佳结果是在使用边长为 2 的幂的方形纹理时获得。因此,如 256 x 256、512 x 512、1024 x 1024 等尺寸效果最佳。以下图像是一个方形纹理的示例:

加载纹理并将其应用于网格

由于纹理的像素(也称为纹理像素)通常不会一对一地映射到面的像素上,因此需要放大或缩小纹理。为此目的,WebGL 和 Three.js 提供了一些不同的选项。你可以通过设置magFilter属性来指定纹理的放大方式,以及通过设置minFilter属性来指定缩小方式。这些属性可以设置为以下两个基本值:

名称 描述
THREE.NearestFilter 此过滤器使用它能够找到的最近的纹理像素的颜色。当用于放大时,这将导致块状效果,当用于缩小,结果将丢失很多细节。
THREE.LinearFilter 此过滤器更高级,使用四个相邻的纹理像素的颜色值来确定正确的颜色。在缩小操作中,你仍然会丢失很多细节,但放大将会更加平滑且不那么块状。

除了这些基本值之外,我们还可以使用米普图。米普图是一组纹理图像,每个图像的大小是前一个图像的一半。这些图像在加载纹理时创建,允许进行更平滑的过滤。因此,当你有一个平方纹理(作为 2 的幂)时,你可以使用一些额外的策略来获得更好的过滤效果。可以使用以下值设置属性:

名称 描述
THREE.NearestMipMapNearestFilter 此属性选择最佳的米普图以映射所需的分辨率,并应用我们在前表中讨论的最近邻过滤原则。放大仍然会有块状效果,但缩小看起来要好得多。
THREE.NearestMipMapLinearFilter 此属性选择不仅仅是单个米普图,而是两个最近的米普图级别。在这两个级别上,都应用最近邻过滤器以获得两个中间结果。这两个结果通过线性过滤器传递以获得最终结果。
THREE.LinearMipMapNearestFilter 此属性选择最佳的米普图以映射所需的分辨率,并应用我们在前表中讨论的线性过滤原则。
THREE.LinearMipMapLinearFilter 此属性选择不仅仅是单个米普图,而是两个最近的米普图级别。在这两个级别上,都应用线性过滤器以获得两个中间结果。这两个结果通过线性过滤器传递以获得最终结果。

如果你没有明确指定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/DOM Image作为输入,并将其转换为正常贴图。

使用光照贴图创建假阴影

在前面的例子中,我们使用了特定的地图来创建看起来真实的阴影,这些阴影会根据房间内的光照做出反应。有一种替代方案可以创建假阴影。在本节中,我们将使用光照贴图。光照贴图是一个预先渲染的阴影(也称为预烘焙阴影),你可以用它来创建真实阴影的错觉。以下截图,来自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 贴图(纹理的哪一部分显示在表面上)。这样做是为了可以独立于其他纹理应用和映射光照贴图。在我们的例子中,我们只是使用了 Three.js 在创建地面平面时自动创建的基本 UV 贴图。更多信息和为什么需要显式 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属性来控制折射率,就像使用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 映射

通常,从 Blender 等程序中自定义 UV 映射,特别是当模型变得更加复杂时。这里需要记住的最重要部分是 UV 映射在两个维度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 噪声。Perlin 噪声(en.wikipedia.org/wiki/Perlin_noise)生成了一种非常自然的外观随机纹理,正如您在前面的截图中所看到的。我们使用来自 github.com/wwwtyro/perlin.js 的 Perlin 噪声函数:

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="img/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="img/EffectComposer.js"></script>
<script type="text/javascript" src="img/MaskPass.js"></script>
<script type="text/javascript" src="img/RenderPass.js"></script>
<script type="text/javascript" src="img/CopyShader.js"></script>
<script type="text/javascript" src="img/ShaderPass.js"></script>

EffectComposer.js文件提供了允许我们添加后处理步骤的THREE.EffectComposer对象。MaskPass.jsShaderPass.jsCopyShader.jsTHREE.EffectComposer内部使用,而RenderPass.js允许我们将渲染 pass 添加到THREE.EffectComposer中。没有这个 pass,我们的场景根本不会渲染。

对于这个示例,我们添加了两个额外的 JavaScript 文件,以给我们的场景添加电影般的特效:

<script type="text/javascript" src="img/FilmPass.js"></script>
<script type="text/javascript" src="img/FilmShader.js"></script>

我们需要做的第一件事是创建THREE.EffectComposer。您可以通过将其构造函数传递THREE.WebGLRenderer来实现这一点:

var webGLRenderer = new THREE.WebGLRenderer();
var composer = new THREE.EffectComposer(webGLRenderer);

接下来,我们向这个合成器添加各种passes

配置 THREE.EffectComposer 进行后处理

每个 pass 都是按照它添加到THREE.EffectComposer中的顺序执行的。我们首先添加的 pass 是THREE.RenderPass。接下来的 pass 渲染我们的场景,但尚未输出到屏幕:

var renderPass = new THREE.RenderPass(scene, camera);
composer.addPass(renderPass);

要创建THREE.RenderPass,我们传递要渲染的场景和我们想要使用的相机。使用addPass函数,我们将THREE.RenderPass添加到THREE.EffectComposer中。下一步是添加另一个将输出其结果的 pass。并非所有可用的 passes 都允许这样做——稍后我们会详细介绍——但在这个示例中使用的THREE.FilmPass允许我们将其 pass 的结果输出到屏幕。要添加THREE.FilmPass,我们首先需要创建它并将其添加到合成器中。生成的代码如下:

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。这个 pass 在renderPass之后添加到THREE.EffectComposer中,所以当使用这个合成器时,首先渲染场景,然后通过THREE.FilmPass,我们也可以在屏幕上看到输出。

更新渲染循环

现在我们只需要对我们的渲染循环进行一些小的修改,以使用合成器而不是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上的渲染函数,它反过来使用传入的THREE.WebGLRenderer,由于我们将FilmPassrenderToScreen设置为true,因此FilmPass的结果将显示在屏幕上。

使用这个基本设置,我们将在接下来的几节中查看可用的后处理 passes。

后处理 passes

Three.js 附带了一些可以直接与THREE.EffectComposer一起使用的后处理 passes。请注意,最好在本章的示例中尝试,以查看这些 passes 的结果并理解正在发生的事情。以下表格概述了可用的 passes:

Pass 名称 描述
THREE.BloomPass 这是一个使光线区域渗透到较暗区域的效果。这模拟了当相机被极其明亮的光线淹没的效果。
THREE.DotScreenPass 这会在屏幕上应用一层代表原始图像的黑点层。
THREE.FilmPass 这通过应用扫描线和扭曲来模拟电视屏幕。
THREE.GlitchPass 这在随机的时间间隔内在屏幕上显示电子故障。
THREE.MaskPass 这允许您将蒙版应用于当前图像。后续流程仅应用于蒙版区域。
THREE.RenderPass 这根据提供的场景和相机渲染场景。
THREE.SavePass 当执行此流程时,它会复制当前渲染步骤,您可以在以后使用。实际上,这个流程并不那么有用,我们不会在我们的任何示例中使用它。
THREE.ShaderPass 这允许您为高级或定制的后处理流程传递自定义着色器。
THREE.TexturePass 这将合成器的当前状态存储在一个纹理中,您可以使用它作为其他 EffectComposer 实例的输入。

让我们从几个简单的流程开始。

简单的后处理流程

对于简单的流程,我们将查看 THREE.FilmPassTHREE.BloomPassTHREE.DotScreenPass 我们能做什么。对于这些流程,有一个示例 02-post-processing-simple,允许您实验这些流程并查看它们如何以不同的方式影响原始输出。以下截图显示了此示例:

简单的后处理流程

在这个示例中,我们同时展示了四个场景,并且在每个场景中添加了不同的后处理流程。左上角显示的是 THREE.BloomPass,右上角显示的是 THREE.FilmPass,左下角显示的是 THREE.DotScreenPass,右下角显示的是原始渲染。

在这个示例中,我们也使用了 THREE.ShaderPassTHREE.TexturePass 来重用原始渲染的输出作为其他三个场景的输入。因此,在我们查看单个流程之前,让我们首先看看这两个流程:

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,它将输出默认场景(右下角的那个)。这个合成器有两个流程。THREE.RenderPass 渲染场景,而 THREE.ShaderPass,当配置为 THREE.CopyShader 时,渲染输出,如果我们设置 renderToScreen 属性为 true,则不会对屏幕进行任何进一步的后处理。如果您查看示例,您会看到我们展示了相同的场景四次,但每次都应用了不同的效果。我们可以使用 THREE.RenderPass 四次从头开始渲染场景,但这会有些浪费,因为我们可以直接重用第一个合成器的输出。为此,我们创建 THREE.TexturePass 并传入 composer.renderTarget2 的值。现在我们可以使用 renderScene 变量作为其他合成器的输入,而无需从头开始渲染场景。让我们首先回顾一下 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 中的第一个通道。接下来,我们只需添加 THREE.FilmPass,效果就会应用。THREE.FilmPass 本身需要四个参数:

Property 描述
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;现在,你需要知道的是,通过这种方式,你可以直接更新后处理通道和着色器的配置,并直接看到结果。

使用 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.EffectComposer 进行比较,后者我们与 THREE.FilmPass 一起使用,你会注意到我们添加了一个额外的通道,effectCopy。这个步骤,我们同样用于正常输出,它不会添加任何特殊效果,只是将最后一个通道的输出复制到屏幕上。我们需要添加这个步骤,因为 THREE.BloomPass 不能直接渲染到屏幕上。

下表列出了你可以在 THREE.BloomPass 上设置的属性:

Property 描述
Strength 这是光晕效果的强度。这个值越高,明亮区域越亮,并且它们向较暗区域的“渗透”越多。
kernelSize 这个属性控制光晕效果的偏移量。
sigma 通过 sigma 属性,你可以控制光晕效果的锐度。值越高,光晕效果看起来越模糊。
Resolution Resolution 属性定义了光晕效果创建的精确度。如果你将其设置得太低,结果看起来会像方块。

通过使用前面提到的示例,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 也可以配置多个属性,如下所示:

属性 描述
center 使用 center 属性,您可以微调点的偏移方式。
angle 点以某种方式对齐。使用 angle 属性,您可以更改这种对齐方式。
Scale 使用此功能,我们可以设置要使用的点的大小。scale 越低,点就越大。

对其他着色器适用的内容也适用于此着色器。通过实验,更容易获得正确的设置。

将场景输出为一系列点

在同一屏幕上显示多个渲染器的输出

本节不深入介绍如何使用后处理效果,而是解释如何在同一屏幕上获取所有四个 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() 函数。如果我们每次在作曲家上调用 render() 函数时都不这样做,之前渲染的场景将被清除。采用这种方法,我们只在渲染循环的开始处清除一切。

为了避免所有作曲家都在相同的空间中渲染,我们将 webGLRenderer 的视口设置到屏幕的不同部分,该视口被我们的作曲家使用。此函数接受四个参数:xywidthheight。如代码示例所示,我们使用此函数将屏幕分成四个区域,并让作曲家在各自区域进行渲染。请注意,如果您想使用多个场景、相机和 WebGLRenderer,也可以使用这种方法。

在本节开头的表格中,我们还提到了 THREE.GlitchPass。使用此渲染通道,您可以为场景添加一种电子故障效果。这种效果与您迄今为止看到的其它效果一样易于使用。要使用它,首先在您的 HTML 页面中包含以下两个文件:

<script type="text/javascript" src="img/GlitchPass.js"></script>
<script type="text/javascript" src="img/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传入的场景之外的所有内容。这在上面的屏幕截图中显示:

使用遮罩的 Advanced 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 还提供了两种高级后处理效果,允许你将散景效果应用到场景中。散景效果在渲染主要主题时非常锐利,而对场景的一部分提供模糊效果。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 提供的。这个着色器不会模糊整个场景,而只是一个小区域。这提供了称为 倾斜移位 的效果。这通常用于从普通照片中创建类似微缩模型的效果。下面的图像展示了这个效果:

模糊着色器

高级着色器

对于高级着色器,我们将做与之前模糊着色器相同的事情。我们只会展示着色器的输出。有关如何配置它们的详细信息,请查看 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)的 texel(纹理中的像素)。

接下来,我们使用这个 texel 的 rgb 属性来计算一个灰度值。这个灰度值被设置为 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 从纹理中获取 texeltDiffuse

  • 我们根据 bitSize 属性计算可以拥有的颜色数量,通过计算 2bitSize 次方(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.RenderPassTHREE.EffectComposer中时,请确保将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="img/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 材质,但允许我们设置材质的frictionrestitution(弹性)。更多关于这一点的内容将在下一节中介绍。另一个新方面是我们添加到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 特定的材质。我们这样做是为了能够设置此对象的frictionrestitution

到目前为止,我们已经看到了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 Physijs.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函数将其添加到场景中来启用它。当你开始尝试使用约束时,你可能会遇到一些奇怪的问题。为了使调试更容易,你可以将true传递给addConstraint函数。如果你这样做,约束点和方向将在场景中显示出来。这可以帮助你正确地获取约束的旋转和位置。

使用 HingeConstraint 创建门状约束

HingeConstraint,正如其名所示,允许你创建一个像铰链一样行为的对象。它围绕一个特定的轴旋转,限制运动到指定的角度。在我们的例子中,HingeConstraint显示了场景中心的两个白色翻板。这些翻板被约束到小而棕色的立方体上,并且可以围绕它们旋转。如果你想玩这些铰链,你可以通过在铰链菜单中勾选enableMotor框来启用它们。这将加速翻板到通用菜单中指定的速度。负速度会使铰链向下移动,而正速度会使它们向上移动。以下截图显示了铰链在上位和下位的位置:

使用 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的方向。在 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.js 中的一个特定助手THREE.GridHelper来创建立方体下面的网格:

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([http://webaudio.github.io/web-audio-api/](http://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 @ 2025-10-25 10:29  绝不原创的飞龙  阅读(17)  评论(0)    收藏  举报