DisplacementMapFilterを使って波紋を表現する

DisplacementMapFilterを使って波紋を表現する

今回も前回に引き続きDisplacementMapFilterを使ったエフェクトに挑戦してみました。今回は、Macウィジェットを追加した時のエフェクトみたいな波紋を作ってみたいと思います。

少し分かりづらいのですが、上の画像はMacウィジェットマネージャを起動してウィジェットを追加したときに背景が波紋で揺れるエフェクトが出た瞬間をキャプチャしたものです。
このような波紋を作るには、下記のような周期的かつ滑らかに変化する置き換えマップが必要になります。

しかし、PerlineNoiseでこのような画像を生成する方法を思いつかなかったので別なアプローチを試すことにしました。

波紋用の置き換えマップ生成法(その1)

まずはじめに試したのは、単純にdrawCircle()関数で円を描きそれをBlurFilterでぼかすという方法です。時間の経過とともに、半径を大きくしアルファ値を0に近づけてみました。

一見良さそうに見えるのですが、よく観察してみると色が滑らかに変化していないため白と黒が切り替わっているエッジが目立ちます。
また、DisplacementMapFilterを適用させる画像によっては変化が目立ちません。
動きが早く大きさも小さければあまり気になりませんが、今回は緩やかに大きく動くエフェクトを作りたいので他の方法を考えました。

波紋用の置き換えマップ生成法(その2)

次に試したのは、Sine関数で描かれる曲面のz座標をすべての点において計算し、その値をピクセルの色とする方法です。この方法では置き換えマップ上のすべての点が計算によって求められます。
そのため置き換えマップのサイズが大きくなると計算量が増えるという問題が生じます。

まずは、計算量のことは考えずに方法1と比較して良いエフェクトが得られるかどうかを確認してみました。

これが生成された置き換えマップ用のBitmapDataになります。その1と比べて変化が滑らかになったためエッジが目立たなくなったのが確認できます。
コードは以下の通りとなります。

// 下記の式で与えられる曲面のz座標を青成分として描画する 
//
// z = 127 * sin(A(l - t) - π) + 128
// l = sqrt(x * x + y * y)
//
for (var i:int = - bmpHeight / 2; i < bmpHeight / 2; i++ )
{
	for ( var j:int = - bmpWidth / 2; j < bmpWidth / 2; j++ )
	{
		var l:Number = Math.sqrt(i * i + j * j); // (1)
		var angle:Number = Math.PI * (l - time) / bmpHeight * frequency; // (2)
		var h:int = (l - time) / bmpHeight / 2 * frequency; //(3)
		var attenuationFactor:Number = (bmpWidth / 2 - l) / (bmpWidth / 2); // (4)
		var density:uint = 0;
		if ( (-frequency <= h) && (h < 0) && l < bmpWidth / 2)  // (5)
		{
			density = attenuationFactor * (Math.floor(127 * Math.sin(angle - Math.PI / 2)) + 128);// (6)
		}
		bmpData.setPixel32(j + bmpWidth / 2, i + bmpHeight / 2, 0xFF000000 | density); // (7)
	}
}
  1. これから色を求める点の中心からの距離
  2. 時刻timeにおける地点(j, i)の角度(ラジアン値)
  3. 時刻timeにおける地点(j, i)の何周期目かを計算
  4. 地点(j,i)における減衰率
  5. 0からfequencyで指定された周期のみを表示する
  6. 色を計算
  7. 求めた色を点に設定

以上で期待通りのエフェクトに仕上がっているのですが少しパフォーマンスが気になりました。

試しに400x400のサイズで実行時間を計測したところ、私の環境では1フレームあたり約140msかかることが分かりました。

計算量を減らすための工夫:Sineの周期性を使い1/4だけ計算し回転とコピーで補完する

Sineは変化が周期的なため中心から4つに分割して1/4を計算してしまえば、あとは回転とコピーをして求めたい画像を生成することができます。
どの部分から計算しても良いのですが、今回は右下部分の計算から開始することにしまいた。

// 実装方針:Sineの周期性を利用し1/4だけ計算し残りの3/4は回転とコピーで求める
//  1. 描画領域の中心点から4等分し、右下部分のみ計算する
//  2. 右下部分を残り部分にコピーする
var lastn:uint = bmpHeight / 2;
for (var i:int = 0; i < lastn; i++ )
{
	for ( var j:int = 0; j < bmpWidth / 2; j++ )
	{
		var l:Number = Math.sqrt(i * i + j * j);
		var angle:Number = Math.PI * (l - time) / lastn * 2 * frequency;
		var h:int = (l - time) / lastn * frequency;
		var attenuationFactor:Number = (bmpWidth / 2 - l) / (bmpWidth / 2);
		var density:uint = 0;
		if ( (-frequency <= h) && (h < 0) && l < bmpWidth / 2)
		{
			density = attenuationFactor * (Math.floor(127 * Math.sin(angle - Math.PI / 2)) + 128);
		}
		bmpData.setPixel32(j + bmpWidth / 2, i + bmpHeight / 2, 0xFF000000 | density);
	}
}
	
// 右下の1/4を左下にコピーする
var rm1:Matrix = new Matrix();
rm1.rotate(Math.PI / 2);
rm1.translate(bmpData.width, 0);
bmpData.draw(bmpData, rm1, null, null, new Rectangle(0, bmpHeight / 2, bmpWidth, bmpHeight / 2));

// 下の1/2を上にコピーする
var rm2:Matrix = new Matrix();
rm2.rotate(Math.PI);
rm2.translate(bmpWidth, bmpHeight);
bmpData.draw(bmpData, rm2);

この結果、1回BitmapDataを更新のに要する時間は平気で40ms程度となり、以前の約1/3になりました。

最終的にできあがったのがこれになります。


まとめ

  • 置き換えマップをsetPixel32()を使って動的に生成することができました
  • 動的生成に使う関数の周期性などを利用して計算量を少なくすることができ、パフォーマンス向上につながることが確認出来ました
    • 今回のケースでは関数がx軸対称かつy軸対称だったため、計算量を1/4に減らすことができました。しかし、データのコピーに時間がかかるため処理時間は1/3程度の短縮となりました。

ダウンロード

TODO

  • 方法その2でにてその1と同じように任意の点から波紋が生成されるように修正する
  • 波の干渉を再現する