Three-js-秘籍-全-

Three.js 秘籍(全)

原文:zh.annas-archive.org/md5/a32c4684c670a5a9df0a19f7a142e3d7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

每年,网络浏览器都变得更加强大,功能更加丰富,性能也在提高。在过去几年中,浏览器已经成为了创建沉浸式、复杂和美观应用程序的绝佳平台。目前大多数正在开发的应用程序都使用了现代 HTML5 功能,例如 WebSocket、本地存储以及用于样式的先进 CSS 技术。

然而,大多数现代浏览器也支持一种技术,可以用来创建美观的 3D 图形和动画,该技术利用 GPU 实现最大性能。这种技术被称为 WebGL,并得到了 Firefox、Chrome、Safari 和 Internet Explorer 最新版本的 支持。使用 WebGL,你可以在浏览器中直接创建 3D 场景,无需任何插件。桌面上的标准支持非常好,大多数现代设备和移动浏览器完全支持此标准。

然而,要创建 WebGL 应用程序,你需要学习一门新的语言(称为 GLSL),并理解如何使用顶点和片段着色器来渲染你的 3D 几何形状。幸运的是,尽管如此,有多个 JavaScript 库可用,它们封装了 WebGL 的内部功能,并提供了一个你可以使用的 JavaScript API,而无需理解 WebGL 最复杂的功能。这些库中最成熟且功能丰富的之一是 Three.js。

Three.js 始于 2010 年,提供了一大批易于使用的 API,这些 API 揭示了 Three.js 的所有功能,并允许你快速在浏览器中创建复杂的 3D 场景和动画。

通过其 API,你可以使用 Three.js 做几乎所有你想做的事情。然而,由于它具有如此多的功能,有时很难找到正确的方法来完成某项任务。多年来,Three.js 一直在进行大量开发,但现在它正在稳定下来。因此,你在线上找到的许多示例和教程都已过时,不再工作。在这本书中,我们将为你提供大量食谱,你可以遵循这些食谱使用 Three.js 完成一些常见任务。每个示例都附有一个可运行的示例,你可以检查以更好地理解食谱或根据你自己的目的进行修改。

本书涵盖的内容

第一章, 入门,介绍了当你创建基于 Three.js 的新应用程序时可以使用的基本食谱。我们将向你展示如何使用任何可用的 Three.js 渲染器设置一个基本的 Three.js 骨架。我们还将进一步展示 WebGL 检测、加载资源、设置动画循环、添加拖放支持以及通过键盘控制场景。

第二章, 几何体和网格,展示了多个专注于创建、使用和操作几何体和网格的食谱。我们将详细介绍如何以不同的方式旋转网格,使用矩阵变换来操作它们,以编程方式生成几何体,以及从 Blender 和其他格式加载模型。

第三章, 使用相机,专注于在 Three.js 中操作相机的食谱。它展示了如何使用透视和正交相机。本章还展示了如何旋转相机、居中相机以及围绕物体进行跟踪的食谱。

第四章, 材质和纹理,包含解释如何使用 Three.js 提供的材质获得良好结果的食谱。它包括关于透明度、反射、UV 贴图、面材质、凹凸和法线贴图的食谱,并解释了各种混合模式的工作原理。

第五章, 光源和自定义着色器,包含处理 Three.js 中不同光源工作原理的食谱,并展示了如何使用 WebGL 着色器。它展示了如何正确设置阴影、创建类似太阳的光源,并探讨了聚光灯、点光源和方向光源之间的区别。在本章中,我们还将提供一些食谱,解释如何创建自定义顶点着色器和自定义片段着色器。

第六章, 点云和后期处理,提供了展示如何设置后期处理的食谱。使用后期处理,你可以通过模糊、着色或其他类型的效果来增强场景。本章还包含解释粒子系统功能的食谱,例如动画和粒子材质。

第七章, 动画和物理,展示了多个帮助你动画场景中对象的食谱,并展示了如何将物理(如重力碰撞检测)添加到场景中。

你需要本书的条件

为了使用本书,你需要一个简单的文本编辑器来实验提供的食谱,以及一个现代网络浏览器来运行示例。对于一些高级食谱,建议安装本地网络服务器或在浏览器中禁用一些安全设置。在第一章 入门 中,提供了解释如何设置此类服务器和禁用相关安全设置的食谱。

本书面向的对象

这本书是为那些对 JavaScript 和 Three.js 有基本了解但想学习如何使用 Three.js 更高级功能的人准备的。你不需要理解高级数学概念或对 WebGL 有深入了解。本书中的食谱将逐步解释 Three.js 的各种功能,我们还提供了所有食谱作为可立即使用的 HTML 源代码。

部分

在这本书中,你会发现一些经常出现的标题(准备就绪、如何操作、工作原理、还有更多、参见)。

为了清楚地说明如何完成食谱,我们使用以下这些部分:

准备就绪

本节告诉你可以在食谱中期待什么,并描述如何设置任何软件或任何为食谱所需的初步设置。

如何操作…

本节包含遵循食谱所需的步骤。

工作原理…

本节通常包含对前一个章节发生情况的详细解释。

还有更多…

本节包含有关食谱的附加信息,以便使读者对食谱有更多的了解。

参见

本节提供了对食谱其他有用信息的链接。

惯例

在这本书中,你会发现许多不同风格的文本,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名将如下所示:“一个值得注意的有趣事情是向ondrop事件处理器添加texture.needsUpdate = true。”

代码块如下设置:

step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

当我们希望将你的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:

    var y = camera.position.y;
 camera.position.y = y * Math.cos(control.rotSpeed) +z * Math.sin(control.rotSpeed);
 camera.position.z = z * Math.cos(control.rotSpeed) –y * Math.sin(control.rotSpeed);

新术语重要词汇以粗体显示。你在屏幕上看到的,例如在菜单或对话框中的单词,在文本中会这样显示:“在这个屏幕上,只需点击我会小心的,我保证按钮。”

注意

警告或重要注意事项以这样的框显示。

小贴士

小技巧和窍门如下所示。

读者反馈

我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者反馈对我们开发你真正能从中获得最大利益的标题非常重要。

要向我们发送一般反馈,只需发送电子邮件到<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/1182OS.pdf下载此文件。

错误清单

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

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

侵权

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

请通过<copyright@packtpub.com>联系我们,并提供涉嫌侵权材料的链接。

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

询问

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

第一章. 入门

在本章中,我们将介绍以下食谱:

  • 开始使用 WebGL 渲染器

  • 开始使用 Canvas 渲染器

  • 开始使用 CSS 3D 渲染器

  • 检测 WebGL 支持

  • 设置动画循环

  • 确定场景的帧率

  • 控制场景中使用的变量

  • 使用 Python 设置本地 Web 服务器

  • 使用 Node.js 设置本地 Web 服务器

  • 使用 Mongoose 设置本地 Web 服务器

  • 解决 Chrome 中的跨域错误消息

  • 解决 Firefox 中的跨域错误消息

  • 添加键盘控制

  • 异步加载纹理

  • 异步加载模型

  • 带进度加载模型

  • 带进度异步加载其他资源

  • 等待资源加载完成

  • 将文件从桌面拖动到场景中

简介

在本章中,我们将向您展示一系列食谱,介绍 Three.js 的基本用法。我们将从一些简单的食谱开始,您可以用它们作为 Three.js 项目的起点。接下来,我们将向您展示您可以添加到项目中的几个功能,例如 WebGL 检测和定义动画循环。我们将以添加拖放支持和同步和异步加载资源等更多高级功能结束。

开始使用 WebGL 渲染器

当您想要创建一个使用 WebGL 进行渲染的初始 Three.js 项目时,您总是必须设置相同的几个变量。您需要一个 THREE.WebGLRenderer 对象,一个 THREE.Scene 对象,一个相机,以及渲染场景的方法。在本食谱中,我们将为您提供您可以在自己的项目中使用的标准模板,以便快速开始使用 WebGL 渲染器。

准备工作

确保您下载了本书的源代码。您可以通过以下两种方式之一来完成此操作:

  • 首先,您可以通过克隆您可以在 github.com/josdirksen/threejs-cookbook 找到的 Git 仓库来完成此操作。

  • 或者,您可以从 Packt Publishing 网站下载源代码。当您解压缩 ZIP 文件或克隆存储库时,您将找到一组目录;每个目录对应本书的一章。对于这个食谱,您可以使用 0 作为参考。

您可以直接通过在浏览器中打开之前提到的文件来查看此食谱的最终结果。当您在浏览器中打开此示例时,您将看到以下截图:

准备工作

这是一个最小化的场景,使用 THREE.WebGLRenderer 对象渲染。

如何操作...

创建一个可以作为您项目基础的骨架很容易。通过几个简单的步骤,您将获得第一个基于 WebGLRenderer 的 Three.js 场景并开始运行:

  1. 让我们先定义我们将要使用的基本 HTML:

    <!DOCTYPE html>
    <html>
      <head>
        <title>01.01 - WebGLRenderer - Skeleton</title>
        <script src="img/three.js"></script>
        <style>
          body {
          margin: 0;
          overflow: hidden;
          }
        </style>
      </head>
      <body>
        <script>
          ...
        </script>
      </body>
    </html>
    

    小贴士

    下载示例代码

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

    如您所见,这是一个简单的页面,其中包含一个 script 标签,它将包含我们的 Three.js 代码。唯一有趣的部分是 CSS 样式。

    我们将此样式添加到 body 元素中,以确保我们的 Three.js 场景将以全屏模式运行,并且不会显示任何滚动条。

  2. 接下来,让我们先填写 script 标签。我们将要做的第一件事是创建一些全局变量,这些变量将在整个配方中使用:

          // global variables
          var renderer;
          var scene;
          var camera;
    

    renderer 变量将保存对我们在下一步中将要创建的 THREE.WebGLRenderer 对象的引用。scene 变量是我们想要渲染的所有对象的容器,而 camera 变量决定了渲染场景时我们将看到的内容。

  3. 通常,您希望在开始运行 JavaScript 之前等待所有 HTML 元素完成加载。为此,我们使用以下 JavaScript:

          // calls the init function when the window is done loading.
          window.onload = init;
    

    通过这段代码,我们告诉浏览器在完整页面加载完毕后调用 init 函数。在下一步中,我们将展示这个 init 函数的内容。

  4. 为了使您的骨架工作,您需要添加一个 init 函数,其外观如下:

    function init() {
    
          // create a scene, that will hold all our elements 
          // such as objects, cameras and lights.
          scene = new THREE.Scene(); 
          // create a camera, which defines where we looking at.
          camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
          // position and point the camera to the center
          camera.position.x = 15;
          camera.position.y = 16;
          camera.position.z = 13;
          camera.lookAt(scene.position);
    
          // create a renderer, set the background color and size
          renderer = new THREE.WebGLRenderer();
          renderer.setClearColor(0x000000, 1.0);
          renderer.setSize(window.innerWidth, window.innerHeight);
    
          // create a cube and add to scene
          var cubeGeometry = new THREE.BoxGeometry(10 * Math.random(), 10 * Math.random(), 10 * Math.random());
    
          var cubeMaterial = new THREE.MeshNormalMaterial();
    
          var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
          scene.add(cube);
    
          // add the output of the renderer to the html element
          document.body.appendChild(renderer.domElement);
    
          // call the render function
          renderer.render(scene, camera);
    
          }
    

    在这个 init 函数中,我们首先创建了一个 THREE.Scene 对象,其中包含我们想要渲染的所有对象的容器。接下来,我们创建了一个相机,它决定了将要渲染的视图范围。接下来,我们创建了 THREE.WebGLRenderer 对象,它用于使用 WebGL 渲染场景。THREE.WebGLRenderer 对象有许多属性。在这个配方中,我们使用了 setClearColor 属性将场景的背景设置为黑色,并告诉渲染器使用整个窗口作为其输出,使用 window.innerWidthwindow.innerHeight 属性。为了检查我们的骨架页面是否正常工作,我们随后将一个简单的 THREE.Mesh 对象和一个 THREE.BoxGeometry 对象添加到场景中。在这个时候,我们可以将 WebGL 的输出作为 HTML body 元素的一个子元素添加。我们通过将渲染器的 DOM 元素附加到文档体来实现这一点。现在,剩下的只是通过调用 renderer.render() 来渲染场景。

通过这些步骤,您已经创建了一个基于 WebGLRenderer 的基本 Three.js 场景,您可以用它作为所有 Three.js 实验的基本起点。

参见

  • THREE.WebGLRenderer对象仅在您的浏览器支持 WebGL 时才工作。尽管大多数现代桌面浏览器(甚至大量移动浏览器)支持 WebGL,但在某些情况下,您可能需要寻找替代方案。Three.js 提供了一些其他渲染器,您可以使用。要获取支持 WebGL 的浏览器最新概述,您可以查看此主题的信息caniuse.com/webgl

  • 除了使用THREE.WebGLRenderer对象来渲染您的场景外,您还可以使用在Canvas 渲染器入门配方中解释的THREE.CanvasRenderer对象,或者在CSS 3D 渲染器入门配方中解释的THREE.CSS3DRenderer对象。

Canvas 渲染器入门

如果您的系统不支持 WebGL,您可以使用另一个渲染器来渲染您的场景:CanvasRenderer对象。这个渲染器不使用 WebGL 来渲染输出,而是直接使用 JavaScript 绘制 HTML5 canvas 元素。

准备就绪

在 Three.js 的 r69 版本中,canvas 渲染器已被从默认分发中删除。要使用此渲染器,我们首先必须导入以下两个文件:

    <script src="img/CanvasRenderer.js"></script>
    <script src="img/Projector.js"></script>

对于这个配方,您可以查看本章源文件中的01.02-canvasrenderer-skeleton.html示例。如果您在浏览器中打开此示例,您将看到一个立方体,几乎就像上一个配方中的那样:

准备就绪

然而,这次这个立方体是用 HTML5 canvas 元素渲染的。HTML5 canvas 在许多设备上受到支持,但性能低于基于 WebGL 的解决方案。

如何操作...

要设置 WebGL 渲染器,您将遵循我们在上一个配方WebGL 渲染器入门中展示的 exactly the same steps,因此我们不会在本节中详细介绍,但我们会列出以下差异:

  1. 要开始使用THREE.CanvasRenderer对象,我们唯一需要更改的是以下内容:

    • 在以下代码片段中替换THREE.WebGLRenderer对象:

            renderer = new THREE.WebGLRenderer();
            renderer.setClearColor(0x000000, 1.0);
            renderer.setSize(window.innerWidth, window.innerHeight);
      
    • 将 THREE.WebGLRenderer 对象替换为以下THREE.CanvasRenderer对象:

          renderer = new THREE.CanvasRenderer();
          renderer.setClearColor(0x000000, 1.0);
          renderer.setSize(window.innerWidth, window.innerHeight);
      

就这样。通过这个更改,我们从使用 WebGL 渲染转变为在 HTML5 canvas 上渲染。

它是如何工作的...

HTML5 canvas 渲染器和 WebGL 渲染器之间的主要区别在于,这种方法使用 JavaScript 直接在 HTML5 canvas 上绘制以渲染您的 3D 场景。这种方法的 主要问题是性能糟糕。当您使用THREE.WebGLRenderer对象时,可以使用硬件加速渲染。然而,使用THREE.CanvasRenderer对象时,您必须完全依赖基于软件的渲染,这会导致性能降低。THREE.CanvasRenderer的另一个缺点是,您无法使用 Three.js 的高级材料和功能,因为那需要 WebGL 特定的功能。

相关内容

  • 如果你可以使用 CSS 3D 渲染器入门 菜谱中给出的 WebGL 方法,你应该真的使用它。它提供了比基于画布的方法更多的功能,并且性能更好。

  • 在下面的菜谱中,CSS 3D 渲染器入门,这也会展示一种不同的方法,其中我们使用基于 CSS 3D 的渲染器来动画化 HTML 元素。CSS 3D 还提供了硬件加速渲染,但只支持有限的三维.js 功能。

CSS 3D 渲染器入门

HTML 和 CSS 每天都在变得越来越强大。现代浏览器,无论是移动版还是桌面版,都对这两个标准提供了很好的支持。CSS 的最新版本也支持 3D 变换。使用 THREE.CSS3DRenderer 对象,我们可以直接访问这些 CSS 3D 功能,并在 3D 空间中变换任意 HTML 元素。

准备工作

要使用 CSS 3D 渲染器,我们首先必须从 Three.js 网站下载特定的 JavaScript 文件,因为它尚未包含在标准的 Three.js 分发中。你可以直接从 GitHub 下载此文件:raw.githubusercontent.com/mrdoob/three.js/master/examples/js/renderers/CSS3DRenderer.js,或者查看本书提供的源代码的 lib 目录。

要查看 CSS3DRenderer 场景的实际效果,你可以在浏览器中打开示例 01.03-cssrenderer-skeleton.html

准备工作

你在这里看到的是一个标准的 HTML div 元素,使用 THREE.CSS3DRenderer 对象以 3D 形式渲染。

如何做到这一点...

要设置一个基于 THREE.CSS3DRenderer 的场景,我们需要执行几个简单的步骤:

  1. 在我们开始 THREE.CSS3DRenderer 特定信息之前,首先,你必须设置一个简单的 HTML 页面,就像我们在 CSS 3D 渲染器入门 菜谱中所做的那样。所以按照那个菜谱的前三个步骤进行,然后继续下一步。

  2. 在初始设置之后,我们首先需要做的是在我们的 head 元素中添加正确的 JavaScript:

        <script src="img/CSS3DRenderer.js"></script>
    

    接下来,我们将从定义所需的全局变量开始:

        var content = '<div>' +
          '<h1>This is an H1 Element.</h1>' +
          '<span class="large">Hello Three.js cookbook</span>' +
          '<textarea> And this is a textarea</textarea>' +
        '</div>';
    
        // global variables, referenced from render loop
        var renderer;
        var scene;
        var camera;
    
  3. 我们在这里定义的是我们想要渲染的元素的字符串表示。由于 THREE.CSS3DRenderer 对象与 HTML 元素一起工作,我们在这里不会使用任何标准的 Three.js 几何体,而只是纯 HTML。渲染器、场景和相机是相应的 Three.js 元素的简单变量,这样我们就可以轻松地从 render() 函数中访问它们,我们稍后会看到这个函数。

  4. 与其他骨骼类似,我们将在 init() 函数中初始化场景。你需要添加到 THREE.CSS3DRenderer 对象中的函数如下所示:

        function init() {
    
          scene = new THREE.Scene();
          camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
    
     // create a CSS3DRenderer
     renderer = new THREE.CSS3DRenderer();
     renderer.setSize(window.innerWidth, window.innerHeight);
     document.body.appendChild(renderer.domElement);
    
          // position and point the camera to the center of the scene
          camera.position.x = 500;
          camera.position.y = 500;
          camera.position.z = 500;
          camera.lookAt(scene.position);
    
     var cssElement = createCSS3DObject(content);
     cssElement.position.set(100, 100, 100);
     scene.add(cssElement);
    
          render();
        }
    
  5. 我们将关注这个代码片段中突出显示的部分。对于这个函数其他部分的解释,我们将参考 使用 WebGL 渲染器入门 菜谱。正如你在这个片段中看到的,这次我们将创建一个 THREE.CSS3DRenderer 对象。就像我们使用其他渲染器一样,我们还需要设置大小。由于我们想要填满屏幕,我们将使用 window.innerWidthwindow.innerHeight 属性。其余的代码保持不变。

  6. 现在,我们只需要添加一个元素来完成这个骨架。使用 CSS 3D 渲染器,我们只能添加 THREE.CSS3DObject 元素。对于这一步,只需添加以下函数:

    function createCSS3DObject(content) 
        {
          // convert the string to dome elements
          var wrapper = document.createElement('div');
          wrapper.innerHTML = content;
          var div = wrapper.firstChild;
    
          // set some values on the div to style it.
          // normally you do this directly in HTML and 
          // CSS files.
          div.style.width = '370px';
          div.style.height = '370px';
          div.style.opacity = 0.7;
          div.style.background = new THREE.Color(Math.random() * 0xffffff).getStyle();
    
          // create a CSS3Dobject and return it.
          var object = new THREE.CSS3DObject(div);
          return object;
        }
    

    这个函数接受一个 HTML 字符串作为输入,将其转换为 HTML 元素,设置一些 CSS 样式,并使用它作为输入创建一个 THREE.CSS3DObject 对象,并将其添加到场景中。

如果你在这个浏览器中打开这个文件,你会看到类似于我们在本食谱 准备阶段 部分展示的示例。你可以使用这个 HTML 页面和 JavaScript 作为你整个 CSS 3D 渲染器项目的模板。

它是如何工作的...

使用 CSS 3D,你可以对 HTML 元素应用各种变换。例如,你可以使用 transform 属性在轴周围应用特定的旋转。有趣的是,你还可以应用矩阵变换。Three.js 使用矩阵变换在内部定位和旋转元素。通过 THREE.CSS3DRenderer 对象,Three.js 隐藏了内部 CSS 3D 特定的变换和样式,并提供了一个很好的抽象级别,你可以使用它来与 3D 中的 HTML 元素一起工作。

相关内容

  • 如果你可以使用从 使用 WebGL 渲染器入门 菜单中获取的 WebGL 方法,你应该真的使用它。它提供了比基于 CSS 的方法更多的功能,但移动设备支持较少。另一方面,如果你想要操作屏幕上的 HTML 元素,THREE.CSS3DRenderer 对象是一个很好的解决方案。

检测 WebGL 支持

并非所有浏览器都支持 WebGL。当你创建一个使用 THREE.WebGLRenderer 对象的页面时,确保浏览器支持 WebGL 是一个好主意。如果一个浏览器不支持它,这将在 JavaScript 控制台中导致各种奇怪的 JavaScript 错误,并且最终用户将看到一个空白的屏幕。为了确保你的 WebGL 项目按预期工作,我们将在这个菜谱中解释如何在浏览器中检测 WebGL 支持。

准备阶段

在这个菜谱中,作为一个例子,我们将使用 01.04-detect-webgl-support.html 文件,你可以在这个书的源文件中找到它。如果你打开这个文件,如果你的浏览器不支持 WebGL,你会看到以下结果:

准备阶段

让我们来看看创建前面示例的菜谱。

如何做到这一点...

为了检测 WebGL 并创建消息 WebGL is not-supported,我们需要执行以下步骤:

  1. 首先,我们将创建当 WebGL 不支持时的弹出窗口的 CSS。

  2. 然后,我们需要检测浏览器是否支持 WebGL。为此,我们将编写一个返回 true 或 false 值的方法。

  3. 最后,我们将使用上一步的结果来显示弹出窗口或继续执行。

    在下一节中,我们将详细查看这些步骤:

  4. 您需要做的第一件事是设置我们将使用的 CSS:

    <!DOCTYPE html>
    <html>
      <head>
        <style>
          .black_overlay {
            display: none;
            position: absolute;
            top: 0;
            left: 0%;
            width: 100%;
            height: 100%;
            background-color: black;
            z-index: 1001;
            opacity: .80;
          }
    
          .white-content {
            display: none;
            position: absolute;
            top: 25%;
            left: 25%;
            width: 50%;
            height: 70px;
            padding: 16px;
            border: 2px solid grey;
            background-color: black;
            z-index: 1002;
          }
    
          .big-message {
            width: 80%;
            height: auto;
            margin: 0 auto;
            padding: 5px;
            text-align: center;
            color: white;
    
            font-family: serif;
            font-size: 20px;
          }
    
        </style>
        <title></title>
      </head>
      <body>
    

    如您所见,这个 CSS 中没有特别之处。我们在这里要做的唯一一件事是创建一些我们将用于创建弹出消息和隐藏背景的类。接下来,我们将定义用于创建弹出窗口的 HTML。

  5. 下面的代码片段展示了包含信息的 HTML 代码。使用我们之前定义的 CSS,我们可以显示或隐藏这个元素:

        <!-- Lightbox to show when WebGL is supported or not-->
        <div id="lightbox" class="white-content">
        <div class="big-message" id="message">
    
        </div>
        <a href="javascript:void(0)" onclick="hideLightbox()">Close</a>
        </div>
        <div id="fade" class="black_overlay"></div>
    

    如您所见,我们只是创建了一些当前隐藏的 div 元素。当我们检测到 WebGL 不支持时,这两个 div 元素将通过改变它们的可见性来显示。

  6. 接下来,让我们看看需要添加到检测 WebGL 的 JavaScript。我们将为它创建以下函数:

        // loosely based on the http://get.webgl.org function detectWebGL() {
    
          // first create a canvas element
          var testCanvas = document.createElement("canvas");
          // and from that canvas get the webgl context
          var gl = null;
    
          // if exceptions are thrown, indicates webgl is null
          try {
            gl = testCanvas.getContext("webgl");
          } catch (x) {
            gl = null;
          }
    
          // if still null try experimental
          if (gl == null) {
            try {
            gl = testCanvas.getContext("experimental-webgl");
            } catch (x) {
            gl = null;
            }
    
        }
        // if webgl is all good return true;
        if (gl) {
          return true;
        } else {
          return false;
        }
    }
    

    如您所见,我们创建了一个 HTML canvas 元素,然后尝试使用 getContext 函数创建一个 WebGL 上下文。如果失败,gl 变量将被设置为 null,但如果成功,gl 变量将包含 WebGL 上下文。如果 gl 变量不为 null,它将返回 true。另一方面,如果它是 null,它将返回 false。

  7. 现在我们能够检测浏览器是否支持 WebGL,我们将使用这个功能来显示一个弹出窗口。对于这个示例,当 WebGL 支持时,我们也会显示一个弹出窗口:

        var hasGl = detectWebGL();
        if (hasGl) {
          showLightbox("WebGL is supported");
        } else {
        showLightbox("WebGL is not-supported");
        }
    
        function showLightbox(message) {
          var lightBox = document.getElementById('light');
          lightBox.style.display = 'block';
    
          var fadeBox = document.getElementById('fade');
          fadeBox.style.display = 'block'
    
          var msg = document.getElementById('message');
          msg.textContent = message;
        }
    
        function hideLightbox() {
          var lightBox = document.getElementById('light');
          lightBox.style.display = 'none';
    
          var fadeBox = document.getElementById('fade');
          fadeBox.style.display = 'none'
        }
    

这就是本示例的全部内容。如果我们把这个添加到网页中,支持 WebGL 的浏览器将显示一个包含 WebGL is supported 的弹出窗口,如果没有 WebGL 可用,将显示一个包含文本 WebGL isn't supported 的弹出窗口。除了这种方法,您还可以使用 Three.js 提供的检测器对象,在 github.com/mrdoob/three.js/blob/master/examples/js/Detector.js。如果您将此文件包含在您的 JavaScript 中,您可以通过检查 Detector 对象的 webgl 属性来检测 WebGL。

设置动画循环

在本章开头的菜谱中,我们向您展示了如何使用可用的渲染器之一设置基本的 Three.js 场景。如果您想为 Three.js 场景添加动画,例如移动相机或旋转对象,您需要多次调用render函数。在 JavaScript 的旧时代,您必须使用setTimeoutsetIntervalJavaScript 函数自行控制此操作。这些函数的问题在于它们没有考虑到浏览器中的情况。例如,您的页面可能被隐藏,或者 Three.js 场景可能被滚动出视图。对于动画来说,一个更好的解决方案,也是我们在本菜谱中使用的解决方案是requestAnimationFrame。使用此函数,浏览器将确定何时调用动画代码的最佳时机。

准备就绪

对于此菜谱,我们将使用01.05-setup-animation-loop.html示例 HTML 文件。要查看动画效果,只需在浏览器中打开此文件:

准备就绪

此示例使用 WebGL 渲染器。当然,你也可以将此相同的菜谱应用于本章中讨论的其他渲染器。

让我们看看设置此类动画循环所需的步骤。

如何操作...

要创建一个动画循环,你不需要在现有代码中做太多更改:

  1. 让我们先看看如何使用requestAnimationFrame进行渲染。为此,我们创建了一个渲染函数:

        function render() {
          renderer.render(scene, camera);
          scene.getObjectByName('cube').rotation.x += 0.05;
          requestAnimationFrame(render);
        }
    

    如你所见,我们将渲染函数作为参数传递以请求动画帧。这将导致render函数以固定间隔被调用。在render函数中,我们还将更新立方体的x轴旋转,以向你展示场景正在重新渲染。

  2. 要使用本章开头提到的这个功能,我们只需替换这个调用:

        function init() {
          ...
          // call the render function
          renderer.render(scene, camera);
        }
    With the following:
        function init() {
          ...
          // call the render function
          render();
        }
    
  3. 现在,你将拥有自己的动画循环,因此对模型、相机或场景中的其他对象所做的任何更改现在都可以在render()函数内完成。

参见

  • 我们提到,在这个菜谱中,我们使用了THREE.WebGLRenderer对象作为示例。当然,你也可以将此应用于从 Canvas 渲染器入门菜谱或从 CSS 3D 渲染器入门菜谱中的骨骼。

  • 你可能还会对确定场景帧率菜谱感兴趣,我们将在此菜谱中为骨骼添加额外的功能,以便你可以轻松地看到requestAnimationFrame调用渲染函数的频率。

确定场景的帧率

当您创建包含许多对象和动画的大型 Three.js 应用程序时,关注浏览器可以渲染您的场景的帧率是很好的。您可以使用动画循环中的日志语句自己来做这件事,但幸运的是,已经有一个很好的解决方案可用,并且与 Three.js 集成得很好(这并不奇怪,因为它最初是为 Three.js 编写的)。

准备工作

对于这个菜谱,我们将使用可以从其 GitHub 仓库github.com/mrdoob/stats.js/下载的stats.js JavaScript 库。要使用这个库,您必须在 HTML 文件的顶部包含它,如下所示:

    <script src="img/stats.min.js"></script>

我们还为此菜谱提供了一个现成的示例。如果您在浏览器中打开01.06-determine-framerate.html文件,您可以直接看到这个库如何显示当前的帧率,您可以在浏览器的左上角看到它,如下面的截图所示:

准备工作

让我们看看您需要采取的步骤,将此功能添加到您的 Three.js 应用程序中。

如何做到这一点...

将此功能添加到您的场景中只需几个小步骤,具体如下:

  1. 首先,我们必须创建stats对象并定位它。为此,我们创建了一个简单的函数:

        function createStats() {
          var stats = new Stats();
          stats.setMode(0);
    
          stats.domElement.style.position = 'absolute';
          stats.domElement.style.left = '0';
          stats.domElement.style.top = '0';
    
          return stats;
        }
    

    我们通过调用new Stats()来创建统计对象。Stats.js库支持两种不同的模式,我们可以使用setMode函数来设置这些模式。如果我们传递0作为参数,您会看到最后一秒内渲染的帧数,如果我们设置模式为1,我们会看到渲染最后一帧所需的毫秒数。对于这个菜谱,我们想看到帧率,所以我们设置模式为0

  2. 现在我们已经创建了统计对象,我们需要添加在骨架菜谱中看到的init方法:

        // global variables
        var renderer;
        var scene;
        var camera;
     var stats;
    
        function init() {
          ...
     stats = createStats();
     document.body.appendChild( stats.domElement );
    
          // call the render function
          render();
        }
    

    正如您所看到的,我们创建了一个名为stats的新全局变量,我们将使用它来访问我们的统计对象。在init方法中,我们使用我们刚刚创建的函数,并将stats对象添加到我们的 HTML 主体中。

  3. 我们几乎完成了。我们现在唯一需要做的是确保在渲染函数被调用时更新stats对象。这样,stats对象就可以计算帧率或运行渲染函数所需的时间:

        function render() {
          requestAnimationFrame(render);
    
          scene.getObjectByName('cube').rotation.x+=0.05;
          renderer.render(scene, camera);
     stats.update();
        }
    

它是如何工作的...

我们提到Stats.js提供了两种模式。它要么显示帧率,要么显示渲染最后一帧所需的时间。Stats.js库通过简单地跟踪调用之间的时间差及其update函数来工作。如果您正在监控帧率,它会计算在最后一秒内更新被调用的频率,并显示该值。如果您正在监控渲染时间,它只显示调用之间的时间和update函数。

控制场景中使用的变量

当您开发和编写 JavaScript 时,您通常需要调整一些变量以获得最佳的视觉效果。您可能需要更改球体的颜色,更改动画的速度,或者对更复杂的材质属性进行实验。您可以简单地更改源代码并重新加载 HTML,但这变得繁琐且耗时。在这个菜谱中,我们将向您展示一种快速且轻松地控制 Three.js 场景中变量的替代方法。

准备工作

对于这个菜谱,我们还需要一个名为dat.gui的外部 JavaScript 库。您可以从code.google.com/p/dat-gui/下载最新版本,或者查看本书提供的源代码的libs目录。要使用这个库,您首先需要在 HTML 文件顶部包含它:

    <script src="img/dat.gui.min.js"></script>

在本章的源文件夹中,还有一个现成的示例,我们将在以下几节中解释。当您打开01.07-control-variables.html文件时,您将看到以下内容:

准备工作

正如您在前一个屏幕截图中所见,右上角有一个菜单,您可以使用它来控制立方体的旋转速度和缩放。

如何操作...

要为自己使用这个库,您只需要做几件小事:

  1. 您首先需要定义一个包含您想要控制的属性的 JavaScript 对象。在这种情况下,您需要将其添加到init函数中,并创建一个名为control的新全局 JavaScript 变量:

        ...
        var control;
        function init() {
          ...
    
          control = new function() {
            this.rotationSpeed = 0.005;
            this.scale = 1;
          };
          addControls(control);
    
          // call the render function
          render();
        }
    
  2. 上一段代码中的控制对象包含两个属性:rotationSpeedscale。在addControls函数中,我们将创建前一个屏幕截图所示的 UI 组件:

        function addControls(controlObject) {
          var gui = new dat.GUI();
          gui.add(controlObject, 'rotationSpeed', -0.1, 0.1);
          gui.add(controlObject, 'scale', 0.01, 2);
        }
    

    在这个addControls函数中,我们使用提供的包含rotationSpeedscale属性的参数来创建控制 GUI。对于每个变量,我们指定四个参数:

    1. 对象:第一个参数是包含变量的 JavaScript 对象。在我们的例子中,它是传递给addControls函数的对象。

    2. 名称:第二个参数是我们想要添加的变量的名称。这应该指向第一个参数中提供的对象中可用的一个变量(或函数)。

    3. 最小值:第三个参数是 GUI 中应显示的最小值。

    4. 最大值:最后一个参数指定应显示的最大值。

  3. 到目前为止,我们已经得到了一个可以用来控制变量的 GUI,正如您在以下屏幕截图中所见:如何操作...

    我们现在唯一需要做的是确保我们在渲染循环中更新我们的对象,该循环基于 GUI 中的变量。我们可以在render函数中轻松地做到这一点,如下所示:

        function render() {
          renderer.render(scene, camera);
          scene.getObjectByName('cube').rotation.x+= control.rotationSpeed;
          scene.getObjectByName('cube').scale.set (control.scale,
            control.scale,
            control.scale);
          requestAnimationFrame(render);
        }
    

还有更多...

在这个配方中,我们只是使用了dat.gui来更改数值。dat.gui库还允许你添加其他类型值的控件,如下所示:

  • 如果你添加的变量是布尔值,将显示一个复选框

  • 如果变量是字符串,你可以添加一个有效值的数组

  • 如果变量是颜色,你可以使用添加颜色来创建颜色选择器

  • 如果变量是函数,你会得到一个触发所选函数的按钮

此外,你还可以添加不同类型的事件监听器,当dat.gui管理的值发生变化时触发自定义回调。有关更多信息,请参阅workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage上的dat.gui库文档。

使用 Python 设置本地 Web 服务器

测试你的 Three.js 应用程序,或者任何 JavaScript 应用程序,最好的方法是在本地 Web 服务器上运行它。这样,你可以最好地展示你的用户最终将如何看到你的 Three.js 可视化。在本章中,我们将向您展示三种不同的方法,您可以在本地运行 Web 服务器。设置本地 Web 服务器的三种不同方法是:

  • 一种方法是使用基于 Python 的方法,如果你已经安装了 Python,你可以使用

  • 另一种方法是如果你使用 Node.js 或者已经尝试过 Node.js,你可以使用npm命令安装一个简单的 Web 服务器

  • 第三种选择是,如果你不想使用npm命令或 Python,你也可以使用Mongoose,这是一个简单的便携式 Web 服务器,可在 OS X 和 Windows 上运行

本配方将重点关注基于 Python 的方法(第一个项目符号)。

准备工作

如果你已经安装了 Python,你可以非常容易地运行一个简单的 Web 服务器。首先,你需要检查你是否已经安装了 Python。最简单的方法是在控制台中输入python并按回车。如果你看到以下输出,你就可以开始了:

> python
Python 2.7.3 (default, Apr 10 2013, 05:09:49) 
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>

如何操作...

  1. 一旦安装了 Python (python.org),你只需执行以下 Python 命令即可运行一个简单的 Web 服务器。你需要在这个目录中执行此操作,即你想要托管文件的目录:

    > python -m SimpleHTTPServer
    
    
  2. 以下输出显示了在端口 8000 上运行的 Web 服务器:

    Serving HTTP on 0.0.0.0 port 8000...
    
    

如果你没有安装 Python,请查看以下两个配方中的替代选项。

使用 Node.js 设置本地 Web 服务器

如果你想要测试你的 Three.js 应用程序,那么根据如何使用 Python 设置本地 Web 服务器配方中的描述,你可以以三种不同的方式运行它。本配方将重点关注 Node.js 方法。

准备工作

要使用 Node.js 运行本地网络服务器(nodejs.org),我们首先必须检查我们是否已经安装了 npm(节点包管理器,与 Node.js 一起安装)。你可以通过在命令行中运行 npm 命令来检查:

> npm

如果输出类似于以下代码片段,则表示你已经安装了 npm,并且可以开始食谱:

Usage: npm <command>
where ...

如何操作...

  1. 你可以使用它来运行一个简单的网络服务器:

    Usage: npm <command>...
    
    
  2. 现在,你准备好通过运行以下命令来安装网络服务器:

    > npm install -g http-server
    
    
  3. 最后,你可以通过在命令行中运行 http-server 来启动网络服务器:

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

在下一节中提供了一个运行你自己的网络服务器的最终食谱。在该食谱中,你不需要安装 Python 或 Node.js,但我们将向你展示如何下载一个可携带的网络服务器,你可以在不进行任何安装的情况下运行它。

使用 Mongoose 设置本地网络服务器

如果你想要测试你的 Three.js 应用程序,那么如 如何使用 Python 设置本地网络服务器 食谱中所述,你可以以三种不同的方式运行它。如果前两种方法失败,你始终可以使用一个简单的 Mongoose 可携带网络服务器。这个食谱将专注于 Mongoose 方法。

准备工作

在运行 Mongoose 之前,你首先必须下载它。你可以从 code.google.com/p/mongoose/downloads/list 下载 Mongoose。

如何操作...

  1. 你所使用的平台将影响你运行 Mongoose 的方式。如果你正在使用 Windows,你可以直接将下载的文件(一个可执行文件)复制到你想要托管文件的文件夹中(例如,你提取本书源代码的目录),然后双击可执行文件以在 8080 端口启动网络服务器。

  2. 对于 Linux 或 OS X 平台,你还需要在包含你的文件的目录中有一个下载文件的副本,但你必须从命令行启动 Mongoose。

参见

  • 如果你无法安装本地网络服务器,你可以查看 解决 Chrome 中的跨域错误信息 食谱。这个食谱为你提供了一个运行更高级 Three.js 示例的替代方法。

解决 Chrome 中的跨域错误信息

当你开发 Three.js 应用程序时,测试你的应用程序最简单的方法就是直接在浏览器中打开文件。对于许多场景,这将会工作,直到你开始加载纹理和模型。如果你尝试这样做,你会看到一个类似以下的错误:

解决 Chrome 中的跨域错误信息

您可以通过将 01.09-solve-cross-origin-issues.html 拖动到浏览器中来轻松地重现此错误,错误信息中会有 跨域SecurityError 等术语。这个错误意味着浏览器阻止当前页面从不同域加载资源。这是避免恶意网站访问个人信息的一个必要功能。然而,在开发过程中,这可能会有些不便。在本配方中,我们将向您展示如何通过调整浏览器的安全设置来绕过这类错误。

我们将探讨如何禁用对支持 WebGL 最好的两个浏览器的安全检查:Chrome 和 Firefox。在本配方中,我们将介绍如何在 Chrome 中实现这一点,而在下一个配方中,我们将向您展示如何在 Firefox 中实现这一点。然而,在继续配方之前,有一个重要的注意事项。如果您能的话,请运行一个本地 web 服务器。它要安全得多,并且不会导致您的浏览器设置安全级别过低。

如何操作...

  1. 在 Chrome 安装完成后,我们还需要在 Chrome 中禁用安全设置,这需要传递一个命令行参数。然而,每个操作系统执行此操作的方式略有不同:

    • 对于 Windows,您需要在命令行中调用以下内容:

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

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

      open -a Google\ Chrome --args --disable-web-security
      
      
  2. 以这种方式启动 Chrome 后,即使是直接从文件系统运行它,也能正确加载资源,给您以下结果:如何操作...

  3. 请记住,在您完成使用 Three.js 进行实验或开发后,正常重启浏览器,因为您已经降低了浏览器的安全设置。

  4. 对于 Firefox 用户,我们将在下面的配方中解释如何解决此浏览器的跨域问题。

解决 Firefox 中的跨域域错误信息

在上一个配方中,我们解释了当您从文件系统运行 Three.js 应用程序时,可能会发生跨域错误信息。在本配方中,我们向您展示了如何在 Chrome 中解决这类问题。在本配方中,我们将探讨如何在另一款流行的浏览器:Firefox 中解决这些问题。

如何操作...

  1. 对于 Firefox,我们接下来需要直接从浏览器中禁用安全设置。如果您在地址栏中输入 about:config,您将看到以下内容:如何操作...

  2. 在此屏幕上,只需单击 我会小心,我保证 按钮。这将带您到一个概述页面,显示 Firefox 中所有可用的内部属性。

  3. 接着,在此屏幕上的搜索框中输入 security.fileuri.strict_origin_policy 并更改其值,如图所示:如何操作...

  4. 现在,当您直接在浏览器中打开文件时,即使是通过异步加载器加载的资源也将正常工作。

  5. 请记住,在完成实验或使用 Three.js 进行开发后,将这些设置改回原来的状态,因为你已经降低了浏览器的安全设置。

它是如何工作的...

我们必须设置这些属性的原因是,默认情况下,现代浏览器会检查你是否被允许从运行域之外请求资源。当你使用 Three.js 加载模型或纹理时,它使用 XMLHTTPRequest 对象来访问该资源。浏览器将检查正确头部的可用性,由于你从本地系统请求资源,而本地系统不提供正确的头部,因此会发生错误。即使在这个食谱中,你可以绕过这个限制,但最好始终使用本地 Web 服务器进行测试,因为这最接近你的用户在线访问的方式。

更多关于 CORS 的信息,请参阅 www.w3.org/TR/cors/

参见

  • 正如我们在上一节中提到的,处理这类错误的一个更好的方法是设置一个本地 Web 服务器。"使用 Python 设置本地 Web 服务器"食谱解释了如何完成这项工作。

添加键盘控制

如果你想要创建游戏或更高级的 3D 场景,你通常需要一个方法来使用键盘控制来控制场景中的元素。例如,你可能制作一个平台游戏,用户可以使用键盘上的箭头在游戏中移动。Three.js 本身并不提供处理键盘事件的具体功能,因为它非常容易将标准的 HTML JavaScript 事件处理连接到 Three.js。

准备工作

对于这个食谱,我们包含了一个示例,你可以使用键盘上的箭头旋转一个立方体,使其围绕其 xz 轴旋转。如果你首先在浏览器中打开示例 01.10-keyboard-controls.html,你会看到一个简单的立方体:

准备工作

使用键盘上的上、下、左和右箭头,你可以旋转这个立方体。打开这个文件后,你现在就可以开始操作了。

如何操作...

在你的浏览器中添加键盘支持非常简单;你所要做的就是将事件处理器分配给 document.onkeydown

  1. 要做到这一点,我们需要将一个函数分配给 document.onkeydown 对象。这个函数将在按下任何键时被调用。以下代码,在 setupKeyControls 函数中封装,注册了这个监听器:

        function setupKeyControls() {
          var cube = scene.getObjectByName('cube');
          document.onkeydown = function(e) {
            switch (e.keyCode) {
              case 37:
              cube.rotation.x += 0.1;
              break;
              case 38:
              cube.rotation.z -= 0.1;
              break;
              case 39:
              cube.rotation.x -= 0.1;
              break;
              case 40:
              cube.rotation.z += 0.1;
              break;
            }
          };
        }
    
  2. 在这个函数中,我们使用传递的事件 e 中的 keyCode 属性来确定要做什么。在这个例子中,如果用户按下对应于键码 37 的左箭头键,我们就会改变场景中 Three.js 对象的 rotation.x 属性。我们将同样的原则应用于上箭头键(38)、右箭头(39)和下箭头(40)。

它是如何工作的...

使用事件处理器是标准的 HTML JavaScript 机制,它们是 DOM API 的一部分。此 API 允许您为所有不同的事件注册函数。每当发生特定事件时,提供的函数就会被调用。在这个菜谱中,我们选择使用KeyDown事件。当用户按下键时,此事件被触发。还有一个KeyUp事件可供使用,它在用户释放键时触发,使用哪个取决于您的用例。请注意,还有一个KeyPress事件可供使用。然而,此事件旨在与字符一起使用,并且不会注册任何非字符键的按下。

还有更多...

在这个菜谱中,我们只展示了箭头的键码值。当然,每个键盘上的键都有一个单独的键码。关于各种键如何映射的详细解释(特别是特殊键,如功能键),可以在unixpapa.com/js/key.html找到。如果您想了解特定键的键值,而且不想在列表中查找值,您也可以使用以下简单的处理器将键码输出到 JavaScript 控制台:

    function setupKeyLogger() {
      document.onkeydown = function(e) {
        console.log(e);
      }
    }

这个小处理器记录了完整的事件。然后,在控制台的输出中,您可以查看所使用的键码,如下面的截图所示:

还有更多...

如您所见,您还可以看到很多其他有趣的信息。例如,您可以看到在事件发生的同时是否也按下了ShiftAlt键。

参见

异步加载纹理

当您创建 Three.js 场景时,您通常需要加载资源。您可能需要为您的对象加载纹理,您可能有一些外部模型想要包含在您的场景中,或者可能有一些 CSV 数据,您想要将其用作可视化的输入。Three.js 提供了一系列不同的异步加载这些资源的方法,我们将在本菜谱和接下来的菜谱中探讨。

要运行这些菜谱并进行实验,我们在本章的源文件夹中包含了一个简单的示例,展示了这种加载的实际操作。如果您在浏览器中打开示例01.11-load-async-resources.html,并打开 JavaScript 控制台,您将看到加载资源的进度和结果。

请注意,由于我们直接从浏览器中加载文件,您需要安装本地 Web 服务器(请参阅 使用 Python 设置本地 Web 服务器 食谱或 使用 Node.js 设置本地 Web 服务器 食谱),或者禁用 解决 Chrome 中的跨源域错误消息 食谱或 解决 Firefox 中的跨源域错误消息 食谱中解释的一些安全检查。

异步加载纹理

在这五个食谱中的第一个,我们将向您展示如何使用 Three.js 异步加载纹理。

准备中

在查看本食谱中的步骤之前,您需要创建一些标准回调,这些回调可以被所有不同的加载器使用。这些回调用于通知您资源何时加载完成、加载失败,以及如果有的话,当前请求的进度。

因此,对于加载资源,我们需要定义三个不同的回调:

  • onload 回调:每当资源加载完成时,此回调将带加载的资源作为参数被调用。

  • onprogress 回调:一些加载器在加载资源时会提供进度信息。在特定的时间间隔内,此回调将被调用以通知您已加载了多少资源。

  • onerror 回调:如果在加载资源的过程中出现问题,此回调将用来通知您发生的错误。

对于所有处理异步加载的食谱,我们将使用相同的加载器集。这些加载器只是向控制台输出一些信息,但当然,您可以根据特定的用例自定义这些回调。

首先,我们定义 onLoadCallback 函数,它在资源加载时被调用:

    function onLoadCallback(loaded) {
      // just output the length for arrays and binary blobs
      if (loaded.length) {
        console.log("Loaded", loaded.length);
      } else {
        console.log("Loaded", loaded);
      }
    }

如从函数定义中可以看出,我们只是将传递的参数输出到控制台。其他两个回调,onProgressCallbackonErrorCallback,与它们所展示的方式完全相同:

    function onProgressCallback(progress) {
      console.log("Progress", progress);
    }

    function onErrorCallback(error) {
      console.log("Error", error)
    }

注意

在以下章节和食谱中,当我们使用 Three.js 提供的功能来加载资源时,我们将引用这些回调。

如何操作...

  1. 要异步加载纹理,我们使用 THREE.ImageUtils 中的 loadTexture 函数:

        function loadTexture(texture) {
          var texture = THREE.ImageUtils.loadTexture(textureURL, null, onLoadCallback, onErrorCallback);
          console.log("texture after loadTexture call", texture);
        }
    
  2. THREE.ImageUtils 中的 loadTexture 函数接受以下四个参数:

    • 第一个参数指向您想要加载的图像位置

    • 第二个参数可以用来提供自定义的 UV 映射(UV 映射用于确定将纹理的哪个部分应用到特定的面上)

    • 第三个参数是纹理加载完成后要使用的回调

    • 最后一个参数是在发生错误时要使用的回调

    如何操作...

  3. 注意,第一个控制台输出还显示了一个有效的纹理对象。Three.js 会这样做,因此您可以立即将此对象作为纹理分配给材质。然而,纹理内部的实际图像只有在 onLoadCallback 函数被调用后才会加载。

它是如何工作的...

Three.js 提供了一个很好的包装器来加载纹理。内部,Three.js 使用从 XMLHTTPRequest 网页加载资源的标准方式。使用 XMLHTTPRequest 网页,你可以为特定资源发出 HTTP 请求并处理结果。如果你不想使用 Three.js 提供的功能,你也可以自己实现一个 XMLHTTPRequest 函数。

参见

  • 要运行这些示例并异步加载资源,我们需要运行一个本地 Web 服务器,如 使用 Python 设置本地 Web 服务器 食谱或 使用 Node.js 设置 Web 服务器 食谱中所述,或者禁用一些安全设置,如 解决 Chrome 中的跨源域错误消息 食谱或 解决 Firefox 中的跨源域错误消息 食谱中所述。

  • 或者,如果你不想异步加载资源,而是等待所有资源加载完毕后再初始化场景,你可以查看下一个 等待资源加载完成 食谱。

异步加载模型

加载纹理异步 食谱中,我们解释了 Three.js 提供了辅助函数来异步加载不同类型的资源。在这个食谱中,我们将探讨如何使用 THREE.JSONLoader 对象异步加载模型。

准备工作

在你开始这个食谱之前,请确保你已经走过了在 加载纹理异步 食谱的 准备就绪 部分中解释的步骤。在下一节中,我们将参考该食谱 准备就绪 部分中定义的 JavaScript 回调。

如何操作...

  1. Three.js 还允许你轻松地加载外部模型。以下函数展示了如何为 Three.js 使用的 JSON 模型执行此操作。然而,同样的方法也适用于任何其他模型加载器:

        function loadModel(modelUrl) {
          var jsonLoader = new THREE.JSONLoader();
          jsonLoader.load(modelUrl, onLoadCallback, null);
        }
    
  2. jsonLoader.load 函数接受以下三个参数:

    • 第一个是你要加载的模型的位置

    • 第二个是当模型成功加载时调用的回调函数

    • 最后一个参数是我们可以指定从哪里加载纹理图像的路径

  3. 当我们调用此函数时,你将在控制台看到以下输出:如何操作...

还有更多...

采用这种方法,JSONLoader 对象不会提供任何关于它加载了多少的反馈。如果你想要加载大型模型,了解进度情况会很好。JSONLoader 对象还提供了一种加载模型的方法,该方法也提供了进度信息。在 异步加载模型并显示进度 菜谱中,我们展示了如何加载模型并提供进度反馈。除了加载 Three.js 自有的专有模型外,Three.js 还附带了许多可以用于其他模型格式的加载器。有关 Three.js 提供的内容概述,请参阅 github.com/mrdoob/three.js/tree/master/examples/js/loaders

异步加载模型并显示进度

在前面的 异步加载模型 菜谱中,我们加载了一个模型,但没有提供进度反馈。在这个菜谱中,我们将解释如何向该场景添加进度反馈。

开始

在开始这个菜谱之前,请确保你已经按照 准备就绪 部分中 异步加载纹理 菜谱中解释的步骤进行了操作。在接下来的部分中,我们将参考该菜谱 准备就绪 部分中定义的 JavaScript 回调函数。

如何操作...

  1. 为了加载模型并显示进度,我们必须使用除 THREE.JSONLoader 之外的其他方法。如果我们使用 loadAjaxJSON 函数,我们也可以指定一个进度回调而不是仅仅加载回调:

        function loadModelWithProgress(model) {
          var jsonLoader = new THREE.JSONLoader();
          jsonLoader.loadAjaxJSON(jsonLoader, model, onLoadCallback, null, onProgressCallback);
        }
    
  2. 如果我们现在加载与之前相同的模型,我们会看到以下加载进度:如何操作...

异步加载其他资源并显示进度

除了加载特定资源外,Three.js 还提供了一个简单的辅助对象来异步加载任何类型的资源。在这个菜谱中,我们将解释如何使用 THREE.XHRLoader 对象异步加载任何类型的资源。

准备就绪

在开始这个菜谱之前,请确保你已经按照 准备就绪 部分中 异步加载纹理 菜谱中解释的步骤进行了操作。在接下来的部分中,我们将参考该菜谱 准备就绪 部分中定义的 JavaScript 回调函数。

如何操作...

  1. 在这个菜谱中,我们想要展示的最后一个资源加载器是 THREE.XHRLoader 对象。这个加载器允许你在 Three.js 场景中加载任何你可能需要的资源:

        function loadOthers(res) {
          var xhrLoader = new THREE.XHRLoader();
          xhrLoader.load(res, onLoadCallback, onProgressCallback, onErrorCallback);
        }
    
  2. XHRLoader.load 函数的参数现在应该看起来很熟悉,因为它几乎与其他加载器相同。首先,我们传递我们想要加载的资源的位置,然后我们指定各种回调函数。这个函数的输出如下:如何操作...

    在前面的屏幕截图中,你还可以看到资源正在加载时的进度。

等待资源加载

异步加载资源 的菜谱中,我们展示了如何异步加载外部 Three.js 资源。对于许多网站和可视化,异步加载资源是一种很好的方法。有时,您可能想要确保场景中所需的所有资源都已在之前加载。例如,当您创建游戏时,您可能希望在创建特定关卡之前加载所有数据。同步加载资源的一种常见方法是将之前菜谱中看到的异步回调嵌套。然而,这很快就会变得难以阅读,并且很难管理。在这个菜谱中,我们将使用不同的方法,并使用一个名为 Q 的 JavaScript 库。

准备就绪

对于我们使用的所有外部库,我们需要在我们的 HTML 中包含 Q 库。您可以从其 GitHub 仓库 github.com/kriskowal/q 下载此库的最新版本,或者使用本书源代码中的 libs 文件夹中提供的版本。要将此库包含到您的 HTML 页面中,请在您的 HTML 页面的 head 元素中添加以下内容:

    <script src="img/q.js"></script>

在本章的源代码中,您还可以找到一个示例,展示了如何同步加载资源。在您的浏览器中打开 01.12-wait-for-resources.html 并打开 JavaScript 控制台:

准备就绪

在控制台输出中,您将看到所需资源和模型一个接一个地加载。

如何操作...

  1. 让我们先看看在这个菜谱中我们想要达到的目标。我们希望使用 Q 库同步加载资源,如下所示:

        loadModel(model)
          .then(function(result) {return loadTexture(texture)})
          .then(function(result) {return loadModel(m)})
          .then(function(result) {return loadTexture(texture)})
          .then(function(result) {return loadOthers(resource)})
          .then(function(result) {return loadModelWithProgress(m)})
          .then(function(result) {return loadModel(model)})
          .then(function(result) {return loadOthers(resource)})
          .then(function(result) {return loadModel(model)})
          .then(function() {console.log("All done with sequence")})
          .catch(function(error) {
            console.log("Error occurred in sequence:",error);
          })
          .progress(function(e){
            console.log("Progress event received:", e);
           });
    
  2. 这段代码的含义是:

    1. 首先,我们需要调用 loadModel(model)

    2. 模型加载完成后,我们使用 then 函数和 loadTexture(texture) 函数加载一个纹理。一旦这个纹理加载完成,我们将加载下一个资源,依此类推。在这段代码片段中,您还可以看到我们调用了 catchprogress 函数。如果在加载过程中发生错误,提供给 catch() 的函数将被调用。对于 progress() 也是如此。如果其中一个方法想要提供有关其进度的信息,传递给 progress() 的函数将被调用。

    3. 然而,您会发现这不会与之前菜谱中的函数一起工作。为了使其正常工作,我们必须将这些函数的回调替换为 Q 构造函数中的一个特殊函数,称为延迟函数:

          function loadTexture(texture) {
      
            var deferred = Q.defer();
            var text = THREE.ImageUtils.loadTexture
            (texture, null, function(loaded) {
              console.log("Loaded texture: ", texture);
              deferred.resolve(loaded);
            }, function(error) {
              deferred.reject(error);
            });
      
            return deferred.promise;
          }
      
    4. 在这个代码片段中,我们创建了一个名为deferred的新 JavaScript 对象。deferred对象将确保回调的结果,这次定义为匿名函数,以这种方式返回,这样我们就可以使用我们在本章开头看到的then函数。如果资源加载成功,我们使用deferred.resolve函数来存储结果;如果资源加载失败,我们使用deferred.reject函数来存储错误。

    5. 我们对loadModelloadOthersloadModelWithProgress函数也采用了相同的方法:

          function loadModel(model) {
      
            var deferred = Q.defer();
            var jsonLoader = new THREE.JSONLoader();
            jsonLoader.load(model, function(loaded) {
              console.log("Loaded model: ", model);
              deferred.resolve(loaded);
            }, null);
      
            return deferred.promise;
          }
      
          function loadOthers(res) {
            var deferred = Q.defer();
      
            var xhrLoader = new THREE.XHRLoader();
            xhrLoader.load(res, function(loaded) {
              console.log("Loaded other: ", res);
              deferred.resolve(loaded);
            }, function(progress) {
              deferred.notify(progress);
            }, function(error) {
              deferred.reject(error);
            });
      
            return deferred.promise;
          }
      
    6. loadOthers函数中,我们也提供了进度信息。为了确保进度回调被正确处理,我们使用deferred.notify()函数并传入progress对象:

          function loadModelWithProgress(model) {
            var deferred = Q.defer();
      
            var jsonLoader = new THREE.JSONLoader();
            jsonLoader.loadAjaxJSON(jsonLoader, model,
            function(model) {
              console.log("Loaded model with progress: ", model);
              deferred.resolve(model)
            }, null,
            function(progress) {
              deferred.notify(progress)
            });
      
            return deferred.promise;
          }
      
    7. 通过这些更改,我们现在可以同步地加载资源。

它是如何工作的...

要理解这是如何工作的,你必须了解 Q 的作用。Q 是一个承诺库。使用承诺,你可以用简单的步骤替换嵌套的回调(也称为calculist.org/blog/2011/12/14/why-coroutines-wont-work-on-the-web/中的“末日金字塔”)。以下 Q 网站的示例很好地展示了这是如何实现的:

step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

使用承诺,我们可以将其简化为以下内容(就像我们在菜谱中做的那样):

Q.fcall(promisedStep1)
then(promisedStep2)
then(promisedStep3)
then(promisedStep4)
then(function (value4) {
  // Do something with value4
})
catch(function (error) {
  // Handle any error from all above steps
})
done();

如果我们要重写 Three.js 库,我们可以在 Three.js 内部使用承诺,但由于 Three.js 已经使用了回调,我们不得不使用 Q 提供的Q.defer()函数将这些回调转换为承诺。

还有更多...

我们只接触了 Q 承诺库可能实现的一小部分。我们用它来同步加载,但 Q 还有许多其他有用的功能。一个非常好的起点是可在github.com/kriskowal/q/wiki找到的 Q 维基百科。

参见

  • 就像每个加载资源的菜谱一样,你必须确保你使用本地 Web 服务器运行它,参见使用 Python 设置本地 Web 服务器菜谱或使用 Node.js 设置 Web 服务器菜谱,或者禁用一些安全设置(参见解决 Chrome 中的跨源域错误消息菜谱或解决 Firefox 中的跨源域错误消息菜谱)。如果你想异步加载资源,你可以查看异步加载任何资源菜谱。

将文件从桌面拖动到场景中

当你创建可视化时,允许你的用户提供他们自己的资源是一个很好的特性。例如,你可能想让用户指定他们自己的纹理或模型。你可以通过传统的上传表单来实现这一点,但使用 HTML5,你也有选项让用户直接从桌面拖放资源。在这个菜谱中,我们将解释如何向用户提供这种拖放功能。

准备中

准备这个食谱的最简单方法是先查看我们为您创建的示例。在您的浏览器中打开示例01.14-drag-file-to-scene.html

注意

请注意,这仅在运行自己的 Web 服务器或禁用安全异常时才有效。

准备中

当您将图像文件拖放到掉落区域(虚线方块)时,您会立即看到旋转盒子的纹理发生了变化,并且您提供的图像被使用。

在接下来的部分中,我们将解释如何创建此功能。

如何操作...

要完成此操作,请执行以下步骤:

  1. 首先,我们必须设置正确的 CSS 并定义掉落区域。要创建虚线掉落区域,我们将以下 CSS 添加到页面head元素中的style元素:

        #holder { border: 10px dashed #ccc; 
        width: 150px; height: 150px; 
        margin: 20px auto;}
        #holder.hover { border: 10px dashed #333; #333}
    

    正如您在这段 CSS 中看到的,我们使用 ID 为holder的 HTML 元素来设置虚线边框。下面展示了holder div元素的 HTML 代码:

      <body>
        <div id="holder"></div>
      </body>
    

    已经定义了掉落区域,所以下一步是向其添加拖放功能。

  2. 然后,我们必须分配正确的事件处理器,以便我们可以响应各种拖放相关事件。

  3. 就像我们之前的食谱一样,我们定义了一个包含所有必要逻辑的函数:

        function setupDragDrop() {
          var holder = document.getElementById('holder');
    
          holder.ondragover = function() {
            this.className = 'hover';
            return false;
          };
    
          holder.ondragend = function() {
            this.className = '';
            return false;
          };
    
          holder.ondrop = function(e) {
            ...
          }
        }
    

    在这个代码片段中,我们定义了三个事件处理器。holder.ondragover事件处理器将 div 元素的类设置为'hover'。这样,用户就可以看到他们可以在此处掉落文件。holder.ondragend事件处理器在用户从掉落区域移开时被调用。在事件处理器中,我们移除div元素的类。最后,如果用户在指定区域掉落文件,将调用holder.ondrop函数,我们使用它来处理掉落的图像。

  4. 最后一步是处理掉落的资源并更新盒子的材质。当用户掉落一个文件时,以下代码块被执行:

          this.className = '';
          e.preventDefault();
    
          var file = e.dataTransfer.files[0],
          var reader = new FileReader();
          reader.onload = function(event) {
            holder.style.background = 
            'url(' + event.target.result + ') no-repeat center';
    
            var image = document.createElement('img');
            image.src = event.target.result;
            var texture = new THREE.Texture(image);
            texture.needsUpdate = true;
    
            scene.getObjectByName('cube').material.map = texture;
          };
          reader.readAsDataURL(file);
          return false;
    

    发生的第一件事是我们调用e.preventDefault()。我们需要这样做以确保浏览器不会仅仅显示文件,因为这是它的正常行为。接下来,我们查看事件并使用e.dataTransfer.files[0]检索掉落的文件。由于 Three.js 不能直接与这些文件工作,所以我们必须将其转换为img元素。为此,我们使用FileReader对象。当读取器加载完成后,我们使用内容来创建这个img元素。然后,这个元素被用来创建THREE.Texture对象,我们将它设置为盒子的材质。

    如何操作...

工作原理...

拖放功能不是 Three.js 默认支持的功能。正如我们在上一节中看到的,我们使用标准的 HTML5 拖放相关事件。关于可用的哪些事件的良好概述可以在官方 HTML5 文档中找到,网址为www.w3.org/TR/html5/editing.html#drag-and-drop-processing-model

值得注意的是,在ondrop事件处理程序中添加了texture.needsUpdate = true。我们需要设置纹理的此属性的原因是通知 Three.js 我们的纹理已更改。这是必需的,因为 WebGL 和 Three.js 出于性能原因都会缓存纹理。如果我们更改纹理,我们必须将此属性设置为true,以确保 WebGL 知道如何渲染。

第二章. 几何体和网格

在本章中,我们将涵盖以下菜谱:

  • 在对象自身轴上旋转对象

  • 在空间中围绕一个点旋转一个对象

  • 通知 Three.js 关于更新

  • 与大量对象一起工作

  • 从高度图创建几何体

  • 将一个对象指向另一个对象

  • 在 3D 中写入文本

  • 将 3D 公式渲染为 3D 几何体

  • 使用自定义几何体对象扩展 Three.js

  • 在两点之间创建样条曲线

  • 从 Blender 创建和导出模型

  • 使用 OBJMTLLoader 与多个材质

  • 应用矩阵变换

简介

Three.js 附带了许多您可以直接使用的几何体。在本章中,我们将向您展示一些菜谱,解释您如何转换这些标准几何体。除此之外,我们还将向您展示如何创建您自己的自定义几何体以及从外部源加载几何体。

注意

您可以从 GitHub 仓库github.com/josdirksen/threejs-cookbook访问本食谱中所有菜谱内的所有示例代码。

在对象自身轴上旋转对象

你可以通过许多方式改变网格的外观。例如,你可以改变其位置、缩放或材质。通常,你还需要改变THREE.Mesh的旋转。在本章关于旋转的第一个菜谱中,我们将向您展示旋转任意网格的最简单方法。

准备工作

要旋转网格,我们首先需要创建一个包含您可以旋转的对象的场景。对于这个菜谱,我们提供了一个示例,02.01-rotate-around-axis.html,您可以在浏览器中打开。当您打开这个菜谱时,您将在浏览器中看到以下截图类似的内容:

准备工作

在这个演示中,您可以看到一个 3D 立方体在其轴上缓慢旋转。使用右上角的控制 GUI,您可以改变对象旋转的速度。

如何做到...

要像我们在上一张截图中所展示的那样,围绕轴旋转这个示例中的立方体,您必须采取几个步骤:

  1. 在本菜谱的第一步中,我们将设置控制 GUI,正如我们在第一章中所示,在控制场景中使用的变量菜谱中,您可以在右上角看到。这次,我们将使用以下作为控制对象:

      control = new function() {
        this.rotationSpeedX = 0.001;
        this.rotationSpeedY = 0.001;
        this.rotationSpeedZ = 0.001;
      };
    

    使用这个control对象,我们将控制围绕三个轴中的任何一个轴的旋转。我们将这个控制对象传递给addControls函数:

      function addControls(controlObject) {
        var gui = new dat.GUI();
        gui.add(controlObject, 'rotationSpeedX', -0.2, 0.2);
        gui.add(controlObject, 'rotationSpeedY', -0.2, 0.2);
        gui.add(controlObject, 'rotationSpeedZ', -0.2, 0.2);
      }
    

    现在我们调用addControls函数时,我们将得到您在菜谱开头截图中所看到的漂亮的 GUI。

  2. 现在我们可以通过 GUI 控制旋转,我们可以使用这些值来直接设置我们对象的旋转。在这个例子中,我们持续更新网格的rotation属性,所以你可以看到示例中的动画。为此,我们定义render函数如下:

      function render() {
        var cube = scene.getObjectByName('cube');
        cube.rotation.x += control.rotationSpeedX;
        cube.rotation.y += control.rotationSpeedY;
        cube.rotation.z += control.rotationSpeedZ;
        renderer.render(scene, camera);
        requestAnimationFrame(render); 
      }
    

    在这个函数中,你可以看到我们通过控制 GUI 中设置的值来增加THREE.Mesh对象的rotation属性。这导致了你在准备就绪部分中看到的动画。请注意,旋转属性是THREE.Vector3类型。这意味着你也可以使用一个语句来设置属性,使用cube.rotation.set(x, y, z)

工作原理...

当你在THREE.Mesh上设置旋转属性,就像我们在本例中所做的那样,Three.js 不会直接计算几何体的顶点的新位置。如果你将这些顶点打印到控制台,你会看到,无论rotation属性如何,它们都将保持完全相同。发生的情况是,当 Three.js 实际上在renderer.render函数中渲染THREE.Mesh时,正是那个确切的时刻,它的确切位置和旋转被计算出来。所以当你平移、旋转或缩放THREE.Mesh时,底层的THREE.Geometry对象保持不变。

相关内容

除了我们在这里展示的方法之外,还有其他方法可以旋转一个对象:

  • 在即将到来的在空间中围绕一个点旋转一个对象配方中,我们将向你展示如何围绕任意点在空间中旋转一个对象,而不是围绕其自身的轴,就像我们在这个配方中展示的那样。

在空间中围绕一个点旋转一个对象

当你使用旋转属性旋转一个对象时,该对象是围绕其自身的中心旋转的。然而,在某些情况下,你可能想要围绕不同的对象旋转一个对象。例如,在模拟太阳系时,你想要围绕地球旋转月球。在这个配方中,我们将解释如何设置 Three.js 对象,以便你可以围绕彼此或空间中的任何点旋转它们。

准备就绪

对于这个配方,我们还提供了一个你可以实验的示例。要加载这个示例,只需在浏览器中打开02.02-rotate-around-point-in-space.html。当你打开这个文件时,你会看到以下截图类似的内容:

准备就绪

通过右侧的控制,你可以旋转各种对象。通过更改rotationSpeedXrotationSpeedYrotationSpeedZ属性,你可以围绕球体的中心旋转红色盒子。

小贴士

为了最好地展示围绕另一个对象旋转一个对象,你应该围绕该对象的y轴旋转。为此,更改rotationSpeedY属性。

如何做...

与之前配方中展示的旋转相比,围绕另一个对象旋转一个对象需要额外的几个步骤:

  1. 让我们先创建截图中所看到的中心蓝色球体。这是我们将在其周围旋转小红色盒子的对象:

      // create a simple sphere
      var sphere = new THREE.SphereGeometry(6.5, 20, 20);
      var sphereMaterial = new THREE.MeshLambertMaterial({
        color: 0x5555ff
      });
      var sphereMesh = new THREE.Mesh(sphere, spherMaterial);
      sphereMesh.receiveShadow = true;
      sphereMesh.position.set(0, 1, 0);
      scene.add(sphereMesh);
    

    到目前为止,这个代码片段中没有什么特别之处。您可以看到一个标准的 THREE.Sphere 对象,我们从中创建 THREE.Mesh 并将其添加到场景中。

  2. 下一步是定义一个单独的对象,我们将使用它作为盒子的支点:

      // add an object as pivot point to the sphere
      pivotPoint = new THREE.Object3D();
      sphereMesh.add(pivotPoint);
    

    pivotPoint 对象是一个 THREE.Object3D 对象。这是 THREE.Mesh 的父对象,可以添加到场景中而不需要几何形状或材质。然而,在这个菜谱中,我们没有将其添加到场景中,而是将其添加到我们在步骤 1 中创建的球体中。因此,如果球体旋转或改变位置,这个 pivotPoint 对象的位置和旋转也会改变,因为我们将其作为子对象添加到球体中。

  3. 现在,我们可以创建红色盒子,而不是将其添加到场景中,我们将其添加到我们刚刚创建的 pivotPoint 对象中:

      // create a box and add to scene
      var cubeGeometry = new THREE.BoxGeometry(2, 4, 2);
      var cubeMaterial = new THREE.MeshLambertMaterial();
      cubeMaterial.color = new THREE.Color('red');
      cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
      // position is relative to it's parent
      cube.position.set(14, 4, 6);
      cube.name = 'cube';
      cube.castShadow = true;
      // make the pivotpoint the cube's parent.
      pivotPoint.add(cube);
    

    现在,我们可以旋转 pivotPoint,立方体将跟随 pivotPoint 的旋转。对于这个菜谱,我们通过在 render 函数中更新 pivotPointrotation 属性来实现这一点:

      function render() {
        renderer.render(scene, camera);
        pivotPoint.rotation.x += control.rotationSpeedX;
        pivotPoint.rotation.y += control.rotationSpeedY;
        pivotPoint.rotation.z += control.rotationSpeedZ;
        requestAnimationFrame(render);
      }
    

它是如何工作的...

当你在 Three.js 中创建 THREE.Mesh 时,你通常只是将其添加到 THREE.Scene 并单独定位它。然而,在这个菜谱中,我们利用了 THREE.Mesh 功能,它从 THREE.Object3D 本身扩展而来,也可以包含子对象。因此,当父对象旋转时,这也会影响子对象。

使用这个菜谱中解释的方法的一个非常有趣的方面是,我们现在可以做几件有趣的事情:

  • 我们可以通过更新 cube.rotation 属性来旋转盒子本身,就像我们在 绕自身轴旋转对象 菜谱中所做的那样

  • 我们也可以通过更改球体的旋转属性来围绕球体旋转盒子,因为我们已经将 pivotPoint 添加为球体网格的子对象

  • 我们甚至可以将所有这些结合起来,我们可以分别旋转 pivotPointsphereMeshcube——所有这些——并创建非常有趣的效果

参见

在这个菜谱中,我们使用了可以将子对象添加到网格的事实,作为绕另一个对象旋转对象的方法。然而,在阅读以下菜谱之后,你将了解更多关于这一点:

  • 绕自身轴旋转对象 的菜谱中,我们向您展示了如何使对象绕其自身轴旋转

通知 Three.js 关于更新

如果你已经使用 Three.js 工作了更长一段时间,你可能已经注意到,有时你对某些几何形状所做的更改似乎并不会总是导致屏幕上的变化。这是因为出于性能原因,Three.js 缓存了一些对象(例如几何形状的顶点和面)并且不会自动检测更新。对于这类更改,你必须明确通知 Three.js 有所变化。在这个菜谱中,我们将向您展示哪些几何形状的属性被缓存并且需要明确通知 Three.js 以进行更新。这些属性包括:

  • geometry.vertices

  • geometry.faces

  • geometry.morphTargets

  • geometry.faceVertexUvs

  • geometry.faces[i].normalgeometry.vertices[i].normal

  • geometry.faces[i].colorgeometry.vertices[i].color

  • geometry.vertices[i].tangent

  • geometry.lineDistances

准备中

有一个示例允许你更改需要显式更新的两个属性:面颜色和顶点位置。如果你在你的浏览器中打开 02.04-update-stuff.html 示例,你会看到以下截图类似的内容:

准备中

使用右上角的菜单,你可以更改这个几何体的两个属性。使用 changeColors 按钮,你可以将每个单独面的颜色设置为随机颜色,使用 changeVertices,你改变这个立方体每个顶点的位置。要应用这些更改,你必须按下 setUpdateColors 按钮或 setUpdateVertices 按钮。

如何做到这一点...

在许多属性中,你必须明确地告诉 Three.js 关于更新的信息。这个食谱将向你展示如何通知 Three.js 关于所有可能的更改。根据你进行的更改,你可以在食谱的任何步骤中跳入:

  1. 首先,如果你想添加顶点或更改几何体单个顶点的值,你可以使用 geometry.vertices 属性。一旦你添加或更改了一个元素,你需要将 geometry.verticesNeedUpdate 属性设置为 true

  2. 在此之后,你可能还想缓存几何体内部的面的定义,这需要你使用 geometry.faces 属性。这意味着当你添加 THREE.Face 或更新现有属性时,你需要将 geometry.elementsNeedUpdate 设置为 true

  3. 然后,你可能想使用可以创建动画的变形目标,其中一个顶点集变形为另一个顶点集。这需要 geometry.morphTargets 属性。为了做到这一点,当你添加一个新的变形目标或更新现有的一个时,你需要将 geometry.morphTargetsNeedUpdate 设置为 true

  4. 接下来,下一步将是添加 geometry.faceVertexUvs。使用这个属性,你定义了纹理如何映射到几何体上。如果你在这个数组中添加或更改元素,你需要将 geometry.uvsNeedUpdate 属性设置为 true

  5. 你可能还想通过更改 geometry.faces[i].normalgeometry.vertices[i].normal 属性来更改顶点或面的法线。当你这样做时,你必须将 geometry.normalsNeedUpdate 设置为 true 以通知 Three.js。除了法线之外,还有一个 geometry.vertices[i].tangent 属性。这个属性用于计算阴影,并计算纹理渲染的时间。如果你进行手动更改,你必须将 geometry.tangentsNeedUpdate 设置为 true

  6. 接下来,你可以在顶点或面上定义单个颜色。你通过设置这些颜色属性来完成此操作:geometry.faces[i].colorgeometry.vertices[i].color。一旦你修改了这些属性,你必须将geometry.colorsNeedUpdate设置为true

  7. 作为最后一步,你可以在运行时选择更改纹理和材质。当你想要更改材质的其中一个属性时,你需要将material.needsUpdate设置为true:纹理、雾、顶点颜色、蒙皮、变形、阴影贴图、alpha 测试、统一变量和灯光。如果你想更新纹理背后的数据,你需要将texture.needsUpdate标志设置为true

它是如何工作的...

作为总结,步骤 1 到 7 适用于几何体以及基于几何体的任何 Three.js 对象。

为了从你的 3D 场景中获得最佳性能,Three.js 缓存了一些通常不会改变的性质和值。特别是在使用 WebGL 渲染器时,通过缓存所有这些值可以获得很多性能提升。当你将这些标志之一设置为 true 时,Three.js 会非常具体地知道它需要更新哪一部分。

相关内容

  • 本书中有一些与这类似的食谱。如果你查看应用矩阵变换食谱的源代码,你可以看到我们在对几何体应用了一些矩阵变换之后使用了verticesNeedUpdate属性。

处理大量对象

如果你有很多对象的场景,你将开始注意到一些性能问题。你创建并添加到场景中的每个网格都需要由 Three.js 管理,当你处理成千上万的对象时,这会导致速度减慢。在这个食谱中,我们将向你展示如何合并对象以提高性能。

准备就绪

合并对象不需要额外的库或资源。我们准备了一个示例,展示了使用单独的对象与合并对象相比在性能上的差异。当你打开02.05-handle-large-number-of-object.html示例时,你可以尝试不同的方法。

你将看到以下截图的类似内容:

准备就绪

在前面的截图中,你可以看到,在使用合并对象的方法时,我们在处理 120,000 个对象时仍然能够达到 60 fps。

如何做到这一点...

在 Three.js 中合并对象非常简单。以下代码片段展示了如何将前一个示例中的对象合并在一起。这里的重要步骤是创建一个新的THREE.Geometry()对象,命名为mergedGeometry,然后创建大量BoxGeometry对象,如高亮代码部分所示:

 var mergedGeometry = new THREE.Geometry();
  for (var i = 0; i < control.numberToAdd; i++) {
    var cubeGeometry = new THREE.BoxGeometry(
      4*Math.random(), 
      4*Math.random(), 
      4*Math.random());
    var translation = new THREE.Matrix4().makeTranslation(
      100*Math.random()-50, 
      0, 100*Math.random()-50);
    cubeGeometry.applyMatrix(translation);
 mergedGeometry.merge(cubeGeometry);
  }
  var mesh = new THREE.Mesh(mergedGeometry, new THREE.MeshNormalMaterial({
    opacity: 0.5,
    transparent: true
  }));
  scene.add(mesh);

我们通过调用merge函数将每个cubeGeometry对象合并到mergedGeometry对象中。结果是单个几何体,我们用它来创建THREE.Mesh,并将其添加到场景中。

它是如何工作的...

当你在几何体(我们称之为 merged)上调用 merge 函数并传入要合并的几何体(我们称之为 toBeMerged)时,Three.js 会执行以下步骤:

  1. 首先,Three.js 从 toBeMerged 几何体中克隆所有顶点并将它们添加到 merged 几何体的顶点数组中。

  2. 接下来,它会遍历 toBeMerged 几何体中的面,并在 merged 几何体中创建新的面,复制原始的法线和颜色。

  3. 作为最后一步,它将 toBeMergeduv 映射复制到 merged 几何体的 uv 映射中。

结果是一个单一的几何体,当添加到场景中时,看起来像多个几何体。

相关内容

  • 这种方法的主要问题是,独立合并的对象着色、样式、动画和变换变得更加困难。对于 Three.js 来说,合并后,它被视为一个单独的对象。然而,可以将特定的材质应用到每个面上。我们将在第四章“材料和纹理”中的使用单独的材质为面着色配方中向你展示如何做这件事。

从高度图中创建几何体

使用 Three.js,创建自己的几何体非常容易。对于这个配方,我们将向你展示如何根据地形高度图创建自己的几何体。

准备工作

要将高度图转换为 3D 几何体,我们首先需要一个高度图。在这本书提供的源文件中,你可以找到一个 Grand Canyon 部分的高度图。以下图像显示了它的样子:

准备中

如果你熟悉大峡谷(Grand Canyon),你可能会认出其独特的形状。本配方结束时的最终结果可以通过在浏览器中打开 02.06-create-terrain-from-heightmap.html 文件来查看。你将看到以下截图类似的内容:

准备中

如何操作...

要创建基于高度图(heightmap)的几何体,你需要执行以下步骤:

  1. 在我们查看所需的 Three.js 代码之前,我们首先需要加载图像并设置一些属性,这些属性决定了几何体的最终大小和高度。这可以通过添加以下代码片段并设置 img.src 属性为我们的高度图位置来完成。一旦图像加载,img.onload 函数将被调用,在那里我们将图像数据转换为 THREE.Geometry

      var depth = 512;
      var width = 512;
      var spacingX = 3;
      var spacingZ = 3;
      var heightOffset = 2;
      var canvas = document.createElement('canvas');
      canvas.width = 512;
      canvas.height = 512;
      var ctx = canvas.getContext('2d');
      var img = new Image();
      img.src = "../assets/other/grandcanyon.png";
      img.onload = function () {...}
    
  2. 一旦图像在 onload 函数中加载,我们需要每个像素的值并将其转换为 THREE.Vector3

      // draw on canvas
      ctx.drawImage(img, 0, 0);
      var pixel = ctx.getImageData(0, 0, width, depth);
      var geom = new THREE.Geometry();
      var output = [];
      for (var x = 0; x < depth; x++) {
        for (var z = 0; z < width; z++) {
          // get pixel
          // since we're grayscale, we only need one element
          // each pixel contains four values RGB and opacity
          var yValue = pixel.data[z * 4 + (depth * x * 4)] / heightOffset;
          var vertex = new THREE.Vector3(x * spacingX, yValue, z * spacingZ);
          geom.vertices.push(vertex);
        }
      }
    

    如此代码片段所示,我们处理图像的每个像素,并根据像素值创建 THREE.Vector3,并将其添加到我们自定义几何体的顶点数组中。

  3. 现在我们已经定义了顶点,下一步是使用这些顶点来创建面:

      // we create a rectangle between four vertices, and we do
      // that as two triangles.
      for (var z = 0; z < depth - 1; z++) {
        for (var x = 0; x < width - 1; x++) {
          // we need to point to the position in the array
          // a - - b
          // |  x  |
          // c - - d
          var a = x + z * width;
          var b = (x + 1) + (z * width);
          var c = x + ((z + 1) * width);
          var d = (x + 1) + ((z + 1) * width);
          var face1 = new THREE.Face3(a, b, d);
          var face2 = new THREE.Face3(d, c, a);
          geom.faces.push(face1);
          geom.faces.push(face2);
        }
      }
    

    如你所见,每一组四个顶点被转换成两个 THREE.Face3 元素并添加到 faces 数组中。

  4. 现在我们需要做的就是让 Three.js 计算顶点和面的法线,然后我们可以从这个几何体创建THREE.Mesh并将其添加到场景中:

      geom.computeVertexNormals(true);
      geom.computeFaceNormals();
      var mesh = new THREE.Mesh(geom, new THREE.MeshLambertMaterial({color: 0x666666}));
      scene.add(mesh);
    

小贴士

如果您渲染这个场景,您可能需要调整相机位置和最终网格的缩放,以获得正确的大小。

它是如何工作的...

高度图是将高度信息嵌入图像的一种方法。图像的每个像素值代表在该点测量的相对高度。在本食谱中,我们处理了这个值,以及它的xy值,并将其转换为顶点。如果我们对每个点都这样做,我们就可以得到一个精确的 2D 高度图的 3D 表示。在这种情况下,它产生了一个包含 512 * 512 个顶点的几何体。

还有更多…

当我们从零开始创建一个几何体时,我们可以添加一些有趣的东西。例如,我们可以为每个单独的面着色。这可以通过以下方式完成:

  1. 首先,添加chroma库(您可以从github.com/gka/chroma.js下载源代码):

      <script src="img/chroma.min.js"></script>
    
  2. 您可以创建一个颜色刻度:

      var scale = chroma.scale(['blue', 'green', red]).domain([0, 50]);
    
  3. 根据面的高度设置面颜色:

      face1.color = new THREE.Color(
        scale(getHighPoint(geom, face1)).hex());
      face2.color = new THREE.Color(
        scale(getHighPoint(geom, face2)).hex())
    
  4. 最后,将材质的vertexColors设置为THREE.FaceColors。结果看起来大致如下:

还有更多…

您还可以应用不同类型的材质,以真正创建类似地形的效果。有关更多信息,请参阅第四章,材质和纹理,关于材质的介绍。

相关内容

  • 在这个示例中,我们使用高度图创建了一个几何体。您还可以将高度图用作凹凸图,以向模型添加深度细节。我们将在第四章,材质和纹理,的使用凹凸图向网格添加深度食谱中向您展示如何这样做。

将一个对象指向另一个对象

许多游戏的一个常见需求是相机和其他对象相互跟随或对齐。Three.js 使用lookAt函数提供了对此的标准支持。在本食谱中,您将学习如何使用lookAt函数将一个对象指向另一个对象。

准备工作

本食谱的示例可以在本书的源代码中找到。如果您在浏览器中打开02.07-point-object-to-another.html,您会看到以下截图类似的内容:

准备中

使用菜单,您可以将大蓝色矩形指向场景中的任何其他网格。

如何操作...

创建lookAt功能实际上非常简单。当您将THREE.Mesh添加到场景中时,您只需调用其lookAt函数并将其指向它应该转向的位置。对于本食谱提供的示例,操作如下:

  control = new function() {
    this.lookAtCube = function() {
      cube.lookAt(boxMesh.position);
    };
    this.lookAtSphere = function() {
      cube.lookAt(sphereMesh.position);
    };
    this.lookAtTetra = function() {
      cube.lookAt(tetraMesh.position);
    };
  };

因此,当您点击lookAtSphere按钮时,矩形的lookAt函数将使用球体的位置被调用。

它是如何工作的...

使用这段代码,将一个对象与另一个对象对齐非常容易。使用lookAt函数,Three.js 隐藏了完成此操作所需的复杂性。内部,Three.js 使用矩阵计算来确定需要应用到对象上的旋转,以正确地对齐你正在查看的对象。所需的旋转随后被设置在对象上(到rotation属性)并在下一个渲染循环中显示。

还有更多…

在这个例子中,我们向你展示了如何将一个对象对齐到另一个对象。使用 Three.js,你可以用相同的方法处理其他类型的对象。你可以使用camera.lookAt(object.position)将相机指向一个特定的对象以使其居中,你也可以使用light.lookAt(object.position)将灯光指向一个特定的对象。

你也可以使用lookAt来跟踪一个移动的对象。只需在渲染循环中添加lookAt代码,对象就会围绕移动的对象移动。

相关内容

  • lookAt函数在内部使用矩阵计算。在本章的最后一个配方中,应用矩阵变换,我们展示了你可以如何使用矩阵计算来完成其他效果。

在 3D 中编写文本

Three.js 的一个酷特性是它允许你在 3D 中编写文本。通过几个简单的步骤,你可以使用任何文本,甚至带有字体支持,作为场景中的 3D 对象。这个配方展示了如何创建 3D 文本,并解释了可用于样式化结果的不同的配置选项。

准备中

要在页面上使用 3D 文本,我们需要包含一些额外的 JavaScript 代码。Three.js 提供了一些你可以使用的字体,它们以单独的 JavaScript 文件的形式提供。要添加所有可用的字体,请包含以下脚本:

  <script src="img/gentilis_bold.typeface.js">
  </script>
  <script src="img/gentilis_regular.typeface.js">
  </script>
  <script src="img/optimer_bold.typeface.js"></script>
  <script src="img/optimer_regular.typeface.js">
  </script>
  <script src="img/helvetiker_bold.typeface.js">
  </script>
  <script src="img/helvetiker_regular.typeface.js">
  </script>
  <script src= "../assets/fonts/droid/droid_sans_regular.typeface.js">
  </script>
  <script src= "../assets/fonts/droid/droid_sans_bold.typeface.js">
  </script>
  <script src= "../assets/fonts/droid/droid_serif_regular.typeface.js">
  </script>
  <script src="img/droid_serif_bold.typeface.js">
  </script>

我们已经在02.09-write-text-in-3D.html示例中做了这个。如果你在浏览器中打开它,你可以尝试在 Three.js 中创建文本时使用的各种字体和属性。当你打开指定的示例时,你会看到以下截图类似的内容:

准备中

如何做…

在 Three.js 中创建 3D 文本非常简单。你所要做的就是创建像这样的THREE.TextGeometry

  var textGeo = new THREE.TextGeometry(text, params);
  textGeo.computeBoundingBox();
  textGeo.computeVertexNormals();

text属性是我们想要写入的文本,而params定义了文本的渲染方式。params对象可以有许多不同的参数,你可以在如何工作…部分中更详细地查看。

然而,在我们的例子中,我们使用了以下参数集(它指向右上角的 GUI 部分):

  var params = {
    material: 0,
    extrudeMaterial: 1,
    bevelEnabled: control.bevelEnabled,
    bevelThickness: control.bevelThickness,
    bevelSize: control.bevelSize,
    font: control.font,
    style: control.style,
    height: control.height,
    size: control.size,
    curveSegments: control.curveSegments
  };

这个几何体可以像其他任何几何体一样添加到场景中:

  var material = new THREE.MeshFaceMaterial([
    new THREE.MeshPhongMaterial({
      color: 0xff22cc,
      shading: THREE.FlatShading
    }), // front
    new THREE.MeshPhongMaterial({
    color: 0xff22cc,
    shading: THREE.SmoothShading
    }) // side
  ]);
  var textMesh = new THREE.Mesh(textGeo, material);
  textMesh.position.x = -textGeo.boundingBox.max.x / 2;
  textMesh.position.y = -200;
  textMesh.name = 'text';
  scene.add(textMesh);

注意

在使用 THREE.TextGeometry 和材料时,需要考虑一件事。如代码片段所示,我们添加了两个材质对象而不是一个。第一个材质应用于渲染文本的前面,第二个材质应用于渲染文本的侧面。如果您只传递一个材质,它将应用于前面和侧面。

工作原理...

如前所述,存在许多不同的参数:

参数 描述
height 高度属性定义文本的深度,换句话说,文本被拉伸多远以使其成为 3D。
size 使用此属性,您设置最终文本的大小。
curveSegments 如果字符有曲线(例如,字母 a),此属性定义曲线将有多平滑。
bevelEnabled 斜边提供了从文本前面到侧面的平滑过渡。如果您将此值设置为 true,则将在渲染的文本上添加斜边。
bevelThickness 如果您已将 bevelEnabled 设置为 true,则定义斜边有多深。
bevelSize 如果您已将 bevelEnabled 设置为 true,则定义斜边有多高。
weight 这是指文字的粗细(正常或粗体)。
font 这是将要使用的字体名称。
material 当提供材料数组时,此属性应包含用于前面的材料索引。
extrudeMaterial 当提供材料数组时,此属性应包含用于侧面的材料索引。

当您创建 THREE.TextGeometry 时,Three.js 内部使用 THREE.ExtrudeGeometry 来创建 3D 形状。THREE.ExtrudeGeometry 通过沿 Z 轴拉伸 2D 形状来工作,使其成为 3D。为了从文本字符串创建 2D 形状,Three.js 使用我们在本配方的 准备工作 部分中包含的 JavaScript 文件。这些基于 typeface.neocracy.org/fonts.html 的 JavaScript 文件允许您将文本渲染为 2D 路径,然后我们可以将其转换为 3D。

更多...

如果您想使用不同的字体,您可以在 typeface.neocracy.org/fonts.html 转换自己的字体。要使用这些字体,您只需在您的页面上包含它们,并将正确的 namestyle 值作为参数传递给 THREE.TextGeometry

将 3D 公式作为 3D 几何体渲染

Three.js 提供了许多不同的创建几何体方法。您可以使用标准的 Three.js 对象,如 THREE.BoxGeometryTHREE.SphereGeometry,从头开始创建几何体,或者只需加载由外部 3D 建模程序创建的模型。在这个配方中,我们将向您展示另一种创建几何体的方法。这个配方将向您展示如何根据数学公式创建几何体。

准备工作

对于这个配方,我们将使用THREE.ParametricGeometry对象。因为这个对象可以从标准的 Three.js 分发中获取,所以不需要包含额外的 JavaScript 文件。

要查看这个配方的最终结果,你可以查看02.10-create-parametric-geometries.html,你会看到以下截图类似的内容:

准备中

这个图形向你展示了格雷的克莱因瓶,它是基于几个简单的数学公式渲染的。

如何做…

使用数学公式通过 Three.js 生成几何形状非常简单,只需两步:

  1. 我们需要做的第一件事是创建一个函数,这个函数将为我们创建几何形状。这个函数将接受两个参数:uv。当 Three.js 使用这个函数来生成几何形状时,它将使用uv的值调用这个函数,从0开始,到1结束。对于这些uv的组合中的每一个,这个函数应该返回一个THREE.Vector3对象,它代表最终几何形状中的一个顶点。创建你在上一节中看到的图形的函数如下所示:

      var paramFunction = function(u, v) {
        var a = 3;
        var n = 3;
        var m = 1;
        var u = u * 4 * Math.PI;
        var v = v * 2 * Math.PI;
        var x = (a + Math.cos(n * u / 2.0) * Math.sin(v) - Math.sin(n * u / 2.0) * Math.sin(2 * v)) * Math.cos(m * u / 2.0);
        var y = (a + Math.cos(n * u / 2.0) * Math.sin(v) - Math.sin(n * u / 2.0) * Math.sin(2 * v)) * Math.sin(m * u / 2.0);
        var z = Math.sin(n * u / 2.0) * Math.sin(v) + Math.cos(n * u / 2.0) * Math.sin(2 * v);
        return new THREE.Vector3(x, y, z);
      }
    

    只要你为每个uv的值返回一个新的THREE.Vector3对象,你就可以提供你自己的函数。

  2. 现在我们已经得到了创建我们几何形状的函数,我们可以使用这个函数来创建THREE.ParametricGeometry

      var geom = new THREE.ParametricGeometry(paramFunction, 100, 100);
      var mat = new THREE.MeshPhongMaterial({
        color: 0xcc3333a,
        side: THREE.DoubleSide,
        shading: THREE.FlatShading
      });
      var mesh = new THREE.Mesh(geom, mat);
      scene.add(mesh);
    

你可以清楚地看到已经将三个参数传递给了THREE.ParametricObject的构造函数。这将在如何工作…部分中更详细地讨论。

在创建几何形状之后,你只需要创建THREE.Mesh并将其添加到场景中,就像添加任何其他 Three.js 对象一样。

如何工作…

从前面的代码片段的第 2 步,你可以看到我们向THREE.ParametricObject的构造函数提供了三个参数。第一个是我们第 1 步中展示的函数,第二个决定了我们如何分割u参数的步数,第三个决定了我们如何分割v参数的步数。数字越高,创建的顶点就越多,最终的几何形状看起来就越平滑。不过,要注意,顶点数量过多会对性能产生不利影响。

当你创建THREE.ParametricGeometry时,Three.js 将根据提供的参数调用该函数多次。函数被调用的次数基于第二个和第三个参数。这会产生一系列THREE.Vector3对象,然后这些对象自动组合成面。这会产生一个你可以像使用任何其他几何形状一样使用的几何形状。

还有更多…

与这个配方中展示的不同,你可以用这些类型的几何形状做很多事情。在02.10-create-parametric-geometries.html源文件中,你可以找到一些其他函数,这些函数可以创建看起来有趣的几何形状,如下面的截图所示:

还有更多…

使用 Three.js 通过自定义几何对象扩展 Three.js

在你迄今为止看到的菜谱中,我们从头开始创建 Three.js 对象。我们或者从头开始使用顶点和面构建一个新的几何体,或者重用现有的一个并为其配置。虽然这对于大多数场景来说已经足够好了,但当需要维护一个包含大量不同几何体的庞大代码库时,这并不是最佳解决方案。在 Three.js 中,你通过实例化一个THREE.GeometryName对象来创建几何体。在这个菜谱中,我们将向你展示如何创建一个自定义几何体对象,并像其他 Three.js 对象一样实例化它。

准备中

你可以使用提供的源代码来实验这个菜谱的示例。在你的浏览器中打开02.11-extend-threejs-with-custom-geometry.html,查看最终结果,它将类似于以下屏幕截图:

准备中

在这个屏幕截图中,你可以看到一个单独旋转的立方体。这个立方体是一个自定义几何体,可以通过使用新的THREE.FixedBoxGeometry()来实例化。在接下来的部分中,我们将解释如何实现这一点。

如何操作...

使用自定义几何体扩展 Three.js 相对简单,只需几个简单的步骤:

  1. 我们需要做的第一件事是创建一个新的 JavaScript 对象,它包含我们新的 Three.js 几何体的逻辑和属性。对于这个菜谱,我们将创建FixedBoxGeometry,它的工作方式与THREE.BoxGeometry完全相同,但使用相同的高度、宽度和深度值。在这个菜谱中,我们在setupCustomObject函数中创建这个新对象:

      function setupCustomObject() {
        // First define the object.
        THREE.FixedBoxGeometry = function ( width, segments) {
          // first call the parent constructor
          THREE.Geometry.call( this );
          this.width = width;
          this.segments = segments;
          // we need to set
          //   - vertices in the parent object
          //   - faces in the parent object
          //   - uv mapping in the parent object
          // normally we'd create them here ourselves
          // in this case, we just reuse the once
          // from the boxgeometry.
          var boxGeometry = new THREE.BoxGeometry(
            this.width, 
            this.width, 
            this.width, this.segments, this.segments);
          this.vertices = boxGeometry.vertices;
          this.faces = boxGeometry.faces;
          this.faceVertexUvs = boxGeometry.faceVertexUvs;
        }
        // define that FixedBoxGeometry extends from 
        // THREE.Geometry
        THREE.FixedBoxGeometry.prototype= Object.create( THREE.Geometry.prototype );
      }
    

    在这个函数中,我们使用THREE.FixedBoxGeometry = function ( width, segments) {..}定义了一个新的 JavaScript 对象。在这个函数中,我们首先调用父对象的构造函数(THREE.Geometry.call( this ))。这确保了所有属性都正确初始化。接下来,我们包装一个现有的THREE.BoxGeometry对象,并使用该对象的信息来设置我们自定义对象的verticesfacesfaceVertexUvs

    最后,我们需要告诉 JavaScript 我们的THREE.BoxGeometry对象继承自THREE.Geometry。这是通过将THREE.FixedBoxGeometry的原型属性设置为Object.create(THREE.Geometry.prototype)来实现的。

  2. 在调用setupCustomObject()之后,我们现在可以使用相同的方法来创建这个对象,就像我们为其他 Three.js 提供的几何体所做的那样:

      var cubeGeometry = new THREE.FixedBoxGeometry(3, 5);
      var cubeMaterial = new THREE.MeshNormalMaterial();
      var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
      scene.add(cube);
    

    在这个阶段,我们已经创建了一个自定义的 Three.js 几何体,你可以像 Three.js 提供的标准几何体一样实例化它。

工作原理...

在这个菜谱中,我们使用了 JavaScript 提供的一种标准方式来创建继承自其他对象的对象。我们定义了以下内容:

  THREE.FixedBoxGeometry.prototype= Object.create( THREE.Geometry.prototype );

这段代码片段告诉 JavaScript THREE.FixedBoxGeometry被创建,它继承自THREE.Geometry的所有属性和函数,它有自己的构造函数。这也是为什么我们也在我们的新对象中添加了以下调用:

  THREE.Geometry.call( this );

这将在创建我们自己的自定义对象时调用 THREE.Geometry 对象的构造函数。

原型继承比这个简短的菜谱中解释的还要多。如果你想了解更多关于原型继承的信息,Mozilla 团队有一个很好的解释,说明了如何使用原型属性进行继承,请参阅developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain

还有更多...

在这个菜谱中,我们封装了一个现有的 Three.js 对象来创建我们的自定义对象。你也可以将这种方法应用于完全从头创建的对象。例如,你可以从我们在 从高度图创建几何体 菜谱中使用的 JavaScript 代码创建 THREE.TerrainGeometry 来创建一个 3D 地形。

在两点之间创建样条曲线

当你创建可视化效果,例如,想要可视化飞机的飞行路径时,在起点和终点之间绘制曲线是一种很好的方法。在这个菜谱中,我们将向你展示如何使用标准的 THREE.TubeGeometry 对象来完成这个操作。

准备工作

当你打开这个菜谱的示例 02.12-create-spline-curve.html 时,你可以看到一个从起点到终点的曲线管几何形状:

准备工作

在接下来的部分中,我们将逐步解释如何创建这条曲线。

如何操作...

要创建一个像前面示例中显示的弯曲样条曲线,我们需要采取几个简单的步骤:

  1. 我们需要做的第一件事是为这条曲线定义一些常数:

      var numPoints = 100;
      var start = new THREE.Vector3(-20, 0, 0);
      var middle = new THREE.Vector3(0, 30, 0);
      var end = new THREE.Vector3(20, 0, 0);
    

    numPoints 对象定义了我们用来定义曲线的顶点数量以及渲染管时使用的段数。start 向量定义了我们想要开始曲线的位置,end 向量决定了曲线的终点,最后,middle 向量定义了曲线的高度和中心点。如果我们,例如,将 numPoints 设置为 5,我们将得到不同类型的曲线。

    如何操作...

  2. 现在我们已经得到了 startendmiddle 向量,我们可以使用它们来创建一个漂亮的曲线。为此,我们可以使用 Three.js 提供的一个对象,称为 THREE.QuadraticBezierCurve3

      var curveQuad = new THREE.QuadraticBezierCurve3(start, middle, end);
    

    基于这个 curveQuad,我们现在可以创建一个简单的管几何体。

  3. 要创建一个管,我们使用 THREE.TubeGeometry 并传入 curveQuad,这是我们上一步创建的:

      var tube = new THREE.TubeGeometry(curveQuad, numPoints, 2, 20, false);
      var mesh = new THREE.Mesh(tube, new THREE.MeshNormalMaterial({
        opacity: 0.6,
        transparent: true
      }));
      scene.add(mesh);
    

它是如何工作的...

在本食谱中我们创建的QuadraticBezierCurve3对象具有多个不同的函数(getTangentAtgetPointAt),这些函数决定了路径上的某个位置。这些函数根据传递给构造函数的startmiddleend向量返回信息。当我们把QuadraticBezierCurve3传递给THREE.TubeGeometry时,THREE.TubeGeometry使用getTangentAt函数来确定其顶点的位置。

还有更多…

在这个食谱中,我们使用了THREE.QuadraticBezierCurve3来创建我们的样条。Three.js 还提供了THREE.CubicBezierCurve3THREE.SplineCurve3曲线,你可以使用这些曲线来定义这类样条。你可以在stackoverflow.com/questions/18814022/what-is-the-difference-between-cubic-bezier-and-quadratic-bezier-and-their-use-c上找到关于二次贝塞尔曲线和三次贝塞尔曲线之间差异的更多信息。

从 Blender 创建和导出模型

你可以从www.blender.org/download/下载 Blender,这是一个创建 3D 模型的优秀工具,并且对 Three.js 有很好的支持。有了合适的插件,Blender 可以直接导出模型到 Three.js 的 JSON 格式,然后可以轻松地将其添加到场景中。

准备中

在我们能够使用 Blender 中的 JSON 导出器之前,我们首先需要在 Blender 中安装这个插件。要安装插件,请按照以下步骤操作:

  1. 你需要做的第一件事是获取插件的最新版本。我们已经将它添加到本书的源代码中。你可以在assets/plugin文件夹中找到这个插件。在那个目录中,你会找到一个名为io_mesh_threejs的单个目录。要安装插件,只需将这个完整的目录复制到 Blender 的插件位置。由于 Blender 是跨平台的,根据你的操作系统,这个插件目录可能存储在不同的位置。

  2. 对于 Windows 系统,将io_mesh_threejs目录复制到C:\Users\USERNAME\AppData\Roaming\Blender Foundation\Blender\2.70a\scripts\addons

  3. 对于 OS X 用户,这取决于你安装 Blender 的位置(解压 ZIP 文件)。你应该将io_mesh_threejs目录复制到/location/of/extracted/zip/blender.app/Contents/MacOS/2.6X/scripts/addons

  4. 最后,对于 Linux 用户,将io_mesh_threejs目录复制到/home/USERNAME/.config/blender/2.70a/scripts/addons

  5. 如果你通过 apt-get 安装了 Blender,你应该将io_mesh_threejs目录复制到/usr/lib/blender/scripts/addons

  6. 下一步是启用 Three.js 插件。如果 Blender 已经运行,请重新启动它并打开用户首选项。你可以通过导航到文件 | 用户首选项来找到它。在打开的屏幕上,选择插件标签页,其中列出了所有可用的插件。准备中

  7. 在这一点上,Three.js 插件已启用。为了确保在您重新启动 Blender 时它保持启用状态,请点击保存用户设置按钮。现在,关闭此窗口,如果您导航到文件 | 导出,您应该会看到一个 Three.js 导出功能,如下面的截图所示:准备就绪

现在,让我们看看这个配方的其余部分,看看我们如何从 Blender 导出一个模型并在 Three.js 中加载它。

如何做...

要从 Blender 导出一个模型,我们首先必须创建一个。在本配方中,我们不会加载现有的一个,而是从头开始创建,导出,并在 Three.js 中加载它:

  1. 首先,当您打开 Blender 时,您会看到一个立方体。首先,我们删除这个立方体。您可以通过按x并点击弹出窗口中的删除来完成此操作。

  2. 现在,我们将创建一个简单的几何体,我们可以使用我们安装的 Three.js 插件将其导出。为此,在底部菜单中点击添加,然后选择猴子,如下面的截图所示:如何做...

    现在,您应该有一个在 Blender 中中间有猴子几何体的空场景:

    如何做...

  3. 我们可以使用本配方准备就绪部分中安装的插件将这只猴子导出到 Three.js。为此,在文件菜单中导航到导出 | Three.js。这会打开导出对话框,您可以在其中确定将模型导出到的目录。在这个导出对话框中,您还可以设置一些额外的 Three.js 特定导出属性,但默认属性通常是可以接受的。对于这个配方,我们将模型导出为monkey.js

  4. 在这一点上,我们已经导出了模型,现在可以使用 Three.js 加载它。要加载模型,我们只需在我们在第一章中展示的使用 WebGL 渲染器入门配方中添加以下 JavaScript 即可:

      function loadModel() {
        var loader = new THREE.JSONLoader();
        loader.load("../assets/models/monkey.js", function(model, material) {
          var mesh = new THREE.Mesh(model, material[0]);
          mesh.scale = new THREE.Vector3(3,3,3);
          scene.add(mesh);
        });
      }
    

结果是一个旋转的猴子,这是我们使用 Blender 创建的,由 Three.js 渲染,如下面的截图所示:

如何做...

参见

有几个配方您会从中受益阅读:

  • 使用 OBJMTLLoader 与多个材质配方中,我们使用不同的格式,将其加载到 Three.js 中

  • 在第七章中,我们探讨了动画与物理,其中我们查看动画,当我们使用使用骨骼动画配方中的骨骼动画时,我们将重新访问 Three.js 导出插件。

使用 OBJMTLLoader 与多个材质

Three.js 提供了一些标准几何体,您可以使用它们来创建 3D 场景。然而,复杂的模型在专门的 3D 建模应用程序(如 Blender 或 3ds Max)中创建更为容易。幸运的是,尽管如此,Three.js 对大量导出格式提供了很好的支持,因此您可以轻松加载在这些包中创建的模型。广泛支持的标准是OBJ格式。使用这种格式,模型由两个不同的文件描述:一个定义几何体的.obj文件和一个定义材质的.mtl文件。在本教程中,我们将向您展示使用 Three.js 提供的OBJMTLLoader成功加载模型所需的步骤。

准备中

要加载.obj.mtl格式的模型,我们首先需要包含正确的 JavaScript 文件,因为这些 JavaScript 对象不包括在标准的 Three.js JavaScript 文件中。因此,在 head 部分,您需要添加以下脚本标签:

  <script src="img/MTLLoader.js"></script>
  <script src="img/OBJMTLLoader.js"></script>

在本例中,我们使用的模型是乐高迷你人偶。在 Blender 中,原始模型看起来是这样的:

准备中

您可以通过在浏览器中打开02.14-use-objmtlloader-with-multiple-materials.html来查看最终的模型。以下截图显示了渲染器模型的外观:

准备中

让我们一步步指导您如何加载这样的模型。

如何操作...

在我们将模型加载到 Three.js 之前,我们首先需要检查.mtl文件中是否定义了正确的路径。因此,我们首先需要做的是在文本编辑器中打开.mtl文件:

  1. 当您打开本例的.mtl文件时,您会看到以下内容:

      newmtl Cap
      Ns 96.078431
      Ka 0.000000 0.000000 0.000000
      Kd 0.990000 0.120000 0.120000
      Ks 0.500000 0.500000 0.500000
      Ni 1.000000
      d 1.00000
      illum 2
      newmtl Minifig
      Ns 874.999998
      Ka 0.000000 0.000000 0.000000
      Kd 0.800000 0.800000 0.800000
      Ks 0.200000 0.200000 0.200000
      Ni 1.000000
      d 1.000000
      illum 2
     map_Kd ../textures/Mini-tex.png
    
    

    这个.mtl文件定义了两种材质:一种用于迷你人偶的身体,另一种用于其帽子。我们需要检查的是map_Kd属性。这个属性需要包含从.obj文件加载的相对路径到 Three.js 可以找到纹理的位置。在我们的例子中,这个路径是:.../textures/Mini-tex.png

  2. 确保.mtl文件包含正确的引用后,我们可以使用THREE.OBJMTLLoader加载模型:

      var loader = new THREE.OBJMTLLoader();
      // based on model from:
      // http://www.blendswap.com/blends/view/69499
      loader.load("../assets/models/lego.obj",
      "../assets/models/lego.mtl", 
      function(obj) {
        obj.translateY(-3);
        obj.name='lego';
        scene.add(obj);
      });
    

    如您所见,我们将.obj.mtl文件都传递给了load函数。这个load函数的最后一个参数是一个callback函数。当模型加载完成时,这个callback函数将被调用。

  3. 到这一步,您可以对加载的模型做任何您想做的事情。在本例中,我们通过右上角的菜单添加了缩放和旋转功能,并将这些属性应用到render函数中:

      function render() {
        renderer.render(scene, camera);
        var lego = scene.getObjectByName('lego');
        if (lego) {
          lego.rotation.y += control.rotationSpeed;
          lego.scale.set(control.scale, control.scale, control.scale);
        }
        requestAnimationFrame(render);
      }
    

工作原理...

.obj.mtl 文件格式是经过良好记录的格式。OBJMTLLoader 解析这两个文件中的信息,并根据该信息创建几何体和材质。它使用 .obj 文件来确定对象的几何形状,并使用 .mtl 文件中的信息来确定材质,在这个例子中是 THREE.MeshLambertMaterial,用于每个几何体。

Three.js 然后将这些组合成 THREE.Mesh 对象,并返回一个包含 Lego 图形所有部分的单个 THREE.Object3D 对象,您可以将它添加到场景中。

还有更多…

在这个菜谱中,我们向您展示了如何加载定义在 .obj.mtl 格式的对象。除了这种格式,Three.js 还支持广泛的其他格式。有关 Three.js 支持的文件格式的良好概述,请参阅 Three.js GitHub 存储库中的此目录:github.com/mrdoob/three.js/tree/master/examples/js/loaders

参见

  • 对于这个菜谱,我们假设我们有一个完整且格式正确的模型。如果您想从头开始创建模型,一个很好的开源 3D 建模工具是 Blender。在 从 Blender 创建和导出模型 菜谱中,解释了如何在 Blender 中创建新模型并将其导出,以便 Three.js 可以加载它。

应用矩阵变换

在本章的前几个菜谱中,我们使用了 rotation 属性并应用平移以获得所需的旋转效果。幕后,Three.js 使用矩阵变换来修改网格或几何体的形状和位置。Three.js 还提供了将自定义矩阵变换直接应用于几何体或网格的功能。在这个菜谱中,我们将向您展示如何将您自己的自定义矩阵变换直接应用于 Three.js 对象。

准备工作

要查看此菜谱的实际操作并实验各种变换,请在您的浏览器中打开 02.15-apply-matrix-transformations.html 示例。您将看到一个简单的 Three.js 场景:

准备工作

在这个场景中,您可以使用右侧的菜单直接将各种变换应用于旋转的立方体。在下一节中,我们将向您展示创建此变换所需的步骤。

如何操作...

创建自己的矩阵变换非常简单。

  1. 首先,让我们看看当您点击 doTranslation 按钮时调用的代码:

      this.doTranslation = function() {
        // you have two options, either use the
        // helper function provided by three.js
        // new THREE.Matrix4().makeTranslation(3,3,3);
        // or do it yourself
        var translationMatrix = new THREE.Matrix4(
          1, 0, 0, control.x,
          0, 1, 0, control.y,
          0, 0, 1, control.z,
          0, 0, 0, 1
        );
        cube.applyMatrix(translationMatrix);
        // or do it on the geometry
        // cube.geometry applyMatrix(translationMatrix);
        // cube.geometry.verticesNeedUpdate = true;
      }
    

    如您在代码中所见,创建自定义矩阵变换非常简单,只需要以下步骤。

  2. 首先,您实例化一个新的 THREE.Matrix4 对象,并将矩阵的值作为参数传递给构造函数。

  3. 接下来,您使用 THREE.MeshTHREE.GeometryapplyMatrix 函数将该变换应用于特定对象。

  4. 如果你将此应用于 THREE.Geometry,你必须将 verticesNeedUpdate 属性设置为 true,因为顶点变化不会自动传播到渲染器(参见 通知 Three.js 关于更新 菜谱)。

工作原理

本菜谱中使用的变换基于矩阵计算。矩阵计算本身是一个相当复杂的话题。如果你对矩阵计算的工作原理以及它们如何用于所有不同类型的变换感兴趣,可以在 www.matrix44.net/cms/notes/opengl-3d-graphics/basic-3d-math-matrices 找到很好的解释。

还有更多...

在本章的示例中,你可以对旋转的立方体应用几个变换。以下代码片段显示了这些变换所使用的矩阵:

  this.doScale = function() {
    var scaleMatrix = new THREE.Matrix4(
      control.x, 0, 0, 0,
      0, control.y, 0, 0,
      0, 0, control.z, 0,
      0, 0, 0, 1
    );
    cube.geometry.applyMatrix(scaleMatrix);
    cube.geometry.verticesNeedUpdate = true;
  }
  this.doShearing = function() {
    var scaleMatrix = new THREE.Matrix4(
      1, this.a, this.b, 0,
      this.c, 1, this.d, 0,
      this.e, this.f, 1, 0,
      0, 0, 0, 1
    );
    cube.geometry.applyMatrix(scaleMatrix);
    cube.geometry.verticesNeedUpdate = true;
  }
  this.doRotationY = function() {
    var c = Math.cos(this.theta),
    s = Math.sin(this.theta);
    var rotationMatrix = new THREE.Matrix4(
      c, 0, s, 0,
      0, 1, 0, 0, -s, 0, c, 0,
      0, 0, 0, 1
    );
    cube.geometry.applyMatrix(rotationMatrix);
    cube.geometry.verticesNeedUpdate = true;
  }

在这个菜谱中,我们从头开始创建了矩阵变换。然而,Three.js 也提供了 Three.Matrix4 类中的一些辅助函数,你可以使用这些函数更轻松地创建这类矩阵:

  • makeTranslation(x, y, z): 这个函数返回一个矩阵,当应用于几何体或网格时,通过指定的 x、y 和 z 值平移对象

  • makeRotationX(theta): 这个函数返回一个矩阵,可以用来绕 x 轴旋转网格或几何体一定量的弧度

  • makeRotationY(theta): 这与上一个函数相同——这次是绕 y 轴旋转

  • makeRotationZ(theta): 这与上一个函数相同——这次是绕 z 轴旋转

  • makeRotationAxis(axis, angle): 这个函数返回一个基于提供的轴和角度的旋转矩阵

  • makeScale(x, y, z): 这个函数返回一个矩阵,可以用来沿任意三个轴缩放对象

相关内容

我们也在本章的其他菜谱中使用了矩阵变换:

  • 在前两个菜谱中,绕对象自身的轴旋转绕空间中的点旋转对象,实际的旋转是通过矩阵变换来应用的

  • 绕对象自身的轴旋转 菜谱中,我们使用了 THREE.Matrix4 对象的辅助函数来绕对象的轴旋转对象

第三章 使用相机

在本章中,我们将涵盖以下配方:

  • 让相机跟踪一个对象

  • 缩放相机到对象

  • 使用透视相机

  • 使用正交相机

  • 创建 2D 叠加层

  • 围绕场景旋转相机

  • 将渲染视图匹配到调整大小的浏览器

  • 将世界坐标转换为屏幕坐标

  • 在场景中选择一个对象

简介

在 Three.js 中,最重要的对象之一就是相机。通过相机,你定义了场景的哪一部分将被渲染,以及信息将如何投影到屏幕上。在本章中,我们将向您展示一些配方,这些配方将允许您向您的 Three.js 应用程序添加更复杂的相机功能。

让相机跟踪一个对象

当你创建包含许多移动对象的游戏或可视化时,你可能想让相机围绕对象移动。通常情况下,当你创建一个相机时,它会指向一个单一的位置,并显示其视野内的场景。在这个配方中,我们将解释如何创建一个可以围绕任何对象移动的相机。

准备就绪

这个配方仅使用核心 Three.js 函数,因此不需要在源代码中包含外部 JavaScript 库。如果你想看到这个配方的最终结果,你可以在浏览器中打开 03.01-camera-follow-object.html,你将看到以下截图所示的内容:

准备就绪

在这个例子中,你可以看到相机聚焦在球体上。随着球体在场景中移动,相机也会移动以保持对球体位置的聚焦。

如何操作...

对于这个配方,我们只需要采取三个简单的步骤:

  1. 我们需要做的第一件事是创建我们想要跟踪的对象。对于这个配方,我们创建了一个简单的 THREE.SphereGeometry 对象,并将其添加到场景中,如下所示:

      var sphereGeometry = new THREE.SphereGeometry(1.5,20,20);
      var matProps = {
        specular: '#a9fcff',
        color: '#00abb1',
        emissive: '#006063',
        shininess: 10
      }
      var sphereMaterial = new
      THREE.MeshPhongMaterial(matProps);
      var sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
      sphereMesh.name = 'sphere';
      scene.add(sphereMesh);
    

    如你在下面的代码片段中所见,我们不需要对我们想要跟踪的对象做任何特殊处理。

  2. 下一步是,我们需要一个渲染场景并保持聚焦于我们想要跟踪的对象的相机。以下 JavaScript 代码创建并定位了这个相机:

      // create a camera, which defines where we're looking at.
      camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
      // position and point the camera to the center of the scene
      camera.position.x = 15;
      camera.position.y = 6;
      camera.position.z = 15;
    

    这是一个标准的 THREE.PerspectiveCamera 对象,我们也在本章的许多其他示例中使用它。再次强调,不需要任何特殊配置。

  3. 对于最后一步,我们定义了渲染循环,它将渲染场景,并确保相机在这个配方中的正确方向:

      function render() {
        var sphere = scene.getObjectByName('sphere');
        renderer.render(scene, camera);
     camera.lookAt(sphere.position);
        step += 0.02;
        sphere.position.x = 0 + (10 * (Math.cos(step)));
        sphere.position.y = 0.75 * Math.PI / 2 + (6 * Math.abs(Math.sin(step)));
        requestAnimationFrame(render);
      }
    

render 函数中,我们使用 camera.lookAt 函数将相机指向球体的 position 函数。由于我们在每一帧渲染时都这样做,所以看起来相机正好跟随球体的位置。

它是如何工作的...

THREE.PerspectiveCameraTHREE.Object3D 对象扩展而来。THREE.Object3D 提供了 lookAt 函数。当这个函数使用要查看的目标位置调用时,Three.js 创建一个变换矩阵 (THREE.Matrix4),使 THREE.Object3D 对象的位置与目标位置对齐。在摄像机的情况下,结果是目标对象被摄像机在场景中跟随,并在屏幕中间渲染。

还有更多...

在这个菜谱中,我们使用 lookAt 函数将摄像机指向一个特定的对象。您可以将这个相同的菜谱应用于所有从 Object3D 扩展的 Three.js 对象。例如,您可以使用它来确保 THREE.SpotLight 总是照亮特定的对象。或者,如果您正在创建动画,您可以使用这个效果来确保一个角色总是看着另一个角色的脸部。

相关内容

  • 由于 lookAt 函数使用矩阵变换来指向一个对象,您也可以不使用 lookAt 函数来做这件事。为此,您将不得不自己创建一个变换矩阵。我们已经在 应用矩阵变换 菜谱中解释了如何做这件事,您可以在 第二章,几何体和网格 中找到它。

将摄像机缩放到物体

通常,当您在场景中定位摄像机时,您可能会移动它或让它聚焦在不同的物体上。在这个菜谱中,我们将向您展示如何缩放到一个物体,使其几乎填满渲染视图。

准备中

要进行缩放,我们使用 THREE.PerspectiveCamera 对象的标准功能。我们提供了一个示例,演示了您在本菜谱结束时将获得的结果。要尝试这个示例,请在您的浏览器中打开 03.02-zoom-camera-to-object.html。您将看到以下截图类似的内容:

准备中

初始时,您将在场景中心看到一个小的旋转立方体。如果您点击右上角菜单中的 updateCamera 按钮,摄像机将更新并显示一个全屏的旋转立方体,如下所示:

准备中

如何操作...

要将摄像机缩放到物体,我们需要采取以下步骤:

  1. 我们需要做的第一件事是创建并定位我们用来缩放的摄像机:

      camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
      // position and point the camera to the center of the scene
      camera.position.x = 15;
      camera.position.y = 15;
      camera.position.z = 15;
      camera.lookAt(scene.position);
    

    如您所见,这是一个标准的 THREE.PerspectiveCamera 对象,我们给它一个位置并将其添加到场景中。

  2. 要使用摄像机进行缩放,我们首先需要确定物体与摄像机之间的距离及其高度:

      // create an helper
      var helper = new THREE.BoundingBoxHelper(cube);
      helper.update();
      // get the bounding sphere
      var boundingSphere = helper.box.getBoundingSphere();
      // calculate the distance from the center of the sphere
      // and subtract the radius to get the real distance.
      var center = boundingSphere.center;
      var radius = boundingSphere.radius;
      var distance = center.distanceTo(camera.position) - radius;
      var realHeight = Math.abs(helper.box.max.y - helper.box.min.y);
    

    在之前的代码片段中,我们使用了 THREE.BoundingBoxHelper 来确定立方体的 realHeight 函数及其与摄像机的距离。

  3. 使用这些信息,我们可以确定摄像机的视场 (fov),使其只显示立方体:

      var fov = 2 * Math.atan(realHeight * control.correctForDepth / (2 * distance)) * (180 / Math.PI);
    

    在此代码片段中,您可以看到我们使用了一个额外的值,即 control.correctForDepth,来计算视场。此值在示例右上角的菜单中设置,略微增加了最终视场。我们这样做是因为在这个计算中,我们假设相机正对着对象。如果相机没有直视对象,我们需要补偿这个偏移。

  4. 现在我们已经为相机设置了视场,我们可以将此值分配给 camera.fov 属性:

      camera.fov = fov;
      camera.updateProjectionMatrix();
    

    由于 Three.js 缓存了相机的 fov 属性,我们需要通知 Three.js 相机配置有一些更改。我们通过 updateProjectionMatrix 函数来完成此操作。

到目前为止,相机已经完全缩放了对象。

它是如何工作的…

要理解它是如何工作的,我们需要了解 THREE.PerspectiveCamera 对象的视场属性。以下图显示了视场属性:

如何工作…

如图中所示,存在一个单独的水平和垂直视场。Three.js 只允许您设置垂直视场,水平视场基于您在相机上定义的纵横比确定。当您查看此图时,您还可以直接看到此菜谱是如何工作的。通过改变视场,我们缩小了近平面和远平面,并限制了渲染的内容,这样我们就可以进行缩放。

还有更多…

除了这里显示的缩放方式之外,还有另一种缩放方式。我们不仅可以改变相机的 fov 属性,还可以将相机移近对象。在 Three.js 的最新版本中,引入了一个 zoom 属性;您也可以使用此属性来缩放场景,但不能直接用于缩放单个对象。

使用透视相机

Three.js 提供了两种相机:一种是以透视投影(正如我们在现实世界中看到图像的方式)渲染场景的相机,另一种是以正交投影(常用于游戏的假 3D;有关此类相机的更多信息,请查看即将到来的 使用正交相机 菜谱)。在本菜谱中,我们将探讨这两种相机中的第一种,并解释您如何在您的场景中使用透视相机。

准备工作

与相机属性一起工作有时可能会有些困惑。为了帮助您更好地理解步骤或此菜谱,我们创建了一个简单的页面,展示了每个相机属性的效果。在浏览器中打开 03.03-use-an-perspective-camera.html,您将看到如下内容:

准备工作

通过右上角菜单中可用的最后四个属性,您可以设置用于渲染此场景的 THREE.PerspectiveCamera 的属性,并立即看到每个属性的效果。

如何做到这一点…

在这个菜谱中,我们分别设置每个相机的属性。这些属性也可以通过 THREE.PerspectiveCamera 的构造函数传入。在这个菜谱的 还有更多… 部分中,我们将向您展示如何做到这一点。

要完全设置 THREE.PerspectiveCamera,我们需要执行几个步骤:

  1. 我们需要做的第一件事是实例化相机:

      camera = new THREE.PerspectiveCamera();
    

    这创建了相机实例,我们将在接下来的步骤中进行配置。

  2. 现在我们已经有了相机,我们首先需要定义视口宽度和高度之间的宽高比:

      camera.aspect = window.innerWidth / window.innerHeight;
    

    在我们的菜谱中,我们使用浏览器的完整宽度和高度,因此我们根据 window.innerWidthwindow.innerHeight 属性指定相机的宽高比。如果我们使用具有固定宽度和高度的 div 元素,您应该使用这些值之间的比率作为相机的 aspect 函数。

  3. 我们接下来需要定义的两个属性是 nearfar 属性:

      camera.near = 0.1;
      camera.far = 1000;
    

    这两个属性定义了相机将要渲染的场景区域。使用这两个值,相机将从距离相机 0.11000 的距离渲染场景。

  4. 可以定义的最后一个属性是(垂直)视野:

      camera.fov = 45;
    

    这个属性以度为单位定义了相机 看到的 区域。例如,人类有 120 度的水平视野,而在视频游戏中,通常使用大约 90 或 100 度的视野。

  5. 每当您更新相机的这四个属性之一时,您必须通知 Three.js 关于这种变化。您可以通过添加以下行来完成此操作:

      camera.updateProjectionMatrix();
    
  6. 现在,剩下要做的就是放置相机并将其添加到场景中:

      camera.position.x = 15;
      camera.position.y = 16;
      camera.position.z = 13;
      scene.add(camera);
    

到目前为止,我们可以使用这个相机与任何可用的渲染器一起渲染一个像这样的场景:renderer.render(scene, camera)

它是如何工作的...

要理解这些属性如何影响屏幕上渲染的内容,最好的方法是查看以下图表,它展示了这些属性:

它是如何工作的...

图中 近平面 的位置基于相机的 near 属性。远平面 基于相机的 far 属性和图中显示的 fov,这对应于 fov 属性。使用 fov 属性,您定义了垂直视野。水平视野基于宽高比,您可以通过相机上的 aspect 属性来定义。

还有更多…

在这个菜谱中,我们分别设置每个属性。THREE.PerspectiveCamera 还提供了一个构造函数,您可以使用它在一个语句中设置所有这些属性:

  camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

还要记住,THREE.PerspectiveCamera 是从标准的 Three.js THREE.Object3D 对象扩展而来的。这意味着这个相机可以像任何其他对象一样旋转和移动。

参见

  • 将相机缩放到对象 配方中,我们使用了相机的 fov 属性来放大对象,而在 使用正交相机 配方中,我们将展示 Three.js 提供的两个相机中的第二个,即 THREE.OrthographicCamera

使用正交相机

在大多数情况下,你会使用 THREE.PerspectiveCamera 来渲染你的场景。使用这种相机,结果是具有逼真视角的场景。Three.js 提供了一个替代相机 THREE.OrthographicCamera。这种相机使用正交投影来渲染场景。在这种投影类型中,所有对象的大小都相同,无论它们与相机的距离如何。这与 THREE.PerspectiveCamera 相比,在相机更远处的对象看起来更小。这常用于游戏中的假 3D 效果,如《模拟人生》或更早版本的《模拟城市》(图片来自 glasnost.itcarlow.ie/~powerk/GeneralGraphicsNotes/projection/projection_images/iosmetric_sim_city.jpg)。

使用正交相机

在本配方中,我们将向您展示如何配置 THREE.OrthographicCamera,以便您可以为您的场景创建这种假 3D 效果。

准备工作

对于这个配方,我们使用的唯一来自 Three.js 的对象是 THREE.OrthographicCamera。这个相机在标准的 Three.js 分发中可用,因此不需要包含任何外部 JavaScript 文件。我们提供了一个示例,展示了 Three.Orthographic Camera 的实际应用。你可以使用这个相机来更好地理解你可以用来配置相机的属性。如果你打开 03.04-use-an-orthographic-camera.html,你可以看到使用 THREE.OrthographicCamera 渲染的多个立方体。在右上角的菜单中,你可以调整相机的配置。

准备工作

现在,让我们看看你需要采取哪些步骤来设置这个相机。

如何做到这一点...

在 Three.js 中设置正交相机需要执行几个非常简单的步骤:

  1. 我们需要做的第一件事是创建相机实例:

      camera = new THREE.OrthographicCamera();
    

    这将创建 THREE.OrthographicCamera,它使用一些默认值进行配置。

  2. 下一步是定义这个相机的边界:

      camera.left = window.innerWidth / -2;
      camera.right =  window.innerWidth / 2;
      camera.top = window.innerHeight / 2;
      camera.bottom = window.innerHeight / - 2;
    

    这定义了由这个相机渲染的区域。在本配方的 还有更多… 部分,我们将解释它是如何工作的。

  3. 最后,我们必须设置相机的 nearfar 属性。这些属性定义了从相机到渲染的距离:

      camera.near = 0.1;
      camera.far = 1500;
    
  4. 当我们在构造函数中不传递参数时,我们必须通知 Three.js 我们已更改相机的参数。为此,我们必须添加以下行:

      camera.updateProjectionMatrix();
    
  5. 最后一步是定位和调整相机:

      camera.position.x = -500;
      camera.position.y = 200;
      camera.position.z = 300;
      camera.lookAt(scene.position);
    
  6. 现在,我们只需像使用任何其他相机一样使用这个相机,并渲染出这样的场景:

      renderer.render(scene, camera);
    

它是如何工作的...

理解这个相机的工作原理的最简单方法是通过查看以下图示:

工作原理...

图中你看到的这个方块是正交相机渲染的区域。在这个图中,你还可以看到我们在相机上定义的leftrighttopbottom属性,这些属性定义了这个方块的边界。最后两个属性,即nearfar,用于定义近平面和远平面。有了这六个属性,我们可以定义使用THREE.OrthographicCamera渲染的完整方块。

还有更多…

我们也可以通过在构造函数中传入这些参数来配置THREE.OrthographicCamera

  camera = new THREE.OrthographicCamera(window.innerWidth / -2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, 0.1, 1500);

一个额外的优势是,这样你就不需要显式调用camera.updateProjectionMatrix()

相关内容

  • Three.js 提供了两种类型的相机。如果你想使用THREE.PerspectiveCamera,请查看使用透视相机食谱,其中解释了创建和配置透视相机的步骤。

创建 2D 叠加

在大多数食谱中,我们只关注 Three.js 的 3D 方面。我们展示了解释 3D 对象和场景如何渲染、如何使用不同的相机来查看它们以及如何通过材质来改变它们外观的食谱。当你创建游戏时,通常也会在你的 3D 场景之上有一个 2D 层。你可以用它来显示生命值条、2D 地图、存货清单等等。在这个食谱中,我们将向你展示如何使用THREE.OrthogonalCameraTHREE.PerspectiveCamera一起创建 2D 叠加。

准备就绪

对于这个食谱,我们需要一个用作叠加层的图像。为了演示这个食谱,我们创建了一个简单的图像,看起来像这样:

准备就绪

在这个食谱中,我们将结合这个静态图像和一个 3D 场景来创建在浏览器中打开03.05-create-an-hud-overview.html示例时可以看到的场景:

准备就绪

在这个示例中,你可以看到我们有一个 3D 旋转的场景,上面有一个静态的 2D 叠加层。

如何做…

让我们看看你需要采取的步骤:

  1. 让我们从创建 2D 叠加开始。在这个食谱中,我们使用的叠加层具有固定的宽度和高度(800 x 600)。因此,在我们创建相机之前,让我们首先创建一个div变量,它作为渲染场景的容器:

      container = document.createElement( 'div' );
      container.setAttribute(
        "style","width:800px; height:600px");
      document.body.appendChild( container );
    
  2. 接下来,让我们创建用于渲染叠加层的相机。为此,我们需要THREE.OrthographicCamera:

      orthoCamera = new THREE.OrthographicCamera( 
        WIDTH / - 2, WIDTH / 2,     HEIGHT / 2, HEIGHT / - 2, - 500, 1000 );
      orthoCamera.position.x = 0;
      orthoCamera.position.y = 0;
      orthoCamera.position.z = 0;
    

    WIDTHHEIGHT属性被定义为具有 800 和 600 值的常量。这段代码创建并定位了一个标准的THREE.OrthographicCamera对象。

  3. 对于 2D 叠加,我们创建了一个单独的场景,我们将 2D 元素放入其中:

      orthoScene = new THREE.Scene();
    
  4. 我们想要添加到 2D 场景中的唯一东西是我们在本食谱的准备就绪部分展示的叠加图像。由于它是一个 2D 图像,我们将使用一个THREE.Sprite对象:

      var spriteMaterial = new THREE.SpriteMaterial({map: THREE.ImageUtils.loadTexture("../assets/overlay/overlay.png")});
      var sprite = new THREE.Sprite(spriteMaterial);
      sprite.position.set(0,0,10);
      sprite.scale.set(HEIGHT,WIDTH,1);
      orthoScene.add(sprite);
    

    THREE.Sprite无论其与摄像机的距离如何,总是以相同的大小(1x1 像素)渲染。为了使精灵全屏,我们将x轴缩放为 800(WIDTH),将y轴缩放为 600(HEIGHT)。使用我们在前面的代码片段中使用的THREE.SpriteMaterial,我们将叠加图像指向,以便在添加THREE.Sprite到场景时显示它。

  5. 到目前为止,我们已经有了THREE.OrthogonalCameraTHREE.Scene,它们以 800x600 的图像显示叠加层。下一步是在我们想要应用此叠加层的 3D 屏幕上创建。在这里,你不需要做任何特别的事情;你可以通过定义THREE.PerspectiveCameraTHREE.Scene并添加一些灯光和对象来创建 3D 场景。对于这个配方,我们假设你已经有一个具有以下名称的相机和场景:

      persCamera = new THREE.PerspectiveCamera(60, WIDTH / HEIGHT, 1, 2100 );
      persScene = new THREE.Scene();
    
  6. 在我们将要进入的渲染循环中定义我们想要将 2D 场景作为叠加层渲染之前,我们需要在渲染器上配置一个额外的属性:

      renderer = new THREE.WebGLRenderer();
      renderer.setClearColor( 0xf0f0f0 );
      renderer.setSize( 800, 600 );
     renderer.autoClear = false;
      container.appendChild( renderer.domElement );
    

    THREE.WebGLRenderer上,我们将autoclear属性设置为false。这意味着在渲染器渲染场景之前,屏幕不会被自动清除。

  7. 最后一步是修改渲染循环。我们首先想要渲染 3D 场景,并且在不清除 3D 渲染输出之前,在顶部渲染叠加层:

      function render() {
        renderer.clear();
        renderer.render( persScene, persCamera );
        renderer.clearDepth();
        renderer.render( orthoScene, orthoCamera );
      }
    

    在渲染循环中,我们首先通过在渲染器上调用clear函数来清除当前输出。我们需要这样做,因为我们禁用了渲染器的autoclear。现在,我们渲染 3D 场景,在渲染 2D 叠加层之前,我们在渲染器上调用clearDepth函数。这确保 2D 叠加层完全渲染在顶部,并且不会与 3D 场景在位置上相交。所以最后,我们通过传递orthoSceneorthoCamera来渲染 2D 叠加层。

它是如何工作的...

这个配方的原理实际上非常简单。我们可以使用相同的渲染器在同一个渲染循环中渲染多个场景,每个场景使用多个不同的摄像机。这样,我们可以在彼此之上定位各种渲染结果。使用THREE.OrthoGraphic摄像机和THREE.Sprite,很容易将对象定位在屏幕上的绝对位置。通过将其缩放至所需大小并应用纹理,我们可以使用渲染器显示图像。这种输出与常规 3D 结果相结合,允许你创建这类叠加层。

参见

有几个配方使用正交摄像机和更高级的技巧来组合最终的渲染:

  • 在本章中,我们探讨了如何在使用正交摄像机配方中设置THREE.OrthographicCamera

  • 在第四章中,我们将展示如何在使用 HTML canvas 作为纹理使用 HTML 视频作为纹理配方中使用 HTML5 canvas 和 HTML5 视频作为纹理的输入。

  • 在第六章中,点云和后期处理,我们在设置后期处理管道配方中向您展示了如何设置更复杂的渲染管道。

围绕场景旋转相机

在第二章中,几何体和网格,我们已经向您展示了多个配方,解释了如何旋转对象。在这个配方中,我们将向您展示如何在保持相机始终注视场景中心的同时旋转相机。

准备工作

对于这个配方,我们将使用标准的THREE.PerspectiveCamera对象,我们将它围绕一个简单的场景旋转。要查看最终结果,请在您的浏览器中打开03.08-rotate-camera-around-scene-y-axis.html示例。

准备工作

在这个网页上,你可以看到相机围绕场景旋转,而地板、箱子和灯光保持在同一位置。

如何操作...

要完成这个任务,我们只需要执行几个非常简单的步骤:

  1. 我们需要做的第一件事是创建THREE.PerspectiveCamera并将其放置在场景中的某个位置:

      // create a camera, which defines where we're looking at.
      camera = new THREE.PerspectiveCamera(45,window.innerWidth / window.innerHeight, 0.1, 1000);
      // position and point the camera to the center of the scene
      camera.position.x = 15;
      camera.position.y = 16;
      camera.position.z = 13;
      camera.lookAt(scene.position);
    
  2. 要旋转相机,我们在渲染循环中重新计算其位置如下:

      function render() {
        renderer.render(scene, camera);
        var x = camera.position.x;
        var z = camera.position.z;
        camera.position.x = x * Math.cos(control.rotSpeed) + z * Math.sin(control.rotSpeed);
        camera.position.z = z * Math.cos(control.rotSpeed) – x * Math.sin(control.rotSpeed);
        camera.lookAt(scene.position);
        requestAnimationFrame(render);
      }
    

    在这个渲染函数中,我们更新了camera.position.xcamera.position.z变量,并通过调用camera.lookAt(scene.position),确保我们始终注视场景的中心。

工作原理...

我们在这里做的是一些基本的向量数学。我们使用旋转矩阵执行相机的一个非常小的旋转。然而,与其他配方中使用的 3D 和 4D 矩阵不同,这次我们只使用一个 2D 矩阵(用渲染循环中的两个计算表示)。旋转后,我们只需确保相机仍然注视正确的位置,所以我们使用lookAt函数(它再次内部使用矩阵计算来确定如何将相机对准场景)。

更多内容...

在这个配方中,我们围绕场景的y轴旋转。这产生了一个非常平滑的动画,其中相机围绕场景旋转。当然,我们也可以将此应用于其他轴。我们提供了一个可以在本书提供的源代码中查看的示例。如果你在浏览器中打开03.08-rotate-camera-around-scene-x-axis.html,相机会围绕x轴旋转而不是y轴。

你需要做的唯一改变是在渲染循环中更改计算:

  function render() {
    renderer.render(scene, camera);
    var z = camera.position.z;
    var y = camera.position.y;
 camera.position.y = y * Math.cos(control.rotSpeed) + z * Math.sin(control.rotSpeed);
 camera.position.z = z * Math.cos(control.rotSpeed) – y * Math.sin(control.rotSpeed);
    camera.lookAt(scene.position);
    requestAnimationFrame(render);
  }

当你在浏览器中查看这个示例时,你可能会注意到一些奇怪的现象。在某个时刻,它看起来像相机在跳跃。原因是相机试图保持直立,所以当它旋转到顶部或底部时,它会迅速改变方向。

相关内容

在第二章中,几何体和网格,我们已经讨论了一些与旋转相关的食谱。如果你想要了解更多关于旋转或其所需的矩阵计算,请查看以下来自第二章,几何体和网格的食谱:

  • 围绕对象的自身轴旋转

  • 在空间中的点周围旋转对象

  • 应用矩阵变换

将渲染视图与调整大小的浏览器匹配

当你在 Three.js 中定义一个相机时,你需要定义其纵横比;对于渲染器,你需要定义其输出大小。通常,你会在设置初始场景时只做一次。这工作得很好,直到用户调整浏览器的大小。在这种情况下,相机的纵横比可能会改变,渲染器的输出大小也可能会改变。在这个食谱中,我们将向你展示你需要采取的步骤来响应屏幕大小的变化。

准备中

与每个食谱一样,我们提供了一个示例,你可以用它来测试和实验这个食谱。在你的浏览器中打开03.06-change-the-camera-on-screen-resize.html,并将屏幕调得很小。

准备中

你会看到场景中显示的信息量相同——只是渲染得更小。当你现在再次增加屏幕大小时,你会看到 Three.js 总是使用完整的可用空间。

准备中

如何做到这一点...

在这个食谱中,我们将向网页添加一个调整大小处理程序,该处理程序会对调整大小事件做出反应。添加此处理程序只需要几个步骤:

  1. 我们需要添加的第一件事是在调整大小事件发生时调用的函数。以下代码片段显示了我们将要调用的onResize函数:

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

    在这个代码片段中,我们首先根据新的宽度和高度重新计算相机的纵横比。由于 Three.js 缓存了相机的某些方面,我们必须调用updateProjectionMatrix()函数以确保使用新的纵横比。我们还更改了渲染器的大小为新宽度和高度,以便使用完整的屏幕空间。

  2. 现在我们已经有了我们的更新函数,我们需要定义一个事件监听器:

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

    如你所见,我们添加了一个对resize事件的监听器。所以每当屏幕调整大小时,提供的函数,即onResize,将被调用。

它是如何工作的...

当浏览器内发生某些事情(按钮被点击,鼠标移动,窗口大小调整等)时,浏览器会抛出一个事件。从 JavaScript 中,你可以注册监听器来响应这些事件。在这个菜谱中,我们使用 resize 事件来监听窗口大小的任何变化。有关此事件的更多信息,你可以查看 Mozilla 提供的出色文档,请参阅 developer.mozilla.org/en-US/docs/Web/Events/resize

将世界坐标转换为屏幕坐标

如果你正在创建一个在 3D 世界之上提供 2D 界面的游戏,例如,如 创建 2D 浮层 菜谱中所示,你可能想知道 3D 坐标如何映射到你的 2D 浮层。如果你知道 2D 坐标,你可以在 2D 浮层上添加各种视觉效果,例如跟踪代码或让 2D 浮层与 3D 场景中的对象交互。

准备工作

你不需要执行任何步骤来为这个菜谱做准备。在这个菜谱中,我们可以使用 Three.js 中可用的 THREE.Projector 对象来确定正确的坐标。你可以通过在浏览器中打开 03.07-convert-world-coordintate-to-screen-coordinates.html 来查看这个菜谱的结果,如下截图所示:

准备工作

当你打开这个示例时,盒子出现在随机位置。当你点击右上角菜单中的 calculateScreenCoordinate 按钮时,盒子的中心 xy 坐标将会显示出来。

如何做到这一点...

要将世界坐标转换为屏幕坐标,我们使用几个 Three.js 的内部对象:

  1. 我们需要的第一个对象是 THREE.Projector

      var projector = new THREE.Projector();
    
  2. 接下来,我们使用这个投影仪将立方体的位置投影到相机上:

    var vector = new THREE.Vector3();
      projector.projectVector(
        vector.setFromMatrixPosition( object.matrixWorld ),
        camera );
    

    vector 变量现在将包含对象的位置,这是由 camera 对象看到的。

  3. 当你投影一个向量,就像我们在第二步中做的那样,得到的 xy 值范围从 -1 到 1。所以在这个最后一步中,我们将这些值转换为当前的屏幕宽度和高度:

      var width = window.innerWidth;
      var height = window.innerHeight;
      var widthHalf = width / 2;
      var heightHalf = height / 2;
      vector.x = ( vector.x * widthHalf ) + widthHalf;
      vector.y = - ( vector.y * heightHalf ) + heightHalf;
    

    到目前为止,vector 变量将包含 object 中心的屏幕坐标。你现在可以使用这些坐标与标准的 JavaScript、HTML 和 CSS 结合,添加效果。

它是如何工作的...

在这个菜谱中,我们使用了 Three.js 渲染场景时使用的相同效果。当你渲染一个场景时,对象会被投影到一个相机上,这决定了需要渲染的区域以及对象出现的位置。通过投影器类,我们可以对单个向量执行这种投影。结果是,基于使用的相机,这个向量在二维空间中的位置。

参见

  • 在这个食谱中,我们将世界坐标转换为屏幕坐标。这实际上相当简单,因为我们已经拥有了所有必要的信息(在三维空间中)来正确确定坐标(在二维空间中)。在在场景中选择对象食谱中,我们将屏幕坐标转换为世界坐标,这更难做到,因为我们没有可以使用任何深度信息。

在场景中选择对象

对于 Three.js 应用程序来说,一个常见的需求是与场景交互。你可能创建了一个射击游戏,其中你想要使用鼠标进行瞄准,或者一个角色扮演游戏,其中你需要与环境交互。在这个食谱中,我们将向你展示如何使用鼠标选择屏幕上渲染的对象。

准备工作

要应用这个效果,我们需要一个场景,其中我们可以选择一些对象。对于这个食谱,我们提供了一个示例,它是03.10-select-an-object-in-the-scene.html。如果你在浏览器中打开这个文件,你会看到许多对象在场景中移动。

准备工作

你可以用鼠标选择屏幕上的任何对象。第一次点击它们时,它们会变成透明,下一次点击时,它们又会变成实心。

如何操作...

为了完成这个食谱,我们需要完成几个步骤:

  1. 我们首先需要做的是设置鼠标监听器。我们希望在每次鼠标按钮被点击时触发一个 JavaScript 函数。为此,我们注册以下监听器:

      document.addEventListener('mousedown', onDocumentMouseDown, false);
    

    这将告诉浏览器在检测到mousedown事件时触发onDocumentMouseDown按钮。

  2. 接下来,我们定义onMouseDown函数如下:

    function onDocumentMouseDown(event) { ... }
    

    当你按下鼠标左键时,这个函数将被调用。在接下来的步骤中,我们将向你展示如何将内容放入这个函数以检测哪个对象被选中。

  3. 我们首先需要做的是将鼠标点击的xy坐标转换为THREE.PerspectiveCamera可以理解的坐标:

    var projector = new THREE.Projector();
      var vector = new THREE.Vector3(
        (event.clientX / window.innerWidth) * 2 - 1,
        -(event.clientY / window.innerHeight) * 2 + 1,
        0.5);
      projector.unprojectVector(vector, camera);
    
    

    到目前为止,向量将包含xy坐标,这些坐标是相机和 Three.js 理解的坐标。

  4. 现在我们可以使用另一个 Three.js 对象,即THREE.Raycaster,来确定场景中哪些对象可能位于我们点击的位置:

      var raycaster = new THREE.Raycaster(camera.position,vector.sub(camera.position).normalize());
      var intersects = raycaster.intersectObjects([sphere, cylinder, cube]);
    

    在这里,我们首先创建THREE.Raycaster并使用intersectObjects函数来确定是否选择了spherecylindercube。如果一个对象被选中,它将被存储在intersects数组中。

  5. 现在我们可以处理intersects数组。第一个元素将是离相机最近的元素,在这个食谱中,这是我们感兴趣的:

      if (intersects.length > 0) {
        intersects[0].object.material.transparent = true;
        if (intersects[0].object.material.opacity === 0.5) {
          intersects[0].object.material.opacity = 1;
        } else {
          intersects[0].object.material.opacity = 0.5;
        }
      }
    

    在这个食谱中,我们只是在对象被点击时切换其不透明度。

就这样。使用这个设置,你可以用鼠标选择对象。

它是如何工作的...

这个食谱通过使用 THREE.RayCaster 来工作。正如其名所示,使用 THREE.RayCaster,你会在场景中射出一束光线。这束光线的路径基于相机的属性、相机的位置以及提供给 intersectObjects 函数的对象。对于提供的每个对象,Three.js 会确定使用 THREE.RayCaster 射出的光线是否能击中指定的对象。

还有更多

可以添加的一个有趣的效果,并且能更好地可视化正在发生的事情,就是渲染由 THREE.RayCaster 射出的光线。你只需在本食谱的第 5 步中添加以下内容就可以很容易地做到这一点:

  var points = [];
  points.push(new THREE.Vector3(camera.position.x, camera.position.y - 0.2, camera.position.z));
  points.push(intersects[0].point);
  var mat = new THREE.MeshBasicMaterial({
    color: 0xff0000,
    transparent: true,
    opacity: 0.6
  });
  var tubeGeometry = new THREE.TubeGeometry(new THREE.SplineCurve3(points), 60, 0.001);
  var tube = new THREE.Mesh(tubeGeometry, mat);
  scene.add(tube);

这段代码片段并没有什么特别之处。我们只是从相机的位置(在 y 轴上有一个小的偏移,否则我们什么也看不到)画一条线到光线相交的位置。结果,你可以在本食谱中 准备就绪 部分讨论的示例中看到,看起来像这样:

还有更多

参见

  • 在这个食谱中,我们将二维坐标转换为三维坐标。在 将世界坐标转换为屏幕坐标 的食谱中,我们解释了如何做相反的操作。

第四章:材料和纹理

在本章中,我们将介绍以下食谱:

  • 使用凹凸贴图给网格添加深度

  • 使用法线贴图给网格添加深度

  • 使用 HTML 画布作为纹理

  • 使用 HTML 视频作为纹理

  • 创建具有多个材料的网格

  • 使用单独的材料为面着色

  • 设置重复纹理

  • 使物体的一部分透明

  • 使用立方体贴图创建反射材料

  • 使用动态立方体贴图创建反射材料

  • 使用 Blender 创建自定义 UV 映射

  • 配置混合模式

  • 使用阴影贴图创建固定阴影

简介

Three.js 提供了大量的不同材料,并支持许多不同类型的纹理。这些纹理提供了一种创建有趣效果和图形的绝佳方式。在本章中,我们将向您展示一些食谱,让您充分利用 Three.js 提供的这些组件。

使用凹凸贴图给网格添加深度

对于详细模型,你需要具有大量顶点和面的几何体。如果一个几何体包含非常多的顶点,加载几何体和渲染它所需的时间将比简单模型所需的时间更长。如果你有一个包含大量模型的场景,尝试尽量减少顶点的数量以获得更好的性能是个好主意。你可以使用多种不同的技术来实现这一点。在本食谱中,我们将向您展示如何使用凹凸贴图纹理为您的模型添加深度感。

准备工作

为了准备这个食谱,我们需要获取我们想要在几何体上使用的纹理。对于这个食谱,我们需要两个纹理:一个颜色图,这是一个标准纹理,以及一个凹凸贴图,它描述了与标准纹理相关的深度。以下截图显示了我们将使用的颜色图(你可以在本书提供的源代码中的assets/textures文件夹中找到这些纹理):

准备中

如您所见,这是一个简单的石头墙颜色图。除了这个纹理,我们还需要凹凸贴图。凹凸贴图是一种灰度图像,其中每个像素的强度决定了高度:

准备中

从前面的截图,你可以看到石头和灰浆之间的部分高度较低,因为它是深色,与颜色较浅的石头本身相比。你可以在浏览器中打开04.01-add-depth-to-mesh-with-bump-map.html示例,查看这个食谱结束时的结果。

准备中

从前面的截图,你可以看到两个立方体。左侧的立方体没有使用凹凸贴图进行渲染,而右侧的立方体使用了凹凸贴图。正如你所见,右侧的立方体比左侧的立方体显示出更多的深度和细节。

如何做到这一点...

当你有纹理时,使用它们给模型添加深度是非常直接的:

  1. 首先,创建您想要与振荡贴图一起使用的几何体:

    var cubeGeometry = new THREE.BoxGeometry(15, 15, 15);
    

    在这个配方中,我们创建 THREE.BoxGeometry,但您可以使用振荡贴图与任何类型的几何体一起使用。

  2. 下一步是创建我们定义振荡贴图的材质:

    var cubeBumpMaterial = new THREE.MeshPhongMaterial();
    
    cubeBumpMaterial.map = THREE.ImageUtils.loadTexture(
                        "../assets/textures/Brick-2399.jpg");
    cubeBumpMaterial.bumpMap = THREE.ImageUtils.loadTexture(
                "../assets/textures/Brick-2399-bump-map.jpg");
    

    在这里,我们创建 THREE.MeshPhongMaterial 并设置其 mapbumpMap 属性。map 属性指向颜色贴图纹理,而 bumpMap 属性指向灰度振荡贴图纹理。

  3. 现在您只需创建 THREE.Mesh 并将其添加到场景中:

    var bumpCube = new THREE.Mesh(cubeGeometry,
                                  cubeBumpMaterial);
    scene.add(bumpCube);
    

通过这三个简单的步骤,您已经创建了一个使用振荡贴图来增加深度的立方体。

它是如何工作的...

振荡贴图中每个像素的值决定了与该纹理部分相关联的高度。在渲染场景时,Three.js 使用这些信息来确定光线如何影响它正在渲染的像素的最终颜色。结果是,即使没有定义一个非常详细的模型,我们也可以添加额外的深度错觉。如果您想了解更多关于振荡贴图如何工作的详细信息,请查看这个网站以获取非常详细的解释:www.tweak3d.net/articles/bumpmapping/.

更多内容…

在这个配方中,我们向您展示了定义振荡贴图的默认方法。然而,您可以使用一个额外的属性来调整振荡贴图。我们在这个配方中使用的材质 cubeBumpMaterial 也有一个 bumpScale 属性。使用这个属性,您可以设置振荡贴图影响深度的程度。如果这个值非常小,您会看到一些增加的深度,如果这个值更高,您会看到更明显的深度效果。您可以在本食谱的示例中设置此属性(04.01-add-depth-to-mesh-with-bump-map.html)。

参考内容

  • 有一种额外的方法可以为您的网格添加细节和深度。在 使用正常贴图为网格添加深度 配方中,我们展示了如何使用正常贴图而不是振荡贴图来添加深度和细节。在 从高度图创建几何体 配方中,第二章,几何体和网格,我们向您展示了创建 THREE.Geometry 来使用振荡贴图的不同方法。

使用正常贴图为网格添加深度

使用振荡贴图为网格添加深度 配方中,我们展示了如何使用特定的纹理为网格添加深度和细节。在这个配方中,我们提供了一种在不增加几何体顶点数的情况下添加更多深度和细节的方法。为此,我们将使用正常贴图。正常贴图描述了每个像素的法向量,该向量应用于计算光线如何影响几何体中使用的材质。

准备工作

要使用正常贴图,我们首先需要一个颜色贴图和一个正常贴图。对于这个配方,我们使用了两个截图。第一个是颜色贴图:

准备中

下一个截图是正常贴图:

准备中

现在我们已经得到了这两张图片,让我们首先看看这在实践中会是什么样子。要看到正常贴图的实际应用,请打开04.02-add-depth-to-mesh-with-normal-map.html示例:

准备就绪

在这个例子中,您可以在左侧看到一个标准渲染的立方体,在右侧可以看到添加了正常贴图的立方体。您可以直接看到右侧立方体的面看起来比左侧立方体的面更详细。

如何操作...

添加正常贴图实际上非常简单:

  1. 首先,创建我们想要渲染的几何形状:

    var cubeGeometry = new THREE.BoxGeometry(15, 15, 15);
    

    对于这个配方,我们使用一个简单的THREE.BoxGeometry对象,但你也可以使用你想要的任何几何形状。

  2. 现在我们已经得到了一个几何形状,我们创建材质并配置属性:

    var cubeNormalMaterial = new THREE.MeshPhongMaterial();
    cubeNormalMaterial.map = THREE.ImageUtils.loadTexture(
                      "../assets/textures/chesterfield.png");
    cubeNormalMaterial.normalMap = THREE.ImageUtils.loadTexture(
               "../assets/textures/chesterfield-normal.png");
    

    map属性包含标准纹理,而normalMap属性包含正常纹理,这是我们在这份配方准备就绪部分向您展示的。

  3. 现在剩下的只是创建一个THREE.Mesh对象并将其添加到场景中,如下所示:

    var normalCube = new THREE.Mesh(
                         cubeGeometry, cubeNormalMaterial);
    scene.add(normalCube);
    

如您从这些步骤中看到的,使用正常贴图非常简单。

它是如何工作的...

在 3D 建模中,有几个数学概念是重要的,需要理解。其中之一是一个法向量的概念。法向量是垂直于几何形状面面的向量。这在上面的屏幕截图中显示:

它是如何工作的...

每条蓝色线条代表法向量,这是垂直于该表面面的向量。在正常贴图中,这些向量的方向以 RGB 值的形式显示。当你将正常贴图应用于特定的面时,Three.js 会使用这个正常贴图和面的法线信息来为该面添加深度,而不添加额外的顶点。有关正常贴图如何使用的更多信息,请参考www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/网站。

更多...

你可以微调从正常贴图中应用到几何形状面的高度和方向。为此,你可以使用normalScale属性,如下所示:

normalCube.material.normalScale.x = 1;
normalCube.material.normalScale.y = 1;

要看到这个效果的实际应用,请查看这个配方的示例04.02-add-depth-to-mesh-with-normal-map.html,在那里你可以使用右上角的菜单来改变这个值。

参见

  • 正常贴图的替代方案是凹凸贴图。在使用凹凸贴图给网格添加深度的配方中,我们向您展示了如何使用这种贴图而不是正常贴图。

使用 HTML 画布作为纹理

通常当你使用纹理时,你会使用静态图像。然而,在 Three.js 中,也可以创建交互式纹理。在这个食谱中,我们将向您展示如何使用 HTML5 画布元素作为纹理的输入。一旦您通知 Three.js 关于纹理使用的这个变化,任何对这个画布的更改都会自动反映出来。

准备工作

对于这个食谱,我们需要一个可以显示为纹理的 HTML5 画布元素。我们可以自己创建一个并添加一些输出,但在这个食谱中,我们选择了其他东西。我们将使用一个简单的 JavaScript 库,它将时钟输出到一个画布元素。生成的网格将看起来像这样(参见04.03-use-html-canvas-as-texture.html示例):

准备工作

渲染时钟所使用的 JavaScript 代码基于这个网站的代码:saturnboy.com/2013/10/html5-canvas-clock/。为了在我们的页面中包含渲染时钟的代码,我们需要在head元素中添加以下内容:

<script src="img/clock.js"></script>

如何做到这一点...

要将画布用作纹理,我们需要执行几个步骤:

  1. 我们需要做的第一件事是创建画布元素:

    var canvas = document.createElement('canvas');
    canvas.width=512;
    canvas.height=512;
    

    在这里,我们通过编程创建一个 HTML 画布元素,并定义一个固定的宽度。

  2. 现在我们已经得到了一个画布,我们需要在它上面渲染我们用作这个食谱输入的时钟。这个库非常容易使用;您只需传入我们刚刚创建的画布元素:

    clock(canvas);
    
  3. 到目前为止,我们已经得到了一个渲染并更新时钟图像的画布。我们现在需要做的是创建一个几何体和一个材质,并使用这个画布元素作为这个材质的纹理:

    var cubeGeometry = new THREE.BoxGeometry(10, 10, 10);
    var cubeMaterial = new THREE.MeshLambertMaterial();
    cubeMaterial.map = new THREE.Texture(canvas);
    var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
    

    要从画布元素创建纹理,我们只需创建一个新的THREE.Texture实例,并传入我们在步骤 1 中创建的canvas元素。我们将这个纹理分配给cubeMaterial.map属性,这样就完成了。

  4. 如果您在这个步骤运行食谱,您可能会看到时钟渲染在立方体的侧面。然而,时钟不会自动更新。我们需要告诉 Three.js 画布元素已经更改。我们通过在渲染循环中添加以下内容来完成此操作:

    cubeMaterial.map.needsUpdate = true;
    

    这通知 Three.js 我们的画布纹理已更改,需要在下次渲染场景时更新。

通过这四个简单的步骤,您可以轻松创建交互式纹理,并将您在画布元素上创建的所有内容用作 Three.js 中的纹理。

它是如何工作的...

这实际上是如何工作的非常简单。Three.js 使用 WebGL 来渲染场景并应用纹理。WebGL 原生支持使用 HTML 画布元素作为纹理,因此 Three.js 只需将提供的画布元素传递给 WebGL,它就会被处理成任何其他纹理。

参见

  • 除了使用图像和画布元素作为纹理外,我们还可以使用视频元素作为纹理。在使用 HTML 视频作为纹理食谱中,我们向您展示如何使用 HTML 视频元素作为纹理的输入。

使用 HTML 视频作为纹理

现代浏览器在无需任何插件的情况下播放视频方面提供了很好的支持。使用 Three.js,我们甚至可以使用这个视频作为纹理的输入。在这个配方中,我们将向您展示输出视频在立方体侧面所需的步骤。

准备工作

当然,对于这个配方,我们需要一个要播放的视频。我们使用了 Blender 制作的影片《Sintel》的预告片(www.sintel.org/),它是免费提供的。要查看此配方的结果,请在您的浏览器中打开04.04-use-html-video-as-texture.html

准备工作

当您运行此示例时,您可以看到视频正在一个立方体的侧面播放,并且即使在立方体旋转时也会持续更新。

如何做到这一点...

要实现这种效果,我们需要将 HTML 视频元素定义为纹理的源。为此,执行以下步骤:

  1. 我们首先需要一种播放视频的方法。为此,我们在页面的body元素中添加以下 HTML 元素:

    <video id="video" autoplay loop style="display:none">
        <source src="img/sintel_trailer-480p.mp4" type='video/mp4'>
        <source src="img/sintel_trailer-480p.webm" type='video/webm'>
        <source src="img/sintel_trailer-480p.ogv" type='video/ogg'>
    </video>
    

    使用这段 HTML,我们将加载视频,并在加载后使用autoplayloop属性循环播放。由于我们设置了display:none,这个video元素不会显示在页面上。

  2. 现在我们已经播放了视频,我们可以获取这个元素的引用,并使用它来创建一个纹理:

    var video = document.getElementById( 'video' );
    
    videoTexture = new THREE.Texture( video );
    videoTexture.minFilter = THREE.LinearFilter;
    videoTexture.magFilter = THREE.LinearFilter;
    videoTexture.format = THREE.RGBFormat;
    videoTexture.generateMipmaps = false;
    

    这里使用的minFiltermagFilterformatgenerateMipmaps属性在将视频用作纹理时提供最佳结果和性能。

  3. 到目前为止,我们得到了一个可以像其他纹理一样使用的纹理:

    var cubeGeometry = new THREE.BoxGeometry(1,9,20);
    var cubeMaterial = new THREE.MeshBasicMaterial({map:videoTexture});
    

    在这里,我们将材质的map属性设置为视频纹理。因此,我们创建的任何使用此材质的THREE.Mesh对象都会显示视频。

  4. 要完成这个配方,创建THREE.Mesh对象并将其添加到场景中:

    var cube = new THREE.Mesh(cubeGeometry, 
                              cubeMaterial);
    scene.add(cube);
    
  5. Three.js 通常缓存纹理,因为它们通常不会经常改变。然而,在这个配方中,纹理是持续变化的。为了通知 Three.js 纹理已更改,我们需要在渲染循环中添加以下内容:

    function render() {
       ...
       videoTexture.needsUpdate = true;
       ...
    }
    

您可以使用这种方法处理任何在浏览器中可以播放的视频。

它是如何工作的...

WebGL,Three.js 用来渲染场景的工具,原生支持使用视频元素作为纹理的输入。Three.js 只需将视频元素传递给 WebGL,无需进行任何预处理。在 WebGL 代码中,视频显示的当前图像被转换为纹理。每次我们设置videoTexture.needsUpdatetrue时,纹理就会在 WebGL 中更新。

还有更多...

在处理视频元素时,需要记住的一件事是,不同的浏览器对视频格式的支持各不相同。有关哪些浏览器支持哪些格式的最新概述,可以在维基百科上找到en.wikipedia.org/wiki/HTML5_video#Browser_support

参见

  • 另一种轻松创建可变纹理的方法在使用 HTML canvas 作为纹理配方中解释。在这个配方中,我们解释了如何使用 HTML canvas元素作为纹理的输入。

创建具有多种材料的网格

当你创建THREE.Mesh时,你可以指定一个用于该网格的单个材料。在大多数情况下,这将是足够的。然而,也有一些情况下,你想结合多种材料。例如,你可能想将THREE.MeshLambertMaterial与显示几何体线框的材料结合。在这个配方中,我们将向你展示创建使用多种材料的网格所需的步骤。

准备工作

对于这个配方,我们不需要额外的资源或库。如果你想查看这个配方的结果,请在浏览器中打开04.05-create-a-mesh-with-multiple-materials.html示例。

准备中

在前面的屏幕截图中,你可以看到一个圆柱体。这个圆柱体是用两种材料渲染的。在下一节中,我们将向你展示创建这个圆柱体所需的步骤。

如何操作...

要创建一个多材料网格,Three.js 提供了一个辅助函数。你可以使用THREE.SceneUtils来做这件事,就像在接下来的几个步骤中展示的那样:

  1. 首先你需要做的是创建你想要使用的几何体。对于这个配方,我们使用了一个简单的THREE.CylinderGeometry对象:

    var cylinderGeometry = new THREE.CylinderGeometry(
                               3, 5, 10,20);
    
  2. 在几何体之后,我们可以创建材料。你可以使用你想要的任何数量,但在这个配方中,我们只使用两个:

    var material1 = new THREE.MeshLambertMaterial(
         {color:0xff0000, 
          transparent: true, 
          opacity: 0.7});
    
    var material2 = new THREE.MeshBasicMaterial(
                               {wireframe:true});
    

    如你所见,我们创建了一个透明的THREE.MeshLambertMaterial对象和一个THREE.MeshBasicMaterial对象,它们只渲染线框。

  3. 现在,我们可以创建可以添加到场景中的对象。我们不是实例化THREE.Mesh,而是使用THREE.SceneUtils对象提供的createMultiMaterialObject函数:

    var cylinder = THREE.SceneUtils.createMultiMaterialObject(
                               cylinderGeometry,
                               [material1, material2]);
    

    你可以将这个函数的结果添加到场景中:

    scene.add(cylinder);
    

需要注意的一件事是,我们在这里创建的对象不是THREE.Mesh,而是THREE.Object3D。为什么创建了一个不同的对象将在下一节中解释。

它是如何工作的...

当你调用createMultiMaterialObject函数时,Three.js 只是简单地创建多个网格并将它们组合在一起。如果你打开 Three.js 文件并查找这个函数,你会看到以下代码:

function createMultiMaterialObject( geometry, materials ) {
var group = new THREE.Object3D();
for ( var i = 0, l = materials.length; i < l; i ++ ) {
group.add( new THREE.Mesh( geometry, materials[ i ] ) );
}
return group;
}

在这个函数中,Three.js 遍历提供的材料,并为每个材料创建一个新的THREE.Mesh对象。因为所有创建的网格都被添加到组中,所以结果看起来像是一个使用多种材料创建的单个网格。

相关内容

  • 当你使用这个配方中的方法创建使用多种材料的材料时,这些材料被应用到完整的几何体上。在为面的每个特定面使用不同的材料配方中,我们展示了如何为几何体的每个特定面使用不同的材料。

为面使用不同的材料

在 Three.js 中,每个几何体都由多个顶点和面组成。在大多数情况下,当你定义一个可以与几何体一起使用的材料时,你使用一个单一的材料。然而,使用 Three.js,你也可以为你的几何体的每个面定义一个独特的材料。例如,你可以使用这种方法为房屋模型的每一面应用不同的纹理。在这个菜谱中,我们将解释如何设置材料,以便你可以为单个面使用不同的纹理。

准备中

在这个菜谱中,我们不会使用任何外部纹理或库。然而,查看我们将在这个菜谱中创建的最终结果是有益的。为此,请在您的浏览器中打开04.06-use-separate-materials-for-faces.html示例。

准备中

在前面的屏幕截图中,你可以看到一个旋转的球体,其中每个面都渲染了不同的颜色,并且一半的面被设置为透明。在下一节中,我们将向您展示重现这一效果所需的步骤。

如何做...

要为每个面定义特定的材料,我们需要执行以下步骤:

  1. 我们首先需要做的是创建几何体。对于这个菜谱,我们使用THREE.SphereGeometry,但这些步骤也可以应用于其他几何体:

    var sphereGeometry = new THREE.SphereGeometry(3, 10, 10);
    
  2. 当我们在步骤 3 中创建材料时,我们提供了一个我们想要使用的材料数组。此外,我们还需要在每个面上指定我们将使用的数组中的材料。你可以用以下代码来完成这个操作:

    var materials = [];
    var count = 0;
    sphereGeometry.faces.forEach(function(face) {
        face.materialIndex = count++;
        var material = new THREE.MeshBasicMaterial(
            {color:Math.abs(Math.sin(count/70))*0xff0000});
        material.side = THREE.DoubleSide;
        if (count % 2 == 0) {
            material.transparent = true;
            material.opacity = 0.4;
        }
        materials.push(material);
    });
    

    在这个代码片段中,我们遍历了我们创建的几何体的所有面。对于每个面,我们将materialIndex属性设置为我们要使用的材料的索引。我们还在这个代码片段中为每个面创建一个独特的material对象,使其中一半透明,最后,将我们创建的材料推入材料数组。

  3. 到目前为止,材料数组包含几何体每个面的独特材料,并且对于所有面,materialIndex属性都指向该数组中的一个材料。现在,我们可以创建THREE.MeshFaceMaterial对象,并与几何体一起创建THREE.Mesh

      var sphere = new THREE.Mesh(
        sphereGeometry, new THREE.MeshFaceMaterial(materials));
      scene.add(sphere);
    

就这样。几何体的每个面都将使用它指向的材料。

它是如何工作的...

因为我们在每个THREE.Face对象上指定了materialIndex,Three.js 知道当它想要渲染特定的面时应该使用提供的数组中的哪个材料。你需要考虑的一件事是,这可能会影响你场景的性能,因为 Three.js 需要管理每个材料;然而,性能比使用单独的网格要好,但比将纹理组合在一起要差。

还有更多...

一些 Three.js 提供的几何体在实例化时已经设置了 materialIndex 属性。例如,当你创建 THREE.BoxGeometry 时,前两个面映射到 materialIndex 1,接下来的两个映射到 materialIndex 2,依此类推。所以,如果你想给盒子的侧面添加样式,你只需要提供一个包含六个材质的数组。

使用特定面的材质的另一个有趣用途是,你可以轻松地创建有趣的图案,例如,当你可以非常容易地创建一个类似这样的棋盘布局:

还有更多…

你只需要对如何分配 materialIndex 属性做一些小的改变,如下所示:

var plane = new THREE.PlaneGeometry(10, 10, 9, 9);

var materials = [];
var material_1 = new THREE.MeshBasicMaterial(
     {color:Math.random()*0xff0000, side: THREE.DoubleSide});
var material_2 = new THREE.MeshBasicMaterial(
      {color:Math.random()*0xff0000, side: THREE.DoubleSide});

materials.push(material_1);
materials.push(material_2);

var index = 0;
for (var i = 0 ; i < plane.faces.length-1 ; i+=2) {
    var face = plane.faces[i];
    var nextFace = plane.faces[i+1];
    face.materialIndex = index;
    nextFace.materialIndex = index;

    if (index === 0) {
        index = 1;
    } else {
        index = 0;
    }
}

相关内容

  • 如果你不想对特定面进行样式化,但想对一个完整的几何体应用多个材质,你可以查看 创建具有多个材质的网格 菜谱,其中我们解释了如何做到这一点。

设置重复纹理

有时候,当你找到一个想要应用的纹理时,你可能希望重复它。例如,如果你有一个大地面,你想要在上面应用无缝木纹纹理,你不想整个平面都应用一个单独的图像。Three.js 允许你定义纹理在几何体上使用时的重复方式。在这个菜谱中,我们将解释你需要采取的步骤来完成这个任务。

准备中

我们首先需要的是用作纹理的图像。为了达到最佳效果,你应该使用无缝纹理。无缝纹理可以在不显示相邻两个纹理之间的接缝的情况下重复。在这个菜谱中,我们将使用 webtreats_metal_6-512px.jpg 纹理,你可以在本书源代码中的 asset/textures 文件夹中找到它。

准备中

要在实际中看到重复效果,你可以在浏览器中打开 04.12-setup-repeating-textures.html 示例。

准备中

通过右上角的菜单,你可以定义纹理沿其 x 轴和 y 轴重复的频率。

如何操作...

设置重复纹理非常简单,只需几个步骤:

  1. 首先,创建几何形状和材质:

    var cubeGeometry = new THREE.BoxGeometry(10, 10, 10);
    var cubeMaterial = new THREE.MeshPhongMaterial();
    

    在这个菜谱中,我们使用 THREE.MeshPhongMaterial,但你也可以使用这个菜谱来处理所有允许使用纹理的材质。

  2. 接下来,我们加载纹理并将其设置在 cubeMaterial 上:

    cubeMaterial.map = THREE.ImageUtils.loadTexture
           ("../assets/textures/webtreats_metal_6-512px.jpg");
    
  3. 下一步是在纹理上设置 wrapSwrapT 属性:

    cubeMaterial.map.wrapS = cubeMaterial.map.wrapT 
                           = THREE.RepeatWrapping; 
    

    这些属性定义了 Three.js 是否应该拉伸纹理到边缘(THREE.ClampToEdgeWrapping)或使用 THREE.RepeatWrapping 重复纹理。

  4. 最后一步是设置沿 x 轴和 y 轴重复纹理的频率:

    cubeMaterial.map.repeat.set( 2, 2 );
    

    在这种情况下,我们在两个轴上重复纹理两次。

  5. 有趣的是,通过向 map.repeat.set 函数提供负值,你也可以镜像纹理。

它是如何工作的...

几何体内的每个面都有一个 UV 映射,它定义了应该使用纹理的哪个部分来表示该面。当您配置重复包装时,Three.js 会根据在 map.repeat 属性上设置的值更改这个 UV 映射。因为我们还定义了我们要使用 THREE.RepeatWrapping,WebGL 就知道如何解释这些更改后的 UV 值。

参见

  • 重复纹理是通过根据重复属性更改 UV 映射来工作的。您也可以手动配置 UV 映射,如 使用 Blender 创建自定义 UV 映射 菜谱中所示。

使对象的一部分透明

您可以使用 Three.js 提供的各种材质创建许多有趣的可视化效果。在这个菜谱中,我们将探讨您如何使用 Three.js 中可用的材质使对象的一部分透明。这将使您能够相对容易地创建看起来复杂的外观。

准备就绪

在我们深入 Three.js 中所需的步骤之前,我们首先需要我们用来使对象部分透明的纹理。对于这个菜谱,我们将使用以下纹理,它是在 Photoshop 中创建的:

准备就绪

您不需要使用 Photoshop;您需要记住的唯一一件事是使用一个带有透明背景的图片。使用这种纹理,在这个菜谱中,我们将向您展示您如何创建以下内容(04.08-make-part-of-object-transparent.html):

准备就绪

正如您在前面看到的,只有球体的一部分是可见的,您可以通过球体看到球体的另一侧。

如何做到这一点...

让我们看看您需要采取的步骤来完成这个任务:

  1. 我们首先要做的是创建几何体。对于这个菜谱,我们使用 THREE.SphereGeometry

    var sphereGeometry = new THREE.SphereGeometry(6, 20, 20);
    

    就像所有其他菜谱一样,您可以使用任何您想要的几何体。

  2. 在第二步中,我们创建材质:

    var mat = new THREE.MeshPhongMaterial();
    mat.map = new THREE.ImageUtils.loadTexture(
             "../assets/textures/partial-transparency.png");
    mat.transparent = true;
    mat.side = THREE.DoubleSide;
    mat.depthWrite = false;
    mat.color = new THREE.Color(0xff0000);
    

    正如您在这个片段中看到的,我们创建 THREE.MeshPhongMaterial 并加载我们在本菜谱 准备就绪 部分中看到的纹理。为了正确渲染,我们还需要将侧面属性设置为 THREE.DoubleSide,以便渲染球体的内部,并且我们需要将 depthWrite 属性设置为 false。这将告诉 WebGL 我们仍然想要测试我们的顶点与 WebGL 深度缓冲区,但我们不写入它。通常,当与更复杂的透明对象或粒子一起工作时,您需要将此设置为 false。

  3. 最后,将球体添加到场景中:

    var sphere = new THREE.Mesh(sphereGeometry, mat);
    scene.add(sphere);
    

通过这些简单的步骤,您可以通过仅对纹理和几何体进行实验来创建非常有趣的效果。

还有更多…

使用 Three.js,可以重复纹理(参考 设置重复纹理 菜谱)。您可以使用它来创建看起来很有趣的对象,例如这个:

还有更多…

设置纹理重复所需的代码如下:

var mat = new THREE.MeshPhongMaterial();
mat.map = new THREE.ImageUtils.loadTexture(
               "../assets/textures/partial-transparency.png");
mat.transparent = true;
mat.map.wrapS = mat.map.wrapT = THREE.RepeatWrapping;
mat.map.repeat.set( 4, 4 );
mat.depthWrite = false;
mat.color = new THREE.Color(0x00ff00);

通过更改mat.map.repeat.set的值,你可以定义纹理重复的频率。

相关内容

  • 有两种不同的方法可以使物体的一部分透明。你可以将物体分成多个几何体并分组,或者你可以像我们在使用单独的材质为面配方中所做的那样,使单个面透明。

使用立方体贴图创建反射材质

由于 Three.js 使用实时渲染场景的方法,创建反射材质既困难又非常计算密集。然而,Three.js 提供了一种你可以作弊并近似反射率的方法。为此,Three.js 使用立方体贴图。在这个配方中,我们将解释如何创建立方体贴图以及如何使用它们来创建反射材质。

准备工作

立方体贴图是一组六个图像,可以映射到立方体的内部。它们可以从全景图片创建,看起来可能像这样:

准备工作

在 Three.js 中,我们在立方体或球体的内部映射这样的贴图,并使用这些信息来计算反射。以下截图(示例04.10-use-reflections.html)显示了在 Three.js 中渲染时的样子:

准备工作

如前一个截图所示,场景中心的物体反射了它们所在的环境。这通常被称为天空盒。为了做好准备,我们首先需要做的是获取一个立方体贴图。如果你在网上搜索,你可以找到一些现成的立方体贴图,但自己创建一个也非常简单。为此,请访问gonchar.me/panorama/。在这个页面上,你可以上传一张全景图片,它将被转换成你可以用作立方体贴图的图片集。为此,执行以下步骤:

  1. 首先,获取一张 360 度的全景图片。一旦你有了,点击大型的打开按钮将其上传到gonchar.me/panorama/网站:准备工作

  2. 一旦上传,该工具将把全景图片转换为立方体贴图,如下截图所示:准备工作

  3. 转换完成后,你可以下载各种立方体贴图站点。本书中的配方使用立方体贴图侧面选项提供的命名约定,因此请下载它们。你将得到六个名为right.pngleft.pngtop.pngbottom.pngfront.pngback.png的图像。

一旦你得到了立方体贴图的侧面,你就可以执行配方中的步骤了。

如何操作...

要使用上一节中创建的立方体贴图并创建反射材质,我们需要执行相当多的步骤,但这并不复杂:

  1. 你需要做的第一件事是从你下载的立方体贴图图像中创建一个数组:

    var urls = [
        '../assets/cubemap/flowers/right.png',
        '../assets/cubemap/flowers/left.png',
        '../assets/cubemap/flowers/top.png',
        '../assets/cubemap/flowers/bottom.png',
        '../assets/cubemap/flowers/front.png',
        '../assets/cubemap/flowers/back.png'
    ];
    
  2. 使用这个数组,我们可以创建一个像这样的立方体贴图纹理:

    var cubemap = THREE.ImageUtils.loadTextureCube(urls);
    cubemap.format = THREE.RGBFormat;
    
  3. 从这个立方体贴图中,我们可以使用THREE.BoxGeometry和一个自定义的THREE.ShaderMaterial对象来创建一个天空盒(围绕我们的网格的环境):

    var shader = THREE.ShaderLib[ "cube" ];
    shader.uniforms[ "tCube" ].value = cubemap;
    
    var material = new THREE.ShaderMaterial( {
    
        fragmentShader: shader.fragmentShader,
        vertexShader: shader.vertexShader,
        uniforms: shader.uniforms,
        depthWrite: false,
        side: THREE.DoubleSide
    
    });
    
    // create the skybox
    var skybox = new THREE.Mesh( new THREE.BoxGeometry( 10000, 10000, 10000 ), material );
    scene.add(skybox);
    

    Three.js 提供了一个自定义着色器(一段 WebGL 代码),我们可以用它来做这个。正如你在代码片段中看到的,要使用这段 WebGL 代码,我们需要定义一个THREE.ShaderMaterial对象。使用这个材质,我们创建一个巨大的THREE.BoxGeometry对象并将其添加到场景中。

  4. 现在我们已经创建了天空盒,我们可以定义反射对象:

    var sphereGeometry = new THREE.SphereGeometry(4,15,15);
    var envMaterial = new THREE.MeshBasicMaterial(
                                     {envMap:cubemap});
    var sphere = new THREE.Mesh(sphereGeometry, envMaterial);
    

    如你所见,我们还把创建的立方体贴图作为属性(envmap)传递给材质。这告诉 Three.js 这个对象位于由cubemap组成的图像定义的天空盒内。

  5. 最后一步是将对象添加到场景中,然后就这样:

    scene.add(sphere);
    

在本食谱开头的示例中,你看到了三个几何体。你可以用这种方法处理所有不同类型的几何体。Three.js 将确定如何渲染反射区域。

它是如何工作的...

Three.js 本身并没有做很多来渲染cubemap对象。它依赖于 WebGL 提供的一个标准功能。在 WebGL 中,有一个叫做samplerCube的结构。使用samplerCube,你可以根据特定的方向采样,以确定与cubemap对象匹配的颜色。Three.js 使用这个来确定几何体每一部分的颜色值。结果是,在每一个网格上,你可以看到使用 WebGL 的textureCube函数反射的周围立方体贴图。在 Three.js 中,这导致以下调用(从 GLSL 的 WebGL 着色器中提取):

vec4 cubeColor = textureCube( tCube, 
                 vec3( -vReflect.x, vReflect.yz ) );

关于这是如何工作的更深入的解释可以在codeflow.org/entries/2011/apr/18/advanced-webgl-part-3-irradiance-environment-map/#cubemap-lookup找到。

还有更多...

在这个食谱中,我们通过提供六个单独的图像创建了cubemap对象。然而,还有一种创建cubemap对象的方法。如果你有一个 360 度的全景图像,你可以使用以下代码直接从该图像创建cubemap对象:

var texture = THREE.ImageUtils.loadTexture( 360-degrees.png',
              new THREE.UVMapping());

通常当你创建一个cubemap对象时,你使用本食谱中显示的代码将其映射到天空盒。这通常给出最佳结果,但需要一些额外的代码。你也可以使用THREE.SphereGeometry创建一个类似这样的天空盒:

var mesh = new THREE.Mesh( 
          new THREE.SphereGeometry( 500, 60, 40 ), 
          new THREE.MeshBasicMaterial( { map: texture }));
mesh.scale.x = -1;

这将纹理应用到球体上,并通过mesh.scale将这个球体翻转内外。

除了反射,你还可以使用cubemap对象进行折射(想想光线通过水滴或玻璃物体弯曲的情况):

还有更多...

要制作一个折射材质,你只需要像这样加载cubemap对象:

var cubemap = THREE.ImageUtils.loadTextureCube(urls, new THREE.CubeRefractionMapping());

按照以下方式定义材质:

var envMaterial = new THREE.MeshBasicMaterial({envMap:cubemap});
envMaterial.refractionRatio = 0.95;

参见

  • 如果您仔细观察这个配方开始时展示的示例,您可能会注意到您看不到各个对象之间的反射。您只能看到天空盒的反射。在 使用动态立方体贴图创建反射材料 配方中,我们向您展示了如何使 cubemap 对象动态化,以便其他渲染网格也能被反射。

使用动态立方体贴图创建反射材料

使用立方体贴图创建反射材料 的配方中,我们展示了如何创建一个能够反射其环境的材料。唯一的限制是场景中渲染的其他网格在反射中并未显示;只有立方体贴图被显示出来。在这个配方中,我们将向您展示如何创建一个动态的立方体贴图,它也能反射场景中的其他网格。

准备就绪

为了准备这个配方,您需要遵循 准备就绪 部分中 使用立方体贴图创建反射材料 配方的说明。对于这个配方,我们提供了一个单独的示例,您可以通过在浏览器中打开 04.11-use-reflections-dynamically.html 来显示它。

准备就绪

如果您仔细观察前面的中央球体,您可以看到它不仅反射了环境,还反射了圆柱体,如果您旋转场景,您还可以看到立方体贴图的反射。

如何操作...

为了实现这一点,我们首先需要执行一些与 使用立方体贴图创建反射材料 配方中相同的步骤。因此,在开始本配方中的步骤之前,请从该配方中取前三个步骤。完成这三个步骤后,您可以继续以下步骤:

  1. 要创建一个动态立方体贴图,我们需要使用 THREE.CubeCamera

    cubeCamera = new THREE.CubeCamera( 0.1, 20000, 256 );
    cubeCamera.renderTarget.minFilter = THREE.LinearMipMapLinearFilter;
    scene.add( cubeCamera );
    

    使用 THREE.CubeCamera,我们可以捕捉环境并使用它作为材料中的 cubemap 对象。为了获得最佳效果,您应该将 THREE.CubeCamera 放置在您想要使用动态 cubemap 对象的网格相同的位置。在这个配方中,我们使用它来处理位于此位置的中央球体:0, 0, 0。因此,我们不需要设置 cubeCamera 的位置。

  2. 对于这个配方,我们使用了三个几何体:

    var sphereGeometry = new THREE.SphereGeometry(4,15,15);
    var cubeGeometry = new THREE.BoxGeometry(5,5,5);
    var cylinderGeometry = new THREE.CylinderGeometry(2,4,10,20, false);
    
  3. 接下来,我们将定义材料。我们使用以下两种材料:

    var dynamicEnvMaterial = new THREE.MeshBasicMaterial({envMap: cubeCamera.renderTarget });
    var envMaterial = new THREE.MeshBasicMaterial({envMap: cubemap });
    

    第一个是使用 cubeCamera 输出作为其立方体贴图的材料,第二个材料使用一个静态的 cubemap 对象。

  4. 使用这两种材料,我们可以创建网格并将它们添加到场景中:

    var 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(cubeGeometry, envMaterial);
    cube.name='cube';
    scene.add(cube);
    cube.position.set(-10,0,0);
    
  5. 我们需要采取的最后一步是在 render 循环中更新 cubeCamera,如下所示:

    function render() {
        sphere.visible = false;
        cubeCamera.updateCubeMap( renderer, scene );
        sphere.visible = true;
        renderer.render(scene, camera);
        ...
        requestAnimationFrame(render);
    }
    

当您完成所有这些步骤后,您将在场景中间得到一个球体,它不仅反射环境,还反射场景中的其他对象。

它是如何工作的...

使用立方体贴图创建反射材料菜谱中,我们解释了如何使用立方体贴图创建反射对象。同样的原理也适用于这个菜谱,所以如果你还没有阅读它是如何工作的…部分,请先阅读。主要区别是,对于这个菜谱,我们使用THREE.CubeCamera动态创建立方体贴图,而不是使用静态的立方体贴图。当你实例化THREE.CubeCamera时,你实际上创建了六个THREE.PerspectiveCamera对象——每个立方体贴图的一个面。每次你调用updateCubeMap,就像我们在本菜谱的render循环中所做的那样,Three.js 只是使用这六个相机渲染场景,并将渲染结果用作要使用的立方体贴图。

更多内容…

在这个菜谱中,我们展示了如何使一个网格反射整个场景。如果你为场景中的每个网格创建单独的THREE.CubeCamera对象,你可以为所有对象创建一个动态立方体贴图。然而,请注意,这是一个相当计算密集的过程。与只渲染一次场景相比,你将需要为每个立方体贴图对象进行六个额外的渲染过程。

参见

  • 对于静态立方体贴图,你可以使用前一个菜谱中解释的步骤,即使用立方体贴图创建反射材料菜谱

使用 Blender 创建自定义 UV 映射

如果你想将纹理(一个 2D 图像)应用到几何体上,你需要告诉 Three.js 纹理的哪一部分应该用于特定的THREE.face对象。如何将纹理映射到几何体的各个面的定义称为 UV 映射。例如,UV 映射告诉 Three.js 如何将地球的 2D 地图映射到 3D 球面几何体。当你处理简单形状或 Three.js 提供的基几何体时,提供的标准 UV 映射通常就足够了。然而,当形状变得更加复杂或你有特定的纹理映射要求时,你需要改变几何体的每个面如何映射到纹理的一部分。一个选择是手动完成,但对于较大的几何体来说,这非常困难且耗时。在这个菜谱中,我们将向你展示如何使用 Blender 创建自定义映射。

准备就绪

对于这个菜谱,你需要安装 Blender;如果你还没有安装 Blender,请查看第二章中“从 Blender 创建和导出模型”菜谱的“准备就绪”部分,几何体和网格。一旦安装了 Blender,启动它,你将看到一个类似于以下截图的屏幕:

准备就绪

在以下部分,我们将向你展示创建此立方体贴图所需采取的步骤。

如何做到这一点...

以下步骤解释了如何在 Blender 中创建自定义 UV 映射并在 Three.js 中使用它:

  1. 首先要做的事情是切换到编辑模式。为此,将鼠标悬停在立方体上并按tab键。您应该会看到类似这样的效果:如何操作...

    如果立方体没有高亮显示,用鼠标悬停在它上面并按 a 键。这将选择所有顶点和面。

  2. 现在,让我们为这个立方体创建一个标准的 UV 映射。为此,导航到网格 | UV 展开 | 展开。现在,分割活动视图并打开UV/图像编辑器视图。如何操作...

    在 Blender 窗口的左侧部分,我们现在可以看到所有选定的面和顶点是如何映射到纹理上的。

  3. 在右侧视图中,选择前面,您可以直接看到该面是如何映射到纹理上的:如何操作...

    现在,我们可以通过移动屏幕左侧的顶点来更改这个面的映射。不过,在我们这样做之前,我们首先加载一个纹理图像。

  4. 将鼠标放在屏幕左侧部分,按Alt + O选择一个图像。对于这个配方,使用在assets/textures目录中可以找到的debug.png纹理是最简单的。一旦打开图像,屏幕将看起来像这样:如何操作...

  5. 通过在左侧视图中拖动角落,我们更改了所选面的 UV 映射。将这些角落移动到类似这样的位置:如何操作...

    如您所见,我们将这个面的 UV 映射从整个纹理更改为仅左上角。

  6. 下一步是导出这个几何形状,在 Three.js 中加载它,并查看我们更改的面是否确实发生了映射变化。在这个配方中,我们将使用OBJ格式来导出模型。因此,导航到文件 | 导出 | Wavefront并保存模型。

  7. 要加载模型,我们首先需要在页面的头部包含OBJLoader JavaScript 文件:

    <script src="img/OBJLoader.js"></script>
    
  8. 现在,我们可以使用加载器来加载模型并将其添加到场景中:

    var loader = new THREE.OBJLoader();
    loader.load("../assets/models/blender/uvmap.obj", function(model) {
        model.children[0].material.map = THREE.ImageUtils
             .loadTexture("../assets/textures/debug.png");
        scene.add(model);
    });
    

    在这个例子中,我们明确设置了我们想要使用的纹理,因为我们没有使用OBJMTLLoader

  9. 作为最后一步,让我们看看结果。我们提供了一个示例,04.14-create-custom-uv-mapping.html,展示了这些步骤的结果。如何操作...

如您从前面的截图中所见,我们更改了 UV 映射的前面只显示了纹理的一部分,而其他侧面则显示了完整的纹理。

还有更多...

我们只是简要介绍了 Blender 在创建 UV 映射方面的帮助。要了解更多关于 Blender 中 UV 映射的信息,以下两个网站是很好的起点:

参考以下内容

  • 关于如何将 Three.js 与 Blender 集成的更多信息,你可以查看第二章中的从 Blender 创建和导出模型食谱,几何体和网格,其中我们展示了如何安装 Blender 的 Three.js 插件以及如何在 Three.js 中直接加载模型及其材质。

配置混合模式

当在 Three.js 中渲染一个对象在另一个对象之上时,你可以配置如何混合来自其后方的对象的颜色。在这个食谱中,我们向您展示了设置特定混合模式所需的步骤。你可以将其与 Photoshop 中各种混合层的工作方式进行比较。

准备工作

理解特定混合模式的结果是困难的。为了帮助理解不同的可用混合模式,我们提供了一个简单的网页,展示了混合模式并允许你在它们之间切换。你可以在浏览器中打开04.13-configuring-blend-modes.html来查看这个示例。

准备工作

通过前一个截图右上角的菜单,你可以看到每种混合模式的结果。

如何做到这一点...

设置混合模式很简单:

  1. 首先,创建一个几何体和一个材质:

    var cubeGeometry = new THREE.BoxGeometry(10, 4, 10);
    var cubeMaterial = new THREE.MeshPhongMaterial({map: THREE.ImageUtils.loadTexture("../assets/textures/debug.png")});
    
  2. 接下来,将blending属性设置为你要使用的混合模式:

    cubeMaterial.blending = THREE.SubtractiveBlending;
    
  3. 然后,将transparent属性设置为true

    cubeMaterial.transparent = true;
    

    你可以通过查看 Three.js 源代码来找到可用标准混合模式的概述:

    THREE.NoBlending = 0;
    THREE.NormalBlending = 1;
    THREE.AdditiveBlending = 2;
    THREE.SubtractiveBlending = 3;
    THREE.MultiplyBlending = 4;
    

它是如何工作的...

正如我们所见,Three.js 使用 WebGL 来渲染场景。你在材质上定义的混合模式被 WebGL 内部用来确定如何混合背景色和前景色。

还有更多...

除了在这个食谱中展示的混合模式外,你也可以定义自己的自定义混合模式。你可以通过将blending属性设置为THREE.CustomBlending来实现这一点。使用以下三个材质属性来定义前景与背景的混合方式:blendSrcblendDstblendEquation。对于blendSrc,你可以使用以下值:

THREE.DstColorFactor = 208;
THREE.OneMinusDstColorFactor = 209;
THREE.SrcAlphaSaturateFactor = 210;

对于blendDst,你可以使用以下值:

THREE.ZeroFactor = 200;
THREE.OneFactor = 201;
THREE.SrcColorFactor = 202;
THREE.OneMinusSrcColorFactor = 203;
THREE.SrcAlphaFactor = 204;
THREE.OneMinusSrcAlphaFactor = 205;
THREE.DstAlphaFactor = 206;
THREE.OneMinusDstAlphaFactor = 207;

对于blendEquation,WebGL 支持以下集合:

THREE.AddEquation = 100;
THREE.SubtractEquation = 101;
THREE.ReverseSubtractEquation = 102;

一个非常好的例子,展示了这些设置中的许多,可以在 Three.js 示例网站上找到,网址为threejs.org/examples/#webgl_materials_blending_custom

使用阴影贴图进行固定阴影

在第五章中,我们将向您展示一些处理灯光和阴影的食谱。然而,使用纹理也可以伪造阴影。这种纹理被称为阴影贴图或光照贴图。在这个食谱中,我们解释了如何在 Three.js 中使用这种纹理。

准备工作

对于这个配方,我们首先需要一个阴影贴图。创建阴影贴图有不同的方法,但这超出了本配方的范围。如果您对创建自己的阴影贴图感兴趣,可以参考 Blender 网站上的这个教程:wiki.blender.org/index.php/Doc:2.4/Tutorials/Game_Engine/YoFrankie/Baking_Shadow_Maps

在本书的源代码中,在assets/textures文件夹中,您可以找到一个shadow-map.png文件,我们将在这个配方中使用它。

准备中

在前面的图中,您可以看到阴影贴图的样子。如您所见,阴影贴图包含在目标几何体中预先渲染的场景的阴影,在本例中是一个平面。如果我们使用这张图片作为阴影贴图,我们可以轻松地查看以下场景:

准备中

在这个场景中,我们使用阴影贴图来为地面平面创建阴影。

如何操作...

使用阴影贴图非常简单。在我们查看步骤之前,请确保您有一个几何体和一个材质。在以下步骤中,我们有名为 floor 的THREE.Mesh

  1. UV 贴图定义了面如何映射到纹理的特定部分。几何体中的 UV 贴图存储在几何体的faceVertexUvs属性中。此数组的第一个元素包含用于其他类型纹理的 UV 映射,第二个元素包含阴影贴图的 UV 映射。由于默认情况下此值未填充,我们将它指向faceVertexUvs数组中的第一个元素:

    floor.geometry.faceVertexUvs[1] = floor.geometry.faceVertexUvs[0];
    
  2. 接下来,您需要将阴影贴图设置为材质的lightmap属性:

    floor.material.lightMap = THREE.ImageUtils.loadTexture("../assets/textures/shadow-map-soft.png");
    
  3. 最后,添加您可能想要使用的其他纹理:

    floor.material.map = THREE.ImageUtils.loadTexture ("../assets/textures/tiles.jpg");
    

这就是您需要做的全部。这效果非常好,尤其是在您有静态网格和固定灯光的场景中,这对性能的提升非常显著。

参见

  • 如果您需要基于动画灯光或场景中的对象更新的动态阴影,您需要其他(或除了阴影贴图之外)的东西。在第五章的Creating shadows with Three.SpotLight配方中,我们解释了如何创建动态阴影。

第五章. 灯光和自定义着色器

在本章中,我们将介绍以下食谱:

  • 使用THREE.SpotLight创建阴影

  • 使用THREE.DirectionalLight创建阴影

  • 通过添加环境光照来柔化灯光

  • 使用THREE.HemisphereLight进行自然照明

  • 添加一个全方向移动的灯光

  • 沿路径移动光源

  • 使光源跟随一个对象

  • 创建自定义顶点着色器

  • 创建自定义片段着色器

简介

Three.js 提供了大量的光源,无需额外依赖。在本章中,我们将展示一些关于灯光的食谱,并展示如何充分利用 Three.js 提供的照明选项。我们还将展示两个高级食谱,解释如何通过创建自己的自定义顶点和片段着色器来访问 WebGL 的原始功能。

使用THREE.SpotLight创建阴影

Three.js 提供了许多不同类型的灯光,你可以在场景中使用。其中一些灯光还允许你向场景添加阴影。当你使用THREE.SpotLightTHREE.DirectionalLight对象时,你可以让 Three.js 根据光源的位置添加阴影。在本食谱中,我们将展示如何使用THREE.SpotLight来实现这一点。

准备工作

对于这个食谱,你不需要任何外部依赖。Three.js 将所有可用的灯光直接包含在主 Three.js JavaScript 库中。我们创建了一个简单的示例,你可以使用它来查看在 Three.js 中如何结合THREE.SpotLight使用阴影。你可以在浏览器中打开05.01-using-shadows-with-a-spotLight.html来查看这个示例。你将看到以下截图类似的内容:

准备工作

在这个场景中,你可以看到我们向场景中添加了两个网格,它们都在地板上投下了阴影。从这个例子中,你也可以直接看到THREE.SpotLight提供的独特光形状。

如何操作...

要使用THREE.SpotLight创建阴影,我们需要设置一些属性,这些属性定义了创建阴影的区域:

  1. 在我们查看THREE.SpotLight之前,我们首先需要做的是告诉渲染器我们想要启用阴影。为此,在THREE.WebGLRenderer上设置以下属性:

      renderer.shadowMapEnabled = true;
    
  2. 下一步是通知 Three.js 哪些对象投下阴影,哪些对象接收阴影。如果你回顾一下准备工作部分中的截图,你可以看到猴子和立方体都投下了阴影,而地板接收了阴影。为此,你必须在应该投下阴影的THREE.Mesh对象上设置以下属性:

    ..monkey.castShadow = true;
      cubeMesh.castShadow = true;
    

    对于接收阴影的对象,你必须在THREE.Mesh对象上设置以下属性:

      floorMesh.receiveShadow = true;
    
  3. 到目前为止,我们已经准备好创建THREE.SpotLight

      var spotLight = new THREE.SpotLight();
      spotLight.angle = Math.PI/8; // in radians
      spotLight.exponent = 30;
      spotLight.position = new THREE.Vector3(40,60,-50);
    

    这些是定义THREE.SpotLight如何向场景添加光的标准属性。

  4. 下一步是设置与阴影相关的属性:

      spotLight.castShadow = true;
      spotLight.shadowCameraNear = 50;
      spotLight.shadowCameraFar = 200;
      spotLight.shadowCameraFov = 35;
    

    第一个属性 castShadow 告诉 Three.js 这个光源可以投射阴影。由于投射阴影是一个昂贵的操作,我们需要定义阴影可以出现的位置。这是通过 shadowCameraNearshadowCameraFarshadowCameraFov 属性来完成的。

  5. Three.js 使用一种称为阴影图的技术来渲染阴影。如果您的阴影在边缘看起来有点块状,这意味着阴影图太小。要增加阴影图的大小,设置以下属性:

      spotLight.shadowMapHeight = 2048;
      spotLight.shadowMapWidth = 2048;
    

    或者,您也可以尝试更改 THREE.WebGLRenderershadowMapType 属性。您可以将此设置为 THREE.BasicShadowMapTHREE.PCFShadowMapTHREE.PCSSoftShadowMap

  6. 最后一步是将 THREE.SpotLight 添加到场景中:

      scene.add(spotLight);
    

确定各种 THREE.SpotLight 属性的正确属性可能很困难。在以下部分,我们将进一步解释这些属性如何影响阴影渲染的区域。

它是如何工作的...

当您想将 THREE.SpotLight 作为可以投射阴影的光源使用时,Three.js 需要知道这些阴影将影响哪些区域。您可以将其与您用于配置 THREE.PerspectiveCamera 的参数进行比较。因此,您通过 shadowCameraNearshadowCameraFarshadowCameraFov 属性所做的操作是定义 Three.js 应该在哪里渲染阴影。确定这些属性的值可能有点困难,但幸运的是,Three.js 可以可视化这个区域。如果您将 THREE.SpotLightshadowCameraVisible 属性设置为 true,Three.js 将显示受影响的区域,如下面的截图所示:

如何工作...

在这个截图中,区域的大小用橙色和红色线条表示。通过启用此 shadowCameraVisible 属性并尝试其他值,您可以快速确定正确的值。

参见

  • 在 Three.js 中,有两种可以投射阴影的光源:THREE.SpotLightTHREE.DirectionalLight。在 使用 THREE.DirectionalLight 创建阴影 食谱中,我们解释了如何使用 THREE.DirectionalLight 投射阴影。在 第四章 材料和纹理使用阴影图创建固定阴影 食谱中,解释了一种更高效但静态的创建阴影的方法。

使用 THREE.DirectionalLight 创建阴影

使用 THREE.DirectionalLight,您可以模拟一个远处的光源,其光线相互平行。一个很好的例子是从太阳接收到的光线。在这个食谱中,我们将向您展示如何创建 THREE.DirectionalLight 并使用它来创建阴影。

准备工作

对于这个食谱,我们创建了一个示例,展示了 THREE.DirectionalLight 对象产生的阴影的样子。在您的浏览器中打开 05.02-using-shadows-with-a-directionalLight.html 示例,您将看到以下截图所示的内容:

准备中

在这个截图中,一个单独的 THREE.DirectionalLight 对象提供了阴影和照明。

如何做到这一点...

使用 THREE.DirectionLight 作为阴影只需要几个步骤:

  1. 要启用任何类型的阴影,我们首先需要将 THREE.WebGLRenderer 上的 shadowMapEnabled 设置为 true

      renderer.shadowMapEnabled = true;
    
  2. 接下来,我们通知 Three.js 哪些对象应该接收阴影,哪些对象应该投射阴影。因此,对于应该投射阴影的对象,在 THREE.Mesh 上设置 castShadow 属性为 true

      monkey.castShadow = true;
      cubeMesh.castShadow = true;
    

    对于应该接收阴影的对象,在这个例子中是地板,在 THREE.Mesh 上设置以下属性为 true

      floorMesh.receiveShadow = true;
    
  3. 现在,我们可以创建 THREE.DirectionalLight 并配置这个光源。添加以下代码以创建 THREE.DirectionalLight

      var directionalLight = new THREE.DirectionalLight();
      directionalLight.position = new THREE.Vector3(70,40,-50);
      directionalLight.castShadow = true;
    
  4. 这将创建并定位 THREE.DirectionalLight,与 castShadow 属性一起,这个光源将被 Three.js 用于渲染阴影。

  5. 下一步是配置应该渲染阴影的区域:

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

    使用这些属性,我们创建了一个类似盒子的区域,Three.js 将在这个区域中渲染阴影。

  6. Three.js 使用两个额外的属性来确定渲染阴影的细节:shadowMapWidthshadowMapHeight。如果你的阴影看起来有点粗糙或块状,你应该增加这些值,如下所示:

      directionalLight.shadowMapWidth = 2048;
      directionalLight.shadowMapHeight = 2048;
    
  7. 在设置完所有这些属性后,你可以将光源添加到场景中:

      scene.add(directionalLight);
    

如从这些步骤中可以看出,正确配置 THREE.DirectionalLight 是有点复杂的。确定正确的值可能很困难。在下一节中,我们将更详细地解释这些属性的作用以及如何确定它们在场景中的最佳值。

工作原理...

如果你回顾到第三章 使用正交相机 中的 使用正交相机 菜谱,使用相机,你会注意到相机使用与 THREE.DirectionalLight 相同的属性。这两个对象都定义了一个边界框,在 THREE.OrthographicCamera 的情况下渲染,在 THREE.DirectionalLight 的情况下用于确定渲染阴影的位置。通过 shadowCameraNearshadowCameraFarshadowCameraLeftshadowCameraRightshadowCameraTopshadowCameraBottom,你定义了这个区域。你还可以在 THREE.DirectionalLight 上设置一个额外的属性来可视化受影响区域。如果你将 directionalLight.shadowCameraVisible 设置为 true,Three.js 将绘制由 shadowCameraXXX 属性定义的框。以下截图显示了启用 shadowCameraVisible 属性的结果:

工作原理

阴影只会在橙色框所包含的区域中渲染。

参见

  • 在 Three.js 中,有两种可以投射阴影的光源:THREE.SpotLightTHREE.DirectionalLight。在 使用 THREE.SpotLight 创建阴影 的配方中,我们解释了如何使用 THREE.SpotLight 投射阴影。在 第四章 的 使用阴影图创建固定阴影 配方中,我们解释了另一种创建阴影的方法。

通过添加环境光照来柔化灯光

当你向场景中添加灯光时,结果可能看起来有点刺眼。你可以看到接受光照的区域和不接受光照的区域之间存在强烈的对比。当你观察现实生活中的照明时,一切都会显得柔和一些,几乎每个表面都会接收到一些光,通常是来自其他表面的反射。在这个配方中,我们将向你展示如何使用 THREE.AmbientLight 在你的场景中柔化灯光的使用。

准备工作

对于这个配方,没有需要准备的步骤。为了看到最终结果,我们提供了一个示例,你可以在浏览器中打开 05.03-soften-lights.html 示例来查看。你将看到以下截图类似的内容:

准备工作

在右上角的菜单中,你可以启用或禁用 THREE.AmbientLight 来查看 THREE.AmbientLight 对象的效果。

如何操作...

THREE.AmbientLight 是最简单的灯光之一。因为它将光照应用到整个场景,所以不需要定位光源。你所要做的就是创建一个 THREE.AmbientLight 实例并将其添加到场景中:

  1. 首先,创建 THREE.AmbientLight 的实例:

      var ambientLight = new THREE.AmbientLight(0x332222);
    

    这将创建光源。当你创建环境光时,你可以指定其颜色为一个十六进制值。不要指定得太高;如果你这样做,你的整个场景将会非常明亮。

  2. 剩下的唯一事情就是将这个灯光添加到场景中:

      scene.add(ambientLight);
    

通过这两个非常简单的步骤,你就创建了 THREE.AmbientLight

它是如何工作的...

THREE.AmbientLight 的工作方式非常简单。当你创建 THREE.AmbientLight 时,你将一个颜色(十六进制)传递给其构造函数。当场景渲染时,Three.js 只是将指定的颜色混合到你的网格的颜色中。

参见

  • 尽管可以使用 THREE.AmbientLight 来柔化场景中的照明,但创建看起来自然的照明是困难的。在 使用 THREE.HemisphereLight 进行自然照明 的配方中,我们展示了如何使用不同的光源来创建自然的外部照明。

使用 THREE.HemisphereLight 进行自然照明

如果你观察外部的照明,你会看到灯光并不是真正来自单一方向。部分阳光被地球反射,其他部分被大气散射。结果是来自许多方向的一种非常柔和的光。在 Three.js 中,我们可以使用 THREE.HemisphereLight 创建类似的效果。

准备工作

就像 Three.js 提供的其他灯光一样,无需包含任何额外的 JavaScript 文件即可使用 THREE.HemisphereLight。你只需要一个包含一些对象的场景,然后就可以添加这个灯光。为了看到 THREE.HemisphereLight 对象的效果,我们提供了一个简单的示例。在浏览器中打开 05.04-create-a-sun-like-light.html。你将看到以下截图类似的内容:

准备中

在右上角的控件中,你可以启用和禁用场景中使用的 THREE.HemisphereLightTHREE.DirectionalLight

如何操作...

创建 THREE.HemisphereLight 的方法几乎与其他灯光相同:

  1. 首先,你需要实例化一个 THREE.HemisphereLight 实例:

      var hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6 );
    

    第一个参数设置天空的颜色,第二个颜色设置从地板反射的颜色。在这两种情况下,我们只是设置了一种白光。通过最后一个属性,你可以控制 THREE.HemisphereLight 对象的强度。在这种情况下,我们将灯光减弱到 0.6

  2. 接下来,我们需要定位灯光:

      hemiLight.position.set( 0, 500, 0 );
    

    当你定位 THREE.HemisphereLight 时,最好将其直接放置在场景上方以获得最佳效果。

  3. 最后,在设置好位置后,最后一步是将灯光添加到场景中:

      scene.add( hemiLight );
    

你可以将 THREE.HemisphereLight 作为场景的主光源,但通常情况下,这个光源会与另一个光源一起使用。为了获得最佳的户外效果,请与可以投射阴影的 THREE.DirectionalLight 一起使用。

它是如何工作的...

THREE.HemisphereLight 几乎等同于两个 THREE.DirectionalLight 对象:一个位于指定的位置,另一个位于完全相反的位置。因此,当场景渲染时,THREE.HemisphereLight 从顶部和相反方向照亮对象,以创建自然的外观效果。

还有更多…

当然,你也可以使用两个 THREE.DirectionalLight 对象来代替 THREE.HemisphereLight。经过仔细调整,你可以达到与使用 THREE.HemisphereLight 相同的效果。额外的优势是,你还可以使用这种方法创建非常微弱的阴影,因为 THREE.DirectionalLight 支持投射阴影,而 THREE.HemisphereLight 则不支持。

参见

  • 通过添加环境光照软化灯光 的配方中,我们向您展示了一种更简单的方法来支持场景中的主光源。我们向您展示了如何使用 THREE.AmbientLight 作为额外的光源来软化灯光和阴影。

添加全方向移动的灯光

在很多情况下,你不需要投射阴影的光源,只需要照亮场景的光源。在 使用 THREE.SpotLight 创建阴影使用 THREE.DirectionalLight 创建阴影 的食谱中,我们已经向你展示了如何使用 THREE.SpotLightTHREE.DirectionalLight。在这个食谱中,我们将展示第三种灯光,即 THREE.PointLight;这种灯光向所有方向发射光线,在这个食谱中,我们将向你展示如何创建一个并使其在场景中移动。

准备中

由于 Three.js 标准自带 THREE.PointLight 对象,因此不需要包含任何额外的 JavaScript。所有包含示例的食谱也是如此,你可以看到这个食谱的实际效果。对于这个食谱,请在浏览器中打开 05.05-use-a-point-light.html 示例,你会看到以下结果:

准备中

在这个例子中,有四个 THREE.PointLight 对象在三条鲨鱼模型之间从上到下移动。你可以使用鼠标在这个场景中移动,看看 THREE.PointLight 如何照亮模型。

如何操作...

创建一个移动的 THREE.PointLight 对象非常简单,只需几个步骤:

  1. 首先要做的事情是创建一个 THREE.PointLight 实例:

      var pointLight = new THREE.PointLight();
      pointLight.color = new THREE.Color(0xff0000);
      pointLight.intensity = 3;
      pointLight.distance = 60;
      pointLight.name = 'pointLight';
    

    使用 color 属性,我们设置 THREE.PointLight 对象发射的颜色,强度允许我们设置发射多少光线。最后,distance 属性用于计算被照亮的物体离灯光越远,强度降低的程度。在这种情况下,当距离灯光为 60 时,强度将为 0

  2. THREE.PointLight 在所有方向上发射光线,因此我们需要设置 position 属性,然后我们可以将灯光添加到场景中:

      pointLight.position = new THREE.Vector3(-30,0,0);
      scene.add(pointLight);
    
  3. 对于这个食谱,我们最后需要做的是将 THREE.PointLight 在场景中移动。像所有动画一样,我们在渲染循环中这样做,通过在 render 函数中添加以下内容:

      var light = scene.getObjectByName('pointLight');
      light.position.y = 15 * Math.sin(count+=0.005);
    

    在这个简短的代码片段中,我们首先获取 THREE.PointLight 对象的引用,然后更新其 position.y 属性。为了使这可行,我们还需要在 JavaScript 的顶部定义一个全局的 count 属性,如下所示:

      var count = 0;
    

通过这些简单的步骤,你已经创建了 THREE.PointLight,它会在场景中上下移动。

它是如何工作的...

THREE.PointLight 在所有方向上发射光线;你可以将其与 THREE.SpotLight 进行比较,但拥有 360 度的视野。这也是为什么 THREE.PointLight 不能用来投射阴影的主要原因。由于 THREE.PointLight 发射了大量的光线,因此计算产生的阴影非常困难且资源密集。

因此,如果你想要阴影并且使用THREE.PointLight,你可以使用阴影贴图,如果你有一个静态的THREE.PointLight对象或额外的THREE.SpotLight对象,并且将其设置为仅通过onlyShadow属性投射阴影。

相关内容

有几个菜谱你可以参考这个菜谱:

  • 使用 THREE.SpotLight 创建阴影菜谱中,我们展示了如何使用THREE.SpotLight来创建阴影。你可以与THREE.PointLight一起使用。

  • 使用 THREE.DirectionalLight 创建阴影菜谱中,我们向你展示了如何设置和配置THREE.DirectionalLight。这种灯光可以投射阴影,可以与THREE.PointLight一起使用。

  • 在第四章,材质和纹理中,我们展示了使用阴影贴图创建固定阴影菜谱。这个菜谱解释了如何使用阴影贴图来模拟阴影。如果你将这个菜谱与这个菜谱一起使用,你可以用它来模拟THREE.PointLight投射的阴影。

沿路径移动灯光源

添加一个全方向移动的灯光菜谱中,我们将灯光源上下移动。虽然这些简单的路径通常已经足够,但在某些情况下,你可能希望对灯光源在场景中的移动有更多的控制。在这个菜谱中,我们将向你展示如何使灯光源沿着预定义的路径移动。

准备工作

要创建这个菜谱,我们将使用THREE.SplineCurve3DTHREE.SpotLight对象。由于这两个对象都包含在 Three.js 中,我们不需要采取任何准备步骤。然而,查看这个菜谱提供的示例是一个好主意,它将展示当你运行05.06-move-a-light-through-the-scene.html示例时,执行这个菜谱的步骤会得到什么结果:

准备中

在截图上,你可以看到一个沿着紫色线条缓慢移动的光。在下一节中,我们将向你展示如何自己创建这样的效果。

如何操作...

对于这个菜谱,我们首先需要创建我们将要遵循的路径:

  1. 对于这个路径,我们将创建THREE.SplineCurve3

      var spline = new THREE.SplineCurve3([
        new THREE.Vector3(-100, 20, 100),
        new THREE.Vector3(-40, 20, 20),
        new THREE.Vector3(0, 20, -100),
        new THREE.Vector3(20, 20, -100),
        new THREE.Vector3(40, 20, 100),
        new THREE.Vector3(70, 20, 10),
        new THREE.Vector3(100, 20, 30),
        new THREE.Vector3(-100, 20, 100)]);
    

    这将产生一个通过THREE.SplineCurve3对象构造函数中添加的点移动的曲线路径。

  2. 在我们将灯光放置在THREE.SplineCurve3对象的路径上之前,让我们创建灯光:

      var pointLight = new THREE.PointLight();
      pointLight.color = new THREE.Color(0xff0000);
      pointLight.intensity = 3;
      pointLight.distance = 60;
      pointlight.name = 'pointLight';
    
  3. 现在,我们可以使用这个SplineCurve3对象来确定我们灯光的位置。为此,我们创建了一个名为positionLight的辅助函数:

      var pos = 0;
      function positionLight() {
        light = scene.getObjectByName('pointLight');
        if (pos <= 1) {
          light.position = spline.getPointAt(pos);
          pos += 0.001
        } else {
          pos = 0;
        }
      }
    

    在这个函数中,我们使用spline.getPointAt(pos)来确定在THREE.SplineCurve3路径上的哪个位置放置我们的灯光。当pos0时,我们在样条的起点,当pos1时,我们在样条的终点。这样,我们以0.001的步长缓慢地移动灯光沿样条。

  4. 剩下的就是从渲染函数中调用positionLight函数:

      function render() {
        renderer.render(scene, camera);
        positionLight();
        orbit.update();
        requestAnimationFrame(render);
      }
    

由于渲染函数大约每秒调用 60 次,而我们为完整路径取了 1000 步,因此光线将在大约 17 秒内沿着完整路径移动。

它是如何工作的...

当你实例化一个 THREE.SplineCurve3 对象时,你传递一个 THREE.Vector3 对象的数组。Three.js 内部对这些点进行插值以创建一个穿过所有这些点的流畅曲线。一旦曲线创建完成,你有两种方式来获取位置。你可以使用 getPointAt 函数,就像我们在本配方中所做的那样,根据提供的参数获取相对位置,从 01,以及曲线的长度。或者,你也可以使用 getPoints 函数,其中你指定作为参数,线应该被分成多少点。

更多...

在本配方的 准备工作 部分,我们向你展示了光线在场景中移动的示例。你所看到的是,我们还展示了光线移动的路径。为了自己做到这一点,你可以使用创建的 THREE.SplineCurve3 对象的 getPoints 函数来创建一个 THREE.Line 对象:

  var geometry = new THREE.Geometry();
  var splinePoints = spline.getPoints(50);
  var material = new THREE.LineBasicMaterial({
    color: 0xff00f0
  });
  geometry.vertices = splinePoints;
  var line = new THREE.Line(geometry, material);
  scene.add(line);

在这个配方中,我们使光线沿着特定的路径移动。然而,由于光线也是一个具有特定位置的对象,我们可以将这个相同的原理应用到场景中的所有其他对象上,例如 THREE.MeshTHREE.PerspectiveCameraTHREE.OrthographicCamera

使光源跟随对象

如果你场景中有一个你想要用聚光灯突出的移动对象,你需要能够改变光线指向的方向。在这个配方中,我们将向你展示如何做到这一点。我们将向你展示如何使场景中的 THREE.SpotLight 指向移动对象。

准备工作

运行这个配方不需要采取任何步骤。你可以在浏览器中打开 05.07-make-a-light-follow-object.html 示例来查看这个配方的最终结果。你将看到以下截图类似的内容:

准备工作

在这个例子中,你可以看到一个从左到右移动然后再返回的球体。场景中的 THREE.SpotLight 跟随这个球体的位置,使其始终直接指向该对象中心。

如何做到...

在 Three.js 中跟踪一个对象非常简单,只需要几个简单的步骤:

  1. 我们需要做的第一件事是创建我们想要跟踪的对象。对于这个配方,这是 THREE.SpotLight

      var sphereGeometry = new THREE.SphereGeometry(1.5, 20,20);
      var matProps = {
        specular: 0xa9fcff,
        color: 0x00abb1,
        emissive: 0x006063,
        shininess: 10
      }
      var sphereMaterial = new THREE.MeshPhongMaterial(matProps);
      var sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);sphereMesh.name = 'sphere'; scene.add(sphereMesh);
    
  2. 接下来,我们创建并添加 THREE.SpotLight 到场景中:

      spotLight = new THREE.SpotLight();
      spotLight.position.set(20, 80, 30);
      spotLight.castShadow = true;
      spotLight.angle = 0.15;
      spotLight.distance = 160;
      scene.add(spotLight);
    

    注意,在这个步骤中,我们没有将创建的光指向球体。我们将在渲染循环的下一步中这样做。

  3. 为了使光线指向球体,我们需要将 target 属性设置为正确的值。我们在场景的 render 函数中这样做:

      var step = 0;
      function render() {
        step += 0.02;
        renderer.render(scene, camera);
        var sphere = scene.getObjectByName('sphere');
        sphere.position.x = 0 + (10 * (Math.cos(step)));
        sphere.position.y = 0.75 * Math.PI / 2 + (6 * Math.abs(Math.sin(step)));
        spotLight.target = sphere;
        requestAnimationFrame(render);
      }
    

    在最后一步中需要注意的一点是,我们将 spotLight 的目标属性设置为 THREE.Mesh 对象,而不是 THREE.Mesh 的位置属性。

更多...

要将 THREE.SpotLight 指向一个特定的位置,我们设置其 target 属性。正如你在食谱步骤中看到的,我们针对 THREE.Object3D,它是 THREE.Mesh 扩展的,而不是一个位置。如果我们想将 THREE.SpotLight 指向任意位置,我们需要首先创建一个空的 THREE.Object3D 对象:

  var target = new THREE.Object3D();
  target.position = new THREE.Vector3(20,10,-10);
  scene.add(target);
  spotLight.target = target;

这样,你可以将 THREE.SpotLight 指向场景中的任意位置,而不仅仅是现有的对象。

相关内容

  • 在这个食谱中,我们将灯光指向了特定的目标,我们也可以让相机围绕场景中的对象移动,就像我们在第三章的 让相机跟随对象 食谱中展示的那样,与相机一起工作,以及将一个对象指向另一个对象,就像在第二章的 指向另一个对象 食谱中展示的那样,几何体和网格

创建自定义顶点着色器

当你想创建具有出色性能的高级 3D 效果时,你可以选择编写自己的着色器。着色器是直接影响你的结果看起来像什么以及用于表示它们的颜色的程序。着色器总是成对出现。顶点着色器确定几何体的外观,而片段着色器将确定最终的颜色。在这个食谱中,我们将向你展示如何在 Three.js 中使用你自己的自定义顶点着色器。

准备就绪

WebGL 和 GLSL,即你编写着色器的语言,被大多数现代浏览器支持。因此,对于这个食谱,在你开始这个食谱之前,不需要采取任何额外的步骤。关于 GLSL 的好资源总是 Khronos 网站 (www.khronos.org);他们有一个关于 WebGL 的优秀教程 (www.khronos.org/webgl/wiki/Tutorial),可以帮助你更好地理解在这个食谱中我们正在做什么。对于这个特定的食谱,我们提供了两个示例。第一个是我们将在本食谱中使用的示例,你可以通过在浏览器中打开 05.09-custom-vertex-shader.html 来查看这个示例。

准备就绪

如前一个截图所示,这个示例展示了 THREE.BoxGeometry,其中其单个顶点的位置已被顶点着色器替换。一个更高级的示例可以在 05.09-custom-vertex-shader-2.html 中找到。

准备就绪

在这个例子中,我们再次改变了单个顶点的位置,但这次,我们使用 THREE.SphereGeometry 作为源,并结合一个 Perlin 噪声生成器。

如何操作...

要创建自定义顶点着色器,你需要遵循以下步骤:

  1. 因为我们只想编写顶点着色器,我们将使用标准的片段着色器,这是 Three.js 中 THREE.MeshBasicMaterial 也使用的着色器。你可以通过从 THREE.ShaderLib 中选择正确的着色器来获取对这个着色器的引用:

      var basicShader = THREE.ShaderLib['basic'];
    
  2. 下一步是定义Uniforms对象。Uniforms是作为参数传递到着色器中的参数:

      Var uniforms = {}
      uniforms = THREE.UniformsUtils.merge([basicShader.uniforms]);
      var texture = THREE.ImageUtils.loadTexture('../assets/textures/debug.png');
      uniforms['map'].value = texture;
      uniforms.delta = {type: 'f', value: 0.0};
      uniforms.scale = {type: 'f', value: 1.0};
    

    在这个代码片段中,我们首先合并了由片段着色器重用的标准统一变量,我们设置了一个纹理,最后两个统一变量是我们将在后面看到的自定义顶点着色器中访问的变量。

  3. 现在,我们可以定义THREE.ShaderMaterial并告诉 Three.js 我们想要使用的着色器:

      var defines = {};
      defines[ "USE_MAP" ] = "";
      var material = new THREE.ShaderMaterial({
        defines: defines,
        uniforms: uniforms,
        vertexShader: document getElementById('sinusVertexShader').text,
        fragmentShader: basicShader.fragmentShader
      });
    

    在这个代码片段中,你可以看到我们引用了在第 2 步中看到的uniform值,我们使用的fragmentShader是第 1 步中的basicShader,对于vertexShader参数,我们引用了我们将在下一步定义的自定义着色器。注意,我们还提供了一个defines元素;这是确保 Three.js 显示我们的纹理所必需的。

  4. 到目前为止,我们可以定义我们自己的自定义顶点着色器。我们直接在 HTML 中这样做:

      <script id="sinusVertexShader" type="x-shader/x-vertex">
        varying vec2 vUv;
        uniform float delta;
        uniform float scale;
        void main() {
          vUv = uv;
          vec3 p = position;
          p.z += sin(2.0 * p.y + delta) * 5.0;
          p.z += cos(2.0 * p.z + delta / 2.0) * 5.0;
          p.z += cos(2.0 * p.x + delta) * 5.0;
          p.x += sin(p.y + delta / 2.0) * 10.0;
          vec4 mvPosition = modelViewMatrix * vec4(scale * p, 1.0 );
          gl_Position = projectionMatrix * mvPosition;
        }
      </script>
    

    使用这个着色器,我们通过改变其位置中的p.zp.x部分来改变顶点的位置。

  5. 到目前为止,我们只需创建一个几何体,并使用我们在第 3 步中创建的材料:

      var cubeGeometry = new THREE.BoxGeometry(5, 5, 5);
      var cube = new THREE.Mesh(cubeGeometry, material);
      scene.add(cube);
    
  6. 如果你查看第 4 步中的着色器代码,你可以看到位置受到 delta 统一值的影响。我们使用render函数传递一个新的值给这个统一变量:

      function render() {
        renderer.render(scene, camera);
        uniforms.delta.value += 0.01;
        requestAnimationFrame(render);
      }
    

这些都是你需要采取的步骤来创建和使用与简单的片段着色器结合的自定义顶点着色器。

它是如何工作的...

让我们更仔细地看看在这个食谱中使用的顶点着色器中发生了什么。在我们开始之前,我们将给你一个非常简短的介绍,说明你可以与着色器代码中的变量一起使用的限定符类型:

  • uniform限定符:这是一个全局变量,可以从 JavaScript 传递到着色器中。你可以在每个渲染循环中更改这个值,但不能在着色器本身中更改这个值。

  • attribute限定符:这是一个可以指定给每个单独顶点的值。attributes限定符被传递到顶点着色器中。

  • varying限定符:用于在顶点着色器和片段着色器之间传递数据。它可以在顶点着色器中写入,但只能在片段着色器中读取。

  • const限定符:这是一个常量值,它直接定义在你的着色器代码中。这个值在着色器执行过程中不能改变。

我们首先定义一些参数:

  varying vec2 vUv;
  uniform float delta;
  uniform float scale;

vUv向量是一个变化变量,是一个传递到片段着色器中的值,对于 Three.js 中的基本着色器工作来说是必需的。其他两个参数作为统一变量从上一节中看到的 JavaScript 中传递进来。让我们看看主函数,这是为每个顶点执行的功能:

  void main() {
    vUv = uv;
    vec3 p = position;
    p.z += sin(2.0 * p.y + delta) * 5.0;
    p.z += cos(2.0 * p.z + delta / 2.0) * 5.0;
    p.z += cos(2.0 * p.x + delta) * 5.0;
    p.x += sin(p.y + delta / 2.0) * 10.0;
    vec4 mvPosition = modelViewMatrix * vec4(scale * p, 1.0 );
    gl_Position = projectionMatrix * mvPosition;
  }

这里发生的主要事情是我们根据传入的 delta 值和一些sincos函数来改变顶点的位置。结果是我们的模型中的每个顶点都以某种方式偏移。最后,我们需要用我们顶点的新位置设置gl_Position变量。

还有更多...

当你寻找关于自定义着色器的信息时,你通常会看到片段着色器的示例。在许多用例中,顶点着色器不需要改变顶点的位置。当它确实需要改变时,通常是为了产生烟雾或火焰等效果。好的顶点着色器示例并不多。然而,以下两个网站提供了学习顶点着色器的良好起点:

参考资料也请查看

  • 由于顶点着色器总是与片段着色器一起使用,因此了解它们的工作原理也是好的。在创建自定义片段着色器菜谱中,我们解释了你需要采取的步骤来设置自定义片段着色器。

创建自定义片段着色器

一个 WebGL 着色器始终由两部分组成:顶点着色器,它可以用来重新定位模型的各个顶点;以及片段着色器,它可以用来给模型添加颜色。在这个菜谱中,我们将向你展示使用自定义片段着色器所需的步骤。

准备中

在我们开始讨论片段着色器之前,有一件事你需要知道。就像顶点着色器一样,你不会用 JavaScript 编写片段着色器代码。这些着色器是用 GLSL 语言编写的。因此,如果你想了解更多关于这个例子中使用的函数和符号,请查看 WebGL 规范,可以在www.khronos.org/registry/webgl/specs/1.0/找到。如果你想实验提供的着色器代码,你只需在浏览器中打开05.10-custom-fragment-shader.html即可。

准备中

这个着色器根据法向量以及从相机到物体的距离来给物体上色。在接下来的章节中,我们将解释如何做到这一点。

如何做到这一点...

让我们从这个菜谱的 JavaScript 部分开始:

  1. 着色器始终由顶点着色器和片段着色器组成。在这个菜谱中,我们将使用 Three.js 提供的标准顶点着色器,并为我们提供自己的自定义片段着色器。Three.js 将其所有着色器保存在THREE.ShaderLib中:

      var basicShader = THREE.ShaderLib['normal'];
    

    在步骤 3 中,我们将引用这个basicShader对象来获取标准的顶点着色器。

  2. 对于我们的自定义着色器,我们有一些配置选项。这些选项通过 uniform 传递到着色器中:

      var uniforms = {};
      uniforms.delta = {type: 'f', value: 0.0};
      uniforms.mNear = { type: "f", value: 1.0 };
      uniforms.mFar = { type: "f", value: 60.0 };
    

    这意味着在我们的着色器代码中,我们可以访问deltamNearmFar值,这些都是浮点值,我们可以使用它们来计算我们想要渲染的颜色。

  3. 接下来,我们可以创建着色器材料:

      var material = new THREE.ShaderMaterial({
        uniforms: uniforms,
        vertexShader: basicShader.vertexShader,
        fragmentShader: document getElementById('simple-fragment').text,
      });
    

    THREE.ShaderMaterial的配置中,我们引用了我们的uniform变量,Three.js 提供的标准顶点着色器basicShader.vertexShader以及我们自己的自定义片段着色器。我们将在第 5 步中展示我们自定义着色器的定义。

  4. 我们最后需要做的是创建THREE.BoxGeometry并将其添加到场景中,使用上一步骤中创建的材料:

      var boxGeometry = new THREE.BoxGeometry(5, 15, 5);
      var box = new THREE.Mesh(boxGeometry, material);
      scene.add(box);
    
  5. 在第 3 步中,我们引用了一个具有简单片段名称的 DOM 元素。在你的 HTML 页面中,你应该这样定义它:

      <script id="simple-fragment" type="x-shader/x-fragment">
        varying vec3 vNormal;
        uniform float delta;
        uniform float mNear;
        uniform float mFar;
        const float PI = 3.14159265358979323846264;
        void main()
        {
          float depth = gl_FragCoord.z / gl_FragCoord.w;
          float depthColor = smoothstep( mNear, mFar, depth );
          gl_FragColor = vec4(abs(sin(delta + 0.7*PI) + cos(normalize(vNormal).x)/2.0) - depthColor,abs(sin(delta + 1.0*PI) + cos(normalize(vNormal).y)/2.0) - depthColor,abs(sin(delta + 1.2*PI) + cos(normalize(vNormal).z)/2.0) – depthColor, 1.0);
        }
      </script>
    

    如果你想了解更多关于这个片段着色器的工作原理,请查看本食谱如何工作...部分中的解释。

  6. 如果你已经查看了准备就绪部分中的示例,你可以看到颜色不断变化。这是因为我们在本页面的render循环中更新了 delta 属性,该属性传递到我们的自定义着色器中:

      function render() {
        renderer.render(scene, camera);
     uniforms.delta.value += 0.005;
        requestAnimationFrame(render);
      }
    

它是如何工作的...

要了解这个着色器是如何工作的,让我们一步一步地查看代码。让我们首先看看在这个着色器中使用的变量:

  varying vec3 vNormal;
  uniform float delta;
  uniform float mNear;
  uniform float mFar;
  float PI = 3.14159265358979323846264;

vNormal对象是一个从标准 Three.js 顶点着色器传入的变量,包含适用于此片段的法线向量的值。三个 uniform 值是从 JavaScript 传入的,正如我们在上一节中看到的。PI 变量是一个随时间不变的常数。每个片段着色器都应该设置gl_fragColor向量,该向量确定每个片段的颜色和透明度。对于这个着色器,我们设置向量如下:

    void main()
    {
      float depth = gl_FragCoord.z / gl_FragCoord.w;
      float depthColor = smoothstep( mNear, mFar, depth );
      gl_FragColor = vec4(
        abs(sin(delta + 0.7*PI) + cos(normalize(vNormal).x)/2.0) – depthColor ,abs(sin(delta + 1.0*PI) + cos(normalize(vNormal).y)/2.0) – depthColor, abs(sin(delta + 1.2*PI) + cos(normalize(vNormal).z)/2.0) – depthColor, 1.0);
    }

不深入 GLSL 的细节,大致上采取以下步骤:

  1. 首先,我们确定这个片段的深度。你可以将其视为这个片段与摄像机的距离。

  2. 由于深度是一个绝对值,我们使用smoothstep函数将其转换为01的刻度。由于此函数也接受mNearmFar统一变量作为其参数,我们可以通过 JavaScript 控制深度对片段颜色的影响程度。

  3. 最后,我们通过设置gl_FragColor来定义片段的颜色。gl_FragColor变量是vec4类型,其中前三个值确定颜色的 RGB 值,最后一个值定义不透明度。这一切都是在01的刻度上。对于颜色的每一部分,我们使用一个包含vNormal向量和计算出的depthColor变量的函数来生成颜色。

这只是使用自定义片段着色器可以做到的事情的冰山一角。在接下来的部分中,你可以找到一些资源来了解更多关于这个的信息。

还有更多...

创建自定义片段着色器相当困难。这需要大量的实验、良好的数学掌握和大量的耐心。然而,有一些资源可以帮助您理解片段着色器并从他人的作品中学习:

  • 您可以在 glslsandbox.com/ 找到大量的片段着色器。

  • 在 Shadertoy 网站上,您可以使用不同类型的输入进行片段着色器实验:www.shadertoy.com/

  • 您可以在 shdr.bkcore.com/ 找到一个简单的在线着色器编辑器。

另一项极大的帮助可能是最新版本的 Firefox 开发者工具。这是一个特殊的 Firefox 版本,它提供了出色的调试支持,甚至包括一个着色器编辑器,您可以使用它来编辑着色器程序并直接查看结果。您可以从www.mozilla.org/en-US/firefox/developer/下载这个版本。

当然,还有 khronos 网站 (www.khronos.org),这是一个查找特定函数实际做什么的极好资源。

参见

  • 由于片段着色器总是与顶点着色器一起使用,因此了解它们的工作方式也是很好的。在 创建自定义顶点着色器 菜单中,我们解释了您需要采取的步骤来设置自定义顶点着色器。

第六章:点云和后处理

在本章中,我们将涵盖以下食谱:

  • 基于几何形状创建点云

  • 从头创建点云

  • 在点云中着色单个点

  • 样式单个点

  • 移动点云的单个点

  • 爆炸点云

  • 设置基本后处理管道

  • 创建自定义后处理步骤

  • 将 WebGL 输出保存到磁盘

简介

Three.js 支持许多不同类型的几何形状和对象。在本章中,我们将向您展示一些使用 THREE.PointCloud 对象的食谱。使用此对象,你可以创建一个点云,其中渲染的是单个顶点而不是完整的网格。对于点,你有各种各样的不同样式选项可用,你甚至可以移动单个点来创建非常有趣(并且逼真)的动画和模拟。

基于几何形状创建点云

Three.js 的一个有趣特性是它还允许你创建点云。点云不是作为一个实体几何形状渲染,而是所有单个顶点都作为单独的点渲染。在本食谱中,我们将向您展示如何基于已经存在的几何形状创建这样的点云。

准备就绪

开始这个食谱不需要额外的步骤。然而,在本食谱中使用的示例中,我们使用外部模型作为点云的基础。我们还使用了一个相机控制对象,THREE.OrbitControls,以便更容易地在示例周围导航。如果你想要自己使用相机控制对象,你需要将以下 JavaScript 库添加到场景中(除了标准的 Three.js 之外):

  <script src="img/OrbitControls.js"></script>
  <script src="img/OBJLoader.js"></script>

我们使用的这个外部模型也包含在这本书的源代码中,可以在 assets/models/cow 文件夹中找到。为了展示这个食谱的结果可能看起来像什么,我们提供了一个示例,展示了基于现有几何形状创建的点云(06.01-create-point-cloud-from-geometry.html)。你将看到以下截图类似的内容:

准备就绪

如您在本截图中所见,我们已经加载了一个牛的几何形状,并基于它创建了一个点云。当然,你可以使用任何你想要的几何形状,但特别是复杂模型作为点云渲染时看起来非常棒。

如何做...

创建点云与创建简单的 THREE.Mesh 对象并没有太大的区别。以下部分解释了你应该采取的步骤:

  1. 在这种方法中,你需要的是 THREE.Geometry。你可以使用标准几何形状之一,或者加载一个外部几何形状。对于这个食谱,我们将加载一个外部几何形状(本食谱中准备就绪部分提到的牛):

      var loader = new THREE.OBJLoader();
      loader.load(
        "../assets/models/cow/cow.obj",
        function(cow) {
          // get the main cow geometry from the 
          // loaded object hierarchy
          var cowGeometry = cow.children[1].geometry;
        }
      );
    

    在这个代码片段中,我们加载了外部模型,因此我们有了可以基于它建立点云的几何形状。

  2. 在我们创建点云之前,我们首先必须告诉 Three.js 我们想要点云看起来像什么。为此,我们创建 THREE.PointCloudMaterial

      var pcMat = new THREE.PointCloudMaterial();
      pcMat.map = THREE.ImageUtils.loadTexture("../assets/textures/ps_smoke.png");
      pcMat.color = new THREE.Color(0x5555ff);
      pcMat.transparent = true;
      pcMat.size = 0.2;
      pcMat.blending = THREE.AdditiveBlending;
    

    这种材质定义了每个点的外观。大多数属性都是相当直观的。这里有趣的一个是 blending 属性。通过将 blending 属性设置为 THREE.AdditiveBlending,您可以得到在食谱开头截图中所看到的漂亮的光晕效果。

  3. 在这一点上,我们有 THREE.GeometryTHREE.PointCloudMaterial;使用这两个对象,我们可以创建点云:

      pc = new THREE.PointCloud(geometry, pcMat);
      pc.sizeAttenuation = true;
      pc.sortPoints = true;
    

    如您所见,我们传递 THREE.GeometryTHREE.PointCloudMaterial 来创建 THREE.PointCloud。在创建的点云上,我们设置两个额外的属性为 truesizeAttenuation 属性确保点的尺寸也取决于与摄像机的距离。因此,远离摄像机的点看起来更小。sortPoints 属性确保当您使用透明点时,正如我们在本食谱中所做的那样,它们会被正确渲染。

  4. 执行的最后一步是将创建的 THREE.PointCloud 对象添加到场景中:

      scene.add(pc);
    

现在,Three.js 将像渲染任何其他 3D 对象一样渲染点云。

它是如何工作的...

当您创建 THREE.PointCloud 时,Three.js 会为提供的 THREE.Geometry 对象的每个顶点创建一个点。THREE.Geometry 中的其他信息不使用。对于 THREE.WebGLRenderer,它内部直接使用 GL_POINTS,这是一个 WebGL 原语,来渲染单个点(更多信息请参考 www.khronos.org/opengles/sdk/docs/man/xhtml/glDrawElements.xml)。然后,使用自定义片段着色器,它为这些点着色。结果是,当您使用 THREE.WebGLRenderer 时,您可以轻松渲染数百万个点,同时保持出色的性能。

还有更多...

点是表示各种不同效果的好方法。对于一些有趣的点应用,您可以查看以下示例:

相关内容

在这一章中,我们有许多与这个密切相关的食谱:

  • 从头创建点云 食谱中,我们从一个自定义创建的几何体创建点云

  • 样式单个点 食谱中,我们向您展示如何样式点云中的单个点

  • 移动点云中的单个点爆炸点云 食谱中,我们向您展示如何移动点

从头创建点云

当您想要创建点云时,您可以传入现有的几何体,并将点云基于它创建。在这个菜谱中,我们将向您展示如何从头创建 THREE.Geometry 并从它创建点云。

准备中

对于这个菜谱,我们不需要任何额外的 JavaScript 库,也不需要加载外部模型,因为我们从头创建我们的几何体。您可以通过在浏览器中打开 06.02-create-point-system-from-scratch.html 来查看我们创建的几何体。您将看到以下截图类似的内容:

准备中

在下一节中,我们将解释如何创建这个自定义几何体并使用它与 THREE.PointCloud 一起。

如何操作...

步骤基本上与 基于几何体创建点云 菜谱中所示相同,除了首先我们需要创建我们自己的自定义几何体:

  1. 创建自定义几何体相当简单:

      var x = 100;
      var y = 100;
      var geometry = new THREE.Geometry();
      for (var i = 0 ; i < x ; i++) {
        for (var j = 0 ; j < y ; j++) {
          var v = new THREE.Vector3();
          v.x = i / 10;
          v.y = Math.sin(i/100 * Math.PI*2) + Math.cos(j/100 * Math.PI) * 2;
          v.z = j / 10;
          geometry.vertices.push(v);
        }
      }
    

    如此代码片段所示,您首先需要实例化 THREE.Geometry,然后创建 THREE.Vector3 实例并将它们推送到几何体的顶点属性中。

  2. 现在我们已经得到了一个几何体,我们只需要 THREE.PointCloudMaterial

      var pcMat = new THREE.PointCloudMaterial(geometry);
      pcMat.map = THREE.ImageUtils.loadTexture ("../assets/textures/ps_smoke.png");
      pcMat.color = new THREE.Color(0x55ff55);
      pcMat.transparent = true;
      pcMat.size = 0.2;
      pcMat.blending = THREE.AdditiveBlending;
    
  3. 使用这种材质与几何体一起创建 THREE.PointCloud 并将其添加到场景中:

      pc = new THREE.PointCloud(geometry, pcMat);
      pc.sizeAttenuation = true;
      pc.sortPoints = true;
      scene.add(pc);
    

如果您已经查看过 基于几何体创建点云 的菜谱,您会注意到大多数步骤都是相同的。这两个菜谱之间的唯一区别是创建几何体的方式。

它是如何工作的...

关于它是如何工作的解释,请参阅 基于几何体创建点云 菜谱中的 它是如何工作的… 部分。

还有更多...

在 第二章,几何体和网格 中,我们展示了如何使用 Three.js 渲染 3D 公式。使用这个菜谱的设置,您也可以创建以点云形式可视化的 3D 公式。例如,以下截图显示了 第二章 中的 3D 公式,渲染为点云:

还有更多…

如您所见,您可以通过这种方式非常容易地创建看起来很棒的点云。

相关阅读

本章中有几个与这个菜谱相关的菜谱:

  • 基于几何体创建点云 菜谱中,我们使用现有的几何体来创建点云

  • 样式单个点 菜谱中,我们向您展示如何样式化点云中的单个点

  • 移动点云中的单个点爆炸点云 的菜谱中,我们向您展示如何移动点

为点云中的单个点着色

当您创建点云时,每个点都有相同的颜色和样式,因为每个点都使用相同的 THREE.PointCloudMaterial 对象。然而,有一种方法可以为单个点添加颜色。

准备中

运行此配方不需要任何额外的步骤。我们将创建一个自定义几何体,就像我们在 从头创建点云 配方中所做的那样,这次我们将为每个单独的点着色。此配方的结果可以通过在您的浏览器中打开 06.03-color-individual-points-in-point-system.html 来查看。您将看到以下截图类似的内容:

准备就绪

如您所见,我们已经用各种红色的阴影给各个点着色。

如何做到这一点...

要实现单个着色点,我们需要在创建 THREE.Geometry 时设置一个额外的属性。以下步骤显示了如何做到这一点:

  1. 我们首先创建几何体。当我们创建单个顶点时,我们也可以通知 Three.js 我们想要为其使用的颜色:

      var x = 100;
      var y = 100;
      var geometry = new THREE.Geometry();
      for (var i = 0 ; i < x ; i++) {
        for (var j = 0 ; j < y ; j++) {
          var v = new THREE.Vector3(i,0,j);
     var rnd = Math.random()/2 + 0.5;
     geometry.colors.push(
     new THREE.Color(rnd, rnd/4, 0));
          geometry.vertices.push(v);
        }
      }
    

    在这个代码片段中,我们创建一个随机颜色并将其推送到 geometry.colors 数组。在这两个循环结束时,我们将有 vertices 数组中的 10000 个顶点和 colors 数组中的 10000 个颜色。

  2. 现在,我们可以创建 THREE.PointCloudMaterial 并与几何体一起使用来创建 THREE.PointCloud

      var pcMat = new THREE.PointCloudMaterial(geometry);
      pcMat.vertexColors = true;
      pcMat.map = THREE.ImageUtils.loadTexture("../assets/textures/ps_smoke.png");
      pcMat.transparent = true;
      pc = new THREE.PointCloud(geometry, pcMat);
      scene.add(pc);
    

    要使用我们在步骤 1 中创建的颜色,我们需要将 THREE.PointCloudMaterialvertexColors 属性设置为 true。在这个代码片段中,我们还加载了一个纹理并将其分配给 map 属性。我们使用单个颜色,因此不需要在需要设置颜色的材质上设置 color 属性。我们将在下一步中展示这一点。

  3. 如果您已经查看过本配方中 准备就绪 部分所示的示例,您会注意到点的颜色发生了变化。我们可以通过只需更改渲染循环中几何体的 colors 数组中的颜色来轻松做到这一点:

      for (var i = 0 ; i < pc.geometry.colors.length ; i++) {
        var rnd = Math.random()/2 + 0.5;
        pc.geometry.colors[i] = new THREE.Color(rnd, rnd/4, 0);
      }
      pc.geometry.colorsNeedUpdate = true;
    

    当您更改颜色时,需要将 colorsNeedUpdate 属性设置为 true,这样 Three.js 就知道需要更新点的颜色。

它是如何工作的...

Three.js 使用 WebGL 来渲染单个点。为此,Three.js 使用顶点着色器和片段着色器(有关此内容的更多配方,请参阅上一章)。为了着色单个点,Three.js 将信息传递到用于确定输出颜色的片段着色器。相应的着色器代码如下:

  gl_FragColor = vec4( psColor, opacity );

psColor 变量是从 THREE.Geometry 的颜色数组传递到用于着色点的着色器的。

参见

  • 在 Three.js 中着色单个点非常简单直接。然而,如果您想更改点的更多属性,如不透明度或大小,您不能使用标准的 Three.js 来做到这一点。在 样式化单个点 配方中,我们将向您展示您如何创建一个自定义着色器来更改点云中点的这些属性。

  • 如果您对给点云中的点添加动画感兴趣,可以查看 移动点云中的单个点爆炸点云 的配方。

单个点的样式设置

使用标准的 Three.js 功能,你不能为点云中的单个点进行样式设置。你可以改变它们的颜色,就像我们在 在点云中为单个点着色 菜单中所展示的那样,但无法改变点的大小或透明度。在这个菜谱中,我们将向你展示如何创建自定义的顶点和片段着色器,这允许你改变点云中单个点的颜色、透明度和大小,并且你可以轻松扩展以添加更多属性。

准备中

在这个菜谱中没有使用外部库。我们将通过创建我们自己的自定义着色器来扩展基本的 Three.js 功能。要查看着色器的效果,请在你的浏览器中打开 06.04-style-individual-points-in-point-system-with-custom-shader.html 示例。你将看到以下菜谱类似的内容:

准备中

如此截图所示,单个点的尺寸、颜色和透明度各不相同。

如何做到这一点……

让我们看看你需要采取的步骤来完成这个任务:

  1. 让我们从简单开始,首先创建我们将从中创建点云的几何体:

      var geometry = new THREE.Geometry();
     var pSize = [];
     var pOpacity = [];
     var width= 100;
     var height = 100;
      // create the geometry and set custom values
      for (var i = 0 ; i < width ; i++) {
        for (var j = 0 ; height < y ; j++) {
          var v = new THREE.Vector3();
          v.x = i / 10;
          v.y = (Math.sin(i/200 * Math.PI*2) + Math.cos(j/50 * Math.PI) + Math.sin((j+i)/40 * Math.PI))/2;
          v.z = j / 10;
          // add the vertex
          geometry.vertices.push(v);
          // add vertex specific color, size and opacity
     geometry.colors.push(new THREE.Color(v.y,0.5,0.7));
     pSize.push(Math.random());
     pOpacity.push(Math.random()/4+0.5);
        }
      }
    

    如你所见,我们从头开始创建 THREE.Geometry 并生成 10,000 个顶点。因为我们想改变单个顶点的颜色、大小和透明度,所以我们也为这 10,000 个顶点生成了这些属性的值。颜色存储在 geometry.colors 数组中,这是 Three.js 的标准功能。我们将大小存储在 pSize 数组中,透明度存储在 pOpacity 数组中。

  2. 现在我们已经得到了一个几何体和包含预期大小和透明度的单个顶点的几个数组,让我们定义点云的材质:

      var attributes = ...; // filled in in next steps
      var uniforms = ...;   // filled in in next steps
      var psMat2 = new THREE.ShaderMaterial({
        attributes: attributes,
        uniforms: uniforms,
        transparent : true,
        blending : THREE.AdditiveBlending,
        vertexShader: document getElementById('pointVertexShader').text,
        fragmentShader: document getElementById('pointFragmentShader').text
      });
    

    我们没有使用标准的 THREE.PointCloudMaterial 对象,而是使用了 THREE.ShaderMaterialtransparentblending 是标准材质属性,表现如你所预期。我们将在接下来的步骤中解释其他属性。

  3. 在步骤 2 中,引用了属性变量。在这个步骤中,我们将配置这个变量:

      var attributes = {
        pSize:    { type: 'f', value: pSize },
        pOpacity: { type: 'f', value: pOpacity }
      };
    

    我们的 attributes 对象包含两个属性。第一个属性指向包含顶点大小的数组,第二个属性指向包含透明度值的数组。类型 fvalue 表示它是一个浮点数数组。因为我们从着色器材质中引用了这个属性,所以我们可以在着色器中访问这些单独的值。

  4. 在步骤 2 中,我们还定义了一些统一变量。uniforms 对象也被传递到着色器中,但对于所有顶点来说都是相同的:

      var basicShader = THREE.ShaderLib['point_basic'];
      var uniforms = THREE.UniformsUtils.merge([basicShader.uniforms]);
      uniforms['map'].value = THREE.ImageUtils.loadTexture("../assets/textures/ps_smoke.png");
      uniforms['size'].value = 100;
      uniforms['opacity'].value = 0.5;
      uniforms['psColor'].value = new THREE.Color(0xffffff);
    

    在这里,我们重用了 Three.js 在其着色器中使用的标准统一变量,并使用它来进一步配置着色器。

  5. 回顾步骤 2,我们只需要定义两个属性:实际的着色器:document.getElementById('pointVertexShader').textdocument.getElementById('pointFragmentShader').text。让我们从顶点着色器开始:

      <script id="pointVertexShader" type="x-shader/x-vertex">
        precision highp float;
        precision highp int;
        attribute vec3 color;
        attribute float pSize;
        attribute float pOpacity;
        uniform float size;
        uniform float scale;
        varying vec3 vColor;
        varying float vOpacity;
        void main() {
          vColor = color;
          vOpacity = pOpacity;
          vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
          gl_PointSize = 2.0 * pSize * size * ( scale / length( mvPosition.xyz ) );
          gl_Position = projectionMatrix * mvPosition;
        }
      </script>
    

    顶点着色器用于确定顶点的位置和大小。在这个着色器中,我们设置顶点和点的大小,并使用pSize属性进行计算。这样,我们可以控制单个像素的大小。我们还把colorpOpacity的值复制到一个varying值中,这样我们就可以在下一步的片段着色器中访问它。

  6. 到目前为止,点的大小可以直接从 Three.js 中配置。现在,让我们看看片段着色器,并对其进行相同的颜色和透明度设置:

      <script id="pointFragmentShader" type="x-shader/x-fragment">
        precision highp float;
        precision highp int;
        uniform vec3 psColor;
        uniform float opacity;
        varying vec3 vColor;
        varying float vOpacity;
        uniform sampler2D map;
        void main() {
          gl_FragColor = vec4( psColor, vOpacity );
          gl_FragColor = gl_FragColor * texture2D( map,vec2( gl_PointCoord.x, 1.0 - gl_PointCoord.y ) );
          gl_FragColor = gl_FragColor * vec4( vColor, 1.0 );
        }
      </script>
    

    片段着色器只是一个小的程序。我们在这里做的事情如下:

    1. 我们首先将片段(点)的颜色设置为材质上定义的颜色(psColor),透明度设置为点特定的透明度(vOpacity)。

    2. 接下来,我们应用提供的纹理(map)。

    3. 最后,我们将颜色值(gl_Fragcolor)与点特定颜色(vcolor)相乘。

  7. 到目前为止,我们已经配置了材质并创建了特定的着色器。现在,我们只需创建点云并将其添加到场景中:

      ps = new THREE.PointCloud(geometry, psMat2);
      ps.sortPoints = true;
      scene.add(ps);
    

    通过这一最后步骤,您就完成了。

如您所见,这不是一个标准的 Three.js 功能,我们需要采取一些额外的步骤来实现我们的目标。

它是如何工作的...

在上一节中,我们已经解释了如何对单个点进行样式化的一些内容。这里要记住的主要事情是,在底层,Three.js 为渲染创建顶点和片段着色器。如果您想要的功能在标准着色器中无法配置,您可以使用THREE.ShaderMaterial来创建自己的自定义实现。您仍然可以使用 Three.js 来创建您的几何体并处理所有 WebGL 初始化内容,但使用您自己的着色器实现。

更多内容...

使用这种设置,您已经有一个基本的框架来创建基于点云的自定义着色器。您现在可以很容易地通过添加更多功能、其他配置选项等来扩展这个设置。

参见

  • 如果您只想着色单个点,可以参考“在点云中着色单个点”食谱,如果您对在点云中添加动画感兴趣,可以参考“移动点云中的单个点”和“爆炸点云”食谱。

  • 此外,还有一些其他使用顶点和片段着色器的食谱。在本章中,您可以找到“创建自定义后处理步骤”食谱,它使用着色器作为后处理效果。在第五章“光和自定义着色器”,我们有“创建自定义顶点着色器”食谱,它使用自定义顶点着色器来改变几何形状,以及“创建自定义片段着色器”食谱,它使用自定义片段着色器实现来着色 3D 对象。

移动点云中的单个点

当您从几何体创建点云时,点的位置基于提供的几何体的顶点。结果是点云中各个点不会移动。在这个示例中,我们向您展示如何移动点云中的各个点。

准备工作

对于这个示例,我们需要一个包含一些点的点云。您可以创建自己的点云(如我们在从头开始创建点云从现有几何体创建点云示例中解释的)。我们将使用在样式化单个点示例中创建的点云。像往常一样,我们提供了一个示例,您可以在其中看到此示例的最终结果。在您的浏览器中打开06.05-move-individual-points.html,您将看到以下截图:

准备工作

如果您在浏览器中打开它,您将看到所有点在屏幕周围移动。在下一节中,我们将解释您如何做到这一点。

如何实现...

要创建移动的点,我们需要执行以下步骤:

  1. 确保您有一个包含一些点的点云。查看从头开始创建点云基于几何体创建点云示例,了解如何创建此类点云。在这个示例中,我们假设点云可以通过ps变量引用。

  2. 下一步是更新点云中各个点的位置。我们通过更新render循环来完成:

      var step = 0;
      function render() {
        renderer.render(scene, camera);
        requestAnimationFrame(render);
        step=0.005;
        var count = 0;
        var geometry = ps.geometry;
        geometry.vertices.forEach(function(v){
          // calculate new value for the y value
          v.y =  ( Math.sin((v.x/20+step) * Math.PI*2) + Math.cos((v.z/5+step*2) * Math.PI) + Math.sin((v.x + v.y + step*2)/4 * Math.PI))/2;
          // and calculate new colors
          geometry.colors[count++]= new THREE.Color(v.y,0.5,0.7);
        });
        geometry.verticesNeedUpdate = true;
        geometry.colorsNeedUpdate = true;
      }
    

    render循环中,我们通过ps变量访问几何体。接下来,我们根据步进变量的值改变每个点的y位置(v.y)。通过在每次渲染循环中增加步进值,我们创建了您查看此示例时可以看到的动画。最后,我们需要通过将geometry.verticesNeedUpdate设置为true来告诉 Three.js 几何体中顶点的位置已更改。

在这个示例中,我们也改变了每个点的颜色,以便通知 Three.js 这些变化,因此我们也设置geometry.colorsNeedUpdatetrue

它是如何工作的...

这个示例以非常简单的方式工作。通过简单地改变可以移动点的顶点位置,基于THREE.Geometry的顶点位置创建点云。

参考内容

  • 在这个示例中,我们以非常简单的方式改变了顶点的位置。我们只是改变了顶点的y值。在爆炸点云示例中,我们向您展示了一种根据顶点的法向量改变顶点位置的方法。

爆炸点云

您可以使用点云创建许多有趣的效果。例如,您可以创建水、烟雾和云效果。在这个示例中,我们向您展示另一种使用点可以创建的有趣效果。我们将向您展示如何根据每个点的法向量爆炸点云。

准备工作

对于这个食谱,在我们开始查看食谱之前,不需要采取任何步骤。我们提供了一个示例,你可以看到结果爆炸的效果。在浏览器中打开 06.06-explode-geometry.html 示例,你会看到一个类似于以下截图的屏幕:

准备中

如果你点击 implode 按钮,点将移动到屏幕中央;如果你点击 explode,它们将向外移动。通过 speed 属性,你可以设置点移动的速度。

如何做到这一点…

要实现这个效果,你只需要执行几个小步骤:

  1. 我们需要做的第一件事是创建几何形状。为了达到最佳效果,我们使用具有许多顶点的几何形状:

      cube = new THREE.CubeGeometry(4,6,4,20,20,20);
      cube.vertices.forEach(function(v) {
        v.velocity = Math.random();
      });
      createPointSystemFromGeometry(cube);
    

    如你所见,我们不仅创建了几何形状,还向每个顶点添加了一个 velocity 参数,并将其设置为随机值。我们这样做是为了确保不是所有点以相同的速度爆炸(这将产生与仅缩放几何形状相同的效果)。

  2. 现在,我们可以创建点云了:

      var psMat = new THREE.PointCloudMaterial();
      psMat.map = THREE.ImageUtils.loadTexture("../assets/textures/ps_ball.png");
      psMat.blending = THREE.AdditiveBlending;
      psMat.transparent = true;
      psMat.opacity = 0.6;
      var ps = new THREE.PointCloud(cube, psMat);
      ps.sortPoints = true;
      scene.add(ps);
    

    这只是一个基于我们在步骤 1 中创建的几何形状的标准点云。

  3. 在食谱的介绍中,我们提到我们想要根据每个点的法向量来爆炸点。因此,在我们开始渲染场景和更新单个点的位置之前,我们首先需要计算每个向量的法线:

      var avgVertexNormals = [];
      var avgVertexCount = [];
      for (var i = 0 ; i < cube.vertices.length ; i++) {
        avgVertexNormals.push(new THREE.Vector3(0,0,0));
        avgVertexCount.push(0);
      }
      // first add all the normals
      cube.faces.forEach(function (f) {
        var vA = f.vertexNormals[0];
        var vB = f.vertexNormals[1];
        var vC = f.vertexNormals[2];
        // update the count
        avgVertexCount[f.a]+=1;
        avgVertexCount[f.b]+=1;
        avgVertexCount[f.c]+=1;
        // add the vector
        avgVertexNormals[f.a].add(vA);
        avgVertexNormals[f.b].add(vB);
        avgVertexNormals[f.c].add(vC);
      });
      // then calculate the average
      for (var i = 0 ; i < avgVertexNormals.length ; i++) {
        avgVertexNormals[i].divideScalar(avgVertexCount[i]);
      }
    

    我们不会详细解释这个代码片段,但在这里我们所做的是根据特定向量所属面的法向量计算每个顶点的法向量。最终的法向量存储在 avgVertexNormals 数组中。

  4. 接下来,我们看看一个辅助函数,我们将在下一步的 render 循环中调用它。这个函数根据我们在步骤 1 中定义的速度函数和步骤 3 中计算的法向量确定每个顶点的新位置:

      function explode(outwards) {
        var dir = outwards === true ? 1 : -1;
        var count = 0;
        cube.vertices.forEach(function(v){
          v.x+=(avgVertexNormals[count].x * v.velocity * control.scale)*dir;
          v.y+=(avgVertexNormals[count].y * v.velocity * control.scale)*dir;
          v.z+=(avgVertexNormals[count].z * v.velocity * control.scale)*dir;
          count++;
        });
        cube.verticesNeedUpdate = true;
      }
    

    control.scale 变量通过 GUI 设置,并决定了我们的几何形状扩展的速度,而 dir 属性则基于我们是要将点向外移动还是向内移动。verticesNeedUpdate 属性是必需的,用于通知 Three.js 这些变化。

  5. 现在剩下的唯一事情就是从 render 循环中调用 explode 函数:

      function render() {
        renderer.render(scene, camera);
        explode(true); // or explode(false)
        requestAnimationFrame(render);
      }
    

还有更多

在这个例子中,我们使用了一个标准几何形状;当然,你也可以使用外部加载的模型。

还有更多

例如,这个截图展示了牛的爆炸模型。

参见

  • 另一个处理动画和移动单个点的食谱可以在 移动点云中的单个点 食谱中找到。

设置基本后处理管线

除了在 3D 中渲染场景外,Three.js 还允许您将后处理效果添加到最终输出中。通过后处理,您可以将最终渲染的 2D 图像应用所有不同类型的过滤器。例如,您可以添加特定的模糊效果,锐化特定的颜色,等等。在这个菜谱中,我们将向您展示如何在 Three.js 中设置后处理管道,您可以使用它来向最终渲染的场景添加效果。

准备中

要在 Three.js 中使用后处理,您需要包含来自 Three.js 分发的多个额外的 JavaScript 文件。对于这个菜谱,以下 JavaScript 文件应该添加到您的 HTML 页面中:

  <script src="img/CopyShader.js"></script>
  <script src="img/EffectComposer.js"></script>
  <script src="img/RenderPass.js"></script>
  <script src="img/ShaderPass.js"></script>
  <script src="img/MaskPass.js"></script>

为了演示后处理是如何工作的,我们将把点屏幕效果应用到 Three.js 场景中。为了这个效果,我们需要一个额外的 JavaScript 文件:

  <script src="img/DotScreenShader.js"></script>

我们还提供了一个示例,展示了这个菜谱的最终结果。您可以通过在浏览器中打开06.07-setup-basic-post-processing-pipeline.html来查看它。您将看到以下截图类似的内容:

准备中

在这个截图中,您可以看到我们已经渲染了一个包含大量立方体的场景,并应用了一个效果将其渲染为一系列点。

如何操作...

设置后处理管道只需要几个小步骤:

  1. 要设置一个后处理管道,我们需要一个叫做作曲家的东西。我们将在render循环中使用这个作曲家来创建最终输出。为此,我们首先需要一个新全局变量:

      var composer;
    
  2. 接下来,我们需要实例化一个作曲家,作为THREE.EffectComposer的新实例:

      composer = new THREE.EffectComposer( renderer );
    

    我们传递THREE.WebGLRenderer,这是我们通常用来渲染场景的。

  3. 现在,我们需要定义作曲家将要执行的步骤。这些步骤是顺序执行的,我们可以使用它们来对场景应用多个效果。我们始终需要采取的第一步是渲染场景。为此,我们使用THREE.RenderPass

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

    渲染过程使用我们在第二步中配置的相机和渲染器渲染场景对象。

  4. 现在我们已经渲染了场景,我们可以应用一个后处理效果。对于这个菜谱,我们使用THREE.DotScreenShader

      var effect = new THREE.ShaderPass( THREE.DotScreenShader);
      effect.uniforms[ 'scale' ].value = 4;
      effect.renderToScreen = true;
      composer.addPass( effect );
    

    在这个代码片段中,我们创建了一个后处理步骤(THREE.ShaderPass),将其添加到作曲家(composer.addPass(effect)),并告诉效果作曲家通过将renderToScreen设置为true将此步骤的输出渲染到屏幕上。

  5. 我们需要采取的最后一步是修改渲染循环:

      function render() {
        composer.render();
        requestAnimationFrame(render);
      }
    

    如您所见,我们现在使用在第二步中创建的composer对象来渲染最终输出,而不是THREE.WebGLRenderer

在这个菜谱中,我们只使用了一个后处理步骤,但您可以使用任意多的步骤。您只需记住,在最终步骤中,您需要将renderToScreen属性设置为true

它是如何工作的...

在几个菜谱中,我们已经解释了 Three.js 使用 WebGL 着色器来渲染 3D 场景。THREE.EffectComposer 使用相同的方法。您添加的每个步骤都在前一个步骤的输出上运行一个简单的顶点和片段着色器。在 创建自定义后处理步骤 菜谱中,我们将深入了解并创建一个自定义后处理步骤。

更多内容

Three.js 提供了大量标准着色器和步骤,您可以在 THREE.EffectComposer 中使用。为了全面了解可能的着色器和标准步骤,请查看以下目录:

相关内容

  • 尽管 Three.js 提供了大量的标准着色器和后处理步骤,但您也可以轻松创建自己的。在 创建自定义后处理步骤 菜谱中,我们向您展示了如何创建一个与 THREE.EffectComposer 一起工作的自定义顶点和片段着色器。

创建自定义后处理步骤

设置基本后处理管道 菜谱中,我们向您展示了如何使用 THREE.EffectComposer 为 Three.js 场景添加后处理效果。在本菜谱中,我们将解释如何创建自定义处理步骤,您可以使用 THREE.EffectComposer 来使用这些步骤。

准备工作

本菜谱使用 THREE.EffectComposer,因此我们需要加载一些包含正确对象的附加 JavaScript 文件。为此,您需要在 HTML 页面的顶部添加以下内容:

  <script src="img/CopyShader.js"></script>
  <script src="img/EffectComposer.js"></script>
  <script src="img/RenderPass.js"></script>
  <script src="img/ShaderPass.js"></script>
  <script src="img/MaskPass.js"></script>

在本菜谱中,我们将创建一个使用马赛克效果转换输出的后处理效果。您可以通过在浏览器中打开 06.08-create-custom-post-processing-step.html 来查看最终结果。您将看到以下截图类似的内容:

准备工作

你可能不会认出这个,但你看到的是大量正在旋转的立方体。

如何操作...

我们通过使用自定义片段着色器来创建此效果。以下步骤解释了如何设置:

  1. 我们首先需要创建 THREE.EffectComposer 并配置步骤:

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

    到目前为止,我们只添加了渲染步骤 (THREE.RenderPass),它渲染场景并允许我们添加额外的后处理效果。

  2. 要使用自定义着色器,我们需要使用 THREE.ShaderPass 对象:

      var customShader = {
        uniforms: {
          "tDiffuse": { type: "t", value: null},
          "scale":    { type: "f", value: 1.0 },
          "texSize":  { type: "v2", value: new THREE.Vector2( 50, 50 ) },
          "center":   { type: "v2", value: new THREE.Vector2( 0.5, 0.5 ) },
        },
        vertexShader: document.getElementById('hexagonVertexShader').text,
        fragmentShader: document.getElementById('hexagonFragmentShader').text
      };
      var effect = new THREE.ShaderPass( customShader );
      effect.renderToScreen = true;
      composer.addPass( effect );
    

    我们将customShader作为参数传递给THREE.ShaderPass。这个customShader对象包含我们自定义着色器的配置。uniforms对象是我们传递给我们的自定义着色器的变量,而vertexShaderfragmentShader指向我们的着色器程序。

  3. 让我们首先看看第 2 步中的vertexShader

      <script id="hexagonVertexShader" type="x-shader/x-vertex">
        varying vec2 texCoord;
        void main() {
          texCoord = uv;
          gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        }
      </script>
    

    这是一个简单的顶点着色器,它不会改变与输出相关的任何内容。在这个着色器代码中需要注意的唯一一点是我们将正在处理的坐标(uv,由 Three.js 自动传入)作为名为texCoordvarying值传递给片元着色器。

  4. 最后一步是查看第 2 步中的片元着色器:

      <script id="hexagonFragmentShader" type="x-shader/x-fragment">
        uniform sampler2D tDiffuse;
        uniform vec2 center;
        uniform float scale;
        uniform vec2 texSize;
        varying vec2 texCoord;
        void main() {
          vec2 tex = (texCoord * texSize - center) / scale;
          tex.y /= 0.866025404;
          tex.x -= tex.y * 0.5;
          vec2 a;
          if (tex.x + tex.y - floor(tex.x) - floor(tex.y) < 1.0)
          a = vec2(floor(tex.x), floor(tex.y));
          else a = vec2(ceil(tex.x), ceil(tex.y));
          vec2 b = vec2(ceil(tex.x), floor(tex.y));
          vec2 c = vec2(floor(tex.x), ceil(tex.y));
          vec3 TEX = vec3(tex.x, tex.y, 1.0 - tex.x - tex.y);
          vec3 A = vec3(a.x, a.y, 1.0 - a.x - a.y);
          vec3 B = vec3(b.x, b.y, 1.0 - b.x - b.y);
          vec3 C = vec3(c.x, c.y, 1.0 - c.x - c.y);
          float alen = length(TEX - A);
          float blen = length(TEX - B);
          float clen = length(TEX - C);
          vec2 choice;
          if (alen < blen) {
            if (alen < clen) choice = a;
            else choice = c;
          } else {
            if (blen < clen) choice = b;
            else choice = c;
          }
          choice.x += choice.y * 0.5;
          choice.y *= 0.866025404;
          choice *= scale / texSize;
          gl_FragColor = texture2D(tDiffuse, choice 
            + center / texSize);
        }
      </script>
    

    这是一个相当大的着色器程序,详细解释超出了这个配方的范围。简而言之,这个着色器查看周围像素的颜色,并根据这个信息确定如何绘制这个像素。这里需要注意的重要项是代码顶部的uniform sampler2D tDiffuse。这是传递给着色器作为 2D 纹理的前一个渲染步骤的输出。在计算中使用tDiffuse,我们可以改变屏幕上渲染的输出。如果我们不想应用效果,我们只需使用vec4 color = texture2D(tDiffuse, texCoord)来设置输出颜色。

  5. 最后一步是将render循环更新为使用 composer 而不是 renderer:

      function render() {
        composer.render();
        requestAnimationFrame(render);
      }
    

编写着色器是一项困难的工作;然而,这样的设置使得创建你自己的自定义着色器变得容易得多。只需用你的实现替换第 4 步中的片段着色器,你就可以开始实验了。

它是如何工作的...

在这个配方中,我们使用了THREE.EffectComposerTHREE.RenderPass来渲染场景。如果我们向THREE.EffectComposer添加更多步骤,我们可以通过访问tDiffuse纹理直接从我们的着色器中访问当前渲染。这样,我们只需编写一个使用tDiffuse纹理作为其输入的着色器,就可以轻松地添加各种效果。

还有更多...

当你编写着色器时,你可以几乎创建任何你想要的东西。然而,着色器的入门可能相当困难。一些应用特定效果的着色器的良好例子可以在github.com/evanw/glfx.js找到。我们在这个配方中使用的着色器也是从hexagonpixalte.js着色器中采用的,该着色器可以在提到的 GitHub 仓库中的src/filters/fun/hexagonalpixelate.js文件夹中找到。

你也可以查看 Three.js 提供的效果的源代码。你可以直接从 GitHub 在github.com/mrdoob/three.js/tree/master/examples/js/shaders访问它们。

参见

在第五章中,我们也创建了两个自定义着色器:

  • 创建自定义顶点着色器 菜谱中,我们解释了您需要采取的步骤来设置自定义顶点着色器

  • 创建自定义片段着色器 菜谱中,我们解释了您需要采取的步骤来设置自定义片段着色器

将 WebGL 输出保存到磁盘

在这本书中,我们迄今为止已经创建了一些非常漂亮的可视化效果。然而,问题在于很难将渲染输出保存为图像。在这个菜谱中,我们将向您展示如何从 WebGL 渲染场景中创建一个普通图像,并将其保存到磁盘上。

准备工作

为了准备这个菜谱,没有太多的事情要做。我们将使用标准的 HTML5 功能,这些功能不仅适用于基于 Three.js 的输出,还适用于任何 HTML5 画布元素。我们已经准备了一个非常简单的示例页面,您可以测试这个菜谱的结果。为此,在您的浏览器中打开 06.09-save-webgl-output.html 示例。您将看到类似于以下截图的内容:

准备中

在这个页面上,您将看到一个单独的 Three.js 场景。如果您按下 p 键,当前状态将被保存为新的图像,然后您可以正常下载。请注意,在先前的截图中,我们已经缩小了页面。

如何做到这一点...

对于这个菜谱,我们只需要采取几个简单的步骤:

  1. 我们首先为按键注册一个事件监听器:

      window.addEventListener("keyup", copyCanvas);
    

    每当按下键时,copyCanvas 函数将被调用。

  2. 现在让我们看看 copyCanvas 函数:

      function copyCanvas(e) {
        var imgData, imgNode;
        if (e.which !== 80) {
          return;
        } else {
          imgData = renderer.domElement.toDataURL();
        }
        // create a new image and add to the document
        imgNode = document.createElement("img");
        imgNode.src = imgData;
        document.body.appendChild(imgNode);
      }
    

    我们在这里做的第一件事是检查哪个键被按下。如果按下 p 键,我们将继续。接下来,我们使用 toDataURL() 函数从画布中获取图像数据。我们需要采取的最后一步是创建一个新的 img 元素,分配数据(imgData),并将其添加到文档中。

  3. 这将适用于非 WebGL 画布元素。然而,如果您使用 WebGL,我们需要采取一个额外的步骤。我们需要像这样实例化 THREE.WebGLRenderer

      renderer = new THREE.WebGLRenderer({preserveDrawingBuffer: true});
    

    如果我们不这样做,您将只看到输出中的黑色屏幕,而不是实际的 WebGL 输出。不过,请注意,这确实会对性能产生不利影响。

它是如何工作的...

在 HTML5 中,可以使用以 data 开头的 URL 描述文件或其他资源。因此,而不是通过多个 HTTP 请求获取资源,这些资源可以直接包含在 HTML 文档中。画布元素允许您将其内容复制为符合此方案的 URL。在这个菜谱中,我们使用这个数据 URL 创建一个新的 img 元素,它可以像普通图像一样保存。

如果你想深入了解数据 URL 方案的细节,你可以查看描述此方案的 RFC(请求评论)tools.ietf.org/html/rfc2397

还有更多

在最新版本的 Chrome 和 Firefox 中,你也可以通过右键点击并选择另存为图片来保存 HTML 画布元素的输出。除了使用标准的浏览器功能外,还可以直接开始下载图片。如果你使用以下代码而不是创建和添加新图片,浏览器将自动将画布作为图片下载:

  var link = document.createElement("a");
  link.download = 'capture.png';
  link.href = imgData;
  link.click();

最后,如果你有一个想要保存为电影的动画,你也可以做到这一点。你可以在以下链接找到如何操作的说明:www.smartjava.org/content/capture-canvas-and-webgl-output-video-using-websockets

第七章:动画与物理

在这一章中,我们将介绍以下配方:

  • 使用 Tween.js 创建动画

  • 使用形变目标进行动画

  • 使用骨骼进行动画

  • 使用 Blender 创建的形变动画

  • 使用 Blender 创建的骨骼动画

  • 添加简单的碰撞检测

  • 在 Chrome 中保存动画电影

  • 在场景周围拖放对象

  • 添加物理引擎

简介

在到目前为止的章节中,我们主要处理静态场景或有限动画的场景。在本章中,我们将向您展示一系列您可以用来使场景更加动态的配方。我们展示了关于如何添加高级动画、如何在场景周围拖放对象以及如何向场景添加物理效果(如重力碰撞检测)的配方。

使用 Tween.js 创建动画

在第一章,入门中,我们已经向你展示了如何设置动画循环,在第二章,几何体和网格中,我们展示了如何通过改变THREE.Mesh的属性来创建简单的动画。当你有很多或复杂的动画时,代码会迅速变得复杂,难以维护或理解。在这个配方中,我们将向你展示如何使用一个外部 JavaScript 库,它可以使动画的创建更容易、更易于维护。我们将使用Tween.js库来完成这个任务。

准备工作

对于这个配方,我们使用来自github.com/sole/tween.js/的库。由于这是一个外部库,我们首先需要确保它包含在我们的 HTML 页面中。为此,首先在页面头部元素中添加以下内容:

  <script src="img/tween.js"></script>

对于这个配方,我们将使用这个库创建一个简单的动画。如果你在浏览器中打开07.01-animation-with-tweenjs.html示例,你可以查看最终结果,它类似于以下截图所示:

准备中

如果你在浏览器中打开这个示例,你会看到一个红色的立方体移动到不同的位置,并在移动过程中旋转。这个动画是通过Tween.js库配置的。

如何做…

一旦你将所需的库添加到你的 HTML 页面中,创建动画只需要几个简单的步骤:

  1. 要使用这个库,我们首先需要创建一个TWEEN.Tween对象的实例:

      var tween = new TWEEN.Tween({x:0 , y:1.25, z:0, rot: 0});
    

    这创建了一个TWEEN.Tween实例。我们可以使用这个实例将提供的属性从起始值(我们在这一步添加的值)移动到结束值。

  2. 下一步是定义属性的靶值。我们通过使用to函数来完成:

      tween.to({x:5, y:15, z:-10, rot: 2*Math.PI}, 5000);
    

    使用此功能,我们告诉tween对象,我们希望将构造函数中提供的值缓慢地改变到这些值。因此,我们将x属性从0改为5。第二个参数,即5000,定义了这种变化需要多少毫秒。

  3. 我们还可以选择值随时间如何变化。例如,您可以使用线性缓动函数,它以恒定速率改变值,一个二次函数,它以小的变化开始并迅速增加,或者甚至使用一个在结束时弹跳(超过)的缓动函数。TWEEN 中预定义了更多缓动函数(有关更多信息,请参阅 还有更多… 部分)。您通过调用缓动函数来实现这一点:

      tween.easing(TWEEN.Easing.Elastic.InOut);
    
  4. 到目前为止,我们已经将这些属性的值从一值更改为另一值,但当一个值改变时,我们并没有真正做任何事情。在这个菜谱中,我们想要改变立方体的位置和旋转。您可以通过调用 onUpdate 函数并传入应在每次更改时调用的函数来实现这一点:

      tween.onUpdate(function() {
        cube.position.set(this.x, this.y, this.z);
        cube.rotation.set(this.rot, this.rot, this.rot);
      });
    

    如您在这段代码片段中所见,我们使用提供的属性来设置立方体的旋转和位置属性。

  5. 您可以在 tween 对象上使用许多其他设置来控制动画的行为。对于这个菜谱,我们告诉 tween 对象无限期地重复其动画,并使用一个每次重复时都会反转动画的悠悠球效果:

      tween.repeat(Infinity);
      tween.yoyo(true);
    
  6. 最后,我们可以通过调用起始函数来开始 tween 对象:

      tween.start();
    
  7. 在这一点上,您不会看到任何发生。您需要添加到 render 循环中的最后一个步骤是通知 tween 对象已经过去的时间,以便它可以计算您在步骤 1 中提供的属性的正确值:

      TWEEN.update();
    

    这将更新您定义的所有 TWEEN.Tween 对象,并使用 updated 值调用 onUpdate 函数。

您定义起始值、结束值以及起始值应该如何过渡到结束值。

它是如何工作的…

每次调用 TWEEN.update() 时,TWEEN 库将确定从上一次调用 TWEEN.update 以来每个 TWEEN.Tween 对象(或对于第一次调用,从在 TWEEN.Tween 对象上调用 start() 以来)经过的时间。基于这个差异,tween 的起始时间和配置的 easing 属性,这个库会为传入的属性计算新的值。最后,它将调用传递给 onUpdate() 的函数,以便您可以对更改的值采取行动。

还有更多…

在这个菜谱中,我们没有展示您可以传递给 TWEEN.Tween 对象的所有配置。有关所有不同的缓动选项和 TWEEN.Tween 对象的其他属性的完整概述,请参阅 GitHub 项目网站 github.com/sole/tween.js/

在我们进入下一个菜谱之前,有一个关于 Tween.js 库的额外有趣方面。在我们的菜谱中,我们逐步配置了 TWEEN.Tween 对象。您也可以像这样在一次调用中配置对象:

  var tween = new TWEEN.Tween({x:0 , y:1.25, z:0, rot: 0}).to({x:5, y:15, z:-10, rot: 2*Math.PI}, 5000).easing(TWEEN.Easing.Elastic.InOut).onUpdate(function() {
    cube.position.set(this.x, this.y, this.z);
    cube.rotation.set(this.rot, this.rot, this.rot);
  })
  .repeat(Infinity)
  .yoyo(true)
  .start();

这之所以可行,是因为 Tween.js 提供了一个流畅的 API。因此,对于每个函数调用,这个库都会返回原始的 TWEEN.Tween 对象。这意味着你可以像我们在前面的代码片段中那样轻松地链式调用。

相关内容

  • 你可以在本书中几乎任何使用动画的地方使用 Tween.js 库。例如,在 第二章,几何体和网格中,我们展示了如何通过旋转对象绕其自身轴的食谱。旋转可以通过一个 TWEEN.Tween 对象轻松管理。在 第三章,与相机一起工作中,我们展示了如何在 将相机缩放到对象 食谱中放大对象。使用 Tween.js 库,我们可以轻松地动画化这个缩放功能。

使用形态目标进行动画化

在建模 3D 对象和角色时,通常有两种不同的创建动画的方式。你可以使用形态目标来动画化,或者使用基于骨骼和骨骼的动画。Three.js 促进了这两种方法。在本例中,我们将查看基于形态的动画。正如其名所示,基于形态的动画会将一个几何形状变形为另一个。这对于面部表情和其他非常详细的动画效果非常适用。

准备就绪

对于这个食谱,我们不需要任何额外的库,因为基于形态的动画由标准的 Three.js 分发支持。为了使这个食谱更易于理解,我们使用一个现有的 3D 模型来演示变形是如何工作的。当你打开浏览器中的 07.02-animation-with-morphing.html 示例时,你可以看到模型和可用的变形。你将看到以下截图所示的内容:

准备就绪

在这个例子中,你可以看到一个简单的汽车模型。使用右上角的滑块,你可以将这个汽车缓慢地变形为不同的模型,如下面的截图所示:

准备就绪

如果你勾选了 animate 复选框,一个自动变形的动画就会开始。

如何操作...

要使用变形动画,我们需要采取以下步骤:

  1. 我们需要做的第一件事是加载包含形态目标的模型。对于这个食谱,我们有一个基于 JSON 的模型,我们这样加载它:

      var jsonLoader = new THREE.JSONLoader();
      jsonLoader.load("../assets/models/morph/car.js",
      function(model, materials) {
        ...
      });
    

    在这里,我们使用 THREE.JSONLoader 来加载一个模型,一旦加载完成,我们就调用提供的函数。

  2. 在我们创建 THREE.Mesh 之前,还有一步需要我们去做。我们需要将 morphTargets 属性设置为 true 的材质设置好:

      materials.forEach(function(mat) {
        mat.morphTargets = true;
      });
    
  3. 接下来,我们需要创建 THREE.Mesh 并将其添加到场景中:

      car = new THREE.Mesh(model,new THREE.MeshFaceMaterial( materials ));
      scene.add(car);
    

    如你所见,我们遵循创建 THREE.Mesh 的标准方式,并将其添加到场景中,就像任何其他对象一样。

  4. 现在我们已经有一个可以在场景中变形的对象,我们可以使用morphTargetInfluences属性来设置对象变形到特定方向的程度。在这个配方的示例中,我们使用 UI 来控制这个设置如下:

      gui.add(control, 'mt_1', 0,1).step(0.01).listen().onChange(function(a){
        car.morphTargetInfluences[1] = a;
      });
      gui.add(control, 'mt_2', 0,1).step(0.01).listen().onChange(function(a){
        car.morphTargetInfluences[2] = a;
      });;
      gui.add(control, 'mt_3', 0,1).step(0.01).listen().onChange(function(a){
        car.morphTargetInfluences[3] = a;
      });
    

    在这个配方中,我们使用了具有四个形态目标(名称分别为mt_0mt_1mt_2mt_3)的模型,其基础状态和三个其他汽车模型。通过增加其中一个其他模型的morphTargetInfluence对象,我们可以将模型变形到那个方向。

正如这个配方中所示,通过简单地改变一个特定的morphTargetInfluences值,你可以改变模型的外观。

它是如何工作的…

在支持多个形态目标的模型中,会存储额外的顶点集来表示每个目标的位置。因此,如果你有一个面部模型,它有一个微笑的形态目标、一个皱眉的形态目标和一个嘲笑的形态目标,你实际上存储了四倍的顶点位置。使用morphTargetInfluences属性,你可以告诉 Three.js 基础状态(geometry.vertices属性)应该向特定形态目标变形多远。然后 Three.js 将计算每个单独顶点的平均位置并渲染更新后的模型。一个非常有趣的事情是你可以组合形态目标。所以如果你有分别针对眼球运动和嘴部运动的独立形态目标,你可以轻松创建非常生动逼真的动画。

还有更多…

在这个配方中,我们加载了一个包含形态目标的模型。如果你已经有一个简单的几何体,你想用于基于形态的动画,你也可以轻松地做到这一点。例如,如果你有一个几何体,你可以使用以下代码添加morphTargets

  cubeGeometry.morphTargets[0] = {name: 't1', vertices:cubeTarget2.vertices};
  cubeGeometry.morphTargets[1] = {name: 't2', vertices:cubeTarget1.vertices};

这里的重要方面是确保你提供给vertices属性与initial几何体中相同的顶点数量。你现在可以使用THREE.Mesh上的morphTargetInfluences属性来控制各种目标之间的变形:

  cube.morphTargetInfluences[0] = 0.4;
  cube.morphTargetInfluences[1] = 0.6;

参见

  • 另一种动画模型的方法可以使用骨骼和骨头。我们在使用骨骼进行动画配方中解释了如何这样做。我们还在本章中提供了两个配方,其中我们在外部工具(在我们的例子中是 Blender)中定义基于形态和骨骼的动画,并在 Three.js 中播放动画。请参阅使用在 Blender 中创建的形态动画使用在 Blender 中创建的骨骼动画配方以获取有关这些方法的更多信息。

使用骨骼进行动画

动画复杂模型的一种常见方法是使用骨骼和蒙皮。在这种方法中,我们定义一个几何体,添加一个骨骼,并将几何体绑定到该骨骼上。每当移动或旋转骨骼的任何一个部分时,几何体都会相应地变形。在这个配方中,我们将向您展示如何使用 Three.js 功能直接从 JavaScript 中移动和旋转骨骼。

准备工作

对于这个配方,我们使用一个外部模型,该模型已经包含我们可以移动的骨骼。为了加载此模型,我们使用THREE.JSONLoader,这是 Three.js 标准分布的一部分。因此,我们不需要导入任何额外的 JavaScript 文件来使这个配方工作。当然,我们提供了一个此配方在动作中的示例,您可以通过在浏览器中打开07.03-animation-with-skeleton.html示例来查看。您将看到以下截图所示的内容:

准备中

此示例向您展示了一头长颈鹿的模型,并提供了一个您可以用来移动颈部骨骼的界面。您可以更改颈部骨骼的旋转甚至其位置。当您这样做时,您会看到网格的一部分会响应该骨骼的运动。在这个配方中,我们将向您展示如何自己完成这项操作。

如何操作…

直接与骨骼一起工作并不困难,只需几个小步骤:

  1. 我们需要做的第一件事是加载一个包含骨骼的模型。对于这个配方,我们再次使用THREE.JSONLoader

      var jsonLoader = new THREE.JSONLoader();
      jsonLoader.load("../assets/models/bones/giraffe.js",function(model, materials) {
        ...
      });
    
  2. 一旦加载了步骤 1 中的模型,我们就可以设置材质并创建网格。让我们首先看看材质:

      materials.forEach(function(mat) {
        mat.skinning = true;
      });
    

    在这里,我们将材质的skinning属性设置为true。这告诉 Three.js 该对象包含骨骼,当骨骼移动时,几何形状应该变形。

  3. 接下来,我们创建网格并将其添加到场景中:

      var giraffe = new THREE.SkinnedMesh(model, materials[0]);
      scene.add(giraffe);
    

    如您所见,我们为这个对象使用了不同类型的网格。我们使用的是THREE.SkinnedMesh对象,而不是THREE.Mesh对象。

  4. 要访问骨骼,我们需要访问THREE.SkinnedMesh的子元素。如果骨骼没有明确命名,获取正确的骨骼进行动画可能需要进行一些实验。确定要使用哪个骨骼的最简单方法是查看 JavaScript 控制台的输出并浏览网格的子元素。如何操作…

  5. 在这种情况下,我们想要旋转尾巴骨骼并旋转和定位颈部。为此,我们在render循环中添加以下内容:

      // the neck bone
      giraffe.children[0].children[1].children[0].children[0].rotation.x = control.neck_rot_x;
      giraffe.children[0].children[1].children[0].children[0].rotation.y = control.neck_rot_y;
      giraffe.children[0].children[1].children[0].children[0].rotation.z = control.neck_rot_z;
      giraffe.children[0].children[1].children[0].children[0].position.x = control.neck_pos_x;
      giraffe.children[0].children[1].children[0].children[0].position.y = control.neck_pos_y;
      giraffe.children[0].children[1].children[0].children[0].position.z = control.neck_pos_z;
      // the tail bone
      giraffe.children[0].children[0].children[0].rotation.z -= 0.1
    

    就这样!每当我们现在更改之前代码片段中使用的骨骼的位置或旋转时,几何形状将相应地变形。

与骨骼一起工作并不困难,但选择要更改和移动的正确骨骼可能需要一些实验。

它是如何工作的…

当你在材质上启用skinning属性时,Three.js 会将有关相关骨骼和位置的所有信息传递到其顶点着色器中。顶点着色器将使用这些信息根据相关骨骼的位置和旋转将顶点定位到新位置。更多信息和关于如何从顶点着色器执行骨骼动画的良好介绍可以在 OpenGL 网站上找到,链接为www.opengl.org/wiki/Skeletal_Animation

还有更多…

如果你想快速了解模型中骨骼的排列方式,你可以使用 Three.js 提供的特定辅助类。以下代码片段展示了如何为我们在本配方中使用的模型创建THREE.SkeletonHelper

  var helper = new THREE.SkeletonHelper(giraffe);
  scene.add(helper);

这将可视化模型的骨骼,如下面的截图所示:

还有更多…

如果你移动骨骼,就像我们在配方中所做的那样,你还需要在你的render循环中添加以下行:

  helper.update();

这样,THREE.SkeletonHelper将始终反映模型的最新状态。

参见

  • 使用形态目标来动画化模型是一种更简单的方法。我们在“使用形态目标进行动画”的配方中解释了如何做到这一点。在本章中,我们还提供了两个配方,其中我们在外部工具(在我们的例子中是 Blender)中定义基于形态和骨骼的动画,并在 Three.js 中播放动画。有关这些方法的更多信息,请参阅“使用在 Blender 中创建的形态动画”和“使用在 Blender 中创建的骨骼动画”配方。

使用在 Blender 中创建的形态动画

在 Three.js 中手动创建形态动画是困难的。简单的变换可能可以处理,但通过编程创建高级动画非常困难。幸运的是,有许多外部 3D 程序可以用来创建模型和动画。在本配方中,我们将使用 Blender,我们在第二章,“几何体和网格”中已经使用过,来创建基于形态的动画,并使用 Three.js 回放。

准备中

要使用此配方,你必须安装 Blender 并启用 Three.js 导出插件。我们已经在第二章,“几何体和网格”中的“创建和导出从 Blender 模型”配方中解释了如何做到这一点。所以如果你还没有这样做,你应该首先安装 Blender,然后安装 Three.js 导出插件。一旦安装了 Blender,你应该创建一个使用形状键定义各种格式的动画。这超出了本书的范围,但为了确保,你可以测试本配方中解释的步骤——我们包含了一个 Blender 文件,它包含一个基于最小形状键的动画。所以在我们开始配方之前,我们将加载示例 Blender 模型。

为了做到这一点,请按照以下步骤操作:

  1. 打开Blender并导航到文件 | 打开

  2. 在打开的窗口中,导航到与本书提供的资源,并打开位于assets/models/blender目录中的simplemorph.blend文件。

  3. 一旦打开此文件,你将看到一个立方体位于一个空场景的中心,就像这样:准备中

    这是我们开始配方的起点。

  4. 如果你想要预览我们在这里创建的(非常简单)动画,只需点击播放按钮或使用Alt + A键组合。

  5. 我们将在 Three.js 中加载此文件并播放我们在 Blender 中创建的动画。要查看最终结果,在你的浏览器中打开07.04-create-morph-in-blender.html示例。你将看到以下截图所示的内容:准备就绪

    你将看到一个使用形态目标(在 Blender 中定义为形状键)来将一个立方体变形为不同形状的动画立方体。

如何操作…

如果你已经按照本食谱中准备就绪部分所解释的步骤进行,你将看到一个简单的 Blender 工作区,其中有一个立方体和一个动画,该动画使用一系列形状键缓慢地变形立方体。要从 Blender 导出此动画并在 Three.js 中使用它,我们需要采取几个步骤:

  1. 我们需要做的第一件事是导出模型和动画,这样我们就可以在 Three.js 中加载它。为此,导航到文件 | 导出 | Three.js

  2. 在打开的窗口中,我们可以选择一个目的地和文件名。对于这个食谱,将文件命名为simplemorph.js并将目的地设置为assets/models/morph文件夹。

  3. 在我们点击导出按钮之前,我们需要配置一些 Three.js 特定的属性。你可以在导出 Three.js部分的左侧面板中这样做。在该部分中,确保选中形态动画复选框。一旦选中复选框,点击导出按钮。

  4. 现在我们已经完成了在 Blender 中的工作,可以加载导出的模型到 Three.js 中。为此,我们使用THREE.JSONLoader,如下所示:

      var loader = new THREE.JSONLoader();
      loader.load("../assets/models/morph/simplemorph.js",function(model){
        ...
      });
    

    在这个代码片段中,我们使用THREE.JSONLoader加载模型。

  5. 一旦模型被加载,我们需要创建一个材质,其中需要将morphTargets属性设置为true

      var mat = new THREE.MeshLambertMaterial({color: 0xff3333, morphTargets:true})
    
  6. 使用这种材质,我们可以创建要添加到场景中的网格。这次,因为我们想使用从 Blender 提供的动画,我们创建THREE.MorphAnimMesh并将其添加到场景中:

      mesh = new THREE.MorphAnimMesh(model, mat);
      mesh.castShadow = true;
      scene.add(mesh);
    
  7. 在我们可以播放动画之前,我们需要采取最后一步:

      mesh.parseAnimations();
      mesh.playAnimation('animation', 20);
      mesh.duration = 10;
      render();
    

    使用parseAnimation()函数,Three.js 将解析模型中提供的形态目标元素的名称,并使用它来创建一个动画。当你使用 Blender 中的 Three.js 插件导出时,动画的名称是animation。要播放动画,我们调用playAnimation函数,传入动画名称和帧率,最后设置动画的持续时间(以秒为单位)。请注意,你并不总是需要设置动画的持续时间。在某些情况下,模型本身提供了持续时间。

  8. 我们需要在render函数本身中进行最后的更改:

      var t = new THREE.Clock();
      function render() {
        renderer.render(scene, camera);
        mesh.updateAnimation(t.getDelta());
        requestAnimationFrame(render);
      }
    

    在这里,我们创建一个全局的THREE.Clock()实例,我们使用它来确定在连续调用render函数之间经过的时间。这个实例被传递到THREE.MorphAnimMeshupdateAnimation函数中,以便它可以计算要显示的帧。

如你所见,从 Blender 将动画播放到 Three.js 中并不困难。然而,这里需要注意的是,当模型具有高顶点数时,这可能会导致文件变得非常大。这是因为 Blender 导出插件为动画的每一帧创建一个新的形态目标。

更多内容…

在这个菜谱中,我们使用了 Blender 的 Three.js 导出功能来保存模型,以便 THREE.JSONLoader 可以加载。还有大量其他 3D 格式可供使用,可以用来存储 Three.js 支持的 3D 场景和动画。Three.js 中可用的文件格式概述可以在 github.com/mrdoob/three.js/tree/master/examples/js/loaders 的 Three.js GitHub 网站上找到。

相关内容

在这一章中,我们还有一些其他与动画相关的菜谱:

  • 使用形态目标进行动画制作

  • 使用骨骼的动画

  • 使用在 Blender 中创建的骨骼动画

使用在 Blender 中创建的骨骼动画

使用骨骼的动画 菜单中,我们通过直接改变模型骨骼的位置和旋转来动画化一个模型。这在交互式场景中效果很好,但并不是创建动画的实用方法。使用 Blender 和其他 3D 工具,你可以获得一套创建基于特定骨骼和一系列骨骼的动画的工具。在这个菜谱中,我们将向你展示如何播放一个在 Blender 中创建的基于骨骼的动画。

准备中

要使用此菜谱,你需要安装 Blender 并启用 Three.js 导出插件。如果你还没有这样做,请按照 第二章 中 从 Blender 创建和导出模型 菜谱的步骤进行操作,几何体和网格。一旦 Blender 和 Three.js 导出插件已安装,我们需要创建一个基于骨骼的动画。在 Blender 中创建这个动画超出了本书的范围,因此我们提供了一个现有的模型来演示这个菜谱。要开始,请执行以下步骤:

  1. 打开 Blender 并导航到 文件 | 打开

  2. 在打开的窗口中,导航到书中提供的资源并打开 assets/models/blender 目录中的 crow-skeleton.blend 文件。

  3. 一旦打开此文件,你将看到一个乌鸦位于一个空场景的中心,如下所示:准备中

    这是本菜谱的起点。

  4. 如果你想要预览乌鸦动画,请点击播放按钮或使用 Alt + A 键组合。

我们还提供了一个示例,你可以在浏览器中打开以查看 Three.js 场景中的相同动画。当你打开浏览器中的 07.05-create-skeleton-animation-in-blender.html 示例时,你应该会看到如下内容:

准备中

如何操作…

在我们能够在 Three.js 中使用模型之前,我们首先必须从 Blender 中导出它:

  1. 要开始导出,首先导航到 文件 | 导出 | Three.js

  2. 在打开的窗口中,我们可以选择一个目的地和文件名。对于这个食谱,将文件命名为 crow.js 并将目的地设置为 assets/models/bones 文件夹。

  3. 在我们点击 导出 按钮之前,我们需要配置一些 Three.js 特定的属性。你可以在 导出 Three.js 部分的左侧面板中完成此操作。在该部分中,确保选中 骨骼皮肤骨骼动画 复选框。如果选中了 形变动画 复选框,请禁用它。一旦选中复选框,点击 导出 按钮。

  4. 现在我们已经导出了模型,在 Three.js 中我们需要做的第一件事是使用 THREE.JSONLoader 加载模型:

      var loader = new THREE.JSONLoader();
      loader.load("../assets/models/bones/crow.js",function(model){
        ...
      });
    
  5. 一旦模型在 Three.js 中加载,我们就可以处理它了。从 loader.load 函数的回调中,我们首先做的事情是设置材质:

      var mat = new THREE.MeshLambertMaterial({color: 0xf33f33,shading: THREE.FlatShading, skinning:true})
    

    这只是一个标准的 THREE.MeshLambertMaterial 对象。你需要确保的唯一事情是将材质的 skinning 属性设置为 true

  6. 现在我们已经得到了模型和材质,我们可以创建一个网格。由于我们正在处理骨骼,我们需要创建 THREE.SkinnedMesh

      mesh = new THREE.SkinnedMesh(model, mat);
    
  7. 接下来,我们需要选择我们想要播放的动画。为此,你可以使用以下代码片段:

      model.animation = "Crow.ArmatureAction";
      THREE.AnimationHandler.add(model.animations[0]);
      var animation = new THREE.Animation(mesh, model.animation );
      animation.play();
    

    你需要确保 animation 属性包含 model.animations 数组中某个动画的名称。在这种情况下,我们只有一个名为 Crow.ArmatureAction 的动画。基于骨骼的动画使用 THREE.AnimationHandler 处理。因此,我们将我们的模型中的动画添加到处理器中。接下来,我们需要创建一个 THREE.Animation 实例。该对象将我们的模型与想要播放的动画结合起来。当我们拥有这个对象时,我们可以调用 play() 函数来告诉 Three.js 播放动画。

  8. 在动画播放之前,我们需要采取的最后一步是更新 render 循环:

      var t = new THREE.Clock();
      function render() {
        renderer.render(scene, camera);
        THREE.AnimationHandler.update( t.getDelta() );
        requestAnimationFrame(render);
      }
    

    在这里,我们使用 THREE.Clock() 来确定这一帧和前一帧之间经过的时间 (t.getDelta())。这个值被传递到 THREE.AnimationHandler 中,以更新所有注册的动画并移动网格到正确的位置。

它是如何工作的…

当导出动画时,Three.js 导出器将输出我们在 Blender 中指定的时刻骨骼的位置和旋转。然后,这些信息可以直接在 Three.js 中使用,以确定播放动画时骨骼的位置和旋转。这样,我们可以在不创建大型模型文件的情况下创建相当复杂的动画。

还有更多...

在 Blender 中处理骨骼并从中创建动画是一个被广泛讨论的主题。如果你对学习如何绑定模型和创建基于骨骼的动画感兴趣,以下是一些不错的起点资源:

参见

在这一章中,我们还有一些其他处理动画的配方:

  • 使用形态目标进行动画

  • 使用骨骼进行动画

  • 使用 Blender 创建的形态动画

添加简单的碰撞检测

当你创建游戏或交互式环境时,一个常见的需求是检测对象之间的碰撞。在添加物理引擎的配方中,我们使用外部库来处理碰撞(以及其他物理)。然而,如果你只需要检测碰撞的选项,这却是一个相当重的解决方案。在这个配方中,我们提供了一个简单的方案,如果你想在不需要使用外部库的情况下检测碰撞,你可以使用它。

准备工作

在这个配方中,我们使用THREE.Raycaster来检查碰撞。这个对象由标准的 Three.js 分发提供,所以你不需要任何额外的库。我们提供了一个简单的示例,展示了如何应用这个配方。为此,在你的浏览器中打开07.06-add-simple-detection-collision.html示例,你将看到以下截图所示的内容:

准备工作

在这个示例中,你可以使用箭头键移动中央立方体,并使用ab键绕y轴旋转它。每当与其他立方体发生碰撞时,我们将改变不透明度以指示碰撞。

如何做…

要实现碰撞检测,我们需要采取几个步骤:

  1. 让我们从简单开始,创建我们将要移动的立方体。我们将检测这个立方体与我们定义的第 2 步中的立方体之间的碰撞:

      var cubeGeometry = new THREE.BoxGeometry(2, 2, 2);
      var cubeMaterial = new THREE.MeshLambertMaterial({color: 0xff2255});
    var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
    cube.name='cube';
    scene.add(cube);
    
  2. 现在,让我们创建一个数组来存储我们可以与之碰撞的所有对象,并向该数组添加一些立方体:

    var cubes = [];
    var cubeMaterial2 = new THREE.MeshLambertMaterial({color: 0xff0000});
    var cube2 = new THREE.Mesh(cubeGeometry, cubeMaterial2);
    cube2.position.set(5,0,0);
    cube2.name='cube-red';
    scene.add(cube2);
    cubes.push(cube2);
    ...
    var cubeMaterial5 = new THREE.MeshLambertMaterial({color: 0xff00ff});
    var cube5 = new THREE.Mesh(cubeGeometry, cubeMaterial5);
    cube5.position.set(-5,0,0);
    cube5.name='cube-purple';
    scene.add(cube5);
    cubes.push(cube5);
    
  3. 现在我们已经得到了可以移动的对象和可以检测碰撞的对象,我们可以添加代码来检测碰撞。在render循环中,我们需要添加以下内容:

    // reset the opacity at the beginning of the loop
    cubes.forEach(function(cube){
        cube.material.transparent = false;
        cube.material.opacity = 1.0;
    
    });
    
    var cube = scene.getObjectByName('cube');
    var originPoint = cube.position.clone();
    
    for (var vertexIndex = 0; 
             vertexIndex < cube.geometry.vertices.length;
             vertexIndex++) {
        var localVertex = cube.geometry.
        vertices[vertexIndex].clone();
        var globalVertex = localVertex.applyMatrix4( 
                              cube.matrix);
        var directionVector = globalVertex.sub( 
                              cube.position);
    
        var ray = new THREE.Raycaster( 
                       originPoint,
                       directionVector.clone().normalize() );
        var collisionResults = ray.intersectObjects( cubes );
        if ( collisionResults.length > 0 
                     && collisionResults[0].distance < 
                             directionVector.length() ) {
             collisionResults[0].object
                        .material.transparent = true;
            collisionResults[0]
                        .object.material.opacity = 0.4;
        }
    }
    

    在这段代码中,我们简单地检查我们移动的立方体的一个顶点是否与cubes数组中的任何立方体相交。如果我们检测到碰撞,我们将改变与之碰撞的立方体的不透明度。

通过这些步骤,我们得到了一个基本的碰撞检测解决方案。这种方法非常适合检测平面物体之间的碰撞,但可能会错过类似小尖刺状物体的检测。你可以通过检查更多顶点来增强这个解决方案。例如,你可以通过增加立方体的widthSegmentsheightSegmentsdepthSegments对象来添加更多顶点,或者你可以自己计算中间顶点。

它是如何工作的…

在这种方法中检测碰撞时,我们使用THREE.RayCaster从移动的立方体的中心向每个顶点发射射线。如果这条射线与从中心到顶点的路径上的cubes数组中的其他立方体相交,这意味着其中一个顶点位于另一个立方体内部。我们将此解释为碰撞,并可以采取适当的行动。

还有更多…

这个配方基于 Lee Stemkoski 所做的大量工作,他在 stemkoski.github.io/Three.js/Collision-Detection.html 上提供了一个这种方法的初始实现。除了基于射线的碰撞检测方法之外,当然还有其他方法。一个非常常见的方法是使用网格的边界框来检测两个网格是否接触。Three.js 甚至在其THREE.Box3对象中提供了一个名为isIntersectionBox的函数。由于使用射线投射方法检测碰撞是一种计算成本相当高的方式,因此通常首先使用边界框方法,然后使用更精确的射线投射方法。

关于这种方法的几个好资源可以在这里找到:

添加物理引擎 的配方中,我们将使用的物理引擎也采用基于形状的碰撞检测方法。除了边界框之外,它还提供了一系列不同的形状来检测碰撞。

参见

  • 添加物理引擎 的配方中,我们使用物理引擎来检测碰撞。对于另一个使用THREE.RayCaster的配方,你也可以查看 在场景中拖放对象 的配方,这个配方也可以在本章中找到。

在 Chrome 中保存动画电影

在本章中,我们向您展示了创建动画的各种方法。然而,有时人们没有启用 WebGL 的浏览器,或者您只想分享生成的动画而不是 WebGL 网站。在这些情况下,能够将动画直接保存到本地文件系统并分享它将非常有帮助。在本菜谱中,我们向您展示了一种可以用于此场景的方法。

准备中

要使用这个菜谱,您需要确保使用 Google Chrome。我们使用一个内部功能将动画保存为 WebM 文件,不幸的是,这仍然只在 Google Chrome 上工作。我们不需要从头开始创建这个菜谱的完整功能,因为有一个库可以为我们处理底层技术细节:CCapture (github.com/spite/ccapture.js/)。要使用这个库,我们需要在 HTML 页面的顶部加载以下两个 JavaScript 文件:

  <script src="img/CCapture.min.js"></script>
  <script src="img/Whammy.js"></script>

我们提供了一个非常简单的示例,展示了这个菜谱的实际应用。如果您在浏览器中打开 07.07-save-a-movie-of-an-animation.html,您将在浏览器中看到一个缓慢移动的立方体,如下面的截图所示:

准备中

这个立方体移动得如此缓慢的原因是在后台正在保存一个电影。使用的库会减慢动画速度,以确保不会跳过任何帧。要保存电影,请点击屏幕顶部的saveMovie菜单按钮。

生成的电影现在可以在支持 WebM 的任何电影播放器中播放(例如,VLC 或 mPlayer),如下面的截图所示:

准备中

如何做到这一点…

一旦您在您的 HTML 页面中包含了适当的库,使用这个库实际上非常简单:

  1. 我们需要做的第一件事是创建一个 capture 对象:

      Var capturer = new CCapture({
        framerate: 20
      });
    

    在这里,我们创建了一个每秒捕获 20 帧的 capturer

  2. 在我们开始渲染场景之前,下一步是启动 capturer

      capturer.start();
      // call the render loop
      render();
    
  3. 我们还需要在 render 循环中告诉 capturer 要捕获什么:

      function render() {
        renderer.render(scene, camera);
        capturer.capture( renderer.domElement );
        requestAnimationFrame(render);
      }
    

    通过这些步骤,capturer 对象将开始以每秒 20 次的速度捕获我们的 WebGL 画布的输出。

  4. 作为最后一步,我们需要添加一个保存电影的功能(在我们的例子中,这是通过点击saveMovie按钮触发的):

      this.saveMovie = function() {
        var videoUrl = capturer.save();
        var link = document.createElement("a");
        link.download = 'video.webm';
        link.href = videoUrl;
        link.click();
      };
    

    这将下载电影为 video.webm 并将其保存到您的本地磁盘。

当您运行这个程序时,您会注意到浏览器中的帧率显著下降。原因是 CCapture 库改变了 requestAnimationFrame 函数的行为,以确保它有足够的时间捕获屏幕并将其添加为电影的一帧。创建的电影文件将看起来如您预期,并且具有每秒帧数,正如本菜谱的第 1 步所指定的。

还有更多…

我们在食谱中展示的方法对于大多数类型的动画都非常好用。然而,当你想要记录用户与你的场景交互时,你不能使用这个库,因为它会减慢场景的渲染速度,这使得与场景交互变得困难。记录场景的另一种方法是使用后端服务,该服务收集截图并在服务器端创建电影。这种设置的示例可以在www.smartjava.org/content/capture-canvas-and-webgl-output-video-using-websockets找到。

参考以下内容

  • 如果你只想保存屏幕截图而不是完整的电影,你可以使用将 WebGL 输出保存到磁盘的食谱,我们在第六章,点云和后处理中解释了它。

在场景中拖动和放置对象

当你创建一个交互式环境时,一个常见的需求是使用鼠标拖动对象。这种功能不是 Three.js 默认支持的。在这个食谱中,我们将向你展示实现此功能所需的步骤。

准备工作

对于这个食谱,我们只使用标准 Three.js 库中可用的功能。我们将使用THREE.Raycaster对象与THREE.Projector一起实现拖放功能。要查看拖放功能的效果,你可以在浏览器中打开07.08-drag-n-drop-object-around-scene.html示例,你将看到以下截图所示的内容:

准备工作

在这个例子中,你可以看到大量的立方体,你可以单独移动它们。只需用鼠标点击一个,并将其拖动到新位置。这个场景也使用了THREE.OrbitControls,所以当你点击白色背景时,你可以使用鼠标旋转场景。

如何操作…

对于这个食谱,我们需要进行相当多的步骤:

  1. 我们首先做的事情是创建一些全局变量,我们将在以下步骤中访问它们:

      var plane;
      var selectedObject;
      var projector = new THREE.Projector();
      var offset = new THREE.Vector3();
      var objects =[];
    

    我们将在接下来的步骤中解释这些对象是如何使用的。

  2. 当我们想要移动一个对象时,我们需要确定我们将要在哪个平面(围绕哪个轴)移动选定的立方体。鼠标在两个维度上移动,而我们的场景在三个维度上移动。为此,我们将使用一个不可见的辅助平面,我们定义如下:

      plane = new THREE.Mesh( new THREE.PlaneGeometry( 2000, 2000, 18, 18 ), new THREE.MeshBasicMaterial() );
      plane.visible = false;
      scene.add( plane );
    

    这个平面被分配给了我们在步骤 1 中看到的全局平面变量。

  3. 下一步是创建所有立方体。为了更容易理解这个食谱,我们列出了创建立方体的代码:

      for (var i = 0 ; i < 200 ; i ++) {
        var cubeGeometry = new THREE.BoxGeometry(2, 2, 2);
        var cubeMaterial = new 
        THREE.MeshLambertMaterial({color: Math.random() * 0xffffff});
        cubeMaterial.transparent = true;
        cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
     objects.push(cube);
        // randomize position, scale and rotation
        scene.add(cube);
      }
    

    最有趣的一行是突出显示的那一行,我们将创建的立方体添加到名为 objects 的全局数组中。只有这个数组中的立方体可以移动。

  4. 现在我们已经处理了基础知识,我们需要告诉 Three.js 当鼠标移动、鼠标按钮被点击或释放时应该做什么。让我们首先看看 onmousemove 函数:

      document.onmousemove = function(e) {
        ...
      };
    

    在我们可以访问鼠标移动的信息之前,我们需要注册一个监听器。如您在代码片段中看到的,我们通过将一个函数分配给 document.onmousemove 属性来完成此操作。在接下来的步骤中,我们将查看此 onmousemove 函数的内容。

  5. onmousemove 函数中,我们做了几件事情。我们首先需要做的第一件事是将鼠标位置转换为 3D 空间中的位置,并为此位置创建 THREE.Raycaster

      // get the mouse position in viewport coordinates
      var mouse_x = ( event.clientX / window.innerWidth ) * 2 - 1;
      var mouse_y = - ( event.clientY / window.innerHeight ) * 2 + 1;
      // get the 3D position and create a raycaster
      var vector = new THREE.Vector3( mouse_x, mouse_y, 0.5 );
      projector.unprojectVector( vector, camera );
      var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() );
    

    在这一点上,我们可以使用 THREE.Raycaster 来选择鼠标位置的物体。

  6. 下一步是,如果我们已经点击了一个对象,就拖动它(有关此操作的更多详细信息,请参阅步骤 7、8 和 9),或者重新定位我们在步骤 2 中创建的平面:

      if (selectedObject) {
        var intersects = raycaster.intersectObject( plane );
        selectedObject.position.copy(intersects[ 0 ] .point.sub( offset ) );
      } else {
        var intersects = raycaster.intersectObjects(objects);
        if ( intersects.length > 0 ) {
          plane.position.copy( intersects[0]
            .object.position );
            plane.lookAt( camera.position );
        }
      }
    

    如果我们已选择一个对象并在其周围拖动,我们根据从鼠标发出的射线与不可见的辅助平面相交的位置设置该对象的位置,使用我们在步骤 9 中计算的偏移量。如果我们没有拖动对象,并且使用我们的射线确定我们与其中一个立方体相交,我们将我们的辅助 plane 对象移动到该对象的位置,并确保平面面向相机 (plane.lookAt(camera.position))。如果选择了对象,它将沿着这个辅助 plane 对象移动。

  7. 接下来,我们需要定义一个函数来处理 onmousedown 事件:

      document.onmousedown = function(event) {
        ...
      };
    
  8. 现在,让我们看看 onmousedown 事件应该填写什么:

      var mouse_x = (event.clientX / window.innerWidth)* 2 - 1;
      var mouse_y = -(event.clientY / window.innerHeight)* 2 + 1;
      var vector = new THREE.Vector3(mouse_x, mouse_y, 0.5);
      projector.unprojectVector(vector, camera);
      var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
      var intersects = raycaster.intersectObjects(objects);
    

    我们再次使用 THREE.Raycaster 来确定一个对象是否与从鼠标位置发出的射线相交。

  9. 现在我们知道了相交点,我们可以使用它们来选择我们感兴趣的对象:

      if (intersects.length > 0) {
        orbit.enabled = false;
        selectedObject = intersects[0].object;
        // and calculate the offset
        var intersects = raycaster.intersectObject(plane);
        offset.copy(intersects[0].point).sub(plane.position);
    }
    

    如您在这段代码片段中看到的,我们首先禁用了 orbit 控制器(因为我们想拖动对象而不是旋转场景)。接下来,我们将第一个相交的对象分配给 selectedObject 变量,我们在步骤 6 中使用它来移动选定的立方体。最后,我们需要确定我们点击的点与平面中心的偏移量。我们需要这个偏移量来在步骤 6 中正确地定位立方体。

  10. 我们需要采取的最后一步是在释放鼠标按钮时启用轨道控制器,并将 selectedObject 属性设置回 null:

      document.onmouseup = function(event) {
        orbit.enabled = true;
        selectedObject = null;
      }
    

如您所见,您需要采取许多步骤来实现这个食谱。您还可以查看来自 07.08-drag-n-drop-object-around-scene.html 的源代码,其中也包含有关为什么需要某些步骤的内置文档。

还有更多……

这个示例基于 Three.js 网站上的示例,您可以在threejs.org/examples/#webgl_interactive_draggablecubes找到。因此,如果您想尝试另一个示例,可以查看该实现。

在这个示例中,我们向您展示了如何移动整个网格。您也可以使用相同的方法来移动单个顶点、面或线。因此,只需稍加努力,您就可以使用这种方法创建一种雕刻工具,可以直接从浏览器中修改几何形状。例如,您可以创建类似这样的东西 stephaneginier.com/sculptgl/

相关内容

  • 在本章中,我们还使用THREE.Raycaster为“添加简单碰撞检测”示例。如果您想将外部文件拖放到 Three.js 场景中,您可以参考第一章中的“从桌面拖动文件到场景”示例,入门

添加物理引擎

在之前的示例中,我们手动添加了动画和检测碰撞到场景中。在这个示例中,我们将向您展示如何使用外部物理引擎添加重力、碰撞检测和其他物理效果到您的场景中。

准备工作

对于这个示例,我们需要使用几个外部库。在您的 HTML 页面顶部,您必须添加以下内容:

  <script src="img/physi.js"></script>

该库包含物理引擎的主要实现。该库本身使用两个额外的库,需要提供。您首先需要确保ammo.js库存储在与physi.js库相同的目录中,并在您的 JavaScript 代码开头添加以下内容:

  Physijs.scripts.worker = "../libs/physijs_worker.js";

这指向一个 Web Worker([www.w3.org/TR/workers/](http://www.w3.org/TR/workers/)),它在一个单独的线程中处理物理计算。当然,这里有一个现成的示例,您可以将其用作参考或进行实验。这个示例的名称为07.09-add-a-physics-engine.html,当在浏览器中打开时,您将看到以下截图所示的内容:

准备工作

在这个示例中,您可以使用添加立方体按钮将立方体添加到场景中。这个立方体将被添加到地面平面之上,然后落下。物理引擎将确定下落的立方体如何与其环境交互。

如何操作…

在这个示例中,我们只设置了一个基本的物理启用场景。有关Physijs库提供的其他功能的详细信息,请参阅此示例的“更多内容…”部分。要创建一个基本场景,您需要执行以下步骤:

  1. 首先要做的是,我们不会创建THREE.Scene,而是创建Physics.Scene

      scene = new Physijs.Scene;
      scene.setGravity(new THREE.Vector3( 0, -30, 0 ));
    

    在这个新创建的场景中,我们还需要设置gravity属性。在这种情况下,我们在y轴上设置了-30的重力,这意味着一个物体向下掉落的场景。

  2. 接下来,让我们创建THREE.GeometryTHREE.MeshLambertMaterial,我们将使用它们来创建立方体:

      var cubeGeometry = new THREE.BoxGeometry(
        4 * Math.random() + 2, 
        4 * Math.random() + 2, 
        4 * Math.random() + 2);
      var cubeMaterial = new THREE.MeshLambertMaterial(
        {
          color: 0xffffff * Math.random()
        }
      );
    

    在这个步骤中对于Physijs没有特殊的事情要做。

  3. 下一步是创建一个网格对象。为了使对象能够与Physijs一起工作,我们需要创建一个Physijs库特定的网格和一个Physijs库特定的材料:

      var box_material = Physijs.createMaterial(
        cubeMaterial, 
        control.friction, 
        control.restitution);
      var cube = new Physijs.BoxMesh(
        cubeGeometry,
        box_material,
        10
      );
      scene.add(cube);
    

    对于材料,我们使用Physijs.createMaterial函数。这个函数包裹我们在第 2 步中创建的材料,并允许我们定义摩擦和恢复属性。摩擦定义了物体的粗糙度,并影响它可以在另一个物体上滑动的距离。restitution对象用于定义物体的弹性。为了创建网格,我们使用Physijs.BoxMesh对象,提供我们刚刚创建的几何形状和材料,并添加对象的重量。Physijs提供了不同形状的网格;有关它们的信息,请参阅本配方中的更多内容…部分。

  4. 我们需要采取的最后一步是更新render循环:

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

    在这里,我们添加了scene.simulate函数。这个函数用于计算所有被包裹在Physijs库特定网格中的对象的新的位置。

通过这些基本步骤,你已经得到了一个完全工作的具有物理功能的 Three.js 场景。在使用这个引擎时需要考虑的一个重要方面是性能会有所下降。对于场景中的每个对象,Physijs都需要计算其下一个位置和旋转。这对于几十个对象来说效果很好,但当你处理数百个由 Physijs 管理的对象时,你会看到性能的严重下降。

它是如何工作的...

我们调用scene.simulate(),这是我们在第 4 步中添加到render循环中的,对于每个渲染的帧。当这个函数被调用时,Physijs将查看它所知道的所有对象,并且它还会查看场景上配置的重力,并将使用这些信息来计算每个对象的新位置和旋转。如果发生对象之间的碰撞,它将使用Physijs材料的frictionrestitution属性以及对象的重量函数来确定该对象和与之碰撞的对象应该如何反应。这将在每个render循环中重复,从而在场景中模拟真实的物理效果。

更多内容…

在这个配方中我们所做的是这个物理引擎可能实现的一小部分。你可以在 Physijs 网站上找到更多信息:chandlerprall.github.io/Physijs/。该网站上的有趣主题包括:

Physijs 使用外部物理库进行所有计算。关于该引擎的更多信息,请查看 ammo.js 网站(github.com/kripken/ammo.js/)。注意,ammo.js 本身是 Bullet 物理引擎的 JavaScript 端口。所以,如果你真的想深入了解细节,你应该查看可以在bulletphysics.org/wordpress/找到的 Bullet 文档。

参见

  • 如果你不想在你的项目中包含完整的物理引擎,你也可以自己模拟物理引擎的部分。如何在场景中添加基本的碰撞检测在添加简单的碰撞检测菜谱中有解释。
posted @ 2025-10-24 10:09  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报