この記事の目次
WebGLのコンテキストが復元されたというイベントは届くのに、三角形が戻らない。以前作ったbufferやprogramを、そのまま描画へ渡していませんか。復元後には描画に必要な資源と設定を作り直す一方、選択中の色などのアプリの情報は別に残します。
JavaScriptのイベントと、WebGLのshader・bufferの基本が分かる人向けです。WebGL 1、GLSL ES 1.00で、架空の色選択見本を作ります。動かない三角形だけを描き、喪失と復元には検証用の拡張を使います。アニメーション、画像取得、実機の障害発生や性能測定は含めません。
残す選択と、作り直すGPU資源を表にする
コンテキストは、WebGLが描画に使う状態や資源を管理する環境です。喪失すると、その環境に作った資源を以前のまま使用できなくなります。復元イベントの説明では、復元後にtextureやbufferなどを作り直す必要が示されています。
| 情報 | 喪失中の扱い | 復元後の扱い |
|---|---|---|
| 選択した色 | JavaScriptの文字列として保持 | 同じ色をuniformへ渡す |
| 頂点配列・shaderの文字列 | 再作成の材料として保持 | 新しい資源へ渡す |
| buffer・program | 古い参照を外す | 新規作成する |
| uniformの場所・viewport | 再利用しない | 新program・現在の寸法で設定 |
資源を解放する記事は、使い終わった資源の所有者を決める課題です。今回は終了せずに表示を再開するため、再作成の材料と利用者の選択をGPUの外へ残す必要があります。
初期化を、最初と復元後の両方から呼ぶ
次をrestore.htmlへ保存します。空フォルダでpython3 -m http.server 8774 --bind 127.0.0.1を実行し、http://127.0.0.1:8774/restore.htmlを開きます。色と位置の対応を見る教材で、拡張が使えない環境では模擬ボタンを無効にします。
<!doctype html>
<html lang="ja"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGLの喪失と復元</title>
<style>
body { margin: 16px; font-family: sans-serif; line-height: 1.8; }
main { max-width: 560px; margin: auto; }
.frame { position: relative; width: 100%; aspect-ratio: 2; }
canvas, svg { position: absolute; inset: 0; width: 100%; height: 100%; }
[hidden] { display: none; }
button, select { font: inherit; margin: 4px; max-width: 100%; }
:focus-visible { outline: 3px solid #174ea6; outline-offset: 3px; }
</style></head><body><main>
<h1>三角形の色見本(架空の練習)</h1>
<label for="color">色</label>
<select id="color"><option value="orange">橙色</option><option value="blue">青色</option></select>
<div class="frame">
<svg id="fallback" role="img" aria-label="選択した色の三角形の静止図" viewBox="0 0 200 100">
<rect width="200" height="100" fill="#fff"/><path id="triangle" d="M30 85L170 85L100 15Z" fill="#ff8000"/>
</svg>
<canvas id="canvas" aria-label="選択した色の三角形" hidden></canvas>
</div>
<p id="status" role="status">静止図を表示しています。</p>
<button id="lose" type="button" disabled>喪失を模擬</button>
<button id="restore" type="button" disabled>復元を要求</button>
<button id="stop" type="button">描画を終了</button>
</main><script>
const canvas = document.querySelector('#canvas');
const fallback = document.querySelector('#fallback');
const status = document.querySelector('#status');
const select = document.querySelector('#color');
const lose = document.querySelector('#lose');
const restore = document.querySelector('#restore');
const stop = document.querySelector('#stop');
const gl = canvas.getContext('webgl');
let active = null;
let generation = 0;
let ended = false;
let color = 'orange';
let extension = null;
const events = new AbortController();
const options = { signal: events.signal };
const observer = new ResizeObserver(() => draw());
function staticView(message) {
canvas.hidden = true;
fallback.hidden = false;
status.textContent = message;
}
function dispose() {
if (!active) return;
if (!gl.isContextLost()) {
gl.deleteBuffer(active.buffer);
gl.deleteProgram(active.program);
}
active = null;
}
function initialize() {
if (ended || gl.isContextLost()) return;
dispose();
const shaders = [];
let program;
let buffer;
try {
function compile(type, source) {
const shader = gl.createShader(type);
if (!shader) throw new Error('shader作成失敗');
shaders.push(shader);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error('shaderの検証失敗');
return shader;
}
const vertex = compile(gl.VERTEX_SHADER,
'attribute vec2 position; void main(){ gl_Position = vec4(position, 0.0, 1.0); }');
const fragment = compile(gl.FRAGMENT_SHADER,
'precision mediump float; uniform vec3 ink; void main(){ gl_FragColor = vec4(ink, 1.0); }');
program = gl.createProgram();
if (!program) throw new Error('program作成失敗');
gl.attachShader(program, vertex);
gl.attachShader(program, fragment);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error('リンク失敗');
buffer = gl.createBuffer();
if (!buffer) throw new Error('buffer作成失敗');
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-0.7,-0.7, 0.7,-0.7, 0,0.7]), gl.STATIC_DRAW);
const position = gl.getAttribLocation(program, 'position');
const ink = gl.getUniformLocation(program, 'ink');
if (position < 0 || ink === null) throw new Error('描画項目が見つかりません');
active = { program, buffer, position, ink };
generation++;
extension = gl.getExtension('WEBGL_lose_context');
canvas.hidden = false;
fallback.hidden = true;
lose.disabled = !extension;
restore.disabled = true;
draw();
} catch (error) {
if (buffer) gl.deleteBuffer(buffer);
if (program) gl.deleteProgram(program);
active = null;
lose.disabled = true;
staticView(error.message + '。静止図を表示します。');
} finally {
shaders.forEach(shader => gl.deleteShader(shader));
}
}
function draw() {
if (ended || !active || gl.isContextLost()) return;
const box = canvas.parentElement.getBoundingClientRect();
const dpr = Math.min(devicePixelRatio, 2);
canvas.width = Math.max(1, Math.round(box.width * dpr));
canvas.height = Math.max(1, Math.round(box.height * dpr));
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(1, 1, 1, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(active.program);
gl.bindBuffer(gl.ARRAY_BUFFER, active.buffer);
gl.enableVertexAttribArray(active.position);
gl.vertexAttribPointer(active.position, 2, gl.FLOAT, false, 0, 0);
gl.uniform3fv(active.ink, color === 'blue' ? [0,0.4,0.8] : [1,0.5,0]);
gl.drawArrays(gl.TRIANGLES, 0, 3);
status.textContent = `描画中:第${generation}世代、${color === 'blue' ? '青色' : '橙色'}`;
}
select.addEventListener('change', () => {
color = select.value;
document.querySelector('#triangle').setAttribute('fill', color === 'blue' ? '#0066cc' : '#ff8000');
draw();
}, options);
canvas.addEventListener('webglcontextlost', event => {
event.preventDefault();
active = null;
lose.disabled = true;
restore.disabled = !extension;
staticView('コンテキストを失いました。選択した色は静止図に残ります。');
}, options);
canvas.addEventListener('webglcontextrestored', initialize, options);
lose.addEventListener('click', () => extension?.loseContext(), options);
restore.addEventListener('click', () => {
extension?.restoreContext();
status.textContent = '復元を要求しました。完了までは静止図を表示します。';
}, options);
stop.addEventListener('click', () => {
ended = true;
observer.disconnect();
events.abort();
dispose();
select.disabled = lose.disabled = restore.disabled = stop.disabled = true;
staticView('描画を終了しました。再開するにはページを読み直してください。');
}, options);
if (gl) { initialize(); observer.observe(canvas.parentElement); }
else { staticView('WebGLを利用できないため静止図を表示します。'); }
</script></body></html>
最初は第1世代です。青へ変更して喪失を模擬し、静止図が青で残ることを確かめます。次に復元を要求すると、第2世代の青い三角形へ戻ります。色の文字列は保持し、programとbufferは新しく作った結果です。
WEBGL_lose_contextの説明が示す拡張を使った試験なので、実際のGPU障害を再現したとは扱いません。復元要求と完了イベントも別です。応答がなければ静止図のままにし、利用できたと断定しない表示を残します。
復元後に古い場所を使う失敗を直す
第一の失敗は、restoredでdrawだけを呼ぶことです。古いprogramやbufferは復元後の描画に使えず、参照を外した完成例ならactiveもありません。修正ではinitializeを呼び、新しいprogram、buffer、属性位置、uniformの場所を一式作り直します。
programだけ作り直しても、以前のgetUniformLocationの結果を使い続けてはいけません。場所はそのprogramに対応する情報です。修正後は世代番号が増えることと、選択色が実際の三角形へ反映されることを両方確認します。番号だけ増やしても描画成功の証拠にはなりません。
第二の失敗は、初期化関数の中でcolorをorangeへ戻すことです。復元はできますが、青を選んだ利用者の状態を失います。色をGPU資源とは別に保持し、初期化後のdrawが現在のcolorを読む形へ戻します。青で喪失・復元を二回行い、第3世代でも青が残るか再確認します。
復元を許可する処理と、永久に終了する処理
喪失イベントのpreventDefaultは、ブラウザーへ復元可能な扱いを求めるために必要です。KhronosのWebGL 1編集草案にあるイベントと復元の規則を参照しています。これを書いても、どんな環境でも復元が保証されるわけではありません。
終了ボタンでは、復元の待機とは異なりイベント登録と寸法監視を終え、残る資源を解放します。喪失中なら古い資源を削除する操作へ進まず、参照を外します。共有する描画エンジンでは、そのエンジンの所有権に合わせて終了処理を設計してください。
この例は静止画なので、アニメーションループや動きを減らす設定の分岐はありません。親幅の変更で再描画し、DPRは2までに制限します。DPRだけが変わるモニター移動などは色を切り替えて再描画するか、再読み込みで確認します。canvasの解像度の記事で扱う寸法の問題と、資源の再作成は別の確認項目です。
青のまま二回復元し、最後に終了する
15〜20分の課題では、初期の橙色、青を選んだ喪失中の静止図、復元後の青という三状態を記録します。復元を二回繰り返し、世代が1・2・3と増えても同じ青を保つことを確かめます。さらに幅を狭め、復元後も三角形全体が枠内に収まるかを見ます。
最後に終了を押すと静止図が残り、色や模擬ボタンは無効になります。復元中に終了した場合も再描画を始めないことが期待結果です。ブラウザー・OS・GPU環境を記録し、ソフトウェア描画による確認を実機GPUの性能検証とは区別してください。カリキュラムの描画課題でも、最初に絵が出ることに加え、何を再作成すれば再開できるかを完成条件にできます。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。