multiple-scenes.html 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  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">(Canvas)</code>,并为每张画布添加一个渲染器<code class="notranslate" translate="no">(Renderer)</code>。</p>
  33. <p>但是,这样你会遇到两个很明显的问题:</p>
  34. <blockquote>
  35. <ol>
  36. <li>浏览器限制了WebGL上下文<code class="notranslate" translate="no">(WebGL contexts)</code>的数量。</li>
  37. </ol>
  38. <p>通常浏览器将其限制为 8 个,一旦超出这个数量,最先创建的WebGL上下文就会被自动弃用。</p>
  39. <ol>
  40. <li>无法在不同的WebGL上下文中共享资源。</li>
  41. </ol>
  42. <p>不同WebGL上下文无法共享任何资源,这就意味着,假设你想要在两个<code class="notranslate" translate="no">Canvas</code>中各加载一个10Mb的模型,并且每个模型都20Mb的纹理,那么这个模型和纹理将分别被加载两次。因此,初始化、着色器编译等都将运行两次,随着<code class="notranslate" translate="no">Canvas</code>数量的增减,情况会变得与来越糟糕。</p>
  43. </blockquote>
  44. <p>那么,我们该如何解决这个问题?</p>
  45. <h2 id="-">基本方法</h2>
  46. <p>解决办法就是用一张<code class="notranslate" translate="no">Canvas</code>在整个背景中填充视口,并利用一些其他元素来代表每个“虚拟画布”<code class="notranslate" translate="no">(virtual canvas)</code>,即只在一张<code class="notranslate" translate="no">Canvas</code>中加载一个<a href="/docs/#api/zh/constants/Renderer"><code class="notranslate" translate="no">Renderer</code></a>,并为每个<code class="notranslate" translate="no">virtual canvas</code>创建一个场景<code class="notranslate" translate="no">(Scene)</code>。这样我们只需要确保每个<code class="notranslate" translate="no">virtual canvas</code>正确的位置,THREE.js就会将它们渲染在屏幕上相应的位置。</p>
  47. <p>利用这个方法,由于我们只添加了一张<code class="notranslate" translate="no">Canvas</code>,也就仅仅使用了一个<code class="notranslate" translate="no">WebGL contexts</code>,因此不仅解决了资源共享问题,且不会引发WebGL上下文数量限制问题。</p>
  48. <p>以一个只有两个<a href="/docs/#api/zh/scenes/Scene"><code class="notranslate" translate="no">Scene</code></a>的简单demo为例。首先,创建HTML结构:</p>
  49. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;canvas id="c"&gt;&lt;/canvas&gt;
  50. &lt;p&gt;
  51. &lt;span id="box" class="diagram left"&gt;&lt;/span&gt;
  52. I love boxes. Presents come in boxes.
  53. When I find a new box I'm always excited to find out what's inside.
  54. &lt;/p&gt;
  55. &lt;p&gt;
  56. &lt;span id="pyramid" class="diagram right"&gt;&lt;/span&gt;
  57. When I was a kid I dreamed of going on an expedition inside a pyramid
  58. and finding a undiscovered tomb full of mummies and treasure.
  59. &lt;/p&gt;
  60. </pre>
  61. <p>接着为它设置一些基本样式:</p>
  62. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#c {
  63. position: fixed;
  64. left: 0;
  65. top: 0;
  66. width: 100%;
  67. height: 100%;
  68. display: block;
  69. z-index: -1;
  70. }
  71. .diagram {
  72. display: inline-block;
  73. width: 5em;
  74. height: 3em;
  75. border: 1px solid black;
  76. }
  77. .left {
  78. float: left;
  79. margin-right: .25em;
  80. }
  81. .right {
  82. float: right;
  83. margin-left: .25em;
  84. }
  85. </pre>
  86. <p>我们将<code class="notranslate" translate="no">Canvas</code>画幅设置为充满整个屏幕,并将其<code class="notranslate" translate="no">z-index</code>设置为<code class="notranslate" translate="no">-1</code>,使它始终位于其他元素的后面。当然,我们要给<code class="notranslate" translate="no">virtual canvas</code>设置相应的宽高,因为此时还没有任何内容可以撑起它的大小。</p>
  87. <p>现在,创建两个<a href="/docs/#api/zh/scenes/Scene"><code class="notranslate" translate="no">Scene</code></a>,其中一个添加了立方体,另一个为菱形,并分别为这两个<a href="/docs/#api/zh/scenes/Scene"><code class="notranslate" translate="no">Scene</code></a>添加灯光<code class="notranslate" translate="no">(Light)</code>和相机<code class="notranslate" translate="no">(Camera)</code>。</p>
  88. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeScene(elem) {
  89. const scene = new THREE.Scene();
  90. const fov = 45;
  91. const aspect = 2; // the canvas default
  92. const near = 0.1;
  93. const far = 5;
  94. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  95. camera.position.z = 2;
  96. camera.position.set(0, 1, 2);
  97. camera.lookAt(0, 0, 0);
  98. {
  99. const color = 0xFFFFFF;
  100. const intensity = 1;
  101. const light = new THREE.DirectionalLight(color, intensity);
  102. light.position.set(-1, 2, 4);
  103. scene.add(light);
  104. }
  105. return {scene, camera, elem};
  106. }
  107. function setupScene1() {
  108. const sceneInfo = makeScene(document.querySelector('#box'));
  109. const geometry = new THREE.BoxGeometry(1, 1, 1);
  110. const material = new THREE.MeshPhongMaterial({color: 'red'});
  111. const mesh = new THREE.Mesh(geometry, material);
  112. sceneInfo.scene.add(mesh);
  113. sceneInfo.mesh = mesh;
  114. return sceneInfo;
  115. }
  116. function setupScene2() {
  117. const sceneInfo = makeScene(document.querySelector('#pyramid'));
  118. const radius = .8;
  119. const widthSegments = 4;
  120. const heightSegments = 2;
  121. const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
  122. const material = new THREE.MeshPhongMaterial({
  123. color: 'blue',
  124. flatShading: true,
  125. });
  126. const mesh = new THREE.Mesh(geometry, material);
  127. sceneInfo.scene.add(mesh);
  128. sceneInfo.mesh = mesh;
  129. return sceneInfo;
  130. }
  131. const sceneInfo1 = setupScene1();
  132. const sceneInfo2 = setupScene2();
  133. </pre>
  134. <p>接着创建一个视图信息获取函数<code class="notranslate" translate="no">renderSceneInfo()</code>和视图渲染函数<code class="notranslate" translate="no">render()</code>,用来渲染那些<code class="notranslate" translate="no">virtual canvas</code>所在的元素出现在了可视区域的<a href="/docs/#api/zh/scenes/Scene"><code class="notranslate" translate="no">Scene</code></a>。只需调用THREE.js的剪裁区域检测<a href="/docs/#api/zh/constants/Renderer.setScissorTest"><code class="notranslate" translate="no">Renderer.setScissorTest</code></a>方法,THREE.js就能实现仅渲染部分画布内容的功能,同时,我们需要调用<a href="/docs/#api/zh/constants/Renderer.setViewport"><code class="notranslate" translate="no">Renderer.setViewport</code></a>和<a href="/docs/#api/zh/constants/Renderer.setScissor"><code class="notranslate" translate="no">Renderer.setScissor</code></a>来分别设定视口大小和剪裁区域。</p>
  135. <p>参数说明如下:
  136. &gt;</p>
  137. <blockquote>
  138. <p>Renderer.setScissorTest( boolean : Boolean ) : null;
  139. // 启用或禁用剪裁检测. 若启用,则只有在所定义的裁剪区域内的像素才会受之后的渲染器影响。
  140. Renderer.setScissor ( x : Integer, y : Integer, width : Integer, height : Integer ) : null;
  141. //将剪裁区域设为(x, y)到(x + width, y + height)
  142. Renderer.### <a href="">setViewport</a> ( x : Integer, y : Integer, width : Integer, height : Integer ) : null
  143. //将视口大小设置为(x, y)到 (x + width, y + height).</p>
  144. </blockquote>
  145. <p>视图信息获取函数如下:</p>
  146. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function renderSceneInfo(sceneInfo) {
  147. const {scene, camera, elem} = sceneInfo;
  148. // get the viewport relative position of this element
  149. const {left, right, top, bottom, width, height} =
  150. elem.getBoundingClientRect();
  151. const isOffscreen =
  152. bottom &lt; 0 ||
  153. top &gt; renderer.domElement.clientHeight ||
  154. right &lt; 0 ||
  155. left &gt; renderer.domElement.clientWidth;
  156. if (isOffscreen) {
  157. return;
  158. }
  159. camera.aspect = width / height;
  160. camera.updateProjectionMatrix();
  161. const positiveYUpBottom = canvasRect.height - bottom;
  162. renderer.setScissor(left, positiveYUpBottom, width, height);
  163. renderer.setViewport(left, positiveYUpBottom, width, height);
  164. renderer.render(scene, camera);
  165. }
  166. </pre>
  167. <p>视图渲染函数如下:</p>
  168. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  169. time *= 0.001;
  170. resizeRendererToDisplaySize(renderer);
  171. renderer.setScissorTest(false);
  172. renderer.clear(true, true);
  173. renderer.setScissorTest(true);
  174. sceneInfo1.mesh.rotation.y = time * .1;
  175. sceneInfo2.mesh.rotation.y = time * .1;
  176. renderSceneInfo(sceneInfo1);
  177. renderSceneInfo(sceneInfo2);
  178. requestAnimationFrame(render);
  179. }
  180. </pre>
  181. <p>最终效果如下:</p>
  182. <p></p><div translate="no" class="threejs_example_container notranslate">
  183. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/threejs-multiple-scenes-v1.html"></iframe></div>
  184. <a class="threejs_center" href="/manual/examples/threejs-multiple-scenes-v1.html" target="_blank">点击此处在新标签页中打开</a>
  185. </div>
  186. <p></p>
  187. <p>可以看到,两个物体被分别渲染到了对应的位置。</p>
  188. <h2 id="-">同步滚动</h2>
  189. <p>虽然我们已经实现了同时渲染多个场景的功能,但是上面的代码依然存在一个问题,如果<code class="notranslate" translate="no">Scenes</code>过于复杂、或者由于其他原因需要更长时间渲染,那么画布中<code class="notranslate" translate="no">Scenes</code>渲染的位置总是会落后于页面的其他元素,如页面滚动时会出现明显的滞后。</p>
  190. <p>为了更直观的观察这个现象,我们给每个<a href="/docs/#api/zh/scenes/Scene"><code class="notranslate" translate="no">Scene</code></a>加上边框,并设置背景颜色:</p>
  191. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">.diagram {
  192. display: inline-block;
  193. width: 5em;
  194. height: 3em;
  195. + border: 1px solid black;
  196. }
  197. </pre>
  198. <p>给每个场景设置背景颜色</p>
  199. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  200. +scene.background = new THREE.Color('red');
  201. </pre>
  202. <p>此时,我们快速滚动屏幕,就会发现这个问题。屏幕滚动时的动画放慢十倍后的效果如下:</p>
  203. <p>为了解决这个问题,先将<code class="notranslate" translate="no">Canvas</code>的定位方式由<code class="notranslate" translate="no">position: fixed</code> 改为<code class="notranslate" translate="no">position: absolute</code>。</p>
  204. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#c {
  205. - position: fixed;
  206. + position: absolute;
  207. </pre>
  208. <p>为了解决这个问题,先将<code class="notranslate" translate="no">Canvas</code>的定位方式由<code class="notranslate" translate="no">position: fixed</code> 改为<code class="notranslate" translate="no">position: absolute</code>。</p>
  209. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  210. ...
  211. const transform = `translateY(${window.scrollY}px)`;
  212. renderer.domElement.style.transform = transform;
  213. </pre>
  214. <p><code class="notranslate" translate="no">position: fixed</code> 会完全禁用画布的滚动,无论其他元素是否已经滚动到它的上;
  215. <code class="notranslate" translate="no">position: absolute</code>则会保持画布与页面的其余部分一起滚动,这意味着我们绘制的任何东西都会与页面一起滚动,就算还未完全渲染出来。当场景完成渲染之后,然后移动画布,场景会与页面被滚动后的位置相匹配,并重新渲染,这就意味着,只有窗口的边缘会显示出一些还未被渲染的数据,当时页面中的场景不会出现这种现象。下面时利用以上方法后的效果(动画同样放慢了10倍)。</p>
  216. <h2 id="-">让它更加通用</h2>
  217. <p>现在,我们已经实现了在一个<code class="notranslate" translate="no">Canvas</code>中渲染多个场景的功能,接下来就来处理一下让它更加好用些。</p>
  218. <p>我们可以封装一个主渲染函数用来管理整个<code class="notranslate" translate="no">Canvas</code>,并定义一个场景元素列表和他们对应的场景初始化函数。对于每个元素,它将检查该元素是否滚动到了可视区域并调用相应的场景初始化函数。这样我们就构建了一个渲染系统,在这个系统中每个独立的<code class="notranslate" translate="no">scenes</code>都会在它们各自定义的空间内独立渲染且不互相影响。</p>
  219. <p>主渲染函数如下:</p>
  220. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sceneElements = [];
  221. function addScene(elem, fn) {
  222. sceneElements.push({elem, fn});
  223. }
  224. function render(time) {
  225. time *= 0.001;
  226. resizeRendererToDisplaySize(renderer);
  227. renderer.setScissorTest(false);
  228. renderer.setClearColor(clearColor, 0);
  229. renderer.clear(true, true);
  230. renderer.setScissorTest(true);
  231. const transform = `translateY(${window.scrollY}px)`;
  232. renderer.domElement.style.transform = transform;
  233. for (const {elem, fn} of sceneElements) {
  234. // get the viewport relative position of this element
  235. const rect = elem.getBoundingClientRect();
  236. const {left, right, top, bottom, width, height} = rect;
  237. const isOffscreen =
  238. bottom &lt; 0 ||
  239. top &gt; renderer.domElement.clientHeight ||
  240. right &lt; 0 ||
  241. left &gt; renderer.domElement.clientWidth;
  242. if (!isOffscreen) {
  243. const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
  244. renderer.setScissor(left, positiveYUpBottom, width, height);
  245. renderer.setViewport(left, positiveYUpBottom, width, height);
  246. fn(time, rect);
  247. }
  248. }
  249. requestAnimationFrame(render);
  250. }
  251. </pre>
  252. <p>从中可以看出,这个函数将遍历每一个包含了所有<a href="/docs/#api/zh/scenes/Scene"><code class="notranslate" translate="no">Scene</code></a>元素的数组对象,且每个元素都由各自的<code class="notranslate" translate="no">elem</code>和<code class="notranslate" translate="no">fn</code>属性。</p>
  253. <p>这个函数将检查每个<a href="/docs/#api/zh/scenes/Scene"><code class="notranslate" translate="no">Scene</code></a>元素是否进入可视区域,一旦进入就会调用它的场景初始化函数,并传给它当前的时间和对应的尺寸位置信息。</p>
  254. <p>现在,把每个<a href="/docs/#api/zh/scenes/Scene"><code class="notranslate" translate="no">Scene</code></a>的信息添加到数组列表中:</p>
  255. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">{
  256. const elem = document.querySelector('#box');
  257. const {scene, camera} = makeScene();
  258. const geometry = new THREE.BoxGeometry(1, 1, 1);
  259. const material = new THREE.MeshPhongMaterial({color: 'red'});
  260. const mesh = new THREE.Mesh(geometry, material);
  261. scene.add(mesh);
  262. addScene(elem, (time, rect) =&gt; {
  263. camera.aspect = rect.width / rect.height;
  264. camera.updateProjectionMatrix();
  265. mesh.rotation.y = time * .1;
  266. renderer.render(scene, camera);
  267. });
  268. }
  269. {
  270. const elem = document.querySelector('#pyramid');
  271. const {scene, camera} = makeScene();
  272. const radius = .8;
  273. const widthSegments = 4;
  274. const heightSegments = 2;
  275. const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
  276. const material = new THREE.MeshPhongMaterial({
  277. color: 'blue',
  278. flatShading: true,
  279. });
  280. const mesh = new THREE.Mesh(geometry, material);
  281. scene.add(mesh);
  282. addScene(elem, (time, rect) =&gt; {
  283. camera.aspect = rect.width / rect.height;
  284. camera.updateProjectionMatrix();
  285. mesh.rotation.y = time * .1;
  286. renderer.render(scene, camera);
  287. });
  288. }
  289. </pre>
  290. <p>至此,我们不再需要分别定义<code class="notranslate" translate="no">sceneInfo1</code> 和 <code class="notranslate" translate="no">sceneInfo2</code>,但每个场景对应的场景初始化函数都已生效。</p>
  291. <p></p><div translate="no" class="threejs_example_container notranslate">
  292. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/threejs-multiple-scenes-generic.html"></iframe></div>
  293. <a class="threejs_center" href="/manual/examples/threejs-multiple-scenes-generic.html" target="_blank">点击此处在新标签页中打开</a>
  294. </div>
  295. <p></p>
  296. <h3 id="-html-dataset">使用HTML Dataset</h3>
  297. <p>更好用的最后一步就是使用HTML <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset">dataset</a>,这是一种将自己的数据添加到HTML元素中的方法,我们不再使用<code class="notranslate" translate="no">id="..."</code>,而是使用<code class="notranslate" translate="no">data-diagram="..."</code>,就像这样:</p>
  298. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;canvas id="c"&gt;&lt;/canvas&gt;
  299. &lt;p&gt;
  300. - &lt;span id="box" class="diagram left"&gt;&lt;/span&gt;
  301. + &lt;span data-diagram="box" class="left"&gt;&lt;/span&gt;
  302. I love boxes. Presents come in boxes.
  303. When I find a new box I'm always excited to find out what's inside.
  304. &lt;/p&gt;
  305. &lt;p&gt;
  306. - &lt;span id="pyramid" class="diagram left"&gt;&lt;/span&gt;
  307. + &lt;span data-diagram="pyramid" class="right"&gt;&lt;/span&gt;
  308. When I was a kid I dreamed of going on an expedition inside a pyramid
  309. and finding a undiscovered tomb full of mummies and treasure.
  310. &lt;/p&gt;
  311. </pre>
  312. <p>同时修改CSS选择器</p>
  313. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">-.diagram
  314. +*[data-diagram] {
  315. display: inline-block;
  316. width: 5em;
  317. height: 3em;
  318. }
  319. </pre>
  320. <p>现在,我们构建一个对象,用来映射每个场景对应的场景初始化函数,并返回一个场景渲染函数。</p>
  321. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sceneInitFunctionsByName = {
  322. 'box': () =&gt; {
  323. const {scene, camera} = makeScene();
  324. const geometry = new THREE.BoxGeometry(1, 1, 1);
  325. const material = new THREE.MeshPhongMaterial({color: 'red'});
  326. const mesh = new THREE.Mesh(geometry, material);
  327. scene.add(mesh);
  328. return (time, rect) =&gt; {
  329. mesh.rotation.y = time * .1;
  330. camera.aspect = rect.width / rect.height;
  331. camera.updateProjectionMatrix();
  332. renderer.render(scene, camera);
  333. };
  334. },
  335. 'pyramid': () =&gt; {
  336. const {scene, camera} = makeScene();
  337. const radius = .8;
  338. const widthSegments = 4;
  339. const heightSegments = 2;
  340. const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
  341. const material = new THREE.MeshPhongMaterial({
  342. color: 'blue',
  343. flatShading: true,
  344. });
  345. const mesh = new THREE.Mesh(geometry, material);
  346. scene.add(mesh);
  347. return (time, rect) =&gt; {
  348. mesh.rotation.y = time * .1;
  349. camera.aspect = rect.width / rect.height;
  350. camera.updateProjectionMatrix();
  351. renderer.render(scene, camera);
  352. };
  353. },
  354. };
  355. </pre>
  356. <p>我们还需要获取所有的<code class="notranslate" translate="no">diagrams</code>,并调用初始化函数。</p>
  357. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">document.querySelectorAll('[data-diagram]').forEach((elem) =&gt; {
  358. const sceneName = elem.dataset.diagram;
  359. const sceneInitFunction = sceneInitFunctionsByName[sceneName];
  360. const sceneRenderFunction = sceneInitFunction(elem);
  361. addScene(elem, sceneRenderFunction);
  362. });
  363. </pre>
  364. <p>经过这番改造,页面的呈现效果没有发生变化,但代码更加通用了。</p>
  365. <p></p>
  366. <h2 id="-">给每个元素增加控制器</h2>
  367. <p>当需要交互时,我们需要为每个场景分别添加交互控件,如<code class="notranslate" translate="no">TrackballControls</code>。首先,需要引入该控件。</p>
  368. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {TrackballControls} from 'three/addons/controls/TrackballControls.js';
  369. </pre>
  370. <p>接着给每个元素增加控制器:</p>
  371. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function makeScene() {
  372. +function makeScene(elem) {
  373. const scene = new THREE.Scene();
  374. const fov = 45;
  375. const aspect = 2; // the canvas default
  376. const near = 0.1;
  377. const far = 5;
  378. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  379. camera.position.set(0, 1, 2);
  380. camera.lookAt(0, 0, 0);
  381. + scene.add(camera);
  382. + const controls = new TrackballControls(camera, elem);
  383. + controls.noZoom = true;
  384. + controls.noPan = true;
  385. {
  386. const color = 0xFFFFFF;
  387. const intensity = 1;
  388. const light = new THREE.DirectionalLight(color, intensity);
  389. light.position.set(-1, 2, 4);
  390. - scene.add(light);
  391. + camera.add(light);
  392. }
  393. - return {scene, camera};
  394. + return {scene, camera, controls};
  395. }
  396. </pre>
  397. <p>从中可以看到,我们将<code class="notranslate" translate="no">camera</code>添加到<code class="notranslate" translate="no">scene</code>中,而<code class="notranslate" translate="no">light</code>则添加到<code class="notranslate" translate="no">camera</code>上,这样可以保证<code class="notranslate" translate="no">light</code>始终与<code class="notranslate" translate="no">camera</code>相关联。因此,当我们通过控制器旋转<code class="notranslate" translate="no">camera</code>的视角时,<code class="notranslate" translate="no">light</code>会始终照亮这个视角。</p>
  398. <p>我们还需要在渲染函数中更新这些控件:</p>
  399. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sceneInitFunctionsByName = {
  400. - 'box': () =&gt; {
  401. - const {scene, camera} = makeScene();
  402. + 'box': (elem) =&gt; {
  403. + const {scene, camera, controls} = makeScene(elem);
  404. const geometry = new THREE.BoxGeometry(1, 1, 1);
  405. const material = new THREE.MeshPhongMaterial({color: 'red'});
  406. const mesh = new THREE.Mesh(geometry, material);
  407. scene.add(mesh);
  408. return (time, rect) =&gt; {
  409. mesh.rotation.y = time * .1;
  410. camera.aspect = rect.width / rect.height;
  411. camera.updateProjectionMatrix();
  412. + controls.handleResize();
  413. + controls.update();
  414. renderer.render(scene, camera);
  415. };
  416. },
  417. - 'pyramid': () =&gt; {
  418. - const {scene, camera} = makeScene();
  419. + 'pyramid': (elem) =&gt; {
  420. + const {scene, camera, controls} = makeScene(elem);
  421. const radius = .8;
  422. const widthSegments = 4;
  423. const heightSegments = 2;
  424. const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
  425. const material = new THREE.MeshPhongMaterial({
  426. color: 'blue',
  427. flatShading: true,
  428. });
  429. const mesh = new THREE.Mesh(geometry, material);
  430. scene.add(mesh);
  431. return (time, rect) =&gt; {
  432. mesh.rotation.y = time * .1;
  433. camera.aspect = rect.width / rect.height;
  434. camera.updateProjectionMatrix();
  435. + controls.handleResize();
  436. + controls.update();
  437. renderer.render(scene, camera);
  438. };
  439. },
  440. };
  441. </pre>
  442. <p>现在,控制器已经生效了,你可以拖动来查看效果:</p>
  443. <p></p><div translate="no" class="threejs_example_container notranslate">
  444. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/threejs-multiple-scenes-controls.html"></iframe></div>
  445. <a class="threejs_center" href="/manual/examples/threejs-multiple-scenes-controls.html" target="_blank">点击此处在新标签页中打开</a>
  446. </div>
  447. 上面提到的方法在本网站上可以找到很多实例,比如<a href="https://threejsfundamentals.org/threejs/lessons/threejs-primitives.html">Three.js 图元</a>和<a href="https://threejsfundamentals.org/threejs/lessons/threejs-materials.html">Three.js 材质</a> 这两篇文章。<p></p>
  448. <h2 id="-">另一个方法</h2>
  449. <p>还有一个方法也可以实现这种效果,原理是渲染到屏幕外的画布上,并将结果复制到对应的2D画布上。这个方法的优点是对如何组合每个独立区域没有限制,因此只需正常编写HTML即可。而第一种方法则需要在背景设置一个<code class="notranslate" translate="no">Canvas</code>。</p>
  450. <p>但这个方法的缺点就是速度较慢,因为每个区域都必须进行复制,因此速度快慢取决于浏览器本身和GPU的性能。</p>
  451. <p>而这种方法所需改动的代码也很少。</p>
  452. <p>第一步,不再需要HTML上的Canvas元素了:</p>
  453. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  454. - &lt;canvas id="c"&gt;&lt;/canvas&gt;
  455. ...
  456. &lt;/body&gt;
  457. </pre>
  458. <p>画布的样式也需要改一下:</p>
  459. <pre class="prettyprint showlinemods notranslate notranslate" translate="no">-#c {
  460. - position: absolute;
  461. - left: 0;
  462. - top: 0;
  463. - width: 100%;
  464. - height: 100%;
  465. - display: block;
  466. - z-index: -1;
  467. -}
  468. canvas {
  469. width: 100%;
  470. height: 100%;
  471. display: block;
  472. }
  473. *[data-diagram] {
  474. display: inline-block;
  475. width: 5em;
  476. height: 3em;
  477. }
  478. </pre><p>这样可以保证所有的<code class="notranslate" translate="no">canvas</code>都能填满他们的容器。</p>
  479. <p>接下来还需要修改一下JavaScript代码,不需要再查找<code class="notranslate" translate="no">canvas</code>元素了,取而代之的是需要创建一个,并且在一开始就要开启可视区域检测功能:</p>
  480. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  481. - const canvas = document.querySelector('#c');
  482. + const canvas = document.createElement('canvas');
  483. const renderer = new THREE.WebGLRenderer({antialias: true, canvas, alpha: true});
  484. + renderer.setScissorTest(true);
  485. ...
  486. </pre>
  487. <p>然后,对于每个场景,我们创建一个二维渲染上下文,并将其画布添加到该场景对应的元素中:</p>
  488. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const sceneElements = [];
  489. function addScene(elem, fn) {
  490. + const ctx = document.createElement('canvas').getContext('2d');
  491. + elem.appendChild(ctx.canvas);
  492. - sceneElements.push({elem, fn});
  493. + sceneElements.push({elem, ctx, fn});
  494. }
  495. </pre>
  496. <p>在渲染时,如果渲染器的画布不够大导致无法渲染在这个区域,就增加其大小;如果这个区域的画布大小错误,就改变它的大小。最后,设置剪裁区域和视口大小、渲染该区域的场景并将结果复制到该区域的画布上。</p>
  497. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  498. time *= 0.001;
  499. - resizeRendererToDisplaySize(renderer);
  500. -
  501. - renderer.setScissorTest(false);
  502. - renderer.setClearColor(clearColor, 0);
  503. - renderer.clear(true, true);
  504. - renderer.setScissorTest(true);
  505. -
  506. - const transform = `translateY(${window.scrollY}px)`;
  507. - renderer.domElement.style.transform = transform;
  508. - for (const {elem, fn} of sceneElements) {
  509. + for (const {elem, fn, ctx} of sceneElements) {
  510. // get the viewport relative position of this element
  511. const rect = elem.getBoundingClientRect();
  512. const {left, right, top, bottom, width, height} = rect;
  513. + const rendererCanvas = renderer.domElement;
  514. const isOffscreen =
  515. bottom &lt; 0 ||
  516. - top &gt; renderer.domElement.clientHeight ||
  517. + top &gt; window.innerHeight ||
  518. right &lt; 0 ||
  519. - left &gt; renderer.domElement.clientWidth;
  520. + left &gt; window.innerWidth;
  521. if (!isOffscreen) {
  522. - const positiveYUpBottom = renderer.domElement.clientHeight - bottom;
  523. - renderer.setScissor(left, positiveYUpBottom, width, height);
  524. - renderer.setViewport(left, positiveYUpBottom, width, height);
  525. + // make sure the renderer's canvas is big enough
  526. + if (rendererCanvas.width &lt; width || rendererCanvas.height &lt; height) {
  527. + renderer.setSize(width, height, false);
  528. + }
  529. +
  530. + // make sure the canvas for this area is the same size as the area
  531. + if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
  532. + ctx.canvas.width = width;
  533. + ctx.canvas.height = height;
  534. + }
  535. +
  536. + renderer.setScissor(0, 0, width, height);
  537. + renderer.setViewport(0, 0, width, height);
  538. fn(time, rect);
  539. + // copy the rendered scene to this element's canvas
  540. + ctx.globalCompositeOperation = 'copy';
  541. + ctx.drawImage(
  542. + rendererCanvas,
  543. + 0, rendererCanvas.height - height, width, height, // src rect
  544. + 0, 0, width, height); // dst rect
  545. }
  546. }
  547. requestAnimationFrame(render);
  548. }
  549. </pre>
  550. <p>最终结果与方法一一样:</p>
  551. <p></p><div translate="no" class="threejs_example_container notranslate">
  552. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/threejs-multiple-scenes-copy-canvas.html"></iframe></div>
  553. <a class="threejs_center" href="/manual/examples/threejs-multiple-scenes-copy-canvas.html" target="_blank">点击此处在新标签页中打开</a>
  554. </div>
  555. <p></p>
  556. <h2 id="-">更新的方法</h2>
  557. <p>还有一种方法是利用<a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas"><code class="notranslate" translate="no">OffscreenCanvas</code></a>方法,但是截至2020年7月,只有Chrome支持这个方法,感兴趣的小伙伴可以点击查看文档。</p>
  558. </div>
  559. </div>
  560. </div>
  561. <script src="../resources/prettify.js"></script>
  562. <script src="../resources/lesson.js"></script>
  563. </body></html>