Three.js 教程 – 如何用 3D 纹理构建简单的汽车

翻译自:Hunor Márton Borbély

在浏览器中用 Three.js 拼凑一个 3D 场景就像在玩乐高积木一样。我们将一些盒子放在一起,添加灯光,定义相机,Three.js 渲染 3D 图像。

在本教程中,我们将用盒子组装一辆简约的汽车,并学习如何将纹理映射到它上面。

首先,我们将进行设置——我们将定义灯光、相机和渲染器。然后我们将学习如何定义几何和材料来创建 3D 对象。最后,我们将使用 JavaScript 和 HTML Canvas 对纹理进行编码。

如何设置 Three.js 项目

Three.js 是一个外部库,所以首先我们需要将它添加到我们的项目中。我使用 NPM 将它安装到我的项目中,然后在 JavaScript 文件的开头导入它。

import * as THREE from "three"; 

const scene = new THREE.Scene();

. . .

首先,我们需要定义场景。场景是一个容器,其中包含我们想要与灯光一起显示的所有 3D 对象。我们即将在这个场景中添加一辆汽车,但首先让我们设置灯光、相机和渲染器。

如何设置灯光

我们将在场景中添加两盏灯:一个环境光和一个定向光。我们通过设置颜色和强度来定义两者。

颜色定义为十六进制值。在这种情况下,我们将其设置为白色。强度是一个介于 0 和 1 之间的数字,由于它们同时发光,我们希望这些值在 0.5 左右。

. . . 

const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(200, 500, 300);
scene.add(directionalLight); 

. . .

环境光从各个方向照射,为我们的几何体提供基本颜色,而定向光模拟太阳。

定向光从很远的地方发出平行光线。我们为此灯设置了一个位置,该位置定义了这些光线的方向。

这个位置可能有点混乱,所以让我解释一下。在所有平行光线中,我们特别定义了一条。这个特定的光线将从我们定义的位置 (200,500,300) 照射到 0,0,0 坐标。其余的将与之并行。

image.png

由于光线是平行的,并且它们从很远的地方发光,所以精确的坐标在这里并不重要——相反,它们的比例很重要。

三个位置参数是 X、Y 和 Z 坐标。默认情况下,Y 轴指向上方,因为它具有最高值 (500),这意味着我们的汽车顶部接收到的光线最多。所以它会是最亮的。

其他两个值定义了光线沿 X 轴和 Z 轴弯曲的程度,即汽车前部和侧面将接收到的光量。

如何设置相机

接下来,让我们设置定义我们如何看待这个场景的相机。

这里有两种选择——透视相机和正交相机。电子游戏大多使用透视相机,但我们将使用正交相机以获得更简约的几何外观。

image.png 在本文中,我们将只讨论如何设置正交相机。

image.png 对于相机,我们需要定义一个视锥体。这是 3D 空间中将被投影到屏幕上的区域。

在正交相机的情况下,这是一个盒子。相机将这个盒子内的 3D 对象投射到它的一侧。因为每条投影线都是平行的,所以正交相机不会扭曲几何形状。

. . .

// Setting up camera
const aspectRatio = window.innerWidth / window.innerHeight;
const cameraWidth = 150;
const cameraHeight = cameraWidth / aspectRatio;

const camera = new THREE.OrthographicCamera(
  cameraWidth / -2, // left
  cameraWidth / 2, // right
  cameraHeight / 2, // top
  cameraHeight / -2, // bottom
  0, // near plane
  1000 // far plane
);
camera.position.set(200, 200, 200);
camera.lookAt(0, 10, 0);

. . .

要设置正交相机,我们必须定义平截头体的每一侧与视点的距离。我们定义左侧距左侧 75 个单位,右侧平面距右侧 75 个单位,依此类推。

这里这些单位不代表屏幕像素。渲染图像的大小将在渲染器中定义。在这里,这些值具有我们在 3D 空间中使用的任意单位。稍后,当在 3D 空间中定义 3D 对象时,我们将使用相同的单位来设置它们的大小和位置。

一旦我们定义了一个相机,我们还需要定位它并朝一个方向转动。我们将相机在每个维度上移动 200 个单位,然后我们将其设置为向后看 0,10,0 坐标。这几乎是原点。我们看向略高于地面的一点,我们的汽车的中心将在那里。

如何设置渲染器

我们需要设置的最后一块是渲染器,它根据我们的相机将场景渲染到浏览器中。我们像这样定义一个 WebGLRenderer:

. . .

// Set up renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.render(scene, camera);

document.body.appendChild(renderer.domElement);

这里我们还设置了画布的大小。这是我们以像素为单位设置大小的唯一地方,因为我们正在设置它在浏览器中的显示方式。如果我们想填满整个浏览器窗口,我们传递窗口的大小。

最后,最后一行将这个渲染的图像添加到我们的 HTML 文档中。它创建一个 HTML Canvas 元素来显示渲染的图像并将其添加到 DOM。

如何在 Three.js 中构建汽车

现在让我们看看我们怎样才能组成一辆汽车。首先,我们将创建一个没有纹理的汽车。这将是一个简约的设计——我们只需将四个盒子放在一起。

image.png

如何添加框

首先,我们创建一对轮子。我们将定义一个代表左右轮的灰色框。由于我们从未从下方看到汽车,因此我们不会注意到我们只有一个大盒子,而不是单独的左右轮。

image.png 我们将需要在汽车的前部和后部都有一对轮子,这样我们就可以创建一个可重用的功能。

. . . 

function createWheels() {
  const geometry = new THREE.BoxBufferGeometry(12, 12, 33);
  const material = new THREE.MeshLambertMaterial({ color: 0x333333 });
  const wheel = new THREE.Mesh(geometry, material);
  return wheel;
}

. . .

我们将轮子定义为网格。网格是几何和材质的组合,它将代表我们的 3D 对象。

几何定义对象的形状。在这种情况下,我们通过将其沿 X、Y 和 Z 轴的尺寸设置为 12、12 和 33 个单位来创建一个框。

然后我们传递将定义我们的网格外观的材料。有不同的材料选择。它们之间的主要区别在于它们对光的反应。

image.png 在本教程中,我们将使用MeshLambertMaterial. 计算MeshLambertMaterial每个顶点的颜色。在绘制一个盒子的情况下,基本上是每一面。

我们可以看到它是如何工作的,因为盒子的每一面都有不同的阴影。我们将定向光定义为主要从上方发光,因此盒子的顶部是最亮的。

一些其他材料计算颜色,不仅针对每一面,而且针对该面内的每个像素。它们会为更复杂的形状生成更逼真的图像。但是对于用定向光照明的盒子,它们并没有太大的区别。

如何建造汽车的其余部分

然后以类似的方式让我们创建汽车的其余部分。我们定义了createCar返回 Group 的函数。这个组是另一个像场景一样的容器。它可以容纳 Three.js 对象。这很方便,因为如果我们想在汽车周围移动,我们可以简单地在 Group 周围移动。

. . .

function createCar() {
  const car = new THREE.Group();
  
  const backWheel = createWheels();
  backWheel.position.y = 6;
  backWheel.position.x = -18;
  car.add(backWheel);
  
  const frontWheel = createWheels();
  frontWheel.position.y = 6;  
  frontWheel.position.x = 18;
  car.add(frontWheel);

  const main = new THREE.Mesh(
    new THREE.BoxBufferGeometry(60, 15, 30),
    new THREE.MeshLambertMaterial({ color: 0x78b14b })
  );
  main.position.y = 12;
  car.add(main);

  const cabin = new THREE.Mesh(
    new THREE.BoxBufferGeometry(33, 12, 24),
    new THREE.MeshLambertMaterial({ color: 0xffffff })
  );
  cabin.position.x = -6;
  cabin.position.y = 25.5;
  car.add(cabin);

  return car;
}

const car = createCar();
scene.add(car);

renderer.render(scene, camera);

. . .

我们用我们的函数生成两对轮子,然后定义汽车的主要部分。然后我们将添加小屋的顶部作为第四个网格。这些都只是不同尺寸和不同颜色的盒子。

image.png 默认情况下,每个几何图形都位于中间,它们的中心位于 0,0,0 坐标处。

首先,我们通过调整它们沿 Y 轴的位置来提升它们。我们将轮子提高了一半的高度——所以它们不是沉到地面的一半,而是躺在地上。然后我们还沿着 X 轴调整碎片以到达它们的最终位置。

我们将这些部分添加到汽车组中,然后将整个组添加到场景中。在渲染图像之前将汽车添加到场景中很重要,否则我们需要在修改场景后再次调用渲染。

如何为汽车添加纹理

现在我们有了非常基本的汽车模型,让我们为车厢添加一些纹理。我们要粉刷窗户。我们将为侧面定义一个纹理,并为机舱的前部和后部定义一个纹理。

image.png 当我们使用材质设置网格的外观时,设置颜色并不是唯一的选择。我们还可以映射纹理。我们可以为每一面提供相同的纹理,或者我们可以为数组中的每一面提供一种材质。

image.png 为纹理,我们可以使用图像。但取而代之的是,我们将使用 JavaScript 创建纹理。我们将使用 HTML Canvas 和 JavaScript 对图像进行编码。

在继续之前,我们需要对 Three.js 和 HTML Canvas 做一些区分。

Three.js 是一个 JavaScript 库。它在后台使用 WebGL 将 3D 对象渲染为图像,并将最终结果显示在画布元素中。

另一方面,HTML Canvas 是一个 HTML 元素,就像div元素或段落标签一样。不过,它的特别之处在于我们可以使用 JavaScript 在这个元素上绘制形状。

这就是 Three.js 在浏览器中渲染场景的方式,也是我们要创建纹理的方式。让我们看看它们是如何工作的。

如何在 HTML 画布上绘图

要在画布上绘图,首先我们需要创建一个画布元素。当我们创建一个 HTML 元素时,这个元素永远不会成为我们 HTML 结构的一部分。它本身不会显示在页面上。相反,我们将把它变成 Three.js 纹理。

让我们看看如何在这个画布上绘图。首先,我们定义画布的宽度和高度。这里的大小并没有定义画布会出现多大,它更像是画布的分辨率。纹理将被拉伸到盒子的一侧,不管它的大小。

function getCarFrontTexture() {
  const canvas = document.createElement("canvas");
  canvas.width = 64;
  canvas.height = 32;
  const context = canvas.getContext("2d");

  context.fillStyle = "#ffffff";
  context.fillRect(0, 0, 64, 32);

  context.fillStyle = "#666666";
  context.fillRect(8, 8, 48, 24);

  return new THREE.CanvasTexture(canvas);
}

然后我们得到 2D 绘图上下文。我们可以使用这个上下文来执行绘图命令。

首先,我们要用一个白色矩形填充整个画布。为此,首先我们将填充样式设置为 while。然后通过设置矩形的左上角位置和大小来填充矩形。在画布上绘图时,默认情况下 0,0 坐标将位于左上角。

然后我们用灰色填充另一个矩形。这个从 8,8 坐标开始,它不填充画布,它只绘制窗口。

就是这样——最后一行将画布元素转换为纹理并将其返回,因此我们可以将它用于我们的汽车。

function getCarSideTexture() {
  const canvas = document.createElement("canvas");
  canvas.width = 128;
  canvas.height = 32;
  const context = canvas.getContext("2d");

  context.fillStyle = "#ffffff";
  context.fillRect(0, 0, 128, 32);

  context.fillStyle = "#666666";
  context.fillRect(10, 8, 38, 24);
  context.fillRect(58, 8, 60, 24);

  return new THREE.CanvasTexture(canvas);
}

以类似的方式,我们可以定义侧面纹理。我们再次创建一个画布元素,获取它的上下文,然后首先填充整个画布以具有基色,然后将窗口绘制为矩形。

如何将纹理映射到盒子

现在让我们看看如何将这些纹理用于我们的汽车。当我们为舱室顶部定义网格时,我们不是只设置一种材质,而是为每一侧设置一种材质。我们定义了一个包含六种材料的数组。我们将纹理映射到机舱的侧面,而顶部和底部仍将具有纯色。

. . .

function createCar() {
  const car = new THREE.Group();

  const backWheel = createWheels();
  backWheel.position.y = 6;
  backWheel.position.x = -18;
  car.add(backWheel);

  const frontWheel = createWheels();
  frontWheel.position.y = 6;
  frontWheel.position.x = 18;
  car.add(frontWheel);

  const main = new THREE.Mesh(
    new THREE.BoxBufferGeometry(60, 15, 30),
    new THREE.MeshLambertMaterial({ color: 0xa52523 })
  );
  main.position.y = 12;
  car.add(main);

  const carFrontTexture = getCarFrontTexture();

  const carBackTexture = getCarFrontTexture();

  const carRightSideTexture = getCarSideTexture();

  const carLeftSideTexture = getCarSideTexture();
  carLeftSideTexture.center = new THREE.Vector2(0.5, 0.5);
  carLeftSideTexture.rotation = Math.PI;
  carLeftSideTexture.flipY = false;

  const cabin = new THREE.Mesh(new THREE.BoxBufferGeometry(33, 12, 24), [
    new THREE.MeshLambertMaterial({ map: carFrontTexture }),
    new THREE.MeshLambertMaterial({ map: carBackTexture }),
    new THREE.MeshLambertMaterial({ color: 0xffffff }), // top
    new THREE.MeshLambertMaterial({ color: 0xffffff }), // bottom
    new THREE.MeshLambertMaterial({ map: carRightSideTexture }),
    new THREE.MeshLambertMaterial({ map: carLeftSideTexture }),
  ]);
  cabin.position.x = -6;
  cabin.position.y = 25.5;
  car.add(cabin);

  return car;
}

. . .

大多数这些纹理将被正确映射而无需任何调整。但是如果我们把车掉头,我们可以看到左侧的窗户以错误的顺序出现。

image.png 这是预期的,因为我们在这里也使用右侧的纹理。我们可以为左侧定义一个单独的纹理,或者我们可以镜像右侧。

不幸的是,我们不能水平翻转纹理。我们只能垂直翻转纹理。我们可以通过 3 个步骤解决此问题。

首先,我们将纹理旋转 180 度,这等于 PI 的弧度。不过,在转动它之前,我们必须确保纹理围绕其中心旋转。这不是默认设置——我们必须将旋转中心设置为中途。我们在两个轴上都设置了 0.5,这基本上意味着 50%。最后我们将纹理倒置以使其处于正确的位置。

包起来

那么我们在这里做了什么?我们创建了一个包含汽车和灯光的场景。我们用简单的盒子制造了这辆车。

你可能觉得这太基础了,但仔细想想,很多外观时尚的手游其实都是用盒子制作的。或者只是想一想 Minecraft,看看你能把盒子放在一起能走多远。

然后我们使用 HTML 画布创建纹理。HTML 画布的功能远比我们在这里使用的要多。我们可以用曲线和弧线绘制不同的形状,但有时我们只需要一个最小的设计。

image.png 最后,我们定义了一个摄像机来确定我们如何看待这个场景,以及一个将最终图像渲染到浏览器中的渲染器。

你可以在CodePen上找到源代码

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片