|
| 1 | +Title: WebGL2 - 动画 |
| 2 | +Description: WebGL动画实现方法 |
| 3 | +TOC: 动画 |
| 4 | + |
| 5 | + |
| 6 | +本文隶属于WebGL系列教程,首篇[基础教程](webgl-fundamentals.html)从核心概念讲起,前文则探讨了[3D相机](webgl-3d-camera.html)相关内容。若尚未阅读,请先参阅。 |
| 7 | + |
| 8 | +如何在WebGL中让物体动起来? |
| 9 | + |
| 10 | +本质上,这并非WebGL特有机制——任何JavaScript动画都需要随时间改变状态并重绘。 |
| 11 | + |
| 12 | +我们选取一个前文示例进行动画改造。 |
| 13 | + |
| 14 | + *var fieldOfViewRadians = degToRad(60); |
| 15 | + *var rotationSpeed = 1.2; |
| 16 | + |
| 17 | + *requestAnimationFrame(drawScene); |
| 18 | + |
| 19 | + // Draw the scene. |
| 20 | + function drawScene() { |
| 21 | + * // Every frame increase the rotation a little. |
| 22 | + * rotation[1] += rotationSpeed / 60.0; |
| 23 | + |
| 24 | + ... |
| 25 | + * // Call drawScene again next frame |
| 26 | + * requestAnimationFrame(drawScene); |
| 27 | + } |
| 28 | + |
| 29 | +效果如下: |
| 30 | + |
| 31 | +{{{example url="../webgl-animation-not-frame-rate-independent.html" }}} |
| 32 | + |
| 33 | +但存在一个潜在问题,代码中的 `rotationSpeed / 60.0` 是基于浏览器每秒60次响应 `requestAnimationFrame` 的假设, |
| 34 | +这种帧率假设并不可靠。 |
| 35 | + |
| 36 | +该假设实际上并不成立。用户可能使用旧款智能手机等低性能设备,或后台运行重负载程序。 |
| 37 | +浏览器帧率受多种因素影响,未必保持60FPS——例如2020年后硬件或支持240FPS,或电竞玩家使用90Hz刷新率的CRT显示器。 |
| 38 | + |
| 39 | +可通过此示例观察该问题: |
| 40 | + |
| 41 | +{{{diagram url="../webgl-animation-frame-rate-issues.html" }}} |
| 42 | + |
| 43 | +上例中,我们想让所有'F'保持相同的转速。中间的F全速运行,帧率无关。 |
| 44 | +左右两侧的F,模拟浏览器只有1/8性能运行的情况。在左侧的F是帧率**无**关的。右边的F,帧率**相**关。 |
| 45 | + |
| 46 | +可见:左侧F因未考虑帧率下降而无法同步,右侧F即使以1/8帧率运行仍与全速运行的中央F保持同步。 |
| 47 | + |
| 48 | +实现帧率无关动画的核心方法是:计算帧间时间差,并据此确定当前帧的动画状态增量。 |
| 49 | + |
| 50 | +先需要获取时间值。幸运的是 `requestAnimationFrame` 在回调时会自动传入页面加载后的时间戳。 |
| 51 | + |
| 52 | +为简化计算,转换为以秒为单位最简单。由于 `requestAnimationFrame` 提供的时间单位为毫秒(1/1000秒),需乘以0.001转换为秒。 |
| 53 | + |
| 54 | +由此,可按下面的方式计算时间增量: |
| 55 | + |
| 56 | + *var then = 0; |
| 57 | + |
| 58 | + requestAnimationFrame(drawScene); |
| 59 | + |
| 60 | + // Draw the scene. |
| 61 | + *function drawScene(now) { |
| 62 | + * // Convert the time to seconds |
| 63 | + * now *= 0.001; |
| 64 | + * // Subtract the previous time from the current time |
| 65 | + * var deltaTime = now - then; |
| 66 | + * // Remember the current time for the next frame. |
| 67 | + * then = now; |
| 68 | + |
| 69 | + ... |
| 70 | + |
| 71 | +一旦获得以秒为单位的`deltaTime`,我们所有的计算都可以基于"每秒单位量"进行。在本例中: |
| 72 | +`rotationSpeed` 设为1.2,表示每秒旋转1.2弧度。约等于1/5圆周,完整旋转一周约需5秒,且该速率与帧率无关。 |
| 73 | + |
| 74 | + |
| 75 | + * rotation[1] += rotationSpeed * deltaTime; |
| 76 | + |
| 77 | +以下是实现效果: |
| 78 | + |
| 79 | +{{{example url="../webgl-animation.html" }}} |
| 80 | + |
| 81 | +除非在低性能设备上运行,否则您可能难以察觉与本页顶部示例的差异。但若不实现帧率无关的动画,部分用户获得的体验将与您的设计预期大相径庭。s |
| 82 | + |
| 83 | +接下来学习如何[应用纹理](webgl-3d-textures.html)。 |
| 84 | + |
| 85 | +<div class="webgl_bottombar"> |
| 86 | +<h3>请勿使用 setInterval 或 setTimeout !</h3> |
| 87 | +<p>若您曾用JavaScript实现动画,可能习惯使用<code>setInterval</code>或<code>setTimeout</code>调用绘制函数。</p> |
| 88 | +<p> 这类方式存在双重缺陷:首先,<code>setInterval</code>和<code>setTimeout</code>与浏览器渲染流程无关,它们无法与屏幕刷新同步,最终将导致与用户设备不同步。 |
| 89 | +若您假设60FPS使用它们,而实际设备以其他帧率运行,就会出现同步问题。</p> |
| 90 | +<p> 其次,浏览器无法识别您调用<code>setInterval</code>或<code>setTimeout</code>的意图。即使页面处于后台标签页(不可见状态),浏览器仍会执行这些代码。虽然这对每分钟检查邮件/Tweet的任务无碍,但若用于WebGL渲染上千个对象,将导致用户设备资源被不可见页面的渲染任务无意义消耗。</p> |
| 91 | +<p> |
| 92 | +<code>requestAnimationFrame</code>能完美解决这些问题。 |
| 93 | +它在屏幕刷新最佳时机触发回调,且仅在标签页可见时执行。 |
| 94 | +</p> |
| 95 | +</div> |
| 96 | + |
| 97 | + |
| 98 | + |
0 commit comments