Claude Media
ClaudeにGodotのシェーダーを書かせる — GLSLとの差と動作確認

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_timeGodotで通る形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. 1

    壊れたシェーダーを置く

    res://shaders/_canary.gdshaderに、言語リファレンスが「invalid」と示すfloatへの整数代入を入れます。

  2. 2

    読み込むだけのスクリプトを書く

    SceneTreeを継承したスクリプトで、シェーダーをColorRectに貼り、1フレーム回して終了します。

  3. 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.log
切り分け

canary.logの読み方

  1. 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のコードの置き換え

BEFORE

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);
}
AFTER

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に書いておけば、検証の周回数そのものが減ります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/shading_langu…
  2. docs.godotengine.org/en/stable/tutorials/shaders/converting_glsl_to_godot_shade…
  3. docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/canvas_item_s…
  4. docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html
  5. docs.godotengine.org/en/stable/tutorials/animation/creating_movies.html
  6. code.claude.com/docs/en/headless
  7. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn