6
头图

使用Three.js在浏览器中组合3D场景就像在玩乐高玩具一样。我们将一些盒子放在一起,添加灯光,定义相机,然后Three.js渲染3D图像。

在本教程中,我们将从盒子中组装一辆简约的汽车,并学习如何在其上绘制纹理。

DEMO

如何设置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坐标。其余的将与之平行。

由于光线是平行的,并且它们从很远的地方发出光线,因此此处的精确坐标无关紧要,而是它们的比例无关紧要。

三个位置参数分别是X,Y和Z坐标。默认情况下,Y轴指向上方,并且Y轴的值最高(500),这意味着我们的汽车顶部受到的光照最多。因此它将是最明亮的。

其他两个值定义为沿X和Z轴弯曲的光线量,也就是汽车的前部和侧面将接收的光线量。

如何设置相机

接下来,让我们设置定义我们如何看待该场景的摄像机。

这里有两个选项–透视相机和正交相机。电子游戏主要使用透视相机,但我们将使用正交摄影机以使外观看起来更简洁。

上一篇文章中,我们更详细地讨论了这两个相机之间的区别。因此,在本教程中,我们将仅讨论如何设置正交摄影机。

对于相机,我们需要定义一个视锥。这是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中构建汽车

现在,让我们看看如何组成汽车。首先,我们将创建没有纹理的汽车。这将是一个简约的设计–我们将只放四个盒子。

如何添加盒子

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

我们将在汽车的前部和后部都需要一对轮子,因此我们可以创建可重用的功能。

. . . 
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个单位来创建一个盒子。

然后,我们传递一种将定义网格外观的材质。有不同的材料选择。它们之间的主要区别是它们对光的反应方式。

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

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

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

如何制造汽车的其余部分

然后以类似的方式让我们创建汽车的其余部分。我们定义了createCar返回Group的函数。该组是场景中的另一个容器。它可以容纳Three.js对象。这很方便,因为如果我们要在汽车中四处走动,我们只需在集团内四处走动。

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

我们使用我们的功能生成两对车轮,然后定义汽车的主要部分。然后,我们将机舱的顶部添加为第四网格。这些都是具有不同尺寸和颜色的盒子。

默认情况下,每个几何都将在中间,并且它们的中心将在0,0,0坐标处。

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

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

如何向汽车添加纹理

现在我们有了基本的汽车模型,让我们为机舱添加一些纹理。我们要给窗户涂油漆。我们将为侧面定义纹理,为机舱的正面和背面定义一个纹理。

当我们使用材料设置网格的外观时,设置颜色不是唯一的选择。我们还可以映射纹理。我们可以为每一侧提供相同的纹理,也可以为阵列中的每一侧提供一种材质。

作为纹理,我们可以使用图像。但是相反,我们将使用JavaScript创建纹理。我们将使用HTML Canvas和JavaScript对图像进行编码。

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

Three.js是一个JavaScript库。它使用引擎盖下的WebGL将3D对象渲染为图像,并将最终结果显示在canvas元素中。

另一方面,HTML Canvas是HTML元素,就像div元素或段落标签一样。但是,让它与众不同的是,我们可以使用JavaScript在此元素上绘制形状。

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

如何在HTML画布上绘制

要在画布上绘制,首先我们需要创建一个canvas元素。当我们创建一个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坐标开始的,它不填充画布,仅绘制窗口。

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

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

以类似的方式,我们可以定义侧面纹理。我们再次创建一个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;
}
. . .

这些纹理中的大多数将正确映射,无需进行任何调整。但是,如果我们将汽车转过身,那么我们可以看到窗户以错误的顺序出现在左侧。

固定纹理前后的左右两侧

这是预期的,因为我们在此处也将纹理用于右侧。我们可以为左侧定义一个单独的纹理,也可以镜像右侧。

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

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

包起来

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

您可能认为这太基础了,但是如果您考虑一下,实际上是使用盒子创建了许多外观时尚的手机游戏。或者只是考虑一下Minecraft,看看将盒子放在一起能走多远。

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

最后,我们定义了一个相机来建立我们如何看待该场景,以及一个渲染器,将最终图像渲染到浏览器中。


如果您想使用该代码,可以在CodePen上找到源代码。

WX20210922-091703.png


雾岛听风
11.9k 声望8.6k 粉丝

丰富自己,胜过取悦别人。