Three.js 进阶之旅:基础入门(下)

8,776 阅读17分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

摘要

专栏上一篇文章《Three.js 进阶之旅:基础入门(上)》主要讲解了 Three.js 环境搭建和项目构建及基本开发流程。本篇文章将继续通过一个简单 3D 创意页面的开发,简要汇总一下必备的 Three.js 的基础知识,梳理要点,为后续页面的开发打下坚实的基础。通过本文的内容,你将获得的知识包括:OrbitControls 镜头轨道控制器的使用、Scene.Fog 场景雾化、Three.js 中的光源、几何体、材质、模型、贴图、动画等。

效果

先来看看本节内容示例的实现效果,整个页面是一个由三维几何体构成的简单 🌌 宇宙背景,可以通过鼠标 🖱 操作星空,其中星球、轨道、卫星以及远处的星星 🪐 都在按照既定的动画规则持续转动。

preview.gif

项目托管在 Github 仓库【threejs-odessey】后续所有专栏项目目录都将在这个仓库中更新

🔗 代码仓库地址:git@github.com:dragonir/threejs-ode…

码上掘金

实现

页面结构

本页面同样没有额外的元素,只需在 body 中添加一个类名为 .webglcanvas 容器即可,用于渲染三维场景。

<canvas class="webgl"></canvas>

资源引入

引入 样式表Three.js,除此之外,本页面额外引入了 OrbitControls,它是镜头轨道控制器。

import './style.css';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

渲染场景初始化

场景渲染初始化也基本和上篇内容一样的。值得注意的是,本文中通过 scene.background 给场景设置了一个深黑色背景,通过 scene.fog 给场景设置了雾化效果,场景缩小到一定程度时页面就会叠加一种雾气一样的效果,场景中的物体会逐渐变得模糊

// 定义渲染尺寸
const sizes = {
  width: window.innerWidth,
  height: window.innerHeight
}

// 初始化渲染器
const canvas = document.querySelector('canvas.webgl');
const renderer = new THREE.WebGLRenderer({ canvas: canvas });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

// 初始化场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1A1A1A);
scene.fog = new THREE.Fog(0x1A1A1A, 1, 1000);

// 初始化相机
const camera = new THREE.PerspectiveCamera(40, sizes.width / sizes.height)
scene.add(camera);
camera.position.set(20, 100, 450);

// 页面缩放事件监听
window.addEventListener('resize', () => {
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;
  // 更新渲染
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  // 更新相机
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();
});

💡 知识点 Scene.Fog 场景雾化

Fog 类定义的是线性雾,雾的密度随着距离线性增大,即场景中物体雾化效果随着随距离线性变化。

构造函数

Fog(color, near, far)
  • color: 表示雾的颜色,如设置为白色,场景中远处物体为蓝色,场景中最近处距离物体是自身颜色,最远和最近之间的物体颜色是物体本身颜色和雾颜色的混合效果。
  • near:表示应用雾化效果的最小距离,距离活动摄像机长度小于 near 的物体将不会被雾所影响。
  • far:表示应用雾化效果的最大距离,距离活动摄像机长度大于 far 的物体将不会被雾所影响。

初始化控制器

初始化镜头轨道控制器 OrbitControls ,通过它可以对三维场景用鼠标 🖱 进行缩放、平移、旋转等操作,本质上改变的不是场景,而是相机的位置参数。可以选择通过设置 controls.enableDampingtrue 来开启控制器的移动惯性,这样在使用鼠标交互过程中就会感觉更加流畅和逼真。

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

添加光源

和现实世界一样,没有光线 🌞 的话就什么都看不见。未添加光源之前场景中的所有网格元素都是黑色的,无法显示其颜色和材质表面的物理特性,此时就需要给场景添加光源,才能看见场景中的物体。

step_0.png

本示例中只添加了一种光源 AmbientLight 环境光,它是一种基础光源,整个场景中的物体都将接收它的颜色。其中两个参数分别代表光照的颜色和强度。

const light = new THREE.AmbientLight(0xdeedff, 1.5);
scene.add(light);

💡 知识点 光源 Light

Three.js 中提供了很多种光源,它们可以模拟现实世界中大部分场景的光照效果。光源的使用方法也大致和本文示例中是一样的。下表列出了几种常用的光源,可以根据自己的需求场景分别选择不同的光源。大家可以在实践中把本示例中的环境光 AmbientLight 换成其他效果光源,看看它们生成的有什么区别。

光源名称描述
AmbientLight环境光,是一种基础光源,它的颜色会添加到整个场景和所有对象的当前颜色上
PointLight点光源,空间中的一点,朝所有的方向发射光线
SpotLight聚光灯光源,这种光源有聚光的效果,类似台灯、天花板上的吊灯,或者手电筒
DirectionLight方向光,也称为无限光。从这种光源发出的光线可以看着平行的。例如,太阳光
HemishpereLight半球光,这是一种特殊光源,可以用来创建更加自然的室外光线,模拟放光面和光线微弱的天空
AreaLight面光源,使用这种光源可以指定散发光线的平面,而不是空间中的一个点
LensFlare镜头眩光,不是源,但可以通过 LensFlare 为场景中的光源添加镜头光晕效果

光源的一些通用属性:

  • color:光源颜色。
  • intensity:光照强度。默认值是 1
  • visible:如果设为 true,该光源就会显示;如果设置为false,光源就会消失。

创建星球

示例页面中所有的模型元素都是采用 Three.js 内置的基础网格模型生成,所以和上篇文章创建立方体的流程是一样的,先创建立方体和材质,再用它们生成网格模型,最后将它添加到场景中即可。星球模型使用了非光泽表面材质 MeshLambertMaterial,立方体采用 SphereGeometry 生成。

const SphereMaterial = new THREE.MeshLambertMaterial({
  color: 0x03c03c,
  wireframe: true,
});
const SphereGeometry = new THREE.SphereGeometry(80, 32, 32);
const planet = new THREE.Mesh(SphereGeometry, SphereMaterial);
scene.add(planet);

step_1.png

为材质设置 wireframe: true 属性就能得到几何模型的线框结构,是不是看起来更加有科技感。

step_2.png

💡 知识点 几何体 Geometry

下面汇总了 Three.js 常用几何体的分类介绍以及构造器的参数,后续使用过程中可通过此表查询。由于本文篇幅内容有限,就不一一展示具体形状,大家在学习过程中一定要亲自动手试试各种几何体创建后是什么样子的,也可以多看看 threejs.org 官网文档。

名称构造器参数
PlaneGeometry【平面几何体】 width — 平面沿着X轴的宽度。默认值是 1height — 平面沿着 Y轴 的高度。默认值是 1widthSegments — 可选,平面的宽度分段数,默认值是 1heightSegments — 可选,平面的高度分段数,默认值是 1
CircleGeometry【圆形几何体】 radius — 圆形的半径,默认值为1segments — 分段的数量,最小值为 3,默认值为 8thetaStart — 第一个分段的起始角度,默认为 0thetaLength — 圆形扇区的中心角,通常被称为 θ。默认值是 2*Pi,这使其成为一个完整的圆。
RingGeometry【环形几何体】 innerRadius — 内部半径,默认值为 0.5outerRadius — 外部半径,默认值为 1thetaSegments — 圆环的分段数。这个值越大,圆环就越圆。最小值为 3,默认值为 8phiSegments — 最小值为 1,默认值为 8thetaStart — 起始角度,默认值为 0thetaLength — 圆心角,默认值为 Math.PI * 2
ShapeGeometry【形状几何体】 shapes — 一个单独的 shape,或者一个包含形状的 ArraycurveSegments - Integer - 每一个形状的分段数,默认值为 12
BoxGeometry【立方几何体】 width — X轴上面的宽度,默认值为 1heightY 轴上面的高度,默认值为 1depthZ 轴上面的深度,默认值为 1widthSegments — 可选,宽度的分段数,默认值是 1heightSegments — 可选,宽度的分段数,默认值是 1depthSegments — 可选,宽度的分段数,默认值是 1
SphereGeometry【球几何体】 radius — 球体半径,默认为 1widthSegments — 水平分段数,最小值为 3,默认值为 8heightSegments — 垂直分段数,最小值为 2,默认值为 6phiStart — 指定水平起始角度,默认值为 0phiLength — 指定水平扫描角度的大小,默认值为 Math.PI * 2thetaStart — 指定垂直起始角度,默认值为 0thetaLength — 指定垂直扫描角度大小,默认值为 Math.PI
CylinderGeometry【圆柱几何体】 radiusTop — 圆柱的顶部半径,默认值是 1radiusBottom — 圆柱的底部半径,默认值是 1height — 圆柱的高度,默认值是 1radialSegments — 圆柱侧面周围的分段数,默认为 8heightSegments — 圆柱侧面沿着其高度的分段数,默认值为 1openEnded — 一个 Boolean 值,指明该圆锥的底面是开放的还是封顶的。默认值为 false,即其底面默认是封顶的。thetaStart — 第一个分段的起始角度,默认为 0thetaLength — 圆柱底面圆扇区的中心角,通常被称为 “θ”。默认值是 2*Pi,这使其成为一个完整的圆柱。
ConeGeometry【圆锥几何体】 radius — 圆锥底部的半径,默认值为 1。height — 圆锥的高度,默认值为1。 radialSegments — 圆锥侧面周围的分段数,默认为 8heightSegments — 圆锥侧面沿着其高度的分段数,默认值为 1openEnded — 一个Boolean值,指明该圆锥的底面是开放的还是封顶的。默认值为 false,即其底面默认是封顶的。·thetaStart· — 第一个分段的起始角度,默认为 0thetaLength — 圆锥底面圆扇区的中心角,通常被称为 “θ”。默认值是 2*Pi,这使其成为一个完整的圆锥。
TorusGeometry【圆环几何体】 radius - 圆环的半径,从圆环的中心到管道的中心。默认值是 1tube — 管道的半径,默认值为 0.4radialSegments — 圆环的分段数,默认值为 8tubularSegments — 管道的分段数,默认值为 6arc — 圆环的中心角,默认值为 Math.PI * 2
TextGeometry【文本几何体】 fontTHREE.Font 的实例。sizeFloat。字体大小,默认值为 100heightFloat。挤出文本的厚度。默认值为 50curveSegmentsInteger。曲线上点的数量。默认值为 12bevelEnabledBoolean。是否开启斜角,默认为 falsebevelThicknessFloat。文本上斜角的深度,默认值为 20bevelSizeFloat。斜角与原始文本轮廓之间的延伸距离。默认值为 8bevelSegmentsInteger。斜角的分段数。默认值为 3
TetrahedronGeometry【四面几何体】 radius — 四面体的半径,默认值为 1detail — 默认值为 0。将这个值设为一个大于 0 的数将会为它增加一些顶点,使其不再是一个四面体。
OctahedronGeometry【八面几何体】 radius — 八面体的半径,默认值为 1detail — 默认值为 0,将这个值设为一个大于 0 的数将会为它增加一些顶点,使其不再是一个八面体。
DodecahedronGeometry【十二面几何体】 radius — 十二面体的半径,默认值为 1detail — 默认值为 0。将这个值设为一个大于 0 的数将会为它增加一些顶点,使其不再是一个十二面体。
IcosahedronGeometry【二十面几何体】 radius — 二十面体的半径,默认为 1detail — 默认值为 0。将这个值设为一个大于 0 的数将会为它增加一些顶点,使其不再是一个二十面体。当这个值大于 1 的时候,实际上它将变成一个球体。
TorusKnotGeometry【圆环扭结几何体】 radius - 圆环的半径,默认值为 1tube — 管道的半径,默认值为 0.4tubularSegments — 管道的分段数量,默认值为 64radialSegments — 横截面分段数量,默认值为 8p — 这个值决定了几何体将绕着其旋转对称轴旋转多少次,默认值是 2q — 这个值决定了几何体将绕着其内部圆环旋转多少次,默认值是 3
PolyhedronGeometry【多面几何体】 vertices — 一个顶点 Array[1,1,1, -1,-1,-1, … ]indices — 一个构成面的索引 Array[0,1,2, 2,3,0, … ]radiusFloat - 最终形状的半径。detailInteger - 将对这个几何体细分多少个级别。细节越多,形状就越平滑。
TubeGeometry【管道几何体】 pathCurve - 一个由基类 Curve 继承而来的路径。tubularSegmentsInteger - 组成这一管道的分段数,默认值为 64radiusFloat - 管道的半径,默认值为 1radialSegmentsInteger - 管道横截面的分段数目,默认值为 8closedBoolean 管道的两端是否闭合,默认值为 false

💡 知识点 材质 Material

材质可以模拟现实世界中物体表面的物理特性,Three.js 也提供的丰富的材质,我们在创建不同物体时可以选择不同的材质,比如创建木制桌面时可以选择 MeshPhysicalMaterial 物理网格材质,创建卡通风格模型时可以选择 MeshToonMaterial 卡通网格材质。下表列出了几种常用的材质类型及其说明。

名称描述
MeshBasicMaterial基础网格基础材质,用于给几何体赋予一种简单的颜色,或者显示几何体的线框。
MeshDepthMaterial网格深度材质,这个材质使用从摄像机到网格的距离来决定如何给网格上色。
MeshStandardMaterial标准网格材质,一种基于物理的标准材质,使用 Metallic-Roughness 工作流程
MeshPhysicalMaterial物理网格材质,MeshStandardMaterial 的扩展,能够更好地控制反射率。
MeshNormalMaterial网格法向材质,这是一种简单的材质,根据法向向量计算物体表面的颜色。
MeshLambertMaterial网格 Lambert 材质,这是一种考虑光照影响的材质,用于创建暗淡的、不光亮的物体。
MeshPhongMaterial网格 Phong 式材质,这是一种考虑光照影响的材质,用于创建光亮的物体。
MeshToonMaterial网格 Phong 式材质,MeshPhongMaterial 卡通着色的扩展。
ShaderMaterial着色器材质,这种材质允许使用自定义的着色器程序,直接控制顶点的放置方式以及像素的着色方式。
LineBasicMaterial直线基础材质,这种材质可以用于 THREE.Line 直线几何体,用来创建着色的直线。

创建星球轨道环

使用上述同样的方法,选择 圆环几何体 TorusGeometry 添加星球轨道到场景中,通过调整它的 rotation 属性来设置倾斜角度。

const TorusGeometry = new THREE.TorusGeometry(150, 8, 2, 120);
const TorusMaterial = new THREE.MeshLambertMaterial({
  color: 0x40a9ff,
  wireframe: true
});
const ring = new THREE.Mesh(TorusGeometry, TorusMaterial);
ring.rotation.x = Math.PI / 2;
ring.rotation.y = -0.1 * (Math.PI / 2);
scene.add(ring);

step_3.png

创建卫星

应用 二十面几何体 IcosahedronGeometry 创建一个卫星。

const IcoGeometry = new THREE.IcosahedronGeometry(16, 0);
const IcoMaterial = new THREE.MeshToonMaterial({ color: 0xfffc00 });
const satellite = new THREE.Mesh(IcoGeometry, IcoMaterial);
scene.add(satellite);

step_4.png

创建星星

我们计划创建 500 个星星 🌟,由于数量较多,每个都单独放置到场景中不仅会造成页面性能问题,而且也不好对星星整体进行管理。为了解决这个问题,我们可以先创建一个 Group,然后将单个星星添加到 Group 分组中,最后再将整个分组添加到场景中。在 for 循环 中创建单个星星时,同样使用的是 IcosahedronGeometry 二十面几何体,材质使用了 MeshToonMaterial 卡通网格材质,并为它们设置了限定范围内的随机位置和角度。

const stars = new THREE.Group();
for (let i = 0; i < 500; i++) {
  const geometry = new THREE.IcosahedronGeometry(Math.random() * 2, 0);
  const material = new THREE.MeshToonMaterial({ color: 0xeeeeee });
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.x = (Math.random() - 0.5) * 700;
  mesh.position.y = (Math.random() - 0.5) * 700;
  mesh.position.z = (Math.random() - 0.5) * 700;
  mesh.rotation.x = Math.random() * 2 * Math.PI;
  mesh.rotation.y = Math.random() * 2 * Math.PI;
  mesh.rotation.z = Math.random() * 2 * Math.PI;
  stars.add(mesh);
}
scene.add(stars);

step_5.png

动画更新

最后除了需要在页面重绘动画中更新相机和渲染器之外,在这里也给场景中的其他物体添加一些动画效果。🪐 星球和轨道朝两个相反的方向自转,卫星 🛰 绕着它们公转。整个星星 Group 也在进行一个同时绕着 Y轴Z轴 的自转。

let rot = 0;
// 动画
const axis = new THREE.Vector3(0, 0, 1);
const tick = () => {
  // 更新渲染器
  renderer.render(scene, camera);
  // 给网格模型添加一个转动动画
  rot += Math.random() * 0.8;
  const radian = (rot * Math.PI) / 180;
  // 星球位置动画
  planet && (planet.rotation.y += .005);
  // 星球轨道环位置动画
  ring && ring.rotateOnAxis(axis, Math.PI / 400);
  // 卫星位置动画
  satellite.position.x = 250 * Math.sin(radian);
  satellite.position.y = 100 * Math.cos(radian);
  satellite.position.z = -100 * Math.cos(radian);
  satellite.rotation.x += 0.005;
  satellite.rotation.y += 0.005;
  satellite.rotation.z -= 0.005;
  // 星星动画
  stars.rotation.y += 0.0009;
  stars.rotation.z -= 0.0003;
  // 更新控制器
  controls.update();
  // 页面重绘时调用自身
  window.requestAnimationFrame(tick);
}
tick();

到此,本文页面的所有功能都全部完成了,如果对 Three.js 以及本文内容实现的页面效果比较感兴趣一定要亲自动手试试。

其他必备知识

下面列出了几个本文示例中未涉及,但是非常重要的几个 Three.js 基础知识,在后续开发中几乎都会用到,也需要牢牢掌握。

模型

在现实开发中,有时除了需要用代码创建模型之外,多数场景需要加载设计师提供的使用设计软件导出的模型。此时就需要使用模型加载器去加载模型,不同格式的模型需要引入对应的模型加载器,虽然加载器不同,但是使用方式基本上是相同的。下面就是使用 OBJLoader 加载 .obj 格式模型的过程。

var loader = new THREE.OBJLoader();
loader.load(model, function (object) {
  object.traverse(function (child) {
    if (child.isMesh) {
      // 对模型子网格的一些操作
    }
  });
  scene.add(object);
});

📌 Three.js 支持的模型格式:3ds (.3ds)、amf (.amf)、3mf (.3mf)、assimp & assimp2json (.assimp |.json)、awd (.awd)、Babylon (.babylon)、BVH (.bvh)、Collada(.dae |.xml)、OpenCTM (.ctm)、draco(.drc)、FBX(.fbx)、GCode (.gcode)、glTF (.gltf)、Clara(.json)、KMZ(.kmz)、LDraw(.mpd)、LightWave(.lwo)、MD2 (.md2)、MMD(.pmd | .vmd)、nrrd (.nrrd)、obj/obj2 (.obj)、pcd (.pcd)、PDB(.pdb)、PlayCanvas(.json)、ply (.ply)、prwm(.prwm)、sea3d(.sea3d)、stl(.stl)、vrm(.vrm)、vrml(.vrml)、vtk、x等。

贴图

为了模拟更加真实的效果,就要给模型材质添加贴图,贴图就像模型的皮肤一样,使其三维效果更佳。添加贴图的原理是通过纹理贴图加载器 TextureLoader() 去新创建一个贴图对象出来,然后再去调用里面的 load() 方法去加载一张图片,这样就会返回一个纹理对象,纹理对象可以作为模型材质颜色贴图 map 属性的值,材质的颜色贴图属性 map 设置后,模型会从纹理贴图上采集像素值。下面列出了几种常用的贴图类型以及加载贴图的基本流程。

  • map:材质贴图
  • normalMap:法线贴图
  • bumpMap:凹凸贴图
  • envMap:环境贴图
  • specularMap:高光贴图
  • lightMap:光照贴图

代码示例:

const texLoader = new THREE.TextureLoader();
loader.load('assets/models/meta.fbx', function (mesh) {
  mesh.traverse(function (child) {
    if (child.isMesh) {
      if (child.name === '需要添加贴图的模型') {
        child.material = new THREE.MeshPhysicalMaterial({
          map: texLoader.load("./assets/images/metal.png"),
        });
      }
    }
  });
})

动画

给三维场景添加动画可以使得页面变得更加生动,像本例中就给模型添加了简单的基础动画和相机控制动画。Three.js 中的动画基本可以分为下面几类,在本专栏后后续内容中会专门使用一篇内容详细讲解 Three.js 中的动画。

  • 基础动画
  • 相机控制
  • 变形动画
  • 用骨骼和蒙皮制作动画
  • 使用外部模型创建动画

其他

最后还有着色器和后期渲染、媒体交互、物体特性等高级功能,专栏后续文章将专门讲解这几项相对复杂的内容。

🔗 源码仓库地址:github.com/dragonir/th…

总结

本文中主要包含的知识点包括:

  • OrbitControls:镜头轨道控制器的使用
  • Scene.Fog 场景雾化
  • Three.js 中的光源
  • Three.js 中的几何体
  • Three.js 中的材质
  • Three.js 中的模型
  • Three.js 中的贴图
  • Three.js 中的动画

想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 👍

附录

参考