透明テキスチャーとStarling タッチイベント

本ポストは@umiboseから頂いた質問の回答です。(質問ありがとうございました!)

Starlingフレームワークは独自タッチイベントを実装し、オブジェクトのシェイプ内にタッチがあった場合にイベントを発生します。ただしオブジェクトのタッチされた部分のテキスチャーが透明だった場合にそのタッチを無視したい場合が多くありますが、標準Starlingではその実装が不可能です。(なぜかというと、Starlingはテキスチャーの画像データをGPUに投げたら保持しませんので、タッチが行った時にはどこが透明だったかは認識しませんから。)したがって、StarlingのImageクラスを継承し、テキスチャーを保持するように変更すれば希望のヒットテストできます。この方法、そしてメモリー負担を抑える改善、を以下解説します。

まずは、Imageに紐付くテキスチャーデータを保持するようにstarling.display.Imageクラスを継承します。

package {

import flash.display.Bitmap;
import flash.display.BitmapData;
import starling.display.Image;
import starling.textures.Texture;

public class CachingImage extends Image {

private var cachedTexture:BitmapData;

public function CachingImage(_bitmap:Bitmap) {
// 渡されたビットマップを保持する
cachedTexture = _bitmap.bitmapData;
// 親クラスImageの実装にフォールバックする
super(Texture.fromBitmap(_bitmap));
}
}
}

次の課題は、タッチイベントを発生すべきかどうかのチェックを、テキスチャーの透明度を認識する事です。Starlingはちょうどこのためにヒットテストのメソッドを実装しています、hitTest()nullを返せばタッチイベントがなかった事になって、当DisplayObjectを返すとタッチイベントが発生します。従ってタッチされた座標でテキスチャーの色を取得して、アルファーが低ければnullを返すようにします。

public var alphaCutoff:int = 20;

// タッチイベントの際のヒットテストを行う
public override function hitTest(localPoint:Point, forTouch:Boolean=false):DisplayObject {
// オブジェクトが非表示、またはタッチ非対応の場合にnullを返す
if (forTouch && (!visible || !touchable)) { return null; }

// タッチが本オブジェクトの枠内でない場合もnullを返す
if (! getBounds(this).containsPoint(localPoint)) { return null; }

// 通常だったらここでthisを返すが、テキスチャーの透明度をチェックを行いましょう
var color:uint = cachedTexture.getPixel32(localPoint.x,localPoint.y);
if (Color.getAlpha(color) > alphaCutoff) {
return this;// ヒットがありました
} else {
return null;
}
}

これで、テキスチャーの透明な部分をタッチしてもイベントが発生しない実装ができました。しかしこの実装ですとテキスチャーそれぞれがActionScriptメモリーに保持され、メモリー使用量の上昇が懸念します。しかしよく考えますと、透明度のヒットテストは通常、完璧にピクセル毎に行う必要がありません。テキスチャーを保持する前に縮小しておけば、ヒットテストが少々アバウトになりますがメモリー使用量の負担はかなり緩和されます。テキスチャーによってサイズを10分の1に減らしてもタッチイベントに違和感を感じません。

*実際のテキスチャー*
*メモリーに保持される画像*
*ヒットテストの正確さは許容範囲内*

そして画像の縮小の実装は、bitmapData.draw()を呼んでFlashのレンダラーに負かせば数行のコードでできます。この改善を下記のように実装できます:

package {

import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.geom.*;
import starling.display.*;
import starling.textures.Texture;
import starling.utils.Color;

public class SmartImage extends Image {

// 保持データの縮小レベル (0.01-1)
private var scaling:Number;

// 保持用画像データ
private var bitmapCache:BitmapData;

// タッチイベントを認識されるアルファーレベル (0-255)
public var alphaCutoff:int = 32;

public function SmartImage(_bitmap:Bitmap, _scaling:Number=0.5) {
scaling = Math.max(_scaling, .01);
scaling = Math.min(scaling, 1);
cacheData(_bitmap);
super( Texture.fromBitmap(_bitmap) );
}

// テキスチャーデータを縮小して保持する
private function cacheData(bmp:Bitmap) {
// 保持用BitmapDataを用意する
var w:int = Math.ceil(bmp.width * scaling);
var h:int = Math.ceil(bmp.height * scaling);
bitmapCache = new BitmapData(w,h,true,0);

// .draw() を使って、縮小をFlashのレンダラーに任せる
var scaleMatrix:Matrix = new Matrix();
scaleMatrix.scale(scaling,scaling);
bitmapCache.draw(bmp, scaleMatrix);
}

// タッチイベントの際のヒットテストを行う;
public override function hitTest(localPoint:Point, forTouch:Boolean=false):DisplayObject {
// オブジェクトが非表示、またはタッチ非対応の場合にnullを返す
if (forTouch && (!visible || !touchable)) { return null; }

// タッチが本オブジェクトの枠内でない場合もnullを返す
if (! getBounds(this).containsPoint(localPoint)) { return null; }

// 通常だったらここでthisを返すが、テキスチャーの透明度をチェックを行いましょう
var color:uint = bitmapCache.getPixel32(localPoint.x*scaling, localPoint.y*scaling);
if (Color.getAlpha(color) > alphaCutoff) {
return this;// ヒットがありました
} else {
return null;
}
}
}
}

本クラスSmartImageを実装する簡単なテストプロジェクトはこちらです:
Starling ヒットテストプロジェクト
(Flash CS6のFLAが入っています。Flash Builderから確認するにはDocument.asのロジックを新プロジェクトに加えてください。)