上一节我们基本完成了我们Demo所需要的所有功能,离成功只差最后一步 —— 性能优化!
目录结构
├── font // 字体文件
|├──── font.ttf // 字体源文件
|└──── font.json // 转换后的字体文件
├── img // 素材图片
|├──── xx.png
|├──── xxx.jpg
|└──── ...
├── js // 自己编写的js文件
|├──── common_fn.js // 公共部分
|├──── composer_fn.js // 后期处理
|├──── create_fn.js // 创建各种几何
|├──── init_fn.js // 初始化项目
|└──── util_fn.js // 工具函数
├── lib // 需要引入的js文件
|├──── three.js
|├──── OrbitControls.js
|├──── RenderPass.js
|└──── ...
├── model // 建模工具导出的模型
|├──── computer.gltf
|└──── ...
└── index.html // 入口文件
性能优化
ThreeJS常用性能优化方法
提取公共部分
在构建Demo的过程,我们需要创建很多几何体 Geometry 和材质 Material,以及一些 Loader 等等,这些资源都是可以单独提取成公共部分的
// 创建一个common_fn.js,放在js目录下
// common_fn.js
// MeshBasicMaterial 这个材质经常使用,并且该黑色材质被多次使用
const blackBasicMaterial = window.blackBasicMaterial = new THREE.MeshBasicMaterial({ color: "black" });
// TextureLoader 也是一个常用的 Loader,先创建一个实例出来供后面共用(这里也是参考了享元模式)
const textureLoader = window.textureLoader = new THREE.TextureLoader();
// 后续代码中如要使用,直接用变量名即可,因为放入了全局变量
利用 clone 方法复用
Demo中有很多几何体都是同一种类型,只是他们的位置不一样,所以我们可以通过 Mesh 的 clone 方法实现复用的目的,复用后修改位置即可
// 在 create_fn.js 中构建一个 createClone 方法
// create_fn.js
function createClone(mesh, conf) {
const newMesh = mesh.clone();
initConfig(newMesh, conf);
return newMesh;
}
在入口文件 index.html 中使用
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<div id="canvas-frame"></div>
<script>...</script>
<script>
// ...
(async function () {
// scene场景的公共内容,用于后面 createClone 公用
const earth = createEarth({ position: { x: -15, y: -1 } });
const machine = createMachine("./img/move.png", {
position: { x: 15, z: -20, y: -5 },
});
// scene场景的第一组内容
let group1, group1Animate;
{
const earth2 = createClone(earth, { position: { x: 15, y: -1 } });
const machine2 = createClone(machine, {
position: { x: -15 },
});
// ...
}
// scene场景的第二组内容
{
const earth1 = createClone(earth, {
position: { x: 0, z: -10, y: -1.1 },
});
const machine1 = createClone(machine, {
position: { x: 0, y: -5, z: 10 },
});
// ...
}
// ...
})();
</script>
</body>
</html>
减少 animate 内容
之前我们的 animate 方法中有如下内容:
// index.html
function animate() {
// 管道运动,路线循环流动效果
group1Animate();
group2Animate();
commonAnimate();
normalSceneAnimate();
// fps监控
stats.update();
// 实现局部辉光
scene.traverse(darkenNonBloomed);
bloomComposer.render();
scene.traverse(restoreMaterial);
finalComposer.render();
requestAnimationFrame(animate);
}
这里我们发现,实现局部辉光的过程,如果我们不转动控制器,就不需要每次 requestAnimationFrame 的时候都调用 bloomComposer.render(),所以对入口文件 index.html 优化如下
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<div id="canvas-frame"></div>
<script>...</script>
<script>
// ...
// 产生局部辉光的前三步,初始状态必须先调用 bloomComposer.render()
scene.traverse(darkenNonBloomed);
bloomComposer.render();
scene.traverse(restoreMaterial);
controls.addEventListener("change", function () {
// 产生局部辉光的前三步,操作控制器的时候重新调用 bloomComposer.render() 更新辉光
scene.traverse(darkenNonBloomed);
bloomComposer.render();
scene.traverse(restoreMaterial);
});
function animate() {
// 管道运动,路线循环流动效果
group1Animate();
group2Animate();
commonAnimate();
normalSceneAnimate();
// fps监控
stats.update();
// 实现局部辉光
finalComposer.render(); // 因为有动画的存在,每帧依然要执行渲染函数 render,这里不能省
requestAnimationFrame(animate);
}
</script>
</body>
</html>
除此之外,在每帧动画中,我们也应该减少更新,这里的更新指的是当前的几何体、材质、纹理等发生了修改,需要Three.js重新更新显存的数据,具体包括:
- 几何体:
geometry.verticesNeedUpdate = true; //顶点发生了修改
geometry.elementsNeedUpdate = true; //面发生了修改
geometry.morphTargetsNeedUpdate = true; //变形目标发生了修改
geometry.uvsNeedUpdate = true; //uv映射发生了修改
geometry.normalsNeedUpdate = true; //法向发生了修改
geometry.colorsNeedUpdate = true; //顶点颜色发生的修改
- 材质
material.needsUpdate = true
- 纹理
texture.needsUpdate = true;
如果它们发生更新,则将其设置为true,Three.js会通过判断,将数据重新传输到显存当中,并将配置项重新修改为false。此外,我们可以利用函数的节流防抖,对 animate 函数进行优化
引入模型优化
通常项目中,需要引入第三方软件建模后的模型,例如这个Demo中也引入了电脑的模型,模型文件通常很大,如何压缩模型文件,加快引入时间也是优化的重要一环
- 利用 obj2gltf 插件把 obj 格式的模型转成 gltf 格式,用法如下:
obj2gltf -i ./xxx.obj -o ./xxx.gltf --unlit --separate --unlit 表示保留环境贴图的效果 --separate 表示将贴图文件提取出来,浏览器可以缓存,如果你需要继续压缩gltf文件,这里可以不加,因为后续压缩的时候也能提出来
- 利用 gltf-pipeline 插件把 gltf 格式的模型进行压缩,用法如下:
gltf-pipeline -i ./xxx.gltf -o ./xxx.gltf -d --separate -d是--draco.compressMeshes的缩写,使用draco算法压缩模型 --separate就是将贴图文件提取出来,不提可以不加
引入字体优化
之前我们创建3D文字的时候,是利用官方推荐的 facetype.js 将 ttf 格式的字体转成 json 格式后引入的,不过实际运用时发现,转成 json文件后,文件大了很多倍,导致加载时间非常长

首先在 index.html 引入所需文件
<script src="lib/opentype.min.js"></script>
<script src="lib/TTFLoader.js"></script>
加载字体
new THREE.TTFLoader().load("../font/simhei.ttf", function (data) {
const font = new THREE.Font(data);
const geometry = new THREE.TextBufferGeometry(text, {
font,
size: 3,
height: 1,
curveSegments: 64,
});
// ...
});
改用 TTFLoader 后,加载速度有了明显的提升

减少请求次数
由于需要引入额外的很多 js 文件,导致请求次数特别多,因此我们需要打包工具 webpack 帮助我们打包压缩(为了利用 webpack 打包,我们需要将原项目重构成 CommonJS 规范,详情见下文)
webpack 配置
// webpack.config.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
entry: "./main.js",
output: {
path: path.resolve(__dirname, "./dist")
}
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
favicon: path.resolve("./public/favicon.ico"),
}),
new webpack.HotModuleReplacementPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: 'src/assets/',
to: 'assets/'
},
],
})
],
devServer: {
open: true,
port: "8080",
hot: true,
hotOnly: true,
}
};
重构为 CommonJS 规范
目录结构:
├─ node_modules
├─ public // 类似 vue 的 public 目录
| ├─ favicon.ico // 网站图标
| └─ index.html // 模板html
├─ src // 源文件
| ├─ assets // 静态资源
| | ├─ font // 字体文件夹
| | | └─ ...
| | ├─ img // 图片文件夹
| | | └─ ...
| | ├─ model // 模型文件夹
| | | └─ ...
| ├─ composer
| | └─ composer.js // 后期处理 EffectComposer
| ├─ group
| | ├─ groupCommon.js // 分组间公用的内容
| | ├─ normalSceneGroup.js // 分组一
| | ├─ sceneGroup1.js // 分组二
| | └─ sceneGroup2.js // 分组三
| ├─ js // 存放自己编写的 js 文件
| | ├─ common.js // 全局变量,注入在 Gvo 类的属性中
| | ├─ create.js // 创建各种几何体物体
| | └─ util.js // 工具函数
| ├─ lib // 存放各种需要的引入的文件
| | ├─ RenderPass.js
| | ├─ three.module.js
| | ├─ tween.cjs.js
| | └─ ...
| └─ gvo.js // Gvo 类
├─ main.js // 入口文件
├─ package.json
└─ webpack.config.js // webpack配置
首先我们在 gvo.js 创建一个类 Gvo,将之前的 init_fn 中的方法写在 Gvo 中,并在 constructor 构造方法里执行初始化
// gvo.js
class Gvo {
constructor(selector, ThreeOption, lightOption) {
this.initThree(selector, ThreeOption); // 默认自动初始化
this.initLight(lightOption); // 默认自动初始化
this.customInit(); // 用户自定义初始化
this.initControls(); // 默认自动初始化
this.initStats(); // 默认自动初始化
}
// 用户自定义初始化
customInit() { ... }
// 初始化三大件:场景、相机、渲染器
initThree(selector, { cameraOption, rendererOption } = {}) { ... }
// 设置灯光
initLight(lightOption = 0xffffff) { ... }
// 添加控制器
initControls() { ... }
// 添加fps
initStats() { ... }
}
再将之前的 create_fn 中的一系列create方法绑定在 Gvo 类的方法上
// create.js
function createClone() { ... }
function createImportModel() { ... }
function createEarth() { ... }
function createMachine() { ... }
...
Gvo.prototype.createClone = createClone;
Gvo.prototype.createImportModel = createImportModel;
Gvo.prototype.createEarth = createEarth;
Gvo.prototype.createMachine = createMachine;
...
然后将之前的 util_fn 中的方法 export 导出,在 composer_fn 中加入 darkenNonBloomed 和 restoreMaterial 方法(这两个方法是直接写在入口文件 index.html 中的),连同 createComposer 方法一并导出
// composer.js
function createComposer() { ... }
...
function darkenNonBloomed() { ... }
function restoreMaterial() { ... }
module.exports = { createComposer, darkenNonBloomed, restoreMaterial };
// util.js
function mergeImage() { ... }
function computeUV() { ... }
module.exports = { mergeImage, computeUV }
接着新建一个group文件夹,在里面写上所有分组的逻辑,这里举例 groupCommon.js 和 sceneGroup1.js
// groupCommon.js
// scene场景的公共内容
module.exports = (gvonte) => {
const earth = gvonte.createEarth({ position: { x: -15, y: -1 } });
const machine = gvonte.createMachine("./assets/img/move.png", { position: { x: 15, z: -20, y: -5 } });
gvonte.groupCommon = { earth, machine };
};
// sceneGroup1.js
// scene场景的第一组内容
module.exports = async (gvonte) => {
const { earth, machine } = gvonte.groupCommon;
const earth1 = gvonte.createClone(earth, { position: { x: 15, y: -1 } });
const machine1 = gvonte.createClone(machine, {
position: { x: -15 },
});
const machine2 = gvonte.createClone(machine, {
rotation: { x: Math.PI / 2 },
position: { x: -15, y: -1, z: 15 },
});
// ...
const group1 = gvonte.createGroup(
machine,
machine1,
earth,
earth1,
machine2,
// ...
);
group1.position.x = -60;
const group1Animate = function () { ... };
return { group1, group1Animate }
};
最后创建入口文件 main.js
// main.js
const gvonte = new Gvo("#canvas-frame", {
rendererOption: {
alpha: true,
antialias: true
}
}); // 新建 Gvo 类的实例
groupCommonFn(gvonte); // 初始化分组间公用的部分
(async function () {
// 创建分组
const { group1, group1Animate } = await sceneGroup1Fn(gvonte); // 新建第一个分组
const { group2, group2Animate } = await sceneGroup2Fn(gvonte); // 新建第二个分组
const { normalSceneGroup, normalSceneGroupAnimate } = await normalSceneGroupFn(gvonte); // 新建第三个分组
gvonte.scene.add(group1);
gvonte.scene.add(group2);
gvonte.normalScene.add(normalSceneGroup);
// 后期处理
const { bloomComposer, finalComposer } = createComposer(gvonte);
document.body.removeChild(document.querySelector("#label1"));
document.body.removeChild(document.querySelector("#label2"));
// 实现局部辉光的准备工作
const bloomLayer = gvonte.createLayer(1);
const materials = {};
// 产生局部辉光的前三步,初始状态必须先调用 bloomComposer.render()
function readyToBloom() {
gvonte.scene.traverse(darkenNonBloomed(bloomLayer, materials, Gvo.BlackBasicMaterial));
bloomComposer.render();
gvonte.scene.traverse(restoreMaterial(materials));
}
readyToBloom();
gvonte.controls.addEventListener("change", readyToBloom);
function animate() {
// 管道运动,路线循环流动效果
group1Animate();
group2Animate();
normalSceneGroupAnimate();
// fps监控
gvonte.stats.update();
// gvonte.renderer.render(gvonte.scene, gvonte.camera);
finalComposer.render();
requestAnimationFrame(animate);
}
animate();
})();