11 September 2008
ページ ツール |
上級
Webだからこそ体験できるコンテンツは何か? それを突き詰めた結果生まれたのが「Firework.jp」です。ユーザー自身が花火をデザインし、自分だけの花火大会を演出する。現実の世界では、とうてい実現できない話も、Webだからこそ実現できる、そんな思いからこの企画が採用されました。
Firework.jpでは、まず「花火玉を作る」画面(図1)で自分だけの花火をデザインします。カラーピッカーから好きな色を選択し、花火玉の上でドラッグしながら「火薬」に色を塗っていきます。デザインしたら、「出来上がりを確認」ボタンをクリックして、花火の打ち上がり具合を確認し、花火玉を保存します。そして、保存した花火玉で自分だけの花火大会を作ったり、みんなの花火大会にエントリーすることができます。
花火玉をデザインした後は、画面左下の「あなたの花火大会を作る」メニューから自分の花火玉と好きな会場を選び、「出来上がりを確認」ボタンをクリックしてエントリー(保存)すると、自分だけの花火大会へ登録することができます(図2)。他にも、みんなの花火大会に参加すると、いろんな人が作った花火と共演することができます。
花火を作ったり、登録したり、打ち上げたりと、自分で好きなように花火大会を演出することができるのがFirework.jpの醍醐味です。それでは、どのようにして自分でデザインした花火を打ち上げているのか、その考え方と構造を解説していきます。
ユーザーは「花火玉を作る」画面で花火玉の各「火薬」に色を塗ります。これを元に花火を再現していくわけですが、まず最初の工程として、火薬の色情報を配列として収集します。この時、取得する配列内の順番は火薬の位置によって決められます。
「花火玉を作る」画面にある花火玉の火薬は、スクリプトで生成・配置されており、中心から徐々に半径を拡張するように円を描きながら火薬の位置を決めています。このスクリプトでは、最初の円周で6つの火薬を等幅に配置し、半径を5つずつ増やしながら円を拡張し、次の円周で12個、18個、24個…と火薬の数を6ずつ増やしていきます。このように生成された火薬(MovieClip)の順番に沿って配列へ追加しています(図3)。
以下は、花火玉を生成するスクリプトです。
// HALF_SIZE : 花火玉全体の半径
// DEFAULT_COLOR : 塗りがないときの星の色
// selected_color : 選択中のRGBカラー
private function starCreate():void {
var renge_length:uint = 5;
var renge_margin:Number = ( HALF_SIZE - 10 ) * 2 / ( renge_length * 2 - 1 );
var renge:Number;
var angle_length:uint;
var angle_margin:Number;
var angle:Number;
var star_mc:Sprite;
var rad:Number;
var half:Number = renge_margin / 2;
var color:uint = DEFAULT_COLOR;
renge = 0;
// 円周を増幅
for ( var i:int = 0; i < renge_length; i++ ) {
angle_length = ( i == 0 ) ? 1 : 6 * i;
angle_margin = 360 / angle_length;
angle = 90;
// 円周の玉数を増幅
for ( var k:int = 0; k < angle_length; k++ ) {
// 花火の星を作る
rad = angle * PI;
star_mc = new Sprite();
star_mc.graphics.beginFill( color );
star_mc.graphics.drawCircle( 0 , 0 , half );
star_mc.graphics.endFill();
star_mc.x = Math.cos( rad ) * renge;
star_mc.y = Math.sin( rad ) * renge;
star_mc.buttonMode = true;
star_mc.addEventListener( MouseEvent.MOUSE_DOWN , starClick );
star_mc.addEventListener( MouseEvent.ROLL_OVER , starOver );
star_mc.addEventListener( MouseEvent.ROLL_OUT , starOut );
container_mc.addChild( star_mc );
// 配列に追加
star_arr.push( star_mc );
// 角度を足す
angle += angle_margin;
}
// 半径を足す
renge += renge_margin;
}
// ボタンオーバーのときのライン
overline_mc = new Sprite();
overline_mc.graphics.lineStyle( 2 , 0x999999 );
overline_mc.graphics.drawCircle( 0 , 0 , half );
overline_mc.mouseEnabled = false;
}
// ボタンクリック
private function starClick( event:MouseEvent ):void {
var target:Sprite = event.target as Sprite;
setStarColor( target , selected_color );
}
// ボタンオーバー - ドラッグしながら色を塗る処理
private function starOver( event:MouseEvent ):void {
var target:Sprite = event.target as Sprite;
if ( event.buttonDown ) {
setStarColor( target , selected_color );
}
overline_mc.x = target.x + HALF_SIZE;
overline_mc.y = target.y + HALF_SIZE;
addChild( overline_mc );
}
// ボタンアウト
private function starOut( event:MouseEvent ):void {
if ( contains( overline_mc ) ) removeChild( overline_mc );
}
// 星にカラーを適応
private function setStarColor( star_mc:Sprite , color:String ):void {
var color_trans:ColorTransform = new ColorTransform();
color_trans.color = ( color != null ) ? Number( "0x" + color ) : DEFAULT_COLOR;
star_mc.transform.colorTransform = color_trans;
}
また、getter/setterメソッドによって、図4のように61個の色情報が入出力されます。
getter/setterメソッドとは、プロパティの値を設定したり返したりするための専用メソッドで、クラス内にはメソッドとして定義しますが、クラスの外側からはプロパティとしてアクセスすることができます。
この場合のサンプルでは、「data」というgetter/setteメソッドを定義しています。dataプロパティを読み取ると、色情報の配列を返し、逆に色情報の配列をdataプロパティに指定すると花火玉の火薬の色が適応されます。
以下は、花火玉の配列を入出力するスクリプトです。
// color_arr : カラー情報の配列
// 入力
public function set datas( color_arr:* ):void {
var len:int = star_arr.length;
var star_mc:Sprite;
var color_str:String;
var data:Object;
for ( var i:int = 0; i < len; i++ ) {
star_mc = star_arr[ i ];
if ( color_arr != null ) {
data = color_arr[ i ];
color_str = data.color;
setStarColor( star_mc , color_str );
} else {
setStarColor( star_mc , null );
}
}
}
// 出力
public function get datas():Array {
var color_arr:Array = new Array();
var len:int = star_arr.length;
var star_mc:Sprite;
var color:uint;
var color_str:String;
var data:Object;
for ( var i:int = 0; i < len; i++ ) {
data = new Object();
star_mc = star_arr[ i ];
color = star_mc.transform.colorTransform.color;
color_str = ( color != DEFAULT_COLOR ) ? ( "000000" + color.toString( 16 ) ).substr( -6 , 6 ) : null;
data.color = color_str;
color_arr[ i ] = data;
}
return color_arr;
}
花火玉から色情報の配列を受け取ったら、花火の打ち上がりを演出するための「粒子」データ(位置、移動距離、持続エネルギー)へと変換していきます。
色は、物理エンジンによって描画されるため、数値化して取得します。
位置は、火花のxとyの座標になりますが、初期設定として花火が拡散する時の中心の位置を指定します。花火の中心位置は打ち上がるときのタイミングによって変わってくるので、値を任意に変更できるようにメソッドの引数として設定できるようにしておきます。
移動距離は、配列の順番によってルール付けされています。その情報から粒子の速さ(半径の長さ)と方向(角度)を取得し、取得された半径と角度からx軸方向とy軸方向 の移動距離を計算していきます(図5)。
色情報から粒子データへ拡張する内容をまとめると、図7のようになります。
以下は、色情報を粒子データに変換するスクリプトです。
// color_arr : カラー情報の配列
// x : パーティクルのX座標
// y : パーティクルのY座標
// n : 星データからパーティクルに拡張する配列量の倍率
// radius : 拡散する距離の基準値
static public function encode( color_arr:Array = null , x:Number = 0 , y:Number = 0 , n:uint = 1 , radius:Number = 14 ):Array {
if ( color_arr == null ) {
return null;
}
var renge_length:uint = 5;
var renge_margin:Number = radius / ( renge_length * 2 - 1 );
var renge:Number;
var angle_length:uint;
var angle_margin:Number;
var angle:Number;
// カラー情報
var star_obj:Object;
var id:int = 0;
// 火花データ
var particle_arr:Array;
var particle_obj:Object;
var p_angle:Number;
var p_renge:Number;
var rad:Number;
particle_arr = new Array();
renge = 0;
// 円周を増幅
for ( var i:int = 0; i < renge_length; i++ ) {
angle_length = ( i == 0 ) ? 1 : 6 * i;
angle_margin = 360 / angle_length;
angle = 90;
// 円周の玉数を増幅
for ( var k:int = 0; k < angle_length; k++ ) {
// カラー情報の配列からオブジェクトを取得
star_obj = color_arr[ id ];
if ( star_obj.color != null ) {
// 火花データを拡張
particle_obj = new Object();
p_angle = angle + Math.random() * ( angle_margin || 0 );
p_renge = renge + Math.random() * ( renge_margin || 0 );
rad = p_angle * PI;
particle_obj.x = x;
particle_obj.y = y;
particle_obj.vx = Math.cos( rad ) * p_renge;
particle_obj.vy = Math.sin( rad ) * p_renge;
particle_obj.energy = 5 + 10 * Math.random();
particle_obj.color = Number( "0x" + star_obj.color );
if ( star_obj.bright ) particle_obj.bright = star_obj.bright || 3;
if ( star_obj.graviton ) particle_obj.graviton = star_obj.graviton || 0.05;
particle_arr.push( particle_obj );
}
id++;
// 角度を足す
angle += angle_margin;
}
// 半径を足す
renge += renge_margin;
}
return particle_arr;
}
実際の花火を見ると、発火して火花が発光した後、時間と共に爆発エネルギーが減衰し、やがて空気中で消えていくという特徴があります。この花火の動きをシミュレーションするために、座標に移動距離を加算しながら、移動距離の値にエネルギー消費量を乗算、y軸方向の移動距離に重力の値を加算するという物理エンジンを生成しました。これを図にしたのが、図8です。火花の移動距離にエネルギー消費量(ここでは0.95)を乗算することで、徐々に花火(粒子)が拡散する勢いを減らしています。
このとき、花火玉の火薬から取得した色と位置を元に、粒子データの位置・移動距離・持続エネルギー・色情報を生成しているため、ユーザーが意図した通りの花火の形で演出することができるのです。
また、拡散した花火の粒子データを描画する処理ですが、粒子データごとにMovieClipインスタンスを生成する方法だと非常に処理が重くなるので、1つのインスタンスの中にそれぞれ点描画するという方法を採用しています。
火花(粒子)が夜空で消えるときの余韻を演出するために、別のMovieClipインスタンスに火花の残像を表現するためのビットマップデータ作成し、さらに時間によって徐々に残像を消し込むために半透明の黒いビットマップデータを作成し、それを重ね塗りするようにします。
// width : ステージの横サイズ
// height : ステージの縦サイズ
// display_mc : 火花の残像を表現するためのインスタンス
display_bmd = new BitmapData( width , height , true, 0x00000000 );
aftereffect_bmd = new BitmapData( width , height, true, 0x1D000000 );
display_mc.addChild( new Bitmap( display_bmd ) );
また、花火のぼかした感じを表現するために、描画用のインスタンスにグローフィルタを適応し、明度を若干上げています。
// draw_mc : 花火の粒子を点描画するためのインスタンス
// フィルター作成
var f_color:Number = 0xFFFFFF;
var f_alpha:Number = 0.8;
var f_blurX:Number = 30;
var f_blurY:Number = 30;
var f_strength:Number = 1;
var f_quality:Number = 1;
var f_inner:Boolean = false;
var f_knockout:Boolean = false;
var filters:GlowFilter = new GlowFilter( f_color , f_alpha , f_blurX , f_blurY , f_strength , f_quality );
draw_mc.filters = [ filters ];
// 明度を上げる
( new Transform( draw_mc ) ).colorTransform = new ColorTransform( 0.5 , 0.5 , 0.5 , 1 , 128 , 128 , 128 , 0 );
火花を描画するとき、毎フレームごとに粒子データから取得した移動距離の値によって座標を計算します。その計算された情報に沿って描画用のMovieClip(draw_mc)に点描画していきます。
更に、毎回 点描画される度に 残像用のビットマップデータ(display_bmd)に重ねて描画しつつ、半透明の黒いビットマップデータ(aftereffect_bmd)を重ね塗りすることによって火花を徐々に消していきます。この黒いビットマップデータ(aftereffect_bmd)の透明度を変更することによって、火花が消える間隔を遅めたり速めたりできます。
このとき、背景画像レイヤー(back_mc)を最背面に置いてしまうと一つの問題が生じます。それは、残像用のビットマップデータ(diplay_bmd)に半透明の黒いビットマップデータ(aftereffect_bmd)を重ね塗りするのですが、全体を徐々に真っ黒にするため、下にあるレイヤーが見えなくなってしまいます。この問題を解決するために、背景画像レイヤー(back_mc)を一番上に置いてMovieClipのblendModeプロパティをBlendMode.SCREENに指定します。そうすることによって、表示オブジェクトの色の補数 (逆)と背景色の補数が乗算されるため、表示オブジェクトの黒い領域が削除されるので、その下にあるグラフィック(花火)が表示されるようになります(図9)。
以下は、粒子の演算に関するスクリプトです。
// ENERGY_CONSUMED : エネルギー消費量(0.95)
// draw_mc : 点描画するステージ
// display_bmd :残像用のビットマップデータ
// aftereffect_bmd :半透明の黒いビットマップデータ
public function draw():void {
var particle:Object;
draw_mc.graphics.clear();
draw_mc.graphics.lineStyle( 3 , 0xFFFFFF , 50 );
for ( var i:int = 0; i < particle_arr.length; i++ ) {
// 粒子データ
particle = particle_arr[ i ];
// 一定以下のエネルギー量をもつ粒子データを削除する
if ( particle.energy < 0.1 ) {
particle_arr.splice( i , 1 );
continue;
}
// 粒子データの重力値と明度
var graviton:Number = ( particle.graviton != undefined ) ? particle.graviton : 0.05;
var bright:int = ( particle.bright != undefined ) ? particle.bright : 3;
with ( particle ) {
x += vx;
y += vy;
//時間が経つごとに粒子のエネルギーを減少させる
energy *= ENERGY_CONSUMED;
vx *= ENERGY_CONSUMED;
vy *= ENERGY_CONSUMED;
//重力
vy += graviton;
// カラーを適応する
draw_mc.graphics.lineStyle( bright , color || 0xFFFFFF , 50 );
}
// 粒子データを点描画する
draw_mc.graphics.moveTo( particle.x , particle.y );
draw_mc.graphics.lineTo( particle.x+0.5 , particle.y+0.5 );
}
//余韻を表現する
display_bmd.draw( aftereffect_bmd );
display_bmd.draw( draw_mc );
}
ActionScript 3.0で開発を採用する理由として、このときの粒子の点描画処理を快適に表現するという意図が前提にあります。
「花火玉を作る」画面で生成された火薬の情報は、自分の花火としてサーバに登録できます。そのとき、大量の花火玉データをサーバと転送する際、XMLでデータを受け取るよりもJSON形式にしたほうがデータ情報量は減ると考えました。また、配列からJSON形式のデータへ変換するときに、ActionScript 3.0用ライブラリである「as3corelib」のJSONクラスを利用しています。
JSONクラスを活用し、encodeで文字列にしたものを繰り返しdecodeすることによって、元のオブジェクトの変更せずに花火情報を複製することができます。これを利用して、スターマインで繰り返し花火を打ち上げる処理を表現しています。
以上で、Firework.jpの仕組みの解説は終了です。
Firework.jpは、2009年4月31日まで開催しています。
夏の思い出に浸るもよし、作品を眺めて癒されるもよし。
自分だけの夜空を、あなたのセンスで彩ってみてください。