基础知识
Three.js是一个 3D 库,使用 WebGL 绘制 3D,它试图尽可能轻松地在网页上获取 3D 内容。 WebGL 是一个非常底层的系统,只绘制点、线和三角形。使用 WebGL 做任何有用的事情通常需要相当多的代码, three.js 封装了场景、灯光、阴影、材质、纹理、3D 数学等,如果直接使用 WebGL所有这些东西都必须自己编写。
在开始之前,让我们尝试先来了解一下 three.js 应用程序的结构。Three.js 应用程序需要您创建一堆对象并将它们连接在一起。这是一个代表一个小的 three.js 应用程序的图表。
初学者友好提示,或帮助!为什么我什么都看不到?
当我们已经学习了几个基本教程,一切都很好。现在您正在创建自己的应用程序,并且您已经按照教程中的说明进行了所有设置。但是你什么都看不见!有吗??
可以采取以下措施来帮助您找出问题所在。
- 检查 浏览器控制台 ****是否有错误消息
- 将背景颜色设置为黑色以外的其他颜色
盯着黑色画布?如果你能看到的只是黑色,就很难判断是否正在发生某事。尝试将背景颜色设置为红色:
import * as THREE from "three";
const { Color } = THREE;
scene.background = new Color("red");
如果你得到一个红色的画布,那么至少你的renderer.render正在工作,可以继续找出还有什么问题。
- 确保你的场景中有灯光并且它正在照亮你的对象
就像在现实世界中一样,three.js 中的大多数材质都需要光线才能看到。在确定光源正常的前提下,可以覆盖场景中的所有材质MeshBasicMaterial。
一种不需要光线可见的材料是 MeshBasicMaterial. 如果您在显示对象时遇到问题,您可以使用 临时覆盖场景中的所有材质MeshBasicMaterial。如果在您执行此操作时物体神奇地出现,那么您的问题是光线不足。
import * as THREE from "three";
const { MeshBasicMaterial } = THREE;
scene.overrideMaterial = new MeshBasicMaterial({ color: "green" });
- 判断对象是否在相机的视锥内?
以下是透视相机的基本成像原理,其中位于近截面(near)和远截面(far)之间的物体才能被看到。近平面和远平面的高度由视野(fov)决定。两个平面的宽度由视野(fov)和纵横比(aspect)决定。
如果您的对象不在 视锥体内,它将被剪裁。尝试使您的远剪裁平面变得非常大:
camera.far = 100000;
camera.updateProjectionMatrix();
请记住,这只是为了测试!相机的平截头体以米为单位,您应该将其尽可能小以获得最佳性能。设置好场景并正常工作后,请尽可能减小平截头体的大小。
- 判断相机是否在物体里面?
默认情况下,所有内容都在此时创建( 0 ,0 ,0 ),又名起源。确保您已将相机移回,以便您可以看到您的场景!
camera.position.z = 10;
- 仔细考虑你的场景规模
尝试可视化您的场景并记住 three.js 中的一个单位是一米。一切都以合理的逻辑方式组合在一起吗?或者您可能看不到任何东西,因为您刚刚加载的对象只有 0.00001 米宽。等等,屏幕中间那个小黑点是什么?
一般提示
- 在 JavaScript 中创建对象很昂贵,所以不要在循环中创建对象。相反,创建单个对象,例如 Vector3并使用
vector.set()或类似方法在循环内重用该对象。 - 您的渲染循环也是如此。为确保您的应用程序以每秒 60 帧的流畅运行速度,请在渲染循环中尽可能少地工作。不要每帧都创建新对象。
- 始终使用
BufferGeometry而不是Geometry,它更快。 - 预建对象也是如此,始终使用缓冲区几何版本(
BoxBufferGeometry而不是BoxGeometry)。 - 始终尝试重复使用对象、材质、纹理等对象(尽管更新某些内容可能比创建新内容要慢)。
准确的颜色
对于(几乎)准确的颜色,请为渲染器使用以下设置:
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 颜色管理并不完全正确。希望它会很快得到修复,但与此同时,颜色的任何不准确都会非常轻微,除非您进行科学或医学渲染,否则任何人都不太可能注意到。
模型、网格和其他可见事物
- 避免使用常见的基于文本的 3D 数据格式(例如 Wavefront OBJ 或 COLLADA)进行资产交付。相反,请使用针对 Web 优化的格式,例如 glTF。
- 使用带有 glTF 的 Draco 网格压缩。有时这会将 glTF 文件减少到其原始大小的 10% 以下!
- 或者,使用块上的 gltfpack,在某些情况下它可能比 Draco 提供更好的结果。
- 如果您需要使大量对象可见和不可见(或从场景中添加/删除它们),请考虑使用 图层以获得最佳性能。
- 位于完全相同位置的对象会导致闪烁(Z-fighting)。尝试将事物偏移 0.001 之类的微小量,以使事物看起来像它们处于相同位置,同时让您的 GPU 满意。
- 保持场景以原点为中心,以减少较大参考系的浮点错误。
- 永远不要移动你的
Scene. 它创建于( 0 ,0 ,0 ),这是其中所有对象的默认参考框架。 - 用 gltf-transform 来转换网格、压缩、把纹理转换成 KTX2、减小文件尺寸。这种程序化的内容处理管线,意味着可以按需运行和重新处理 GLTF 资源,无论模型还是管线本身有了任何变更,都自动重新生成资源。
相机
- 使您的平截头体尽可能小以获得更好的性能。在开发中使用大截头锥体很好,但是一旦你为部署微调你的应用程序,让你的截头锥体尽可能小以获得一些重要的 FPS。
- 不要把东西放在远裁剪平面上(特别是如果你的远裁剪平面真的很大),因为这会导致闪烁。例如:圆形的球滚动到边缘,会变成椭圆,目前暂时没有技术手段可以优化这种现象。
渲染器
-
preserveDrawingBuffer除非你需要,否则不要启用 。如果我们需要对场景进行截图,则需要开启这个参数。 -
除非您需要,否则禁用 alpha 缓冲区。
-
除非您需要,否则不要启用模板缓冲区。
-
除非您需要,否则禁用深度缓冲区(但您可能确实需要它),开启后性能很差。
-
创建渲染器时使用
powerPreference: "high-performance"。这可能会鼓励用户的系统在多 GPU 系统中选择高性能 GPU。 -
CPU 每次调用图形 API 的渲染函数,会将数据提交到 GPU 中计算渲染,称之为一次 draw call。
- 频繁的渲染状态的切换和跨单元数据传递会造成更大的开销。渲染大量重复的 mesh 时,利用 Instancing 可以将他们合并为一次 draw call。
-
仅在相机位置因 epsilon 变化或发生动画时才考虑渲染。
-
如果你的场景是静态的并且使用
OrbitControls,你可以监听控件的change事件。这样,只需要在相机移动时渲染场景:
OrbitControls.addEventListener("change", () => renderer.render(scene, camera));
最后两个技巧不会使应用获得更高的帧速率,但会得到更少的风扇开启,以及移动设备上的电池消耗更少。
灯光
- 直射光(
SpotLight、PointLight、RectAreaLight和DirectionalLight)很慢。在场景中使用尽可能少的直射光。 - 避免在场景中添加和移除灯光,因为这需要
WebGLRenderer重新编译所有着色器程序(它确实会缓存程序,因此您在以后执行此操作时会比第一次更快)。相反,使用light.visible = falseorlight.intensiy = 0。 - 打开
renderer.physicallyCorrectLights使用 SI 单位的精确照明。 - 使用尽可能少的光源。比如:如果你有5个光源,那就意味着每一个material都需要考虑到5个光源的作用效果。最终渲染的时候就会变得5倍复杂。如果光源本身位置不变的话,一般可以使用blender等工具把灯光的效果渲染到texture里。这样就不会有实际的动态光效。通常scene里加光源是为了让它们移动,或者只加一两个。但是如果需要更复杂的东西的话,一般最好还是把灯光效果bake进texture里。这样的话渲染就会变得很快了。然后剩余的有需求的地方可以用动态光源渲染。
材料
MeshLambertMaterial不适用于闪亮的材料,但对于像布料这样的哑光材料,它会给出非常相似的结果,MeshPhongMaterial但速度更快。- 如果您使用的是变形目标,请确保
morphTargets = true在您的材质中进行设置,否则它们将不起作用! - 变形法线也是如此 。
- 如果您将 SkinnedMesh用于骨骼动画,请确保
material.skinning = true. - 不能共享与变形目标、变形法线或蒙皮一起使用的材质。您需要为每个蒙皮或变形网格创建一个独特的材质( 可使用
material.clone())。
几何
- 避免使用
LineLoop,因为它必须由线带模拟。
纹理
- 您的所有纹理都需要是 2 的幂 (POT) 大小:1 、2 、4 、8 、1 6 ,…,5 1 2 ,2 0 4 8 ,….
- 不要更改纹理的尺寸。而是创建新的, 它更快
- 尽可能使用最小的纹理尺寸。
- 非二次幂 (NPOT) 纹理需要线性或最近过滤,以及钳到边界或钳到边缘的环绕。不支持 Mipmap 过滤和重复包装。但说真的,不要使用 NPOT 纹理。
- 所有具有相同尺寸的纹理在内存中的大小相同,因此 JPG 的文件大小可能比 PNG 小,但它在 GPU 上占用的内存量相同。
- KTX2(Khronos TeXture)是一种纹理传输格式(GLTF 的全称也是「GL 传输格式」),专门为在 GPU 渲染中传送和编码转换而优化。KTX2 跟 PNG 或 JPEG 相比,相似大小的文件,在运行时需要的内存更少。
抗锯齿
- 抗锯齿的最坏情况是由许多相互平行排列的细直条组成的几何体。想想金属百叶窗或格子栅栏。如果可能的话,不要在场景中包含这样的几何图形。 如果您别无选择,请考虑用纹理替换晶格,因为这样可能会产生更好的效果。
后处理
注意:我在网上看到一些地方建议您禁用抗锯齿并改为应用后处理 AA pass。在我的测试中,这是不正确的。在现代硬件上,即使在低功耗移动设备上,内置 MSAA 似乎也非常便宜,而后处理 FXAA 或 SMAA 通道在我测试过的每个场景中都会导致相当大的帧率下降,而且质量也较低比 MSAA。
- three.js 有大量的后处理着色器,这太棒了!但请记住,每个通道都需要渲染整个场景。完成测试后,请考虑是否可以将您的通行证合并为一个自定义通行证。这样做需要做更多的工作,但可以带来相当大的性能提升。
处理物品
从你的场景中移除一些东西?
首先,考虑不要这样做,特别是如果您稍后再将其添加回来。object.visible = false您可以使用(也适用于灯光)或.临时隐藏对象material.opacity = 0。您可以设置light.intensity = 0禁用灯光而不导致着色器重新编译。
如果您确实需要从场景中永久移除东西,请先阅读这篇文章: 如何处理对象。通常需要使用dispose分别对Geometries、Materials、Textures等递归查找并清除。
性能
- 设置
object.matrixAutoUpdate = false表示静态或很少移动的对象 ,并在其位置/旋转/四元数/比例更新时手动调用object.updateMatrix()。 - 透明物体很慢。 在场景中使用尽可能少的透明对象。
- 如果可能,使用
alphatest而不是标准透明度,它会更快。 - 在测试您的应用程序的性能时,您需要做的第一件事就是检查它是受 CPU 限制还是受 GPU 限制。使用基本材料替换所有材料
scene.overrideMaterial。如果性能提高,那么您的应用程序受 GPU 限制。如果性能没有提高,则您的应用受 CPU 限制。 - 在快速机器上进行性能测试时,您可能会获得 60FPS 的最大帧速率。使用
open -a "Google Chrome" --args --disable-gpu-vsync运行 chrome可获得无限帧速率 。 - 现代移动设备的像素比率高达 5 - 考虑将这些设备的最大像素比率限制为 2 或 3。以牺牲一些非常轻微的场景模糊为代价,您将获得相当大的性能提升。
- 烘焙照明和阴影贴图以减少场景中的灯光数量。
- 密切注意场景中的绘制调用数量。一个好的经验法则是更少的绘制调用 = 更好的性能。
- 远处的物体不需要与靠近相机的物体相同的细节水平。有许多技巧可以通过降低远处物体的质量来提高性能。考虑使用 LOD(细节级别)对象。您也可以只为远处的对象每 2 帧或 3 帧更新一次位置/动画,或者用广告牌替换它们 - 即对象的绘图。
- 复杂场景考虑离屏canvas和web worker。
- WEBGL不允许在多个context之间share资源,考虑基于FBO(帧缓冲)和MRT(绑定多个renderbuffer到FBO上)进行资源共享。
进阶技巧
- 不要用
TriangleFanDrawMode,很慢。 - 当您有成百上千个相似的几何图形时,使用几何图形实例化。
- 在 GPU 而不是 CPU 上进行动画处理,尤其是在对顶点或粒子进行动画处理时( THREE.Bas应用了这里的处理方式)。
扩展阅读
Unity 和 Unreal 文档也有很多性能建议的页面,其中大部分都与 three.js 相关。可阅读下列文章:
WebGL Insights 从整本书中收集了很多技巧。它更具技术性,但也值得一读,尤其是在您编写自己的着色器时。