【Godot】Fragment Shaderの基礎 - ピクセル単位で色とエフェクトを操る

作成: 2025-12-08最終更新: 2026-07-08

GodotのFragment Shaderの基本を、CPUとGPUの違いから丁寧に解説。COLOR・UV・TIMEを使った色操作と動的エフェクト、被弾フラッシュの実装、if文を避けるパフォーマンス最適化まで、具体的なコードと図で学びます。

2Dゲームを作っていて、「敵を倒したときに一瞬だけ白く光らせたい」「水面をゆらゆら流したい」「敵をじわっと溶けるように消したい」——そんな表現を作ろうとして、modulate やアニメーションだけでは手が届かず悩んだことはないでしょうか。

こうした「ピクセル単位のこだわった見た目」を実現するのが シェーダー (Shader) です。シェーダーは GPU(グラフィックス処理ユニット)の上で動く小さなプログラム で、画面に映る1ピクセル1ピクセルの色を、あなたのコードで直接決められます。この記事では、その中でも色を担当する Fragment Shader(フラグメントシェーダー) に絞って、「最初の一歩」を踏み出せるところまで解説します。

細かいピクセルのグリッドでできた画面パネルを、左から右へソフトブルーの光の波が走り、ピクセルが次々と染まっていくシェーダーのイメージ

この記事でわかること

  • シェーダーとは何か ——なぜGPUだと重い見た目でも軽く動くのか
  • Godotシェーダーの 基本構造shader_typevertex / fragment / light
  • fragment 関数で COLORUVTEXTURE を使い、色を操作する
  • UVTIME で作る 動的エフェクト(円形マスク・スクロール・ディゾルブ)
  • if 文を避けるなど、 パフォーマンスの勘どころ

Sponsored

シェーダーとは? CPUとGPUの絵の描き方の違い

シェーダーを理解する第一歩は、 「絵を描いているのは誰か」 を知ることです。ゲームの画面は、毎フレーム何十万個ものピクセルに色を塗って作られています。この塗り作業を担当するのが GPU です。

普段GDScriptで書くゲームのロジック(移動・当たり判定・スコア計算など)は CPU が処理します。CPUは1つの仕事を順番にこなすのが得意です。一方GPUは、 たくさんの単純な作業を一斉に片付ける のが得意——だからこそ、何十万ピクセルもの塗り作業を一瞬で終わらせられます。

CPUは1体の作業員がピクセルを1つずつ順番に塗るのに対し、GPUは多数の作業員が全ピクセルを同時に�塗る様子を対比した図

シェーダーとは、この 「GPUに、ピクセルの色の塗り方を教える小さなプログラム」 のことです。GPUの並列処理に乗るので、炎・水・歪み・画面全体のトランジションといった重たそうな表現も、ゲーム本体のロジックを邪魔せず軽快に動きます。

modulateとの違い ——「一律」か「ピクセルごと」か

「色を変えるだけなら、modulate プロパティでもできるのでは?」と思うかもしれません。ここが重要な分かれ目です。

  • modulate :オブジェクト 全体を一律に 同じ色で染める。「キャラ全体を赤くする」のようなシンプルな用途向き。
  • シェーダー :ピクセル 1つ1つに別々の計算 ができる。「縁だけ光らせる」「上半分だけ透明にする」「中心から放射状にグラデをかける」など、 位置によって効果が変わる 表現が作れる。

つまり「一律の色変え」なら modulate、「位置ごとに変わる凝った表現」ならシェーダー、という住み分けです。単純な点滅だけなら modulateによるホワイトフラッシュ の方が手軽なこともあります。

Sponsored

Godotシェーダーの基本構造

Godotのシェーダーは、GLSL ES 3.0に似た独自の言語で書きます。シェーダーは .gdshader ファイル(またはノードの ShaderMaterial 内)に記述し、まず 「何に適用するか」 を1行目で宣言します。

shader_type canvas_item; // 2D(スプライトやUIなどCanvasItem系)に適用
// shader_type spatial;  // 3Dメッシュに適用する場合はこちら

2Dのエフェクトを作るときは canvas_item を指定します。そのうえで、シェーダーは主に次の3つの 関数(エントリーポイント) で構成されます。

関数名役割実行タイミング
vertex頂点(位置)を操作する。変形・波打ち・回転など頂点ごとに1回
fragmentピクセル(フラグメント)の色を計算する。 色調整・エフェクトの核ピクセルごとに1回
lightオブジェクトに当たる光の影響を計算する。カスタムライティング光源×ピクセルごとに1回
vertex(頂点を動かす)・fragment(ピクセルの色を決める・主役)・light(光の影響を計算)の3つの関数の役割を横並びで示した図。fragmentカードだけ強調されている

この記事の主役は fragment 関数 です。画面に映る すべてのピクセル に対して1回ずつ実行され、そのピクセルの最終的な色を決めます。まずはここだけ押さえれば、色変更やエフェクトの大半が作れます。

Sponsored

fragment関数で色を決める - COLORとUV

fragment 関数の仕事は、 「今、塗ろうとしているこのピクセルを何色にするか」 を決めることです。そのために2つの重要な組み込み変数を使います。

  • COLOR :出力。ここに vec4(R, G, B, A) 形式で色を代入すると、そのピクセルがその色になる(各成分は 0.01.0
  • UV :入力。 今処理しているピクセルが、テクスチャ上のどの位置か0.01.0 の座標で表す

UV は、テクスチャの左上を (0, 0)、右下を (1, 1) とする座標です。ピクセルごとに違う UV が渡されるので、「左半分だけ」「中心からの距離に応じて」といった 位置に応じた処理 の基準になります。

UV座標の説明図。スプライトの四隅が(0,0)(1,0)(0,1)(1,1)、中心が(0.5,0.5)で、Uは右方向、Vは下方向に0〜1で増える。画面上の位置を0〜1で表す座標

コード例1:単色で塗りつぶす

もっともシンプルな、全ピクセルを同じ色にするシェーダーです。

shader_type canvas_item;

void fragment() {
    // COLORに色を直接代入。vec4(R, G, B, A)、各成分は0.0〜1.0
    COLOR = vec4(0.8, 0.2, 0.3, 1.0); // 不透明のワインレッド
}

コード例2:テクスチャの色を反転する

元の絵を活かして加工してみましょう。texture() 関数で、今のピクセルの元の色を取り出せます。

shader_type canvas_item;

void fragment() {
    vec4 tex = texture(TEXTURE, UV); // このピクセルの元の色を取得
    vec3 inverted = vec3(1.0) - tex.rgb; // RGBを反転(1.0から引く)
    COLOR = vec4(inverted, tex.a); // アルファ(透明度)は元のまま
}

texture(TEXTURE, UV) が「このスプライトの、この位置の色を読む」の基本形です。透明な部分(tex.a0)まで色を変えてしまわないよう、 アルファは元の値を引き継ぐ のがポイントです。

コード例3:セピア調フィルター

もう少し実践的に、写真をセピア調にするフィルターです。まず明るさ(輝度)を求め、そこにセピアの色味を掛けます。

shader_type canvas_item;

void fragment() {
    vec4 tex = texture(TEXTURE, UV);
    float gray = dot(tex.rgb, vec3(0.299, 0.587, 0.114)); // 輝度に変換
    vec3 sepia = vec3(gray * 1.07, gray * 0.74, gray * 0.43); // セピアの色味
    COLOR = vec4(sepia, tex.a);
}

同じ「色を読んで加工して書き戻す」流れで、グレースケール化・色調補正・2値化など、画面のトーンを変えるフィルターが一通り作れます。

Sponsored

UVとTIMEで動かす - 動的エフェクト

Fragment Shaderの真価は、UV に加えて TIME(起動からの経過秒数) を使い、静止画を 動くエフェクト に変えられる点にあります。定番の3パターンを見てみましょう。

UVとTIMEで作る動的エフェクトの実例。円形マスク(スポットライト状)、スクロール(縞模様が流れる)、ディゾルブ(粒状に溶けて消える)の3つを並べた図

コード例4:円形マスク(スポットライト)

UV から中心までの距離を測り、外側を透明にすると、スポットライトのような円形マスクになります。

shader_type canvas_item;

void fragment() {
    vec2 centered = UV - vec2(0.5);      // 中心を(0,0)に補正
    float dist = length(centered);       // 中心からの距離
    // 半径0.3〜0.4の間でなめらかに透明へ(境界をぼかす)
    float mask = 1.0 - smoothstep(0.3, 0.4, dist);
    vec4 tex = texture(TEXTURE, UV);
    COLOR = vec4(tex.rgb, tex.a * mask);  // 元のアルファにマスクを掛ける
}

smoothstep() は、境界をカクッとさせず なめらかに 切り替える関数です。後述しますが、if で「距離が0.4以上なら透明」と書くよりGPUに優しい書き方でもあります。

コード例5:時間でスクロールする

UVTIME を足すと、テクスチャが自動でスクロールします。流れる水面・雲・ベルトコンベアなどに使えます。

shader_type canvas_item;

uniform float scroll_speed = 0.1; // GDScriptから速度を渡せる

void fragment() {
    vec2 uv = UV + vec2(TIME * scroll_speed, 0.0); // X方向へずらす
    uv = fract(uv);                    // 0.0〜1.0にループさせる(無限スクロール)
    COLOR = texture(TEXTURE, uv);
}

fract()(小数部を取り出す関数)でUVを 0.01.0 に折り返すことで、テクスチャが途切れず無限に流れ続けます。

コード例6:ノイズで溶けるディゾルブ

ノイズテクスチャを使うと、キャラが 有機的に溶けて消える ディゾルブが作れます。撃破演出やワープに定番です。

shader_type canvas_item;

uniform sampler2D noise_texture;                       // ノイズ画像を外部から渡す
uniform float dissolve : hint_range(0.0, 1.0) = 0.0;   // 進行度(0=通常, 1=消滅)

void fragment() {
    float noise = texture(noise_texture, UV).r; // ノイズの濃さ(0〜1)
    if (noise < dissolve) {
        discard;                                // このピクセルを描かない(消す)
    }
    COLOR = texture(TEXTURE, UV);
}

GDScriptから dissolve0.01.0Tween で変化させれば、ノイズの薄いところから順に穴が空いて、じわっと溶けるアニメーションになります。

Sponsored

実践:被弾で一瞬白く光る「ヒットフラッシュ」を作る

ここまでの「色を読んで加工する」を、実際のゲームで最もよく使う演出に落とし込んでみましょう。 敵が攻撃を受けた瞬間に一瞬だけ真っ白に光る「ヒットフラッシュ」 です。

シューティングの敵編隊、アクションRPGのボス、ベルトスクロールの雑魚——ジャンルを問わず「当たった感触」を出す定番演出で、しかも 1つのシェーダーを全キャラで使い回せる のが嬉しいところです。

敵キャラが被弾した瞬間の3コマ。通常(flash=0.0)→真っ白に発光(flash=1.0)→通常に戻る(flash=0.0)。同じシェーダーを敵・ボス・雑魚に使い回せる

シェーダー:元の色を白へ寄せる

やることはシンプルです。flash_amount(白くする度合い)を外から受け取り、その分だけ元の色を白へ寄せます。ここで if を使わず mix()(2つの値を割合で混ぜる関数)を使うのがポイントです。

shader_type canvas_item;

// GDScriptから0.0〜1.0で渡す「白くする度合い」
uniform float flash_amount : hint_range(0.0, 1.0) = 0.0;

void fragment() {
    vec4 tex = texture(TEXTURE, UV);              // 元の色
    // flash_amountの割合だけ、元の色を白(1,1,1)へ寄せる
    vec3 rgb = mix(tex.rgb, vec3(1.0), flash_amount);
    COLOR = vec4(rgb, tex.a);                     // アルファは元のまま=形は保つ
}

flash_amount0.0 なら元の見た目、1.0 なら真っ白なシルエット。 アルファを元のまま残す ので、キャラの形(透明な余白)は崩れず、色だけが白く飛びます。

GDScript:当たった瞬間に光らせて戻す

あとは被弾時に flash_amount1.0 にして、Tween で素早く 0.0 へ戻すだけです。

extends Sprite2D  # AnimatedSprite2Dでも同じ

func flash() -> void:
    var mat := material as ShaderMaterial
    mat.set_shader_parameter("flash_amount", 1.0)   # 一瞬で真っ白に
    var tween := create_tween()
    # 0.15秒かけてじわっと元へ(ここが「ピカッ」の余韻になる)
    tween.tween_property(mat, "shader_parameter/flash_amount", 0.0, 0.15)

func take_damage(amount: int) -> void:
    hp -= amount
    flash()          # ダメージ処理のついでに光らせる

被弾を検知する側(当たり判定の シグナルtake_damage() の呼び出し)から flash() を呼べば完成です。

ポイントは2つです。

  • シェーダーは1つ、キャラは何体でもよい :同じ ShaderMaterial を敵・ボス・雑魚のスプライトに割り当てれば、全員が同じヒットフラッシュで光ります。個別の見た目(元のテクスチャ)は関係ありません。
  • 演出のタイミングはGDScript側で決める :シェーダーは「今どのくらい白いか」を描くだけ。「いつ・どのくらいの速さで戻すか」は Tween の秒数で調整します。0.15 を短くすればシャープに、長くすればふわっと光ります。

「白フラッシュだけなら modulate でも近いことはできます。ですが、 縁だけを光らせる部分的にグラデで発光させる といった一歩進んだ表現に育てたくなったら、それはシェーダーの独壇場です。

Sponsored

パフォーマンスと最適化

シェーダーは強力ですが、fragment 関数は ピクセルごとに実行される ため、わずかな無駄が画面全体で何十万倍にも膨らみます。とくに効くのが次の3点です。

if文を使うと一部のスレッドが別の道を通り残りが待たされるのに対し、step/mixなら全スレッドがそろって進み待ちが出ない様子を対比した図
  • if 文を避ける :GPUは多数のピクセルを足並みをそろえて処理します。if で分岐すると、一部のスレッドだけ別の道を通り、他が待たされます(スレッド分岐)。step()smoothstep()mix()計算に置き換える のが定石です。
  • 重い計算は vertex :ピクセルごとに変わらない計算(TIME を使った sin/cos など)は vertex 関数で1回だけ計算し、varying 変数で fragment に渡すと計算量が激減します。
  • texture() の呼び出しを減らす :テクスチャの読み取りは比較的重い処理です。同じテクスチャを複数回読むなら、結果を変数に保存して使い回しましょう。

「まず動くものを作り、重かったら if を潰す」くらいの温度感で十分です。最初から最適化に神経質になる必要はありません。

Sponsored

よくある間違いとベストプラクティス

Fragment Shaderで陥りがちなミスと、その対処をまとめました。

よくある間違いベストプラクティス
if 文の多用step()smoothstep()mix() で条件を算術演算に置き換える
色を変えたら形まで変わるCOLORアルファ(.a)は元のテクスチャの値を引き継ぐ。透明な余白まで塗らない
UV が必ず0〜1だと思い込むタイル配置などでは範囲外になる。fract()mod() で正規化する習慣をつける
discard の多用便利だが一部GPUで深度最適化を妨げる。アルファを0にする方法も検討する
GDScriptで済むことをシェーダーでオブジェクト全体の一律な色変更は modulate の方がシンプルで軽い

おまけ:先に知っておくと良いこと

基本の fragment が書けるようになったら、次はこのあたりを知っておくと表現の幅が一気に広がります。今すぐ全部覚える必要はありません。

  • uniform でGDScriptから値を渡すuniform float flash_amount; のように宣言した変数は、set_shader_parameter("flash_amount", 値) でスクリプトから制御できます。ヒットフラッシュやディゾルブの「進行度」は、すべてこの仕組みです。
  • Visual Shader(ノードでシェーダーを組む) :コードを書かず、ノードをつないで視覚的にシェーダーを作れる機能もあります。数式の見通しを掴むのに便利です。
  • 画面全体のエフェクトhint_screen_texture で描画済みの画面を読み込むと、ブラー・色収差・画面遷移など ポストプロセス が作れます。特定範囲だけに適用したいときは SubViewport と組み合わせます。
  • 粒や光と重ねる :シェーダー単体でなく、GPUParticles2D の粒や 2Dライティング の光と重ねると、エフェクトの説得力がさらに増します。

まとめ

  • シェーダー はGPU上で動く小さなプログラム。ピクセル単位で色を決め、modulate では届かない「位置ごとに変わる表現」を作れる
  • fragment 関数 が主役。COLOR に出力、texture(TEXTURE, UV) で元の色を読み、UV で位置を、TIME で時間を扱う
  • 動的エフェクトUVTIME の組み合わせ。マスク・スクロール・ディゾルブが定番
  • 最適化if を避けて step/mix に置き換えるのが第一歩

まずは手持ちのスプライトに ShaderMaterial を割り当て、コード例2の「色反転」から試してみてください。COLOR に代入する値をいじるだけで、画面の色がリアルタイムに変わる感覚が掴めるはずです。そこにヒットフラッシュを足せば、あなたのゲームの手触りが一段と良くなります。

さらに学ぶために