offscreencanvas.html 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037
  1. <!DOCTYPE html><html lang="ja"><head>
  2. <meta charset="utf-8">
  3. <title>のOffscreenCanvas</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 – のOffscreenCanvas">
  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="/manual/resources/lesson.css">
  12. <link rel="stylesheet" href="/manual/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. </head>
  24. <body>
  25. <div class="container">
  26. <div class="lesson-title">
  27. <h1>のOffscreenCanvas</h1>
  28. </div>
  29. <div class="lesson">
  30. <div class="lesson-main">
  31. <p><a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas"><code class="notranslate" translate="no">OffscreenCanvas</code></a>は新しいブラウザの機能で現在はChromeでしか利用できませんが、他のブラウザにも来るようです。
  32. <code class="notranslate" translate="no">OffscreenCanvas</code> はWeb Workerでキャンバスにレンダリングできます。
  33. 複雑な3Dシーンのレンダリングなど重い作業をWeb Workerで行い負荷を軽減させ、ブラウザのレスポンスを低下させない方法です。
  34. また、データが読み込まれWorkerで解析されてるのでページ読み込み中にページ表示の途切れは少ないでしょう。</p>
  35. <p>OffscreenCanvasの利用を<em>開始</em>するのは非常に簡単です。
  36. <a href="responsive.html">レスポンシブデザインの記事</a>から3つのキューブを回転させるコードに修正してみましょう。</p>
  37. <p>通常はWorkerのコードを別ファイルに分離しますが、このサイトのほとんどのサンプルコードではスクリプトをHTMLファイルに埋め込んでいます。</p>
  38. <p>ここでは <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> というファイルを作成し、<a href="responsive.html">レスポンシブデザインの例</a>から全てのJavaScriptをコピーして下さい。
  39. そして、Workerで実行するために必要な変更を行います。</p>
  40. <p>HTMLファイルにはJavaScriptのいくつかの処理が必要です。
  41. まず最初に行う必要があるのはキャンバスを検索し、<code class="notranslate" translate="no">canvas.transferControlToOffscreen</code> 呼び出してキャンバスのコントロールをオフスクリーンに転送します。</p>
  42. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  43. const canvas = document.querySelector('#c');
  44. const offscreen = canvas.transferControlToOffscreen();
  45. ...
  46. </pre>
  47. <p><code class="notranslate" translate="no">new Worker(pathToScript, {type: 'module'})</code>でWorkerを起動し、<code class="notranslate" translate="no">offscreen</code> オブジェクトを渡します。</p>
  48. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  49. const canvas = document.querySelector('#c');
  50. const offscreen = canvas.transferControlToOffscreen();
  51. const worker = new Worker('offscreencanvas-cubes.js', {type: 'module'});
  52. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  53. }
  54. main();
  55. </pre>
  56. <p>ここで重要なのはWorkerが <code class="notranslate" translate="no">DOM</code> にアクセスできない事です。
  57. HTML要素の参照やマウスイベントやキーボードイベントを受け取る事もできません。
  58. Workerは、送られたメッセージに返信してWebページにメッセージを送り返す事だけです。</p>
  59. <p>Workerにメッセージを送信するには<a href="https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage"><code class="notranslate" translate="no">worker.postMessage</code></a>を呼び出し、1つまたは2つの引数を渡します。
  60. 1つ目の引数は<a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm">クローン</a>されるJavaScriptオブジェクトでWorkerに送ります。
  61. 2番目の引数は任意でWorkerに <em>転送</em> したい最初のオブジェクトです。
  62. このオブジェクトはクローンされません。
  63. その代わりに <em>転送</em> され、メインページには存在しなくなります。
  64. 存在しなくなるというのはおそらく間違った説明であり、むしろ取り除かれます。
  65. クローンではなく、特定のタイプのオブジェクトのみを転送する事ができます。
  66. 転送するオブジェクトには <code class="notranslate" translate="no">OffscreenCanvas</code> が含まれているので、1度転送した <code class="notranslate" translate="no">offscreen</code> オブジェクトをメインページに戻しても意味がありません。</p>
  67. <p>Workerは <code class="notranslate" translate="no">onmessage</code> ハンドラからメッセージを受け取ります。
  68. <code class="notranslate" translate="no">postMessage</code> に渡したオブジェクトはWorkerの <code class="notranslate" translate="no">onmessage</code> ハンドラに渡され <code class="notranslate" translate="no">event.data</code> を更新します。
  69. 上記のコードではWorkerに渡すオブジェクトに <code class="notranslate" translate="no">type: 'main'</code> を宣言しています。
  70. このオブジェクトはブラウザには何の意味もありません。Workerで使うためだけのものです。
  71. <code class="notranslate" translate="no">type</code> に基づいて、Worker内で別の関数を呼び出すハンドラを作成します。
  72. あとは必要に応じて関数を追加し、メインページから簡単に呼び出す事ができます。</p>
  73. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const handlers = {
  74. main,
  75. };
  76. self.onmessage = function(e) {
  77. const fn = handlers[e.data.type];
  78. if (typeof fn !== 'function') {
  79. throw new Error('no handler for type: ' + e.data.type);
  80. }
  81. fn(e.data);
  82. };
  83. </pre>
  84. <p>上記コードのように <code class="notranslate" translate="no">type</code> に基づいてハンドラを検索し、メインページから送られてきた <code class="notranslate" translate="no">data</code> を渡します。
  85. あとは<a href="responsive.html">レスポンシブデザインの記事</a>から <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> に貼り付けた <code class="notranslate" translate="no">main</code> を変更するだけです。</p>
  86. <p>DOMからキャンバスを探すのではなく、イベントデータからキャンバスを受け取ります。</p>
  87. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function main() {
  88. - const canvas = document.querySelector('#c');
  89. +function main(data) {
  90. + const {canvas} = data;
  91. const renderer = new THREE.WebGLRenderer({canvas});
  92. ...
  93. </pre>
  94. <p>最初の問題はWorkerからDOMを参照できず、<code class="notranslate" translate="no">resizeRendererToDisplaySize</code> が <code class="notranslate" translate="no">canvas.clientWidth</code> と <code class="notranslate" translate="no">canvas.clientHeight</code> を参照できない事です。
  95. <code class="notranslate" translate="no">clientWidth</code> と <code class="notranslate" translate="no">canvas.clientHeight</code> はDOMの値です。</p>
  96. <p>元のコードは以下の通りです。</p>
  97. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
  98. const canvas = renderer.domElement;
  99. const width = canvas.clientWidth;
  100. const height = canvas.clientHeight;
  101. const needResize = canvas.width !== width || canvas.height !== height;
  102. if (needResize) {
  103. renderer.setSize(width, height, false);
  104. }
  105. return needResize;
  106. }
  107. </pre>
  108. <p>DOMを参照できないため、変更したサイズの値をWorkerに送る必要があります。
  109. そこでグローバルな状態を追加し、幅と高さを維持するようにしましょう。</p>
  110. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const state = {
  111. width: 300, // canvas default
  112. height: 150, // canvas default
  113. };
  114. </pre>
  115. <p>これらの値を更新するための <code class="notranslate" translate="no">'size'</code> ハンドラを追加してみます。</p>
  116. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function size(data) {
  117. + state.width = data.width;
  118. + state.height = data.height;
  119. +}
  120. const handlers = {
  121. main,
  122. + size,
  123. };
  124. </pre>
  125. <p>これで <code class="notranslate" translate="no">resizeRendererToDisplaySize</code> を変更すると <code class="notranslate" translate="no">state.width</code> と <code class="notranslate" translate="no">state.height</code> が使えるようになりました。</p>
  126. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
  127. const canvas = renderer.domElement;
  128. - const width = canvas.clientWidth;
  129. - const height = canvas.clientHeight;
  130. + const width = state.width;
  131. + const height = state.height;
  132. const needResize = canvas.width !== width || canvas.height !== height;
  133. if (needResize) {
  134. renderer.setSize(width, height, false);
  135. }
  136. return needResize;
  137. }
  138. </pre>
  139. <p>以下も同様の変更が必要です。</p>
  140. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  141. time *= 0.001;
  142. if (resizeRendererToDisplaySize(renderer)) {
  143. - camera.aspect = canvas.clientWidth / canvas.clientHeight;
  144. + camera.aspect = state.width / state.height;
  145. camera.updateProjectionMatrix();
  146. }
  147. ...
  148. </pre>
  149. <p>メインページに戻りページのリサイズの度に <code class="notranslate" translate="no">size</code> イベントを送信します。</p>
  150. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
  151. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  152. +function sendSize() {
  153. + worker.postMessage({
  154. + type: 'size',
  155. + width: canvas.clientWidth,
  156. + height: canvas.clientHeight,
  157. + });
  158. +}
  159. +
  160. +window.addEventListener('resize', sendSize);
  161. +sendSize();
  162. </pre>
  163. <p>初期サイズを送るために1度sendSizeを呼んでいます。</p>
  164. <p>ブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> を完全にサポートしていると仮定して、これらの変更を行うだけで動作するはずです。
  165. 実行する前にブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> を実際にサポートしているか確認し、サポートしていない場合はエラーを表示してみましょう。
  166. まずはエラーを表示するためのHTMLを追加します。</p>
  167. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  168. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  169. + &lt;div id="noOffscreenCanvas" style="display:none;"&gt;
  170. + &lt;div&gt;no OffscreenCanvas support&lt;/div&gt;
  171. + &lt;/div&gt;
  172. &lt;/body&gt;
  173. </pre>
  174. <p>そして、CSSを追加します。</p>
  175. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">#noOffscreenCanvas {
  176. display: flex;
  177. width: 100vw;
  178. height: 100vh;
  179. align-items: center;
  180. justify-content: center;
  181. background: red;
  182. color: white;
  183. }
  184. </pre>
  185. <p>ブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> をサポートしているか確認するためには <code class="notranslate" translate="no">transferControlToOffscreen</code> を呼びます。</p>
  186. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  187. const canvas = document.querySelector('#c');
  188. + if (!canvas.transferControlToOffscreen) {
  189. + canvas.style.display = 'none';
  190. + document.querySelector('#noOffscreenCanvas').style.display = '';
  191. + return;
  192. + }
  193. const offscreen = canvas.transferControlToOffscreen();
  194. const worker = new Worker('offscreencanvas-picking.js', {type: 'module});
  195. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  196. ...
  197. </pre>
  198. <p>ブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> をサポートしていれば、このサンプルは動作するはずです。</p>
  199. <p></p><div translate="no" class="threejs_example_container notranslate">
  200. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas.html"></iframe></div>
  201. <a class="threejs_center" href="/manual/examples/offscreencanvas.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
  202. </div>
  203. <p></p>
  204. <p>これは素晴らしい事ですが、今の所は全てのブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> をサポートしている訳ではなく、
  205. <code class="notranslate" translate="no">OffscreenCanvas</code> サポートありとサポートなしの両方で動作するコードに変更し、サポートなしの場合はメインページのキャンバスを通常のように表示します。</p>
  206. <blockquote>
  207. <p>余談ですがページをレスポンシブにするためにOffscreenCanvasが必要な場合、フォールバックを持つ意味がよくわかりません。
  208. メインページで実行するかWorkerで実行するかには、Workerで実行している時にメインページで実行している時よりも多くの事ができるように
  209. 調整するかもしれません。何をするかは本当にあなた次第です。</p>
  210. </blockquote>
  211. <p>まず最初にthree.jsのコードとWorkerの固有コードを分離しましょう。
  212. これでメインページとWorkerの両方で同じコードを使う事ができます。
  213. つまり、3つのファイルを持つ事になります。</p>
  214. <ol>
  215. <li><p>htmlファイル</p>
  216. <p><code class="notranslate" translate="no">threejs-offscreencanvas-w-fallback.html</code></p>
  217. </li>
  218. <li><p>three.jsを含むJavaScriptコード</p>
  219. <p><code class="notranslate" translate="no">shared-cubes.js</code></p>
  220. </li>
  221. <li><p>workerをサポートするコード</p>
  222. <p><code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code></p>
  223. </li>
  224. </ol>
  225. <p><code class="notranslate" translate="no">shared-cubes.js</code> と <code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code> は前の <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> ファイルを分割したものです。</p>
  226. <p>まず <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> を全て <code class="notranslate" translate="no">shared-cube.js</code> にコピーします。
  227. 次にHTMLファイルには既に <code class="notranslate" translate="no">main</code> があり、<code class="notranslate" translate="no">init</code> と <code class="notranslate" translate="no">state</code> をエクスポートする必要があるため <code class="notranslate" translate="no">main</code> の名前を <code class="notranslate" translate="no">init</code> に変更します。</p>
  228. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from '../../build/three.module.js';
  229. -const state = {
  230. +export const state = {
  231. width: 300, // canvas default
  232. height: 150, // canvas default
  233. };
  234. -function main(data) {
  235. +export function init(data) {
  236. const {canvas} = data;
  237. const renderer = new THREE.WebGLRenderer({canvas});
  238. </pre>
  239. <p>そして、three.js関連以外の部分だけを切り取ります。</p>
  240. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function size(data) {
  241. - state.width = data.width;
  242. - state.height = data.height;
  243. -}
  244. -
  245. -const handlers = {
  246. - main,
  247. - size,
  248. -};
  249. -
  250. -self.onmessage = function(e) {
  251. - const fn = handlers[e.data.type];
  252. - if (typeof fn !== 'function') {
  253. - throw new Error('no handler for type: ' + e.data.type);
  254. - }
  255. - fn(e.data);
  256. -};
  257. </pre>
  258. <p>削除した部分を <code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code> にコピーして <code class="notranslate" translate="no">shared-cubes.js</code> をインポートし、<code class="notranslate" translate="no">main</code> の代わりに <code class="notranslate" translate="no">init</code> を呼び出します。</p>
  259. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {init, state} from './shared-cubes.js';
  260. function size(data) {
  261. state.width = data.width;
  262. state.height = data.height;
  263. }
  264. const handlers = {
  265. - main,
  266. + init,
  267. size,
  268. };
  269. self.onmessage = function(e) {
  270. const fn = handlers[e.data.type];
  271. if (typeof fn !== 'function') {
  272. throw new Error('no handler for type: ' + e.data.type);
  273. }
  274. fn(e.data);
  275. };
  276. </pre>
  277. <p>同様にメインページに <code class="notranslate" translate="no">shared-cubes.js</code> を含める必要があります。</p>
  278. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;script type="module"&gt;
  279. +import {init, state} from './shared-cubes.js';
  280. </pre>
  281. <p>前に追加したHTMLとCSSを削除します。</p>
  282. <pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;body&gt;
  283. &lt;canvas id="c"&gt;&lt;/canvas&gt;
  284. - &lt;div id="noOffscreenCanvas" style="display:none;"&gt;
  285. - &lt;div&gt;no OffscreenCanvas support&lt;/div&gt;
  286. - &lt;/div&gt;
  287. &lt;/body&gt;
  288. </pre>
  289. <p>そして、CSSは以下のようになります。</p>
  290. <pre class="prettyprint showlinemods notranslate lang-css" translate="no">-#noOffscreenCanvas {
  291. - display: flex;
  292. - width: 100vw;
  293. - height: 100vh;
  294. - align-items: center;
  295. - justify-content: center;
  296. - background: red;
  297. - color: white;
  298. -}
  299. </pre>
  300. <p>次にブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> をサポートありなしに応じて、メインページのコードを変更して起動関数を呼び出すようにしてみましょう。</p>
  301. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
  302. const canvas = document.querySelector('#c');
  303. - if (!canvas.transferControlToOffscreen) {
  304. - canvas.style.display = 'none';
  305. - document.querySelector('#noOffscreenCanvas').style.display = '';
  306. - return;
  307. - }
  308. - const offscreen = canvas.transferControlToOffscreen();
  309. - const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
  310. - worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  311. + if (canvas.transferControlToOffscreen) {
  312. + startWorker(canvas);
  313. + } else {
  314. + startMainPage(canvas);
  315. + }
  316. ...
  317. </pre>
  318. <p>Workerのセットアップコードを全て <code class="notranslate" translate="no">startWorker</code> の中に移動します。</p>
  319. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startWorker(canvas) {
  320. const offscreen = canvas.transferControlToOffscreen();
  321. const worker = new Worker('offscreencanvas-worker-cubes.js', {type: 'module'});
  322. worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  323. function sendSize() {
  324. worker.postMessage({
  325. type: 'size',
  326. width: canvas.clientWidth,
  327. height: canvas.clientHeight,
  328. });
  329. }
  330. window.addEventListener('resize', sendSize);
  331. sendSize();
  332. console.log('using OffscreenCanvas');
  333. }
  334. </pre>
  335. <p>そして <code class="notranslate" translate="no">main</code> の代わりに <code class="notranslate" translate="no">init</code> を送信します。</p>
  336. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">- worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
  337. + worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
  338. </pre>
  339. <p>メインページで開始するには次のようにします。</p>
  340. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startMainPage(canvas) {
  341. init({canvas});
  342. function sendSize() {
  343. state.width = canvas.clientWidth;
  344. state.height = canvas.clientHeight;
  345. }
  346. window.addEventListener('resize', sendSize);
  347. sendSize();
  348. console.log('using regular canvas');
  349. }
  350. </pre>
  351. <p>このサンプルコードではOffscreenCanvasで実行、またはメインページで実行されるようにフォールバックしています。</p>
  352. <p></p><div translate="no" class="threejs_example_container notranslate">
  353. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-fallback.html"></iframe></div>
  354. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-fallback.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
  355. </div>
  356. <p></p>
  357. <p>比較的簡単でした。ピッキングしてみましょう。
  358. <a href="picking.html">ピッキングの記事</a>にある <code class="notranslate" translate="no">RayCaster</code> の例からコードをいくつか取り出し、画面外でオフスクリーンが動作するようにします。</p>
  359. <p><code class="notranslate" translate="no">shared-cube.js</code> を <code class="notranslate" translate="no">shared-picking.js</code> にコピーし、ピッキング部分を追加してみましょう。
  360. この例では <code class="notranslate" translate="no">PickHelper</code> をコピーします。</p>
  361. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
  362. constructor() {
  363. this.raycaster = new THREE.Raycaster();
  364. this.pickedObject = null;
  365. this.pickedObjectSavedColor = 0;
  366. }
  367. pick(normalizedPosition, scene, camera, time) {
  368. // restore the color if there is a picked object
  369. if (this.pickedObject) {
  370. this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
  371. this.pickedObject = undefined;
  372. }
  373. // cast a ray through the frustum
  374. this.raycaster.setFromCamera(normalizedPosition, camera);
  375. // get the list of objects the ray intersected
  376. const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  377. if (intersectedObjects.length) {
  378. // pick the first object. It's the closest one
  379. this.pickedObject = intersectedObjects[0].object;
  380. // save its color
  381. this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
  382. // set its emissive color to flashing red/yellow
  383. this.pickedObject.material.emissive.setHex((time * 8) % 2 &gt; 1 ? 0xFFFF00 : 0xFF0000);
  384. }
  385. }
  386. }
  387. const pickPosition = {x: 0, y: 0};
  388. const pickHelper = new PickHelper();
  389. </pre>
  390. <p>マウスの <code class="notranslate" translate="no">pickPosition</code> を以下のように更新しました。</p>
  391. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  392. const rect = canvas.getBoundingClientRect();
  393. return {
  394. x: (event.clientX - rect.left) * canvas.width / rect.width,
  395. y: (event.clientY - rect.top ) * canvas.height / rect.height,
  396. };
  397. }
  398. function setPickPosition(event) {
  399. const pos = getCanvasRelativePosition(event);
  400. pickPosition.x = (pos.x / canvas.width ) * 2 - 1;
  401. pickPosition.y = (pos.y / canvas.height) * -2 + 1; // note we flip Y
  402. }
  403. window.addEventListener('mousemove', setPickPosition);
  404. </pre>
  405. <p>Workerではマウスの位置を直接読み取れないので、サイズのコードと同じようにマウスの位置を指定してメッセージを送信してみましょう。
  406. サイズのコードと同様にマウスの位置を送信して <code class="notranslate" translate="no">pickPosition</code> を更新します。</p>
  407. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function size(data) {
  408. state.width = data.width;
  409. state.height = data.height;
  410. }
  411. +function mouse(data) {
  412. + pickPosition.x = data.x;
  413. + pickPosition.y = data.y;
  414. +}
  415. const handlers = {
  416. init,
  417. + mouse,
  418. size,
  419. };
  420. self.onmessage = function(e) {
  421. const fn = handlers[e.data.type];
  422. if (typeof fn !== 'function') {
  423. throw new Error('no handler for type: ' + e.data.type);
  424. }
  425. fn(e.data);
  426. };
  427. </pre>
  428. <p>メインページに戻ってマウスをWorkerやメインページに渡すコードを追加します。</p>
  429. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+let sendMouse;
  430. function startWorker(canvas) {
  431. const offscreen = canvas.transferControlToOffscreen();
  432. const worker = new Worker('offscreencanvas-worker-picking.js', {type: 'module'});
  433. worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
  434. + sendMouse = (x, y) =&gt; {
  435. + worker.postMessage({
  436. + type: 'mouse',
  437. + x,
  438. + y,
  439. + });
  440. + };
  441. function sendSize() {
  442. worker.postMessage({
  443. type: 'size',
  444. width: canvas.clientWidth,
  445. height: canvas.clientHeight,
  446. });
  447. }
  448. window.addEventListener('resize', sendSize);
  449. sendSize();
  450. console.log('using OffscreenCanvas'); /* eslint-disable-line no-console */
  451. }
  452. function startMainPage(canvas) {
  453. init({canvas});
  454. + sendMouse = (x, y) =&gt; {
  455. + pickPosition.x = x;
  456. + pickPosition.y = y;
  457. + };
  458. function sendSize() {
  459. state.width = canvas.clientWidth;
  460. state.height = canvas.clientHeight;
  461. }
  462. window.addEventListener('resize', sendSize);
  463. sendSize();
  464. console.log('using regular canvas'); /* eslint-disable-line no-console */
  465. }
  466. </pre>
  467. <p>全てのマウス操作コードをメインページにコピーし、<code class="notranslate" translate="no">sendMouse</code> を使用するようにマイナーチェンジを加えます。</p>
  468. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function setPickPosition(event) {
  469. const pos = getCanvasRelativePosition(event);
  470. - pickPosition.x = (pos.x / canvas.clientWidth ) * 2 - 1;
  471. - pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1; // note we flip Y
  472. + sendMouse(
  473. + (pos.x / canvas.clientWidth ) * 2 - 1,
  474. + (pos.y / canvas.clientHeight) * -2 + 1); // note we flip Y
  475. }
  476. function clearPickPosition() {
  477. // unlike the mouse which always has a position
  478. // if the user stops touching the screen we want
  479. // to stop picking. For now we just pick a value
  480. // unlikely to pick something
  481. - pickPosition.x = -100000;
  482. - pickPosition.y = -100000;
  483. + sendMouse(-100000, -100000);
  484. }
  485. window.addEventListener('mousemove', setPickPosition);
  486. window.addEventListener('mouseout', clearPickPosition);
  487. window.addEventListener('mouseleave', clearPickPosition);
  488. window.addEventListener('touchstart', (event) =&gt; {
  489. // prevent the window from scrolling
  490. event.preventDefault();
  491. setPickPosition(event.touches[0]);
  492. }, {passive: false});
  493. window.addEventListener('touchmove', (event) =&gt; {
  494. setPickPosition(event.touches[0]);
  495. });
  496. window.addEventListener('touchend', clearPickPosition);
  497. </pre>
  498. <p>これでこのピッキングは <code class="notranslate" translate="no">OffscreenCanvas</code> で動作するはずです。</p>
  499. <p></p><div translate="no" class="threejs_example_container notranslate">
  500. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-picking.html"></iframe></div>
  501. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-picking.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
  502. </div>
  503. <p></p>
  504. <p>もう1歩踏み込んで <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> を追加してみましょう。
  505. これはもう少し複雑です。
  506. <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> はマウス、タッチイベント、キーボードなどDOMをかなり広範囲にチェックしています。</p>
  507. <p>これまでのコードとは異なり、グローバルな <code class="notranslate" translate="no">state</code> オブジェクトを使う事はできません。
  508. これを使用して動作するようにOrbitControlsのコードを全て書き換える必要はありません。
  509. OrbitControlsは <code class="notranslate" translate="no">HTMLElement</code> を取り、それに使用するDOMイベントのほとんどをアタッチします。
  510. OrbitControlsが必要とする機能をサポートする必要があります。</p>
  511. <p><a href="https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js">OrbitControlsのソースコード</a>を掘り下げてみると、次のイベントを処理する必要があるように見えます。</p>
  512. <ul>
  513. <li>contextmenu</li>
  514. <li>pointerdown</li>
  515. <li>pointermove</li>
  516. <li>pointerup</li>
  517. <li>touchstart</li>
  518. <li>touchmove</li>
  519. <li>touchend</li>
  520. <li>wheel</li>
  521. <li>keydown</li>
  522. </ul>
  523. <p>マウスイベントには <code class="notranslate" translate="no">ctrlKey</code>、 <code class="notranslate" translate="no">metaKey</code>、 <code class="notranslate" translate="no">shiftKey</code>、 <code class="notranslate" translate="no">button</code>、 <code class="notranslate" translate="no">pointerType</code>、 <code class="notranslate" translate="no">clientX</code>、 <code class="notranslate" translate="no">clientY</code>、 <code class="notranslate" translate="no">pageX</code>、 <code class="notranslate" translate="no">pageY</code> プロパティが必要です。</p>
  524. <p>キーダウンイベントには <code class="notranslate" translate="no">ctrlKey</code>, <code class="notranslate" translate="no">metaKey</code>, <code class="notranslate" translate="no">shiftKey</code>, <code class="notranslate" translate="no">keyCode</code> プロパティが必要です。</p>
  525. <p>ホイールイベントに必要なのは <code class="notranslate" translate="no">deltaY</code> プロパティだけです。</p>
  526. <p>また、タッチイベントに必要なのは <code class="notranslate" translate="no">touches</code> プロパティの <code class="notranslate" translate="no">pageX</code> と <code class="notranslate" translate="no">pageY</code> だけです。</p>
  527. <p>そこでproxyオブジェクトのペアを作ってみましょう。
  528. ある時はメインページで実行され、全てのイベント、関連するプロパティ値をWorkerに渡します。
  529. また、ある時はWorkerで実行され、全てのイベント、DOMイベントと同じ構造をもつイベントをメインページに渡すので、OrbitControlsは違いを見分けられません。</p>
  530. <p>ここにWorker部分のコードがあります。</p>
  531. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {EventDispatcher} from '../../build/three.module.js';
  532. class ElementProxyReceiver extends EventDispatcher {
  533. constructor() {
  534. super();
  535. }
  536. handleEvent(data) {
  537. this.dispatchEvent(data);
  538. }
  539. }
  540. </pre>
  541. <p>メッセージを受信した場合にdataを送信するだけです。
  542. これは <a href="/docs/#api/ja/core/EventDispatcher"><code class="notranslate" translate="no">EventDispatcher</code></a> を継承しており、DOM要素のように <code class="notranslate" translate="no">addEventListener</code> や <code class="notranslate" translate="no">removeEventListener</code> のようなメソッドを提供しているので、OrbitControlsに渡せば動作するはずです。</p>
  543. <p><code class="notranslate" translate="no">ElementProxyReceiver</code> は1つの要素を扱います。
  544. 私たちの場合は1つの頭しか必要ありませんが、頭で考えるのがベストです。
  545. つまり、マネージャーを作って複数のElementProxyReceiverを管理するようにしましょう。</p>
  546. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ProxyManager {
  547. constructor() {
  548. this.targets = {};
  549. this.handleEvent = this.handleEvent.bind(this);
  550. }
  551. makeProxy(data) {
  552. const {id} = data;
  553. const proxy = new ElementProxyReceiver();
  554. this.targets[id] = proxy;
  555. }
  556. getProxy(id) {
  557. return this.targets[id];
  558. }
  559. handleEvent(data) {
  560. this.targets[data.id].handleEvent(data.data);
  561. }
  562. }
  563. </pre>
  564. <p><code class="notranslate" translate="no">ProxyManager</code>のインスタンスを作成し <code class="notranslate" translate="no">makeProxy</code> メソッドにidを指定して呼び出す事で、そのidを持つメッセージに応答する <code class="notranslate" translate="no">ElementProxyReceiver</code> を作成できます。</p>
  565. <p>Workerのメッセージハンドラに接続してみましょう。</p>
  566. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const proxyManager = new ProxyManager();
  567. function start(data) {
  568. const proxy = proxyManager.getProxy(data.canvasId);
  569. init({
  570. canvas: data.canvas,
  571. inputElement: proxy,
  572. });
  573. }
  574. function makeProxy(data) {
  575. proxyManager.makeProxy(data);
  576. }
  577. ...
  578. const handlers = {
  579. - init,
  580. - mouse,
  581. + start,
  582. + makeProxy,
  583. + event: proxyManager.handleEvent,
  584. size,
  585. };
  586. self.onmessage = function(e) {
  587. const fn = handlers[e.data.type];
  588. if (typeof fn !== 'function') {
  589. throw new Error('no handler for type: ' + e.data.type);
  590. }
  591. fn(e.data);
  592. };
  593. </pre>
  594. <p>共有のthree.jsコードでは <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> をインポートして設定する必要があります。</p>
  595. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from '../../build/three.module.js';
  596. +import {OrbitControls} from '/examples/jsm/controls/OrbitControls.js';
  597. export function init(data) {
  598. - const {canvas} = data;
  599. + const {canvas, inputElement} = data;
  600. const renderer = new THREE.WebGLRenderer({canvas});
  601. + const controls = new OrbitControls(camera, inputElement);
  602. + controls.target.set(0, 0, 0);
  603. + controls.update();
  604. </pre>
  605. <p>OffscreenCanvas以外のサンプルコード例のようにキャンバスを渡すのではなく、
  606. <code class="notranslate" translate="no">inputElement</code> を介してOrbitControlsをProxyに渡している事に注目して下さい。</p>
  607. <p>次に <code class="notranslate" translate="no">canvas</code> を <code class="notranslate" translate="no">inputElement</code> に変更し、HTMLファイルから全てのピッキングイベントのコードを共有のthree.jsコードに移動させます。</p>
  608. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
  609. - const rect = canvas.getBoundingClientRect();
  610. + const rect = inputElement.getBoundingClientRect();
  611. return {
  612. x: event.clientX - rect.left,
  613. y: event.clientY - rect.top,
  614. };
  615. }
  616. function setPickPosition(event) {
  617. const pos = getCanvasRelativePosition(event);
  618. - sendMouse(
  619. - (pos.x / canvas.clientWidth ) * 2 - 1,
  620. - (pos.y / canvas.clientHeight) * -2 + 1); // note we flip Y
  621. + pickPosition.x = (pos.x / inputElement.clientWidth ) * 2 - 1;
  622. + pickPosition.y = (pos.y / inputElement.clientHeight) * -2 + 1; // note we flip Y
  623. }
  624. function clearPickPosition() {
  625. // unlike the mouse which always has a position
  626. // if the user stops touching the screen we want
  627. // to stop picking. For now we just pick a value
  628. // unlikely to pick something
  629. - sendMouse(-100000, -100000);
  630. + pickPosition.x = -100000;
  631. + pickPosition.y = -100000;
  632. }
  633. *inputElement.addEventListener('mousemove', setPickPosition);
  634. *inputElement.addEventListener('mouseout', clearPickPosition);
  635. *inputElement.addEventListener('mouseleave', clearPickPosition);
  636. *inputElement.addEventListener('touchstart', (event) =&gt; {
  637. // prevent the window from scrolling
  638. event.preventDefault();
  639. setPickPosition(event.touches[0]);
  640. }, {passive: false});
  641. *inputElement.addEventListener('touchmove', (event) =&gt; {
  642. setPickPosition(event.touches[0]);
  643. });
  644. *inputElement.addEventListener('touchend', clearPickPosition);
  645. </pre>
  646. <p>メインページに戻り、上記で列挙した全てのイベントにメッセージを送信するコードが必要です。</p>
  647. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">let nextProxyId = 0;
  648. class ElementProxy {
  649. constructor(element, worker, eventHandlers) {
  650. this.id = nextProxyId++;
  651. this.worker = worker;
  652. const sendEvent = (data) =&gt; {
  653. this.worker.postMessage({
  654. type: 'event',
  655. id: this.id,
  656. data,
  657. });
  658. };
  659. // register an id
  660. worker.postMessage({
  661. type: 'makeProxy',
  662. id: this.id,
  663. });
  664. for (const [eventName, handler] of Object.entries(eventHandlers)) {
  665. element.addEventListener(eventName, function(event) {
  666. handler(event, sendEvent);
  667. });
  668. }
  669. }
  670. }
  671. </pre>
  672. <p><code class="notranslate" translate="no">ElementProxy</code> はProxyしたいイベントの要素を受け取ります。
  673. 次にWorkerにidを登録し、先ほど設定した <code class="notranslate" translate="no">makeProxy</code> メッセージを使って送信します。
  674. Workerは <code class="notranslate" translate="no">ElementProxyReceiver</code> を作成しそのidに登録します。</p>
  675. <p>そして登録するイベントハンドラのオブジェクトを用意します。
  676. このようにして、Workerに転送したいイベントにハンドラを渡す事ができます。</p>
  677. <p>Workerを起動する時はまずProxyを作成しイベントハンドラを渡します。</p>
  678. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startWorker(canvas) {
  679. const offscreen = canvas.transferControlToOffscreen();
  680. const worker = new Worker('offscreencanvas-worker-orbitcontrols.js', {type: 'module'});
  681. + const eventHandlers = {
  682. + contextmenu: preventDefaultHandler,
  683. + mousedown: mouseEventHandler,
  684. + mousemove: mouseEventHandler,
  685. + mouseup: mouseEventHandler,
  686. + pointerdown: mouseEventHandler,
  687. + pointermove: mouseEventHandler,
  688. + pointerup: mouseEventHandler,
  689. + touchstart: touchEventHandler,
  690. + touchmove: touchEventHandler,
  691. + touchend: touchEventHandler,
  692. + wheel: wheelEventHandler,
  693. + keydown: filteredKeydownEventHandler,
  694. + };
  695. + const proxy = new ElementProxy(canvas, worker, eventHandlers);
  696. worker.postMessage({
  697. type: 'start',
  698. canvas: offscreen,
  699. + canvasId: proxy.id,
  700. }, [offscreen]);
  701. console.log('using OffscreenCanvas'); /* eslint-disable-line no-console */
  702. }
  703. </pre>
  704. <p>以下はイベントハンドラです。
  705. 受信したイベントからプロパティのリストをコピーするだけです。
  706. <code class="notranslate" translate="no">sendEvent</code> 関数に渡され作成したデータを渡します。
  707. この関数は正しいidを追加してWorkerに送信します。</p>
  708. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mouseEventHandler = makeSendPropertiesHandler([
  709. 'ctrlKey',
  710. 'metaKey',
  711. 'shiftKey',
  712. 'button',
  713. 'pointerType',
  714. 'clientX',
  715. 'clientY',
  716. 'pageX',
  717. 'pageY',
  718. ]);
  719. const wheelEventHandlerImpl = makeSendPropertiesHandler([
  720. 'deltaX',
  721. 'deltaY',
  722. ]);
  723. const keydownEventHandler = makeSendPropertiesHandler([
  724. 'ctrlKey',
  725. 'metaKey',
  726. 'shiftKey',
  727. 'keyCode',
  728. ]);
  729. function wheelEventHandler(event, sendFn) {
  730. event.preventDefault();
  731. wheelEventHandlerImpl(event, sendFn);
  732. }
  733. function preventDefaultHandler(event) {
  734. event.preventDefault();
  735. }
  736. function copyProperties(src, properties, dst) {
  737. for (const name of properties) {
  738. dst[name] = src[name];
  739. }
  740. }
  741. function makeSendPropertiesHandler(properties) {
  742. return function sendProperties(event, sendFn) {
  743. const data = {type: event.type};
  744. copyProperties(event, properties, data);
  745. sendFn(data);
  746. };
  747. }
  748. function touchEventHandler(event, sendFn) {
  749. const touches = [];
  750. const data = {type: event.type, touches};
  751. for (let i = 0; i &lt; event.touches.length; ++i) {
  752. const touch = event.touches[i];
  753. touches.push({
  754. pageX: touch.pageX,
  755. pageY: touch.pageY,
  756. });
  757. }
  758. sendFn(data);
  759. }
  760. // The four arrow keys
  761. const orbitKeys = {
  762. '37': true, // left
  763. '38': true, // up
  764. '39': true, // right
  765. '40': true, // down
  766. };
  767. function filteredKeydownEventHandler(event, sendFn) {
  768. const {keyCode} = event;
  769. if (orbitKeys[keyCode]) {
  770. event.preventDefault();
  771. keydownEventHandler(event, sendFn);
  772. }
  773. }
  774. </pre>
  775. <p>これで動くと思われるが、実際に試してみると <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> がもう少し必要なものがあると分かります。</p>
  776. <p>1つは <code class="notranslate" translate="no">element.focus</code> です。Workerには必要ないのでStubを追加しておきましょう。</p>
  777. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxyReceiver extends THREE.EventDispatcher {
  778. constructor() {
  779. super();
  780. }
  781. handleEvent(data) {
  782. this.dispatchEvent(data);
  783. }
  784. + focus() {
  785. + // no-op
  786. + }
  787. }
  788. </pre>
  789. <p>もう1つは <code class="notranslate" translate="no">event.preventDefault</code> と <code class="notranslate" translate="no">event.stopPropagation</code> を呼び出す事です。
  790. メインページでは既に対応してるのでそれらも不要になります。</p>
  791. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function noop() {
  792. +}
  793. class ElementProxyReceiver extends THREE.EventDispatcher {
  794. constructor() {
  795. super();
  796. }
  797. handleEvent(data) {
  798. + data.preventDefault = noop;
  799. + data.stopPropagation = noop;
  800. this.dispatchEvent(data);
  801. }
  802. focus() {
  803. // no-op
  804. }
  805. }
  806. </pre>
  807. <p>もう1つは <code class="notranslate" translate="no">clientWidth</code> と <code class="notranslate" translate="no">clientHeight</code> を見る事です。
  808. 以前はサイズを渡してましたが、Proxyペアを更新してそれも渡すようにします。</p>
  809. <p>Workerの中では</p>
  810. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxyReceiver extends THREE.EventDispatcher {
  811. constructor() {
  812. super();
  813. }
  814. + get clientWidth() {
  815. + return this.width;
  816. + }
  817. + get clientHeight() {
  818. + return this.height;
  819. + }
  820. + getBoundingClientRect() {
  821. + return {
  822. + left: this.left,
  823. + top: this.top,
  824. + width: this.width,
  825. + height: this.height,
  826. + right: this.left + this.width,
  827. + bottom: this.top + this.height,
  828. + };
  829. + }
  830. handleEvent(data) {
  831. + if (data.type === 'size') {
  832. + this.left = data.left;
  833. + this.top = data.top;
  834. + this.width = data.width;
  835. + this.height = data.height;
  836. + return;
  837. + }
  838. data.preventDefault = noop;
  839. data.stopPropagation = noop;
  840. this.dispatchEvent(data);
  841. }
  842. focus() {
  843. // no-op
  844. }
  845. }
  846. </pre>
  847. <p>メインページに戻るにはサイズと左と上の位置も送信する必要があります。
  848. このままではキャンバスを移動しても処理されず、サイズを変更しても処理されないです。
  849. 移動を処理したい場合は何かがキャンバスを移動する度に <code class="notranslate" translate="no">sendSize</code> を呼び出す必要があります。</p>
  850. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxy {
  851. constructor(element, worker, eventHandlers) {
  852. this.id = nextProxyId++;
  853. this.worker = worker;
  854. const sendEvent = (data) =&gt; {
  855. this.worker.postMessage({
  856. type: 'event',
  857. id: this.id,
  858. data,
  859. });
  860. };
  861. // register an id
  862. worker.postMessage({
  863. type: 'makeProxy',
  864. id: this.id,
  865. });
  866. + sendSize();
  867. for (const [eventName, handler] of Object.entries(eventHandlers)) {
  868. element.addEventListener(eventName, function(event) {
  869. handler(event, sendEvent);
  870. });
  871. }
  872. + function sendSize() {
  873. + const rect = element.getBoundingClientRect();
  874. + sendEvent({
  875. + type: 'size',
  876. + left: rect.left,
  877. + top: rect.top,
  878. + width: element.clientWidth,
  879. + height: element.clientHeight,
  880. + });
  881. + }
  882. +
  883. + window.addEventListener('resize', sendSize);
  884. }
  885. }
  886. </pre>
  887. <p>そして共有のthree.jsコードでは <code class="notranslate" translate="no">state</code> は不要になりました。</p>
  888. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-export const state = {
  889. - width: 300, // canvas default
  890. - height: 150, // canvas default
  891. -};
  892. ...
  893. function resizeRendererToDisplaySize(renderer) {
  894. const canvas = renderer.domElement;
  895. - const width = state.width;
  896. - const height = state.height;
  897. + const width = inputElement.clientWidth;
  898. + const height = inputElement.clientHeight;
  899. const needResize = canvas.width !== width || canvas.height !== height;
  900. if (needResize) {
  901. renderer.setSize(width, height, false);
  902. }
  903. return needResize;
  904. }
  905. function render(time) {
  906. time *= 0.001;
  907. if (resizeRendererToDisplaySize(renderer)) {
  908. - camera.aspect = state.width / state.height;
  909. + camera.aspect = inputElement.clientWidth / inputElement.clientHeight;
  910. camera.updateProjectionMatrix();
  911. }
  912. ...
  913. </pre>
  914. <p>他にもいくつかのハックがあります。
  915. OrbitControlsは <code class="notranslate" translate="no">pointermove</code> と <code class="notranslate" translate="no">pointerup</code> イベントをマウスキャプチャ(マウスがウィンドウの外に出た時)を処理するための要素の <code class="notranslate" translate="no">ownerDocument</code> です。</p>
  916. <p>さらにコードはグローバルな <code class="notranslate" translate="no">document</code> を参照していますが、Workerにはグローバルなdocumentはありません。</p>
  917. <p>これは2つの簡単なハックで全て解決できます。
  918. Workerコードでは両方の問題に対してProxyを再利用します。</p>
  919. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function start(data) {
  920. const proxy = proxyManager.getProxy(data.canvasId);
  921. + proxy.ownerDocument = proxy; // HACK!
  922. + self.document = {} // HACK!
  923. init({
  924. canvas: data.canvas,
  925. inputElement: proxy,
  926. });
  927. }
  928. </pre>
  929. <p>これで <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> が期待に沿った検査を行うための機能を提供します。</p>
  930. <p>難しいのは分かっていますが手短に言うと:</p>
  931. <p><code class="notranslate" translate="no">ElementProxy</code> はメインページ上で動作し、DOMイベントを転送します。
  932. Worker内の <code class="notranslate" translate="no">ElementProxyReceiver</code> は一緒に使うことができる <code class="notranslate" translate="no">HTMLElement</code> を装っています。
  933. <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> と独自のコードを使用しています。</p>
  934. <p>最後にOffscreenCanvasを使用していない時のフォールバックです。
  935. 必要なのはcanvas自体を <code class="notranslate" translate="no">inputElement</code> として渡す事です。</p>
  936. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startMainPage(canvas) {
  937. - init({canvas});
  938. + init({canvas, inputElement: canvas});
  939. console.log('using regular canvas');
  940. }
  941. </pre>
  942. <p>これでOrbitControlsがOffscreenCanvasで動作するようになりました。</p>
  943. <p></p><div translate="no" class="threejs_example_container notranslate">
  944. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-orbitcontrols.html"></iframe></div>
  945. <a class="threejs_center" href="/manual/examples/offscreencanvas-w-orbitcontrols.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
  946. </div>
  947. <p></p>
  948. <p>これはおそらくこのサイトで最も複雑な例です。
  949. 各サンプルには3つのファイルが含まれているので少しわかりにくいです。
  950. HTMLファイル、Workerファイル、共有のthree.jsコードなどです。</p>
  951. <p>理解する事が難し過ぎず、少しでも参考になれば幸いです。
  952. three.js、OffscreenCanvas、Web Workerを使った動作の便利な例を紹介しました。</p>
  953. </div>
  954. </div>
  955. </div>
  956. <script src="/manual/resources/prettify.js"></script>
  957. <script src="/manual/resources/lesson.js"></script>
  958. </body></html>