rendering-on-demand.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <!DOCTYPE html><html lang="zh"><head>
  2. <meta charset="utf-8">
  3. <title>按需渲染</title>
  4. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  5. <meta name="twitter:card" content="summary_large_image">
  6. <meta name="twitter:site" content="@threejs">
  7. <meta name="twitter:title" content="Three.js – 按需渲染">
  8. <meta property="og:image" content="https://threejs.org/files/share.png">
  9. <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
  10. <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
  11. <link rel="stylesheet" href="../resources/lesson.css">
  12. <link rel="stylesheet" href="../resources/lang.css">
  13. <!-- Import maps polyfill -->
  14. <!-- Remove this when import maps will be widely supported -->
  15. <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
  16. <script type="importmap">
  17. {
  18. "imports": {
  19. "three": "../../build/three.module.js"
  20. }
  21. }
  22. </script>
  23. <link rel="stylesheet" href="/manual/zh/lang.css">
  24. </head>
  25. <body>
  26. <div class="container">
  27. <div class="lesson-title">
  28. <h1>按需渲染</h1>
  29. </div>
  30. <div class="lesson">
  31. <div class="lesson-main">
  32. <p>这一章的主题即便已经很明显了, 但... 以防万一, 还是说大多数情况下three.js给出的例子都是连续渲染的. 换言之他们使用了<code class="notranslate" translate="no">requestAnimationFrame</code>循环或者写成<em>rAF loop</em></p>
  33. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render() {
  34. ...
  35. requestAnimationFrame(render);
  36. }
  37. requestAnimationFrame(render);
  38. </pre>
  39. <p>有些场景连续渲染是有意义的, 但是有些情况下不需要一直动呢? 这种情况下不断地渲染会浪费电, 对于移动设备来说属实不能接受. </p>
  40. <p>显而易见的解决方法是一开始的时候渲染一次, 只有当什么东西改变了以后再次渲染. 这种改变包括纹理的变化, 或者再入了模型, 其他源传来了什么数据, 用户调整了设置或者是动了摄像机. </p>
  41. <p>我们以<a href="responsive.html">响应式设计</a>这一章为例, 稍作修改以满足按需渲染.</p>
  42. <p>首先我们添加<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>, 这样当摄像机改变之后场景就可以随之渲染</p>
  43. <p>First we'll add in the <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> so there is something that could change
  44. that we can render in response to.</p>
  45. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  46. +import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
  47. </pre>
  48. <p>然后</p>
  49. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const fov = 75;
  50. const aspect = 2; // the canvas default
  51. const near = 0.1;
  52. const far = 5;
  53. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  54. camera.position.z = 2;
  55. +const controls = new OrbitControls(camera, canvas);
  56. +controls.target.set(0, 0, 0);
  57. +controls.update();
  58. </pre>
  59. <p>我们不需要再渲染那三个正方体了所以不再追踪</p>
  60. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-const cubes = [
  61. - makeInstance(geometry, 0x44aa88, 0),
  62. - makeInstance(geometry, 0x8844aa, -2),
  63. - makeInstance(geometry, 0xaa8844, 2),
  64. -];
  65. +makeInstance(geometry, 0x44aa88, 0);
  66. +makeInstance(geometry, 0x8844aa, -2);
  67. +makeInstance(geometry, 0xaa8844, 2);
  68. </pre>
  69. <p>把这些代码移除, 然后调用<code class="notranslate" translate="no">requestAnimationFrame</code></p>
  70. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function render(time) {
  71. - time *= 0.001;
  72. +function render() {
  73. if (resizeRendererToDisplaySize(renderer)) {
  74. const canvas = renderer.domElement;
  75. camera.aspect = canvas.clientWidth / canvas.clientHeight;
  76. camera.updateProjectionMatrix();
  77. }
  78. - cubes.forEach((cube, ndx) =&gt; {
  79. - const speed = 1 + ndx * .1;
  80. - const rot = time * speed;
  81. - cube.rotation.x = rot;
  82. - cube.rotation.y = rot;
  83. - });
  84. renderer.render(scene, camera);
  85. - requestAnimationFrame(render);
  86. }
  87. -requestAnimationFrame(render);
  88. </pre>
  89. <p>我们这次只需要渲染一次</p>
  90. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">render();
  91. </pre>
  92. <p>我们需要在<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>改变摄像机设置的时候渲染场景.
  93. 幸好<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>提供了一个<code class="notranslate" translate="no">change</code>事件来监听变化</p>
  94. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">controls.addEventListener('change', render);
  95. </pre>
  96. <p>我们同样需要捕捉到用户改变窗口大小的情况. 在之前连续渲染的时候这种情况是自动处理的, 但是现在是按需渲染, 我们需要在窗口改变的时候显式<code class="notranslate" translate="no">resize</code>窗口大小</p>
  97. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">window.addEventListener('resize', render);
  98. </pre>
  99. <p>然后我们就实现了按需渲染的功能</p>
  100. <p></p><div translate="no" class="threejs_example_container notranslate">
  101. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/render-on-demand.html"></iframe></div>
  102. <a class="threejs_center" href="/manual/examples/render-on-demand.html" target="_blank">点击此处在新标签页中打开</a>
  103. </div>
  104. <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> 有个选项可以增加某种惯性, 让整个画面显得不那么僵硬. 我们启用<code class="notranslate" translate="no">enableDamping</code>来实现它<p></p>
  105. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">controls.enableDamping = true;
  106. </pre>
  107. <p>开启<code class="notranslate" translate="no">enableDamping</code>, 我们需要在渲染函数中调用<code class="notranslate" translate="no">controls.update</code>, 让<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a>可以丝滑地让摄像机移动. 但是, 这就意味着我们不能直接地在<code class="notranslate" translate="no">change</code>事件中调用<code class="notranslate" translate="no">render</code>, 如此这般会导致死循环. 控制器响应一个<code class="notranslate" translate="no">change</code>事件然后调用<code class="notranslate" translate="no">render</code>, 然后<code class="notranslate" translate="no">render</code>调用<code class="notranslate" translate="no">controls.update</code>. 这个方法会再发出另一个<code class="notranslate" translate="no">change</code>事件. </p>
  108. <p>我们可以通过使用<code class="notranslate" translate="no">requestAnimationFrame</code>调用<code class="notranslate" translate="no">render</code>, 但是需要确保仅仅在需要一个新帧的时候才执行. 如果没有请求</p>
  109. <p>我们可以通过使用<code class="notranslate" translate="no">requestAnimationFrame</code>调用<code class="notranslate" translate="no">render</code>来解决这个问题, 但是我们需要确保我们只在还没有请求一个新帧的情况下请求一个新帧, 我们可以通过一个变量来跟踪我们已经请求的帧</p>
  110. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+let renderRequested = false;
  111. function render() {
  112. + renderRequested = false;
  113. if (resizeRendererToDisplaySize(renderer)) {
  114. const canvas = renderer.domElement;
  115. camera.aspect = canvas.clientWidth / canvas.clientHeight;
  116. camera.updateProjectionMatrix();
  117. }
  118. renderer.render(scene, camera);
  119. }
  120. render();
  121. +function requestRenderIfNotRequested() {
  122. + if (!renderRequested) {
  123. + renderRequested = true;
  124. + requestAnimationFrame(render);
  125. + }
  126. +}
  127. -controls.addEventListener('change', render);
  128. +controls.addEventListener('change', requestRenderIfNotRequested);
  129. </pre>
  130. <p>我们应该会在改变窗口大小的时候用到<code class="notranslate" translate="no">requestRenderIfNotRequested</code></p>
  131. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-window.addEventListener('resize', render);
  132. +window.addEventListener('resize', requestRenderIfNotRequested);
  133. </pre>
  134. <p>可能很难看出来有什么不同. 试着点一下下面的例子, 然后用方向键移动, 或者拖拽旋转. 然后在上面的例子中做同样的事, 你应该能感觉出来区别. 上面的像是一帧帧在放幻灯片, 下面则是丝滑柔顺.</p>
  135. <p></p><div translate="no" class="threejs_example_container notranslate">
  136. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/render-on-demand-w-damping.html"></iframe></div>
  137. <a class="threejs_center" href="/manual/examples/render-on-demand-w-damping.html" target="_blank">点击此处在新标签页中打开</a>
  138. </div>
  139. <p></p>
  140. <p>让我们加一个简单的GUI</p>
  141. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  142. import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
  143. +import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
  144. </pre>
  145. <p>这个控制器可以改变每个立方体的颜色和在x方向缩放. 为了设置颜色我们用了<code class="notranslate" translate="no">ColorGUIHelper</code>, 这个在<a href="lights.html">光线</a>一章提到过</p>
  146. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  147. </pre>
  148. <p>对每一个立方体, 我们建一个折叠菜单, 一个是<code class="notranslate" translate="no">material.color</code>, 另一个是<code class="notranslate" translate="no">cube.scale.x</code></p>
  149. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeInstance(geometry, color, x) {
  150. const material = new THREE.MeshPhongMaterial({color});
  151. const cube = new THREE.Mesh(geometry, material);
  152. scene.add(cube);
  153. cube.position.x = x;
  154. + const folder = gui.addFolder(`Cube${x}`);
  155. + folder.addColor(new ColorGUIHelper(material, 'color'), 'value')
  156. + .name('color')
  157. + .onChange(requestRenderIfNotRequested);
  158. + folder.add(cube.scale, 'x', .1, 1.5)
  159. + .name('scale x')
  160. + .onChange(requestRenderIfNotRequested);
  161. + folder.open();
  162. return cube;
  163. }
  164. </pre>
  165. <p>上面的GUI用了一个<code class="notranslate" translate="no">onChange</code>方法, 在数值改变的时候调用传入一个回调函数. 这个例子中, 我们仅仅需要它调用<code class="notranslate" translate="no">requestRenderIfNotRequested</code>. <code class="notranslate" translate="no">folder.open</code>是使折叠菜单展开的方法</p>
  166. <p></p><div translate="no" class="threejs_example_container notranslate">
  167. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/render-on-demand-w-gui.html"></iframe></div>
  168. <a class="threejs_center" href="/manual/examples/render-on-demand-w-gui.html" target="_blank">点击此处在新标签页中打开</a>
  169. </div>
  170. <p></p>
  171. <p>我希望这篇文章能在将连续渲染改成按需渲染的时候给你一些启发. 按需渲染不像是连续渲染那么常见, 因为3D游戏或者艺术创作中必须要让场景动出来. 但是有些场合, 例如地图浏览器, 3D编辑器, 3D图产生器等等的, 可能还是按需渲染比较好. </p>
  172. </div>
  173. </div>
  174. </div>
  175. <script src="../resources/prettify.js"></script>
  176. <script src="../resources/lesson.js"></script>
  177. </body></html>