跳到主要内容

Three.js应用程序的基本组件

在上一章中,您了解了Three.js的基础知识。 我们看了一些例子,并创建了您的第一个完整的Three.js应用程序。 在本章中,我们将深入了解Three.js,并解释构成Three.js应用程序的基本组件。

通过本章结束时,您将学会使用在每个Three.js应用程序中使用的基本组件, 并能够使用这些标准组件创建简单的场景。 您还应该能够轻松处理使用更高级对象的Three.js应用程序, 因为Three.js在处理简单和高级组件时采用的方法是相同的。

在本章中,我们将涵盖以下主题:

  • 创建场景
  • 几何体和网格的关系
  • 为不同场景使用不同的摄像机

我们将从查看如何创建场景和添加对象开始。

创建场景

在第1章中,您创建了一个THREE.Scene,因此您已经了解了Three.js的一些基础知识。 我们看到,要使场景显示任何内容,我们需要四种不同类型的对象:

  • 摄像机:确定在屏幕上呈现THREE.Scene的哪个部分。
  • 灯光:这些影响材质的显示方式,并在创建阴影效果时使用(在第3章使用Three.js中的光源中详细讨论)。
  • 网格:这些是从摄像机的透视图中呈现的主要对象。这些对象包含组成几何体的顶点和面(例如,球体或立方体),并包含一个定义几何体外观的材质。
  • 渲染器:使用摄像机和场景中的信息在屏幕上绘制(渲染)输出。

THREE.Scene充当包含要呈现的灯光和网格的主容器。 THREE.Scene本身没有太多选项和功能。 THREE.Scene是一种有时也称为场景图的结构。 场景图可以保存图形场景的所有必要信息。 在Three.js中,这意味着THREE.Scene包含进行渲染所需的所有对象。 有趣的是,场景图,顾名思义,不仅仅是对象数组;场景图由树结构中的一组节点组成。 正如我们将在第8章创建和加载高级网格和几何体中看到的, Three.js提供了可以用来创建不同网格或光源组的对象。 您用于此的主要对象是THREE.Group。 顾名思义,此对象允许您将对象组合在一起。 THREE.Group扩展自Three.js中的另一个基类,称为THREE.Object3D, 它提供了一组标准函数,用于添加和修改子元素。 THREE.MeshTHREE.Scene也都扩展自THREE.Object3D, 因此您也可以使用它们来创建嵌套结构。 但是,按照惯例,并且更语义正确,使用THREE.Group来构建场景图。

场景的基本功能

通过查看一个示例,我们可以最好地探索场景的功能。 在本章的源代码中,您可以找到basic-scene.html示例。 我们将使用此示例来解释场景的各种功能和选项。 当在浏览器中打开此示例时, 输出将类似于下一个截图所示的内容(请记住,您可以使用鼠标移动、缩放和平移渲染的场景):

前面的图像看起来像我们在第1章使用Three.js创建您的第一个3D场景中看到的示例。 即使场景看起来相当空旷,它已经包含了一些对象:

  • 我们有THREE.Mesh,表示您可以看到的地板区域
  • 我们使用THREE.PerspectiveCamera确定我们正在查看的内容
  • 我们添加了THREE.AmbientLightTHREE.DirectionalLight来提供照明

此示例的源代码可以在basic-scene.js中找到, 我们可以使用bootstrap/bootstrap.jsbootstrap/floor.jsbootstrap/lighting.js中的代码, 因为这是我们在本书中始终使用的通用场景设置。 所有这些文件中发生的事情都可以简化为以下代码:

// 创建摄像机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 创建渲染器
const renderer = new THREE.WebGLRenderer(
{
antialias: true
}
);
// 创建场景
const scene = new THREE.Scene();
// 创建灯光
scene.add(new THREE.AmbientLight(0x666666));
scene.add(THREE.DirectionalLight(0xaaaaaa));
// 创建地板
const geo = new THREE.BoxBufferGeometry(10, 0.25, 10, 10,
10, 10);
const mat = new THREE.MeshStandardMaterial(
{
color:
0xffffff,
}
);
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);

正如您在上面的代码中所看到的, 我们创建了THREE.WebGLRendererTHREE.PerspectiveCamera,因为我们始终需要这两者。 接下来,我们创建一个THREE.Scene并添加我们想要使用的所有对象。 在这种情况下,我们添加了两个灯光和一个网格。 现在,我们拥有了启动渲染循环所需的所有组件, 正如我们在第1章使用Three.js创建您的第一个3D场景中已经看到的那样。

在更深入地查看THREE.Scene对象之前,我们将首先解释演示中可以执行的操作,之后再查看代码。 在浏览器中打开chapter-2/basic-scene.html示例, 并查看位于屏幕右上角的Controls菜单,如下面的截图所示:

添加和删除对象

通过这些控制,您可以向场景中添加立方体并删除最后添加的立方体。 它还允许您更改场景的背景并为场景中的所有对象设置材质和环境贴图。 我们将探讨这些不同的选项以及如何使用它们来配置THREE.Scene。 我们将首先看看如何向场景中添加和删除THREE.Mesh对象。 以下代码显示了单击addCube按钮时调用的函数:

const addCube = (scene) => {
const color = randomColor();
const pos = randomVector({
xRange: { fromX: -4, toX: 4 },
yRange: { fromY: -3, toY: 3 },
zRange: { fromZ: -4, toZ: 4 },
});
const rotation = randomVector({
xRange: { fromX: 0, toX: Math.PI * 2 },
yRange: { fromY: 0, toY: Math.PI * 2 },
zRange: { fromZ: 0, toZ: Math.PI * 2 },
});

const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const cubeMaterial = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.1,
metalness: 0.9,
});
const cube = new THREE.Mesh(geometry, cubeMaterial);
cube.position.copy(pos);
cube.rotation.setFromVector3(rotation);
cube.castShadow = true;
scene.add(cube);
};

让我们详细了解上述代码:

  • 首先,我们确定了将要添加的立方体的一些随机设置: 随机颜色(通过调用randomColor()辅助函数)、 随机位置和随机旋转。 这两个后者是通过调用randomVector()随机生成的。
  • 接下来,我们创建要添加到场景中的几何体:一个立方体。 我们为此创建了一个新的THREE.BoxGeometry,定义了一个材质(在这个示例中是THREE.MeshStandardMaterial), 并将这两者组合成了THREE.Mesh。我们使用随机变量设置立方体的位置和旋转。
  • 最后,可以通过调用scene.add(cube)将此THREE.Mesh添加到场景中。

在上述代码中引入的新元素是我们还使用name属性为立方体指定了名称。 名称设置为cube-,并附加了当前场景中的对象数(scene.children.length)。 名称对于调试非常有用,但也可以用于直接从场景中访问对象。 如果使用THREE.Scene.getObjectByName(name)函数, 可以直接检索特定对象,并且例如更改其位置而无需使JavaScript对象成为全局变量。

有时您可能希望从THREE.Scene中删除现有对象。 由于THREE.Scene通过children属性公开了其所有子项, 因此我们可以使用以下简单的代码来删除最后添加的子项:

const removeCube = (scene) => {
scene.children.pop();
};

Three.js还为THREE.Scene提供了其他一些有关处理场景的子项的有用函数:

  • add: 我们已经看到了这个函数,它将提供的对象添加到场景中。 如果它先前添加到了不同的THREE.Object3D,则将从该对象中删除。
  • attach: 这类似于add,但如果使用它,将保留对此对象应用的任何旋转或平移。
  • getObjectById: 当您将对象添加到场景时,它会获得一个ID。 第一个是1,第二个是2,以此类推。使用此函数,您可以根据此ID获取子项。
  • getObjectByName: 根据其name属性返回一个对象。 名称是您可以在对象上设置的 – 这与id属性形成对比,后者是由Three.js分配的。
  • remove: 这将从场景中删除此对象。
  • clear: 这将从场景中删除所有子项。

请注意,上述函数实际上是从THREE.Scene扩展的基本对象的函数:THREE.Object3D。 在整本书中,如果我们想要操作场景的子项(或者在稍后将要探讨的THREE.Group中),我们将使用这些函数。

除了添加和删除对象的功能外,THREE.Scene还提供了一些其他设置。我们将首先查看的是添加雾。

添加雾

fog属性允许您向整个场景添加雾效果;物体距离摄像机越远,就越难以看到。如下截图所示: 为了最好地看到添加的雾效果,请使用鼠标放大和缩小,您将看到立方体受到雾的影响。 在Three.js中启用雾效果非常简单。只需在定义场景后添加以下代码行:

scene.fog = new THREE.Fog( 0xffffff, 1, 20 );

在这里,我们定义了白色的雾(0xffffff)。其他两个属性可用于调整雾的外观。 值1设置了near属性,值20设置了far属性。 通过这些属性,您可以确定雾从何处开始以及它变得多么密集的速度。 使用THREE.Fog对象,雾线性增加。 在chapter-02/basic-scene.html示例中, 您可以使用屏幕右侧的菜单修改这些属性,以查看这些设置如何影响屏幕上显示的内容。 Three.js还提供了由THREE.FogExp2提供的另一种雾实现:

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

这次,我们不指定nearfar,而只指定颜色(0xffffff)和雾的密度(0.01)。 通常,最好尝试一下这些属性,以获得您想要的效果。 场景的另一个有趣功能是您可以配置背景。

更改背景

我们已经看到,可以通过设置WebGLRendererclearColor来更改背景颜色, 如下所示:renderer.setClearColor(backgroundColor)。 您还可以使用THREE.Scene对象更改背景。 为此,您有三个选项:

  • 选项1:您可以使用纯色。
  • 选项2:您可以使用纹理,这基本上是一张图像,拉伸以填充整个屏幕(有关纹理的更多信息,请参见第10章,加载和使用纹理)。
  • 选项3:您可以使用环境贴图。这也是一种纹理,但是它完全包围相机,并且在更改相机方向时移动。

请注意,这将设置我们渲染到的HTML画布的背景颜色,而不是HTML页面的背景颜色。 如果要使用透明画布,您需要将渲染器的alpha属性设置为true

new THREE.WebGLRenderer({ alpha: true })

chapter-02/basic-scene.html右侧的菜单中,有一个下拉菜单显示所有这些不同的设置。 如果从backGround下拉菜单中选择Texture选项,您将看到以下内容:

我们将在第10章,加载和使用纹理中更详细地介绍纹理和立方贴图。 但是现在我们将简要查看如何配置这些和场景的 简单背景颜色(此代码的源代码可以在controls/scene-controls.js中找到):

// 移除任何背景,将背景设置为null
scene.background = null;

// 如果您想要简单的颜色,只需将背景设置为颜色
scene.background = new THREE.Color(0x44ff44);

// 使用THREE.TextureLoader可以加载纹理
const textureLoader = new THREE.TextureLoader();
textureLoader.load(
"/assets/textures/wood/abstract-antique-backdrop-164005.jpg",
(loaded) => {
scene.background = loaded;
}
);

// 使用THREE.TextureLoader也可以加载立方贴图
textureLoader.load("/assets/equi.jpeg", (loaded) => {
loaded.mapping = THREE.EquirectangularReflectionMapping;
scene.background = loaded;
});

如前面的代码所示,可以将nullTHREE.ColorTHREE.Texture分配给场景的background属性。 加载纹理或立方贴图是异步完成的,因此我们必须等待THREE.TextureLoader加载图像数据, 然后才能将其分配给background。 在立方贴图的情况下,我们需要额外的步骤,并告诉Three.js我们加载了什么类型的纹理。 当我们深入探讨纹理的工作原理时,我们将在第10章,加载和使用纹理中详细了解。

如果回顾以下代码部分的开头,您将看到我们如何创建添加到场景中的立方体:

const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const cubeMaterial = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.1,
metalness: 0.9,
});
const cube = new THREE.Mesh(geometry, cubeMaterial);

在上述代码中,我们创建了一个几何体并指定了一个材质。 THREE.Scene对象还提供了一种强制场景中的网格使用相同材质的方法。 在接下来的部分中,我们将探讨它是如何工作的。

更新场景中的所有材质

THREE.Scene有两个影响场景中网格材质的属性。 第一个是overrideMaterial属性。 首先,让我们演示一下它是如何工作的。 在chapter-02/basic-scene.html页面上,您可以单击Toggle Override Material按钮。 这将将场景中所有网格的材质更改为THREE.MeshNormalMaterial

如前图所示,现在所有对象(包括地板)都使用相同的材质 - 在本例中为THREE.MeshNormalMaterial。 该材质根据网格各个面到相机的方向(其法向量)着色。 在代码中,通过简单调用scene.overrideMaterial = new THREE.MeshNormalMaterial(); 即可轻松实现此目的。

除了将完整材质应用于场景之外,Three.js还提供了一种将每个网格材质的environmentMap属性设置为相同值的方法。 环境贴图模拟网格所在的环境(例如,室内、户外或洞穴)。 环境贴图可用于在网格上创建反射,使其感觉更真实。

在前面的背景部分中,我们已经了解了如何加载环境贴图。 如果我们希望所有材质都使用环境贴图进行更动态的反射和阴影, 我们可以将加载的环境贴图分配给场景的environment属性:

textureLoader.load("/assets/equi.jpeg", (loaded) => {
loaded.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = loaded;
});

演示上述代码的最佳方法是从chapter-02/basic-scene.html示例中切换Toggle Environment按钮。 如果现在将相机缩放到立方体附近,您会看到它们的面反射了环境的一部分,而不再是单一颜色: 既然我们已经讨论了渲染所有要渲染的对象的基本容器,

接下来,我们将仔细查看您可以添加到场景中的对象(将THREE.MeshTHREE.Geometry和材质结合使用)。

几何体和网格的关系

在迄今为止的每个示例中,您都看到了几何体和网格的使用。 例如,要创建一个球体并将其添加到场景中,我们使用了以下代码:

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

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

几何体的属性和函数

Three.js提供了一套大量的几何体,您可以在其中选择在您的3D场景中使用。 只需添加一个材质,创建一个网格,您就基本完成了。 下面的屏幕截图来自chapter-2/geometries示例, 展示了在Three.js中可用的一些标准几何体:

在第5章《学习使用几何体》和第6章《探索高级几何体》中, 我们将探讨Three.js提供的所有基本和高级几何体。 现在,让我们更详细地了解几何体的实质。

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

  • 一个立方体有八个角。每个角都可以定义为x、y和z坐标。因此,每个立方体在3D空间中有八个点。
  • 一个立方体有六个面,每个角都有一个顶点。在Three.js中,一个面总是由三个构成三角形的顶点组成(具有三条边)。 因此,在立方体的情况下,每个面由两个三角形组成,形成完整的面。 在图2.7中,通过查看红色立方体,可以看到这是什么样子。

当您使用Three.js提供的几何体之一时,您无需自己定义所有的顶点和面。 对于一个立方体,您只需定义宽度、高度和深度。 Three.js使用这些信息创建一个具有八个在正确位置且正确数量面(立方体的情况下为12个,每个面2个三角形)的几何体。 尽管通常使用Three.js提供的几何体或自动生成它们, 但您仍然可以完全手动创建几何体,使用顶点和面,尽管这可能很快变得复杂,正如您在以下代码中所见:

const v = [
[1, 3, 1],
[1, 3, -1],
[1, -1, 1],
[1, -1, -1],
[-1, 3, -1],
[-1, 3, 1],
[-1, -1, -1],
[-1, -1, 1]
];
const faces = new Float32Array([
...v[0], ...v[2], ...v[1],
...v[2], ...v[3], ...v[1],
...v[4], ...v[6], ...v[5],
...v[6], ...v[7], ...v[5],
...v[4], ...v[5], ...v[1],
...v[5], ...v[0], ...v[1],
...v[7], ...v[6], ...v[2],
...v[6], ...v[3], ...v[2],
...v[5], ...v[7], ...v[0],
...v[7], ...v[2], ...v[0],
...v[1], ...v[3], ...v[4],
...v[3], ...v[6], ...v[4]
]);
const bufferGeometry = new THREE.BufferGeometry();
bufferGeometry.setAttribute("position", new THREE.BufferAttribute(faces, 3));
bufferGeometry.computeVertexNormals();

上述代码显示了如何创建一个简单的立方体。 我们在v数组中定义了构成该立方体的点(顶点)。 接下来,我们可以创建这些点的面。 在Three.js中,我们需要在一个大的Float32Array中提供所有的面信息。 正如我们所提到的,一个面由三个顶点组成。 因此,对于每个面,我们需要定义九个值:每个顶点的x、y和z。 由于每个面有三个顶点,因此有九个值。 为了使它看起来更容易阅读,我们使用JavaScript中的...(展开)运算符将每个顶点的各个值添加到数组中。 因此,...v[0], ...v[2], ...v[1]将在数组中产生以下值:1, 3, 1, 1, -1, 1, 1, 3, 1

请注意,必须注意定义面的顶点顺序。 定义它们的顺序确定了Three.js是否认为它是一个面朝向前(朝向相机的面)还是朝向后(背面)。 如果创建面,应使用顺时针顺序定义前向面,如果要创建后向面,则应使用逆时针顺序。

在我们的示例中,我们使用了一些顶点来定义立方体的六个面,每个面使用两个三角形。 在Three.js的先前版本中,您还可以使用四边形而不是三角形。 四边形使用四个顶点而不是三个来定义面。 使用四边形还是三角形更好是3D建模世界中的激烈争论。 基本上,使用四边形在建模过程中通常更容易增强和平滑,而不像三角形那样渲染和游戏引擎中, 使用三角形通常更容易,因为可以使用三角形非常高效地渲染任何形状。

有了这些顶点和面,我们现在可以创建THREE.BufferGeometry的新实例, 并将顶点分配给position属性。 最后一步是在我们创建的几何体上调用computeVertexNormals()函数。 调用此函数时,Three.js确定每个顶点和面的法线向量。 这是Three.js用于根据场景中的各种灯光确定如何着色面的信息(如果使用THREE.MeshNormalMaterial,您可以轻松可视化)。

有了这个几何体,我们现在可以创建一个网格,就像我们之前看到的那样。 我们创建了一个示例,您可以在其中玩转顶点位置,还显示了各个面。 在我们的chapter-2/custom-geometry示例中, 您可以更改立方体的所有顶点的位置,看看面的反应。 这在以下屏幕截图中显示:

这个例子使用与我们其他所有示例相同的设置,有一个渲染循环。 每当更改下拉控制框中的一个属性时,根据一个顶点的位置的更改来渲染立方体。 这并不是直接可以工作的事情。 出于性能原因,Three.js假设一个网格的几何体在其生命周期内不会改变。 对于大多数几何体和用例,这是一个非常有效的假设。 然而,如果您更改了底层数组(在这种情况下,是const faces = new Float32Array([...])数组), 我们需要告诉Three.js发生了变化。 您可以通过将相关属性的needsUpdate属性设置为true来实现这一点。这将类似于以下内容:

mesh.geometry.attributes.position.needsUpdate = true;
mesh.geometry.computeVertexNormals();

请注意,在更新顶点的情况下,重新计算法线向量也是一个好主意,以确保材质也能正确渲染。 有关法线向量是什么以及为什么它很重要的更多信息将在第10章《加载和使用纹理》中解释。

chapter-2/custom-geometry菜单中有一个我们尚未讨论的按钮。 在右侧的菜单中,有一个克隆按钮。 我们提到几何体定义了对象的形状和形状,并与材质结合在一起,我们创建了一个可以添加到场景中由Three.js渲染的对象。 使用clone()函数,正如其名称所示,我们可以复制几何体, 并且可以使用它来创建一个具有不同材质的不同网格。 在相同的示例chapter-2/custom-geometry中,您可以在控制GUI的顶部看到一个克隆按钮, 如下面的屏幕截图所示:

如果单击此按钮,将克隆(复制)几何体的当前状态; 然后,将创建一个带有不同材质的新对象,并最终将该对象添加到场景中。 其代码如下:

const cloneGeometry = (scene) => {
const clonedGeometry = bufferGeometry.clone();
const backingArray = clonedGeometry.getAttribute("position").array;
// 更改x顶点的位置,使其放置在原始对象的旁边
for (const i in backingArray) {
if ((i + 1) % 3 === 0) {
backingArray[i] = backingArray[i] + 3;
}
}
clonedGeometry.getAttribute("position").needsUpdate = true;
const cloned = meshFromGeometry(clonedGeometry);
cloned.name = "clonedGeometry";
const p = scene.getObjectByName("clonedGeometry");
if (p) scene.remove(p);
scene.add(cloned);
};

如上述代码所示,我们使用clone()函数克隆bufferGeometry。 一旦克隆,我们确保更新每个顶点的x值, 以便克隆放置在与原始对象不同的位置(我们也可以使用本章后面解释的translateX)。 接下来,我们创建一个THREE.Mesh,如果存在克隆的网格,则删除克隆的网格,并添加新的克隆。 要创建新的网格,我们使用一个名为meshFromGeometry的自定义函数。

作为一个快速的旁注,让我们看一下它是如何实现的:

const meshFromGeometry = (geometry) => {
var materials = [
new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }),
new THREE.MeshLambertMaterial({ opacity: 0.1, color: 0xff0044, transparent: true }),
];
var mesh = createMultiMaterialObject(geometry, materials);
mesh.name = "customGeometry";
mesh.children.forEach(function (e) {
e.castShadow = true;
});
return mesh;
};

如果您回顾一下这个例子,您会看到一个透明的立方体和构成我们几何体的线(边缘)。 为了做到这一点,我们创建了一个多材质网格。 这意味着我们告诉Three.js在一个单一的网格中使用两种不同的材质。 为此,Three.js提供了一个很好的辅助函数,称为createMultiMaterialObject, 它的功能与名称相符。基于几何体和材质列表,它创建一个我们可以添加到场景中的对象。 然而,在使用createMultiMaterialObject调用的结果时,您需要知道的一件事是, 您得到的不是一个单一的网格;它是一个THREE.Group, 在这种情况下,它包含我们提供的每种材质的单独的THREE.Mesh。 因此,在渲染网格时,它看起来像一个单一的对象,但实际上它包含在彼此之上渲染的多个THREE.Mesh对象。 这也意味着如果我们想要阴影,我们需要为组内的每个网格启用它(这是我们在上面的代码片段中所做的)。

在上述代码中,我们使用了THREE.SceneUtils对象的createMultiMaterialObject来向我们创建的几何体添加线框。 Three.js还提供了一种使用THREE.WireframeGeometry添加线框的替代方法。 假设您有一个名为geom的几何体, 您可以从中创建一个线框几何体:const wireframe = new THREE.WireframeGeometry(geom);。 接下来,您可以使用Three.LineSegments对象绘制该几何体的线, 首先创建一个const line = new THREE.LineSegments(wireframe)对象, 然后将其添加到场景中:scene.add(line)。 由于这个辅助对象在内部实际上只是一个THREE.Line对象,因此您可以设置线框的外观方式。 例如,要设置线框线条的宽度,可以使用line.material.linewidth = 2;

我们已经略微了解了THREE.Mesh对象。在接下来的部分中,我们将更深入地探讨您可以使用它做什么。

网格的函数和属性

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

  • position:确定对象相对于其父对象位置的属性。 大多数情况下,对象的父对象是THREE.Scene对象或THREE.Group对象。
  • rotation:通过此属性,您可以设置对象围绕其任何轴的旋转。 Three.js还提供围绕单个轴旋转的特定函数:rotateX()rotateY()rotateZ()
  • scale:此属性允许您沿其x、y和z轴缩放对象。
  • translateX() / translateY()translateZ():此属性沿相应轴移动对象一定量。
  • lookAt():此属性将对象指向空间中的特定矢量。这是手动设置旋转的替代方法。
  • visible:此属性确定是否应渲染此网格。
  • castShadow:此属性确定网格是否在被光照射到时投射阴影。默认情况下,网格不投射阴影。

在旋转对象时,我们是围绕一个轴旋转的。 在3D场景中,有多个具有可以围绕其旋转的轴的空间。 rotateN() 函数在本地空间中围绕轴旋转对象。这意味着对象围绕其父对象的轴旋转。 因此,当您将对象添加到场景时,rotateN() 函数将围绕场景的主轴旋转该对象。

如果它是嵌套组的一部分,这些函数将使对象围绕其父对象的轴旋转,这通常是您要寻找的行为。 Three.js还具有一个特定的 rotateOnWorldAxis, 它允许您围绕主THREE.Scene的轴旋转对象,而不考虑对象的实际父对象。 最后,您还可以通过调用 rotateOnAxis 函数强制对象围绕其自身轴(称为对象空间)旋转。

与往常一样,我们为您准备了一个示例,可以让您玩弄这些属性。 如果在浏览器中打开 chapter-2/mesh-properties, 您将获得一个下拉菜单,您可以在其中更改所有这些属性并直接看到结果, 如下图所示:

让我带您了解这些属性;我将从 position 属性开始。

使用 position 属性设置网格的位置

我们已经几次见过此属性,所以让我们迅速解决它。 使用此属性,您设置对象相对于其父对象的x、y和z坐标。 我们将在第5章学习使用几何体中回顾这一点,当我们查看对象分组时。 我们可以通过三种不同的方式设置对象的 position 属性。 我们可以直接设置每个坐标:

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

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

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

还有第三种选择position 属性是一个 THREE.Vector3 对象。 这意味着我们还可以执行以下操作来设置此对象:

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

接下来是我们列表上的下一个属性:rotation。 您已经在这里和第1章使用Three.js创建您的第一个3D场中看到了此属性的使用。

使用 rotation 属性定义网格的旋转

通过此属性,您设置对象围绕其轴之一的旋转。 您可以像我们设置 position 一样设置此值。 完整的旋转,您可能记得来自数学课的,是2π。 您可以通过Three.js以几种不同的方式配置此值:

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

如果要使用度数(从0360),我们必须将其转换为弧度。 这可以很容易地完成,如下所示:

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

在上述代码块中,我们自己进行了转换。 Three.js还提供了 MathUtils 类,它提供了许多有用的转换,包括与我们在上述代码块中所做的相同的转换。 您可以使用 chapter-2/mesh-properties 示例玩弄此属性。

我们列表上的下一个属性是我们尚未讨论的一个属性:scale。 名称基本上总结了您可以使用此属性执行的操作。 您可以沿特定轴缩放对象。如果将比1小的值设置为缩放,则对象将缩小, 如下所示:

当您使用大于1的值时,对象将变大,如下所示:

我们将查看网格的下一部分是 translate 属性。

使用 translate 属性更改位置

使用 translate,您还可以更改对象的位置,但与定义您希望对象位于的绝对位置不同, 您定义了对象相对于其当前位置应该移动的距离。 例如,我们有一个添加到场景中的球体,其位置已设置为(1, 2, 3)。 接下来,我们沿其x轴平移对象:translateX(4)。其位置现在将是(5, 2, 3)。 如果我们想将对象恢复到其原始位置,我们使用 translateX(-4)。 在 chapter-2/mesh-properties 示例中,有一个名为 translate 的菜单选项卡。 从那里,您可以尝试此功能。只需为x、y和z设置平移值,然后点击平移按钮。 您将看到对象根据这三个值被移动到新位置。 我们将查看的最后两个属性用于通过将 visible 属性设置为 false 来完全移除对象, 并通过将 castShadow 属性设置为 false 来禁用此对象是否投射阴影。 当您点击这些按钮时,您将看到立方体变得不可见和可见,并且您可以禁用它是否投射阴影。

有关网格、几何体以及您可以对这些对象执行的操作的更多信息,请查阅第5章学习使用几何体和第7章点和精灵

到目前为止,我们已经看过 THREE.Scene,这是保存我们想要渲染的所有对象的主要对象, 并且我们已经详细查看了 THREE.Mesh 是什么,以及如何创建 THREE.Mesh 并将其定位在场景中。 在前面的部分中,我们已经使用相机来确定您想要渲染的 THREE.Scene 的哪个部分,但尚未详细解释如何配置相机。

在下一节中,我们将深入探讨这些细节。

使用不同的相机进行不同场景

Three.js 中有两种不同的相机类型:正交相机和透视相机。 请注意,Three.js 还提供了一些非常特定的相机,用于创建可以使用 3D 眼镜或 VR 设备查看的场景。 在本书中,我们不会详细介绍这些相机,因为它们与本章中解释的相机完全相同。 如果您对这些相机感兴趣,Three.js 提供了一些标准示例:

如果您正在寻找简单的 VR 相机,您可以使用 THREE.StereoCamera 创建渲染为并排的 3D 场景(标准立体效果), 使用平行屏障(如 3DS 提供的)或提供同色效果,其中不同的视图以不同的颜色渲染。 此外,Three.js 对 WebVR 标准提供了一些实验性支持, 该标准得到多个浏览器的支持(有关更多信息, 请参见 https://webvr.info/developers/)。 要使用此功能,不需要太多更改。 只需设置 renderer.vr.enabled = true,Three.js 将处理其余。 Three.js 网站上有一些演示此属性以及 Three.js 对 WebVR 支持的其他功能的示例:https://threejs.org/examples/

现在,我们将专注于标准透视正交相机。解释这些相机之间的区别最好的方法是通过查看一些示例。

正交相机与透视相机

在本章的示例中,您可以找到一个名为 chapter2/cameras 的演示。

打开此示例后,您将看到类似以下的内容:

这个截图称为透视视图,是最自然的视图。

从这个图中可以看出,离摄像机越远的立方体,渲染得越小。

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

使用正交相机时,所有的立方体都以相同的大小渲染;对象和相机之间的距离无关紧要。

这在 2D 游戏中经常使用,例如 Civilization 的旧版本和 SimCity 4:

透视相机属性

首先让我们更仔细地看一看 THREE.PerspectiveCamera。在示例中,您可以设置一些属性,这些属性定义了通过相机镜头所看到的内容:

  • fov:视场(FOV)是从相机位置可以看到的场景的部分。例如,人类的 FOV 几乎为 180 度,而某些鸟类甚至具有完整的 360 度 FOV。但由于普通计算机屏幕不能完全填充我们的视野,通常选择较小的值。通常,对于游戏,FOV 选择在 60 到 90 度之间。良好的默认值:50
  • aspect:这是我们正在渲染输出的区域的水平和垂直大小之间的纵横比。在我们的情况下,由于我们使用整个窗口,我们只是使用该比率。纵横比确定了水平 FOV 和垂直 FOV 之间的差异。良好的默认值:window.innerWidth / window.innerHeight
  • nearnear 属性定义了 Three.js 应该渲染场景的靠近相机的距离。通常,我们将其设置为非常小的值,以直接从相机位置渲染所有内容。良好的默认值:0.1
  • farfar 属性定义了相机从相机位置可以看到的距离。如果我们将其设置得太低,我们的场景的某些部分可能不会被渲染,如果我们将其设置得太高,在某些情况下可能会影响渲染性能。良好的默认值:100
  • zoomzoom 属性允许您在场景中放大和缩小。当使用小于 1 的数字时,您会缩小场景,如果使用大于 1 的数字,您会放大。请注意,如果指定负值,场景将倒置渲染。良好的默认值:1

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

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

正交相机属性

要配置正交相机,我们需要使用其他属性。 正交投影不关心使用哪个纵横比或者场景的视场是多少,因为所有的对象都以相同的大小渲染。 当您定义一个正交相机时,您定义了需要渲染的长方体区域。 正交相机的属性反映了这一点,如下所示:

  • left:在 Three.js 文档中,这被描述为相机视锥左平面。 您可以将其视为将要渲染的内容的左边界。 如果将此值设置为 -100,则看不到位于左侧距离超过该值的任何对象。
  • rightright 属性的工作方式类似于 left 属性,但这次在屏幕的另一侧。距离右侧更远的任何内容都不会被渲染。
  • top:这是要渲染的顶部位置。
  • bottom:这是要渲染的底部位置。
  • near:从此点开始,基于相机的位置,场景将被渲染。
  • far:到这一点,基于相机的位置,场景将被渲染。
  • zoom:这允许您在场景中放大和缩小。当使用小于 1 的数字时,您将缩小场景;如果使用大于 1 的数字,您将放大。请注意,如果指定负值,场景将倒置渲染。默认值为 1。

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

与透视相机一样,您可以精确地定义要渲染的场景区域:

在前面的部分中,我们解释了Three.js支持的不同相机。 您已经学会了如何配置它们,以及如何使用它们的属性来渲染场景的不同部分。 但我们尚未展示如何控制相机看向场景的哪个部分,我们将在下一部分中解释。

查看特定点

到目前为止,您已经了解了如何创建相机以及各种参数的含义。 在第1章中,使用Three.js创建您的第一个3D场景, 您还看到您需要将相机定位在场景的某个位置,并从该相机的视图中进行渲染。 通常,相机是指向场景中心的:位置 (0, 0, 0)。然而,我们可以轻松地改变相机的观察方向, 如下所示:

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

chapter2/cameras示例中,您还可以指定您希望相机观察的坐标。 请注意,当您在OrthographicCamera设置中更改lookAt时,立方体仍然保持相同的大小。 使用lookAt函数时,您将相机对准特定位置。您还可以使用此功能使相机跟随场景中的对象移动。 由于每个THREE.Mesh对象都有一个位置,该位置是一个THREE.Vector3对象, 因此您可以使用lookAt函数指向场景中的特定网格。 您只需要使用这个:camera.lookAt(mesh.position)。 如果您在渲染循环中调用此函数,您将使相机在对象通过场景时跟随该对象。

调试相机的观察目标

在配置相机时,拥有一个可以调整不同设置的菜单会很有帮助。 然而,有时您可能想要精确地查看相机将渲染的区域。 Three.js允许您通过可视化相机的截锥体(相机显示的区域)来实现这一点。 为此,我们只需在场景中添加一个额外的相机并添加一个相机助手。 要查看这个过程,请打开chapter-2/debug-camera.html示例:

在上图中,您可以看到透视相机截锥体的轮廓。 如果您在菜单中更改属性,您会看到截锥体也在变化。 通过以下方式可视化这个截锥体:

const helper = new THREE.CameraHelper(camera);
scene.add(helper);
// 在渲染循环中
helper.update();

我们还添加了一个switchCamera按钮,允许您在外部相机和场景内的主相机之间切换。 这提供了一个获取相机正确设置的好方法: 在Three.js中切换相机非常容易。 您唯一需要做的事情就是告诉Three.js您想通过不同的相机渲染场景。

总结

在这第二个入门章节中,我们讨论了很多内容。 我们展示了THREE.Scene的函数和属性,并解释了如何使用这些属性配置主场景。 我们还向您展示了如何创建几何体。 您可以使用THREE.Buffergeometry对象从头开始创建它们,或者使用Three.js提供的任何内置几何体。 最后,我们向您展示了如何配置Three.js提供的两个主要相机。 THREE.PerspectiveCamera使用真实世界的透视渲染场景, 而THREE.OrthographicCamera提供了游戏中经常看到的虚拟3D效果。 我们还涵盖了Three.js中几何体的工作原理, 您现在可以轻松地从Three.js提供的标准几何体中创建自己的几何体, 或者通过手工制作它们。

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