ビットマップの例:回転する月のアニメーション

Flash Player 9 以降、Adobe AIR 1.0 以降

回転する月のアニメーションの例では、Bitmap オブジェクトとビットマップイメージデータ(BitmapData オブジェクト)を操作する手法について説明します。 この例では、平らな月面のイメージを未処理のイメージデータとして使用し、回転する球体の月のアニメーションを作成します。次の手法について説明します。

  • 外部イメージの読み込みと未処理のイメージデータへのアクセス

  • ソースイメージの様々な部分からピクセルを繰り返しコピーすることによるアニメーションの作成

  • ピクセル値の設定によるビットマップイメージの作成

このサンプルのアプリケーションのファイルを入手するには、 www.adobe.com/go/learn_programmingAS3samples_flash_jp を参照してください。 回転する月のアニメーションに関するアプリケーションファイルは Samples/SpinningMoon フォルダーにあります。 このアプリケーションは次のファイルで構成されています。

ファイル

説明

SpinningMoon.mxml

または

SpinningMoon.fla

Flex(MXML)または Flash(FLA)のメインアプリケーションファイル。

com/example/programmingas3/moon/MoonSphere.as

月の読み込み、表示、アニメーション機能を実行するクラス。

moonMap.png

月面写真が含まれるイメージファイル。このファイルを読み込み、回転する月のアニメーション作成に使用します。

ビットマップデータとしての外部イメージの読み込み

このサンプルで実行する最初の主要なタスクは、外部イメージファイル(月面写真)の読み込みです。読み込み操作は、MoonSphere クラスの 2 つのメソッドである読み込みプロセスを開始する MoonSphere() コンストラクターと、外部イメージが完全に読み込まれたときに呼び出される imageLoadComplete() メソッドによって処理されます。

外部イメージの読み込みは外部 SWF のロードに類似しており、いずれも flash.display.Loader クラスのインスタンスを使用して読み込み操作を実行します。 イメージの読み込みを開始する MoonSphere() メソッドの実際のコードは次のとおりです。

var imageLoader:Loader = new Loader(); 
imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageLoadComplete); 
imageLoader.load(new URLRequest("moonMap.png"));

最初の行で imageLoader という名前の Loader インスタンスを宣言します。3 行目で、Loader オブジェクトの load() メソッドを呼び出して実際に読み込みプロセスを開始し、読み込むイメージの URL を表す URLRequest インスタンスを渡します。2 行目では、イメージが完全に読み込まれたときにトリガーされるイベントリスナーを設定します。 Loader インスタンス自体では addEventListener() メソッドは呼び出されず、Loader オブジェクトの contentLoaderInfo プロパティで呼び出されます。Loader インスタンス自体は、読み込み中のコンテンツに関連するイベントを送出しません。ただし、その contentLoaderInfo プロパティには、Loader オブジェクトに読み込まれるコンテンツ(この場合は外部イメージ)に関連付けられた LoaderInfo オブジェクトに対する参照が含まれます。LoaderInfo オブジェクトは、イメージが完全に読み込まれたときに imageLoadComplete() メソッドの呼び出しをトリガーする complete イベント( Event.COMPLETE )など、外部コンテンツの読み込みの進捗や完了に関連するいくつかのイベントを提供します。

外部イメージの読み込みを開始することは、このプロセスにおいて重要ですが、読み込みの終了時の動作を把握しておくことも同様に重要です。前のコードに示したように、イメージが読み込まれると、 imageLoadComplete() 関数が呼び出されます。この関数は、読み込まれたイメージデータについて、以下で説明するいくつかの操作を行います。 ただし、イメージデータを使用するには、そのデータにアクセスする必要があります。 Loader オブジェクトを使用して外部イメージを読み込むと、読み込まれたイメージが Bitmap インスタンスになり、これが Loader オブジェクトの子表示オブジェクトとして関連付けられます。 この場合、Loader インスタンスは、メソッドにパラメーターとして渡されるイベントオブジェクトの一部としてイベントリスナーメソッドに使用できます。 imageLoadComplete() メソッドの最初の行は次のとおりです。

private function imageLoadComplete(event:Event):void 
{ 
    textureMap = event.target.content.bitmapData; 
    ... 
}

イベントオブジェクトのパラメーターの名前は event で、これは Event クラスのインスタンスです。Event クラスのすべてのインスタンスには target プロパティがあります。これは、イベント(この場合は、前述のように addEventListener() メソッドが呼び出される LoaderInfo インスタンス)をトリガーするオブジェクトを参照します。LoaderInfo オブジェクトには、読み込まれたビットマップイメージと共に Bitmap インスタンスが含まれる content プロパティがあります(読み込みプロセスが完了した場合)。イメージを画面に直接表示する場合は、この Bitmap インスタンス( event.target.content )を表示オブジェクトコンテナに関連付けることができます(Loader オブジェクトを表示オブジェクトコンテナに関連付けることもできます)。 ただし、このサンプルでは、読み込まれたコンテンツは画面上に表示されるのではなく、未処理イメージデータのソースとして使用されます。 したがって、 imageLoadComplete() メソッドの最初の行は、読み込まれた Bitmap インスタンス( event.target.content.bitmapData )の bitmapData プロパティを読み取り、これを textureMap という名前のインスタンス変数に格納します。この変数は、回転する月のアニメーションを作成するためのイメージデータのソースとして使用されます。これについては、次に説明します。

ピクセルのコピーによるアニメーションの作成

アニメーションの基本的な定義は、イメージを徐々に変化させることによって、動いている、または変化しているように見せることです。 このサンプルでは、球体の月がその垂直軸を中心に回転しているように見せることが目標です。 ただし、このアニメーションでは、サンプルの球体の歪みは無視できます。 月のイメージデータのソースとして実際に読み込んで使用するイメージを次に示します。

ここからわかるように、イメージは 1 つまたは複数の球体ではなく、月面の長方形の写真です。この写真は月の赤道で撮影されたものなので、イメージの上下に近い部分は引き伸ばされ、歪んでいます。イメージから歪みを削除し、球体に見えるようにするには、後で説明するように置き換えマップフィルターを使用します。 ただし、このソースイメージは長方形なので、球体が回転しているように見せるには、コードで月面写真を水平方向に移動する必要があります。

このイメージには、実際には月面写真のコピーが 2 つ並んで含まれています。 このイメージはソースイメージであり、ここからイメージデータを繰り返しコピーして、動いている様子を作成します。 イメージの 2 つのコピーを並べることにより、連続した、中断されないスクロール効果をより簡単に作成できます。 アニメーションのプロセスを順を追って説明し、その動作を確認していきます。

このプロセスには、実際には 2 つの個別の ActionScript オブジェクトが関連しています。 まず読み込まれたソースイメージがあり、このコードでは、 textureMap という名前の BitmapData インスタンスによって表されます。前述のように、外部イメージが読み込まれると、直ちにイメージデータが textureMap に入力されます。

textureMap = event.target.content.bitmapData;

textureMap のコンテンツは、長方形の月のイメージです。さらに、回転のアニメーションを作成するために、月のイメージを画面上に表示する実際の表示オブジェクトである、 sphere という名前の Bitmap インスタンスをコードで使用します。 textureMap と同様に sphere オブジェクトが作成され、次のコードを使用して、 imageLoadComplete() メソッドの初期イメージデータが入力されます。

sphere = new Bitmap(); 
sphere.bitmapData = new BitmapData(textureMap.width / 2, textureMap.height); 
sphere.bitmapData.copyPixels(textureMap, 
                         new Rectangle(0, 0, sphere.width, sphere.height), 
                         new Point(0, 0));

コードに示されているように、 sphere はインスタンス化されます。その bitmapData プロパティ( sphere によって表示される未処理のイメージデータ)が、 textureMap と同じ高さと半分の幅で作成されます。つまり、 textureMap イメージには並べられた 2 枚の月面写真が含まれているので、 sphere のコンテンツは 1 枚の写真のサイズになります。次に、 bitmapData プロパティに、その copyPixels() メソッドを使用してイメージデータが入力されます。 copyPixels() メソッド呼び出しのパラメーターは次のことを示します。

  • 最初のパラメーターは、イメージデータが textureMap からコピーされたことを示します。

  • 2 番目のパラメーター(新規の Rectangle インスタンス)は、 textureMap のどの部分からイメージスナップショットを取得するかを指定します。この場合は、スナップショットは、 textureMap の左上隅(最初の 2 つの Rectangle() パラメーター 0, 0 で示される)から始まる長方形で、その長方形スナップショットの幅と高さは、 sphere width および height プロパティに一致します。

  • 3 番目のパラメーター(x および y 値が 0 の新規の Point インスタンス)は、ピクセルデータのターゲットを定義します。この場合は、 sphere.bitmapData の左上隅(0, 0)です。

視覚的に表現すると、このコードは、次のイメージに示す textureMap からピクセルをコピーし、 sphere に貼り付けます。つまり、 sphere の BitmapData コンテンツは、ここで強調表示した textureMap の一部です。

ただし、これは sphere の初期状態にすぎません。つまり、 sphere にコピーされる最初のイメージコンテンツです。

ソースイメージを読み込んで sphere を作成すると、 imageLoadComplete() メソッドによって実行される最後のタスクは、アニメーションの設定になります。アニメーションは、次のコードによって作成および開始される、 rotationTimer という名前の Timer インスタンスによって制御されます。

var rotationTimer:Timer = new Timer(15); 
rotationTimer.addEventListener(TimerEvent.TIMER, rotateMoon); 
rotationTimer.start();

このコードは、最初に rotationTimer という名前の Timer インスタンスを作成します。 Timer() コンストラクターに渡されたパラメーターは、 rotationTimer がその timer を 15 ミリ秒ごとにトリガーすることを示します。次に addEventListener() メソッドが呼び出され、 timer イベント( TimerEvent.TIMER )が発生したときにメソッド rotateMoon() を呼び出すことを指定します。最後に、 start() メソッドを呼び出すことにより、タイマーを実際に起動します。

rotationTimer の定義に従って、月のアニメーションが実行される MoonSphere クラスの rotateMoon() メソッドを Flash Player が約 15 ミリ秒ごとに呼び出します。 rotateMoon() メソッドのソースコードは次のとおりです。

private function rotateMoon(event:TimerEvent):void 
{ 
    sourceX += 1; 
    if (sourceX > textureMap.width / 2) 
    { 
        sourceX = 0; 
    } 
     
    sphere.Data.copyPixels(textureMap, 
                                    new Rectangle(sourceX, 0, sphere.width, sphere.height), 
                                    new Point(0, 0)); 
     
    event.updateAfterEvent(); 
}

このコードによって次の 3 つの操作が行われます。

  1. 変数 sourceX の値(初期値は 0 に設定)が 1 ずつ大きくなります。

    sourceX += 1;

    後で説明するように、 sourceX は、 sphere にピクセルをコピーする textureMap での元の位置を指定するので、このコードによって、 textureMap で長方形が 1 ピクセル右に移動します。視覚的な表現で説明すると、数サイクルのアニメーションの後、ソース長方形は次のように数ピクセル右に移動します。

    その数サイクル後には、長方形はさらに遠くに移動します。

    このようにピクセルのコピー元を徐々に絶えず移動することが、アニメーションでは非常に重要です。 ソースの位置をゆっくりと継続して右方向に移動することにより、 sphere の画面に表示されるイメージは、左方向に絶え間なく移動しているように見えます。月面写真の 2 つのコピーがソースイメージ( textureMap )に必要なのはこのためです。長方形が絶え間なく右方向に移動しているので、ほとんどの時間は、長方形は 1 つの月面写真ではなく、2 つの月面写真に重なっています。

  2. ソース長方形が右方向にゆっくりと移動すると、1 つの問題が発生します。 最終的に長方形が textureMap の右端に達し、 sphere にコピーする月面写真のピクセルがなくなります。

    次のコード行でこの問題を解決します。

    if (sourceX >= textureMap.width / 2) 
    { 
        sourceX = 0; 
    }

    このコードは、 sourceX (長方形の左端)が textureMap の中央に達したかどうかをチェックします。達した場合は、 sourceX を 0 にリセットし、 textureMap の左端に戻して、サイクルを再開します。

  3. 適切な sourceX 値を計算したら、アニメーション作成の最後の手順は、新しいソース長方形のピクセルを sphere に実際にコピーすることです。この操作を実行するコードは、最初に sphere にデータを入力したコードとよく似ています(前述)。唯一異なる点は、 new Rectangle() コンストラクター呼び出しで長方形の左端が sourceX に置かれることです。

    sphere.bitmapData.copyPixels(textureMap, 
                                new Rectangle(sourceX, 0, sphere.width, sphere.height), 
                                new Point(0, 0));

このコードは 15 ミリ秒ごとに繰り返し呼び出されます。 ソース長方形の位置が絶え間なく移動し、ピクセルが sphere にコピーされると、画面上では sphere が表す月面写真イメージが絶え間なく流れ続けます。つまり、月が回転し続けているように見えます。

球体の外観の作成

当然ながら、月は球体であり、長方形ではありません。 したがって、サンプルでは、アニメーション化を続けながら、長方形の月面写真を取得し、球体に変換する必要があります。 このためには 2 つの個別の手順が必要です。マスクを使用して、月面写真の円形の領域を除くすべてのコンテンツを非表示にし、置き換えマップフィルターを使用して月面写真の外観を歪め、3 次元に見えるようにします。

最初に、円形マスクを使用して、フィルターによって作成された球体を除く、MoonSphere オブジェクトのすべてのコンテンツを非表示にします。 次のコードは、マスクを Shape インスタンスとして作成し、これを MoonSphere インスタンスのマスクとして適用します。

moonMask = new Shape(); 
moonMask.graphics.beginFill(0); 
moonMask.graphics.drawCircle(0, 0, radius); 
this.addChild(moonMask); 
this.mask = moonMask;

MoonSphere は Sprite クラスに基づく表示オブジェクトなので、継承した mask プロパティを使用して、マスクを MoonSphere インスタンスに直接適用できます。

円形のマスクを使用して写真の一部を非表示にするだけでは、回転する球体のリアルな効果を作成するには不十分です。月面写真の撮影方法により、写真のサイズは実際のサイズに比例していません。イメージの上下に近い部分は、赤道部分よりも大きく歪み、引き伸ばされています。月面写真の外観を歪ませて、3 次元に見えるようにするために、置き換えマップフィルターを使用します。

置き換えマップフィルターは、イメージを歪ませるために使用するフィルターです。 この場合は、中央を変化させずにイメージの上下を水平方向に押しつぶし、よりリアルに見せるように月面写真を「歪ませ」ます。フィルターが写真の正方形部分に作用するものと仮定すると、中央ではなく上下を押しつぶすことにより、正方形は円に変わります。 この歪ませたイメージのアニメーション化によって発生する副作用として、イメージの中央が、上下に近い領域よりも、実際のピクセル距離にして大きく動くように見えます。この結果、円が実際に 3 次元オブジェクト(球体)のように見えます。

次のコードを使用して、 displaceFilter という名前の置き換えマップフィルターを作成します。

var displaceFilter:DisplacementMapFilter; 
displaceFilter = new DisplacementMapFilter(fisheyeLens, 
                                new Point(radius, 0),  
                                BitmapDataChannel.RED, 
                                BitmapDataChannel.GREEN, 
                                radius, 0);

最初のパラメーター fisheyeLens は、マップイメージと呼ばれます。ここでは、プログラムにより作成される BitmapData オブジェクトです。そのイメージの作成については、 ピクセル値の設定によるビットマップイメージの作成 で説明します。他のパラメーターは、フィルターイメージ内のフィルターを適用する部分、置き換え効果を制御するために使用するカラーチャンネル、置き換えに影響を与える範囲などを記述します。 作成された置き換えマップフィルターは、 imageLoadComplete() メソッドの内部で sphere に適用されます。

sphere.filters = [displaceFilter];

マスクと置き換えマップフィルターが適用された最終的なイメージは次のようになります。

回転する月のアニメーションの 1 サイクルごとに、球体の BitmapData コンテンツがソースイメージデータの新しいスナップショットによって上書きされます。 ただし、フィルターを毎回適用し直す必要はありません。 これは、フィルターがビットマップデータ(未処理のピクセル情報)ではなく、Bitmap インスタンス(表示オブジェクト)に適用されるからです。 Bitmap インスタンスは実際のビットマップデータではなく、ビットマップデータを画面に表示する表示オブジェクトです。 例えば、Bitmap インスタンスは、写真のスライドを画面に表示するために使用するスライドプロジェクターのようなもので、BitmapData オブジェクトは、スライドプロジェクターを通じて表示できる実際の写真スライドのようなものです。 フィルターは BitmapData オブジェクトに直接適用できます。これは、写真のスライドに直接描画して、イメージを変更するようなものです。 フィルターは、Bitmap インスタンスを含む任意の表示オブジェクトにも適用できます。これは、スライドプロジェクターのレンズの前にフィルターを置き、元のスライドは変更しないまま、画面上に表示される出力を歪ませることと似ています。未処理のビットマップデータには Bitmap インスタンスの bitmapData プロパティを通じてアクセスできるので、フィルターを未処理のビットマップデータに直接適用できます。ただし、この場合は、ビットマップデータよりも Bitmap 表示オブジェクトにフィルターを適用することをお勧めします。

ActionScript での置き換えマップフィルターの使用について詳しくは、 表示オブジェクトのフィルター処理 を参照してください。

ピクセル値の設定によるビットマップイメージの作成

置き換えマップフィルターの重要な特性の 1 つに、このフィルターが実際には 2 つのイメージを必要とする点があります。 1 つのイメージ、つまりソースイメージは、フィルターによって実際に変更されるイメージです。 このサンプルでは、ソースイメージは、 sphere という名前の Bitmap インスタンスです。フィルターによって使用されるもう 1 つのイメージはマップイメージです。 マップイメージは、実際には画面に表示されません。 代わりに、その各ピクセルのカラーが置き換え関数への入力として使用され、マップイメージの特定の x, y 座標のピクセルのカラーによって、ソースイメージのその x, y 座標のピクセルに適用される置き換え(位置の物理的な移動)の量が決まります。

したがって、置き換えマップフィルターを使用して球体の効果を作成するには、サンプルに適切なマップイメージが必要です。つまり、ここに示すように、背景が灰色で、暗から明に水平方向に変化する 1 つのカラー(赤)のグラデーションで塗りつぶされた円を含むマップイメージです。

このサンプルでは 1 つのマップイメージとフィルターだけが使用されているので、マップイメージは、 imageLoadComplete() メソッドで(つまり、外部イメージの読み込みが完了したときに)1 回だけ作成されます。 fisheyeLens という名前のマップイメージは、MoonSphere クラスの createFisheyeMap() メソッドを呼び出すことにより作成されます。

var fisheyeLens:BitmapData = createFisheyeMap(radius);

createFisheyeMap() メソッドの内部で、実際のマップイメージは BitmapData クラスの setPixel() メソッドを使用して、一度に 1 ピクセルずつ描画されます。次に createFisheyeMap() メソッドの終了コードを示し、その動作について順を追って説明します。

private function createFisheyeMap(radius:int):BitmapData 
{ 
    var diameter:int = 2 * radius; 
     
    var result:BitmapData = new BitmapData(diameter, 
                                        diameter, 
                                        false, 
                                        0x808080); 
     
    // Loop through the pixels in the image one by one 
    for (var i:int = 0; i < diameter; i++) 
    { 
        for (var j:int = 0; j < diameter; j++) 
        { 
            // Calculate the x and y distances of this pixel from 
            // the center of the circle (as a percentage of the radius). 
            var pctX:Number = (i - radius) / radius; 
            var pctY:Number = (j - radius) / radius; 
             
            // Calculate the linear distance of this pixel from 
            // the center of the circle (as a percentage of the radius). 
            var pctDistance:Number = Math.sqrt(pctX * pctX + pctY * pctY); 
             
            // If the current pixel is inside the circle, 
            // set its color. 
            if (pctDistance < 1) 
            { 
                // Calculate the appropriate color depending on the 
                // distance of this pixel from the center of the circle. 
                var red:int; 
                var green:int; 
                var blue:int; 
                var rgb:uint; 
                red = 128 * (1 + 0.75 * pctX * pctX * pctX / (1 - pctY * pctY)); 
                green = 0; 
                blue = 0; 
                rgb = (red << 16 | green << 8 | blue); 
                // Set the pixel to the calculated color. 
                result.setPixel(i, j, rgb); 
            } 
        } 
    } 
    return result; 
}

最初にメソッドが呼び出されると、そのメソッドは、作成する円形イメージの半径を示すパラメーター radius を受け取ります。次に、その上に円を描画する BitmapData オブジェクトが作成されます。 result という名前のそのオブジェクトは、最後にはメソッドの戻り値として返されます。次のコードスニペットに示すように、 result BitmapData インスタンスは、円の直径と同じ幅と高さで作成され、3 番目のパラメーターが false なので透明度がなく、カラー 0x808080 (中間の灰色)であらかじめ塗りつぶされています。

var result:BitmapData = new BitmapData(diameter, 
                                    diameter, 
                                    false, 
                                    0x808080);

次に、コードは 2 つのループを使用し、イメージの各ピクセルに対して繰り返し処理を実行します。 外側のループはイメージの各列を左から右に移動し(現在処理中のピクセルの水平位置を表す変数 i を使用します)、内側のループは現在の列の各ピクセルを上から下に移動します(現在のピクセルの垂直位置を表す変数 j を使用します)。次に、ループのコード(内側のループの内容は省略)を示します。

for (var i:int = 0; i < diameter; i++) 
{ 
    for (var j:int = 0; j < diameter; j++) 
    { 
        ... 
    } 
}

ループがピクセルを 1 つずつ進むと、各ピクセルで値(マップイメージでのそのピクセルのカラー値)が計算されます。 このプロセスには次の 4 つ手順が含まれます。

  1. コードが、円の中心から現在のピクセルまでの距離を x 軸に沿って計算します( i - radius )。この値を半径で割り、絶対距離ではなく、半径のパーセンテージにします( (i - radius) / radius )。次のコードに示すように、そのパーセンテージ値が pctX という名前の変数に格納され、y 軸の等価の値が計算されて、変数 pctY に格納されます。

    var pctX:Number = (i - radius) / radius; 
    var pctY:Number = (j - radius) / radius;
  2. 標準的な三角関数の公式、ピタゴラスの定義を使用して、 pctX pctY から円の中心と現在のポイントとの直線距離を計算します。次に示すように、この値が pctDistance という名前の変数に格納されます。

    var pctDistance:Number = Math.sqrt(pctX * pctX + pctY * pctY);
  3. 次に、コードは、距離のパーセンテージが 1 よりも小さいかどうか(半径の 100 %。つまり、対象のピクセルが円の半径の内部にあるかどうか)をチェックします。 ピクセルが円の内部にある場合は、計算されたカラー値がそのピクセルに割り当てられます(ここでは省略しますが、手順 4 で説明します)。内部にない場合は、そのピクセルについては何も実行されないので、そのカラーは、デフォルト値である中間の灰色のままになります。

    if (pctDistance < 1) 
    { 
        ... 
    }
  4. 円の内部にあるピクセルでは、そのピクセルについてカラー値が計算されます。 最終的なカラーは、円の左端の黒(赤 0 %)から円の右端の鮮明な赤(100 %)までの赤の明暗になります。 次に示すように、カラー値は、最初は 3 つの部分(赤、緑、青)で計算されます。

    red = 128 * (1 + 0.75 * pctX * pctX * pctX / (1 - pctY * pctY)); 
    green = 0; 
    blue = 0;

    実際には、カラーの赤の部分(変数 red )だけに値があります。わかりやすいように、ここでは緑と青の値(変数 green および blue )を示していますが、省略することもできます。このメソッドの目的は、赤のグラデーションが含まれる円を作成することなので、緑または青の値は不要です。

    3 つの個々のカラー値を決定したら、次のコードに示す標準のビットシフトアルゴリズムを使用して、これらの値を 1 つの整数のカラー値に結合できます。

    rgb = (red << 16 | green << 8 | blue);

    最後にカラー値を計算し、ここに示すように result BitmapData オブジェクトの setPixel() メソッドを使用して、その値を現在のピクセルに実際に割り当てます。

    result.setPixel(i, j, rgb);