使用Three.js在浏览器中组合3D场景就像在玩乐高玩具同样。咱们将一些盒子放在一块儿,添加灯光,定义相机,而后Three.js渲染3D图像。javascript
在本教程中,咱们将从盒子中组装一辆简约的汽车,并学习如何在其上绘制纹理。java
DEMOcanvas
Three.js是一个外部库,所以首先咱们须要将其添加到咱们的项目中。我使用NPM将其安装到个人项目中,而后将其导入JavaScript文件的开头。浏览器
import * as THREE from "three"; const scene = new THREE.Scene(); . . .
首先,咱们须要定义场景。场景是一个容器,其中包含咱们要与灯光一块儿显示的全部3D对象。咱们将向该场景添加汽车,但首先让咱们设置灯光,相机和渲染器。app
咱们将向场景添加两个灯光:环境光和定向光。咱们经过设置颜色和强度来定义。dom
颜色定义为十六进制值。在这种状况下,咱们将其设置为白色。强度是介于0和1之间的数字,而且当它们同时发光时,咱们但愿这些值在0.5左右。ide
. . . 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。
如今,让咱们看看如何组成汽车。首先,咱们将建立没有纹理的汽车。这将是一个简约的设计–咱们将只放四个盒子。
首先,咱们建立一对轮子。咱们将定义一个表明左右轮的灰色框。因为咱们从未从下方看到汽车,所以咱们不会注意到,除了拥有一个单独的左右轮外,咱们只有一个大箱子。
咱们将在汽车的前部和后部都须要一对轮子,所以咱们能够建立可重用的功能。
. . . 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和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在浏览器中渲染场景的方式,这就是咱们要建立纹理的方式。让咱们看看它们是如何工做的。
要在画布上绘制,首先咱们须要建立一个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上找到源代码。