在本指南中,我将介绍如何设置开发环境,理解核心概念,并从头开始构建一个Three.js应用程序。无论您是初学者还是扩展Web开发技能,本教程将为您提供3D Web图形的基础知识。
设置项目
开始,首先创建一个文件夹,使用Visual Studio Code打开它。然后,在导航菜单中选择New Terminal。
打开终端后,输入以下命令:npm init -y。此命令将生成一个package.json文件。
在本教程中,我们将使用Vite进行配置。要安装它,请运行以下命令:npm install vite --save-dev。
然后,创建一个index.html。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My 1st Three.js App!</title>
</head>
<body>
<script src="/main.js" type="module"></script>
</body>
</html>
接下来,创建名为main.js的文件。
现在我们的项目文件已经准备好了,让我们通过输入以下命令npm install three安装Three.js。
完成后,输入这个命令npx vite,对代码进行任何更新时自动刷新。
到目前为止,您的项目目录应该如下所示:
Three.js应用程序的基本组成部分
坐标系
首先,当我们讨论3D概念时,我们指的是由三个轴组成的坐标系:X,Y和Z。
第一个轴被称为x轴,它表示图上一个点的水平位置。
第二个轴被称为y轴,它表示图上一个点的垂直位置。
第三个轴称为z轴,代表深度。
场景【Scene】
假设我们想录制一个节目;显然,我们需要在核实的地方录制,也就是现场。我们还需要一个相机。然后,需要在场景外添加组件。我所说的组件是指灯光、对象和演员。
这些完全相同的事情使Three.js来完成。首先,我们通过实例化Scene类来创建一个场景。然后,我们选择合适的相机并创建它的实例。一旦这两个基本部分准备就绪,我们就可以向场景中引入元素。
相机【Cameras】
Three.js中有几种类型的相机,但作为初学者,你只需要关注两种。
第一种是透视相机【perspective camera】 ,其功能类似于现实生活中的相机,适用于大多数项目。
要在Three.js中创建一个透视相机,我们需要4个值。
视野【Field of view】 :它是通过摄像机的透镜所能看到的最大角度。
宽高比:这一个表示由相机捕获的图像的宽度和高度的比例。它通常通过画布宽度除以画布高度来计算。
近/远剪裁平面【Near and far clipping planes】 :这两个表示可以看到的范围。场景中任何位置比近剪裁平面更靠近摄影机的物体都不会被渲染。除此之外,任何远于远剪裁平面的东西都不会被渲染。
第二种类型的摄影机是正交摄影机【orthographic camera】 。
正交摄影机用于在2D透视图中渲染3D场景,在这种情况下不考虑景深。
正交摄影机由6个值定义。
- 左、右、上,下:这些是相机从场景中拍摄出来的图像的边缘。
- 近剪裁平面和远剪裁平面
下面是一个示例,可以更好地理解透视和正交摄影机之间的区别。
使用透视摄影机时,对象的大小会根据它们与摄影机的距离而变化。物体在离相机越近时看起来越大,而在离相机越远时看起来越小。
另一方面,对于正交摄影机,无论对象与摄影机的距离如何,对象的大小都保持一致。
创建基本场景
在main.js中,将所有Three.js功能导入其中。
import * as THREE from 'three';
接下来,我们将创建WebGLRenderer的一个实例。在这个阶段,可以将渲染器视为Three.js在网页上分配空间的工具,我们可以在其中显示和操作3D内容。
要设置渲染空间的大小,我们需要调用setSize()方法。在本例中,我使用window.innerWidth和window.innerHeight属性来使空间覆盖整个页面。
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
接下来, 我们需要做的是将我们刚刚创建的空间注入到页面中:
document.body.appendChild(renderer.domElement);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My 1st Three.js App!</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<script src="/main.js" type="module"></script>
</body>
</html>
现在是把我们的理论知识付诸实践的时候了。让我们从创建场景开始。
const scene = new THREE.Scene();
接下来,我们需要实例化PerspectiveCamera类来添加一个相机。它的构造函数有四个参数。
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
- 视野取决于具体项目,但通常40到80之间的值就足够了。
- 宽高比的计算方法是窗口的宽度除以高度。
- 我们将近裁剪平面和远裁剪平面分别设置为0.1和1000。请务必注意,这两个值之间的范围会对应用的性能产生负面影响。较小的范围通常会带来更好的性能,而较大的范围可能会降低性能。
完成后,我们使用渲染器的render()方法将场景链接到相机。
确保此指令位于代码的底部。
renderer.render(scene, camera);
执行最后一步将导致画布变为黑色,表明代码运行正常。
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// The body of the app
renderer.render(scene, camera);
辅助对象
辅助对象是一种工具,充当向导。在本节中,我们将介绍几个辅助对象,稍后我们将使用。
AxesHelper
顾名思义,此辅助对象可视化3D显示坐标。
// 5 here represents the length of the axes.
const axesHelper = new THREE.AxesHelper(5);
现在我们已经准备好了AxesHelper,我们使用add()方法将其添加到场景中。
scene.add(axesHelper);
如果您没有看到任何新内容,请不要担心;相机最初定位在场景的原点(坐标为(0,0,0)的点)。要查看辅助对象,我们需要移动相机。
camera.position.z = 5;
辅助对象出现的时候,显示的不是三轴,而是两轴。这是因为相机被定位沿着z轴。为了显示第三个轴,我们需要调整相机的x或y值。
camera.position.z = 5;
camera.position.y = 2;
现在我们已经调整了相机的位置,我们现在可以清楚地看到所有三个轴。
顺便说一下,我们可以通过调用set()方法。第一个参数指向x轴,第二个参数指向y轴,第三个参数指向z轴。
camera.position.set(0, 2, 5);
GridHelper
在Three.js中,GridHelper是一种在3D场景中创建网格的辅助对象。此网格为可视化空间、对齐提供参考。
const gridHelper = new THREE.GridHelper();
scene.add(gridHelper);
上面代码生成一个 10*10网格。为了改变它的大小,我们可以将所需的大小值传递给构造函数。
const gridHelper = new THREE.GridHelper(15);
如果需要,我们还可以向构造函数传递第二个参数,以将网格划分为更小的正方形。
const gridHelper = new THREE.GridHelper(15, 50);
动画循环
让我们在场景中添加一个盒子。
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({color: 0x00FF00});
const box = new THREE.Mesh(geometry, material);
scene.add(box);
现在我们有了盒子,我们可以对它进行几何变换,例如旋转。在本例中,让我们在x轴和y轴上执行5弧度旋转。
box.rotation.x = 5;
box.rotation.y = 5;
动画由一系列随时间发生的变换组成。
我们将创建一个名为animate的函数来封装代码。
此外,我们需要在每次变换后重新渲染。我们将通过调用render()来实现这一点。
function animate() {
box.rotation.x += 0.01;
box.rotation.y += 0.01;
renderer.render(scene, camera);
}
完成后,我们需要将animate()作为参数传递给setAnimationLoop()。
// This will create a loop
// that causes the renderer to draw the scene
// every time the screen is refreshed.
// On a typical screen this means 60 times per second
renderer.setAnimationLoop(animate);
请记住,我们有一个时间输入,以更好地控制动画的速度。这个时间参数表示动画循环开始后经过的时间,由Three.js自动提供,作为动画循环机制的一部分。
function animate(time) {
box.rotation.x = time / 1000;
box.rotation.y = time / 1000;
renderer.render(scene, camera);
}
现阶段的完整代码:
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
camera.position.set(0, 2, 5);
const gridHelper = new THREE.GridHelper(15, 50);
scene.add(gridHelper);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({color: 0x00FF00});
const box = new THREE.Mesh(geometry, material);
scene.add(box);
function animate(time) {
box.rotation.x = time / 1000;
box.rotation.y = time / 1000;
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
摄像机控制
在Three.js中,相机控件使用户能够交互式控制相机在3D场景中的位置。它们允许用户从不同的视角探索场景。
Three.js中有几种相机控件,包括OrbitControls,DragControls,FlyControls等。
FirstPersonControls
FirstPersonControls模拟第一人称飞行体验,允许用户使用键盘输入或鼠标移动来控制相机的位置和方向。这种控制方案通常用于飞行模拟或探索应用。
要使用FirstPersonControls,我们需要先导入它,因为它不是Three.js的核心部分。
import {FirstPersonControls} from
'three/examples/jsm/controls/FirstPersonControls.js';
然后我们实例化FirstPersonControls类,向其构造函数传递两个参数:摄像机和渲染器的画布。
// Ensure this line comes after the instantiation of the camera.
const fPControls =
new FirstPersonControls(camera, renderer.domElement);
在本例中,我们将介绍时钟实用程序。
const clock = new THREE.Clock();
function animate(time) {
fPControls.update(clock.getDelta());
box.rotation.x = time / 1000;
box.rotation.y = time / 1000;
renderer.render(scene, camera);
}
update()方法更新控件,clock.getDelta()返回帧之间经过的时间。
有了这个,通过按住鼠标左键和右键,能够在场景中飞来飞去,类似于第一人称射击游戏。
轨道控制【OrbitControls】
此控制方案允许用户围绕场景中的目标点进行轨道移动。通过拖动鼠标,用户可以围绕目标旋转相机,放大和缩小,以及水平和垂直平移。
所以我现在要你做的是删除与FirstPersonControls相关的代码,导入OrbitControls类,与FirstPersonControls相同的方式创建它的实例,因为它们的构造函数方法具有相同的参数。
import * as THREE from 'three';
import {OrbitControls} from
'three/examples/jsm/controls/OrbitControls';
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const orbitControls =
new OrbitControls(camera, renderer.domElement);
camera.position.set(0, 2, 5);
orbitControls.update();
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
const gridHelper = new THREE.GridHelper(15, 50);
scene.add(gridHelper);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({color: 0x00FF00});
const box = new THREE.Mesh(geometry, material);
scene.add(box);
function animate(time) {
box.rotation.x = time / 1000;
box.rotation.y = time / 1000;
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
调用update()方法对于OrbitControls来说是不必要的,除非你需要复位相机。
另一种必须调用update()方法的情况是启用它的某些属性,比如autoRotate和enableDamping。
几何、材质和网格
在Three.js中创建对象分为三个阶段。
- 创建几何体,充当我们要添加到场景中的对象的骨架。
- 创建材质,该材质基本上充当对象的蒙皮或覆盖层。有各种各样的材料可供选择,要仔细选择,因为有些可能需要更多的资源和更苛刻的算法。
- 我们将材质应用到几何体上。
注意: 在3D领域中,对象通常被称为网格【mesh】。这些可以是各种形状,如立方体,圆形或使用3D软件创建的字符。
几何体【Geometries】
在下面的代码块中,第一行代码,我们创建BoxGeometry类的一个实例,它定义了一个盒子的骨架。
第二行代码。我们创建了一个MeshBasicMaterial的实例,它不需要在场景中显示灯光。正如你所看到的,我们有一个配置对象,它包含一个color属性,它代表了材质的颜色。
第三行代码,该阶段涉及合并几何体和材质。
第四行代码,将网格添加到场景中。
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({color: 0x00FF00});
const box = new THREE.Mesh(geometry, material);
scene.add(box);
Three.js有更多的几何图形,我们不仅可以创建盒子,还可以创建其它。
例如,让我们创建一个平面。我们可以创建PlaneGeometry类的实例。然后,我们创建一个材质,将其与几何体合并为网格,最后将其添加到场景中。
const planeGeometry = new THREE.PlaneGeometry(15, 15);
const planeMaterial = new THREE.MeshBasicMaterial();
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
scene.add(planeMesh);
// This rotates the plane to match the grid.
planeMesh.rotation.x = -0.5 * Math.PI;
从底部观察平面会使其消失,因为只渲染其面。要改变这一点,我们需要将side属性添加到材质中,并将其值设置为THREE.DoubleSide。
const planeMaterial = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide
});
创建一个球体,我们将创建一个SphereGeometry类的实例,并将半径作为参数传递。
const sphereGeometry = new THREE.SphereGeometry(2);
const sphereMaterial = new THREE.MeshBasicMaterial({
color: 0x0000FF
});
const sphereMesh =
new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphereMesh);
我们已向场景中添加了一个球体,但仔细观察后,您会发现它并不是完美的圆形。
我们可以在线框形式中显示球体的形状。这涉及到将另一个属性添加到称为wireframe的材质,并将其值设置为true。
const sphereMaterial = new THREE.MeshBasicMaterial({
color: 0x0000FF,
wireframe: true
});
正如你所看到的,球体是由线、点和三角形构成。此外,网格的质量取决于从这些图元创建的面的数量。
现在,让我们尝试减少这个球体面数量。
// The default values for the horizontal segments is 32
// and 16 for the vertical segments
// THREE.SphereGeometry(2) is same as
// THREE.SphereGeometry(2, 32, 16)
const sphereGeometry = new THREE.SphereGeometry(2, 10, 10);
球体应该看起来不那么圆了:
另一方面,如果我们尝试增加球面的数量,球体看起来会很完美。
const sphereGeometry = new THREE.SphereGeometry(2, 50, 50);
材料【Materials】
如前所述,我们为网格使用了MeshBasicMaterial,它允许在场景中不需要光源的情况下出现。现在,让我们尝试使用另一种材质并观察球体的外观。
const sphereGeometry = new THREE.SphereGeometry(2, 50, 50);
// Using the MeshStandardMaterial now
const sphereMaterial = new THREE.MeshStandardMaterial({
color: 0x0000FF,
//wireframe: true
});
const sphereMesh =
new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphereMesh);
如您所见,球体被黑色覆盖,因为我们尚未向场景添加光源。这反映了真实的生活,没有光你什么都看不见,对吧?
这同样适用于其他材质,如MeshPhongMaterial和MeshPhysicalMaterial;它们需要可见光。
几何变换
对Three.js中的对象几何变换,可以通过修改其属性的值来实现。
Translation【移动】
要更改对象的位置,我们可以直接为其position.x、position.y或position.z属性设置值。或者,我们可以使用position.set()方法或pixelteX()、pixelteY()或pixelteZ()方法。
这将沿x轴以沿着5个单位放置对象。
box.position.x = 5;
将对象沿x轴设置为5个单位,沿Y和Z轴设置为0。
box.position.set(5, 0, 0);
长方体沿x轴平移5个单位。如果长方体位于位置0,则会将其x位置更新为5。
box.translateX(5);
Rotation【旋转】
要旋转对象,我们可以直接为rotation.x、rotation.y或rotation.z属性设置值。或者,我们可以使用rotation.set()或rotateX()、rotateY()或rotateZ()方法来指定旋转角度。
沿z轴的旋转-π / 3
box.rotation.z = -Math.PI / 3;
沿y轴的旋转,沿X和Z轴的旋转设置为0。
box.rotation.set(0, Math.PI, 0);
沿x轴的弧度旋转
box.rotateX(Math.PI / 2);
Scale【缩放】
要放大或缩小对象,我们可以直接为其scale.x、scale.y或scale.z属性设置值。或者,我们可以使用scale.set()方法。
沿x轴将长方体放大4倍。
box.scale.x = 4;
沿着y轴将对象放大2倍,并沿着Y和Z轴保留初始比例。
box.scale.set(1, 2, 1);
使用lil-gui输入数据
键入以下命令:npm install lil-gui安装lil-gui。
import { GUI } from 'lil-gui';
// You can place this anywhere in your code
// except within the animate() function of course.
const gui = new GUI();
调色板
现在,我们想通过在画布上添加调色板来更改球体的颜色。
const options = {
color: 0x0000FF
}
调用onChange()方法并定义一个回调函数。改变球体的颜色。
gui.addColor(options, 'color').onChange(function(e) {
sphereMesh.material.color.set(e);
});
color.set(e)这里是用于改变球体颜色的方法,其中变量e是从调色板获得的颜色代码。
如果看到黑色球体,是因为使用的材质不是MeshBasicMaterial。请记住,我们尚未向场景中添加任何灯光。因此,使用材质(如MeshStandardMaterial)将导致球体显示为黑色,而不管从调色板中选择的颜色如何。
import * as THREE from 'three';
import {OrbitControls} from
'three/examples/jsm/controls/OrbitControls';
import { GUI } from 'lil-gui';
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const orbitControls =
new OrbitControls(camera, renderer.domElement);
camera.position.set(0, 2, 5);
orbitControls.update();
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
const gridHelper = new THREE.GridHelper(15, 50);
scene.add(gridHelper);
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({color: 0x00FF00});
const box = new THREE.Mesh(geometry, material);
scene.add(box);
const planeGeometry = new THREE.PlaneGeometry(15, 15);
const planeMaterial = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
scene.add(planeMesh);
planeMesh.rotation.x = -0.5 * Math.PI;
const sphereGeometry = new THREE.SphereGeometry(2, 50, 50);
const sphereMaterial = new THREE.MeshBasicMaterial({
color: 0x0000FF
});
const sphereMesh =
new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphereMesh);
const gui = new GUI();
const options = {
color: 0x0000FF
}
gui.addColor(options, 'color').onChange(function(e) {
sphereMesh.material.color.set(e);
});
function animate(time) {
box.rotation.x = time / 1000;
box.rotation.y = time / 1000;
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
复选框
GUI提供了不同类型的元素。我们可以添加一个复选框,在线框模式和常规模式之间切换网格。
为此,让我们向选项对象添加一个属性。
const options = {
color: 0x0000FF,
wireframe: false
}
这一次,我们使用add()方法而不是addColor(),传递options对象和属性键作为参数。
滑块
假设我们想让球体反弹,我们需要有一个滑块来控制反弹速度。
首先,让我们将speed属性添加到options对象。
const options = {
color: 0x0000FF,
wireframe: false,
speed: 0.01
}
我们需要调用add()方法。我们将添加几个额外的参数,表示速度范围的最小值和最大值。
gui.add(options, 'speed', 0, 0.1);
接下来,我将创建一个新变量step:
gui.add(options, 'wireframe').onChange(function(e) {
sphereMesh.material.wireframe = e;
});
gui.add(options, 'speed', 0, 0.1);
let step = 0;
function animate(time) {
box.rotation.x = time / 1000;
box.rotation.y = time / 1000;
step += options.speed;
sphereMesh.position.y = 3 * Math.abs(Math.sin(step));
renderer.render(scene, camera);
}
光
Three.js中有各种类型的灯光。在本教程中,我们将探索其中的三个。
环境光【Ambient Light】
环境光是环境中存在的整体照明,间接来自其他光源。一个日常的例子是房间里的日光。
要添加环境光,我们只需创建AmbientLight类的实例。
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1);
scene.add(ambientLight);
当观察结果时,您可能会注意到没有任何变化。这是因为网格的材质是使用不受灯光影响的MeshBasicMaterial创建的。
将球体和平面的材质更改为MeshPhongMaterial或MeshStandardMaterial。
const sphereMaterial = new THREE.MeshPhongMaterial({
color: 0x0000FF
});
const planeMaterial = new THREE.MeshStandardMaterial({
side: THREE.DoubleSide
});
定向光
定向光的一个常见示例是太阳光。它来自一个遥远的光源,以至于它有效地用平行光线覆盖了所有空间。
要添加平行光,我们创建DirectionalLight类的实例,并在构造函数中指定灯光的颜色和强度。
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 10);
scene.add(directionalLight);
要添加一个平行光辅助对象,我们创建一个DirectionalLightHelper类的实例:
const dLightHelper =
new THREE.DirectionalLightHelper(directionalLight);
scene.add(dLightHelper);
现在,像场景中的任何其他元素一样。我们可以调整光源的位置以改变其方向,从而影响阴影的方向:
directionalLight.position.set(-5, 8, 0);
我们可以给构造函数设置第二个参数,来改变辅助方块的大小。
const dLightHelper =
new THREE.DirectionalLightHelper(directionalLight, 3);
聚光灯【Spotlight】
该光源以圆锥体的形式发射光。光源离接收光的表面越远,圆锥半径就变得越大。
要创建一个聚光灯,我们需要创建一个Spotlight类的实例,并设置灯光的颜色和强度。
// Make sure you comment out the directional light code.
const spotlight = new THREE.SpotLight(0xFFFFFF, 1000);
scene.add(spotlight);
spotlight.position.set(-5, 8, 0);
在场景中引入聚光灯辅助对象,我们需要创建SpotLightHelper类的实例。
const sLightHelper = new THREE.SpotLightHelper(spotlight);
scene.add(sLightHelper);
此处的白色线段表示聚光灯创建的光锥的方向和边界。
这种类型的光提供了一系列的属性,使我们能够创建各种效果。
const options = {
color: 0x0000FF,
wireframe: false,
speed: 0.01,
intensity: 0,
angle: 0.2,
penumbra: 0
}
gui.add(options, 'intensity', 0, 1000).onChange(function(e) {
spotlight.intensity = e;
});
gui.add(options, 'angle', 0, 2).onChange(function(e) {
spotlight.angle = e;
});
gui.add(options, 'penumbra', 0, 1).onChange(function(e) {
spotlight.penumbra = e;
});
每当我们实时更改灯光属性的值时,需要通过调用update()。
function animate(time) {
//...
sLightHelper.update();
//...
}
penumbra本质上是指聚光灯边缘的逐渐模糊。
阴影
默认情况下,Three.js中的阴影不启用。因此,要启用它们,我们需要将渲染器的shadowMap.enabled属性的值设置为true。
renderer.shadowMap.enabled = true;
然后我们需要手动设置对象是否接收或投射阴影。
planeMesh.receiveShadow = true;
另一方面,球体通过位于光源前面而投射形成阴影。要使其成为阴影投射器,我们需要将其castShadow属性设置为true。
sphereMesh.castShadow = true;
灯光也会投射阴影。
spotlight.castShadow = true;
现在你应该能看到影子了。但是如果你的设置和我的一样,你会注意到阴影出现像素化。但是,通过调整聚光灯的角度,您将观察到较窄的角度会产生较好的阴影,而较宽的角度会产生较模糊的阴影。
您可能面临的另一个问题是使用定向光时丢失部分阴影。为了更好地理解我在说什么,注释掉或删除聚光灯的代码并添加定向光。
import * as THREE from 'three';
import {OrbitControls} from
'three/examples/jsm/controls/OrbitControls';
import { GUI } from 'lil-gui';
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const orbitControls =
new OrbitControls(camera, renderer.domElement);
camera.position.set(0, 2, 5);
orbitControls.update();
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({color: 0x00FF00});
const box = new THREE.Mesh(geometry, material);
scene.add(box);
const planeGeometry = new THREE.PlaneGeometry(15, 15);
const planeMaterial = new THREE.MeshStandardMaterial({
side: THREE.DoubleSide
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
scene.add(planeMesh);
planeMesh.rotation.x = -0.5 * Math.PI;
const sphereGeometry = new THREE.SphereGeometry(2, 50, 50);
const sphereMaterial = new THREE.MeshPhongMaterial({
color: 0x0000FF
});
const sphereMesh =
new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphereMesh);
const gui = new GUI();
const options = {
color: 0x0000FF,
wireframe: false,
speed: 0.01
}
gui.addColor(options, 'color').onChange(function(e) {
sphereMesh.material.color.set(e);
});
gui.add(options, 'wireframe').onChange(function(e) {
sphereMesh.material.wireframe = e;
});
gui.add(options, 'speed', 0, 0.1);
let step = 0;
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1);
scene.add(ambientLight);
const directionalLight =
new THREE.DirectionalLight(0xFFFFFF, 10);
scene.add(directionalLight);
directionalLight.position.set(-5, 8, 0);
sphereMesh.position.x = 5;
const dLightHelper =
new THREE.DirectionalLightHelper(directionalLight, 3);
scene.add(dLightHelper);
renderer.shadowMap.enabled = true;
planeMesh.receiveShadow = true;
sphereMesh.castShadow = true;
directionalLight.castShadow = true;
function animate() {
step += options.speed;
sphereMesh.position.y = 3 * Math.abs(Math.sin(step));
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
球体的阴影只有一半是可见的,这是由于相机的配置。
每种类型的灯光都有一个用于其阴影的相机。例如directionalLight使用正交照相机。
此外,为了可视化阴影的摄像机标记的区域,我们可以使用另一个辅助对象。
const dLightShadowHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(dLightShadowHelper);
如您所见,现在有四个橙色线段从摄影机延伸到平面。
为了扩大渲染阴影的表面积,我们必须将这些段彼此分开。具体来说,我们需要通过移动阴影摄像机的顶部来调整位置。
directionalLight.shadow.camera.top = 7;
雾和背景颜色
雾
我们可以使用两种不同的方法在Three.js中为场景添加雾。
第一个方法涉及创建Fog类的实例,该实例在其构造函数方法中需要三个参数。第一个参数指定颜色,而另外两个参数定义雾可见的空间的远近。
下面的代码根据对象到摄影机的距离增加雾的密度。在距离相机0个单位的距离处,没有雾。当距离超过50个单位时,场景变得完全模糊。
scene.fog = new THREE.Fog(0x5500AF, 0, 50);
创建雾的第二种方法是实例化FogExp2类,该类的构造函数有两个参数:颜色和密度。
scene.fog = new THREE.FogExp2(0x5500AF, 0.03);
使用此方法,雾的密度会随着与摄影机的距离的增加而呈指数级增加。
背景颜色
到目前为止,我们一直使用黑色背景,但我们可以通过使用renderer中的setClearColor()方法来更改它。
renderer.setClearColor(0x00EA00);
纹理
从本质上讲,纹理是应用于3D对象的表面,为它们提供颜色,图案或细节。
纹理还用于更高级的目的,例如置换贴图、Alpha贴图、粗糙度贴图等,以增强3D场景中的真实感。
在项目目录中,创建一个新文件夹并将其命名为 public。这是您放置静态文件(如图像和3D文件)的地方。
纹理作为场景背景
创建TextureLoader类的实例。这将为我们加载图像并将其转换为Texture对象。
const textureLoader = new THREE.TextureLoader();
接下来,我们调用load()方法并提供图像的路径。
const backgrounImage = textureLoader.load('/stars.jpg');
scene.background = backgrounImage;
完成这些步骤后,纹理将出现。但您可能会注意到它看起来有点褪色。
该问题与颜色空间有关,这超出了本教程的范围。有关详细信息,请参阅本文。
要解决这个问题,只需添加这行代码:
backgrounImage.colorSpace = THREE.SRGBColorSpace;
背景已经改变,但它看起来是二维的。然而,这可以改变。因为场景本质上是一个立方体,有六个面,每个面都有自己的背景。
为了实现这一点,我们需要使用另一种类型的加载器,即CubeTextureLoader。此加载器的load方法接受图像路径,其中每条路径对应于立方体特定面。
图像必须有1:1的比例,这意味着每个图像应该有相同的高度和宽度。
const cubeTextureLoader = new THREE.CubeTextureLoader();
const cubeTexture = cubeTextureLoader.load([
'/cubeTexture.jpg',
'/cubeTexture.jpg',
'/cubeTexture.jpg',
'/cubeTexture.jpg',
'/cubeTexture.jpg',
'/cubeTexture.jpg'
]);
scene.background = cubeTexture;
将纹理应用于网格
要将纹理映射到网格上,我们首先需要使用TextureLoader加载纹理,设置其颜色空间。然后,将其分配给map属性。
// I'm using the same TextureLoader instance
// that I used to load the scene's background image.
const boxTexture =
textureLoader.load('/cubeTexture1.jpg');
boxTexture.colorSpace = THREE.SRGBColorSpace;
const boxGeo = new THREE.BoxGeometry(2, 2, 2);
const boxMat = new THREE.MeshBasicMaterial({map: boxTexture});
const boxMesh = new THREE.Mesh(boxGeo, boxMat);
scene.add(boxMesh);
boxMesh.position.set(-5, 2, 0);
除此之外,我们可以通过网格材质的map属性来更改纹理。
const boxTexture2 =
textureLoader.load('/cubeTexture2.jpg');
boxTexture2.colorSpace = THREE.SRGBColorSpace;
boxMesh.material.map = boxTexture2;
盒子的所有面都是一样的。但是,如果希望每个面具有不同的纹理,则需要为每个面创建单独的材质。
// boxTexture and boxTexture2 are already loaded
const boxTexture3 =
textureLoader.load('/cubeTexture3.jpg');
boxTexture3.colorSpace = THREE.SRGBColorSpace;
const boxTexture4 =
textureLoader.load('/cubeTexture4.jpg');
boxTexture4.colorSpace = THREE.SRGBColorSpace;
const boxTexture5 =
textureLoader.load('/cubeTexture5.jpg');
boxTexture5.colorSpace = THREE.SRGBColorSpace;
const boxTexture6 =
textureLoader.load('/cubeTexture6.jpg');
boxTexture6.colorSpace = THREE.SRGBColorSpace;
const box2MultiMaterials = [
new THREE.MeshBasicMaterial({map: boxTexture}),
new THREE.MeshBasicMaterial({map: boxTexture2}),
new THREE.MeshBasicMaterial({map: boxTexture3}),
new THREE.MeshBasicMaterial({map: boxTexture4}),
new THREE.MeshBasicMaterial({map: boxTexture5}),
new THREE.MeshBasicMaterial({map: boxTexture6})
];
const box2Mesh = new THREE.Mesh(boxGeo, box2MultiMaterials);
scene.add(box2Mesh);
box2Mesh.position.set(0, 2, 0);
让画布响应
目前,画布没有响应。如果您尝试调整窗口,您会注意到画布大小保持不变。
为了解决这个问题,我们可以添加一个事件侦听器来检测窗口。在回调函数中,我们更新相机的aspect属性,以调整窗口的长宽比。
此外,每次修改任何相机属性时都必须调用updateProjectionMatrix() 方法。
// At the end of the code.
window.addEventListener('resize', function() {
camera.aspect =
this.window.innerWidth / this.window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(
this.window.innerWidth,
this.window.innerHeight
);
});