|
| 1 | +--- |
| 2 | +title: オフスクリーンキャンバス |
| 3 | +author: 池田 泰延 |
| 4 | +published_date: 2019-02-12 |
| 5 | +modified_date: 2019-02-12 |
| 6 | +--- |
| 7 | + |
| 8 | +オフスクリーンキャンバスはWeb Workers(ウェブワーカーズ)を使用してWorkerスレッドで描画処理を行える機能です。負荷の高い描画処理をWorkerスレッドに移動することで、メインスレッドの負担が軽くなり、余裕ができます。これによりメインスレッドでスムーズなユーザー操作を実現できるようになり、ユーザー体験の向上が期待できます。 |
| 9 | + |
| 10 | +Three.jsでもオフスクリーンキャンバスを利用できます。複雑なことをしなければ難しくなく、通常のThree.jsのコードに少しの実装を追加するだけで実現できます。 |
| 11 | + |
| 12 | +オフスクリーンキャンバスの機能については記事「[オフスクリーンキャンバスを使ったJSのマルチスレッド描画 – スムーズなユーザー操作実現の切り札 \- ICS MEDIA](https://ics.media/entry/19043)」で詳しく解説しています。Three.jsで利用する前に一読をオススメします。 |
| 13 | + |
| 14 | + |
| 15 | +## 使い方 |
| 16 | + |
| 17 | +ワーカーを利用するにあたって、メインスレッド側とワーカー側と処理を分離する必要があります。これはファイル単位で分けなければなりません。まずはメインスレッド側のコードを紹介します。HTMLに`canvas`要素を配置し、JavaScriptではワーカー側に`canvas`要素のオフスクリーン用オブジェクトを転送します。 |
| 18 | + |
| 19 | +```html |
| 20 | +<canvas id="myCanvas"></canvas> |
| 21 | +``` |
| 22 | + |
| 23 | +```js |
| 24 | +// 普通のキャンバスを取得 |
| 25 | +const canvasElement = document.querySelector('#myCanvas'); |
| 26 | +// オフスクリーンキャンバスを取得 |
| 27 | +const offscreenCanvas = canvasElement.transferControlToOffscreen(); |
| 28 | +// ワーカーを起動 |
| 29 | +const worker = new Worker('osc_simple_worker.js'); |
| 30 | +// ワーカー側にオフスクリーンキャンバスを転送 |
| 31 | +worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas]); |
| 32 | +``` |
| 33 | + |
| 34 | +続いて、ワーカー側の処理を解説します。ワーカー側では、Three.jsを`importScripts()`メソッドを使って読み込みます。`importScripts()`メソッドはワーカーでのみ利用できる機能です。 |
| 35 | + |
| 36 | +```js |
| 37 | +importScripts( |
| 38 | + 'https://cdnjs.cloudflare.com/ajax/libs/three.js/101/three.min.js' |
| 39 | +); |
| 40 | +``` |
| 41 | + |
| 42 | +メインスレッド側からの起動コールを受信するために、`onmessage`イベントを監視します。ここに初期化処理を記述します。引数の`event.data`オブジェクトで、メインスレッド側からのデータを受け取れます。 |
| 43 | + |
| 44 | +```js |
| 45 | +// メインスレッドから通達があったとき |
| 46 | +onmessage = event => { |
| 47 | + // メインスレッドからオフスクリーンキャンバスを受け取る |
| 48 | + const canvas = event.data.canvas; |
| 49 | + // ・・・いろいろ処理 |
| 50 | +``` |
| 51 | +
|
| 52 | +バッドノウハウですが、ひとつだけ工夫しなければ、Three.jsをワーカー側で利用できません。Three.jsは内部でCanvas要素のstyleにアクセスします。しかし、OffscreenCanvasはDOM要素ではないため、`style`属性を持ちません。Three.jsで使用する場合はランタイムエラーを避けるため、OffscreenCanvasオブジェクトに明示的に`style`プロパティを付加します。 |
| 53 | +
|
| 54 | +```js |
| 55 | + // Three.jsのライブラリの内部で style.width にアクセスされてしまう |
| 56 | + // 対策しないと、エラーが発生するためダミーの値を指定 |
| 57 | + canvas.style = { width: 0, height: 0 }; |
| 58 | +``` |
| 59 | +
|
| 60 | +あとは普通にコードをかけばThree.jsが動きます。コードを全部みて呆気なさを感じてください。 |
| 61 | +
|
| 62 | +
|
| 63 | +## 画像の使い方 |
| 64 | +
|
| 65 | +オフスクリーンキャンバスで画像を使うには`ImageBitmap`を利用します。通常の`THREE.ImageLoader()`だとDOM APIの`Image`オブジェクト、つまり`img`タグが使われます。ワーカー側ではDOM APIが利用できないため、`img`タグで画像を読み込むことはできないのです。オフスクリーンキャンバスと同時期に用意された、`ImageBitmap`オブジェクトを使います。Three.jsでは`THREE.ImageBitmapLoader()`でファイルを読み込み、`ImageBitmap`インスタンスを`THREE.CanvasTexture`でテクスチャーへと変換します。あとは、適当なマテリアルにテクスチャーとして設定するだけです。 |
| 66 | +
|
| 67 | +```js |
| 68 | +// テクスチャーを読み込み |
| 69 | +const texture = await new Promise(resolve => { |
| 70 | + new THREE.ImageBitmapLoader().load('imgs/earthmap1k.jpg', imageBitmap => { |
| 71 | + const texture = new THREE.CanvasTexture(imageBitmap); |
| 72 | + resolve(texture); |
| 73 | + }); |
| 74 | +}); |
| 75 | +// マテリアルを作成 |
| 76 | +const material = new THREE.MeshStandardMaterial({ map: texture }); |
| 77 | +``` |
| 78 | +
|
| 79 | +## リサイズの方法 |
| 80 | +
|
| 81 | +通常のThree.jsのりサイズ処理は、記事「[リサイズ処理](renderer_resize.md)」を先に読んで学習しておいてください。その上で解説します。 |
| 82 | +
|
| 83 | +オフスクリーンキャンバスからだと、メインスレッド側のりサイズを検知できません。メインスレッド側のりサイズは、メインスレッド側で検知しなけばなりません。たとえば、次のようなコードで、ワーカー側にリサイズイベントを通知します。ワーカー側では`worker.postMessage()`メソッドにより通達を受けますが、初期化なのかリサイズイベントなのか判断する手がかりが必要なため、任意の`type`プロパティーを付与しています。 |
| 84 | +
|
| 85 | +```js |
| 86 | +// 普通のキャンバスを取得 |
| 87 | +const canvasElement = document.querySelector('#myCanvas'); |
| 88 | +// オフスクリーンキャンバスを取得 |
| 89 | +const offscreenCanvas = canvasElement.transferControlToOffscreen(); |
| 90 | +const worker = new Worker('osc_resize_worker.js'); |
| 91 | +worker.postMessage( |
| 92 | + { |
| 93 | + type: 'init', |
| 94 | + canvas: offscreenCanvas, |
| 95 | + width: innerWidth, |
| 96 | + height: innerHeight, |
| 97 | + devicePixelRatio: devicePixelRatio |
| 98 | + }, |
| 99 | + [offscreenCanvas] |
| 100 | +); |
| 101 | + |
| 102 | +window.addEventListener('resize', event => { |
| 103 | + worker.postMessage({ |
| 104 | + type: 'resize', |
| 105 | + width: innerWidth, |
| 106 | + height: innerHeight, |
| 107 | + devicePixelRatio: devicePixelRatio |
| 108 | + }); |
| 109 | +}); |
| 110 | +``` |
| 111 | +
|
| 112 | +ワーカー側の処理では、任意の`type`プロパティーの値をみて、条件文で処理を分離しています。 |
| 113 | +
|
| 114 | +```js |
| 115 | +let renderer; |
| 116 | +let camera; |
| 117 | + |
| 118 | +// メインスレッドから通達があったとき |
| 119 | +onmessage = event => { |
| 120 | + switch (event.data.type) { |
| 121 | + case 'init': |
| 122 | + init(event); |
| 123 | + break; |
| 124 | + case 'resize': |
| 125 | + resize(event.data.width, event.data.height, event.data.devicePixelRatio); |
| 126 | + break; |
| 127 | + } |
| 128 | +}; |
| 129 | +function init(event) { |
| 130 | + // ・・・初期化処理(省略) |
| 131 | +} |
| 132 | + |
| 133 | +function resize(width, height, devicePixelRatio) { |
| 134 | + // レンダラーのサイズを調整する |
| 135 | + renderer.setPixelRatio(devicePixelRatio); |
| 136 | + renderer.setSize(width, height); |
| 137 | + |
| 138 | + // カメラのアスペクト比を正す |
| 139 | + camera.aspect = width / height; |
| 140 | + camera.updateProjectionMatrix(); |
| 141 | +} |
| 142 | +``` |
0 commit comments