search
LoginSignup
3
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

GameMaker Studio Advent Calendar 2020 Day 6

posted at

updated at

【GameMaker Studio2】シェーダーの基本的な使い方

この記事では、シェーダーの基本的な使い方について解説します。
複雑な計算式で書かれているシェーダーを理解するのは大変ですが、GameMakerでシェーダーの機能を呼び出して使うぶんにはあまり難しくありません。

ということで、この記事を参考に自作ゲームにシェーダーを組み込む手助けとなればなによりです。

単色化シェーダーを作成する

まずはシェーダーに慣れるための練習ということで、画像を単色化するシェーダーを作ってみます。
名称未設定.png

厳密には単色化というよりも色の置き換えかもしれませんが、イメージとしてはこのとおりです。
特定の色を抽出するだけであれば、ブレンドカラーを指定するだけでできるのですが、この例のように色を置き換えるには、GameMakerの場合はシェーダーを使う必要があります。

ということで作成手順です。

  1. プロジェクトを新規作成(TestShaderなど)
  2. オブジェクト(obj_Test)とスプライト(spr_Test)を追加
  3. 作成したオブジェクト(obj_Test)をルームに配置

obj_TestにはひとまずDrawイベントに以下のコードを書いておきます。

obj_TestのDrawイベント
// spr_Testを描画
draw_sprite(spr_Test, 0, 0, 0);

準備ができたので、シェーダーを作成します。
TestShader_-_GameMaker_Studio_2.png

Asset Browserから、「Shader」フォルダを右クリックして、Create > Shader を選択して、シェーダーを作成します。

作成したシェーダーは、単色化ということで「sh_MonoColor」に名前を変更しておきます。
TestShader_-_GameMaker_Studio_2.png

TestShader_-_GameMaker_Studio_2.png
TestShader_-_GameMaker_Studio_2.png

シェーダーを作成すると、「sh_MonoColor.vsh」「sh_MonoColor.fsh」と2つのファイルが作成されます。

  • sh_MonoColor.vsh:頂点シェーダー(バーテックスシェーダー)
  • sh_MonoColor.fsh:フラグメントシェーダー(ピクセルシェーダー)

*.vsh という拡張子は「[V]ertex [Sh]ader」の頭文字を取っています。
*.fsh という拡張子は「[F]ragment [Sh]ader」の頭文字です。

簡単に説明すると、頂点シェーダーは「頂点情報(位置)」を書き換え、フラグメントシェーダーは「ピクセルの色」を書き換えます。

これから実装する単色化は色の変更なので、フラグメントシェーダー(sh_MonoColor.fsh)を書き換えることになります。

ということで、まずは sh_MonoColor.fsh の説明です

//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord; // テクスチャの座標
varying vec4 v_vColour;   // ブレンドカラー

void main()
{
  // テクスチャを指定のブレンドカラーで合成する
  gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
}

gl_FragColor というのが最終的な色となります。texture2D( gm_BaseTexture, v_vTexcoord ) という記述で、テクスチャの指定の座標の色を取得し、ブレンドカラー v_vColour で合成している処理となります。

これを単色化するには以下のように修正します。

//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord; // テクスチャの座標.
varying vec4 v_vColour; // ブレンドカラー.

void main()
{
  // テクスチャの色を取得.
  vec4 colour = texture2D( gm_BaseTexture, v_vTexcoord );

  // 頂点カラーで RGBを置き換える.
  colour.rgb = v_vColour.rgb;

  // アルファ値のみ合成する.
  colour.a *= v_vColour.a;

  // 計算結果をフラグメントカラーに設定する.
  gl_FragColor = colour;
}

obj_Testに戻って、Createイベントを作成します。

obj_TestのCreateイベント
timer = 0;

Drawイベントで単色化のシェーダーを適用します。

obj_TestのDrawイベント
// タイマー更新.
timer++;

// 元の画像を描画.
draw_sprite(spr_Test, 0, 0, 0);

// 加算ブレンドに変更
gpu_set_blendmode(bm_add);

// 単色化シェーダーを適用
shader_set(sh_MonoColor);

// sinカーブでアルファ値を 0.0〜1.0 に揺らす
var alpha = 0.5 + 0.5 * dsin(timer * 4);

// 単色化で描画
draw_sprite_ext(spr_Test, 0, 0, 0, 1, 1, 0, c_white, alpha);

// シェーダーをリセット
shader_reset();

// 通常のブレンドモードに戻す
gpu_set_blendmode(bm_normal);

実行すると以下のように白点滅するようになります。

flash.gif

ちなみにブレンドカラー (v_vColour) は draw_sprite_ext() の値を使っているので、以下のように書き換えると赤点滅に変わります。

// 赤色 (c_red) で単色化で描画
draw_sprite_ext(spr_Test, 0, 0, 0, 1, 1, 0, c_red, alpha);

flash.gif

Uniform を使ってぼかしを実装する

次にシェーダーにブレンドカラー以外の値を渡すサンプルとして、ぼかしを実装してみます。
シェーダーコードは以下のページを参考にしました

この方のサイトには、シェーダーのサンプルがたくさん用意されて、とても参考になります。
http://prester.org/static/obake/categories/shader/

では新たにシェーダーを作成して名前を「sh_Blur」にします。
今回もフラグメントシェーダーのみの修正なので、「sh_Blur.fsh」を開いて以下のように修正します。

sh_Blur.fsh
//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec2  resolution; // Uniform: 画面解像度
uniform float blur_amount; // Uniform: ぼかしの強さ

void main()
{
   // 解像度とぼかしの強さからぼかしのサイズを決める
   float blurSize = 1.0/resolution.y * blur_amount;

   // 周囲の色を合成する
   vec4 sum = vec4(0.0);
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y - 4.0*blurSize)) * 0.05;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y - 3.0*blurSize)) * 0.09;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y - 2.0*blurSize)) * 0.12;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y - blurSize)) * 0.15;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y)) * 0.16;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y + blurSize)) * 0.15;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y + 2.0*blurSize)) * 0.12;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y + 3.0*blurSize)) * 0.09;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y + 4.0*blurSize)) * 0.05;

   // 最終的な色を決定
   gl_FragColor = sum;
}

まず最初に注目するのは以下の2行です。

uniform vec2  resolution; // Uniform: 画面解像度
uniform float blur_amount; // Uniform: ぼかしの強さ

uniform というキーワードを指定するとシェーダーの外部から指定した変数を使用することができます。(function() の引数のようなものです)
ここでは「画面解像度」と「ぼかしの強さ」を受け取って、それをもとにガウスの処理をします。

そして「周囲の色を合成する」という部分ですが、ここでは上下の色のみを一定の割合で受け取って(0.05+0.09+0.12+0.15+0.16+0.15+0.12+0.09+0.05=0.98なので合計が1.0に近い値になるように合成している)最終的なピクセルの値を決定しています。

では、オブジェクト側の処理です。
まずはCreateイベントです。

Createイベント
// ぼかしの値を設定
blur_amount = 0;

次にDrawイベントを以下のように修正します。

Drawイベント
if(keyboard_check(vk_up)) {
    blur_amount += 0.01; // 上キーで値を増やす
}
if(keyboard_check(vk_down)) {
    blur_amount -= 0.01; // 下キーで値を減らす
}
// 現在の値を表示
draw_text(8, 8, "blur_amount: " + string(blur_amount));

// ぼかしシェーダーを適用
shader_set(sh_Blur);

// ぼかす値の uniform を取得
var blur = shader_get_uniform(sh_Blur, "blur_amount");
// ぼかす値を設定
shader_set_uniform_f(blur, blur_amount);

// 画面解像度の uniform を取得
var resolution = shader_get_uniform(sh_Blur, "resolution");
// 画面解像度を設定
shader_set_uniform_f(resolution, display_get_gui_width(), display_get_gui_height());

// 描画
draw_sprite(spr_Test, 0, 0, 0);

// シェーダーをリセット
shader_reset();

shader_get_uniform() を使うと、シェーダーの値を変更するハンドルを取得できます(値ではありません)。そしてそのハンドルを shader_set_uniform_f() で使用すると、シェーダーに値を渡すことができます。

実行すると、上下キーでぼかしの強さが変更できます。
blur.gif
(※ぼかしを強くするとホラーっぽい絵になりますね……)

シェーダーのコンパイルエラーをチェックする

シェーダーの記述にミスがあった場合、shader_set() で設定しても何も実行されません。シェーダーにミスがあるかどうかをチェックするには shader_is_compiled() を使用します。

// "sh_Blur" が正常にコンパイルできているかどうかをチェックする
if(shader_is_compiled(sh_Blur) == false) {
  show_message("sh_Blur がコンパイルエラー");
}

コンパイルは最初だけ行われるので、ゲーム開始時に呼び出せば問題ありません。
もしシェーダーの記述にエラーがある場合は以下のように表示されます。

Created_with_GameMaker_Studio_2_と_Created_with_GameMaker_Studio_2.png

まとめ

以上がGameMakerでシェーダーを使う際の基本となります。
簡単にまとめると、

  • シェーダーには「頂点シェーダー」「フラグメントシェーダー」の2つがある
  • 「頂点シェーダー(.vsh)」は位置情報、「フラグメントシェーダー(.fsh)」は色情報を変更する
  • シェーダーにパラメータを渡すには Uniform を使う
    • shader_get_uniform() でハンドルを取得し、shader_set_uniform_f() で値を設定する
  • シェーダーのエラーチェックは shader_is_compiled() で行う

です。

今回作成したプロジェクトは以下からダウンロードできます。

参考になりそうなサイトやアセット

例えば、"Free Shaders" の Wave サンプルでは以下のシェーダーが確認できます。
wave.gif

"bktGlitch" のグリッチノイズもなかなかよき。
glitch.gif

"Shockwave"はまだ試せていないけど、良さげな衝撃波エフェクトが作れそう。

Shader Tutorial for GameMaker 26b - Shockwave Distortionは、かなりいい感じの衝撃波・歪みエフェクトの実装方法を紹介している動画

shockwave.gif

アクションゲームにこの手の演出があるとクオリティが高く見えるので、時間のあるときに挑戦してみたいところ……

おまけ:2値化シェーダー

2値化を試しに作ってみたところ良い感じのものができた気がするので、ついでに実装方法とサンプルをのせておきます。

念のため、2値化についての説明です。

2値化とは、画像を白と黒の2階調に変換する処理のことである。
2値化では、あらかじめ閾値を決めておき、画素の値が閾値より大きければ白、小さければ黒に変換する。

2値化は、ぼやけている画像を鮮明にできるという特徴がある。一方、閾値の設定や画像の明るさによって、画像が黒一色、または、白一色になってしまう場合もある。
一般的に2値化は、1つの閾値を設定することが多いが、2つの閾値を設定してその間の値で変換する方法もある。

(引用元:2値化とは何? Weblio辞書

2値化をリアルタイム処理すると以下のようになります。

threthold.gif

2値化は通常「白」と「黒」で表現しますが、この例では「赤」と「黒」にしています。

シェーダーコード (フラグメント・シェーダー) は以下のとおりです。

sh_threthold.fsh
//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec3  uColorHigh; // しきい値より大きい場合の色.
uniform vec3  uColorLow;  // しきい値より小さい場合の色.
uniform float uThrethold; // しきい値 (0.f〜1.f)
uniform float uRatio;     // 2値化の色のブレンド率 (0.f〜1.f)

void main()
{
  // 元のテクスチャの色を取得
  vec4 color = texture2D( gm_BaseTexture, v_vTexcoord );

  // RGBの合計を求める (0.f〜3.f)
  float sum = color.r + color.g + color.b;
  if(sum > 3 * uThrethold) {
    // しきい値を超えた場合は uColorHighとブレンドする
    color.rgb = mix(color.rgb, uColorHigh, uRatio);
  }
  else {
    // しきい値より低い場合は uColorLowとブレンドする
    color.rgb = mix(color.rgb, uColorLow, uRatio);
  }

  // 頂点カラーと合成
  gl_FragColor = v_vColour * color;
}

Uniform を4つ渡すようにしました。

オブジェクト側の処理は以下のとおりです。

obj_testのCreateイベント
// タイマー初期化
timer = 0;

if(shader_is_compiled(sh_threthold) == false) {
  // シェーダーエラーチェック
  show_message("shader error");
}
obj_testのDrawイベント
// タイマー更新
timer++;

// シェーダーを設定.
shader_set(sh_threthold);

// 割合を計算 (0.f〜1.f)
var rate = abs(dsin(timer*0.5));

// 各シェーダーハンドラを取得.
var uColorHigh = shader_get_uniform(sh_threthold, "uColorHigh");
var uColorLow  = shader_get_uniform(sh_threthold, "uColorLow");
var uThrethold = shader_get_uniform(sh_threthold, "uThrethold");
var uRatio     = shader_get_uniform(sh_threthold, "uRatio");

// シェーダーパラメータを設定.
shader_set_uniform_f(uColorHigh, 1, 0, 0); // (R,G,B)=(1,0,0)なので赤色.
shader_set_uniform_f(uColorLow,  0, 0, 0); // (R,G,B)=(0,0,0)なので黒色.
shader_set_uniform_f(uThrethold, rate/2); // しきい値は 0.f〜0.5f.
shader_set_uniform_f(uRatio,     rate);   // ブレンドの割合は 0.f〜1.f.

// 描画
draw_sprite(spr_test, 0, 0, 0);

// シェーダー終了.
shader_reset();

// デバッグ情報の描画.
draw_text(8, 8, "timer: " + string(timer));
draw_text(8, 20, "rate: " + string(rate));

最初は、しきい値を 0.f〜1.f の範囲で動かしてみたのですが、当然ながら 1.f 近くになると画面が真っ暗になって微妙な感じになったので、0.5f までの増加としました。

完成プロジェクト

2値化シェーダーのプロジェクトは以下からダウンロードできます。

おまけ2:ブルームシェーダー

シンプルなブルームシェーダーのコードを見つけたので紹介です。
https://forum.yoyogames.com/index.php?threads/free-bloom-glow-shader.11839/

ブルームとは、明るい部分を広げる処理をして強調するポストエフェクトとなります。

bloom.gif

基本的には先程のリンク先のコードをそのまま使います。

プロジェクトを作成して、シェーダー「sh_bloom」を作成して、フラグメント・シェーダー(sh_bloom.fsh)を以下のように記述します。(sh_bloom.vsh は修正不要です)

sh_bloom.fsh
//
// Simple passthrough fragment shader
// free-bloom-glow-shader. (license: creative commons)
// @url https://forum.yoyogames.com/index.php?threads/free-bloom-glow-shader.11839/
// @author DukeSoft
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
const float blurSize = 1.0/512.0; // ブルームのサイズ
uniform float intensity;

void main()
{
   vec4 sum = vec4(0);
   int j;
   int i;

   // take nine samples, with the distance blurSize between them
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x - 4.0*blurSize, v_vTexcoord.y)) * 0.05;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x - 3.0*blurSize, v_vTexcoord.y)) * 0.09;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x - 2.0*blurSize, v_vTexcoord.y)) * 0.12;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x - blurSize, v_vTexcoord.y)) * 0.15;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y)) * 0.16;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x + blurSize, v_vTexcoord.y)) * 0.15;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x + 2.0*blurSize, v_vTexcoord.y)) * 0.12;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x + 3.0*blurSize, v_vTexcoord.y)) * 0.09;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x + 4.0*blurSize, v_vTexcoord.y)) * 0.05;

   // take nine samples, with the distance blurSize between them
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y - 4.0*blurSize)) * 0.05;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y - 3.0*blurSize)) * 0.09;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y - 2.0*blurSize)) * 0.12;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y - blurSize)) * 0.15;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y)) * 0.16;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y + blurSize)) * 0.15;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y + 2.0*blurSize)) * 0.12;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y + 3.0*blurSize)) * 0.09;
   sum += texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, v_vTexcoord.y + 4.0*blurSize)) * 0.05;

   //increase blur with intensity!
   gl_FragColor = sum * intensity + texture2D(gm_BaseTexture, v_vTexcoord);
}

一定の割合で周りの色を拾ってきて、それをもとに色を決定しています。

次にこれを使用するオブジェクトを作成します。
obj_testとして、Createイベントを作成します。

Createイベント
// 光の強さ
intensity = 0.5;
// 経過時間
timer = 0;

さらにDrawイベントを作成して以下のように記述します

Draw
timer++;
// sinカーブで光の強さを求める
intensity = abs(1 * dsin(timer*2));

// シェーダーを設定
shader_set(sh_bloom);

// "intensity" のUniformハンドラを取得
var intensity_param = shader_get_uniform(sh_bloom, "intensity");

// Uniformパラメータを設定
shader_set_uniform_f(intensity_param, intensity);

// スプライトを描画
draw_sprite(spr_test, 0, 0, 0);

// シェーダーリセット
shader_reset();

// デバッグ表示
draw_text(8, 8, "intensity:" + string(intensity));

ちなみに光の強さは "intensity" の値で決定していて、上記コードは 0.0〜1.0 の範囲で動く値となります。

これを 0.0〜8.0 に大きくする(以下のコード)と……

// sinカーブで光の強さを求める (0.0〜8.0)
intensity = abs(8 * dsin(timer*2));

bloom2.gif

という感じで、かなり眩しくなります。

完成プロジェクト

ブルームの完成プロジェクトは以下からダウンロードできます。

おまけ3:2つのシェーダーを合成する

おまけでは、2値化とブルームの紹介をしましたが、さらにこの2つを合成してみます。
方法としては1つ目のシェーダーをサーフェースに描画し、その結果に対してシェーダーを適用してサーフェースを描画します。

まずはCreateイベントです

Createイベント
// タイマー初期化
timer = 0;

if(shader_is_compiled(sh_threthold) == false) {
  // シェーダーエラーチェック
  show_message("shader error");
}
if(shader_is_compiled(sh_bloom) == false) {
  // シェーダーエラーチェック
  show_message("shader error");
}

// 光の強さ
intensity = 0.5;

// サーフェースを作成
surface = surface_create(room_width, room_height);

ポイントは最後のサーフェースの作成です。
これを使ってDrawイベントを記述します。

Drawイベント
// タイマー更新
timer++;

// レンダリングターゲットをサーフェースにする
surface_set_target(surface);

// シェーダーを設定.
shader_set(sh_threthold);

// 割合を計算 (0.f〜1.f)
//var rate = abs(dsin(timer*0.5));

// 各シェーダーハンドラを取得.
var uColorHigh = shader_get_uniform(sh_threthold, "uColorHigh");
var uColorLow  = shader_get_uniform(sh_threthold, "uColorLow");
var uThrethold = shader_get_uniform(sh_threthold, "uThrethold");
var uRatio     = shader_get_uniform(sh_threthold, "uRatio");

// シェーダーパラメータを設定.
shader_set_uniform_f(uColorHigh, 1, 0, 0); // (R,G,B)=(1,0,0)なので赤色.
shader_set_uniform_f(uColorLow,  0, 0, 0); // (R,G,B)=(0,0,0)なので黒色.
//shader_set_uniform_f(uThrethold, rate/2); // しきい値は 0.f〜0.5f.
//shader_set_uniform_f(uRatio,     rate);   // ブレンドの割合は 0.f〜1.f.
shader_set_uniform_f(uThrethold, 0.2);
shader_set_uniform_f(uRatio, 1);

// 描画
draw_sprite(spr_test, 0, 0, 0);

// シェーダー終了.
shader_reset();

// レンダリングターゲットからサーフェースを切り離す
surface_reset_target();

// sinカーブで光の強さを求める
intensity = abs(4 * dsin(timer*2));

// シェーダーを設定
shader_set(sh_bloom);

// "intensity" のUniformハンドラを取得
var intensity_param = shader_get_uniform(sh_bloom, "intensity");

// Uniformパラメータを設定
shader_set_uniform_f(intensity_param, intensity);

// スプライトを描画
//draw_sprite(spr_test, 0, 0, 0);
// 2値化で描画したサーフェースを描画する
draw_surface(surface, 0, 0);

// シェーダーリセット
shader_reset();

surface_set_target() でサーフェースを設定して描画を行い、surface_reset_target () で切り離して、そのサーフェースで2回目の描画を行います。

最後にDestroyイベントでサーフェースを破棄します

Destroyイベント
// サーフェースを破棄
surface_free(surface);

実行すると以下のように2値化シェーダーにさらにブルームをかけることができます

surface.gif

ただ注意点として、サーフェースはいつメモリから消えるかわからないものなので、存在チェックをして消えていたら作り直す……、という処理が必要になります。

完成プロジェクト

完成プロジェクトは以下からダウンロードできます

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
3
Help us understand the problem. What are the problem?