WebGL / WebGPU公開 約13分

WebGL シェーダーのエラーを読む:コンパイルとリンクを分ける

シェーダーを直したのに描画へ進めないときに。WebGL 1の小さな診断ページで、構文エラーとvaryingの型の不一致を再現します。成功判定とログの取得先を分け、直した段階を確かめます。

この記事の目次
  1. 一枚の黒い画面からは、失敗した段階が分からない
  2. 三条件を切り替える診断ページ
  3. セミコロンを消した場合は、fragmentのログを読む
  4. 両方コンパイルできても、接続する型は食い違う
  5. 診断できた範囲を、描画成功と区別する

WebGLで何も描かれないとき、頂点の座標を何度変えても直らない場合があります。描画を始める前のシェーダーに問題があれば、その座標を使う段階まで到達していません。まず「コンパイルできたか」「二つをリンクできたか」を別々に調べます。

JavaScriptの関数、配列、tryとfinallyが読める人向けです。WebGL 1とGLSL ES 1.00を使い、正常・構文エラー・リンクエラーの三条件を選べる診断ページを作ります。三角形は描きません。描画以前の検査に絞り、結果をcanvasの外の文章へ表示します。

一枚の黒い画面からは、失敗した段階が分からない

シェーダーは、描画に使う短いプログラムです。ここでは頂点の位置を扱うvertexと、色を扱うfragmentを一つずつ用意します。それぞれのソースをコンパイルし、使える形へ変換します。その後、両者をprogramへ取り付け、組み合わせとして成立するかリンクします。

KhronosのWebGL 1仕様のPrograms and Shadersにも、この順序が示されています。参照先は更新される編集草案です。今回はWebGL 2のin・outやGLSL ES 3.00を混在させず、WebGL 1のvaryingで両段階の値を受け渡します。

段階 合否を調べる対象 失敗時のログ
vertexのコンパイル vertex shaderのCOMPILE_STATUS そのshaderのgetShaderInfoLog
fragmentのコンパイル fragment shaderのCOMPILE_STATUS そのshaderのgetShaderInfoLog
二つのリンク programのLINK_STATUS そのprogramのgetProgramInfoLog

getShaderParameterとgetProgramParameterは、対象も指定する定数も異なります。コンパイルが二つとも通ったことを、そのままリンク成功とは扱いません。

三条件を切り替える診断ページ

次をshader-check.htmlへ保存して開きます。追加のライブラリや画像は不要です。診断用canvasは1×1の非表示要素で、画面の拡大率や幅に応じた描画は行いません。出力欄だけが狭い幅でも折り返す構成です。

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>シェーダーの段階別診断</title>
<style>
body { margin: 16px; font-family: sans-serif; line-height: 1.8; }
main { max-width: 640px; margin: auto; }
button, select { font: inherit; margin-block: 8px; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
:focus-visible { outline: 3px solid #174ea6; outline-offset: 3px; }
</style>
</head>
<body>
<main>
<h1>シェーダーの診断</h1>
<p>描画前のコンパイルとリンクを調べる練習です。</p>
<label for="mode">検査する条件</label>
<select id="mode">
  <option value="ok">正常</option>
  <option value="syntax">構文エラー</option>
  <option value="link">リンクエラー</option>
</select>
<button id="run" type="button" disabled>検査する</button>
<pre id="result" role="status">JavaScriptとWebGLを確認しています。</pre>
<canvas id="gpu" width="1" height="1" hidden></canvas>
</main>
<script>
const canvas = document.querySelector('#gpu');
const result = document.querySelector('#result');
const button = document.querySelector('#run');
const mode = document.querySelector('#mode');
const gl = canvas.getContext('webgl');

const vertex = `
varying mediump vec3 vTint;
void main() {
  vTint = vec3(1.0, 0.4, 0.0);
  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}`;
const fragment = `
precision mediump float;
varying mediump vec3 vTint;
void main() {
  gl_FragColor = vec4(vTint, 1.0);
}`;

function diagnose(kind) {
  const shaders = [];
  let program = null;
  function compile(type, source, label) {
    const shader = gl.createShader(type);
    if (!shader) throw new Error(label + ': 作成できません');
    shaders.push(shader);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      throw new Error(label + ' compile\n' +
        (gl.getShaderInfoLog(shader) || 'ログなし'));
    }
    return shader;
  }
  try {
    let source = fragment;
    if (kind === 'syntax') {
      source = source.replace('vec4(vTint, 1.0);', 'vec4(vTint, 1.0)');
    }
    if (kind === 'link') {
      source = source.replace('vec3 vTint', 'vec2 vTint')
        .replace('vec4(vTint, 1.0)', 'vec4(vTint, 0.0, 1.0)');
    }
    const vs = compile(gl.VERTEX_SHADER, vertex, 'vertex');
    const fs = compile(gl.FRAGMENT_SHADER, source, 'fragment');
    program = gl.createProgram();
    if (!program) throw new Error('program: 作成できません');
    gl.attachShader(program, vs);
    gl.attachShader(program, fs);
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      throw new Error('program link\n' +
        (gl.getProgramInfoLog(program) || 'ログなし'));
    }
    return 'vertex compile: OK\nfragment compile: OK\nprogram link: OK';
  } catch (error) {
    return gl.isContextLost() ? 'WebGLを失いました。再読み込みしてください。'
      : error.message;
  } finally {
    if (program) gl.deleteProgram(program);
    for (const shader of shaders) gl.deleteShader(shader);
  }
}

if (gl) {
  button.disabled = false;
  result.textContent = '条件を選び、検査してください。';
  button.addEventListener('click', () => {
    result.textContent = diagnose(mode.value);
  });
} else {
  result.textContent = 'この環境ではWebGLを利用できません。';
}
canvas.addEventListener('webglcontextlost', () => {
  button.disabled = true;
  result.textContent = 'WebGLを失いました。再読み込みしてください。';
});
</script>
</body>
</html>

正常を選ぶと三行のOKが出ます。createShaderなどが使う資源は、途中で失敗した場合もfinallyで後始末します。この診断ではリンク後にprogramを描画へ使わないため、成功時にも削除します。実際の描画処理へ移すときは、使用中のprogramまでこの場所で消さないよう、寿命を設計し直します。

セミコロンを消した場合は、fragmentのログを読む

構文エラーを選ぶと、fragment内の代入末尾からセミコロンがなくなります。期待する結果はfragment compileの失敗です。ログに示された行番号は、JavaScriptを置いたHTMLの行番号ではなく、渡したGLSLソース内の位置として読みます。

今回の文字列には先頭に空行があります。表示された番号だけをHTMLの行へ当てはめると、関係のない場所を直してしまいます。JavaScriptのエラーの読み方と同じく、種類と場所を拾いますが、調べるソースの言語も分ける必要があります。

修正はfragmentの代入をセミコロンで終えることです。例では正常を選び直せば、元のソースを渡せます。再検査でfragmentだけでなくprogram linkまでOKになるか確認します。診断文の英文全体は環境で異なるため、一字一句の一致を合格条件にしません。

両方コンパイルできても、接続する型は食い違う

リンクエラーではfragment側のvTintをvec2へ変え、色を作る式もvec2を受け取れる形へ変えています。片側だけを見れば構文と式は成立します。しかしvertexは同名の値をvec3で渡すので、組み合わせの型が一致しません。期待する結果はprogram linkの失敗です。

ここでfragmentのログを読み直しても、二つの接続に関する診断は得られません。getProgramInfoLogの資料にあるprogramのログを調べ、両方の宣言を照合します。修正では名前、型、精度の対応をそろえ、正常条件を再検査します。

また、ログの文字数だけで合否を決めないでください。getShaderInfoLogには警告も含まれます。まずステータスで分岐し、失敗した対象のログを読む順序です。この例は失敗時のログへ絞っていますが、開発用の道具なら成功時の警告を別欄へ残すこともできます。

診断できた範囲を、描画成功と区別する

三つの条件を順に選び、「三行のOK」「fragment compile」「program link」へ分かれるか確認します。最後に正常へ戻し、同じページで再び三行のOKが出れば、失敗後も再検査できます。キーボードで条件選択とボタン操作ができることも確認します。

リンク成功は、描画結果の保証ではありません。座標、バッファーの接続、viewport、色の設定などはまだ調べていません。表示のぼけや拡大率の問題ならcanvasの解像度の記事へ進みます。今回は非表示の1×1 canvasなので、DPRに応じた解像度変更やアニメーションは不要です。

WebGLが使えない場合はボタンを有効にせず、文章で案内します。コンテキストを失った後の自動復元もこの教材には含めません。ブラウザー・OS・描画環境を記録し、他のGPUで同じ英文が出るとは考えないでください。JavaScriptの無料レッスンで身に付ける分岐は、ここでは「どの段階まで成功したか」を残すために使えます。

TRY A LESSON

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

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

WebGL・3Dの発展課題 →

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

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

← 記事一覧に戻る