【Three.js】知识梳理二十一:Three.js性能优化和实践建议

news2025/1/15 6:43:10

Three.js 是一个功能强大的 3D 引擎,用于创建 WebGL 应用。尽管它功能强大,但在复杂的 3D 场景中保持高性能是一个挑战。本文将分享一些在使用 Three.js 时的性能优化提示,帮助你提高应用的运行效率。

1. 使用 stats.js 监视性能

在进行任何优化之前,首先要监视应用的性能。stats.js 是一个简单而有效的工具,可以帮助你实时监视帧率(FPS)、每帧渲染所需时间(MS)和内存使用情况(MB)。

github: GitHub - mrdoob/stats.js: JavaScript Performance Monitor

安装和使用 stats.js

首先,通过 npm 安装 stats.js

npm install --save stats.js

然后,可以在 Three.js 项目中使用它:

import Stats from 'stats.js';
​
const stats = new Stats();
stats.showPanel(0); // 显示面板 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(stats.dom);
​
const tick = () => {
  stats.begin();
  // 监视的代码放在这里
  stats.end();
  requestAnimationFrame(tick);
};
​
requestAnimationFrame(tick);

FPS:在最后一秒内渲染的帧数。数值越高越好。

MS:渲染一帧所需的毫秒数。数值越低越好。

MB:分配的内存大小(以兆字节为单位)。需要在 Chrome 中使用 --enable-precise-memory-info 启动。

CUSTOM:用户自定义面板支持。

image.png

2. 优化几何体和材质

复杂的几何体和高分辨率的材质会显著影响渲染性能。以下是一些优化建议:

降低几何体细节

使用 THREE.LOD(Level of Detail)类来根据摄像机距离动态切换几何体细节。

import * as THREE from 'three';
​
// 创建场景和相机
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 10;
​
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
  antialias: true,
  powerPreference: 'high-performance'
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
​
// 创建不同细节级别的几何体
const highDetailGeometry = new THREE.BoxGeometry(1, 1, 1, 32, 32, 32);
const mediumDetailGeometry = new THREE.BoxGeometry(1, 1, 1, 16, 16, 16);
const lowDetailGeometry = new THREE.BoxGeometry(1, 1, 1, 8, 8, 8);
​
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
​
// 使用 LOD 动态切换几何体
const lod = new THREE.LOD();
lod.addLevel(new THREE.Mesh(highDetailGeometry, material), 0);
lod.addLevel(new THREE.Mesh(mediumDetailGeometry, material), 5);
lod.addLevel(new THREE.Mesh(lowDetailGeometry, material), 10);
scene.add(lod);
​
// 动画循环
const animate = function () {
  requestAnimationFrame(animate);
​
  // 旋转 LOD
  lod.rotation.x += 0.01;
  lod.rotation.y += 0.01;
​
  // 更新摄像机位置
  camera.position.x = Math.sin(Date.now() * 0.001) * 20;
  camera.position.z = Math.cos(Date.now() * 0.001) * 20;
  camera.lookAt(scene.position);
​
  // 渲染场景和相机
  renderer.render(scene, camera);
};
​
animate();

使用压缩纹理

使用压缩纹理格式(如 DDS、KTX2)来减少内存占用和加载时间。这里以 KTX2 为例。

首先,安装 three/examples/jsm/loaders/KTX2Loader.jsBasisu 解码器,然后,在你的项目中使用 KTX2Loader 加载压缩纹理:

import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
import { MeshStandardMaterial } from 'three';
​
// 创建 KTX2Loader
const ktx2Loader = new KTX2Loader()
  .setTranscoderPath('path/to/basisu/transcoder/') // 设置 Basisu 解码器路径
  .detectSupport(renderer);
​
// 加载 KTX2 压缩纹理
ktx2Loader.load('path/to/texture.ktx2', (texture) => {
  const material = new MeshStandardMaterial({ map: texture });
​
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  const mesh = new THREE.Mesh(geometry, material);
  scene.add(mesh);
});

合并几何体

将多个几何体合并为一个几何体,以减少绘制调用(draw call)的次数。使用 BufferGeometryUtils 合并几何体。

import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
​
// 创建多个几何体
const geometries = [];
for (let i = 0; i < 50; i++) {
  const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
​
  geometry.translate(
    (Math.random() - 0.5) * 10,
    (Math.random() - 0.5) * 10,
    (Math.random() - 0.5) * 10
  );
​
  geometries.push(geometry);
}
​
// 合并几何体
const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mesh);
​
// 动画循环
const animate = function () {
  requestAnimationFrame(animate);
​
  // 旋转合并后的几何体
  mesh.rotation.x += 0.01;
  mesh.rotation.y += 0.01;
​
  // 渲染场景和相机
  renderer.render(scene, camera);
};
​
animate();

3. 优化灯光和阴影

灯光和阴影计算开销较大,特别是多光源和动态阴影。以下是一些优化建议:

  • 减少光源数量:尽量减少场景中的光源数量,选择性能开销较小的光源如 AmbientLight 和 DirectionalLight。
  • 优化阴影贴图:降低阴影贴图的分辨率,并限制阴影相机的视野范围,以减少计算开销。
// 优化阴影贴图
directionalLight.shadow.mapSize.width = 1024; // 默认值是 512
directionalLight.shadow.mapSize.height = 1024; // 默认值是 512
​
// 限制阴影相机的视野范围
directionalLight.shadow.camera.top = 3;
directionalLight.shadow.camera.right = 6;
directionalLight.shadow.camera.left = -6;
directionalLight.shadow.camera.bottom = -3;
directionalLight.shadow.camera.near = 0.1;
directionalLight.shadow.camera.far = 10;
​
// 可选:使用相机助手查看阴影相机的范围
const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(cameraHelper);
  • 静态光照贴图:对于静态场景,可以预先计算光照和阴影,生成光照贴图。这里我们使用 Lightmap,一个 Three.js 的扩展,可以帮助实现静态光照贴图。

首先,安装 three-lightmap

npm install three-lightmap

然后,在你的项目中使用 three-lightmap 来生成静态光照贴图:

import { Lightmap } from 'three-lightmap';
​
// 创建静态光照贴图
const lightmap = new Lightmap(scene, renderer, {
  mapSize: 1024,
  samples: 4,
  bake: true,
  exposure: 0.7,
  softEdges: 0.01,
  aoOnly: false,
  aoStrength: 0.6
});
​
// 在几何体上启用静态光照贴图
cube.material.lightMap = lightmap.generate(cube.geometry);
plane.material.lightMap = lightmap.generate(plane.geometry);
​
// 运行一次性光照贴图烘焙过程
lightmap.bake();

4. 纹理贴图

纹理贴图非常消耗 GPU 内存,以下是一些优化建议:

  • 调整尺寸:调整纹理贴图的分辨率可以通过图像编辑工具(如 Photoshop、GIMP)或编程工具(如 Sharp for Node.js)来实现。在加载纹理时,可以使用 Three.js 内置的 THREE.TextureLoader 来加载已经调整好尺寸的纹理。
  • 使用正确格式:确保使用合适的文件格式(如 .jpg 或 .png)。可以使用在线工具如 TinyPNG 来压缩纹理文件,减小文件大小,同时保持较高的视觉质量。
  • 保持分辨率为 2 的幂次方:确保纹理尺寸为 2 的幂次方(如 256x256, 512x512, 1024x1024)。如果纹理的尺寸不是 2 的幂次方,Three.js 会自动调整它们,但这会影响性能。

5. 使用对象池

在动画或游戏应用中,经常需要频繁创建和销毁对象。使用对象池可以有效减少内存分配和垃圾回收频繁的开销。

class ObjectPool {
  constructor(createFunc, size) {
    this.createFunc = createFunc;
    this.pool = [];
    for (let i = 0; i < size; i++) {
      this.pool.push(this.createFunc());
    }
  }
​
  get() {
    return this.pool.length ? this.pool.pop() : this.createFunc();
  }
​
  release(obj) {
    this.pool.push(obj);
  }
}

6. 渲染器优化

以下是一些针对渲染器的优化建议:

  • 限制像素比:一些设备有非常高的像素比,但渲染的像素越多,消耗的性能越大。将渲染器的像素比限制为 2:

    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    
  • 配置偏好:指定 powerPreference 属性来提示用户代理适当的 GPU 配置:

    const renderer = new THREE.WebGLRenderer({ powerPreference: 'high-performance' });
    
  • 抗锯齿:只有在有明显锯齿且不会显著影响性能时才启用抗锯齿。

    // 创建渲染器时启用抗锯齿
    const renderer = new THREE.WebGLRenderer({
      canvas: document.querySelector('#canvas'),
      antialias: true, // 启用抗锯齿
      powerPreference: 'high-performance' // 提示浏览器选择高性能的 GPU
    });
    

7. 相机优化

通过缩小相机的视野范围(FOV)以及调整相机的 nearfar 属性,可以显著减少渲染的对象数量,从而提高渲染性能。下面是具体的实现代码和逻辑说明。

缩小相机的视野范围

通过减少相机的视野角度(FOV),可以让屏幕中显示的对象更少,从而减少需要渲染的三角形数量。

调整相机的近端面和远端面

调整相机的 nearfar 属性,可以确保只渲染特定范围内的对象,避免渲染不必要的远距离对象。

// 调整相机的视野角度和近端面、远端面
const fov = 50; // 缩小视野角度(默认值通常为75)
const aspect = window.innerWidth / window.innerHeight;
const near = 1; // 将 near 属性从 0.1 增大到 1
const far = 50; // 将 far 属性从 100 缩小到 50
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 10;

8. 清除不必要的对象

当场景中不再需要某个对象时,及时清除它:

// 创建示例对象
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
​
// 在某个时刻移除对象
function removeObject(object) {
  // 移除对象
  scene.remove(object);
  // 释放几何体资源
  if (object.geometry) {
    object.geometry.dispose();
  }
  // 释放材质资源
  if (object.material) {
    if (Array.isArray(object.material)) {
      // 如果材质是数组,遍历并释放每个材质
      object.material.forEach((material) => {
        material.dispose();
      });
    } else {
      // 单一材质,直接释放
      object.material.dispose();
    }
  }
  // 释放纹理资源
  if (object.material.map) {
    object.material.map.dispose();
  }
}
// 在某个时刻调用函数移除对象
removeObject(cube);

9. 后期处理和着色器优化

限制后期处理通道

每个后期处理过程都会增加渲染负担,尽量减少不必要的后期处理步骤。

着色器优化

  • 指定精度:强制材质中着色器的精度:

    const shaderMaterial = new THREE.ShaderMaterial({ precision: 'lowp' });
    

  • 保持代码简单:尽量保持着色器代码简单,避免复杂的逻辑和多层嵌套。

  • 使用贴图纹理:尽量使用纹理来代替复杂的计算,例如噪声生成。

  • 使用 defines:对于不会改变的值,使用 defines 而不是 uniform

    const shaderMaterial = new THREE.ShaderMaterial({
      defines: { uDisplacementStrength: 1.5 },
    });
    

性能优化是一个持续的过程,需要根据具体的应用场景进行调整。以上提示可以帮助你在使用 Three.js 构建 3D 应用时提高性能。希望本文对你有所帮助!

附送250套精选项目源码

源码截图

 源码获取:关注公众号「码农园区」,回复 【源码】,即可获取全套源码下载链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1819634.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

苦日子开始了,普通人应该怎么做?

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 以为疫情后&#xff0c;我们的收入会好起来&#xff0c;谁曾想连工作都快保不住了&#xff0c;这几年大家日子过的比较苦&#xff0c;很多人想多一份收入。 面对这种情况&#xff0c;我们普通人应该怎么办?如何多…

【秋招突围】2024届秋招笔试-阿里系列笔试题-第一套-三语言题解(Java/Cpp/Python)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系计划跟新各公司春秋招的笔试题 &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f497; &#x1f4e7; 清隆这边…

C++ 27 之 初始化列表

c27初始化列表.cpp #include <iostream> #include <string.h> using namespace std;class Students06{ public:int s_a;int s_b;int s_c;Students06(int a, int b, int c){s_a a;s_b b;s_c c;}// 初始化列表写法1&#xff1a;// Students06():s_a(4),s_b(5),s_…

使用QT绘制简单的动态数据折线图

两个核心类时QChart和QLineSeries 下面这个示例代码中&#xff0c;定时器每隔一段时间将曲线图中的数据点向右移动 一个单位&#xff0c;同时调整横坐标轴的范围&#xff0c;实现了一次滚动对应移动一个数据点的效果。 QLineSeries最多容纳40961024个点 #include <QtWidg…

【RabbitMQ】初识 RabbitMQ

初识 RabbitMQ 1.认识 RabbitMQ1.1 介绍1. 2.使用场景1.2.1 推送通知1.2.2 异步任务1.2.3 多平台应用的通信1.2.4 消息延迟1.2.5 远程过程调用 1.3 特性 2.基本概念2.1 生产者、消费者和代理2.2 消息队列2.3 交换机2.3.1 direct2.3.2 topic2.3.3 headers2.3.4 fanout 2.4 绑定2…

Downie for Mac v4.7.17 在线视频下载软件 安装(简单易学,小白轻松搞定)

Mac分享吧 文章目录 效果一、准备工作二、开始安装1、双击运行软件&#xff0c;将其从左侧拖入右侧文件夹中&#xff0c;等待安装完毕2、应用程序显示软件图标&#xff0c;表示安装成功 三、运行测试1、打开软件&#xff0c;进行设置2、下载视频&#xff0c;测试3、根据需要选…

关于unbuntu的终端自动退出的解决方案

输入sudo vim /etc/profile 将下面的TMOUT的时间注释掉 source /etc/profile使得更改生效

打破数据分析壁垒:SPSS复习必备(一)

一、数据录入与数据获取 1.变量的测量尺度 &#xff08;1&#xff09;定类尺度 顾名思义&#xff0c;是对事物的类别或属性的一种测度&#xff0c;按照事物的某种属性对其进行分类或分组。 该类变量只能计算频数和频率&#xff0c;用表示 &#xff08;2&#xff09;定序尺…

人生感悟 | 我们为什么贫穷?

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 我们为什么贫穷&#xff1f; 因为我们都在局中并坦然的按着规则制定者循规蹈矩的生活。 01 负债导致贫穷 最近同事买房&#xff0c;总价一百多万&#xff0c;月供四千多&#xff0c;讲话&#xff1a;已入坑&#xff0…

python反序列化知识点学习

最近遇到了python反序列化的题目&#xff0c;简单学习一下相关的知识点 基础知识 Python 的序列化指的是将 Python 对象转换为一种格式&#xff0c;以便可以将其存储在文件或通过网络传输。Python 中最常用的序列化模块是 pickle 模块。 序列化使用的是pickle.dumps方法&…

qt仿制qq登录界面

#include "mainwindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent) {// 设置窗口大小this->resize(window_width, window_heigth);// 固定窗口大小this->setFixedSize(window_width, window_heigth);// 设置窗口图标this->se…

JavaScript-数组

学习目标&#xff1a; 掌握数组 学习内容&#xff1a; 数组是什么数组的基本使用练习操作数组 数组是什么&#xff1a; 数组Array 是一种可以按顺序保存数据的数据类型。场景&#xff1a;如果有多个数据可以用数组保存起来&#xff0c;然后放到一个变量中&#xff0c;管理非常…

SQL 表连接(表关联)

目录 一、INNER JOIN&#xff08;内连接,等值连接&#xff09; 二、LEFT JOIN&#xff08;左连接&#xff09; 三、RIGHT JOIN&#xff08;右连接&#xff09;&#xff1a; 一、INNER JOIN&#xff08;内连接,等值连接&#xff09; 用途&#xff1a;获取两个表中字段能匹配上…

返回值返回引用返回指针之间的区别

一、返回值 当函数返回一个值时&#xff0c;实际返回的是一个变量的拷贝。 优点&#xff1a; 简单易用&#xff1b;安全&#xff0c;不会导致悬挂指针或悬挂引用&#xff1b; 缺点&#xff1a; 当返回值是一个较大的对象时会产生拷贝开销&#xff0c;影响程序性能&#xf…

TcpClient 服务器、客户端连接

TcpClient 服务器 TcpListener 搭建tcp服务器的类&#xff0c;基于socket套接字通信的 1 创建服务器对象 TcpListener server new TcpListener(IPAddress.Parse("127.0.0.1"), 3000); 2 开启服务器 设置最大连接数 server.Start(1000); 3 接收客户端的链接,只能…

git如果将多次提交压缩成一次

将N个提交压缩到单个提交中有两种方式&#xff1a; git reset git reset的本意是版本回退&#xff0c;回退时可以选择保留commit提交。我们基于git reset的作用&#xff0c;结合新建分支&#xff0c;可以实现多次commit提交的合并。这个不需要vim编辑&#xff0c;很少有冲突。…

C语言| 编程获取数组的长度

用sizeof也可以获得整个数组在内存中所占的字节数。 总的字节数除以一个元素所占的字节数就是数组的总长度。 这样不管数组是增加还是减少元素&#xff0c;sizeof(a) /sizeof(a[0])都能自动求出数组的长度。 字符串中有一个strlen()函数可以求出字符数组中字符串的长度。 #inc…

Linux结业测试题,旨在检测ip网络配置,文件权限等基础

Linux期末结业考试 一、评分方式&#xff08;总分100分&#xff0c;理论40分在职教云考试&#xff09; 主要涉及的知识和技能点*分值权重*Linux的最小安装10%激活网络&#xff0c;并正确设置ip地址10%克隆1台机器&#xff0c;并正确设置ip地址10%SSH免密互信服务10%文件和目录…

硬件相关——硬盘分区

文章目录 系统分区什么是分区&我们为什么要用分区逻辑分区分区规则为什么主分区最多只能分4个&#xff1f;硬盘的结构扩展分区 格式化啥叫格式化为什么我们需要格式化索引呢&#xff1f; 系统分区 什么是分区&我们为什么要用分区 磁盘分区是指&#xff0c;使用分区编辑…

论文笔记:ATime-Aware Trajectory Embedding Model for Next-Location Recommendation

Knowledge and Information Systems, 2018 1 intro 1.1 背景 随着基于位置的社交网络&#xff08;LBSNs&#xff09;&#xff0c;如Foursquare和Facebook Places的日益流行&#xff0c;大量用户签到数据变得可用 这些大量签到数据的可用性带来了许多有用的应用&#xff0c;以…