Three.js入门教程

937 阅读22分钟

在本指南中,我将介绍如何设置开发环境,理解核心概念,并从头开始构建一个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.innerWidthwindow.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
);
  1. 视野取决于具体项目,但通常40到80之间的值就足够了。
  2. 宽高比的计算方法是窗口的宽度除以高度。
  3. 我们将近裁剪平面和远裁剪平面分别设置为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中有几种相机控件,包括OrbitControlsDragControlsFlyControls等。

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()方法的情况是启用它的某些属性,比如autoRotateenableDamping

几何、材质和网格

在Three.js中创建对象分为三个阶段。

  1. 创建几何体,充当我们要添加到场景中的对象的骨架。
  2. 创建材质,该材质基本上充当对象的蒙皮或覆盖层。有各种各样的材料可供选择,要仔细选择,因为有些可能需要更多的资源和更苛刻的算法。
  3. 我们将材质应用到几何体上。

注意: 在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);

如您所见,球体被黑色覆盖,因为我们尚未向场景添加光源。这反映了真实的生活,没有光你什么都看不见,对吧?

这同样适用于其他材质,如MeshPhongMaterialMeshPhysicalMaterial;它们需要可见光。

几何变换

对Three.js中的对象几何变换,可以通过修改其属性的值来实现。

Translation【移动】

要更改对象的位置,我们可以直接为其position.xposition.yposition.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.xrotation.yrotation.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.xscale.yscale.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创建的。

将球体和平面的材质更改为MeshPhongMaterialMeshStandardMaterial

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

原文: waelyasmina.net/articles/th…