three.js的画面渲染卡不卡跟什么有关?
- 硬件性能
- GPU(图形处理单元):Three.js 渲染依赖于 WebGL,而 WebGL 的执行效率很大程度上取决于 GPU 的性能。高性能的 GPU 能够更快地处理图形和渲染计算,从而提供更流畅的渲染体验。
- CPU(中央处理单元):虽然 Three.js 主要依赖 GPU 渲染,但 CPU 仍然承担着场景管理、动画计算、物理模拟等任务。CPU 性能也会影响到渲染流畅度。
- 场景复杂度
- 网格(Mesh)和几何体(Geometry)数量:场景中的物体数量越多,需要渲染的网格和几何体越多,对计算资源的要求就越高。
- 纹理和材质:高分辨率的纹理和复杂的材质(如具有光泽、反射等特性的材质)会增加渲染负担。
- 渲染技术
- 阴影和光照效果:实时阴影和复杂的光照模型(如全局光照)需要大量计算,可能会降低渲染性能。
- 后处理效果:后处理效果(如模糊、HDR、色彩校正等)会在渲染流程的最后阶段应用,这些效果虽然可以显著提升视觉效果,但也会增加渲染开销。
- 分辨率和
devicePixelRatio
- 画面看起来越大需要处理的像素就越多
- 渲染的目标分辨率越高,需要处理的像素就越多,相应地,渲染性能要求也会增加。在高
devicePixelRatio
的设备上(如Retina显示屏),如果没有适当优化,渲染可能会显得卡顿。
- 浏览器和WebGL实现
- 不同的浏览器和其底层的WebGL实现可能会导致性能差异。优化和兼容性问题都可能影响渲染性能。
- 优化措施
- 减少 drawcall 和重计算:尽量减少场景中动态改变的部分,利用Three.js的各种缓存机制。
- 使用LOD(Level of Detail)技术:根据相机与对象的距离,动态调整对象的细节级别,以减少渲染负担。
- 合理使用纹理:优化纹理大小和数量,避免过大的纹理造成的内存负担。
- 使用Instancing:对大量相似的对象使用实例化渲染,以减少 drawcall 次数。
确保Three.js应用的流畅性,需要综合考虑上述因素,并根据具体情况进行优化。
性能
material/geometry.dispose(); //删除材质/几何体
使用merge方法合并不需要单独操作的模型
几何体mesh.updateMatrix(); //提取位置.position、缩放.scale和四元数.quaternion的属性值 转化为 变换矩阵设置本地矩阵属性.matrix
geometry.merge(何体mesh.geometry, 几何体mesh.matrix); //将几何体合并
在循环渲染中避免使用更新:
//几何体:
geometry.verticesNeedUpdate = true; //顶点发生了修改ht
geometry.elementsNeedUpdate = true; //面发生了修改
geometry.morphTargetsNeedUpdate = true; //变形目标发生了修改
geometry.uvsNeedUpdate = true; //uv映射发生了修改
geometry.normalsNeedUpdate = true; //法向发生了修改
geometry.colorsNeedUpdate = true; //顶点颜色发生的修改
//材质
material.needsUpdate = true
//纹理
texture.needsUpdate = true;
优化大量对象
将大量盒子合并为一个几何体, 具体步骤如下:
- 创建一个盒子几何体(BoxGeometry)作为每个数据点的基础几何体。
- 使用辅助对象(Object3D)来定位和旋转盒子,以使其与数据点在球体上的位置对应。
- 创建一个空数组,用于存储每个数据点的盒子几何体。
- 遍历数据点,为每个数据点创建盒子几何体,并根据数据值设置盒子的颜色和尺寸。
- 将每个盒子几何体添加到数组中。
- 使用BufferGeometryUtils.mergeGeometries()方法将所有盒子几何体合并为一个几何体。
- 创建一个网格(Mesh)对象,并将合并后的几何体和材质(MeshBasicMaterial)应用于网格。
- 将网格添加到场景中进行渲染。
在合并几何体时,还需要处理顶点属性(Attribute),例如颜色、法线、纹理坐标等。
以下是在合并几何体时处理顶点属性的一般步骤:
- 在创建盒子几何体时,为每个顶点设置相应的属性值。例如,在上述示例中,我们想要为每个顶点设置颜色。
- 创建一个新的属性数组,用于存储所有数据点的属性值。例如,颜色属性可以使用Float32Array或Uint8Array来存储颜色值。
- 遍历数据点,将每个数据点的属性值添加到属性数组中。需要根据顶点的索引来确定每个数据点的属性值在属性数组中的位置。
- 创建一个顶点属性(BufferAttribute)对象,并将属性数组作为参数传递给它。
- 在合并几何体时,将顶点属性对象添加到合并后的几何体中。可以使用setAttribute()方法来设置合并后的几何体的顶点属性。
官网案例
- 多个数据集(渲染19000个立方体)之间进行动画切换
- 使用 MorphTargets(变形目标)来实现动画过渡
- MorphTargets 是一种在几何体的每个顶点处都提供多个值,并通过线性插值(Linear Interpolation)在它们之间进行过渡的方法
- OffscreenCanvas 允许 web worker 渲染到 canvas
- OrbitControls 是一个用来控制 3D 场景视图的工具,它需要监听鼠标和键盘事件来调整场景中的相机位置。然而,由于 Web Worker 无法直接操作 DOM,OrbitControls 无法在 Web Worker 中正常工作。所以,让主线程监听 DOM 事件,转发给伪装成 HTMLElement的代理对象, 然后 Web Worker 从它拿事件,从而让 OrbitControls 能够在 Web Worker 中正常工作。
https://discoverthreejs.com/zh/tips-and-tricks/
- 初级技巧:包括如何解决在设置场景后看不到任何东西的问题,例如检查浏览器控制台错误,设置背景颜色,确保场景中有光源,确保物体在相机的视域中,考虑场景的比例等。
- 一般技巧:包括在循环中不要创建对象,尽量复用 Vector3 等对象,总是使用 BufferGeometry 而不是 Geometry,总是试图重用对象,材质,纹理等。
- 材料:MeshLambertMaterial 更适合于布料等非光泽材料,与 MeshPhongMaterial 相比,性能更佳。
- 相机:为了提高性能,应尽可能降低视锥体的大小。尤其在开发阶段结束,准备发布应用时,应尽可能缩小视锥体的大小。
- 渲染器:在创建渲染器时,使用 powerPreference: "high-performance" 可以在多 GPU 系统中优先使用高性能 GPU。
- 光源:直接光源(SpotLight, PointLight, RectAreaLight, 和 DirectionalLight)运行较慢,应尽量减少场景中直接光源的使用。
- 阴影:如果场景是静态的,只有当有东西改变时才更新阴影图,而不是每一帧都更新。
- 纹理:所有的纹理需要是 2 的幂(POT)大小,例如:1,2,4,8,16,…,512,2048,…。
- 性能:对于静态或很少移动的对象,设置 object.matrixAutoUpdate = false, 并在其位置/旋转/四元数/缩放 更新时手动调用 object.updateMatrix()。
- 高级技巧:使用几何实例化处理数百或数千个相似的几何体,尤其是在 GPU 上进行动画处理时。
- 后处理:内置的抗锯齿不能与后处理一起使用(至少在 WebGL 1 中)。你需要手动使用 FXAA 或 SMAA 来进行抗锯齿处理。
- 清除物体:如果你需要从场景中永久性地移除物体,需要读这篇文章:如何清除物体。
- 更新场景中的物体:如果你需要更新场景中的物体,可以阅读这篇文章:如何更新物体。
- 性能测试:测试应用的性能时,首先需要检查它是 CPU 受限,还是 GPU 受限。通过将所有材质替换为基础材质来进行测试。
为什么我什么都看不到
- 检查浏览器控制台以查看错误信息。
- 将背景颜色设置为除黑色之外的其他颜色
- 盯着一块黑色的画布?如果你只能看到黑色,很难判断是否发生了什么。尝试将背景颜色设置为红色:
import { Color } from "./vendor/three/build/three.module.js";
scene.background = new Color("red");
如果你得到了一块红色的画布,那么至少你的 renderer.render
调用是有效的,然后你可以继续找出其他的问题所在。 3. 确保你的场景中有一盏灯,并且它照亮了你的物体。就像在现实世界中一样,大多数在three.js中的材质需要光才能被看到。 4. 用 MeshBasicMaterial
覆盖场景中的所有材料, 一个不需要光线才能可见的材料是 MeshBasicMaterial
。如果你在让物体显示出来方面遇到困难,你可以暂时用 MeshBasicMaterial
覆盖场景中的所有材料。如果在这样做时物体突然出现,那么你的问题就是缺乏光线。
import { MeshBasicMaterial } from "./vendor/three/build/three.module.js";
scene.overrideMaterial = new MeshBasicMaterial({ color: "green" });
- 您的物体是否在相机的视锥体内?如果您的物体不在视锥体内,它将被裁剪。尝试将远裁剪平面设置得非常大:
camera.far = 100000;
camera.updateProjectionMatrix();
记住这只是用于测试!相机的视锥体以米为单位测量,为了获得最佳性能,您应该尽量将其缩小。 一旦您的场景设置好并正常运行,尽量减小视景体的大小。 6. 你的相机在物体内部吗?默认情况下,所有东西都会在点 (0,0,0) ,也就是原点处创建。确保将相机后移,以便能够看到你的场景!
camera.position.z = 10;
- 仔细考虑你场景的规模。
- 尝试将场景可视化,并记住在three.js中,一个单位代表一米。所有的元素是否以合理的逻辑方式组合在一起?
- 或许你什么都看不见,因为你刚刚加载的物体只有0.00001米宽。等等,屏幕中间那个小黑点是什么?
常规提示
- JavaScript中的对象创建是昂贵的,所以不要在循环中创建对象。相反,创建一个单一的对象,比如一个Vector3,并使用
vector.set()
或类似的方法在循环中重复使用它。 - 同样适用于你的渲染循环。为了确保你的应用以每秒六十帧的流畅度运行,尽量在渲染循环中做尽可能少的工作。不要在每一帧都创建新的对象。
- 始终使用
BufferGeometry
而不是Geometry
,它更快。 - 对于预先构建的对象也是一样,始终使用缓冲几何版本(
BoxBufferGeometry
而不是BoxGeometry
)。 - 始终尝试重复使用物体,如物体、材料、纹理等(尽管更新某些内容可能比创建新内容慢,参见下面的纹理提示)。
用国际单位制
three.js在所有地方都使用国际单位制。如果您也使用国际单位制,您会发现事情会更加顺利。如果由于某种原因您使用不同的单位,比如英寸(令人不安),请确保您有充分的理由这样做。
- 距离以米为单位测量(1个three.js单位=1米)。
- 时间以秒为单位衡量。
- 光线以国际单位制的照度(cd)、流明(lm)和勒克斯(lx)进行测量(只要你至少打开
renderer.physicallyCorrectLights
)。 如果你正在创造真正史诗般的规模的东西(如太空模拟等),要么使用一个缩放因子,要么切换到使用对数深度缓冲。
准确的颜色
为了(几乎)准确的颜色,请使用以下渲染器设置:
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 { 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
。自己测试一下,或者找到最近几个月有适当测试的文章。
使用样式指南和代码检查工具
个人而言,我使用Eslint、Prettier和Airbnb风格指南的组合。在VSCode中,我花了大约30分钟使用这个教程(第2部分)进行设置,现在我再也不必浪费时间来格式化、检查代码质量,或者纠结于某个特定语法是否合适了。 许多使用three.js的人更喜欢Mr.doob的Code Style™而不是Airbnb,所以如果你喜欢使用它,只需将eslint-config-airbnb插件替换为eslint-config-mdcs。
模型、网格和其他可见物品
- 避免使用常见的基于文本的3D数据格式,如Wavefront OBJ或COLLADA,用于资产交付。相反,使用针对Web进行优化的格式,如glTF。
- 使用Draco网格压缩与glTF。有时,这可以将glTF文件压缩至原始大小的不到10%!
- 另外,还有一个新的工具叫做gltfpack,在某些情况下可能比Draco产生更好的结果。
- 如果您需要使大量的对象可见或不可见(或者将它们添加/移除到您的场景中),请考虑使用图层以获得最佳性能。
- 物体在完全相同的位置会引起闪烁(Z-fighting)。 尝试将事物偏移一个很小的量,比如0.001,使它们看起来处于相同的位置,同时保持你的GPU的良好状态。
- 保持场景围绕原点居中,以减少在大坐标下的浮点误差。
- 永远不要移动你的
Scene
。它在 (0,0,0) 处创建,这是其中所有对象的默认参考框架。
相机
- 尽量将你的视锥体缩小,以获得更好的性能。 在开发过程中使用大的截锥体是可以的,但是一旦你开始为部署调优你的应用程序时,尽量将截锥体缩小,以获得几个关键的帧率。
- 不要将物体放在远裁剪平面上(尤其是如果你的远裁剪平面非常大),因为这可能会导致闪烁。
渲染器
- 除非你需要,否则不要启用
preserveDrawingBuffer
。 - 除非需要,否则禁用alpha缓冲区。
- 除非需要,否则不要启用模板缓冲区。
- 除非你需要它(但你可能确实需要它),否则禁用深度缓冲区。
- 在创建渲染器时使用
powerPreference: "high-performance"
。这可能会促使用户的系统选择高性能的GPU,在多GPU系统中。 - 仅在相机位置变化超过epsilon或发生动画时进行渲染。
- 如果您的场景是静态的并且使用
OrbitControls
,您可以监听控件的change
事件。这样,只有在相机移动时才会渲染场景。
OrbitControls.addEventListener("change", () => renderer.render(scene, camera));
你不会从后两个中获得更高的帧率,但你会得到更少的风扇启动和移动设备上更少的电池耗电。 注意:我在网上看到一些地方建议禁用抗锯齿并应用后处理的抗锯齿效果。在我的测试中,这是不正确的。 在现代硬件上,即使在低功耗移动设备上,内置的多重采样抗锯齿(MSAA)似乎非常廉价,而后处理的快速近似抗锯齿(FXAA)或子像素抗锯齿(SMAA)通道在我测试的每个场景中都会导致相当大的帧率下降,并且质量也比不上MSAA。
灯光
- 直接光(
SpotLight
,PointLight
,RectAreaLight
和DirectionalLight
)速度较慢。在您的场景中尽量少使用直接光。 - 避免在场景中添加和移除灯光,因为这会导致
WebGLRenderer
重新编译所有着色器程序(它会缓存程序,所以后续操作会比第一次更快)。相反,使用light.visible = false
或light.intensiy = 0
。 - 打开
renderer.physicallyCorrectLights
以使用国际单位进行准确照明。
阴影
- 如果你的场景是静态的,只有在有变化时才更新阴影贴图,而不是每一帧都更新。
- 使用
CameraHelper
来可视化阴影相机的视锥体。 - 尽量使阴影锥体尺寸最小化。
- 尽可能将阴影纹理降低分辨率。
- 请记住,点光源阴影比其他阴影类型更昂贵,因为它们必须渲染六次(每个方向一次),而
DirectionalLight
和SpotLight
阴影只需渲染一次。 - 谈到
PointLight
阴影,注意到CameraHelper
只能可视化点光源阴影中的六个方向之一。它仍然有用,但你需要想象其他五个方向。
材料
MeshLambertMaterial
对于光亮材料无效,但对于像布料这样的哑光材料,它会给出与MeshPhongMaterial
非常相似的结果,而且速度更快。- 如果您正在使用形态目标,请确保在您的材质中设置
morphTargets = true
,否则它们将无法工作! - 同样适用于形态法线。
- 如果您正在使用SkinnedMesh进行骨骼动画,请确保
material.skinning = true
. - 使用morph targets、morph normals或蒙皮时,不能共享材质。您需要为每个蒙皮或变形的网格创建一个独特的材质(
material.clone()
在这里是您的朋友)。
定制材料
- 只有在制服发生变化时才更新,而不是每一帧都更新。
几何学
- 避免使用
LineLoop
,因为它必须通过行条带来模拟。
纹理
- 所有的纹理都需要是2的幂大小: 1,2,4,8,16,…,512,2048,… .
- 不要改变纹理的尺寸。相反,创建新的纹理,这样更快。
- 尽量使用最小的纹理尺寸(你能使用256x256的平铺纹理吗?也许会让你惊讶!)。
- 非2的幂次方(NPOT)纹理需要线性或最近过滤,并且使用边界夹取或边缘夹取包装。不支持多级纹理过滤和重复包装。 但是说真的,就是不要使用非2的幂次方纹理。
- 所有尺寸相同的纹理在内存中的大小相同,所以JPG可能比PNG文件大小更小,但在GPU上占用的内存量相同。
抗锯齿
- 抗锯齿的最糟糕情况是由许多细长直线构成的几何体,彼此平行。想象金属百叶窗或格子围栏。如果可能的话,请不要在场景中包含这样的几何体。如果没有选择,考虑用纹理替换格子,可能会得到更好的效果。
后期处理
- 内置的抗锯齿在后期处理中不起作用(至少在WebGL 1中)。您需要手动进行处理,使用FXAA或SMAA(可能更快,更好)。
- 由于您没有使用内置的AA功能,请确保将其禁用!
- three.js有很多后期处理着色器,这太棒了!但请记住,每个通道都需要渲染整个场景。 一旦测试完成,请考虑是否可以将您的通行证合并为一个单一的自定义通行证。这样做需要更多的工作,但可以显著提高性能。
处理对象
从你的场景中移除某物? 首先,考虑不要这样做,特别是如果你之后会再次添加它。你可以使用 object.visible = false
(也适用于灯光)或 material.opacity = 0
来暂时隐藏对象。你可以设置 light.intensity = 0
来禁用灯光而不会导致着色器重新编译。 如果您确实需要永久删除场景中的物体,请先阅读本文:如何处理物体。
更新您场景中的对象
阅读这篇文章:如何更新事物。
表现
- 为静态或很少移动的物体设置
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。 以稍微模糊场景为代价,您将获得显著的性能提升。
- 烘焙光照和阴影贴图以减少场景中的灯光数量。
- 请注意场景中 drawcall 的数量。一个好的经验法则是 drawcall 越少,性能越好。
- 远处的物体不需要与镜头附近的物体一样的细节水平。有许多技巧可以通过降低远处物体的质量来提高性能。考虑使用LOD(细节级别)对象。 你也可以只在远处的物体上每2或3帧更新位置/动画,或者用广告牌替换它们 - 即物体的绘画。
高级提示
- 不要使用
TriangleFanDrawMode
,它很慢。 - 当你有数百或数千个相似的几何体时,请使用几何实例化。
- 在动画顶点或粒子时,尤其是在GPU上进行动画处理(参考THREE.Bas的一种方法)。
从DrawCall到显示
CPU阶段: a) Draw Call 准备 (几微秒到几毫秒):
- 应用程序代码准备渲染命令
- 设置着色器参数、纹理、渲染状态等
- 可能涉及复杂的场景图遍历和剔除操作
- DrawCall的内容
- 几何数据
- 顶点坐标
- 法线
- 纹理坐标
- 颜色信息(如果有)
- 索引数据(如果使用索引绘制)
- 渲染状态
- 使用的着色器程序
- 纹理绑定
- 混合模式
- 深度测试设置
- 剔除设置
- Uniform数据
- 变换矩阵(模型、视图、投影)
- 材质属性
- 光照参数
- 绘制命令
- 图元类型(如三角形、线条、点)
- 起始索引
- 顶点数量或索引数量 b) API调用 (几微秒到几毫秒):
- 几何数据
- 将渲染命令转换为图形API调用(如OpenGL, DirectX, Vulkan等)
- API驱动程序处理这些调用
c) 命令缓冲构建 (几微秒到几毫秒):
- 驱动程序将API调用转换为GPU可理解的命令
- 构建命令缓冲区
GPU处理阶段: a) 命令处理 (几微秒到几毫秒):
- GPU从命令缓冲区读取并解析命令
b) 几何处理 (取决于复杂度,可能从几微秒到几十毫秒):
- 顶点着色器处理
- 曲面细分(如果适用)
- 几何着色器处理(如果适用)
- 裁剪、背面剔除等操作
c) 光栅化 (取决于像素数量和复杂度,可能从几微秒到几十毫秒):
- 将几何转换为像素
- 片段着色器处理每个像素
d) 后处理 (几微秒到几毫秒):
- 深度测试、模板测试、混合等操作
e) 帧缓冲操作 (几微秒到几毫秒):
- 将结果写入帧缓冲区
显示阶段: a) 帧缓冲交换 (依赖于垂直同步设置,最长可达16.7ms @ 60Hz):
- 等待显示器刷新间隔
- 交换前后缓冲区
b) 扫描输出 (依赖于显示器刷新率,通常为16.7ms @ 60Hz):
- 显示器从帧缓冲读取数据并显示
考虑到所有这些阶段,从drawcall到画面实际变化的整个过程可能持续时间如下:
- 最短情况: 几毫秒 (简单场景,高性能硬件,无垂直同步)
- 典型情况: 16.7ms到33.3ms (取决于帧率,60FPS或30FPS)
- 最长情况: 可能超过100ms (复杂场景,低性能硬件,存在CPU或GPU瓶颈)