コンポジットオペレーションで簡単エフェクト

Akashic Advent Calendar 2019 十五日目の記事です。

ゲームタイトルのような派手に見せたいもの、ゲームスタートボタンのようなユーザの興味を引きたいものに、ちょっとした演出を加えたいことがあります。この記事ではそういった演出に使えそうな、画像を光らせる方法について紹介します。

## ゴールの確認

最初に完成形を確認しましょう。ゲームのタイトルが白くフラッシュしています。

![flash-sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/b1d3d6f6-f1c2-670f-d0a2-48eeb2ea1110.gif)

_画像は全て Akashic Engine [公式サイトのもの](https://akashic-games.github.io/asset/material.html)を利用しています。_

素直に考えると、白く光らせたい画像を白く塗りつぶした、もう一枚の画像を用意して重ね合わせる必要がありそうですが、コンポジットオペレーションを使えばプログラムだけで実現できます。

## コンポジットオペレーション

コンポジトオペレーションは Akashic Engine の公式サイトでは[次のように説明](https://akashic-games.github.io/guide/composite-operation.html)されています。

> E#compositeOperation とは、エンティティを描画する際の合成方法を指定するプロパティです。

色々ある合成方法のうち `SourceAtop` を使用します（同じく Akashic Engine の公式サイトより引用）。

> SourceAtop
> 先に描画された領域と重なった部分のみを描画します。
> 
> 以下の例は、赤色と青色の FilledRect に SourceAtop 指定し、緑色の FilledRect と重なった部分だけ描画する例です。
> ![source-atop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/cfe4273b-d010-a234-5bc6-c38aed0f01a1.png)

これを使えば、元画像の上に白い矩形を描画することで白く光らせることができます。

### 余談: sourceAtop の計算式

macOSなどで利用されている Core Graphics フレームワークにも `sourceAtop` と名付けられた合成方法があり、[その計算式](https://developer.apple.com/documentation/coregraphics/cgblendmode/sourceatop)は次の通りです。

```
R = S*Da + D*(1 - Sa)
```

言葉だけの説明よりわかりやすいですね。 Akashic Engine がこれと同じかはわかりません。

## 実装

では、コンポジットオペレーションを利用して白く光るカスタムE `ShinyEffect` のコードをみてみましょう。

```typescript
/**
 * キラキラエフェクトパラメタオブジェクト。
 */
interface ShinyEffectParameterObject extends g.CacheableEParameterObject {
	/** キラキラさせたい画像 */
	src: g.ImageAsset;

	/** キラキラの色 */
	effectColor?: string;
}

/**
 * キラキラエフェクト。
 */
class ShinyEffect extends g.CacheableE {
	/** 光沢の強さ(0~1)。 */
	shininess: number;

	/** キラキラの色 */
	effectColor: string;

	private surface: g.Surface;

	constructor(param: ShinyEffectParameterObject) {
		param.width = param.src.width;
		param.height = param.src.height;

		super(param);

		this.shininess = 0;
		this.effectColor = param.effectColor || "white";
		this.surface = param.src.asSurface();
	}

	renderCache(renderer: g.Renderer, camera?: g.Camera): void {
		// 光らせたい画像を最初に描画する。
		renderer.drawImage(this.surface, 0, 0, this.surface.width, this.surface.height, 0, 0);

		renderer.save();

		// SourceAtop に設定。
		renderer.setCompositeOperation(g.CompositeOperation.SourceAtop);

		// 次に描画する矩形の透明度で光沢の強さを表現する。
		renderer.setOpacity(this.shininess);

		// 光らせるため、描画する画像と同じ大きさの白い矩形を描画する。
		renderer.fillRect(0, 0, this.surface.width, this.surface.height, this.effectColor);

		renderer.restore();
	}
}
```

直接画面上でコンポジットオペレーションを行うと、他の要素まで白く光ってしまいます。`g.CacheableE` の派生クラスにすることで、コンポジットオペレーションを行う専用の領域を用意しています。

`ShinyEffect` 自体は輝き方を制御するロジックを持たないので、適宜実装する必要があります。例えば次のようになるでしょう。

```typescript
// １秒に一度キラッと輝く。
let cntr = 0;
shinyEffect.update.add(() => {
	cntr++;
	shinyEffect.shininess = 1 - ((cntr % g.game.fps) / g.game.fps);
	shinyEffect.invalidate();
});
```

コンポジットオペレーションによる演出の説明は以上になります。デモは[GitHubから入手](https://github.com/blackspotbear/akashic-sample/tree/master/shiny)できます。

## 最後に

コンポジットオペレーションで画像を光らせることができました。白く光らせる演出は、シューティングゲームや格闘ゲームのダメージ表現でもよく見かけるものです。白以外の色も使えるので、簡単で使い勝手の良い技法だと思います。

この記事では単純に光沢の強弱を変更するものを作成しましたが `g.CacheableE#renderCache()` の実装を工夫すれば他にも色々な表現が可能です。

![onemore.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/73469754-59d8-5cb0-a0f2-17eaef2d078b.gif)