【GameMaker Studio2】マスク処理の実装方法と使い方について


この記事では、Gamemaker Studio2でのマスク処理の実装方法について書いていきます。

# マスク処理とは

マスク処理とは、画像の特定の部分をくり抜いて描画する処理です。

![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/bf37a8b4-f29c-3854-fa77-520c49248116.png)

この処理を使うことで、以下のようにくり抜き部分を広げていく、というアニメーションを実装できます。

![mask.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/4e403fec-4304-16d4-5236-cd08e8f64f00.gif)

マスク処理で効果的なのがキャラクターのカットイン演出ですね。顔に近い部分をくり抜いて表情に注目させる、という使い方がよくされます。

![cutin.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/ceea5c58-45e3-36e3-16cc-85984ffc6e48.gif)

# マスクの実装手順

## マスクスクリプトの追加

まず、scr_rmask スクリプトを作成して以下の記述をします。

```js:scr_rmask
/// @description start(end) to write rendering mask.
/// @param xarea
/// @param yarea
/// @param warea
/// @param harea
function rmask_write() {
	var enable = argument[0];

	if(enable) {
		// start to write rendering mask.
		var xarea = 0;
		var yarea = 0;
		var warea = display_get_gui_width();
		var harea = display_get_gui_height();

		if(argument_count == 5) {
			xarea = argument[1];
			yarea = argument[2];
			warea = argument[3];
			harea = argument[4];
		}

		// clear alpha channel.
		gpu_set_blendenable(false); // disable blend mode.
		gpu_set_colorwriteenable(false, false, false, true); // alpha channel only.
		draw_set_alpha(0); // write alpha 0.
		draw_rectangle(xarea, yarea, warea, harea, false);
		draw_set_alpha(1); // reset alpha.
	}
	else {
		/// end to write rendering mask.
		gpu_set_blendenable(true); // enable blend mode.
		gpu_set_colorwriteenable(true, true, true, true); // reset color channel setting.
	}
}

/// @description enable rendering mask.
/// @param enable
function rmask_enable(enable) {
	if(enable) {
		/// enable rendering mask.
		gpu_set_blendmode_ext(bm_dest_alpha, bm_inv_dest_alpha); // alpha only.
		gpu_set_alphatestenable(true); // enable alpha test.
	}
	else {
		/// disable rendering mask.
		gpu_set_alphatestenable(false); // disable alpha test.
		gpu_set_blendmode(bm_normal); // reset blend mode.
	}
}
```

この処理は以下のページのコードを参考にしました。

* [DYNAMIC RENDERING MASKS](https://www.yoyogames.com/blog/430/dynamic-rendering-masks)

## マスク処理の呼び出し

スクリプトに定義したマスク処理は以下の手順で呼び出します

1. くり抜き部分の作成: rmask_write()
2. くり抜いた部分への描画: rmask_enable()

`rmask_write()` がくり抜き範囲の指定で、`rmask_enable()` がくり抜いた部分への描画を行う際に呼び出す処理です。
例えば以下のように使用します。

```js
// くり抜き開始
rmask_write(true);

// 左上(0, 0) - 右下(120, 80) の範囲をくり抜く
draw_rectangle(0, 0, 120, 80, false);

// くり抜き終了
rmask_write(false);

// くり抜いた部分への描画開始.
rmask_enable(true);

// スプライトを描画。くり抜いた部分にしか描画されない
draw_sprite(spr_001, 0, 0, 0);

// くり抜き描画を終了
rmask_enable(false);
```

注意点として、`rmask_enable(false)` で終了しないと、くり抜いた部分以外には描画できなくなります。また、透過値を指定して半透明でくり抜くことはできません。

これで、いつでもどこでもマスク処理が使えるようになります！


----

# おまけ：円弧描画でくり抜く

おまけとして、円弧描画でくり抜く処理を作ってみます。
draw_arcスクリプトを作成して以下のコードを追加します。

```js:draw_arc
/// @description 円弧を描画する.
/// @param cx 中心座標(X).
/// @param cy 中心座標(Y)
/// @param radius 半径.
/// @param start_rot 開始角度.
/// @param end_rot 終了角度.
/// @param color 色.
/// @param divide 円の分割数（大きくするほど滑らかになります)
function draw_arc(cx, cy, radius, start_rot, end_rot, color, divide) {

	// 現在設定している色を取得する
	var prev_color = draw_get_color();
	
	// 色を設定
	draw_set_color(color);

	// 角度差を計算
	var dir = sign(end_rot - start_rot);
	
	// 開始角度を設定
	var rot = start_rot;
	
	// 1ポリゴンあたりの角度
	var drot = dir * real(360) / divide;
	for(var i = 0; i < divide; i++) {
	
		var r1 = rot;
		var r2 = rot + drot;
		if(dir > 0) {
			// 角度差がプラス
			if(r2 >= end_rot) {
				r2 = end_rot; // 終了角度を超えた
			}
		}
		else {
			// 角度差がマイナス
			if(r2 <= end_rot) {
				r2 = end_rot; // 終了角度を超えた
			}
		}
	
		var x1 = cx + radius * dcos(r1);
		var y1 = cy - radius * dsin(r1);
		var x2 = cx + radius * dcos(r2);
		var y2 = cy - radius * dsin(r2);
		
		// ポリゴンを描画
		draw_triangle(cx, cy, x1, y1, x2, y2, false);
		
		// 次の角度に進める
		rot += drot;
	
		if(dir > 0) {
			// 角度差がプラス
			if(rot >= end_rot) {
				break; // 終了角度を超えた
			}
		}
		else {
			// 角度差がマイナス
			if(rot <= end_rot) {
				break; // 終了角度を超えた
			}
		}
	}

	// 色を戻す
	draw_set_color(prev_color);
}
```

これは以下のように、円を指定の角度の範囲で描画する関数です。

![arc.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/93b31aa4-96a6-ed13-d71e-59697b648103.gif)

これを使って円弧を使ったマスクを実装してみます。

まずはCreateイベントを以下のように記述します。

```js:Createイベント
timer = 0;
bg_spr = spr_001; // 背景スプライトを設定
fg_spr = spr_002; // 前面に描画するスプライトを設定
```

そして、Drawイベントに以下のように記述します

```js:Drawイベント

// タイマー更新
timer += 1;
if(timer > 90) {
	// 4倍した値を使うので90度で終了
	timer = 0;
	// 背景と前面のスプライトを交換する
	var tmp = bg_spr;
	bg_spr = fg_spr;
	fg_spr = tmp;
}

// 背景スプライトを赤色で描画
draw_sprite_ext(bg_spr, 0, 0, 0, 1, 1, 0, c_red, 1);

// くり抜きの円弧を描画
var cx = room_width/2;
var cy = room_height/2;
var radius = room_width;
rmask_write(true);
draw_arc(cx, cy, radius, 90, 90-timer*4, c_white, 32);
rmask_write(false);

// くり抜いた部分に前面のスプライトを描画
rmask_enable(true);
draw_sprite(fg_spr, 0, 0, 0);
rmask_enable(false);
```

これを実行すると以下のように、円弧でくり抜いた映画風の演出ができます。

![arc2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/d19bf205-e1d5-7198-3216-77ecfff305cf.gif)

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

* [TestMask2](http://syun777.sakura.ne.jp/tmp/gms2/TestMask2.yyz)

# 注意点
このマスク処理は、あくまでエミュレートなのでくり抜き部分の描画に**半透明部分のある画像**を重ねると、描画がおかしくなります。
![bug.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/d8bf3b1d-12f3-ed4f-801c-b2b10ac528c0.gif)
上記の例では、キャラクターの背後に影を表示するようにしたのですが、画像の縁にアルファ値が入っているためなのか、キャラクターの前面にその縁取り線が出てしまっています。

このマスク処理のエミュレートの仕組みを完全には理解していませんが、おそらくアルファ値を使ってマスクしているため、アルファ値を使った描画ができないような気がしています。そのため、もしアルファ値があるマスクを使用したい場合には、サーフェースに描画した結果をマスクするか、シェーダーを書く必要がありそうです。
もしくはシーケンサーの [Clipping Mask](https://manual.yoyogames.com/The_Asset_Editors/Sequence_Properties/Clipping_Masks.htm) を使うとよいのかもしれません（※未検証）

# 参考

* [DYNAMIC RENDERING MASKS](https://www.yoyogames.com/blog/430/dynamic-rendering-masks)