某ゲームのようなエフェクト
フィルタに使うマップ画像はリアルタイムに生成しています。
さらに、setPixelを各ピクセルに対して呼ぶ操作を画像全体に対する操作に置き換え、速度を改善しています。
マウス押下で開始・停止
左がMayaで書き出したZバッファです。この形状の表面をテクスチャが這っているようなマッピングが目標です。
中がランタイムに作成したマップ画像です。四隅がベタ塗りになっているのは、DisplacementMapFilterの"color"モードで設定した色を採用するためです。
右がDisplacementMapFilterを適用した出力画像です。適用する前の入力画像は、perlinNoiseにpaletteMapで着色したものです。
以下がZバッファからマップ画像を作成する部分です。
// マップ画像作成
private function createMap():Void {
for (var y:Number = 0xFF; y >= 0; --y) {
var outmap:Number = y < 0x80 ? 0x000000 : 0x00FF00;
for (var x:Number = 0xFF; x >= 0; --x) {
var z:Number = zbuffer.getPixel(x, y) & 0xFF;
if (z > 0) {
var tx:Number = 0x80 - (x - 0x80) / 0xFF * z;
var ty:Number = 0x80 - (y - 0x80) / 0xFF * z;
map.setPixel(x, y, ty << 8 | tx);
} else {
map.setPixel(x, y, outmap | (x < 0x80 ? 0 : 0xFF));
}
}
}
}
各ピクセルに対してzbufferから深度を調べて、それが0でなければX, Yに対し0x80 - (x - 0x80) / 0xFF * z(yも同様)の変形を行い、0であれば"color"モードで設定した色を採用します。
上記の式の左側の0x80は、DisplacementMapFilterにおける基準値です。右側の0x80は、画像の中心座標です。
つまり、この画像を使った変形は、「画像の中心からの距離 * 深度」に比例した量だけ離れたピクセルを採用します。これは3Dグラフィックスの投影に似ていますが、用意したZバッファを作成した際のカメラの投影と全く同じではないため、多少違和感が残っています。
さて、上記のcreateMapでは全ピクセルに対するsetPixelが行われているため、実行に時間がかかります。開発に使用したマシンではおよそ420ミリ秒でしたので、毎フレーム実行するのはかなり無理があります。
これを、画像全体に対する操作に置き換えることで改善してみます。
まず、各ピクセルの青チャンネルに対して行っている0x80 - (x - 0x80) / 0xFF * zの操作は、以下のように場合分けすることが可能です。
- 0x80 - (x - 0x80) / 0xFF * z ... x >= 0x80のとき
- 0x80 + (0x80 - x) / 0xFF * z ... x < 0x80のとき
同じ式のように見えますが、この場合分けを行った結果、計算のどの段階にも負数が発生しません。これで元の数式をビットマップの演算に置き換えることができます。
次に、(x - 0x80)または(0x80 - x)の計算ですが、これはグラデーション塗りを行うことで達成しています。0x80→0x00→0x80で塗られた幅256pxのグラデーションは、各ピクセルの値が(x - 0x80)または(0x80 - x)に相当します。
その後、塗られたグラデーションを"multiply"ブレンドモードでzbufferに乗算しています。これは「/ 0xFF * z」の計算と等価です。
最後に、ここまでの計算結果を0x80に減算(x >= 0x80のとき)または加算(x < 0x80のとき)しています。
緑チャンネルに対してyの計算を同様に行い、四隅のベタ塗りをthresholdで行って完成です。
// マップ画像作成
private function createMap():Void {
zbuffer.colorTransform(zbuffer.rectangle, new ColorTransform(0, 0, 1, 1, 0, 0, 0, 0));
var xmap:BitmapData = new BitmapData(256, 256, false, 0x80);
var ymap:BitmapData = new BitmapData(256, 256, false, 0x80);
map.fillRect(map.rectangle, 0x008080);
var a:BitmapData = zbuffer.clone();
gradientFill(a, true);
xmap.draw(a, new Matrix(), null, "add", new Rectangle(0x00, 0x00, 0x80, 0x100));
xmap.draw(a, new Matrix(), null, "subtract", new Rectangle(0x80, 0x00, 0x80, 0x100));
var a:BitmapData = zbuffer.clone();
gradientFill(a, false);
ymap.draw(a, new Matrix(), null, "add", new Rectangle(0x00, 0x00, 0x100, 0x80));
ymap.draw(a, new Matrix(), null, "subtract", new Rectangle(0x00, 0x80, 0x100, 0x80));
map.copyChannel(xmap, map.rectangle, new Point(), 4, 4);
map.copyChannel(ymap, map.rectangle, new Point(), 4, 2);
map.threshold(zbuffer, new Rectangle(0x00, 0x00, 0x80, 0x80), new Point(0x00, 0x00), "==", 0x00, 0xFF000000, 0xFF);
map.threshold(zbuffer, new Rectangle(0x80, 0x00, 0x80, 0x80), new Point(0x80, 0x00), "==", 0x00, 0xFF0000FF, 0xFF);
map.threshold(zbuffer, new Rectangle(0x00, 0x80, 0x80, 0x80), new Point(0x00, 0x80), "==", 0x00, 0xFF00FF00, 0xFF);
map.threshold(zbuffer, new Rectangle(0x80, 0x80, 0x80, 0x80), new Point(0x80, 0x80), "==", 0x00, 0xFF00FFFF, 0xFF);
}
// 水平または垂直にグラデーション塗り
private function gradientFill(bmp:BitmapData, horizontal:Boolean):Void {
graphics.clear();
graphics.beginGradientFill("linear", [0x80, 0x00, 0x7F], [0xFF, 0xFF, 0xFF], [0x00, 0x80, 0xFF], {matrixType:"box", x:0, y:0, w:bmp.width, h:bmp.height, r:horizontal ? 0 : Math.PI / 2});
graphics.moveTo(0x00, 0x00);
graphics.lineTo(0xFF, 0x00);
graphics.lineTo(0xFF, 0xFF);
graphics.lineTo(0x00, 0xFF);
graphics.lineTo(0x00, 0x00);
graphics.endFill();
bmp.draw(graphics, new Matrix(), null, "multiply");
}
改善後 - マウス押下で開始・停止
押下直後に出力が得られるようになりました。開発に使用したマシンで10ミリ秒ほどになりましたので、毎フレーム実行しても問題ない範囲です。
おまけ:毎フレーム実行することができたので、こんなことも可能です。他の部分が重いのであまり良いサンプルではないですが……
コメント一覧