Three.js

初学Three.js(一)

使用Three.js创建第一个三维场景

Yixuan Lang
2022-06-01
7 min

# 初学 Three.js-使用 Three.js 创建第一个三维场景

之前在网络上看到了一个很优秀的Web3D Developer——Bruno Simon 关于Three.js的课程非常的感兴趣,但是因为自己的懒惰学了几课时便没有继续下去。国内关于 Three.js 的课程和资料并不是很全面大多也比较老旧。这次下定决心要去结合一些中文资料及书籍把Bruno Simon的课程啃下来。这个系列的文章会记录一些学习知识的总结~~~

学习的最好方式就是不断的实践,尤其是对于Three.js这种比较抽象的课程,更应该结合大量的案例来学习。这里不多赘述如何引入Three.js,可在 github 中查看。

👉 Three.js Github

👉 Three.js 官方文档

👉 还可以使用 threejs-webpack-starter快速搭建一个基于 webpack 的 three.js 开发环境

# 通过案例来感受一下 Three.js 的魅力

结合案例,才能够更加清晰的学习Three.js,下面会来讲解如何一步一步的完成下面这个效果 👇

pic01

上面的效果使用了以下的 Three.js 模块,后续会结合案例一步步进行讲解

  • 场景 Sence:场景是一个容器,主要用于保存、跟踪所要渲染的物体和使用的光源等。如果没有THREE.Sence对象,那么 Three.js 就无法渲染任何物体
  • 几何体 Geometry:Three.js 提供了多种几何体,其使用方式也非常简单,例如要创建一个长宽高都为 100 的立方体通过代码const box = new THREE.BoxGeometry(100, 100, 100)即可创建。
  • 材质 Material:材质就相当于几何体的衣服,threejs 提供了很多常用的材质效果,这些效果本质上都是对 WebGL 着色器的封装。
  • 光源 Light:没有光源。渲染场景将不可见(除非使用基础材质或者线框材质)。Three.js 中包含大量的光源,每一个光源都有特别的用法。
  • 相机 Camera:摄像机决定着你最后所能看到的输出效果,不同的摄像角度,摄像机类型都会产出不同的效果,这就像你生活中拍照人是同一个人,但是你拍照的位置角度不同,显示的效果肯定不同。这也是后续的一个重难点。
  • 渲染器 Renderer:该对象会给予摄像机的角度来计算场景对象在浏览器重会渲染成什么样子,最后WebGLRenderer将会使用电脑显卡来渲染场景。

# 👉 创建一个 3D 场景

要使用Three.js创建一个三维场景,首先需要定义三大基本结构场景(Sence)——相机(Camera)——渲染器(Renderer)

// Scene 场景
const scene = new THREE.Scene();
// Camera 摄像机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  100
);
// Renderer 渲染器
const canvas = document.querySelector("canvas.webgl"); // 需要渲染整个3D场景要挂载的元素
const renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight); // 设置场景的大小

# 👉 在场景中添加物体和光源

现在我们已经创建了空白的场景、渲染器和摄像机,但是还没有渲染任何东西,接下来会在场景中添加我们所需要渲染的物体,和照亮物体的光源。

# 创建球体

案例中我们在场景中使用THREE.SphereBufferGeometry创建了球形几何体,除此以外通过创建材质Material对象来设置球体的外观。最后将创建的球体和设置的材质合并进Mesh对象中。效果如下 👇

image-20220602113430457

// Geometry 创建球形几何体
const geometry = new THREE.SphereBufferGeometry(0.6, 80, 80);
// SphereGeometry(radius, widthSegments, heightSegments)

// Loading 加载法线贴图
const textureLoader = new THREE.TextureLoader();
const normalTexture = textureLoader.load("/texture/NormalMap.png");

// Materials 添加材质
const material = new THREE.MeshStandardMaterial();
material.color = new THREE.Color(0xff0000);
material.roughness = 0.2;
material.metalness = 1;
material.normalMap = normalTexture;

// Mesh
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);

✨==SphereBufferGeometry(radius, widthSegments, heightSegments)==

第一个参数radius约束的是球体的大小,参数widthSegmentsheightSegments约束的是球面的精度,球体你可以理解为正多面体,就像圆一样是正多边形,当分割的边足够多的时候,正多边形就会无限接近于圆,球体同样的道理。

MeshStandardMaterial:一种基于物理的标准材质,PBR 物理材质,相比较高光 Phong 材质可以更好的模拟金属、玻璃等效果。

除了设置了一些 material 的类似于color颜色roughness粗糙程度metalness金属材质的基本属性以外还设置了 material 的normalMap法线贴图。法线贴图(Normal mapping)是一种模拟凹凸处光照效果的技术,是凸凹贴图的一种实现。法线贴图可以在不添加多边形的前提下,为模型添加细节。

normal map generator:帮助我们生成法线贴图的网站。

# dat.GUI 的使用

在添加光源之前,我们先来了解一个非常有用的工具dat.GUIdat.GUI是一个轻量级的图形用户界面库(GUI 组件),使用这个库可以很容易地创建出能够改变代码变量的界面组件。

使用这个工具可以帮助我们将后续设置的光源属性设置成可视化的调控界面,帮助我们更加容易的调控到合适的光源。

可以直接使用npm直接下载后在项目中引入使用。

$ npm install --save dat.gui
import * as dat from "dat.gui";
const gui = new dat.GUI();

我们在后续的添加光源中会实践dat.GUI的使用,这里就不多赘述了,详细可以参考这篇文章 👉 Dat.gui 使用教程

# 添加光源

为了更好的渲染场景,three.js 提供了生活中常见的一些光源的 API。案例中我们使用了PointLight点光源来帮助我们照亮球体。在场景中我们可以根据需求添加一个或多个光源,该案例中添加了两个光源。

在案例中使用了dat.GUI将光源的positioncolorintensity设置为可调控的控件。

// Light1
const pointLight2 = new THREE.PointLight(0xff0000, 2);
pointLight2.position.set(1.72, 1.13, -0.19);
pointLight2.intensity = 10;
scene.add(pointLight2);
// 设置dat.GUI
const light2 = gui.addFolder("Light 2");
light2
  .add(pointLight2.position, "x")
  .min(-3)
  .max(3)
  .step(0.01);
light2
  .add(pointLight2.position, "y")
  .min(-3)
  .max(3)
  .step(0.01);
light2
  .add(pointLight2.position, "z")
  .min(-3)
  .max(3)
  .step(0.01);
light2
  .add(pointLight2, "intensity")
  .min(0)
  .max(10)
  .step(0.01);
const light2Color = { color: 0xd1ff };
light2
  .addColor(light2Color, "color")
  .onChange(() => pointLight2.color.set(light2Color.color));
const pointLightHelper = new THREE.PointLightHelper(pointLight2);
scene.add(pointLightHelper);

// Light2
const pointLight3 = new THREE.PointLight(0xff0000, 2);
pointLight3.position.set(-1.58 - 1.19, -0.53);
pointLight3.intensity = 10;
scene.add(pointLight3);
const light3 = gui.addFolder("Light 3");
light3
  .add(pointLight3.position, "x")
  .min(-3)
  .max(3)
  .step(0.01);
light3
  .add(pointLight3.position, "y")
  .min(-3)
  .max(3)
  .step(0.01);
light3
  .add(pointLight3.position, "z")
  .min(-3)
  .max(3)
  .step(0.01);
light3
  .add(pointLight3, "intensity")
  .min(0)
  .max(10)
  .step(0.01);
const light3Color = { color: 0xa5ff00 };
light3
  .addColor(light3Color, "color")
  .onChange(() => pointLight3.color.set(light3Color.color));
const pointLightHelper2 = new THREE.PointLightHelper(pointLight3);
scene.add(pointLightHelper2);

image-20220602124005846

上图我们添加了点光源和 dat.GUI 控件,通过控件我们可以调控光源的位置,强度以及颜色直到满意为止。

点光源(PointLight):从一个点向各个方向发射的光源。一个常见的例子是模拟一个灯泡发出的光。

✨ 场景中借助PointLightHelper来辅助我们查看点光源。【创建一个虚拟的球形网格 Mesh 的辅助对象来模拟点光源 PointLight.】

✨ 常见的光源类型还有:环境光 AmbientLight平行光比如太阳光 DirectionalLight聚光源 SpotLight

# 👉 让场景动起来

如果希望我们的场景动起来,首先需要解决的问题就是如何在特定的时间间隔重新渲染场景。在 HTML5 和相关 JavaScript API 出现之前,是通过setInterval方法来实现的。比如,通过setInterval()方法指定某个函数每 100 毫秒调用一次。但是这个方法的缺点在于它不管浏览器当前发生什么,都会每个相应的时间执行一次。除此之外,setInterval() 方法并没有与屏幕的刷新同步,这将会导致较高的 CPU 使用率和性能不良。

# requestAnimationFrame( )方法

现代浏览器通过requestAnimationFrame函数为稳定而连续的渲染场景提供了良好的解决方案。通过这个函数,你可以向浏览器提供一个回调函数,无须定义回调时间间隔,浏览器将自行决定最佳的回调时机。我们所需要做的就是在这个回调函数中完成一帧绘制操作,剩下的工作交给浏览器,它负责使场景尽量高效和平滑的进行。

案例中,球体会随着时间不断的进行旋转,这里就是通过递归调用requestAnimationFrame()方法,将用于不断旋转物体的方法做为requestAnimationFrame的回调函数,不断进行调用进行渲染,从而达到物体不断旋转的效果。

const clock = new THREE.Clock();

const tick = () => {
  // elapsedTime用于保存时钟运行的总时长
  const elapsedTime = clock.getElapsedTime();
  // 让物体围绕x和y轴进行旋转
  sphere.rotation.y = 0.5 * elapsedTime;
  sphere.rotation.x = 0.5 * elapsedTime;
  // Render 进行渲染
  renderer.render(scene, camera);
  // //请求再次执行tick
  window.requestAnimationFrame(tick);
};

tick();

THREE.Clock:该对象用于跟踪时间,案例中通过clock.getElapsedTime()记录了首次初始化 Clock 到当前的总时长,随着时间的流动 elapseTime 会越来越大,将其做为物体旋转计算的一部分,可以保持物体一直随着时间围绕 x 和 y 坐标轴进行旋转。

# 👉 场景对浏览器自适应

当浏览器大小改变时改变摄像机是很容易实现的,首先我们需要做的就是为浏览器注册一个事件

window.addEventListener("resize", onResize, false);

这样,每当浏览器尺寸发生改变时onResize()方法就会执行,在onResize()方法中需要更新摄像机和渲染器:

function onResize() {
  camera.aspect = window.innnerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

对于摄像机,需要更新它的aspect属性,这个属性表示屏幕的长宽比,对于渲染器需要该案它的尺寸。

# 总结

本篇文章结合案例讲述了使用Three.js最为基本的一些步骤,后续还有很多知识需要继续学习。Three.js是基于原生WebGl API和着色器封装得到的 3D 引擎,想要真正学好并运用Three.js并不是多么熟悉其 API,而是要关注其底层也就是计算机图形学以及 3D 等相关知识。后续还会继续深入学习Three.js也会不断更新该系列的学习总结和记录。

可在该连接中获取该篇文章的代码 👉 代码