WebGL / WebGPU公開 約18分

WebGLコンテキストの復元:資源を作り直し、選択した色を戻す

WebGLが復元されたのに絵が戻らない場合に。GPU側の資源とJavaScript側の選択状態を分け、program・buffer・uniformの場所を再取得します。喪失中の代替図、復元後の色、終了後の後始末を同じ見本で確かめます。

この記事の目次
  1. 残す選択と、作り直すGPU資源を表にする
  2. 初期化を、最初と復元後の両方から呼ぶ
  3. 復元後に古い場所を使う失敗を直す
  4. 復元を許可する処理と、永久に終了する処理
  5. 青のまま二回復元し、最後に終了する

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

記事の内容を、課題で確かめる

前提になる表示サイズの練習 →

WebGL・3Dの発展課題 →

記事についてのお問い合わせ

記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。

← 記事一覧に戻る