You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1037 lines
48 KiB
1037 lines
48 KiB
<!DOCTYPE html><html lang="ja"><head>
|
|
<meta charset="utf-8">
|
|
<title>のOffscreenCanvas</title>
|
|
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:site" content="@threejs">
|
|
<meta name="twitter:title" content="Three.js – のOffscreenCanvas">
|
|
<meta property="og:image" content="https://threejs.org/files/share.png">
|
|
<link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)">
|
|
<link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)">
|
|
|
|
<link rel="stylesheet" href="/manual/resources/lesson.css">
|
|
<link rel="stylesheet" href="/manual/resources/lang.css">
|
|
<!-- Import maps polyfill -->
|
|
<!-- Remove this when import maps will be widely supported -->
|
|
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "../../build/three.module.js"
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="lesson-title">
|
|
<h1>のOffscreenCanvas</h1>
|
|
</div>
|
|
<div class="lesson">
|
|
<div class="lesson-main">
|
|
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas"><code class="notranslate" translate="no">OffscreenCanvas</code></a>は新しいブラウザの機能で現在はChromeでしか利用できませんが、他のブラウザにも来るようです。
|
|
<code class="notranslate" translate="no">OffscreenCanvas</code> はWeb Workerでキャンバスにレンダリングできます。
|
|
複雑な3Dシーンのレンダリングなど重い作業をWeb Workerで行い負荷を軽減させ、ブラウザのレスポンスを低下させない方法です。
|
|
また、データが読み込まれWorkerで解析されてるのでページ読み込み中にページ表示の途切れは少ないでしょう。</p>
|
|
<p>OffscreenCanvasの利用を<em>開始</em>するのは非常に簡単です。
|
|
<a href="responsive.html">レスポンシブデザインの記事</a>から3つのキューブを回転させるコードに修正してみましょう。</p>
|
|
<p>通常はWorkerのコードを別ファイルに分離しますが、このサイトのほとんどのサンプルコードではスクリプトをHTMLファイルに埋め込んでいます。</p>
|
|
<p>ここでは <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> というファイルを作成し、<a href="responsive.html">レスポンシブデザインの例</a>から全てのJavaScriptをコピーして下さい。
|
|
そして、Workerで実行するために必要な変更を行います。</p>
|
|
<p>HTMLファイルにはJavaScriptのいくつかの処理が必要です。
|
|
まず最初に行う必要があるのはキャンバスを検索し、<code class="notranslate" translate="no">canvas.transferControlToOffscreen</code> 呼び出してキャンバスのコントロールをオフスクリーンに転送します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
|
|
const canvas = document.querySelector('#c');
|
|
const offscreen = canvas.transferControlToOffscreen();
|
|
|
|
...
|
|
</pre>
|
|
<p><code class="notranslate" translate="no">new Worker(pathToScript, {type: 'module'})</code>でWorkerを起動し、<code class="notranslate" translate="no">offscreen</code> オブジェクトを渡します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
|
|
const canvas = document.querySelector('#c');
|
|
const offscreen = canvas.transferControlToOffscreen();
|
|
const worker = new Worker('offscreencanvas-cubes.js', {type: 'module'});
|
|
worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
|
|
}
|
|
main();
|
|
</pre>
|
|
<p>ここで重要なのはWorkerが <code class="notranslate" translate="no">DOM</code> にアクセスできない事です。
|
|
HTML要素の参照やマウスイベントやキーボードイベントを受け取る事もできません。
|
|
Workerは、送られたメッセージに返信してWebページにメッセージを送り返す事だけです。</p>
|
|
<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つの引数を渡します。
|
|
1つ目の引数は<a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm">クローン</a>されるJavaScriptオブジェクトでWorkerに送ります。
|
|
2番目の引数は任意でWorkerに <em>転送</em> したい最初のオブジェクトです。
|
|
このオブジェクトはクローンされません。
|
|
その代わりに <em>転送</em> され、メインページには存在しなくなります。
|
|
存在しなくなるというのはおそらく間違った説明であり、むしろ取り除かれます。
|
|
クローンではなく、特定のタイプのオブジェクトのみを転送する事ができます。
|
|
転送するオブジェクトには <code class="notranslate" translate="no">OffscreenCanvas</code> が含まれているので、1度転送した <code class="notranslate" translate="no">offscreen</code> オブジェクトをメインページに戻しても意味がありません。</p>
|
|
<p>Workerは <code class="notranslate" translate="no">onmessage</code> ハンドラからメッセージを受け取ります。
|
|
<code class="notranslate" translate="no">postMessage</code> に渡したオブジェクトはWorkerの <code class="notranslate" translate="no">onmessage</code> ハンドラに渡され <code class="notranslate" translate="no">event.data</code> を更新します。
|
|
上記のコードではWorkerに渡すオブジェクトに <code class="notranslate" translate="no">type: 'main'</code> を宣言しています。
|
|
このオブジェクトはブラウザには何の意味もありません。Workerで使うためだけのものです。
|
|
<code class="notranslate" translate="no">type</code> に基づいて、Worker内で別の関数を呼び出すハンドラを作成します。
|
|
あとは必要に応じて関数を追加し、メインページから簡単に呼び出す事ができます。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const handlers = {
|
|
main,
|
|
};
|
|
|
|
self.onmessage = function(e) {
|
|
const fn = handlers[e.data.type];
|
|
if (typeof fn !== 'function') {
|
|
throw new Error('no handler for type: ' + e.data.type);
|
|
}
|
|
fn(e.data);
|
|
};
|
|
</pre>
|
|
<p>上記コードのように <code class="notranslate" translate="no">type</code> に基づいてハンドラを検索し、メインページから送られてきた <code class="notranslate" translate="no">data</code> を渡します。
|
|
あとは<a href="responsive.html">レスポンシブデザインの記事</a>から <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> に貼り付けた <code class="notranslate" translate="no">main</code> を変更するだけです。</p>
|
|
<p>DOMからキャンバスを探すのではなく、イベントデータからキャンバスを受け取ります。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function main() {
|
|
- const canvas = document.querySelector('#c');
|
|
+function main(data) {
|
|
+ const {canvas} = data;
|
|
const renderer = new THREE.WebGLRenderer({canvas});
|
|
|
|
...
|
|
</pre>
|
|
<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> を参照できない事です。
|
|
<code class="notranslate" translate="no">clientWidth</code> と <code class="notranslate" translate="no">canvas.clientHeight</code> はDOMの値です。</p>
|
|
<p>元のコードは以下の通りです。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
|
|
const canvas = renderer.domElement;
|
|
const width = canvas.clientWidth;
|
|
const height = canvas.clientHeight;
|
|
const needResize = canvas.width !== width || canvas.height !== height;
|
|
if (needResize) {
|
|
renderer.setSize(width, height, false);
|
|
}
|
|
return needResize;
|
|
}
|
|
</pre>
|
|
<p>DOMを参照できないため、変更したサイズの値をWorkerに送る必要があります。
|
|
そこでグローバルな状態を追加し、幅と高さを維持するようにしましょう。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const state = {
|
|
width: 300, // canvas default
|
|
height: 150, // canvas default
|
|
};
|
|
</pre>
|
|
<p>これらの値を更新するための <code class="notranslate" translate="no">'size'</code> ハンドラを追加してみます。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function size(data) {
|
|
+ state.width = data.width;
|
|
+ state.height = data.height;
|
|
+}
|
|
|
|
const handlers = {
|
|
main,
|
|
+ size,
|
|
};
|
|
</pre>
|
|
<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>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function resizeRendererToDisplaySize(renderer) {
|
|
const canvas = renderer.domElement;
|
|
- const width = canvas.clientWidth;
|
|
- const height = canvas.clientHeight;
|
|
+ const width = state.width;
|
|
+ const height = state.height;
|
|
const needResize = canvas.width !== width || canvas.height !== height;
|
|
if (needResize) {
|
|
renderer.setSize(width, height, false);
|
|
}
|
|
return needResize;
|
|
}
|
|
</pre>
|
|
<p>以下も同様の変更が必要です。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
|
|
time *= 0.001;
|
|
|
|
if (resizeRendererToDisplaySize(renderer)) {
|
|
- camera.aspect = canvas.clientWidth / canvas.clientHeight;
|
|
+ camera.aspect = state.width / state.height;
|
|
camera.updateProjectionMatrix();
|
|
}
|
|
|
|
...
|
|
</pre>
|
|
<p>メインページに戻りページのリサイズの度に <code class="notranslate" translate="no">size</code> イベントを送信します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
|
|
worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
|
|
|
|
+function sendSize() {
|
|
+ worker.postMessage({
|
|
+ type: 'size',
|
|
+ width: canvas.clientWidth,
|
|
+ height: canvas.clientHeight,
|
|
+ });
|
|
+}
|
|
+
|
|
+window.addEventListener('resize', sendSize);
|
|
+sendSize();
|
|
</pre>
|
|
<p>初期サイズを送るために1度sendSizeを呼んでいます。</p>
|
|
<p>ブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> を完全にサポートしていると仮定して、これらの変更を行うだけで動作するはずです。
|
|
実行する前にブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> を実際にサポートしているか確認し、サポートしていない場合はエラーを表示してみましょう。
|
|
まずはエラーを表示するためのHTMLを追加します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-html" translate="no"><body>
|
|
<canvas id="c"></canvas>
|
|
+ <div id="noOffscreenCanvas" style="display:none;">
|
|
+ <div>no OffscreenCanvas support</div>
|
|
+ </div>
|
|
</body>
|
|
</pre>
|
|
<p>そして、CSSを追加します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-css" translate="no">#noOffscreenCanvas {
|
|
display: flex;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: red;
|
|
color: white;
|
|
}
|
|
</pre>
|
|
<p>ブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> をサポートしているか確認するためには <code class="notranslate" translate="no">transferControlToOffscreen</code> を呼びます。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
|
|
const canvas = document.querySelector('#c');
|
|
+ if (!canvas.transferControlToOffscreen) {
|
|
+ canvas.style.display = 'none';
|
|
+ document.querySelector('#noOffscreenCanvas').style.display = '';
|
|
+ return;
|
|
+ }
|
|
const offscreen = canvas.transferControlToOffscreen();
|
|
const worker = new Worker('offscreencanvas-picking.js', {type: 'module});
|
|
worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
|
|
|
|
...
|
|
</pre>
|
|
<p>ブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> をサポートしていれば、このサンプルは動作するはずです。</p>
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/offscreencanvas.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>これは素晴らしい事ですが、今の所は全てのブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> をサポートしている訳ではなく、
|
|
<code class="notranslate" translate="no">OffscreenCanvas</code> サポートありとサポートなしの両方で動作するコードに変更し、サポートなしの場合はメインページのキャンバスを通常のように表示します。</p>
|
|
<blockquote>
|
|
<p>余談ですがページをレスポンシブにするためにOffscreenCanvasが必要な場合、フォールバックを持つ意味がよくわかりません。
|
|
メインページで実行するかWorkerで実行するかには、Workerで実行している時にメインページで実行している時よりも多くの事ができるように
|
|
調整するかもしれません。何をするかは本当にあなた次第です。</p>
|
|
</blockquote>
|
|
<p>まず最初にthree.jsのコードとWorkerの固有コードを分離しましょう。
|
|
これでメインページとWorkerの両方で同じコードを使う事ができます。
|
|
つまり、3つのファイルを持つ事になります。</p>
|
|
<ol>
|
|
<li><p>htmlファイル</p>
|
|
<p><code class="notranslate" translate="no">threejs-offscreencanvas-w-fallback.html</code></p>
|
|
</li>
|
|
<li><p>three.jsを含むJavaScriptコード</p>
|
|
<p><code class="notranslate" translate="no">shared-cubes.js</code></p>
|
|
</li>
|
|
<li><p>workerをサポートするコード</p>
|
|
<p><code class="notranslate" translate="no">offscreencanvas-worker-cubes.js</code></p>
|
|
</li>
|
|
</ol>
|
|
<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>
|
|
<p>まず <code class="notranslate" translate="no">offscreencanvas-cubes.js</code> を全て <code class="notranslate" translate="no">shared-cube.js</code> にコピーします。
|
|
次に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>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
|
|
|
|
-const state = {
|
|
+export const state = {
|
|
width: 300, // canvas default
|
|
height: 150, // canvas default
|
|
};
|
|
|
|
-function main(data) {
|
|
+export function init(data) {
|
|
const {canvas} = data;
|
|
const renderer = new THREE.WebGLRenderer({canvas});
|
|
</pre>
|
|
<p>そして、three.js関連以外の部分だけを切り取ります。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function size(data) {
|
|
- state.width = data.width;
|
|
- state.height = data.height;
|
|
-}
|
|
-
|
|
-const handlers = {
|
|
- main,
|
|
- size,
|
|
-};
|
|
-
|
|
-self.onmessage = function(e) {
|
|
- const fn = handlers[e.data.type];
|
|
- if (typeof fn !== 'function') {
|
|
- throw new Error('no handler for type: ' + e.data.type);
|
|
- }
|
|
- fn(e.data);
|
|
-};
|
|
</pre>
|
|
<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>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {init, state} from './shared-cubes.js';
|
|
|
|
function size(data) {
|
|
state.width = data.width;
|
|
state.height = data.height;
|
|
}
|
|
|
|
const handlers = {
|
|
- main,
|
|
+ init,
|
|
size,
|
|
};
|
|
|
|
self.onmessage = function(e) {
|
|
const fn = handlers[e.data.type];
|
|
if (typeof fn !== 'function') {
|
|
throw new Error('no handler for type: ' + e.data.type);
|
|
}
|
|
fn(e.data);
|
|
};
|
|
</pre>
|
|
<p>同様にメインページに <code class="notranslate" translate="no">shared-cubes.js</code> を含める必要があります。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-html" translate="no"><script type="module">
|
|
+import {init, state} from './shared-cubes.js';
|
|
</pre>
|
|
<p>前に追加したHTMLとCSSを削除します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-html" translate="no"><body>
|
|
<canvas id="c"></canvas>
|
|
- <div id="noOffscreenCanvas" style="display:none;">
|
|
- <div>no OffscreenCanvas support</div>
|
|
- </div>
|
|
</body>
|
|
</pre>
|
|
<p>そして、CSSは以下のようになります。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-css" translate="no">-#noOffscreenCanvas {
|
|
- display: flex;
|
|
- width: 100vw;
|
|
- height: 100vh;
|
|
- align-items: center;
|
|
- justify-content: center;
|
|
- background: red;
|
|
- color: white;
|
|
-}
|
|
</pre>
|
|
<p>次にブラウザが <code class="notranslate" translate="no">OffscreenCanvas</code> をサポートありなしに応じて、メインページのコードを変更して起動関数を呼び出すようにしてみましょう。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function main() {
|
|
const canvas = document.querySelector('#c');
|
|
- if (!canvas.transferControlToOffscreen) {
|
|
- canvas.style.display = 'none';
|
|
- document.querySelector('#noOffscreenCanvas').style.display = '';
|
|
- return;
|
|
- }
|
|
- const offscreen = canvas.transferControlToOffscreen();
|
|
- const worker = new Worker('offscreencanvas-picking.js', {type: 'module'});
|
|
- worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
|
|
+ if (canvas.transferControlToOffscreen) {
|
|
+ startWorker(canvas);
|
|
+ } else {
|
|
+ startMainPage(canvas);
|
|
+ }
|
|
...
|
|
</pre>
|
|
<p>Workerのセットアップコードを全て <code class="notranslate" translate="no">startWorker</code> の中に移動します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startWorker(canvas) {
|
|
const offscreen = canvas.transferControlToOffscreen();
|
|
const worker = new Worker('offscreencanvas-worker-cubes.js', {type: 'module'});
|
|
worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
|
|
|
|
function sendSize() {
|
|
worker.postMessage({
|
|
type: 'size',
|
|
width: canvas.clientWidth,
|
|
height: canvas.clientHeight,
|
|
});
|
|
}
|
|
|
|
window.addEventListener('resize', sendSize);
|
|
sendSize();
|
|
|
|
console.log('using OffscreenCanvas');
|
|
}
|
|
</pre>
|
|
<p>そして <code class="notranslate" translate="no">main</code> の代わりに <code class="notranslate" translate="no">init</code> を送信します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">- worker.postMessage({type: 'main', canvas: offscreen}, [offscreen]);
|
|
+ worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
|
|
</pre>
|
|
<p>メインページで開始するには次のようにします。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startMainPage(canvas) {
|
|
init({canvas});
|
|
|
|
function sendSize() {
|
|
state.width = canvas.clientWidth;
|
|
state.height = canvas.clientHeight;
|
|
}
|
|
window.addEventListener('resize', sendSize);
|
|
sendSize();
|
|
|
|
console.log('using regular canvas');
|
|
}
|
|
</pre>
|
|
<p>このサンプルコードではOffscreenCanvasで実行、またはメインページで実行されるようにフォールバックしています。</p>
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-fallback.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/offscreencanvas-w-fallback.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>比較的簡単でした。ピッキングしてみましょう。
|
|
<a href="picking.html">ピッキングの記事</a>にある <code class="notranslate" translate="no">RayCaster</code> の例からコードをいくつか取り出し、画面外でオフスクリーンが動作するようにします。</p>
|
|
<p><code class="notranslate" translate="no">shared-cube.js</code> を <code class="notranslate" translate="no">shared-picking.js</code> にコピーし、ピッキング部分を追加してみましょう。
|
|
この例では <code class="notranslate" translate="no">PickHelper</code> をコピーします。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class PickHelper {
|
|
constructor() {
|
|
this.raycaster = new THREE.Raycaster();
|
|
this.pickedObject = null;
|
|
this.pickedObjectSavedColor = 0;
|
|
}
|
|
pick(normalizedPosition, scene, camera, time) {
|
|
// restore the color if there is a picked object
|
|
if (this.pickedObject) {
|
|
this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
|
|
this.pickedObject = undefined;
|
|
}
|
|
|
|
// cast a ray through the frustum
|
|
this.raycaster.setFromCamera(normalizedPosition, camera);
|
|
// get the list of objects the ray intersected
|
|
const intersectedObjects = this.raycaster.intersectObjects(scene.children);
|
|
if (intersectedObjects.length) {
|
|
// pick the first object. It's the closest one
|
|
this.pickedObject = intersectedObjects[0].object;
|
|
// save its color
|
|
this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
|
|
// set its emissive color to flashing red/yellow
|
|
this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
|
|
}
|
|
}
|
|
}
|
|
|
|
const pickPosition = {x: 0, y: 0};
|
|
const pickHelper = new PickHelper();
|
|
</pre>
|
|
<p>マウスの <code class="notranslate" translate="no">pickPosition</code> を以下のように更新しました。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
return {
|
|
x: (event.clientX - rect.left) * canvas.width / rect.width,
|
|
y: (event.clientY - rect.top ) * canvas.height / rect.height,
|
|
};
|
|
}
|
|
|
|
function setPickPosition(event) {
|
|
const pos = getCanvasRelativePosition(event);
|
|
pickPosition.x = (pos.x / canvas.width ) * 2 - 1;
|
|
pickPosition.y = (pos.y / canvas.height) * -2 + 1; // note we flip Y
|
|
}
|
|
window.addEventListener('mousemove', setPickPosition);
|
|
</pre>
|
|
<p>Workerではマウスの位置を直接読み取れないので、サイズのコードと同じようにマウスの位置を指定してメッセージを送信してみましょう。
|
|
サイズのコードと同様にマウスの位置を送信して <code class="notranslate" translate="no">pickPosition</code> を更新します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function size(data) {
|
|
state.width = data.width;
|
|
state.height = data.height;
|
|
}
|
|
|
|
+function mouse(data) {
|
|
+ pickPosition.x = data.x;
|
|
+ pickPosition.y = data.y;
|
|
+}
|
|
|
|
const handlers = {
|
|
init,
|
|
+ mouse,
|
|
size,
|
|
};
|
|
|
|
self.onmessage = function(e) {
|
|
const fn = handlers[e.data.type];
|
|
if (typeof fn !== 'function') {
|
|
throw new Error('no handler for type: ' + e.data.type);
|
|
}
|
|
fn(e.data);
|
|
};
|
|
</pre>
|
|
<p>メインページに戻ってマウスをWorkerやメインページに渡すコードを追加します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+let sendMouse;
|
|
|
|
function startWorker(canvas) {
|
|
const offscreen = canvas.transferControlToOffscreen();
|
|
const worker = new Worker('offscreencanvas-worker-picking.js', {type: 'module'});
|
|
worker.postMessage({type: 'init', canvas: offscreen}, [offscreen]);
|
|
|
|
+ sendMouse = (x, y) => {
|
|
+ worker.postMessage({
|
|
+ type: 'mouse',
|
|
+ x,
|
|
+ y,
|
|
+ });
|
|
+ };
|
|
|
|
function sendSize() {
|
|
worker.postMessage({
|
|
type: 'size',
|
|
width: canvas.clientWidth,
|
|
height: canvas.clientHeight,
|
|
});
|
|
}
|
|
|
|
window.addEventListener('resize', sendSize);
|
|
sendSize();
|
|
|
|
console.log('using OffscreenCanvas'); /* eslint-disable-line no-console */
|
|
}
|
|
|
|
function startMainPage(canvas) {
|
|
init({canvas});
|
|
|
|
+ sendMouse = (x, y) => {
|
|
+ pickPosition.x = x;
|
|
+ pickPosition.y = y;
|
|
+ };
|
|
|
|
function sendSize() {
|
|
state.width = canvas.clientWidth;
|
|
state.height = canvas.clientHeight;
|
|
}
|
|
window.addEventListener('resize', sendSize);
|
|
sendSize();
|
|
|
|
console.log('using regular canvas'); /* eslint-disable-line no-console */
|
|
}
|
|
</pre>
|
|
<p>全てのマウス操作コードをメインページにコピーし、<code class="notranslate" translate="no">sendMouse</code> を使用するようにマイナーチェンジを加えます。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function setPickPosition(event) {
|
|
const pos = getCanvasRelativePosition(event);
|
|
- pickPosition.x = (pos.x / canvas.clientWidth ) * 2 - 1;
|
|
- pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1; // note we flip Y
|
|
+ sendMouse(
|
|
+ (pos.x / canvas.clientWidth ) * 2 - 1,
|
|
+ (pos.y / canvas.clientHeight) * -2 + 1); // note we flip Y
|
|
}
|
|
|
|
function clearPickPosition() {
|
|
// unlike the mouse which always has a position
|
|
// if the user stops touching the screen we want
|
|
// to stop picking. For now we just pick a value
|
|
// unlikely to pick something
|
|
- pickPosition.x = -100000;
|
|
- pickPosition.y = -100000;
|
|
+ sendMouse(-100000, -100000);
|
|
}
|
|
window.addEventListener('mousemove', setPickPosition);
|
|
window.addEventListener('mouseout', clearPickPosition);
|
|
window.addEventListener('mouseleave', clearPickPosition);
|
|
|
|
window.addEventListener('touchstart', (event) => {
|
|
// prevent the window from scrolling
|
|
event.preventDefault();
|
|
setPickPosition(event.touches[0]);
|
|
}, {passive: false});
|
|
|
|
window.addEventListener('touchmove', (event) => {
|
|
setPickPosition(event.touches[0]);
|
|
});
|
|
|
|
window.addEventListener('touchend', clearPickPosition);
|
|
</pre>
|
|
<p>これでこのピッキングは <code class="notranslate" translate="no">OffscreenCanvas</code> で動作するはずです。</p>
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-picking.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/offscreencanvas-w-picking.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>もう1歩踏み込んで <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> を追加してみましょう。
|
|
これはもう少し複雑です。
|
|
<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> はマウス、タッチイベント、キーボードなどDOMをかなり広範囲にチェックしています。</p>
|
|
<p>これまでのコードとは異なり、グローバルな <code class="notranslate" translate="no">state</code> オブジェクトを使う事はできません。
|
|
これを使用して動作するようにOrbitControlsのコードを全て書き換える必要はありません。
|
|
OrbitControlsは <code class="notranslate" translate="no">HTMLElement</code> を取り、それに使用するDOMイベントのほとんどをアタッチします。
|
|
OrbitControlsが必要とする機能をサポートする必要があります。</p>
|
|
<p><a href="https://github.com/mrdoob/three.js/blob/master/examples/jsm/controls/OrbitControls.js">OrbitControlsのソースコード</a>を掘り下げてみると、次のイベントを処理する必要があるように見えます。</p>
|
|
<ul>
|
|
<li>contextmenu</li>
|
|
<li>pointerdown</li>
|
|
<li>pointermove</li>
|
|
<li>pointerup</li>
|
|
<li>touchstart</li>
|
|
<li>touchmove</li>
|
|
<li>touchend</li>
|
|
<li>wheel</li>
|
|
<li>keydown</li>
|
|
</ul>
|
|
<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>
|
|
<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>
|
|
<p>ホイールイベントに必要なのは <code class="notranslate" translate="no">deltaY</code> プロパティだけです。</p>
|
|
<p>また、タッチイベントに必要なのは <code class="notranslate" translate="no">touches</code> プロパティの <code class="notranslate" translate="no">pageX</code> と <code class="notranslate" translate="no">pageY</code> だけです。</p>
|
|
<p>そこでproxyオブジェクトのペアを作ってみましょう。
|
|
ある時はメインページで実行され、全てのイベント、関連するプロパティ値をWorkerに渡します。
|
|
また、ある時はWorkerで実行され、全てのイベント、DOMイベントと同じ構造をもつイベントをメインページに渡すので、OrbitControlsは違いを見分けられません。</p>
|
|
<p>ここにWorker部分のコードがあります。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {EventDispatcher} from 'three';
|
|
|
|
class ElementProxyReceiver extends EventDispatcher {
|
|
constructor() {
|
|
super();
|
|
}
|
|
handleEvent(data) {
|
|
this.dispatchEvent(data);
|
|
}
|
|
}
|
|
</pre>
|
|
<p>メッセージを受信した場合にdataを送信するだけです。
|
|
これは <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>
|
|
<p><code class="notranslate" translate="no">ElementProxyReceiver</code> は1つの要素を扱います。
|
|
私たちの場合は1つの頭しか必要ありませんが、頭で考えるのがベストです。
|
|
つまり、マネージャーを作って複数のElementProxyReceiverを管理するようにしましょう。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ProxyManager {
|
|
constructor() {
|
|
this.targets = {};
|
|
this.handleEvent = this.handleEvent.bind(this);
|
|
}
|
|
makeProxy(data) {
|
|
const {id} = data;
|
|
const proxy = new ElementProxyReceiver();
|
|
this.targets[id] = proxy;
|
|
}
|
|
getProxy(id) {
|
|
return this.targets[id];
|
|
}
|
|
handleEvent(data) {
|
|
this.targets[data.id].handleEvent(data.data);
|
|
}
|
|
}
|
|
</pre>
|
|
<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>
|
|
<p>Workerのメッセージハンドラに接続してみましょう。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const proxyManager = new ProxyManager();
|
|
|
|
function start(data) {
|
|
const proxy = proxyManager.getProxy(data.canvasId);
|
|
init({
|
|
canvas: data.canvas,
|
|
inputElement: proxy,
|
|
});
|
|
}
|
|
|
|
function makeProxy(data) {
|
|
proxyManager.makeProxy(data);
|
|
}
|
|
|
|
...
|
|
|
|
const handlers = {
|
|
- init,
|
|
- mouse,
|
|
+ start,
|
|
+ makeProxy,
|
|
+ event: proxyManager.handleEvent,
|
|
size,
|
|
};
|
|
|
|
self.onmessage = function(e) {
|
|
const fn = handlers[e.data.type];
|
|
if (typeof fn !== 'function') {
|
|
throw new Error('no handler for type: ' + e.data.type);
|
|
}
|
|
fn(e.data);
|
|
};
|
|
</pre>
|
|
<p>共有のthree.jsコードでは <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> をインポートして設定する必要があります。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
|
|
+import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
|
|
|
|
export function init(data) {
|
|
- const {canvas} = data;
|
|
+ const {canvas, inputElement} = data;
|
|
const renderer = new THREE.WebGLRenderer({canvas});
|
|
|
|
+ const controls = new OrbitControls(camera, inputElement);
|
|
+ controls.target.set(0, 0, 0);
|
|
+ controls.update();
|
|
</pre>
|
|
<p>OffscreenCanvas以外のサンプルコード例のようにキャンバスを渡すのではなく、
|
|
<code class="notranslate" translate="no">inputElement</code> を介してOrbitControlsをProxyに渡している事に注目して下さい。</p>
|
|
<p>次に <code class="notranslate" translate="no">canvas</code> を <code class="notranslate" translate="no">inputElement</code> に変更し、HTMLファイルから全てのピッキングイベントのコードを共有のthree.jsコードに移動させます。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function getCanvasRelativePosition(event) {
|
|
- const rect = canvas.getBoundingClientRect();
|
|
+ const rect = inputElement.getBoundingClientRect();
|
|
return {
|
|
x: event.clientX - rect.left,
|
|
y: event.clientY - rect.top,
|
|
};
|
|
}
|
|
|
|
function setPickPosition(event) {
|
|
const pos = getCanvasRelativePosition(event);
|
|
- sendMouse(
|
|
- (pos.x / canvas.clientWidth ) * 2 - 1,
|
|
- (pos.y / canvas.clientHeight) * -2 + 1); // note we flip Y
|
|
+ pickPosition.x = (pos.x / inputElement.clientWidth ) * 2 - 1;
|
|
+ pickPosition.y = (pos.y / inputElement.clientHeight) * -2 + 1; // note we flip Y
|
|
}
|
|
|
|
function clearPickPosition() {
|
|
// unlike the mouse which always has a position
|
|
// if the user stops touching the screen we want
|
|
// to stop picking. For now we just pick a value
|
|
// unlikely to pick something
|
|
- sendMouse(-100000, -100000);
|
|
+ pickPosition.x = -100000;
|
|
+ pickPosition.y = -100000;
|
|
}
|
|
|
|
*inputElement.addEventListener('mousemove', setPickPosition);
|
|
*inputElement.addEventListener('mouseout', clearPickPosition);
|
|
*inputElement.addEventListener('mouseleave', clearPickPosition);
|
|
|
|
*inputElement.addEventListener('touchstart', (event) => {
|
|
// prevent the window from scrolling
|
|
event.preventDefault();
|
|
setPickPosition(event.touches[0]);
|
|
}, {passive: false});
|
|
|
|
*inputElement.addEventListener('touchmove', (event) => {
|
|
setPickPosition(event.touches[0]);
|
|
});
|
|
|
|
*inputElement.addEventListener('touchend', clearPickPosition);
|
|
</pre>
|
|
<p>メインページに戻り、上記で列挙した全てのイベントにメッセージを送信するコードが必要です。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">let nextProxyId = 0;
|
|
class ElementProxy {
|
|
constructor(element, worker, eventHandlers) {
|
|
this.id = nextProxyId++;
|
|
this.worker = worker;
|
|
const sendEvent = (data) => {
|
|
this.worker.postMessage({
|
|
type: 'event',
|
|
id: this.id,
|
|
data,
|
|
});
|
|
};
|
|
|
|
// register an id
|
|
worker.postMessage({
|
|
type: 'makeProxy',
|
|
id: this.id,
|
|
});
|
|
for (const [eventName, handler] of Object.entries(eventHandlers)) {
|
|
element.addEventListener(eventName, function(event) {
|
|
handler(event, sendEvent);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
</pre>
|
|
<p><code class="notranslate" translate="no">ElementProxy</code> はProxyしたいイベントの要素を受け取ります。
|
|
次にWorkerにidを登録し、先ほど設定した <code class="notranslate" translate="no">makeProxy</code> メッセージを使って送信します。
|
|
Workerは <code class="notranslate" translate="no">ElementProxyReceiver</code> を作成しそのidに登録します。</p>
|
|
<p>そして登録するイベントハンドラのオブジェクトを用意します。
|
|
このようにして、Workerに転送したいイベントにハンドラを渡す事ができます。</p>
|
|
<p>Workerを起動する時はまずProxyを作成しイベントハンドラを渡します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startWorker(canvas) {
|
|
const offscreen = canvas.transferControlToOffscreen();
|
|
const worker = new Worker('offscreencanvas-worker-orbitcontrols.js', {type: 'module'});
|
|
|
|
+ const eventHandlers = {
|
|
+ contextmenu: preventDefaultHandler,
|
|
+ mousedown: mouseEventHandler,
|
|
+ mousemove: mouseEventHandler,
|
|
+ mouseup: mouseEventHandler,
|
|
+ pointerdown: mouseEventHandler,
|
|
+ pointermove: mouseEventHandler,
|
|
+ pointerup: mouseEventHandler,
|
|
+ touchstart: touchEventHandler,
|
|
+ touchmove: touchEventHandler,
|
|
+ touchend: touchEventHandler,
|
|
+ wheel: wheelEventHandler,
|
|
+ keydown: filteredKeydownEventHandler,
|
|
+ };
|
|
+ const proxy = new ElementProxy(canvas, worker, eventHandlers);
|
|
worker.postMessage({
|
|
type: 'start',
|
|
canvas: offscreen,
|
|
+ canvasId: proxy.id,
|
|
}, [offscreen]);
|
|
console.log('using OffscreenCanvas'); /* eslint-disable-line no-console */
|
|
}
|
|
</pre>
|
|
<p>以下はイベントハンドラです。
|
|
受信したイベントからプロパティのリストをコピーするだけです。
|
|
<code class="notranslate" translate="no">sendEvent</code> 関数に渡され作成したデータを渡します。
|
|
この関数は正しいidを追加してWorkerに送信します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const mouseEventHandler = makeSendPropertiesHandler([
|
|
'ctrlKey',
|
|
'metaKey',
|
|
'shiftKey',
|
|
'button',
|
|
'pointerType',
|
|
'clientX',
|
|
'clientY',
|
|
'pageX',
|
|
'pageY',
|
|
]);
|
|
const wheelEventHandlerImpl = makeSendPropertiesHandler([
|
|
'deltaX',
|
|
'deltaY',
|
|
]);
|
|
const keydownEventHandler = makeSendPropertiesHandler([
|
|
'ctrlKey',
|
|
'metaKey',
|
|
'shiftKey',
|
|
'keyCode',
|
|
]);
|
|
|
|
function wheelEventHandler(event, sendFn) {
|
|
event.preventDefault();
|
|
wheelEventHandlerImpl(event, sendFn);
|
|
}
|
|
|
|
function preventDefaultHandler(event) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
function copyProperties(src, properties, dst) {
|
|
for (const name of properties) {
|
|
dst[name] = src[name];
|
|
}
|
|
}
|
|
|
|
function makeSendPropertiesHandler(properties) {
|
|
return function sendProperties(event, sendFn) {
|
|
const data = {type: event.type};
|
|
copyProperties(event, properties, data);
|
|
sendFn(data);
|
|
};
|
|
}
|
|
|
|
function touchEventHandler(event, sendFn) {
|
|
const touches = [];
|
|
const data = {type: event.type, touches};
|
|
for (let i = 0; i < event.touches.length; ++i) {
|
|
const touch = event.touches[i];
|
|
touches.push({
|
|
pageX: touch.pageX,
|
|
pageY: touch.pageY,
|
|
});
|
|
}
|
|
sendFn(data);
|
|
}
|
|
|
|
// The four arrow keys
|
|
const orbitKeys = {
|
|
'37': true, // left
|
|
'38': true, // up
|
|
'39': true, // right
|
|
'40': true, // down
|
|
};
|
|
function filteredKeydownEventHandler(event, sendFn) {
|
|
const {keyCode} = event;
|
|
if (orbitKeys[keyCode]) {
|
|
event.preventDefault();
|
|
keydownEventHandler(event, sendFn);
|
|
}
|
|
}
|
|
</pre>
|
|
<p>これで動くと思われるが、実際に試してみると <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> がもう少し必要なものがあると分かります。</p>
|
|
<p>1つは <code class="notranslate" translate="no">element.focus</code> です。Workerには必要ないのでStubを追加しておきましょう。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxyReceiver extends THREE.EventDispatcher {
|
|
constructor() {
|
|
super();
|
|
}
|
|
handleEvent(data) {
|
|
this.dispatchEvent(data);
|
|
}
|
|
+ focus() {
|
|
+ // no-op
|
|
+ }
|
|
}
|
|
</pre>
|
|
<p>もう1つは <code class="notranslate" translate="no">event.preventDefault</code> と <code class="notranslate" translate="no">event.stopPropagation</code> を呼び出す事です。
|
|
メインページでは既に対応してるのでそれらも不要になります。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function noop() {
|
|
+}
|
|
|
|
class ElementProxyReceiver extends THREE.EventDispatcher {
|
|
constructor() {
|
|
super();
|
|
}
|
|
handleEvent(data) {
|
|
+ data.preventDefault = noop;
|
|
+ data.stopPropagation = noop;
|
|
this.dispatchEvent(data);
|
|
}
|
|
focus() {
|
|
// no-op
|
|
}
|
|
}
|
|
</pre>
|
|
<p>もう1つは <code class="notranslate" translate="no">clientWidth</code> と <code class="notranslate" translate="no">clientHeight</code> を見る事です。
|
|
以前はサイズを渡してましたが、Proxyペアを更新してそれも渡すようにします。</p>
|
|
<p>Workerの中では</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxyReceiver extends THREE.EventDispatcher {
|
|
constructor() {
|
|
super();
|
|
}
|
|
+ get clientWidth() {
|
|
+ return this.width;
|
|
+ }
|
|
+ get clientHeight() {
|
|
+ return this.height;
|
|
+ }
|
|
+ getBoundingClientRect() {
|
|
+ return {
|
|
+ left: this.left,
|
|
+ top: this.top,
|
|
+ width: this.width,
|
|
+ height: this.height,
|
|
+ right: this.left + this.width,
|
|
+ bottom: this.top + this.height,
|
|
+ };
|
|
+ }
|
|
handleEvent(data) {
|
|
+ if (data.type === 'size') {
|
|
+ this.left = data.left;
|
|
+ this.top = data.top;
|
|
+ this.width = data.width;
|
|
+ this.height = data.height;
|
|
+ return;
|
|
+ }
|
|
data.preventDefault = noop;
|
|
data.stopPropagation = noop;
|
|
this.dispatchEvent(data);
|
|
}
|
|
focus() {
|
|
// no-op
|
|
}
|
|
}
|
|
</pre>
|
|
<p>メインページに戻るにはサイズと左と上の位置も送信する必要があります。
|
|
このままではキャンバスを移動しても処理されず、サイズを変更しても処理されないです。
|
|
移動を処理したい場合は何かがキャンバスを移動する度に <code class="notranslate" translate="no">sendSize</code> を呼び出す必要があります。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">class ElementProxy {
|
|
constructor(element, worker, eventHandlers) {
|
|
this.id = nextProxyId++;
|
|
this.worker = worker;
|
|
const sendEvent = (data) => {
|
|
this.worker.postMessage({
|
|
type: 'event',
|
|
id: this.id,
|
|
data,
|
|
});
|
|
};
|
|
|
|
// register an id
|
|
worker.postMessage({
|
|
type: 'makeProxy',
|
|
id: this.id,
|
|
});
|
|
+ sendSize();
|
|
for (const [eventName, handler] of Object.entries(eventHandlers)) {
|
|
element.addEventListener(eventName, function(event) {
|
|
handler(event, sendEvent);
|
|
});
|
|
}
|
|
|
|
+ function sendSize() {
|
|
+ const rect = element.getBoundingClientRect();
|
|
+ sendEvent({
|
|
+ type: 'size',
|
|
+ left: rect.left,
|
|
+ top: rect.top,
|
|
+ width: element.clientWidth,
|
|
+ height: element.clientHeight,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ window.addEventListener('resize', sendSize);
|
|
}
|
|
}
|
|
</pre>
|
|
<p>そして共有のthree.jsコードでは <code class="notranslate" translate="no">state</code> は不要になりました。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-export const state = {
|
|
- width: 300, // canvas default
|
|
- height: 150, // canvas default
|
|
-};
|
|
|
|
...
|
|
|
|
function resizeRendererToDisplaySize(renderer) {
|
|
const canvas = renderer.domElement;
|
|
- const width = state.width;
|
|
- const height = state.height;
|
|
+ const width = inputElement.clientWidth;
|
|
+ const height = inputElement.clientHeight;
|
|
const needResize = canvas.width !== width || canvas.height !== height;
|
|
if (needResize) {
|
|
renderer.setSize(width, height, false);
|
|
}
|
|
return needResize;
|
|
}
|
|
|
|
function render(time) {
|
|
time *= 0.001;
|
|
|
|
if (resizeRendererToDisplaySize(renderer)) {
|
|
- camera.aspect = state.width / state.height;
|
|
+ camera.aspect = inputElement.clientWidth / inputElement.clientHeight;
|
|
camera.updateProjectionMatrix();
|
|
}
|
|
|
|
...
|
|
</pre>
|
|
<p>他にもいくつかのハックがあります。
|
|
OrbitControlsは <code class="notranslate" translate="no">pointermove</code> と <code class="notranslate" translate="no">pointerup</code> イベントをマウスキャプチャ(マウスがウィンドウの外に出た時)を処理するための要素の <code class="notranslate" translate="no">ownerDocument</code> です。</p>
|
|
<p>さらにコードはグローバルな <code class="notranslate" translate="no">document</code> を参照していますが、Workerにはグローバルなdocumentはありません。</p>
|
|
<p>これは2つの簡単なハックで全て解決できます。
|
|
Workerコードでは両方の問題に対してProxyを再利用します。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function start(data) {
|
|
const proxy = proxyManager.getProxy(data.canvasId);
|
|
+ proxy.ownerDocument = proxy; // HACK!
|
|
+ self.document = {} // HACK!
|
|
init({
|
|
canvas: data.canvas,
|
|
inputElement: proxy,
|
|
});
|
|
}
|
|
</pre>
|
|
<p>これで <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> が期待に沿った検査を行うための機能を提供します。</p>
|
|
<p>難しいのは分かっていますが手短に言うと:</p>
|
|
<p><code class="notranslate" translate="no">ElementProxy</code> はメインページ上で動作し、DOMイベントを転送します。
|
|
Worker内の <code class="notranslate" translate="no">ElementProxyReceiver</code> は一緒に使うことができる <code class="notranslate" translate="no">HTMLElement</code> を装っています。
|
|
<a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> と独自のコードを使用しています。</p>
|
|
<p>最後にOffscreenCanvasを使用していない時のフォールバックです。
|
|
必要なのはcanvas自体を <code class="notranslate" translate="no">inputElement</code> として渡す事です。</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">function startMainPage(canvas) {
|
|
- init({canvas});
|
|
+ init({canvas, inputElement: canvas});
|
|
console.log('using regular canvas');
|
|
}
|
|
</pre>
|
|
<p>これでOrbitControlsがOffscreenCanvasで動作するようになりました。</p>
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/offscreencanvas-w-orbitcontrols.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/offscreencanvas-w-orbitcontrols.html" target="_blank">ここをクリックして別のウィンドウで開きます</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>これはおそらくこのサイトで最も複雑な例です。
|
|
各サンプルには3つのファイルが含まれているので少しわかりにくいです。
|
|
HTMLファイル、Workerファイル、共有のthree.jsコードなどです。</p>
|
|
<p>理解する事が難し過ぎず、少しでも参考になれば幸いです。
|
|
three.js、OffscreenCanvas、Web Workerを使った動作の便利な例を紹介しました。</p>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/manual/resources/prettify.js"></script>
|
|
<script src="/manual/resources/lesson.js"></script>
|
|
|
|
|
|
|
|
|
|
</body></html>
|