導入:なぜシェーダーを学ぶべきか
Godot Engineでゲーム開発を進める上で、 シェーダー は避けて通れない強力なツールです。シェーダーとは、グラフィックス処理ユニット(GPU)上で実行される小さなプログラムのことで、オブジェクトの見た目、色、光の反射、そして画面全体のエフェクトをピクセル単位で制御します。
なぜシェーダーが重要なのでしょうか?それは、 視覚的な魅力を飛躍的に向上させる と同時に、 高いパフォーマンス を実現できるからです。CPUで複雑な計算を行う代わりに、GPUの並列処理能力を最大限に活用することで、炎、水、カスタムライティング、ユニークな画面トランジションなど、GDScriptだけでは実現が難しい表現を軽快に実現できます。
本記事では、シェーダーの中でも特に視覚的な表現を担う Fragment Shader(フラグメントシェーダー) に焦点を当て、その基礎知識から、色変更やエフェクトを作成する最初の一歩を、具体的なコード例とともに解説します。
Godotシェーダーの基本構造
Godot Engineのシェーダーは、GLSL ES 3.0に似た独 自のシェーディング言語を使用します。シェーダーファイル(.gdshader)を作成する際、まずそのシェーダーが何に適用されるかを定義する必要があります。
shader_type canvas_item; // 2Dオブジェクトに適用
// shader_type spatial; // 3Dオブジェクトに適用
canvas_itemは2DスプライトやUI要素に、spatialは3Dメッシュに適用されます。
Godotシェーダーは、主に以下の3つの関数(エントリーポイント)で構成されています。
| 関数名 | 役割 | 実行タイミング |
|---|---|---|
vertex | オブジェクトの頂点(位置)を操作します。変形、波打ち、回転などに使用されます。 | 頂点ごとに1回 |
fragment | ピクセル(フラグメント)の色を計算します。 テクスチャのサンプリング、色調整、エフェクトの核となります。 | ピクセルごとに1回 |
light | オブジェクトに当たる光の影響を計算します。カスタムライティングの実現に使用されます。 | 光源とピクセルの組み合わせごとに1回 |
本記事の主役であるfragment関数は、画面上の すべてのピクセル に対して実行され、そのピクセルの最終的な色を決定する役割を担います。
実践1: Fragment Shaderによる基本的な色操作
Fragment Shaderの最も基本的な役割は、ピクセルの色を決定することです。ここでは、COLOR出力変数とtexture()関数を使った基本的な色の操作方法を学びます。
コード例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() {
// 1. 現在のUV座標におけるテクスチャの色を取得します。
vec4 original_color = texture(TEXTURE, UV);
// 2. RGB成分を1.0から引くことで色を反転させます。
vec3 inverted_rgb = vec3(1.0) - original_color.rgb;
// 3. 最終的な出力色として、反転したRGBと元のアルファ値を設定します。
COLOR = vec4(inverted_rgb, original_color.a);
}
コード例3:セピア調フィルター
より実践的な例として、画像に古典的なセピア調のフィルターをかけてみましょう。
shader_type canvas_item;
void fragment() {
vec4 original_color = texture(TEXTURE, UV);
vec3 c = original_color.rgb;
// グレースケールに変換(輝度計算)
float gray = dot(c, vec3(0.299, 0.587, 0.114));
// セピア色のトーンを適用
vec3 sepia_color = vec3(
gray * 1.07, // 赤みを強く
gray * 0.74, // 緑を抑えめに
gray * 0.43 // 青をさらに抑える
);
COLOR = vec4(sepia_color, original_color.a);
}
実践2: UV座標と時間を使った動的エフェクト
Fragment Shaderの真価は、UV(テクスチャ座標)やTIME(時間)といった組み込み変数を使って、静的な画像を動的なエフェクトに昇華させられる点にあります。
コード例4:UV座標を使った円形マスク
UV座標を加工して、スポットライトのような円形マスクを作成します。
shader_type canvas_item;
void fragment() {
// 1. UV座標の中心を(0,0)に補正します。
vec2 centered_uv = UV - vec2(0.5);
// 2. 中心からの距離を計算します。
float dist = length(centered_uv);
// 3. smoothstepを使い、境界線を滑らかに描画します。
float mask = 1.0 - smoothstep(0.3, 0.4, dist);
vec4 original_color = texture(TEXTURE, UV);
// 4. 元のアルファ値にマスクを乗算して適用します。
COLOR = vec4(original_color.rgb, original_color.a * mask);
}
コード例5:時間を使ったスクロールアニメーション
TIME変数をUV座標に加算することで、テクスチャを自動でスクロールさせることができます。流れる水面や雲の表現に不可欠です。
shader_type canvas_item;
uniform float scroll_speed = 0.1;
void fragment() {
// 1. UV座標のx成分に時間を加算して、UVをずらします。
vec2 scrolled_uv = UV + vec2(TIME * scroll_speed, 0.0);
// 2. fract()関数でUV座標を0.0-1.0の範囲にループさせます。
scrolled_uv = fract(scrolled_uv);
// 3. 新しいUV座標でテクスチャをサンプリングします。
COLOR = texture(TEXTURE, scrolled_uv);
}
fract()関数を使うことで、テクスチャが無限にスクロールし続けるようになります。
コード例6:ノイズを使ったディゾルブエフェクト
ノイズテクスチャを利用すると、より有機的で複雑なエフェクトが作成可能です。
shader_type canvas_item;
// ノイズテクスチャを外部から設定
uniform sampler2D noise_texture;
// ディゾルブの進行度をGDScriptから制御
uniform float dissolve_threshold : hint_range(0.0, 1.0) = 0.5;
void fragment() {
// ノイズテクスチャから値を取得
float noise_value = texture(noise_texture, UV).r;
// ノイズの値がしきい値より低い場合、ピクセルを破棄(透明に)する
if (noise_value < dissolve_threshold) {
discard; // discardはピクセルを描画しない命令
}
COLOR = texture(TEXTURE, UV);
}
dissolve_thresholdをGDScriptから0.0から1.0まで変化させることで、滑らかなディゾルブアニメーションが実現できます。
パフォーマンスと最適化
シェーダーは強力ですが、無計画な実装はパフォーマンスのボトルネックになり得ます。特にFragment Shaderはピクセル単位で実行されるため、わずかな非効率が大きな負荷につながります。
if文を避ける: GPUの並列処理の性質上、if文による分岐はコストが高い処理です。可能な限りstep(),smoothstep(),mix()といった分岐を伴わない関数でロジックを代替しましょう。- 重い計算はVertex Shaderで: ピクセルごとに変化しない計算(例:
TIMEを使ったsin/cosの計算など)は、Vertex Shaderで行い、その結果をvarying変数でFragment Shaderに渡すことで、計算量を大幅に削減できます。 - テクスチャサンプリングを減らす:
texture()関数の呼び出しは比較的重い処理です。同じテクスチャを複数回サンプリングする場合は、結果を変数に保存して再利用しましょう。
よくある間違いとベストプラクティス
Fragment Shaderを扱う上で陥りがちなミスと、それを避けるためのベストプラクティスをまとめました。
| よくある間違い | ベストプラクティス |
|---|---|
if文の多用 | step(), smoothstep(), mix()関数を活用して、条件分岐を算術演算に置き換える。 |
| UV座標のハードコーディング | UVは常にvec2(0.0, 1.0)の範囲にあるとは限りません。fract()やmod()を使ってUVを正規化する習慣をつける。 |
discardの過度な使用 | discardは便利な一方、一部のGPUでは深度テストの最適化を阻害することがあります。アルファ値を0にする方法も検討する。 |
| テクスチャの過剰なサンプリング | 同じテクスチャを何度もtexture()でサンプリングすると負荷が増えます。結果を変数に保存して再利用する。 |
| GDScriptでできることをシェーダーでやる | シェーダーはピクセル単位の操作に特化しています。オブジェクト全体の単純な色変更などは、Sprite2Dのmodulateプロパティを使った方がシンプルで高速な場合があります。 |
まとめ:Fragment Shaderの次なる一歩
本記事では、Godot EngineにおけるFragment Shaderの基本的な役割、構造、そして色変更や動的なエフェクトを実現する方法を学びました。
Fragment Shaderは、ピクセルごとの色を計算する強力なツールであり、 UV 座標や TIME といった組み込み変数を活用することで、無限の視覚エフェクトを生み出すことができます。
本記事の要点:
- シェーダーはGPUで実行され、高いパフォーマンスで視覚表現を制御します。
fragment関数はピクセルごとの色を決定し、COLOR変数に出力します。UV座標はテクスチャ上の位置を示し、グラデーションや座標ベースのエフェクトに不可欠です。TIMEユニフォームを使うことで、効率的な時間ベースのアニメーションが可能です。
この基礎をマスターしたら、次は外部から値を渡す ユニフォーム(Uniforms) を学び、GDScriptからシェーダーのパラメーターを制御する方法や、 vertex シェーダーを使ったオブジェクトの変形に挑戦してみましょう。