跳到主要内容

three.js 提示和技巧的大全!

初学者!为什么我什么都看不到?

您已经遵循了几个基本教程,一切都很顺利。现在,您正在创建自己的应用程序,并且已经完全按照教程中所述设置了所有内容。但你就是什么也看不到!什么?? 您可以采取以下措施来帮助找出问题。

  1. 检查浏览器控制台是否有错误消息

但你已经这样做了,对吧?

  1. 将背景颜色设置为黑色以外的颜色

盯着黑色画布?如果你能看到的只是黑色,就很难判断是否发生了什么事情。尝试将背景颜色设置为红色:

import { Color } from "./vendor/three/build/three.module.js";
scene.background = new Color("red");

如果你得到一个红色的画布,那么至少你的 renderer.render 调用是有效的,你可以继续找出还有什么问题。

  1. 确保场景中有光源,并且它正在照亮您的对象

就像在现实世界中一样,three.js 中的大多数材质都需要光线才能看到。

  1. 使用 MeshBasicMaterial# 覆盖场景中的所有材质

一种不需要光线可见的材质是 MeshBasicMaterial。如果您在显示对象时遇到问题,您可以使用 MeshBasicMaterial 基础网格材质 临时覆盖场景中的所有材质。如果在执行此操作时对象神奇地出现,那么您的问题是缺乏光线。

import { MeshBasicMaterial } from "./vendor/three/build/three.module.js";
scene.overrideMaterial = new MeshBasicMaterial({ color: "green" });
  1. 您的物体是否在摄像机的 视锥体 viewing frustum 范围内?

如果您的对象不在视锥体内,它将被裁剪。尝试将远裁剪平面做得非常大:

camera.far = 100000;
camera.updateProjectionMatrix();

请记住,这只是为了测试!摄像机的视锥体以米为单位,您应该使其尽可能小以获得最佳性能。设置场景并正常工作后,请尽可能减小视锥体的大小。

  1. 您的相机是否在物体内?

默认情况下,所有内容都在 点 (0,0,0) (也就是源) 创建。确保您已将摄像机向后移动,以便您可以看到您的场景!

camera.position.z = 10;
  1. 仔细考虑场景的规模

尝试可视化您的场景,并记住 three.js 中的 1 个单位是 1 米。一切都以合理的逻辑方式组合在一起吗?或者,也许您什么都看不到,因为您刚刚加载的对象只有 0.00001 米宽。等等,屏幕中间的那个小黑点是什么?

一般提示

  1. 在 JavaScript 中创建对象的成本很高,因此不要在循环中创建对象。相反,创建一个对象(例如 Vector3),并使用 vector.set() 或类似方法在循环中重用 that。

  2. 渲染循环也是如此。为了确保您的应用以每秒 60 帧的速度运行,请在渲染循环中尽可能少地执行工作。不要每帧都创建新对象。

  3. 始终使用 BufferGeometry 而不是 Geometry,这样更快。

  4. 预构建的对象也是如此,请始终使用缓冲区几何版本( BoxBufferGeometry 而不是 BoxGeometry)。

  5. 始终尝试重用对象,例如对象、材质、纹理等(尽管更新某些内容可能比创建新内容慢,请参阅下面的纹理提示)。

以 SI 单位工作

three.js is 到处都使用 SI 单位。如果您还使用 SI 单位,您会发现事情进展得更顺利。如果您出于某种原因(例如英寸(颤抖))确实使用了不同类型的单位,请确保您有充分的理由这样做。

SI Units# 国际单位

  • 距离以米为单位(1 three.js 单位 = 1 米)。
  • 时间以秒为单位。
  • 光线以 SI 光单位、坎德拉 (cd)、流明 (lm) 和勒克斯 (lx) 来衡量(至少只要您打开 renderer.physicallyCorrectLights )。

如果您正在创建真正史诗般的规模(空间模拟和类似的东西),请使用缩放因子或切换到使用对数深度缓冲区。

准确的颜色

对于(几乎)准确的颜色,请对渲染器使用以下设置:

renderer.gammaFactor = 2.2;
renderer.outputEncoding = THREE.sRGBEncoding;

For colors do this: 对于颜色,请执行以下操作:

const color = new Color(0x800080);
color.convertSRGBToLinear();

或者,在材质中使用颜色的更常见情况下:

const material = new MeshBasicMaterial({ color: 0x800080 });
material.color.convertSRGBToLinear();

最后,要在纹理中获得(几乎)正确的颜色,您只需为颜色、环境和自发光贴图设置纹理编码:

import { sRGBEncoding } from "./vendor/three/build/three.module.js";

const colorMap = new TextureLoader().load("colorMap.jpg");
colorMap.encoding = sRGBEncoding;

所有其他纹理类型应保留在线性颜色空间中。这是默认设置,因此您无需更改除颜色、环境和自发光贴图之外的任何纹理的编码。

请注意,我在这里说的几乎是正确的 three.js 因为目前色彩管理并不完全正确。希望它能很快得到修复,但与此同时,颜色的任何不准确都将非常小,除非您正在进行科学或医学渲染,否则不太可能有人注意到。

不要假设你知道什么会更快

Web 浏览器使用的 JavaScript 引擎经常更改,并在后台对您的代码进行了大量优化。不要相信你关于什么会更快,总是测试。不要听几年前的文章告诉你要避免使用某些方法,比如 array.map 或 array.forEach。自己测试这些,或者查找过去几个月的文章进行适当的测试。

使用样式指南和格式化代码

就个人而言,我结合使用了 EslintPrettyAirbnb 风格指南。我花了大约 30 分钟使用本教程(第 2 部分)在 VSCode 中进行设置,现在我再也不用浪费时间进行格式化、linting 或想知道特定语法是否是一个好主意了。

许多使用 three.js 的人更喜欢 Mr.doob’s Code Style™ ™ 而不是 Airbnb,所以如果你更喜欢使用它,只需将 eslint-config-Airbnb 插件替换为 eslint-config-mdcs

模型、网格和其他可见事物

  1. 避免使用常见的基于文本的 3D 数据格式(如 Wavefront OBJ 或 COLLADA)进行资产交付。相反,请使用针对 Web 优化的格式,例如 glTF。
  2. 将 Draco 网格压缩与 glTF 一起使用。有时这会将 glTF 文件缩小到其原始大小的 10% 以下!
  3. 或者,块上有一个名为 gltfpack 的新孩子,在某些情况下它可能比 Draco 提供更好的结果。
  4. 如果需要使大型对象组可见和不可见(或在场景中添加或删除它们),请考虑使用 Layers 以获得最佳性能。
  5. 位于相同位置的物体会导致闪烁(Z 冲突)。尝试将事物偏移一点,例如 0.001,使它们看起来处于同一位置,同时保持 GPU 满意。
  6. 使场景以原点为中心,以减少大坐标处的浮点错误。
  7. 切勿移动 Scene。它是在 中创建 (0,0,0) 的,这是其中所有对象的默认参考系。

Camera 相机

  1. 使 frustum 尽可能小以获得更好的性能。在开发中使用较大的 frustum 是可以的,但是在微调应用程序以进行部署后,请尽可能减小 frustum 以获得一些重要的 FPS。
  2. 不要在远裁剪平面上正确放置(特别是如果您的远裁剪平面真的很大),因为这可能会导致闪烁。

Renderer 渲染器

  1. 除非需要,否则不要启用 preserveDrawingBuffer
  2. 除非需要,否则请禁用 alpha 缓冲区。
  3. 除非需要,否则不要启用模具缓冲区。
  4. 除非需要,否则请禁用深度缓冲区(但您可能确实需要它)。
  5. 在创建渲染器时使用 powerPreference: "high-performance" 。 这可能会鼓励用户的系统在多 GPU 系统中选择高性能 GPU。
  6. 仅考虑在摄像机位置因 epsilon 而更改或发生动画时进行渲染。
  7. 如果您的场景是静态的并使用 OrbitControls,则可以侦听控件的 change 事件。这样,您只能在摄像机移动时渲染场景:
OrbitControls.addEventListener("change", () => renderer.render(scene, camera));

您不会从最后两个中获得更高的帧速率,但您将获得更少的风扇打开,以及更少的移动设备电池消耗。

注意:我在网上看到一些地方建议您禁用抗锯齿并改为应用后处理 AA 通道。在我的测试中,这不是真的。在现代硬件上,即使在低功耗移动设备上,内置 MSAA 似乎也非常便宜,而后处理 FXAA 或 SMAA 通道在我测试的每个场景中都会导致帧速率大幅下降,并且质量也低于 MSAA。

Lights 灯光

  1. 直接光源(SpotLight、PointLight、RectAreaLight 和 DirectionalLight)速度较慢。在场景中使用尽可能少的直接光。
  2. 避免在场景中添加和删除灯光,因为这需要 WebGLRenderer 重新编译所有着色器程序(它会缓存程序,因此以后执行此操作时,它将比第一次更快)。请改用 light.visible = false 或 light.intensiy = 0。
  3. 启用 renderer.physicallyCorrectLights 此选项可使用 SI 单位进行精确照明。

Shadows 阴影

  1. 如果场景是静态的,则仅在发生更改时更新阴影贴图,而不是每帧更新阴影贴图。
  2. 使用 CameraHelper 可视化阴影摄像机的视锥体。
  3. 使阴影视锥体尽可能小。
  4. 使阴影纹理的分辨率尽可能低。
  5. 请记住,点光源阴影比其他阴影类型消耗更多,因为它们必须渲染六次(每个方向一次),而 DirectionalLight 和 SpotLight 阴影只能渲染一次。
  6. 当我们讨论 PointLight 阴影的主题时,请注意,当用于可视化点光源阴影时,CameraHelper 只能可视化六个阴影方向中的一个。它仍然有用,但您需要对其他五个方向发挥您的想象力。

Materials 材料

  1. MeshLambertMaterial 不适用于有光泽的材质,但对于布料等无光泽材质,它将提供与 MeshPhongMaterial 非常相似的结果,但速度更快。
  2. 如果您正在使用变形目标,请确保在您的材质中设置 morphTargets = true,否则它们将不起作用!
  3. 变形法线 也是如此。
  4. 如果你将 蒙皮网格 SkinnedMesh 用于骨骼动画,请确保 material.skinning = true。
  5. 与变形目标、变形法线或蒙皮一起使用的材质不能共享。您需要为每个蒙皮或变形网格创建一个唯一的材质( material.clone() 克隆材质 更好 )。

Custom Materials 自定义材质

  1. 仅在制服发生变化时更新,而不是每一帧都更新。

Geometry 几何图形

  1. 避免使用 环线 LineLoop ,因为它必须由 Line Strip 模拟。

Textures 纹理

  1. 您的所有纹理都需要是 2 的幂 (POT) 大小: 1,2,4,8,16,…,512,2048,… 。
  2. 不要更改纹理的尺寸。相反,创建新的 Scal 会更快
  3. 使用尽可能小的纹理大小(您能摆脱 256x256 的平铺纹理吗?你可能会感到惊讶!
  4. 非 2 的幂 (NPOT) 纹理需要线性或最近的过滤,以及限制到边界或限制到边缘的包裹。不支持 Mipmap 筛选和重复换行。但说真的,不要使用 NPOT 纹理。
  5. 所有具有相同尺寸的纹理在内存中的大小相同,因此 JPG 的文件大小可能比 PNG 小,但它在 GPU 上占用的内存量相同。

Antialiasing 抗锯齿

抗锯齿的最坏情况是由许多彼此平行排列的薄直块组成的几何体。想想金属百叶窗或格子栅栏。如果可能的话,请不要在场景中包含这样的几何体。如果您别无选择,请考虑将晶格替换为纹理,因为这可能会产生更好的结果。

Post-Processing 后处理

  1. 内置的抗锯齿不适用于后处理(至少在 WebGL 1 中)。您需要使用 FXAASMAA 手动执行此操作(可能更快、更好)
  2. 由于您没有使用内置的 AA,因此请务必禁用它!
  3. three.js 有大量的后处理着色器,这真是太棒了!但请记住,每个通道都需要渲染整个场景。完成测试后,请考虑是否可以将通行证合并为一个自定义通行证。执行此操作需要多做一些工作,但可以带来相当大的性能提升。

Disposing of Things 处理物品

从场景中删除某些内容?

首先,考虑不要这样做,特别是如果您稍后会再次添加它。你可以使用 object.visible = false(也适用于灯光)或 material.opacity = 0 暂时隐藏对象。您可以设置 light.intensity = 0 以禁用光源,而不会导致着色器重新编译。

如果您确实需要从场景中永久删除内容,请先阅读此文章:如何处理对象? How to dispose of objects.

更新场景中的对象?

阅读此文章:如何更新内容

Performance# 性能

  1. 为静态或很少移动的对象设置 object.matrixAutoUpdate = false ,并在其位置/旋转/四元数/缩放更新时手动调用 object.updateMatrix()。
  2. 透明对象速度较慢。在场景中使用尽可能少的透明对象。
  3. 如果可能,请使用 AlphaTest 而不是 Standard Transparency,这样会更快。
  4. 在测试应用程序的性能时,您需要做的第一件事是检查它是 CPU 密集型还是 GPU 密集型。使用 scene.overrideMaterial 将所有材质替换为基本材质(请参阅初学者提示和页面开头)。如果性能提高,则您的应用程序受 GPU 限制。如果性能没有提高,则您的应用程序受 CPU 限制。
  5. 在快速机器上进行性能测试时,您可能会获得 60FPS 的最大帧速率。使用 chrome open -a "Google Chrome" --args --disable-gpu-vsync 以获得无限的帧速率。
  6. 现代移动设备具有高达 5 的高像素比 - 请考虑在这些设备上将最大像素比限制为 2 或 3。以场景的一些非常轻微的模糊为代价,您将获得相当大的性能提升。
  7. 烘焙照明贴图和阴影贴图以减少场景中的灯光数量。
  8. 密切关注场景中的 drawcall 数量。一个好的经验法则是更少的绘制调用 = 更好的性能。
  9. 远处的对象不需要与靠近摄像机的对象相同的细节级别。有许多技巧可用于通过降低远处对象的质量来提高性能。考虑使用 LOD (Level Of Detail) 对象。您也可以只为远处的物体每 2 帧或第 3 帧更新一次位置/动画,或者用公告牌替换它们 - 即对象的绘图。

Advanced Tips 高级提示

  1. 不要使用 TriangleFanDrawMode,它很慢。
  2. 当您有数百或数千个类似的几何体时,请使用几何体实例化。
  3. 在 GPU 而不是 CPU 上制作动画,尤其是在为顶点或粒子制作动画时(请参阅 Three.Bas 是执行此操作的一种方法)。

也请阅读这些页面!

Unity 和 Unreal 文档也有一些页面提供了许多性能建议,其中大多数都与 three.js 同样相关。还要阅读这些内容:

WebGL Insights 从整本书中收集了许多技巧。它更具技术性,但也值得一读,特别是如果您正在编写自己的着色器。

原文地址

https://discoverthreejs.com/tips-and-tricks/

React + TS + Threejs 的 demo

先看下我使用 react + Threejs 写的 demo

import React, { useEffect, useRef, useState } from "react";
import * as THREE from "three";

const ThreeDemo: React.FC = () => {
const [winSize, setWinSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
// 使用useRef获取canvas元素
const canvasRef = useRef < HTMLDivElement > null;
useEffect(() => {
if (canvasRef.current) {
// 1. 摆放好相机
const camera = new THREE.PerspectiveCamera(
70,
winSize.width / winSize.height,
0.01,
10
);
camera.position.z = 1;
// 2. 创建一个场景
const scene = new THREE.Scene();
// scene.background = new THREE.Color("red");
scene.overrideMaterial = new THREE.MeshBasicMaterial({ color: "green" });
// 3. 创建一个网格模型
const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const material = new THREE.MeshBasicMaterial({
color: 0xff0000, //0xff0000设置材质颜色为红色
});
const mesh = new THREE.Mesh(geometry, material);
// 设置网格模型在三维空间中的位置坐标,默认是坐标原点
// mesh.position.set(0, 0, 0);
scene.add(mesh);
// 4. 创建一个渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(winSize.width, winSize.height);
// 5. 将场景和相机添加到渲染器中, 可能很多人死在了这一步。
// renderer.render(scene, camera);
// 5.1 渲染动画
renderer.setAnimationLoop(animate);
canvasRef.current.innerHTML = "";
canvasRef.current.appendChild(renderer.domElement);
// 动画循环
function animate(time: number) {
mesh.rotation.x = time / 2000;
mesh.rotation.y = time / 1000;
renderer.render(scene, camera);
}
}
const handleResize = () => {
console.log("handleResize");
setWinSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [winSize.width, winSize.height]);

return (
<div>
<div style={{ width: "100vw", height: "100vh" }} ref={canvasRef}>
Demo002
</div>
</div>
);
};

export default ThreeDemo;