offscreencanvas.html 48 KB

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