ClaudeにGodotのシェーダーを書かせる — GLSLとの差と動作確認
Claude Codeに.gdshaderを書かせるとき、GLSLの癖で落ちる箇所と、壊れたシェーダーで検査経路を確かめてから回す手順をまとめます。
Claude Codeに.gdshaderを書かせると、文法の似たGLSLの癖が混ざります。main()を書く、float a = 2;と書く、gl_FragColorに代入する。どれもGodotでは通りません。しかも、コマンドライン一覧にある--check-onlyはスクリプト向けで、シェーダーを対象にした検査オプションは載っていません。書けたかどうかを何で確かめるかが、この題材の論点です。
ここでは、GLSLとの差をCLAUDE.mdに落とす方法、検査経路が効くかを壊れたシェーダーで先に確かめる手順、描画結果をPNGで受け取る方法の順に進めます。Godotのプロジェクト全体の持たせ方はClaude CodeでGodotゲーム開発を始める手順にあります。ここでは.gdshaderだけを扱います。
Claudeが外しやすいGLSLとの差
Godotのシェーディング言語は、GLSL ES 3.0に近い言語です。ほとんどの型と関数は使えます。ただ、書き方の決まりがいくつか違います。GLSLやShadertoyの書き方が混ざりやすいので、違いを表にして渡すのが早い道です。
| GLSLで書きがちな形 | Godotで通る形 |
|---|---|
void main() | Godotで通る形void fragment() / void vertex() |
gl_FragColor = c; | Godotで通る形COLOR = c; |
float a = 2; | Godotで通る形float a = 2.0; または float(2) |
uniform vec4 c;(色) | Godotで通る形uniform vec4 c : source_color; |
precision mediump float; | Godotで通る形変数ごとにlowp / mediump / highp |
iTime / u_time | Godotで通る形TIME |
表の6行のうち、つまずきの多い4点を順に見ます。iTimeは移植の節で扱います。
エントリポイントと出力は組み込み変数に変わる
GLSLでは頂点とフラグメントで別々のプログラムを書きます。Godotは1つのシェーダーにvertex()とfragment()を並べ、片方だけ書けば、もう片方は用意されます。GLSLのmainは、頂点シェーダーならvertex、フラグメントシェーダーならfragmentに改名します(変換ガイドの案内)。出力色はgl_FragColorでなくCOLOR、座標はFRAGCOORDです。
暗黙のキャストは通らない
型のキャストはGLSL ES 3.0と同じ流儀で、整数から小数への暗黙変換を許しません。float a = 2;は無効で、float a = 2.0;かfloat a = float(2);と書きます。符号なし整数も同じで、uint a = 2;は無効です。数値リテラルの小数点が落ちた行は、最初に疑う場所になります。
色のuniformとテクスチャにはヒントを付ける
vec3 / vec4のuniformを色として扱うならsource_colorを付けます。テクスチャは、sRGBの色データならsource_colorが要ります。Godotはリニア色空間で描画するため、ヒントを外すと画像が白っぽく見えます。
ForwardプラスとMobileのレンダラーでは必須で、Compatibilityでは省略できます。それでも、レンダラーを切り替えても動くように、常に付けておく運用がシェーダー言語の解説で勧められています。法線やラフネスのテクスチャには、基本的に付けません。
精度の指定は変数単位になる
GLSLはファイル先頭にprecision文を置きます。Godotでは、lowp / mediump / highpを変数の型の前に付けます。必要がなければ精度を指定しないのが無難、というのが言語リファレンスの記述です。Claudeにはまず外させ、実機の負荷で困ってから足すほうが手戻りが少なくなります。
CLAUDE.mdに書くシェーダー規約
差が分かったら、毎回の指示文でなく、リポジトリのCLAUDE.mdに一度だけ書きます。次の断片は、上の表とリファレンスの記述をそのまま規約にした例です。
## Godotシェーダー(.gdshader)
- 1行目は必ず `shader_type canvas_item;` などの宣言にする
- エントリは `vertex()` / `fragment()` / `light()`。`main()` は書かない
- 出力は `COLOR`(canvas_item)。`gl_FragColor` は使わない
- 数値リテラルは小数点を付ける(`2.0`)。整数は `float(n)` で変換する
- 色のuniformとsRGBテクスチャには `source_color` を付ける
- `SCREEN_TEXTURE` は使わない。`sampler2D` に `hint_screen_texture` を付ける
- 共通関数は `.gdshaderinc` に切り出し、`#include` で読む
- Shadertoy / GLSLからの移植は、変換ガイドの対応表に沿って変数を置き換える
- 検証は `scripts/check_shader.sh` の出力を読んで直す。目視は最後に1回SCREEN_TEXTUREの行は、Godot 4で組み込み変数が削除され、hint_screen_texture付きのsampler2Dに置き換わったことに由来します。Godot 3の書き方が混ざると、ここで落ちます。
CLAUDE.mdが長くなってきた場合の上限は、CLAUDE.mdが途中までしか読まれない上限と確認法で確かめられます。
検査経路が効くかを、壊れたシェーダーで先に確かめる
ここが本題です。godot --headless --check-only -s <スクリプト>の--check-onlyは、コマンドライン一覧で「エラーだけを調べて終了する。--scriptと併用する」と書かれています。.gdshaderを対象にするとは書かれていません。
--headlessは--display-driver headless --audio-driver Dummyの別名で、ディスプレイと音声を無効にした実行モードです。この状態で描画系がシェーダーを解釈するかどうかは、コマンドラインのページに記載がありません。
つまり、--headlessで走らせて終了コードが0だったからといって、シェーダーが正しいとは限りません。そこで、本番のシェーダーを触る前に、わざと壊したシェーダーで経路を試します。
検査経路の動作確認
- 1
壊れたシェーダーを置く
res://shaders/_canary.gdshaderに、言語リファレンスが「invalid」と示すfloatへの整数代入を入れます。 - 2
読み込むだけのスクリプトを書く
SceneTreeを継承したスクリプトで、シェーダーをColorRectに貼り、1フレーム回して終了します。 - 3
ログに何が出るかを見る
エラー行が出れば、その行の特徴的な語を合否判定に使えます。何も出なければ、この経路は検査に使えません。
壊れたシェーダーの例は次のとおりです。
shader_type canvas_item;
void fragment() {
float broken = 2; // 整数を小数に暗黙変換できない
COLOR = vec4(broken);
}読み込み用のスクリプトの一例です。引数で受け取ったシェーダーをShaderMaterialに入れ、描画対象へ実際に貼ります。
extends SceneTree
func _init():
var args := OS.get_cmdline_user_args()
var mat := ShaderMaterial.new()
mat.shader = load(args[0])
var rect := ColorRect.new()
rect.size = Vector2(64, 64)
rect.material = mat
root.add_child(rect)
await process_frame
quit(0)godot --headless --path . -s scripts/load_shader.gd \
-- res://shaders/_canary.gdshader 2>&1 | tee canary.logcanary.logの読み方
- Q壊れたシェーダーに対応するエラー行がログに出たか?答えが出ない答えがはい
--headlessはシェーダーの検査に使えません。描画ありの実行(次の節以降)で確かめる構成に切り替えます。
ヘッドレスの検査が使える
エラー行の特徴的な語をgrepパターンにして、検査スクリプトの合否にします。正しいシェーダーでは同じ語が出ないことも、1回確かめておきます。
結果は、Godotのバージョンや使うレンダラーで変わりえます。確認はプロジェクトごとに1回、Godotを更新したときにもう1回やれば足ります。確かめた結果は、CLAUDE.mdの検証コマンドの行に書き残します。
Claudeに回させる検証スクリプトと権限
canaryで経路が決まったら、検査をスクリプトにまとめます。Claudeには「このスクリプトの出力を読んで直す」と指示するだけで済みます。
#!/usr/bin/env bash
# scripts/check_shader.sh — 引数のシェーダーを貼って1フレーム回す
set -u
out=$(godot --headless --path . -s scripts/load_shader.gd -- "$1" 2>&1)
echo "$out"
# canaryで確認したエラー行の語に置き換える
if echo "$out" | grep -q "<canaryで見つけた語>"; then exit 1; fi権限は、コマンドを具体的に書く形式で絞ります。Claude Codeの権限ルールにはBash(npm run build)のように、完全一致のコマンドを許可する書き方があります。godotと検証スクリプトだけを通す設定は、次のようになります。
{
"permissions": {
"allow": [
"Bash(godot --headless *)",
"Bash(scripts/check_shader.sh *)"
]
}
}スクリプトから非対話で回すなら、claude -pを使います。使うツールを--allowedToolsで渡す形が、ヘッドレス実行のドキュメントに例があります。
claude -p "shaders/dissolve.gdshader の fragment を書き、\
scripts/check_shader.sh で通るまで直して" \
--allowedTools "Read,Edit,Bash"Bashを丸ごと許可すると広すぎます。常用するなら、上のsettings.jsonのようにコマンド単位のルールへ絞ります。ヘッドレスのゲームエンジンで検証する構成は、Pygameをdummyドライバで検証する手順にもあり、考え方が近いものです。
描画結果はPNG連番で受け取る
構文が通っても、見た目が意図どおりとは限りません。ディゾルブの境目が逆になる、色が白っぽい、といった問題は、実際に描画しないと分かりません。
Godotには、ムービーを書き出す--write-movieがあります。拡張子を.pngにすると、PNGの連番とWAVが出ます。ファイル名は、指定したパスに8桁の連番が付く形式です。out/frame.pngならout/frame00000000.pngから始まります。相対パスは、カレントディレクトリでなくプロジェクトフォルダが基準です。
mkdir -p out
godot --path . --write-movie out/frame.png \
--fixed-fps 30 --resolution 640x360 --quit-after 10--quit-after Nは、描画するフレーム数を指定する引数です。ムービー書き出しのページは、Ctrl+Cで止めるとAVIの時間情報が壊れるので、--quit-afterかスクリプトのquit()で終えるよう案内しています。--fixed-fpsはムービー書き出しの有効時に強制され、フレームの間隔が一定になります。
このコマンドは--headlessを付けていません。ムービー書き出しのページには、ウィンドウサイズが画面解像度で頭打ちになるという説明があり、描画する環境を前提にした機能です。ヘッドレスとの組み合わせは、ページに記載がありません。GPUのないCIでは動かない場合を見込んでおき、手元かGPU付きのランナーで回します。
出力したPNGは、Claude Codeにそのまま読ませます。基準画像との差分は、ImageMagickなど外部ツールで取る例が分かりやすいでしょう。
magick compare -metric RMSE \
out/frame00000005.png baseline/dissolve_f5.png null:完全一致の判定にすると、GPUやドライバが変わっただけで失敗しえます。許容差つきの比較にして、基準画像は同じ環境で撮ったものを置きます。
Shadertoyのシェーダーを移植させる
ShadertoyやThe Book of Shadersのコードの移植も、よく頼む作業です。変換ガイドには対応表があるので、そのまま指示文に貼れます。
Shadertoyのコードの置き換え
Shadertoy
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
fragColor = vec4(uv, 0.5 + 0.5 * sin(iTime), 1.0);
}Godot
shader_type canvas_item;
void fragment() {
vec2 uv = UV;
COLOR = vec4(uv, 0.5 + 0.5 * sin(TIME), 1.0);
}置き換えの要点は3つです。
mainImageの中身をfragment()に移します。引数のfragColor/fragCoordはCOLOR/FRAGCOORDに当たり、宣言は不要ですiTimeはTIME、iResolutionは1.0 / SCREEN_PIXEL_SIZEが対応しますiMouseやiFrameのように対応する組み込み変数がないものは、uniformを自分で宣言して、スクリプトから渡します
上の例のUVは、小さなスプライトでも使えるようfragCoord / iResolution.xyの代わりに置いた形です。変換ガイドの表も、小さな四角形ではUVを使うよう案内しています。ただしY軸の向きはShadertoyと逆になります。次の落とし穴を見てください。移植後は、前節のPNGで目視確認を1回入れます。
検証を通っても残る落とし穴
落とし穴
UVのY軸がGLSLと逆になる
移植ガイドは、GodotでUVを使うとY座標が上下反転すると注記しています。FRAGCOORDはGLSLのgl_FragCoordと同じ座標系です。移植したグラデーションの向きが逆なら、まずここを疑います。
落とし穴
TIMEは3600秒でゼロに戻る
TIMEはエンジン起動からの秒数で、既定では3,600秒ごとに繰り返されます。長時間表示する演出でsin(TIME)が飛ぶ場合は、この周期が原因になりえます。周期はプロジェクト設定のrolloverで変えられます。
落とし穴
色テクスチャの白っぽさはヒント抜けが原因
sampler2Dにsource_colorが付いていないと、画像は白っぽく表示されます。構文は通るため、構文検査では気づけません。PNGの目視確認で拾う項目です。
プリプロセッサにも注意点が一つあります。#includeが使えるのは.gdshaderincだけで、深さの上限は25です。.gdshader同士で読み込もうとすると失敗します。共通部分は.gdshaderincに切り出すよう、上のCLAUDE.mdの規約に1行入れてあります。
まとめ
シェーダーの検証は、構文検査と描画確認の2段に分かれます。前段が--headlessで効くかは、壊れたシェーダーを1本置いて、ログに出る語を見て決めます。効かなければ、描画ありのPNG出力で受けます。GLSLとの差をCLAUDE.mdに書いておけば、検証の周回数そのものが減ります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/shading_langu…
- docs.godotengine.org/en/stable/tutorials/shaders/converting_glsl_to_godot_shade…
- docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/canvas_item_s…
- docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html
- docs.godotengine.org/en/stable/tutorials/animation/creating_movies.html
- code.claude.com/docs/en/headless
- code.claude.com/docs/en/permissions