Three.js 进阶之旅:模型光源结合生成明暗变化的创意页面-光与影之诗 💡

7,345 阅读11分钟

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

摘要

本篇文章将根据专栏前两章《Three.js 进阶之旅:基础入门(上)》《Three.js 进阶之旅:基础入门(下)》学到的基础知识以及 Tween 补间动画相关的知识,创建一个使用光源和模型结合而成的 3D 创意页面。通过本文内容,你将学到的知识包括:使用 Blender 压缩模型、使用模型加载管理器管理加载进度、使用模型加载器加载压缩过的模型、优化渲染器的输出效果、使用 TWEEN 实现位移动画和镜头补间动画、点光源随鼠标移动动画、鼠标光标悬浮到导航栏时虚拟光标动画、监听页面元素可见性以及 CSS 动画效果等。

效果

开始之前,我们先来看看最终页面的实现效果。页面共分为两页,首先是第 1 页,主体是一个头部雕像 🗿 模型,使用鼠标在头像周围移动可以观察到由光源 💡 引起的明暗变化效果以及模型朝相反的方向发生位移。页面其余部分是导航栏和一些装饰性文字。

preview_0.gif

向下 👇 滚动到第 2 页,左侧是一个 Tab 菜单及下方的诗句文案,点击 🖱 菜单选项可以引起右侧雕像位置和角度的变化,在右侧雕像上移动鼠标,同样能观察到由光源 💡 引起的明暗变化效果。两个页面使用同一个模型,只是因为相机角度的不同因而呈现不用的视觉效果。

preview_1.gif

打开以下链接中的任意一个就可以在线预览效果,大屏访问效果更佳。

本专栏系列代码托管在 Github 仓库【threejs-odessey】后续所有目录也都将在此仓库中更新

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

码上掘金

准备工作

本页面的开发,涉及到外部模型 🗿 的加载,开发之前,需要准备所需的模型资源。如果你已经具备较强的建模能力,可以使用 3D 建模软件 Blender3D Max 创建模型并导出前端开发需要的模型格式备用。如果只想练习 Three.js 开发技能,可以直接在网络上找一些免费的模型练手。

下载模型

本案例中使用的雕像模型 🗿 来源于 Sketchfab 的免费模型。Sketchfab是一个发布、共享、发现 3DVRAR 内容的平台,它提供了基于 WebGLWebVR 技术的查看器,可以在 Web 上显示3D 模型,并可以在任何移动浏览器,桌面浏览器或虚拟现实头盔上进行查看。大家也可以在上面找一些自己喜欢的模型下载练习。

sketchfab.png

📌 下载网络上的模型时一定要注意模型的使用许可范围,不要用于商业用途,避免产生版权纠纷!

压缩模型

下载到模型后,就会发现即使是一些简单模型体积一般都有 30M-50M,模型体积过大不仅会占用大量存储空间,而且会影响页面加载速度和性能。此时就需要对模型进行压缩优化。我们可以直接使用 Blender 软件进行优化。基本操作步骤是:

  • Blender 中导入下载好的模型;
  • 在右侧场景集合栏中删掉场景、光源、摄像机等多余元素,只留模型主体网格;
  • 导出模型,导出时勾选压缩选项去选动画等选项。

模型压缩后体积一般可以缩小到 几百K 以内,压缩效果近乎百倍。若此时使用 Windows 自带的 3D查看器 等软件预览压缩之后的模型时会报错,不用担心,在 Three.js 中有用于专门加载压缩模型的加载器,使用专门的 Loader 即可在页面正常渲染预览,在本文后续内容中将详细介绍。

blender.png

📌 Blender 是一款跨平台可以在Linux、macOS以及Windows系统下运行的免费开源三维图形图像软件。它体积小巧,便于分发,提供了大量的基础工具,包括 建模、渲染、动画绑定、视频编辑、视觉效果、合成、贴图以及多种类型的模拟等。

实现

页面结构

先来开发页面整体布局,页面主要由以下几部分构成:#loading-text-introLoading 加载页,用于在模型加载完成之前展示;nav.header 是页面的导航栏;如以下示例图所示,两页内容主体区域分别是section.firstsection.second 它们的宽都为 100vw,高都为 100vh充满整个可视区,位于其中的 .webgl 分别用于加载和渲染两页内容上的 3D 模型。

<div class='shadow_page'>
  <div id="loading-text-intro"><p>Loading</p></div>
  <div class="content" style="visibility: hidden">
    <nav class="header"></nav>
    <section class="section first">
      <div class='info'></div>
      <canvas id='canvas-container' class='webgl'></canvas>
    </section>
    <section class="section second">
      <div class="second-container"></div>
      <canvas id='canvas-container-details' class='webgl'></canvas>
    </section>
  </div>
</div>

layout.png

资源引入

在代码顶端引入开发页面功能必需的库和资源。style.css 是全局样式表;Three.js 中的类和方法在本示例中采用按需引入的方式加载;TWEEN 是用于生成页面补间动画的库,在本页面中使用它实现场景镜头切换和元素位移动画效果;DRACOLoader 用于加载压缩过的模型,是提高页面加载速率和性能的关键;GLTFLoader 用于加载 .gltf.glb 格式的模型。

import './style.css';
import { Clock, Scene, LoadingManager, WebGLRenderer, sRGBEncoding, Group, PerspectiveCamera, DirectionalLight, PointLight, MeshPhongMaterial } from 'three';
import { TWEEN } from 'three/examples/jsm/libs/tween.module.min.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

场景初始化

场景初始化流程和前两节内容基本一致,需要注意 的是本示例页面有上下两页,每页上面都各自有一个三维场景需要渲染,因此渲染器、和相机 📷 都需要创建两个,Scene 只需创建一个即可。当页面产生缩放事件时,所有相机和渲染器也都需要同时更新,不然可能会产生模型变形和错位问题。

const section = document.getElementsByClassName('section')[0];
let width = section.clientWidth;
let height = section.clientHeight;
// 初始化渲染器1
const renderer = new WebGLRenderer({
  canvas: document.querySelector('#canvas-container'),
  antialias: true,
  alpha: true,
  // 提示用户代理怎样的配置更适用于当前的WebGL环境
  powerPreference: 'high-performance'
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 定义渲染器是否在渲染每一帧之前自动清除其输出
renderer.autoClear = true;
// 定义渲染器的输出编码
renderer.outputEncoding = sRGBEncoding;

// 初始化渲染器2
const renderer2 = new WebGLRenderer({
  canvas: document.querySelector('#canvas-container-details'),
  antialias: false
});
renderer2.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer2.setSize(width, height);
renderer2.outputEncoding = sRGBEncoding;

// 初始化场景
const scene = new Scene();

// 初始化相机1
const cameraGroup = new Group();
scene.add(cameraGroup);
const camera = new PerspectiveCamera(35, width / height, 1, 100)
camera.position.set(19, 1.54, -.1);
cameraGroup.add(camera);

// 初始化相机2
const camera2 = new PerspectiveCamera(35, width / height, 1, 100);
camera2.position.set(3.2, 2.8, 3.2);
camera2.rotation.set(0, 1, 0);
scene.add(camera2);

// 页面缩放事件监听
window.addEventListener('resize', () => {
  let section = document.getElementsByClassName('section')[0];
  camera.aspect = section.clientWidth / section.clientHeight
  camera.updateProjectionMatrix();
  camera2.aspect = section.clientWidth / section.clientHeight;
  camera2.updateProjectionMatrix();
  renderer.setSize(section.clientWidth, section.clientHeight);
  renderer2.setSize(section.clientWidth, section.clientHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer2.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

💡 知识点 渲染编码outputEncoding

outputEncoding 属性控制输出渲染编码。

  • 默认情况下,值为 THREE.LinearEncoding,这种线性编码的缺点是看起来不够真实。
  • 此时可以将值设置为 THREE.sRGBEncoding 提升渲染输出效果的真实性。
  • 此外还有另一个可选属性值为 THREE.GammaEncoding,它是一种存储颜色的方法, 这种编码的优点在于它允使用一种表现像亮度的 gammaFactor 值,根据人眼的敏感度优化明暗值的存储方式。当使用 sRGBEncoding 时,其实就像使用默认 gammaFactor 值为 2.2GammaEncoding

加载管理

模型 🗿 加载会使页面产生一段空白时间,此时可以添加一个 Loading 加载页展示页面资源加载进度 ,缓解等待时间。使用 THREE.LoadingManager 即可实现加载进度管理。在正式加载模型前,我们先定义好模型加载后的回调方法,在本示例中的回调方法中,使用 TWEEN 添加了两个补间动画效果:一个是当模型资源加载完成时,隐藏 Loading 加载页面;另一个是当第一个页面场景加载时,镜头由近到远的镜头拉升动画。

// 初始化加载管理器
const loadingManager = new LoadingManager();
loadingManager.onLoad = () => {
  document.querySelector('.content').style.visibility = 'visible';
  const yPosition = { y: 0 };
  const ftsLoader = document.querySelector('.lds-roller');
  const loadingCover = document.getElementById('loading-text-intro');
  // 隐藏加载页面动画
  new TWEEN.Tween(yPosition)
    .to({ y: 100 }, 900)
    .easing(TWEEN.Easing.Quadratic.InOut)
    .start()
    .onUpdate(() => { loadingCover.style.setProperty('transform', `translate(0, ${yPosition.y}%)`)})
    .onComplete(function () {
      loadingCover.parentNode.removeChild(document.getElementById('loading-text-intro'));
      TWEEN.remove(this);
    });
  // 第一个页面相机添加入场动画
  new TWEEN.Tween(
    camera.position.set(0, 4, 2))
    .to({ x: 0, y: 2.4, z: 5.8 }, 3500)
    .easing(TWEEN.Easing.Quadratic.InOut)
    .start()
    .onComplete(function () {
      TWEEN.remove(this)
      document.querySelector('.header').classList.add('ended')
      document.querySelector('.description').classList.add('ended')
    })
  ftsLoader.parentNode.removeChild(ftsLoader);
  window.scroll(0, 0)
}

step_0.png

💡 知识点 THREE.LoadingManager

其功能是处理并跟踪已加载和待处理的数据。如果未手动设置加强管理器,则会为加载器创建和使用默认全局实例加载器管理器。以下是加载器的基本使用方法:

// 初始化加载器
const manager = new THREE.LoadingManager();
// 此函数在加载开始时被调用
manager.onStart = function ( url, itemsLoaded, itemsTotal ) {
  console.log( 'Started loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
};
// 所有的项目加载完成后将调用此函数。默认情况下,该函数是未定义的,除非在构造函数中传入
manager.onLoad = function ( ) {
  console.log( 'Loading complete!');
};
// 此方法加载每一个项,加载完成时进行调用
manager.onProgress = function ( url, itemsLoaded, itemsTotal ) {
  console.log( 'Loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
};
// 此方法将在任意项加载错误时,进行调用
manager.onError = function ( url ) {
  console.log( 'There was an error loading ' + url );
};
const loader = new THREE.OBJLoader( manager );
// 加载模型
loader.load('file.obj', function (object) {});

💡 知识点 补间动画TWEEN

Tween.js 是附加在 Three.js 库中的一个扩充动画库,它可以平滑的修改元素的属性值,使一个对象在一定时间内从一个状态缓动变化到另外一个状态,配合动画函数实现丝滑的动画效果TWEEN.js 本质就是一系列缓动函数算法,结合CanvasThree.js 很简单就能实现很多动画效果。本专栏项目示例中将多次使用它实现创意的动画效果。

基本使用

var tween = new TWEEN.Tween({x: 1})     // position: {x: 1}
.delay(100)                             // 等待100ms
.to({x: 200}, 1000)                     // 1s时间,x到200
.onUpdate(render)                       // 变更期间执行render方法
.onComplete(() => {})                   // 动画完成
.onStop(() => {})                       // 动画停止
.start();                               // 开启动画

要让动画真正动起来,需要在 requestAnimationFrame 中调用 update 方法。

TWEEN.update()

缓动类型

TWEEN.js 最强大的地方在于提供了很多常用的缓动动画类型,由 api easing() 指定。如示例中用到的:

tween.easing(TWEEN.Easing.Cubic.InOut);

链式调用

TWEEN.js 支持链式调用,如在 动画A 结束后要执行 动画B,可以这样 tweenA.chain(tweenB) 利用链式调用创建往复来回循环的动画:

var tweenA = new TWEEN.Tween(position).to({x: 200}, 1000);
var tweenB = new TWEEN.Tween(position).to({x: 0}, 1000);
tweenA.chain(tweenB);
tweenB.chain(tweenA);
tweenA.start();

加载雕像模型

定义好 模型加载管理器,就可以将它作为参数传递给 模型加载器 来进行加载进度管理了。本文示例中使用的模型是 .glb 格式,因此需要使用对应的模型加载器 GLTFLoader 来加载模型。在前面步骤中我们为减小模型体积对其进行了压缩,此时直接使用 GLTFLoader 加载模型会发生报错,需要配合使用 DRACOLoader 才能正常加载模型。模型 🗿 加载完成后,为实现雕像光滑反光的效果,可以将它的材质替换为 MeshPhongMaterial 来创建光亮的表面。

// 使用 dracoLoader 加载用blender压缩过的模型
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
dracoLoader.setDecoderConfig({ type: 'js' });
const loader = new GLTFLoader(loadingManager);
loader.setDRACOLoader(dracoLoader);

// 模型加载
let oldMaterial;
loader.load('/models/statue.glb', function (gltf) {
  gltf.scene.traverse((obj) => {
    if (obj.isMesh) {
      oldMaterial = obj.material;
      obj.material = new MeshPhongMaterial({
        shininess: 100
      })
    }
  })
  scene.add(gltf.scene);
  oldMaterial.dispose();
  renderer.renderLists.dispose();
});

💡 知识点 DRACOLoader

  • DRACOLoader 是使用 Draco 库压缩的几何图形加载器。
  • Draco 是一个开源库,用于压缩和解压缩 3D 网格和点云。压缩后的几何图形可以明显更小,代价是客户端设备上的额外解码时间。
  • 独立的 Draco 文件具有 .drc 扩展名,并包含顶点位置、法线、颜色和其他属性。 Draco 文件不包含材质、纹理、动画或节点层次结构,要使用这些功能,需要将 Draco 几何图形嵌入到 glTF 文件中。 可以使用 glTF-Pipeline 将普通的 glTF 文件转换为 Draco 压缩的 glTF 文件。 当使用带有 glTFDraco 时,GLTFLoader 将在内部使用 DRACOLoader 的实例。
  • DRACOLoader 依赖于 IE11 不支持的 ES6 Promises,要在 IE11 中使用加载器,必须包含一个提供 Promise 替换的 polyfill
  • 开源 3D 建模工具 Blender 可以生成使用 Draco 压缩过的模型。

使用 DRACOLoader 时,必须在静态资源目录引入以下文件,并在初始化时设置正确的资源路径。

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
dracoLoader.setDecoderConfig({ type: 'js' });

draco.png

添加光源

模型加载完成之后,为了模型可以显示在场景中,就需要添加光源 💡 。本示例中添加了两种初始光源:白色 直射光和偏绿色 🟩点光源。其中直射光作为照亮场景的基础光源;点光源用来实现鼠标略过模型时明暗变化效果。点光源是一种单点发光,照射所有方向的光源,它的发光效果类似于夜空中发射的照明弹,在点光源的照射下,物体的迎光面会亮一些,背光面会暗一些。

// 直射光
const directionLight = new DirectionalLight(0xffffff, .8);
directionLight.position.set(-100, 0, -100);
scene.add(directionLight);
// 点光源
const fillLight = new PointLight(0x88ffee, 2.7, 4, 3);
fillLight.position.set(30, 3, 1.8);
scene.add(fillLight);

step_1.png

使用不同的点光源颜色,可以产生不同的视觉效果,本文中装饰主色调是 #03c03c 🟩,因此选取了一种偏绿的点光源,大家实践时可以调整成自己喜欢的颜色 🤣,下图是使用偏蓝色 🟦 的点光源效果。

step_1_1.png

通过调整两个页面对应的相机参数,调整好不同的模型的初始位置,页面 2 的模型初始位置如下图。

step_2.png

动画效果

示例页面虽然只有两个页面,但是涉及到的动画效果种类较多,需要结合 TWEEN、页面重绘动画 requestAnimationFrame 以及 CSS 来共同实现,可以按以下几个步骤分别实现本示例中的所有动画效果 🌠

① 鼠标移动时添加虚拟光标

监听鼠标在页面上的移动事件并记录鼠标在页面上的位置信息 📍,以供在页面重绘动画中通过动态修改点光源的方式实现点光源跟随鼠标效果。可以观察到本页面是隐藏默认鼠标光标的,而是显示个性化样式的圆形光标 。在此步骤中实时修改圆形光标元素的位置,实现自定义鼠标光标。

const cursor = { x: 0, y: 0 };
document.addEventListener('mousemove', event => {
  event.preventDefault();
  cursor.x = event.clientX / window.innerWidth - .5;
  cursor.y = event.clientY / window.innerHeight - .5;
  // 鼠标移动时添加虚拟光标
  document.querySelector('.cursor').style.cssText = `left: ${event.clientX}px; top: ${event.clientY}px;`;
}, false);

② 点光源随鼠标移动动画

现在实现当鼠标在模型上方悬浮移动时点光源跟随变化的动画效果。

  • 首先,我使用 IntersectionObserver 来检测当前页面是 页面1 还是 页面2,以便对两个页面对应的相机和渲染器分别更新。
  • 接着,获取通过 Clock().getElapsedTime() 获取两次重绘的时间间隔。
  • 使用时间间隔作为种子更新点光源在 X轴Y轴 的位置,效果是当鼠标上下左右移动时光源随鼠标相同方向移动。
  • 使用时间间隔作为种子更新相机组在 X轴Z轴 的位置,效果是当鼠标左右移动时模型往相反方向移动,当鼠标上下移动是模型变大或缩小。
  • 最后,别忘了还需要在页面重绘动画中调用 TWEEN.update 来更新镜头补间动画,添加的所有 TWEEN 动画都无法生效。
let secondContainer = false;
const ob = new IntersectionObserver(payload => {
  secondContainer = payload[0].intersectionRatio > 0.05;
}, { threshold: 0.05 });
ob.observe(document.querySelector('.second'));

// 页面重绘动画
const clock = new Clock()
let previousTime = 0;
const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  const deltaTime = elapsedTime - previousTime;
  previousTime = elapsedTime;
  const parallaxY = cursor.y;
  const parallaxX = cursor.x
  // 点光源位置
  fillLight.position.y -= (parallaxY * 9 + fillLight.position.y - 2) * deltaTime;
  fillLight.position.x += (parallaxX * 8 - fillLight.position.x) * 2 * deltaTime;
  // 相机组位置
  cameraGroup.position.z -= (parallaxY / 3 + cameraGroup.position.z) * 2 * deltaTime;
  cameraGroup.position.x += (parallaxX / 3 - cameraGroup.position.x) * 2 * deltaTime;
  TWEEN.update();
  secondContainer ? renderer2.render(scene, camera2) : renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();

step_3.gif

鼠标在 页面2 的模型上方移动,也同样有光线明暗变化的效果。

step_4.gif

③ 鼠标光标悬浮到导航栏时虚拟光标动画

为了有更好的交互和视觉体验,也添加了一些当鼠标 🖱页面1 导航栏菜单上移动时的动画效果:当鼠标悬浮到导航菜单栏时,鼠标光标放大,在选中的菜单上移动光标菜单会跟随发生部分偏移;当离开菜单时,鼠标光标恢复原状。这种效果是通过动态修改菜单元素的样式 cssText 属性以及结合 CSS 实现的。

const btn = document.querySelectorAll('nav > .a');
function update(e) {
  const span = this.querySelector('span');
  if (e.type === 'mouseleave') {
    span.style.cssText = '';
  } else {
    const { offsetX: x, offsetY: y } = e;
    const { offsetWidth: width, offsetHeight: height } = this;
    const walk = 20;
    const xWalk = (x / width) * (walk * 2) - walk, yWalk = (y / height) * (walk * 2) - walk;
    span.style.cssText = `transform: translate(${xWalk}px, ${yWalk}px);`
  }
}
btn.forEach(b => b.addEventListener('mousemove', update));
btn.forEach(b => b.addEventListener('mouseleave', update));

CSS 中给菜单栏选项和鼠标光标添加 :hover 动画效果。

nav.header .a:hover {
  cursor: pointer;
  color: #afafaf;
  transform: scale(1.1);
}
nav.header .a:hover~.cursor {
  transform: translate(-50%, -50%) scale(5);
  opacity: 0.1;
}

step_5.gif

④ 点击Tab菜单栏时模型旋转动画

页面2 上,点击左侧的三个 Tab 菜单可以产生模型转动的效果,这部分就实现这个功能。页面2 上模型 🗿 的旋转,其实是通过改变 相机2📷 的位置和角度来实现的。为了旋转动画丝滑不生硬,同样使用 TWEEN 补间动画来实现此效果。首先定义一个相机旋转动画,当 Tab 标签被点击时,相机2📷 根据传入的参数变换到对应的角度和位置,视觉上就产生了模型发生旋转的效果。

function animateCamera(position, rotation) {
  // 相机2的位置动画
  new TWEEN.Tween(camera2.position)
    .to(position, 1800)
    .easing(TWEEN.Easing.Quadratic.InOut)
    .start()
    .onComplete(function () {
      TWEEN.remove(this)
    })
  // 相机2的旋转动画
  new TWEEN.Tween(camera2.rotation)
    .to(rotation, 1800)
    .easing(TWEEN.Easing.Quadratic.InOut)
    .start()
    .onComplete(function () {
      TWEEN.remove(this)
    })
}

// 点击第一Tab菜单
document.getElementById('one').addEventListener('click', () => {
  document.getElementById('one').classList.add('active')
  document.getElementById('three').classList.remove('active')
  document.getElementById('two').classList.remove('active')
  document.getElementById('content').innerHTML = '昨夜西风凋碧树。独上高楼,望尽天涯路。'
  animateCamera({ x: 3.2, y: 2.8, z: 3.2 }, { y: 1 });
});
// 点击第二个Tab菜单和第三个菜单的交互逻辑与第一个类似
document.getElementById('two').addEventListener('click', () => { /* ...*/ });
document.getElementById('three').addEventListener('click', () => { /* ...*/ });

step_6.gif

💡 知识点 THREE.Clock

本文中使用 Clock 提供的 getElapsedTime 方法来获取页面重绘两帧之间的时间间隔。 Clock 本质上就是对 Date 进行封装,提供了一些方法和属性,在 Three.js 使用过程中涉及到时间相关的方法时不用对 Date 进行封装,直接使用 Clock 提供的方法即可。在骨骼动画、变形动画、粒子动画等功能的开发中常常需要调用 Clock 的方法。

两个常用方法

  • getElapsedTime():获取自时钟启动后的秒数,同时将 .oldTime 设置为当前时间。 如果 .autoStart 设置为 true 且时钟并未运行,则该方法同时启动时钟。
  • getDelta():获取自 .oldTime 设置后到当前的秒数。 同时将 .oldTime 设置为当前时间。 如果 .autoStart 设置为 true 且时钟并未运行,则该方法同时启动时钟。

💡 知识点 Intersection Observer

  • 本文中使用 Intersection Observer 来辨识当前处于哪个页面以更新相机位置。
  • IntersectionObserver 接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗 viewport 交叉状态的方法。
  • 可以使用它来检测元素在页面上的可视状态或者两个元素之间的相对可视状态。应用这一特性可以用它来实现页面滚动加载、图片懒加载等功能。

📌 其他使用方法详情可访问文章末尾提供的参考链接。

页面装饰

为了使页面更具艺术气息,可以添加一些额外的页面装饰。本示例中 DRAGONIRTHREE.JS ODESSEY 字样使用了免费字体 abduction.ttf,可以在 CSS 中使用如下方式定义,并在需要的选择器当中使用 font-family 引用。

@font-face {
  font-family: abduction;
  src: url('../static/fonts/abduction.ttf');
}

step_7.png

最后再为页面添加一些百万诗句文案,升华一下页面氛围,“光与影之诗” 的页面主题就瞬间跃然屏上 。文案引用的是王国维先生的人生的三个境界,细细品之,Three.js 进阶之旅、前端学习之路何尝不是这三种境界呢 🤔

"昨夜西风凋碧树。独上高楼,望尽天涯路。"此第一境也。"衣带渐宽终不悔,为伊消得人憔悴。"此第二境也。"众里寻他千百度,蓦然回首,那人却在灯火阑珊处。"此第三境也。

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

总结

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

  • 使用 Blender 压缩模型;
  • 使用 LoadingManager 管理模型资源加载进度;
  • 使用 DRACOLoader 和模型对应格式加载器加载压缩过的模型;
  • 修改 outputEncoding 设置渲染器的编码输出;
  • 使用 TWEEN 实现位移动画和镜头补间动画;
  • 使用点光源照亮模型;
  • 隐藏默认鼠标光标创建虚拟光标;
  • 点光源随鼠标移动动画;
  • 鼠标光标悬浮到导航栏时虚拟光标动画;
  • 点击 Tab 菜单栏时模型旋转动画;
  • THREE.Clock 基本使用方法;
  • Intersection Observer 监听页面元素可见性;
  • 使用 @font-face 添加字体装饰页面等。

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

附录

参考