offscreencanvas.html 48 KB

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