この記事の目次
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
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。