ルービックキューブ I - 3
今回は<Cube>クラスを解説する予定でしたが、その前に、座標変換とそれに関係する<Ctm3d>クラスの話をしておきたいと思います。
3Dのプログラムでは一般に内部に次のような座標系を持ち、元の図形に対して次々に変換をかけてディスプレイに表示するという仕組みになっているようです。
ローカル座標⇒ワールド座標⇒ビュー座標⇒パースペクティブ座標⇒ディスプレイ座標
このルービックキューブでもこのやり方を踏襲しています。
まず《図1》。これが初期状態で、x軸は右方向、y軸は上方向、z軸は前方向をそれぞれプラスとしています。(これを右手座標系というようです。)
《図1》
次に《図2》は、ローカル座標を示しています。
《図2》
初期状態からのブロックの移動はローカル座標に保存されます。ローカル座標は27個の<Cube>ひとつひとつで異なりますから、<Cube>クラスのインスタンス変数として保存されます。
《図3》はワールド座標です。ルービックキューブ全体を回転させた結果がワールド座標に保存されます。ワールド座標は<Ctm3d>クラス内部に保存されて、他のどのクラスからでも利用できるようにしています。
《図3》
《図4》はビュー座標です。ワールド座標によるルービックキューブの回転とは別に、ビュー座標は視点(カメラ位置)から見た座標を示しています。ここで視点はz軸上に乗っていることになります。ビュー座標もワールド座標と同様に<Ctm3d>クラス内部に保存されています。
《図4》
《図5》はパースペクティブ座標です。パースペクティブ座標はビュー座標の視点位置から図形に透視変換をかけた結果で、ここから二次元になります。パースペクティブ座標は変換行列によるのではなく、個々の<Cube>のインスタンスで計算しています。
《図5》
最後は《図6》のディスプレイ座標です。iPhoneのディスプレイは、左上を原点としてx軸を右にプラス、y軸を下にプラスに取っています。パースペクティブ座標の結果をディスプレイ座標に変換するには、描画の際にCGAffineTransformを使って一括して行っています。
《図6》
さて、それでは上記のワールド座標とビュー座標を保存する<Ctm3d>クラスを紹介しておきます。
<Ctm3d.h>
#import <Foundation/Foundation.h> #import <QuartzCore/CATransform3D.h> #import "Point3d.h" @interface Ctm3d : NSObject { CATransform3D world; CATransform3D view; } - (id)initCtm3d; - (void)addViewRotate:(CGFloat)r X:(CGFloat)x Y:(CGFloat)y Z:(CGFloat)z; - (void)addWorldRotate:(CGFloat)r X:(CGFloat)x Y:(CGFloat)y Z:(CGFloat)z; - (CATransform3D)getCATransform3DConcat:(CATransform3D)base; - (Point3d *)Point3dApplyCtm3dViewInvert:(Point3d *)p; @end
ここでは、図形の三次元変換操作のために<QuartzCore.framework>の<QuartzCore/CATransform3D.h>をimportして、ワールド座標用(world)とビュー座標用(view)にCATransform3Dクラスのインスタンスを保持しています。
ところで、このCATransform3Dクラスは、二次元のCGAffineTransformとは違って用意されているメソッドがかなりプアです。<QuartzCore/CATransform3D.h>を使わず全部自前でやった方がいいかも知れません。なお、<QuartzCore/CATransform3D.h>をimportするためには、<QuartzCore.framework>をプロジェクトに追加しておかなければなりません。XCodeのプロジェクト画面のファイル一覧の上でマウスを右クリックして[追加→既存のフレームワーク]から<QuartzCore.framework>を選択してください。
#import "Ctm3d.h" @implementation Ctm3d - (id)initCtm3d { [super init]; world = CATransform3DMakeTranslation(0.0, 0.0, 0.0); view = CATransform3DMakeTranslation(0.0, 0.0, 0.0); return self; } - (void)addViewRotate:(CGFloat)r X:(CGFloat)x Y:(CGFloat)y Z:(CGFloat)z { view = CATransform3DConcat(view, CATransform3DMakeRotation(r, x, y, z)); } - (void)addWorldRotate:(CGFloat)r X:(CGFloat)x Y:(CGFloat)y Z:(CGFloat)z { world = CATransform3DConcat(world, CATransform3DMakeRotation(r, x, y, z)); } - (CATransform3D)getCATransform3DConcat:(CATransform3D)base { CATransform3D tmp = CATransform3DConcat(base, world); tmp = CATransform3DConcat(tmp, view); return tmp; } - (Point3d *)Point3dApplyCtm3dViewInvert:(Point3d *)p { CATransform3D tmp = CATransform3DInvert(view); CGFloat nx, ny, nz; nx = p.x * tmp.m11 + p.y * tmp.m21 + p.z * tmp.m31 + tmp.m41; ny = p.x * tmp.m12 + p.y * tmp.m22 + p.z * tmp.m32 + tmp.m42; nz = p.x * tmp.m13 + p.y * tmp.m23 + p.z * tmp.m33 + tmp.m43; Point3d *np = [[[Point3d alloc] Point3dMakeX:nx Y:ny Z:nz] autorelease]; return np; } @end
イニシャルルーチンinitCtm3dはworldとviewを初期化しています。
addViewRotateとaddWorldRotateは、viewとworldそれぞれに回転を加えるルーチンです。
getCATransform3DConcatは、引数の(CATransform3D)baseに対してworldとviewを合成して返します。このbaseで渡されるのは、具体的には<Cube>クラスのインスタンスに保存されているローカル座標です。ここで、ローカル座標、ワールド座標、ビュー座標が合成されるわけです。
Point3dApplyCtm3dViewInvertは意図がわかりにくいかも知れませんが、画面をドラッグしてルービックキューブ全体を回転させるときに使います。画面のドラッグ方向をビュー座標適用前のワールド座標に逆変換するためにCATransform3DInvert(逆行列を求める)を使用しているわけです。
さて、次はいよいよ<Cube>クラスに進みたいと思います。