Beautifl - Flash Gallery

Thumbnail : PS2 Firework (Particles and Motion blur)
PS2 Firework (Particles and Motion blur)
k0rin 2009-08-27 MIT License

再生するにはFlash Playerが必要です。デスクトップのブラウザでご覧ください。

package {
	import flash.display.*;
	import flash.events.*;
	import flash.filters.*;
	import flash.geom.*;
	import flash.text.*;
	import flash.utils.*;

	[SWF(width="465", height="465", frameRate="40")]
	public class Firework extends Sprite
	{
		// 火花
		private var position:Vector.<Number> = new Vector.<Number>();
		private var velocity:Vector.<Number> = new Vector.<Number>();
		private var diameters:Vector.<Number> = new Vector.<Number>();
		private var sparkNumber:Number = 0;
		
		private var projectedPosition:Vector.<Number> = new Vector.<Number>();
		private var uvts:Vector.<Number> = new Vector.<Number>();
		
		private var sparkBitmapDatas:Vector.<BitmapData> = new Vector.<BitmapData>();
		private var sparkBitmapDataRectangles:Vector.<Rectangle> = new Vector.<Rectangle>();
		private const SPARK_COLOR:uint = 0x664020;
		
		private const GRAVITY:Number = 0.02;
		private const AIR_RESISTANCE:Number = 0.999;
		
		// 1フレームに放出する火花の数
		private const EMIT_SPARK_NUMBER:int = 3;
		
		// 照り返し
		private var reflectionBitmapData:BitmapData;
		private const REFLECTION_COLOR:uint = 0xFF1800;
		private const REFLECTION_RADIUS:Number = 16;
		
		// フレームバッファ
		private var displayBuffer:BitmapData = new BitmapData(stage.stageWidth, stage.stageHeight, false);
		private var splitBuffer:BitmapData = new BitmapData(stage.stageWidth, stage.stageHeight, false);
		private const NUMBER_OF_SPLIT_FRAME:int = 10;
		
		private var perspective:PerspectiveProjection = new PerspectiveProjection();
		private var projectionMatrix:Matrix3D = new Matrix3D();
		
		private var viewRotationY:Number = 0;
		private var viewRotationX:Number = 25;
		
		private const CENTER_X:Number = stage.stageWidth / 2;
		private const CENTER_Y:Number = stage.stageHeight * 0.65;
		
		private var stats:TextField = new TextField();
		
		public function Firework()
		{
			addChild(new Bitmap(displayBuffer));

			stats.x = 5;
			stats.y = 5;
			stats.autoSize = TextFieldAutoSize.LEFT;
			stats.textColor = 0xFFFFFF;
			addChild(stats);
			
			var shape:Shape = new Shape();
			var g:Graphics = shape.graphics;
			
			// 火花のプリレンダリング
			var bitmapData:BitmapData = new BitmapData(1, 1, true, SPARK_COLOR);
			sparkBitmapDatas.push(bitmapData);
			sparkBitmapDataRectangles.push(bitmapData.rect);
			
			for (var diameter:int = 1; diameter <= 32; diameter++) {
				bitmapData = new BitmapData(diameter, diameter, true, 0);
				
				g.clear();
				g.beginFill(SPARK_COLOR);
				var radius:Number = diameter / 2;
				g.drawCircle(radius, radius, radius);
				g.endFill();
				bitmapData.draw(shape);
				
				sparkBitmapDatas.push(bitmapData);
				sparkBitmapDataRectangles.push(bitmapData.rect);
				// bitmapData.rectはアクセサで重いのでキャッシュしておく。これにより30%ほど高速化
			}
			
			// 照り返しのプリレンダリング
			reflectionBitmapData = new BitmapData(REFLECTION_RADIUS * 2, REFLECTION_RADIUS * 2, true, 0);
			g.clear();
			var matrix:Matrix = new Matrix();
			matrix.createGradientBox(REFLECTION_RADIUS * 2, REFLECTION_RADIUS * 2, 0, 0, 0);
			g.beginGradientFill(GradientType.RADIAL, [ REFLECTION_COLOR, REFLECTION_COLOR ], [ 1, 0 ], [ 0, 255 ], matrix);
			g.drawCircle(REFLECTION_RADIUS, REFLECTION_RADIUS, REFLECTION_RADIUS);
			g.endFill();
			reflectionBitmapData.draw(shape);
			
			createFireworkModel();
			
			addEventListener(Event.ENTER_FRAME, enterFrameHandler);
//			stage.addEventListener(MouseEvent.CLICK, function (e:Event):void {
//				emitSpark();
//			});
		}
		
		private function enterFrameHandler(e:Event):void
		{
			displayBuffer.fillRect(displayBuffer.rect, 0);
			
			setupProjectionMatrix();
			
			var startTime:uint = getTimer();
			renderFloor();
			var floorRenderingTime:uint = getTimer() - startTime;
			
			startTime = getTimer();
			for (var i:int = 0; i < NUMBER_OF_SPLIT_FRAME; i++) {
				const FACTOR:Number = 0.06;
				viewRotationY += (getTimer() * 0.01 + (-(CENTER_X - mouseX) * 0.4) - viewRotationY) * FACTOR;
				viewRotationX += ((27 + (CENTER_Y - mouseY) * 0.15) - viewRotationX) * FACTOR;
				
				setupProjectionMatrix();
				
				for (var j:int = 0; j < EMIT_SPARK_NUMBER; j++) {
					emitSpark();
				}
				updateSparks();
				renderSparks();
			}
			var sparksRenderingTime:uint = getTimer() - startTime;
			
			stats.text = 
				"Number of Sparks : " + sparkNumber + "\n" + 
				"Floor Rendering Time : " + floorRenderingTime + " ms\n" + 
				"Sparks Rendering Time : " + sparksRenderingTime + " ms \n";
		}
		
		private function setupProjectionMatrix():void
		{
			perspective.fieldOfView = 60;
			
			projectionMatrix.identity();
			projectionMatrix.appendRotation(viewRotationY, Vector3D.Y_AXIS);
			projectionMatrix.appendRotation(viewRotationX, Vector3D.X_AXIS);
			projectionMatrix.appendTranslation(0, 0, perspective.focalLength);
			projectionMatrix.append(perspective.toMatrix3D());
			
			correctMatrix3DMultiplyBug(projectionMatrix);
		}
		
		private function correctMatrix3DMultiplyBug(matrix:Matrix3D):void
		{
			// see http://bugs.adobe.com/jira/browse/FP-670
			var m1:Matrix3D = new Matrix3D(Vector.<Number>([ 0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 1, 0 ]));
			var m2:Matrix3D = new Matrix3D(Vector.<Number>([ 0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 1,  0, 0, 0, 0 ]));
			m1.append(m2);
			if (m1.rawData[15] == 20) {
				// バグ持ち!
				var rawData:Vector.<Number> = matrix.rawData;
				rawData[15] /= 20;
				matrix.rawData = rawData;
			}
		}
		
		private function createSpark(x:Number, y:Number, z:Number, vx:Number, vy:Number, vz:Number, diameter:Number):void
		{
			position.push(x, y, z);
			velocity.push(vx, vy, vz);
			diameters.push(diameter);
			sparkNumber++;
		}
		
		private function emitSpark():void
		{
			var azimuth:Number = Math.random() * 2 * Math.PI;
			var vy:Number = -(Math.random() * 0.06 + 0.94);
			var vx:Number = Math.sqrt(1 - vy * vy) * Math.cos(azimuth);
			var vz:Number = Math.sqrt(1 - vy * vy) * Math.sin(azimuth);
			
			var speed:Number = Math.random() * 2.3 + 0.7;
			vx *= speed;
			vy *= speed;
			vz *= speed;
			
			var diameter:Number = 3 + Math.random() * 3;
			createSpark(0, -FIREWORK_HEIGHT, 0, vx, vy, vz, diameter);
		}
		
		private function updateSparks():void
		{
			for (var i:int = 0; i < sparkNumber; ) {
				var xIndex:int = i * 3;
				var yIndex:int = xIndex + 1;
				var zIndex:int = xIndex + 2;
				
				velocity[yIndex] += GRAVITY;
				
				velocity[xIndex] *= AIR_RESISTANCE;
				velocity[yIndex] *= AIR_RESISTANCE;
				velocity[zIndex] *= AIR_RESISTANCE;
				
				if (position[yIndex] + velocity[yIndex] > 0) {
					velocity[yIndex] = -velocity[yIndex] * 0.2;
					
					position[xIndex] += velocity[xIndex];
					position[yIndex] += velocity[yIndex];
					position[zIndex] += velocity[zIndex];
					
					// 小さい火花に分割
					const DIAMETER_MIN:Number = 2;
					if (diameters[i] > DIAMETER_MIN) {
						var diameterLimit:Number = diameters[i];
						
						while (1) {
							var diameter:Number = Math.random() * (4 - DIAMETER_MIN) + DIAMETER_MIN;
							diameterLimit -= diameter;
							if (diameterLimit < 0) {
								break;
							}
							var azimuth:Number = Math.random() * 2 * Math.PI;
							var r:Number = Math.random() * 2;
							
							createSpark(position[xIndex], position[yIndex], position[zIndex], 
								(velocity[xIndex] + Math.cos(azimuth) * r) * 0.5, 
								velocity[yIndex], 
								(velocity[zIndex] + Math.sin(azimuth) * r) * 0.5, 
								diameter);
						}
					}
					
					// spliceは重い!
//					position.splice(xIndex, 3);
//					velocity.splice(xIndex, 3); 
//					diameters.splice(i, 1);

					position[xIndex] = position[sparkNumber * 3 - 3];
					position[yIndex] = position[sparkNumber * 3 - 2];
					position[zIndex] = position[sparkNumber * 3 - 1];

					velocity[xIndex] = velocity[sparkNumber * 3 - 3];
					velocity[yIndex] = velocity[sparkNumber * 3 - 2];
					velocity[zIndex] = velocity[sparkNumber * 3 - 1];
					
					diameters[i] = diameters[sparkNumber - 1];
					
					position.pop();
					position.pop();
					position.pop();

					velocity.pop();
					velocity.pop();
					velocity.pop();
					
					diameters.pop();
					
					sparkNumber--;
					continue;
				}
				position[xIndex] += velocity[xIndex];
				position[yIndex] += velocity[yIndex];
				position[zIndex] += velocity[zIndex];
				
				i++;
			}
		}
		
		private function renderSparks():void
		{
			splitBuffer.fillRect(splitBuffer.rect, 0x000000);
			
			Utils3D.projectVectors(projectionMatrix, position, projectedPosition, uvts);
			
			// ループ外に追い出したら30%以上高速化
			var p:Point = new Point();
			var focalLength:Number = perspective.focalLength;
			
			for (var i:int = 0; i < sparkNumber; i++) {
				// パーティクルの大きさ = 1/z * focalLength * pixel
				var diameter:int = uvts[i * 3 + 2] * focalLength * diameters[i] + 0.5;
				// (i * 2) => (i << 1)で10%ほど高速化
				p.x = CENTER_X + projectedPosition[(i << 1)    ] - diameter / 2;
				p.y = CENTER_Y + projectedPosition[(i << 1) + 1] - diameter / 2;
				splitBuffer.copyPixels(sparkBitmapDatas[diameter], sparkBitmapDataRectangles[diameter], p);
			}
			
			displayBuffer.draw(splitBuffer, null, null, BlendMode.ADD);
		}
		
		private const FLOOR_SIZE:Number = 300;
		private const FLOOR_HALFSIZE:Number = FLOOR_SIZE / 2;
		private const FLOOR_VERTICES:Vector.<Number> = Vector.<Number>([
			 FLOOR_HALFSIZE, 0,  FLOOR_HALFSIZE, 
			-FLOOR_HALFSIZE, 0,  FLOOR_HALFSIZE, 
			-FLOOR_HALFSIZE, 0, -FLOOR_HALFSIZE, 
			 FLOOR_HALFSIZE, 0, -FLOOR_HALFSIZE, 
			 
			 FLOOR_HALFSIZE, 0,               0, 
			-FLOOR_HALFSIZE, 0,               0, 
			              0, 0,  FLOOR_HALFSIZE, 
			              0, 0, -FLOOR_HALFSIZE, 
		]);
		private const FLOOR_VERTEX_NUMBER:int = FLOOR_VERTICES.length / 3;
		
		private var floorProjectedVertices:Vector.<Number> = new Vector.<Number>();
		private var floorIndices:Vector.<int> = Vector.<int>([
			0, 1, 2,  2, 3, 0
		]);
		private var floorUvts:Vector.<Number> = Vector.<Number>([
			1, 0, 0,  0, 0, 0,  0, 1, 0,  1, 1, 0,  
			0, 0, 0,  0, 0, 0,  0, 0, 0,  0, 0, 0
		]);
		
		private const TEXTURE_SIZE:int = 150;
		private var floorTexture:BitmapData = new BitmapData(TEXTURE_SIZE, TEXTURE_SIZE, false);
		
		private const WIREFRAME_COLOR:uint = 0x555566;
		
		private var sprite:Sprite = new Sprite();
		
		private function renderFloor():void
		{
			// 照り返しを描画する閾値
			const THRESHOLD_Y:Number = 50;
			
			var matrix:Matrix = new Matrix();
			var colorTransform:ColorTransform = new ColorTransform();
			
			floorTexture.fillRect(floorTexture.rect, 0x180C00);
			
			for (var i:int = 0; i < sparkNumber; i++) {
				var yIndex:int = i * 3 + 1;
				
				if (position[yIndex] > -THRESHOLD_Y) {
					var xIndex:int = i * 3;
					var zIndex:int = i * 3 + 2;
					
					matrix.identity();
					var scale:Number = (0.2 - (position[yIndex] / THRESHOLD_Y) * 0.8) * diameters[i];
					matrix.scale(scale, scale);
					matrix.translate(FLOOR_HALFSIZE + position[xIndex] - REFLECTION_RADIUS * scale, FLOOR_HALFSIZE - position[zIndex] - REFLECTION_RADIUS * scale);
					matrix.scale(TEXTURE_SIZE / FLOOR_SIZE, TEXTURE_SIZE / FLOOR_SIZE);
					
					colorTransform.alphaMultiplier = 1 + position[yIndex] / THRESHOLD_Y;
					floorTexture.draw(reflectionBitmapData, matrix, colorTransform, BlendMode.ADD);
				}
			}
			
			Utils3D.projectVectors(projectionMatrix, FLOOR_VERTICES, floorProjectedVertices, floorUvts);
			for (i = 0; i < FLOOR_VERTEX_NUMBER; i++) {
				floorProjectedVertices[(i << 1)    ] += CENTER_X;
				floorProjectedVertices[(i << 1) + 1] += CENTER_Y;
			}
			
			Utils3D.projectVectors(projectionMatrix, fireworkVertices, fireworkProjectedVertices, fireworkUvts);
			for (i = 0; i < fireworkProjectedVertices.length / 2; i++) {
				fireworkProjectedVertices[(i << 1)    ] += CENTER_X;
				fireworkProjectedVertices[(i << 1) + 1] += CENTER_Y;
			}
			
			var g:Graphics = sprite.graphics;
			g.clear();
			g.beginBitmapFill(floorTexture);
			g.drawTriangles(floorProjectedVertices, floorIndices, floorUvts);
			g.endFill();
			displayBuffer.draw(sprite);
			
			g.clear();
			g.lineStyle(1, WIREFRAME_COLOR);
			g.moveTo(floorProjectedVertices[0], floorProjectedVertices[1]);
			g.lineTo(floorProjectedVertices[2], floorProjectedVertices[3]);
			g.lineTo(floorProjectedVertices[4], floorProjectedVertices[5]);
			g.lineTo(floorProjectedVertices[6], floorProjectedVertices[7]);
			g.lineTo(floorProjectedVertices[0], floorProjectedVertices[1]);
			
			g.moveTo(floorProjectedVertices[8], floorProjectedVertices[9]);
			g.lineTo(floorProjectedVertices[10], floorProjectedVertices[11]);
			g.moveTo(floorProjectedVertices[12], floorProjectedVertices[13]);
			g.lineTo(floorProjectedVertices[14], floorProjectedVertices[15]);
			displayBuffer.draw(sprite, null, null, BlendMode.ADD);
			
			g.clear();
			g.beginFill(FIREWORK_COLOR);
			g.drawTriangles(fireworkProjectedVertices, fireworkIndices);
			g.endFill();
			displayBuffer.draw(sprite);
		}
		
		private const FIREWORK_RADIUS:Number = 8;
		private const FIREWORK_HEIGHT:Number = 40;
		private const FIREWORK_COLOR:uint = 0xAACCFF;
		
		private var fireworkVertices:Vector.<Number> = new Vector.<Number>();
		private var fireworkIndices:Vector.<int> = new Vector.<int>();
		private var fireworkProjectedVertices:Vector.<Number> = new Vector.<Number>();
		private var fireworkUvts:Vector.<Number> = new Vector.<Number>();
		
		private function createFireworkModel():void
		{
			const SEGMENT:int = 8;
			for (var i:int = 0; i < SEGMENT; i++) {
				var x:Number = Math.cos(2 * Math.PI * i / SEGMENT) * FIREWORK_RADIUS;
				var z:Number = Math.sin(2 * Math.PI * i / SEGMENT) * FIREWORK_RADIUS;
				fireworkVertices.push(x, -FIREWORK_HEIGHT, z);
				fireworkVertices.push(x, 0, z);
				
				var i0:int = (i * 2    ) % (SEGMENT * 2);
				var i1:int = (i * 2 + 1) % (SEGMENT * 2);
				var i2:int = (i * 2 + 2) % (SEGMENT * 2);
				var i3:int = (i * 2 + 3) % (SEGMENT * 2);
				fireworkIndices.push(i2, i0, i1);
				fireworkIndices.push(i1, i3, i2);
			}
		}
	}
}