うちの会社も先日9月10日にようやく1周年を迎えました。
近頃このブログも全く更新されていなかったので、これを機に心新たにまた頑張ってネタを考えてみようと思います(^^;
さて、Flex Builder3をずいぶん前に買って、埃をかぶってしまっていたので、これはもったいない・・というわけで。
今回はFlexネタです。
Flexを使って簡単な(初歩の)3Dアニメーションをやってみようと思います。
初歩というのは簡易的なライティング(後述)を行うもので、BitmapDataクラスなどを使えば、よりリアルなレンダリングもできると思います。
上の画像は実際に動作させてみたswfをキャプチャしたものです。
実装手順および解説は以下の通り。
ActionScriptプロジェクトの作成
上の画像のように、ファイル >> 新規 >> ActionScriptプロジェクトと選択してください。
すると、プロジェクト作成ダイアログが出ますので、プロジェクト名を入力して「終了」を押します。
これで、まず土台となるスケルトンが生成されます。
生成されたスケルトンコードはプロジェクト名としてつけた名前と同じクラスがSpriteクラスのサブクラスとして定義されていると思います。Spriteクラスというのは表示リストノードの基本要素で要はグラフィック処理が可能なクラスです(詳しくはリンク先を参照のこと)。
3D幾何学演算クラス
大げさな感じのサブタイトルですが、次は3Dの計算を行う上で必須といえる、ベクトルや行列、プリミティブのクラスを作っていきます。プリミティブというのは描画の単位となる図形と考えてもらえば良いかと思います。今回は三角形のみです。
ベクトルは、3Dなので3次元ベクトルになります。最低限の機能として、
- 内積
- 外積
- 正規化
- 和
- 差
- 複製
くらいは欲しいので、以下のような感じに実装します。
package geom { public class Vector3D { public var x:Number; public var y:Number; public var z:Number; public function Vector3D(x:Number, y:Number, z:Number) { this.x = x; this.y = y; this.z = z; } // 内積 public function dotProduct(vec:Vector3D):Number { return this.x*vec.x + this.y*vec.y + this.z*vec.z; } // 外積 public function crossProduct(vec:Vector3D):Vector3D { var x:Number = this.y*vec.z-this.z*vec.y; var y:Number = this.z*vec.x-this.x*vec.z; var z:Number = this.x*vec.y-this.y*vec.x; return new Vector3D(x,y,z); } // 正規化 public function normalize():void { var length:Number = Math.sqrt(x*x+y*y+z*z); // 零ベクトルの場合は何もしない if(length == 0) return; x /= length; y /= length; z /= length; } // 複製 public function clone():Vector3D { return new Vector3D(x,y,z); } // 和 public function add(v:Vector3D):Vector3D { return new Vector3D(this.x+v.x, this.y+v.y, this.z+v.z); } // 差 public function sub(v:Vector3D):Vector3D { return new Vector3D(this.x-v.x, this.y-v.y, this.z-v.z); } } }
おおざっぱな説明をすると、内積はライティング(局所照明)の計算に、外積は面法線の算出に使います。正規化は計算前にベクトルの長さを単位長で統一しないと結果がおかしくなるので、使っています。
詳しくは3Dプログラミングに詳しい他のサイトに任せます(笑)
行列については、今回はとりあえず回転行列のみを使います。
実際の3Dプログラミングでは、ビュー変換やパースペクティブ変換時にそれぞれ行列を使いますが、今回は手抜きで画面のx,y座標と空間のそれを合わせてしまっているので変換はなしです。パースペクティブの変換もしていないので、遠近感がありません(笑)
行列のコードは以下のようになります。
package geom { import flash.geom.Matrix; public class Matrix4 { private var m00:Number; private var m01:Number; private var m02:Number; private var m03:Number; private var m10:Number; private var m11:Number; private var m12:Number; private var m13:Number; private var m20:Number; private var m21:Number; private var m22:Number; private var m23:Number; private var m30:Number; private var m31:Number; private var m32:Number; private var m33:Number; public function Matrix4( m00:Number=0, m01:Number=0, m02:Number=0, m03:Number=0, m10:Number=0, m11:Number=0, m12:Number=0, m13:Number=0, m20:Number=0, m21:Number=0, m22:Number=0, m23:Number=0, m30:Number=0, m31:Number=0, m32:Number=0, m33:Number=0 ) { set(m00,m01,m02,m03,m10,m11,m12,m13,m20,m21,m22,m23,m30,m31,m32,m33); } public function set( m00:Number, m01:Number, m02:Number, m03:Number, m10:Number, m11:Number, m12:Number, m13:Number, m20:Number, m21:Number, m22:Number, m23:Number, m30:Number, m31:Number, m32:Number, m33:Number ):void { this.m00 = m00; this.m01 = m01; this.m02 = m02; this.m03 = m03; this.m10 = m10; this.m11 = m11; this.m12 = m12; this.m13 = m13; this.m20 = m20; this.m21 = m21; this.m22 = m22; this.m23 = m23; this.m30 = m30; this.m31 = m31; this.m32 = m32; this.m33 = m33; } public function setZero():void { m00 = 0; m01 = 0; m02 = 0; m03 = 0; m10 = 0; m11 = 0; m12 = 0; m13 = 0; m20 = 0; m21 = 0; m22 = 0; m23 = 0; m30 = 0; m31 = 0; m32 = 0; m33 = 0; } public function setIdentity():void { m00 = 1.0; m01 = 0.0; m02 = 0.0; m03 = 0.0; m10 = 0.0; m11 = 1.0; m12 = 0.0; m13 = 0.0; m20 = 0.0; m21 = 0.0; m22 = 1.0; m23 = 0.0; m30 = 0.0; m31 = 0.0; m32 = 0.0; m33 = 1.0; } public function addScalar(scalar:Number):void { m00 += scalar; m01 += scalar; m02 += scalar; m03 += scalar; m10 += scalar; m11 += scalar; m12 += scalar; m13 += scalar; m20 += scalar; m21 += scalar; m22 += scalar; m23 += scalar; m30 += scalar; m31 += scalar; m32 += scalar; m33 += scalar; } public function add(mat:Matrix4):void { m00 += mat.m00; m01 += mat.m01; m02 += mat.m02; m03 += mat.m03; m10 += mat.m10; m11 += mat.m11; m12 += mat.m12; m13 += mat.m13; m20 += mat.m20; m21 += mat.m21; m22 += mat.m22; m23 += mat.m23; m30 += mat.m30; m31 += mat.m31; m32 += mat.m32; m33 += mat.m33; } public function sub(mat:Matrix4):void { m00 -= mat.m00; m01 -= mat.m01; m02 -= mat.m02; m03 -= mat.m03; m10 -= mat.m10; m11 -= mat.m11; m12 -= mat.m12; m13 -= mat.m13; m20 -= mat.m20; m21 -= mat.m21; m22 -= mat.m22; m23 -= mat.m23; m30 -= mat.m30; m31 -= mat.m31; m32 -= mat.m32; m33 -= mat.m33; } public function transpose():void { var tmp:Number = m01; m01 = m10; m10 = tmp; tmp = m02; m02 = m20; m20 = tmp; tmp = m03; m03 = m30; m30 = tmp; tmp = m12; m12 = m21; m21 = tmp; tmp = m13; m13 = m31; m31 = tmp; tmp = m23; m23 = m32; m32 = tmp; } public function rotX(angle:Number):void { var c:Number = Math.cos(angle); var s:Number = Math.sin(angle); m00 = 1.0; m01 = 0.0; m02 = 0.0; m03 = 0.0; m10 = 0.0; m11 = c; m12 = -s; m13 = 0.0; m20 = 0.0; m21 = s; m22 = c; m23 = 0.0; m30 = 0.0; m31 = 0.0; m32 = 0.0; m33 = 1.0; } public function rotY(angle:Number):void { var c:Number = Math.cos(angle); var s:Number = Math.sin(angle); m00 = c; m01 = 0.0; m02 = s; m03 = 0.0; m10 = 0.0; m11 = 1.0; m12 = 0.0; m13 = 0.0; m20 = -s; m21 = 0.0; m22 = c; m23 = 0.0; m30 = 0.0; m31 = 0.0; m32 = 0.0; m33 = 1.0; } public function rotZ(angle:Number):void { var c:Number = Math.cos(angle); var s:Number = Math.sin(angle); m00 = c; m01 = -s; m02 = 0.0; m03 = 0.0; m10 = s; m11 = c; m12 = 0.0; m13 = 0.0; m20 = 0.0; m21 = 0.0; m22 = 1.0; m23 = 0.0; m30 = 0.0; m31 = 0.0; m32 = 0.0; m33 = 1.0; } public function mulScalar(scalar:Number):void { m00 *= scalar; m01 *= scalar; m02 *= scalar; m03 *= scalar; m10 *= scalar; m11 *= scalar; m12 *= scalar; m13 *= scalar; m20 *= scalar; m21 *= scalar; m22 *= scalar; m23 *= scalar; m30 *= scalar; m31 *= scalar; m32 *= scalar; m33 *= scalar; } public function mul(m1:Matrix4, m2:Matrix4):void { set( m1.m00*m2.m00 + m1.m01*m2.m10 + m1.m02*m2.m20 + m1.m03*m2.m30, m1.m00*m2.m01 + m1.m01*m2.m11 + m1.m02*m2.m21 + m1.m03*m2.m31, m1.m00*m2.m02 + m1.m01*m2.m12 + m1.m02*m2.m22 + m1.m03*m2.m32, m1.m00*m2.m03 + m1.m01*m2.m13 + m1.m02*m2.m23 + m1.m03*m2.m33, m1.m10*m2.m00 + m1.m11*m2.m10 + m1.m12*m2.m20 + m1.m13*m2.m30, m1.m10*m2.m01 + m1.m11*m2.m11 + m1.m12*m2.m21 + m1.m13*m2.m31, m1.m10*m2.m02 + m1.m11*m2.m12 + m1.m12*m2.m22 + m1.m13*m2.m32, m1.m10*m2.m03 + m1.m11*m2.m13 + m1.m12*m2.m23 + m1.m13*m2.m33, m1.m20*m2.m00 + m1.m21*m2.m10 + m1.m22*m2.m20 + m1.m23*m2.m30, m1.m20*m2.m01 + m1.m21*m2.m11 + m1.m22*m2.m21 + m1.m23*m2.m31, m1.m20*m2.m02 + m1.m21*m2.m12 + m1.m22*m2.m22 + m1.m23*m2.m32, m1.m20*m2.m03 + m1.m21*m2.m13 + m1.m22*m2.m23 + m1.m23*m2.m33, m1.m30*m2.m00 + m1.m31*m2.m10 + m1.m32*m2.m20 + m1.m33*m2.m30, m1.m30*m2.m01 + m1.m31*m2.m11 + m1.m32*m2.m21 + m1.m33*m2.m31, m1.m30*m2.m02 + m1.m31*m2.m12 + m1.m32*m2.m22 + m1.m33*m2.m32, m1.m30*m2.m03 + m1.m31*m2.m13 + m1.m32*m2.m23 + m1.m33*m2.m33 ); } public function transform(v:Vector3D):Vector3D { return new Vector3D( m00*v.x + m01*v.y + m02*v.z + m03, m10*v.x + m11*v.y + m12*v.z + m13, m20*v.x + m21*v.y + m22*v.z + m23 ); } } }
一応転置行列の作成などのメソッドも実装してますが、今回は使いません。
三角形のクラスには、面法線の計算と行列による座標変換のメソッドを用意します。
package geom { import flash.display.Graphics; public class Triangle { public var vertex:Array = new Array(); public var normal:Vector3D; public function Triangle(v1:Vector3D, v2:Vector3D, v3:Vector3D) { vertex[0] = v1.clone(); vertex[1] = v2.clone(); vertex[2] = v3.clone(); calcNormal(); } private function calcNormal():void { var v1:Vector3D = vertex[0]; var v2:Vector3D = vertex[1]; var v3:Vector3D = vertex[2]; // 面法線の計算 var a:Vector3D = v2.sub(v1); var b:Vector3D = v3.sub(v1); normal = a.crossProduct(b); normal.normalize(); } // 座標の変換 public function transform(mat:Matrix4):Triangle { var v1:Vector3D = mat.transform(vertex[0]); var v2:Vector3D = mat.transform(vertex[1]); var v3:Vector3D = mat.transform(vertex[2]); return new Triangle(v1,v2,v3); } } }
箱クラス
今回は比較的簡単な形状ということで箱を作ってみます。応用すれば他の形状もできると思います。
左のように立方体の各面を2分割して三角形の状態を考えます。
各頂点にv0〜v7まで振ってありますが、これが実際のコードの変数名と対応します。
各三角形の頂点の選び方は、必ず反時計回りになるようにしています(一番上の三角形だと、v0,v1,v2の順)。
今回の描画の方針としては、
- フレームごとに角度を変えて回転行列を生成
- 求めた行列を乗算して箱の各頂点および面法線ベクトルの座標を変換
- 面法線ベクトルのz成分が負の場合反対側を向いているので描画しない
- 描画可能な面について塗りつぶしを行う(色は環境光色+拡散反射係数×拡散光色)
こんな感じで、結構いい加減です(^^;
ライティングは、平行光源とフラットシェーディングのみなのであまりリアルじゃないです。。
箱のコードは以下のようになります。大半が座標指定などです。
package shape { import flash.display.Sprite; import geom.Triangle; import geom.Vector3D; import geom.Matrix4; import flash.geom.Point; public class Box extends Sprite { private var faces:Array = new Array(); private var ambCol:Vector3D = new Vector3D(0,0,0); // 環境光 private var difCol:Vector3D = new Vector3D(0,0,0); // 拡散反射光 private var pos:Point = new Point(); public var rotMat:Matrix4 = new Matrix4(); public function Box(size:uint) { var s:Number = size / 2.0; // 頂点 var v0:Vector3D = new Vector3D(-s,s,s); var v1:Vector3D = new Vector3D(-s,s,-s); var v2:Vector3D = new Vector3D(s,s,s); var v3:Vector3D = new Vector3D(s,s,-s); var v4:Vector3D = new Vector3D(-s,-s,-s); var v5:Vector3D = new Vector3D(s,-s,-s); var v6:Vector3D = new Vector3D(s,-s,s); var v7:Vector3D = new Vector3D(-s,-s,s); // 三角形 faces[0] = new Triangle(v0,v1,v2); faces[1] = new Triangle(v2,v1,v3); faces[2] = new Triangle(v1,v4,v3); faces[3] = new Triangle(v3,v4,v5); faces[4] = new Triangle(v0,v7,v4); faces[5] = new Triangle(v0,v4,v1); faces[6] = new Triangle(v3,v5,v2); faces[7] = new Triangle(v2,v5,v6); faces[8] = new Triangle(v4,v6,v5); faces[9] = new Triangle(v4,v7,v6); faces[10] = new Triangle(v7,v0,v6); faces[11] = new Triangle(v0,v2,v6); // 単位行列にしとく rotMat.setIdentity(); } public function draw(light:Vector3D):void { var color:Vector3D = new Vector3D(0,0,0); graphics.clear(); // 各facesを塗りつぶす(法線ベクトルのz値が正のもののみ) for(var i:uint = 0; i < 12; i++) { var face:Triangle = ((Triangle)(faces[i])).transform(rotMat); if(face.normal.z > 0) { var w:Number = face.normal.dotProduct(light); if(w < 0) w = 0; color.x = (int)((ambCol.x + w*difCol.x) * 255); color.y = (int)((ambCol.y + w*difCol.y) * 255); color.z = (int)((ambCol.z + w*difCol.z) * 255); // サチる if(color.x < 0) color.x = 0; if(color.x > 255) color.x = 255; if(color.y < 0) color.y = 0; if(color.y > 255) color.y = 255; if(color.z < 0) color.z = 0; if(color.z > 255) color.z = 255; var col:int = (color.x << 16) | (color.y << 8 ) | color.z; var v1:Vector3D = face.vertex[0]; var v2:Vector3D = face.vertex[1]; var v3:Vector3D = face.vertex[2]; graphics.moveTo(pos.x+v1.x,pos.y+v1.y); graphics.beginFill(col); graphics.lineTo(pos.x+v2.x,pos.y+v2.y); graphics.lineTo(pos.x+v3.x,pos.y+v3.y); graphics.lineTo(pos.x+v1.x,pos.y+v1.y); graphics.endFill(); } } } public function setAmbientColor(r:Number, g:Number, b:Number):void { ambCol.x = r; ambCol.y = g; ambCol.z = b; } public function setDiffuseColor(r:Number, g:Number, b:Number):void { difCol.x = r; difCol.y = g; difCol.z = b; } // 箱の中心位置 public function setPosition(x:int, y:int):void { pos.x = x; pos.y = y; } } }
完成!
左が今回作ったものです。一応、それっぽく見えてるかな?
パースペクティブの変換がないのでちょっと変ですが・・。
次回は、もうちょっと複雑なものに挑戦するかもしれません。
今回作ったソース一式は以下からダウンロードできます。
screen.zip
うーん、たまに箱が左上の方に行ってしまうんだけどなぜだろう・・。
何回かリロードすると中央に出ます(^^;