扩展性能

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 资产,这尤其有用,因为它将几何图形和材料联系起来,从而创建可重复使用的模型。

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 的工作更少。

Re-using geometry and level of detail

在 Drei 中,有一个叫做 的小组件,它可以在没有模板的情况下设置 LOD。你可以加载或准备一些分辨率的阶段,你想有多少就有多少,然后给它们与摄像机相同的距离,从最高质量开始到最低。

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

嵌套式加载意味着较小的纹理和模型首先被加载,较高的分辨率在后面。下面的沙盒经历了三个加载阶段。

  • 一个加载指示器
  • 低质量
  • 高质量

Progressive loading states with suspense

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 });
  // ...
}

Links

上一页