threejs 技能特效(译文Three.js教程)
在浏览器中用 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 轴弯曲的程度,即汽车前部和侧面将接收到的光量。
如何设置相机接下来,让我们设置定义我们如何看待这个场景的相机。
这里有两种选择——透视相机和正交相机。电子游戏大多使用透视相机,但我们将使用正交相机以获得更简约的几何外观。
在本文中,我们将只讨论如何设置正交相机。
对于相机,我们需要定义一个视锥体。这是 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
如何添加框首先,我们创建一对轮子。我们将定义一个代表左右轮的灰色框。由于我们从未从下方看到汽车,因此我们不会注意到我们只有一个大盒子,而不是单独的左右轮。
我们将需要在汽车的前部和后部都有一对轮子,这样我们就可以创建一个可重用的功能。
. . .
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 对象。这很方便,因为如果我们想在汽车周围移动,我们可以简单地在 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);
. . .
我们用我们的函数生成两对轮子,然后定义汽车的主要部分。然后我们将添加小屋的顶部作为第四个网格。这些都只是不同尺寸和不同颜色的盒子。
默认情况下,每个几何图形都位于中间,它们的中心位于 0,0,0 坐标处。
首先,我们通过调整它们沿 Y 轴的位置来提升它们。我们将轮子提高了一半的高度——所以它们不是沉到地面的一半,而是躺在地上。然后我们还沿着 X 轴调整碎片以到达它们的最终位置。
我们将这些部分添加到汽车组中,然后将整个组添加到场景中。在渲染图像之前将汽车添加到场景中很重要,否则我们需要在修改场景后再次调用渲染。
如何为汽车添加纹理现在我们有了非常基本的汽车模型,让我们为车厢添加一些纹理。我们要粉刷窗户。我们将为侧面定义一个纹理,并为机舱的前部和后部定义一个纹理。
当我们使用材质设置网格的外观时,设置颜色并不是唯一的选择。我们还可以映射纹理。我们可以为每一面提供相同的纹理,或者我们可以为数组中的每一面提供一种材质。
为纹理,我们可以使用图像。但取而代之的是,我们将使用 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;
}
. . .
大多数这些纹理将被正确映射而无需任何调整。但是如果我们把车掉头,我们可以看到左侧的窗户以错误的顺序出现。
这是预期的,因为我们在这里也使用右侧的纹理。我们可以为左侧定义一个单独的纹理,或者我们可以镜像右侧。
不幸的是,我们不能水平翻转纹理。我们只能垂直翻转纹理。我们可以通过 3 个步骤解决此问题。
首先,我们将纹理旋转 180 度,这等于 PI 的弧度。不过,在转动它之前,我们必须确保纹理围绕其中心旋转。这不是默认设置——我们必须将旋转中心设置为中途。我们在两个轴上都设置了 0.5,这基本上意味着 50%。最后我们将纹理倒置以使其处于正确的位置。
包起来那么我们在这里做了什么?我们创建了一个包含汽车和灯光的场景。我们用简单的盒子制造了这辆车。
你可能觉得这太基础了,但仔细想想,很多外观时尚的手游其实都是用盒子制作的。或者只是想一想 Minecraft,看看你能把盒子放在一起能走多远。
然后我们使用 HTML 画布创建纹理。HTML 画布的功能远比我们在这里使用的要多。我们可以用曲线和弧线绘制不同的形状,但有时我们只需要一个最小的设计。
最后,我们定义了一个摄像机来确定我们如何看待这个场景,以及一个将最终图像渲染到浏览器中的渲染器。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com