【问题标题】:Why is GPU render time inconsistent?为什么 GPU 渲染时间不一致?
【发布时间】:2018-11-05 19:34:49
【问题描述】:

我正在使用 THREE 构建一个 WebGL 应用程序,并注意到 GPU 上出现了一些奇怪的时序。我目前没有可用的重现代码,但我想我会问这个问题,以防它是一个已知的浏览器怪癖或常见且可修复的东西。

场景设置

  • 具有约 2,000,000 个多边形、136 个网格和 568 个 Object3D 实例的场景。
  • 将 THREE.Composer 与 FXAA 和 Unreal Bloom 通道结合使用。
  • 使用 THREE.OrbitControls。
  • 仅当已知某些内容发生更改时才会渲染场景。例如,当用户拖动场景以使用控件移动相机或场景中的某些东西移动时,就会安排绘制。场景通常是静态的,因此我们尽量不要在这些情况下进行不必要的渲染。

问题

当场景是静态的(暂时未绘制)然后用户通过拖动更改相机位置时会出现此问题。一旦用户开始拖动,帧率就会非常不稳定——可能是 10-20 fps 或更低——持续几帧,然后平滑回到接近 60 的水平。当场景保持几秒钟然后再次拖动时,这种情况会一直发生。如果鼠标在初始卡顿后持续拖动,则帧速率保持平滑。这些帧没有呈现任何不同。

如果使用requestAnimationFrame 渲染每一帧,则不会发生这种卡顿,并且场景仍然很流畅。

这是仅在某些情况发生变化时才渲染场景时出现卡顿的性能分析器。您可以看到,在再次平滑之前出现卡顿的帧期间,GPU 花费了更多时间:

场景以 60 fps 渲染时的分析器:

有什么想法吗?为什么在拖动时突然发生这么多 GPU 工作?绘制是否会被其他渲染进程阻止?为什么在几秒钟没有渲染后会如此一致地发生?我使用最新版本的 Chrome 进行了分析,但 Firefox 中也存在口吃。

谢谢!

【问题讨论】:

  • 我做过几个项目,我在不需要的时候跳过渲染,它总是在 60FPS 停止的地方重新开始,所以 Three.js 没有问题。你是如何暂停渲染的?你只是在requestAnimationFrame 循环中使用return 吗?您是否要删除任何事件侦听器然后重新添加它们?
  • 我只在已知场景发生变化时使用requestAnimationFrame 安排绘制,因此无需在渲染函数中提前返回。如果在发生更改时已经安排了抽奖,则安排另一场抽奖。这意味着当用户不与画布交互时,javascript 中根本没有发生任何事情。我没有尝试过让渲染循环提前返回,但我也可以尝试。
  • 如果这是确切原因,我必须查看您的代码以查明,但我认为您的渲染循环中的一些简单的东西,例如:if(!needsToRender){return null;} 应该执行得很好,因为您没有添加/删除事件监听器。我理解完全阻止 JavaScript 在后台执行任何操作的愿望,但是调用一个每秒仅检查布尔值 60 次的函数是微不足道的。
  • 我添加了与您提到的内容类似的内容,虽然这种情况发生的频率较低,但有时仍会出现奇怪的口吃。你说得对,添加一个空函数调用是非常轻量级的,但我在不理解 why 解决问题的情况下犹豫添加它(它似乎并没有完全解决)。这也不是页面上运行的唯一 javascript,所以如果可能的话,我宁愿不添加它。为什么有一个空的 requestAnimationFrame 调用总比没有好?当我有机会时,我会尝试制作一个重现案例,以便更容易展示问题。感谢您的帮助!

标签: javascript three.js webgl


【解决方案1】:

没有现场样本,就没有简单的方法可以知道但是......

1 Three.js 可以对对象进行平截头体剔除。

这意味着如果某些对象不在视图之外,它们将不会被绘制。因此,将相机放置在所有物体可见的情况下会比只有一些物体可见时运行得更慢

2 原始剪辑

除 GPU 级别外,与上述相同。 GPU剪辑基元(它不会在视图之外绘制或计算像素)与上面非常相似,如果您尝试绘制的许多东西恰好在视图之外,它会比一切都在视图内运行得更快查看。

3 深度 (Z) 缓冲区拒绝

再次与上述类似,如果您的对象是不透明的,那么如果通过深度测试一个像素位于现有像素的后面,那么 GPU 将跳过调用像素着色器(如果可以)。这意味着如果你画了 568 个东西,而你画的第一个是离相机最近的东西,并且覆盖了它后面的很多东西,那么它的运行速度会比它后面的所有东西都先绘制的情况要快。 Three.js 可以选择在绘制之前进行排序。通常为透明度打开排序,因为需要将透明对象绘制回前面。对于不透明的对象,如果前面的对象遮挡了后面的对象,则从前到后绘制会更快。

4 画的帧太多?

另一个问题是您如何排队抽奖?理想情况下,您只排队一次抽奖,在抽奖发生之前不要再排队。

所以

// bad
someElement.addEventListener('mousemove', render);

上面的代码会尝试为每次鼠标移动进行渲染,即使是 > 60 fps

// bad
someElement.addEventListener('mousemove', () => {
  requestAnimationFrame(render);
});

上面的代码可能会排队很多很多 requestAnimationFrames 所有这些都将在下一帧执行,每帧多次绘制你的场景

// good?
let frameQueued = false;

function requestFrame() {
  if (!frameQueued) {
    frameQueued = true;
    requestAnimationFrame(render);
  }
}

function render(time) {
  frameQueued = false;
  ...
}      


someElement.addEventListener('mousemove', () => {
  requestFrame();
});

或者类似的东西,这样你最多只能在渲染时排队,在渲染完成之前不再排队。上面的代码只是构建代码的一个示例,这样您就不会绘制超出需要的帧数。

【讨论】:

  • 嗨,gman,感谢您的彻底回复!我已经看过这些,我不相信它们中的任何一个都是问题。让我感到震惊的是,它在前几帧中出现了断断续续的情况,然后平滑了,即使它绘制了所有相同的几何图形(相机被拉回并始终包含完整的场景)。我需要一些时间才能整理出一份我可以分享的复制品,但我会在有机会时尝试这样做。
  • 我注意到的是,如果窗口较小,则不会发生口吃。由于 composer 和 unreal Bloom pass 都根据画布的大小创建渲染目标,因此涉及到一些纹理内存。我对 webGL 实现不是很熟悉,但它可能与 GPU 在一段时间未使用后试图重新分配纹理内存有关吗?这不可能是像素填充问题,因为在将视图摆动几帧后帧速率会变得平滑。
猜你喜欢
  • 1970-01-01
  • 2013-05-14
  • 1970-01-01
  • 2015-07-18
  • 2020-06-22
  • 1970-01-01
  • 2020-04-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多