AS3Graphics#4 perlinNoiseで遊ぶ

AS3でグラフィカルプログラミング改め、AS3Graphicsです。今回からサブタイトルを付けました。
今回は、BitmapDataクラスのperlinNoiseメソッドを使ってみます。

perlinNoise

perlinNoiseは、Perlinノイズイメージを生成するメソッドです。Perlinノイズイメージは、単純なランダムノイズと違って、近接したピクセルの値の変化が小さくなるように調整されます。また、引数の指定によって、上下のエッジと左右のエッジのピクセル値も連続的にすることができます。では、生成されるPerlinノイズイメージを見てみましょう。

//Perlinノイズイメージを作成します。
bmd = new BitmapData(200, 150);
var seed:uint = 1000 * Math.random()
var octaves:int = 3;
bmd.perlinNoise(200, 150, octaves, seed, true, true);

//PerlinノイズイメージのBitmapを作成し、4つ並べて表示します。
var bm:Bitmap;
bm = new Bitmap(bmd);
addChild(bm);

bm = new Bitmap(bmd);
bm.x = 200;
addChild(bm);

bm = new Bitmap(bmd);
bm.y = 150;
addChild(bm);

bm = new Bitmap(bmd);
bm.x = 200;
bm.y = 150;
addChild(bm);

2x2で4枚並んだPerlinノイズイメージのそれぞれの境界線が滑らかになっているのが分かります。

何に使える?

perlinNoiseを使うと、滑らかに変化するランダムなイメージを作成できることが分かりました。では、このイメージを何に使えるのか考えてみます。Perlinノイズイメージをあくまで画像としてしか見ない場合、あまり応用の余地はありません。せいぜい面白い模様が作れる程度だと思います。今回はもう少し踏み込んで、ピクセルのRGB値に着目します。つまり、Perlinノイズイメージを、滑らかに変化するランダムな数値の配列、と考えます。この値を座標値に変換して曲線を描いてみます。

package
{
	import flash.display.BitmapData;
	import flash.display.BitmapDataChannel;
	import flash.display.GradientType;
	import flash.display.Graphics;
	import flash.display.Shape;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.geom.Matrix;

	/**
	* perlinNoiseの応用サンプルです。
	* @author ishigami.shintaro
	*/
	public class PerlinNoiseTest extends Sprite
	{

		/** 生成するperlinNoiseの幅です。 */
		private static const NOISE_WIDTH:int = 600;

		/** 生成するperlinNoiseの高さです。 */
		private static const NOISE_HEIGHT:int = 600;

		/** 描画領域の幅です。 */
		private static const GRADIENT_WIDTH:int = 600;

		/** 描画領域の高さです。 */
		private static const GRADIENT_HEIGHT:int = 300;

		/** perlinNoiseの1ピクセルを何ピクセルの幅の線で表すかを示す値です。 */
		private static const STEP:int = 4;

		/** perlinNoise上の1フレームあたりの横移動量です。 */
		private static const X_SPEED:Number = 0.2;

		/** perlinNoise上の1フレームあたりの縦移動量です。 */
		private static const Y_SPEED:Number = 2;

		/** 赤のピクセル値を取得する際のx座標のオフセット値です。 */
		private static const OFFSET_RED:int = 0;

		/** 緑のピクセル値を取得する際のx座標のオフセット値です。 */
		private static const OFFSET_GREEN:int = 200;

		/** 青のピクセル値を取得する際のx座標のオフセット値です。 */
		private static const OFFSET_BLUE:int = 400;

		/** ピクセル値をy座標値に変換する際の比率です。 */
		private static const Y_RATIO:Number = 1.2;

		/** ピクセル値をy座標値に変換する際のオフセットです。 */
		private static const Y_OFFSET:Number = 0;

		/** 赤のグラデーション色です。 */
		private static const GRADIENT_COLOR_RED:Array = [0xFF9999, 0];

		/** 緑のグラデーション色です。 */
		private static const GRADIENT_COLOR_GREEN:Array = [0x99FF99, 0];

		/** 青のグラデーション色です。 */
		private static const GRADIENT_COLOR_BLUE:Array = [0x9999FF, 0];

		/** グラデーションアルファです。(三色共通) */
		private static const GRADIENT_ALPHA:Array = [1, 0];

		/** グラデーション比率です。(三色共通) */
		private static const GRADIENT_RATIO:Array = [0, 255];

		/**
		* コンストラクタです。
		*/
		public function PerlinNoiseTest()
		{
			stage.scaleMode = "noScale";
			shape = addChild(new Shape()) as Shape;

			//Perlinノイズイメージを作成します。
			bmd = new BitmapData(NOISE_WIDTH, NOISE_HEIGHT);
			var seed:uint = 1000 * Math.random();
			var octaves:int = 3;
			bmd.perlinNoise(NOISE_WIDTH, NOISE_HEIGHT, octaves, seed, true, true, BitmapDataChannel.RED | BitmapDataChannel.GREEN | BitmapDataChannel.BLUE);

			//背景を描画します。
			matrix = new Matrix();
			matrix.createGradientBox(GRADIENT_WIDTH, GRADIENT_HEIGHT, Math.PI / 2);
			graphics.beginGradientFill(GradientType.LINEAR, [0x333333, 0], [1, 1], [0, 255], matrix);
			graphics.drawRect(0, 0, GRADIENT_WIDTH, GRADIENT_HEIGHT);
			graphics.endFill();

			addEventListener(Event.ENTER_FRAME, enterFrameHandler);
		}

		/** 描画用のShapeです。 */
		private var shape:Shape;

		/** perlinNoise保持用のBitmapDataです。 */
		private var bmd:BitmapData;

		/** perlinNoise上での現在のX座標です。 */
		private var currentX:Number = 0;

		/** perlinNoise上での現在のY座標です。 */
		private var currentY:Number = 0;

		/** グラデーションのMatrixです。 */
		private var matrix:Matrix;

		/**
		* enterFrameイベントハンドラです。
		* @param event
		*/
		protected function enterFrameHandler(event:Event):void
		{
			draw();
		}

		/**
		* 描画を行います。
		*/
		private function draw():void
		{
			var g:Graphics = shape.graphics;
			g.clear();

			//赤の波を描画します。
			drawWave(GRADIENT_COLOR_RED, OFFSET_RED, 0xFF0000, 16);
			//緑の波を描画します。
			drawWave(GRADIENT_COLOR_GREEN, OFFSET_GREEN, 0x00FF00, 8);
			//青の波を描画します。
			drawWave(GRADIENT_COLOR_BLUE, OFFSET_BLUE, 0x0000FF);

			//xの位置を進めます。
			currentX += X_SPEED;
			if (bmd.width <= currentX)
			{
				currentX -= bmd.width;
			}

			//yの位置を進めます。
			currentY += Y_SPEED;
			if (bmd.height <= currentY)
			{
				currentY -= bmd.height;
			}
		}

		/**
		* 波を描画します。
		* @param colors    グラデーションの色です。
		* @param offset    ピクセル値を取得する際のx座標のオフセット値です。
		* @param mask      取得したピクセル値との論理積を求める値です。
		* @param shift     取得したピクセル値を右シフトする桁数です。
		*
		*/
		private function drawWave(colors:Array, offset:int, mask:int, shift:int = 0):void
		{
			var g:Graphics = shape.graphics;
			//グラデーション描画を開始します。
			g.beginGradientFill(GradientType.LINEAR, colors, GRADIENT_ALPHA, GRADIENT_RATIO, matrix);
			//左下へ移動します。
			g.moveTo(0, GRADIENT_HEIGHT);
			for (var i:uint = 0; i <= GRADIENT_WIDTH / STEP; i++)
			{
				//perlinNoise上のx座標を求めます。
				var pixelX:int = i + currentX + offset;
				pixelX = NOISE_WIDTH <= pixelX ? pixelX % NOISE_WIDTH : pixelX;
				//ピクセル値から算出した位置へ線を描きます。
				var pixel:uint = bmd.getPixel(pixelX, currentY);
				var lineY:Number = ((pixel & mask) >> shift) * Y_RATIO + Y_OFFSET;
				g.lineTo(i * STEP, lineY);
			}
			//右下へ線を描きます。
			g.lineTo(GRADIENT_WIDTH, GRADIENT_HEIGHT);
			g.endFill();
		}
	}
}

実行結果

[SWF]http://public-blog-dev.s3.amazonaws.com/wp-content/uploads/2011/08/PerlinNoiseTest.swf, 600, 300[/SWF]

このサンプルを見るにはFlash Playerがインストールされている必要があります。

解説

条件分岐の回数を極力落としてパフォーマンス重視で組んだので、若干読みにくいコードになってしまいましたが、解説します。RGBの3チャンネルで生成したPerlinノイズイメージのピクセル値から、赤と緑と青の各チャンネルの値を取り出してy座標値とし、それぞれのチャンネルに対応した色で曲線を描いています。フレームごとに、ピクセルを取得するPerlinノイズイメージ上の開始座標値を変化させ、ゆるやかに流れながら波打っているような動きを表現しています。