three.js开发技巧

564 阅读13分钟

基础知识

Three.js是一个 3D 库,使用 WebGL 绘制 3D,它试图尽可能轻松地在网页上获取 3D 内容。 WebGL 是一个非常底层的系统,只绘制点、线和三角形。使用 WebGL 做任何有用的事情通常需要相当多的代码, three.js 封装了场景、灯光、阴影、材质、纹理、3D 数学等,如果直接使用 WebGL所有这些东西都必须自己编写。

在开始之前,让我们尝试先来了解一下 three.js 应用程序的结构。Three.js 应用程序需要您创建一堆对象并将它们连接在一起。这是一个代表一个小的 three.js 应用程序的图表。

初学者友好提示,或帮助!为什么我什么都看不到?

当我们已经学习了几个基本教程,一切都很好。现在您正在创建自己的应用程序,并且您已经按照教程中的说明进行了所有设置。但是你什么都看不见!有吗??

可以采取以下措施来帮助您找出问题所在。

  1. 检查 浏览器控制台 ****是否有错误消息
  2. 将背景颜色设置为黑色以外的其他颜色

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

import * as THREE from "three";
const { Color } = THREE;
scene.background = new Color("red");

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

  1. 确保你的场景中有灯光并且它正在照亮你的对象

就像在现实世界中一样,three.js 中的大多数材质都需要光线才能看到。在确定光源正常的前提下,可以覆盖场景中的所有材质MeshBasicMaterial

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

import * as THREE from "three";
const { MeshBasicMaterial } = THREE;
scene.overrideMaterial = new MeshBasicMaterial({ color: "green" });
  1. 判断对象是否在相机的视锥内?

以下是透视相机的基本成像原理,其中位于近截面(near)和远截面(far)之间的物体才能被看到。近平面和远平面的高度由视野(fov)决定。两个平面的宽度由视野(fov)和纵横比(aspect)决定。

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

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

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

  1. 判断相机是否在物体里面?

默认情况下,所有内容都在此时创建( 0 ,0 ,0 ),又名起源。确保您已将相机移回,以便您可以看到您的场景!

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

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

一般提示

  1. 在 JavaScript 中创建对象很昂贵,所以不要在循环中创建对象。相反,创建单个对象,例如 Vector3并使用 vector.set()或类似方法在循环内重用该对象。
  2. 您的渲染循环也是如此。为确保您的应用程序以每秒 60 帧的流畅运行速度,请在渲染循环中尽可能少地工作。不要每帧都创建新对象。
  3. 始终使用 BufferGeometry而不是 Geometry,它更快。
  4. 预建对象也是如此,始终使用缓冲区几何版本( BoxBufferGeometry而不是 BoxGeometry)。
  5. 始终尝试重复使用对象、材质、纹理等对象(尽管更新某些内容可能比创建新内容要慢)。

准确的颜色

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

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

对于颜色,请执行以下操作:

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

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

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

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

import * as THREE from "three";
const { sRGBEncoding } = THREE;
const colorMap = new TextureLoader().load("colorMap.jpg");
colorMap.encoding = sRGBEncoding;

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

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

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

  1. 避免使用常见的基于文本的 3D 数据格式(例如 Wavefront OBJ 或 COLLADA)进行资产交付。相反,请使用针对 Web 优化的格式,例如 glTF。
  2. 使用带有 glTF 的 Draco 网格压缩。有时这会将 glTF 文件减少到其原始大小的 10% 以下!
  3. 或者,使用块上的 gltfpack,在某些情况下它可能比 Draco 提供更好的结果。
  4. 如果您需要使大量对象可见和不可见(或从场景中添加/删除它们),请考虑使用 图层以获得最佳性能。
  5. 位于完全相同位置的对象会导致闪烁(Z-fighting)。尝试将事物偏移 0.001 之类的微小量,以使事物看起来像它们处于相同位置,同时让您的 GPU 满意。
  6. 保持场景以原点为中心,以减少较大参考系的浮点错误。
  7. 永远不要移动你的Scene. 它创建于( 0 ,0 ,0 ),这是其中所有对象的默认参考框架。
  8. 用 gltf-transform 来转换网格、压缩、把纹理转换成 KTX2、减小文件尺寸。这种程序化的内容处理管线,意味着可以按需运行和重新处理 GLTF 资源,无论模型还是管线本身有了任何变更,都自动重新生成资源。

相机

  1. 使您的平截头体尽可能小以获得更好的性能。在开发中使用大截头锥体很好,但是一旦你为部署微调你的应用程序,让你的截头锥体尽可能小以获得一些重要的 FPS。
  2. 不要把东西放在远裁剪平面上(特别是如果你的远裁剪平面真的很大),因为这会导致闪烁。例如:圆形的球滚动到边缘,会变成椭圆,目前暂时没有技术手段可以优化这种现象。

渲染器

  1. preserveDrawingBuffer除非你需要,否则不要启用 。如果我们需要对场景进行截图,则需要开启这个参数。

  2. 除非您需要,否则禁用 alpha 缓冲区。

  3. 除非您需要,否则不要启用模板缓冲区。

  4. 除非您需要,否则禁用深度缓冲区(但您可能确实需要它),开启后性能很差。

  5. 创建渲染器时使用powerPreference: "high-performance"。这可能会鼓励用户的系统在多 GPU 系统中选择高性能 GPU。

  6. CPU 每次调用图形 API 的渲染函数,会将数据提交到 GPU 中计算渲染,称之为一次 draw call。

    1.   频繁的渲染状态的切换和跨单元数据传递会造成更大的开销。渲染大量重复的 mesh 时,利用 Instancing 可以将他们合并为一次 draw call。
  7. 仅在相机位置因 epsilon 变化或发生动画时才考虑渲染。

  8. 如果你的场景是静态的并且使用OrbitControls,你可以监听控件的change事件。这样,只需要在相机移动时渲染场景:

OrbitControls.addEventListener("change", () => renderer.render(scene, camera));

最后两个技巧不会使应用获得更高的帧速率,但会得到更少的风扇开启,以及移动设备上的电池消耗更少。

灯光

  1. 直射光(SpotLightPointLightRectAreaLightDirectionalLight)很慢。在场景中使用尽可能少的直射光。
  2. 避免在场景中添加和移除灯光,因为这需要WebGLRenderer重新编译所有着色器程序(它确实会缓存程序,因此您在以后执行此操作时会比第一次更快)。相反,使用light.visible = falseor light.intensiy = 0
  3. 打开renderer.physicallyCorrectLights使用 SI 单位的精确照明。
  4. 使用尽可能少的光源。比如:如果你有5个光源,那就意味着每一个material都需要考虑到5个光源的作用效果。最终渲染的时候就会变得5倍复杂。如果光源本身位置不变的话,一般可以使用blender等工具把灯光的效果渲染到texture里。这样就不会有实际的动态光效。通常scene里加光源是为了让它们移动,或者只加一两个。但是如果需要更复杂的东西的话,一般最好还是把灯光效果bake进texture里。这样的话渲染就会变得很快了。然后剩余的有需求的地方可以用动态光源渲染。

材料

  1. MeshLambertMaterial不适用于闪亮的材料,但对于像布料这样的哑光材料,它会给出非常相似的结果,MeshPhongMaterial但速度更快。
  2. 如果您使用的是变形目标,请确保 morphTargets = true在您的材质中进行设置,否则它们将不起作用!
  3. 变形法线也是如此 。
  4. 如果您将 SkinnedMesh用于骨骼动画,请确保 material.skinning = true.
  5. 不能共享与变形目标、变形法线或蒙皮一起使用的材质。您需要为每个蒙皮或变形网格创建一个独特的材质( 可使用material.clone())。

几何

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

纹理

  1. 您的所有纹理都需要是 2 的幂 (POT) 大小:1 、2 、4 、8 、1 6 ,…,5 1 2 ,2 0 4 8 ,….
  2. 不要更改纹理的尺寸。而是创建新的, 它更快
  3. 尽可能使用最小的纹理尺寸。
  4. 非二次幂 (NPOT) 纹理需要线性或最近过滤,以及钳到边界或钳到边缘的环绕。不支持 Mipmap 过滤和重复包装。但说真的,不要使用 NPOT 纹理。
  5. 所有具有相同尺寸的纹理在内存中的大小相同,因此 JPG 的文件大小可能比 PNG 小,但它在 GPU 上占用的内存量相同。
  6. KTX2(Khronos TeXture)是一种纹理传输格式(GLTF 的全称也是「GL 传输格式」),专门为在 GPU 渲染中传送和编码转换而优化。KTX2 跟 PNG 或 JPEG 相比,相似大小的文件,在运行时需要的内存更少。

抗锯齿

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

后处理

  1. 内置的抗锯齿不适用于后处理(至少在 WebGL 1 中)。您需要手动执行此操作,使用 FXAASMAA(可能更快、更好)
  2. 由于您没有使用内置 AA,请务必禁用它!

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

  1. three.js 有大量的后处理着色器,这太棒了!但请记住,每个通道都需要渲染整个场景。完成测试后,请考虑是否可以将您的通行证合并为一个自定义通行证。这样做需要做更多的工作,但可以带来相当大的性能提升。

处理物品

从你的场景中移除一些东西?

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

如果您确实需要从场景中永久移除东西,请先阅读这篇文章: 如何处理对象。通常需要使用dispose分别对Geometries、Materials、Textures等递归查找并清除。

性能

  1. 设置object.matrixAutoUpdate = false 表示静态或很少移动的对象 ,并在其位置/旋转/四元数/比例更新时手动调用object.updateMatrix()
  2. 透明物体很慢。 在场景中使用尽可能少的透明对象。
  3. 如果可能,使用 alphatest而不是标准透明度,它会更快。
  4. 在测试您的应用程序的性能时,您需要做的第一件事就是检查它是受 CPU 限制还是受 GPU 限制。使用基本材料替换所有材料scene.overrideMaterial。如果性能提高,那么您的应用程序受 GPU 限制。如果性能没有提高,则您的应用受 CPU 限制。
  5. 在快速机器上进行性能测试时,您可能会获得 60FPS 的最大帧速率。使用open -a "Google Chrome" --args --disable-gpu-vsync运行 chrome可获得无限帧速率 。
  6. 现代移动设备的像素比率高达 5 - 考虑将这些设备的最大像素比率限制为 2 或 3。以牺牲一些非常轻微的场景模糊为代价,您将获得相当大的性能提升。
  7. 烘焙照明和阴影贴图以减少场景中的灯光数量。
  8. 密切注意场景中的绘制调用数量。一个好的经验法则是更少的绘制调用 = 更好的性能。
  9. 远处的物体不需要与靠近相机的物体相同的细节水平。有许多技巧可以通过降低远处物体的质量来提高性能。考虑使用 LOD(细节级别)对象。您也可以只为远处的对象每 2 帧或 3 帧更新一次位置/动画,或者用广告牌替换它们 - 即对象的绘图。
  10. 复杂场景考虑离屏canvas和web worker。
  11. WEBGL不允许在多个context之间share资源,考虑基于FBO(帧缓冲)和MRT(绑定多个renderbuffer到FBO上)进行资源共享。

进阶技巧

  1. 不要用TriangleFanDrawMode,很慢。
  2. 当您有成百上千个相似的几何图形时,使用几何图形实例化。
  3. 在 GPU 而不是 CPU 上进行动画处理,尤其是在对顶点或粒子进行动画处理时( THREE.Bas应用了这里的处理方式)。

扩展阅读

Unity 和 Unreal 文档也有很多性能建议的页面,其中大部分都与 three.js 相关。可阅读下列文章:

WebGL Insights 从整本书中收集了很多技巧。它更具技术性,但也值得一读,尤其是在您编写自己的着色器时。