オレンジブログ

オレンジブログ

GameMakerのちょっとしたTipsとか

【GMS2】テクスチャのUVを取得してシェーダーに適用する


はじめに 

GameMakerStudio2でシェーダを扱う場合、
UV座標を 0~1 の範囲で設定して書くケースが基本だと思います。

これはテクスチャを扱う際の基本となる部分で、
UV座標と言えば0~1だと思います。


シェーダーでUVを使用する際の問題点

例として、今回は下記のようなスプライトを用います。

真ん中の顔はシェーダーが適用されているのがわかりやすいように配置しました

下記コードはvertex shaderから送られてきたUVをそのまま使っているので、
v_vTexcoordが0~1の範囲で正規化されています。

varying vec2 v_vTexcoord; // uv(0 ~ 1)
varying vec4 v_vColour; // color

// 波の強さ
const float WAVE_STRENGTH = 0.08;
// 波の速度
const float WAVE_SPEED = 10.0;
// 波の周波数
const float WAVE_FREQUENCY = 20.0;
// 時間を渡して動かす
uniform float u_time; 

void main()
{
    // X座標に応じてY座標を変化させる
    float wave = sin(v_vTexcoord.x * WAVE_FREQUENCY + u_time * WAVE_SPEED) * WAVE_STRENGTH; // 波の振幅
    // Y方向に波をつける
    vec2 wavedUV = vec2(v_vTexcoord.x, v_vTexcoord.y + wave);
    // テクスチャの色を取得
    vec4 texColor = texture2D(gm_BaseTexture, wavedUV);
    // 反映
    gl_FragColor = texColor;
}

このシェーダーを適用すると、下記のような見た目になるのが理想です。

波のように揺れる
しかし、実際にシェーダーを適用すると
別のスプライトが割り込んできたりするケースがあります

下の方に何か見える

これはGameMakerのテクスチャページの仕様によるものです。


GameMakerのテクスチャページの仕様

シェーダーのUVはvec2(0.0, 0.0) から vec2(1.0, 1.0) までの範囲を持ちますが、
GameMakerでは複数のスプライトを1つのテクスチャページにまとめるため、
必ずしも目的のスプライトが0 ~ 1の間に収まっているとは限りません。

イメージ
現実

この問題を解決する方法として2つのパターンを紹介しようと思います。


Separate Texture Page を使った方法

スプライトには「Separate Texture Page(分けたテクスチャページ)」という設定があり、
スプライトを他のテクスチャと分離して保存できます。

manual.gamemaker.io

スプライトアセットを開いて、テクスチャ設定を開きます。

そこの「Separate Texture Page」にチェックをつけます。

日本語だと「分けたテクスチャページ」と書いてあります

チェックがついたらこのような見た目になります。

こうなればOK

設定はこれで終わりです。

これにチェックを入れると、チェックを入れたスプライトは他のスプライトと混ぜたテクスチャにならず
そのスプライト1枚のテクスチャとしてエクスポートされます

なので、必ずuvも0~1になります

メリット

  • UV 座標が 0~1 のまま維持される
  • シェーダー側の処理がシンプルになる

デメリット

公式マニュアルには下記が問題点として記されています。

  • VRAM(ビデオメモリ)の浪費
    • 1スプライトごとに1つのテクスチャページを作成するため、無駄が多い
  • ドローコール(描画処理)の増加
    • テクスチャページの切り替えが頻繁に発生し、GPU負荷が増加
  • モバイルでは特に非推奨
    • VRAMが不足し、ゲームがクラッシュするリスクが高まる

このようにSeparate Texture Page
一時的な解決策にはなるが、最適とは言えない ため、
大量のスプライトに設定することはおすすめしません。

大量のスプライトに適用させたい場合は、後述する texture_get_uvs を使った方法がおすすめです


texture_get_uvs を使った方法

GMLの関数にtexture_get_uvsという関数があります。

manual.gamemaker.io

こちらの関数の引数にテクスチャを入れると、
下記のような配列が返ってきます。

  • [0] = left

    • スプライトのUVの左端座標(テクスチャページ上)
  • [1] = top

    • スプライトのUVの上端座標(テクスチャページ上)
  • [2] = right

    • スプライトのUVの右端座標(テクスチャページ上)
  • [3] = bottom

    • スプライトのUVの下端座標(テクスチャページ上)
  • [4] = left_trim

    • スプライトの左側で削除されたピクセル(スプライトのみ適用。サーフェスのテクスチャでは0が返る)
  • [5] = top_trim

    • スプライトの上側で削除されたピクセル(スプライトのみ適用。サーフェスのテクスチャでは0が返る)
  • [6] = width_ratio

    • 元のスプライトの幅に対して、テクスチャページに保存されたピクセルデータの比率(0.0 ~ 1.0)
  • [7] = height_ratio

    • 元のスプライトの高さに対して、テクスチャページに保存されたピクセルデータの比率(0.0 ~ 1.0)

直訳でこんな感じです。

つまりこの関数は、スプライトが保存されているテクスチャページ上での
UVの値を返してくれると言うわけです。

イメージ

これは便利な関数ですね。

メリット

  • テクスチャページにトリミングされたUV座標を正しく取得できる
  • VRAMを節約しながら描画可能
  • 描画処理の増加を防げる

デメリット

  • シェーダーをそれに合わせて記述する必要がある
  • GML側にも追記で記述するコードがある
  • 特にアニメーションを行う際は毎フレーム取得する必要がある

とまぁこんな感じです。
実装が大変になる分、メモリ周りには優しいって認識でOKです。


texture_get_uvsを用いたコード

実際にtexture_get_uvsを使ったサンプルを紹介します。

シェーダー側

シェーダーにuv値を受け取るuniformを作ります。

// texture_get_uvsの結果を受け取る
uniform float u_uv_left;
uniform float u_uv_top;
uniform float u_uv_right;
uniform float u_uv_bottom;

後はこの変数を用いて既存のシェーダー(fragment shader)を書き換えます。
先ほどのシェーダーを書き換えたサンプルはこちらです。

//
// Simple passthrough fragment shader (with texture_get_uvs support)
// 
varying vec2 v_vTexcoord; // uv
varying vec4 v_vColour;   // color
// 波の強さ
const float WAVE_STRENGTH = 0.08;
// 波の周波数
const float WAVE_FREQUENCY = 20.0;
// 波の速度を定数として定義
const float WAVE_SPEED = 10.0;

// 時間を渡して動かす
uniform float u_time;
// texture_get_uvsの結果を受け取る
uniform float u_uv_left;
uniform float u_uv_top;
uniform float u_uv_right;
uniform float u_uv_bottom;

void main()
{
    // UV範囲の大きさを計算(右端 - 左端 、下端 - 上端)
    float uv_width = u_uv_right - u_uv_left;    // 今回は未使用なのでコメントアウトしてもOK
    float uv_height = u_uv_bottom - u_uv_top;

    // 波の振幅を UV の高さに対する割合で調整
    float wave_strength = WAVE_STRENGTH * uv_height;  // WAVE_STRENGTHをUV比率に調整
    // 波の計算のため、テクスチャ内のx座標ではなくローカルでのx座標にする
    float local_x = v_vTexcoord.x - u_uv_left;
    // X座標に応じてY座標を変化させる
    float wave = sin(local_x * WAVE_FREQUENCY + u_time * WAVE_SPEED) * wave_strength;
    // 波を適用したUV座標(UV範囲を超えないようにクランプ)
    float waved_y = clamp(v_vTexcoord.y + wave, u_uv_top, u_uv_bottom);

    // uvからテクスチャの色を取得
    vec4 texColor = texture2D(gm_BaseTexture, vec2(v_vTexcoord.x, waved_y));
    // 色を決定
    gl_FragColor = texColor;
}

GML

次はGML側からテクスチャのuvをシェーダー側に送ります。
Drawイベントに下記のように記述します。

// ミリ秒を秒に変換
var _time = current_time / 1000.0;
// シェーダーセット
shader_set(ShaderFlagWave);
// timeを設定
shader_set_uniform_f(shader_get_uniform(ShaderFlagWave, "u_time"), _time);
// 現在のスプライトとアニメーションインデックスからテクスチャを取得
var _tex = sprite_get_texture(sprite_index, image_index);
// テクスチャのuvを取得
var _uvs = texture_get_uvs(_tex);
// uvを設定
shader_set_uniform_f(shader_get_uniform(ShaderFlagWave, "u_uv_left"), _uvs[0]);
shader_set_uniform_f(shader_get_uniform(ShaderFlagWave, "u_uv_top"), _uvs[1]);
shader_set_uniform_f(shader_get_uniform(ShaderFlagWave, "u_uv_right"), _uvs[2]);
shader_set_uniform_f(shader_get_uniform(ShaderFlagWave, "u_uv_bottom"), _uvs[3]);
// 自信を描画
draw_self();
// シェーダーリセット
shader_reset();

これにより、トリミングされたスプライトの UV 座標を適切に補正し、正しく描画することができます


実践

先ほどのコードを使って実際に動かしてみた結果をキャプチャしました。

うまくいった!

このように、Separate Texture Page を使わなくても、トリミングされたスプライトを正しく描画できることが確認できます


まとめ

今回の記事のまとめです。

  • GameMaker のスプライトはテクスチャページの影響で UV 座標が 0~1 にならないことがある
  • Separate Texture Page を使えば解決できるが、メモリ消費・パフォーマンスの問題がある
  • texture_get_uvs() を使えばスプライトごとのUVを取得することができ、描画時に補正できる
  • この方法を使うことでVRAMを節約しつつシェーダーを扱うことができる

大体こんな感じです。
メモリの節約や容量の節約はゲーム開発に必須だと思うので、ぜひ活用してください。