扩展性能
React Three Fiber 扩展性能
运行 WebGL 可能相当昂贵,这取决于你的设备有多强大。为了减轻这种情况,特别是如果你想让你的应用程序适用于各种设备,包括较弱的设备,你应该研究一下性能优化。本文将介绍其中的几项。
按需渲染
three.js 应用程序通常以每秒执行 60 次的游戏循环方式运行,React Three Fiber 也不例外。当你的场景中有不断移动的部件时,这是很好的。这通常是最耗费电池和使风扇旋转起来的原因。但是如果你的场景中的移动部件被允许休息,那么继续渲染就会造成浪费。在这种情况下,你可以选择按需渲染,只有在必要时才进行渲染。这可以节省电池,并使嘈杂的风扇得到控制。在全屏中打开下面的沙盘,并查看开发工具,你会发现在没有任何事情发生的时候,它是完全空闲的。只有当你移动模型时,它才会进行渲染。
你需要做的就是把 canvas frameloop 属性设置为 demand,每当它检测到整个组件树上的 Props 变化时,它就会渲染框架。
<Canvas frameloop="demand">
一个主要的注意事项是,如果树中的任何东西突变了道具,那么 React 就不能意识到它,显示就会变味。例如,相机控件只是抓取到相机并突变其值。在这里你可以使用 React Three Fiber 的 invalidate 函数来手动触发帧。
function Controls() {
const ref = useRef()
const { invalidate, camera, gl } = useThree()
useEffect(() => {
ref.current.addEventListener('change', invalidate)
return () => ref.current.removeEventListener('change', invalidate)
}, [])
return <orbitControls ref={ref} args={[camera, gl.domElement]} />
一般来说,只要你需要渲染,就可以调用 invalidate。
invalidate();
复用 Geometries 与 Materials
每个 Geometry 和 Material 都意味着 GPU 的额外开销。如果你知道资源会重复使用,你应该尽量重复使用。
const red = new THREE.MeshLambertMaterial({ color: "red" })
const sphere = new THREE.SphereGeometry(1, 28, 28)
function Scene() {
return (
<>
<mesh geometry={sphere} material={red} />
<mesh position={[1, 2, 3]} geometry={sphere} material={red} />
如果您在整个组件树中通过 useLoader 以相同的 URL 访问一个资源,那么您将总是引用相同的资产,从而重复使用它。如果您通过 GLTFJSX 运行您的 GLTF 资产,这尤其有用,因为它将几何图形和材料联系起来,从而创建可重复使用的模型。
function Shoe(props) {
const { nodes, materials } = useLoader(GLTFLoader, "/shoe.glb")
return (
<group {...props} dispose={null}>
<mesh geometry={nodes.shoe.geometry} material={materials.canvas} />
</group>
)
}
<Shoe position={[1, 2, 3]} />
<Shoe position={[4, 5, 6]} />
Instancing
每个网格都是一个绘制调用,你应该注意你使用了多少个这样的调用:最多不超过 1000 个,最好是几百个或更少。你可以通过减少绘制调用来赢回性能,例如通过实例化重复对象。这样你就可以在一次绘图调用中拥有成百上千的对象。
function Instances({ count = 100000, temp = new THREE.Object3D() }) {
const ref = useRef();
useEffect(() => {
// Set positions
for (let i = 0; i < count; i++) {
temp.position.set(Math.random(), Math.random(), Math.random());
temp.updateMatrix();
ref.current.setMatrixAt(i, temp.matrix);
}
// Update the instance
ref.current.instanceMatrix.needsUpdate = true;
}, []);
return (
<instancedMesh ref={ref} args={[null, null, count]}>
<boxGeometry />
<meshPhongMaterial />
</instancedMesh>
);
}
Level of detail
有时,物体离相机越远,降低其质量是有益的。如果它几乎不可见,你为什么要全分辨率显示它。这可能是一个很好的策略,可以减少整个顶点数量,这意味着 GPU 的工作更少。
在 Drei 中,有一个叫做
import { Detailed, useGLTF } from '@react-three/drei'
function Model() {
const [low, mid, high] = useGLTF(["/low.glb", "/mid.glb", "/high.glb"])
return (
<Detailed distances={[0, 10, 20]}>
<mesh geometry={high} />
<mesh geometry={mid} />
<mesh geometry={low} />
<Detailed/>
)
}
Nested loading
嵌套式加载意味着较小的纹理和模型首先被加载,较高的分辨率在后面。下面的沙盒经历了三个加载阶段。
- 一个加载指示器
- 低质量
- 高质量
function App() {
return (
<Suspense fallback={<span>loading...</span>}>
<Canvas>
<Suspense fallback={<Model url="/low-quality.glb" />}>
<Model url="/high-quality.glb" />
</Suspense>
</Canvas>
</Suspense>
);
}
function Model({ url }) {
const { scene } = useGLTF(url);
return <primitive object={scene} />;
}
Performance Monitoring
Drei 有一个新的组件 PerformanceMonitor,允许你监测和适应设备的性能。该组件将收集一段时间内的平均帧数(每秒帧数)。如果经过几次迭代,平均数低于或高于某个阈值,它将触发 onIncline 和 onDecline 回调,让你做出反应。通常情况下,你会降低你的场景的质量、分辨率、效果、渲染的东西的数量,或者,如果你有足够的帧数来填补的话,就增加它。由于这通常会导致两个回调之间的乒乓效应,所以你定义了帧率的上下限,只要你保持在这个范围内,就不会有任何触发。理想情况下,你的应用程序应该通过逐渐改变质量来找到进入该范围的方法。
一个调节分辨率的简单例子。一开始是 1.5,如果系统低于这个界限,它就会变成 1,如果它足够快,就会变成 2。
function App() {
const [dpr, setDpr] = useState(1.5)
return (
<Canvas dpr={dpr}>
<PerformanceMonitor onIncline={() => setDpr(2)} onDecline={() => setDpr(1)} >
你也可以使用 onChange 回调,在平均值向哪个方向变化时得到通知。这允许你进行渐进式变化。它给你一个介于 0 和 1 之间的系数,它因倾斜而增加,因下降而减少。默认情况下,该系数最初为 0.5。
import round from 'lodash/round'
const [dpr, set] = useState(1)
return (
<Canvas dpr={dpr}>
<PerformanceMonitor onChange={({ factor }) => setDpr(round(0.5 + 1.5 * factor, 1))}>
如果你仍然遇到翻转,尽管有界限,你可以定义一个翻转的限制。如果它被满足,onFallback 将被触发,这通常会给应用程序设置一个最低可能的基线。在回退被调用后,PerformanceMonitor 将关闭。
<PerformanceMonitor flipflops={3} onFallback={() => setDpr(1)}>
PerformanceMonitor 也可以有子代,如果你把你的应用包在里面,你就可以使用 usePerformanceMonitor,它允许嵌套树下的各个组件对性能变化做出自己的反应。
<PerformanceMonitor>
<Effects />
</PerformanceMonitor>;
function Effects() {
usePerformanceMonitor({ onIncline, onDecline, onFallback, onChange });
// ...
}