補足
一点書き落としましたので、補足しておきます。
<Cube>クラスのdrawCubeの説明などで触れたeclipseが、ループを作ってしまうことがあるという問題についてです。
eclipseを作る<RubicCube>のmakeEclipseInfoメソッドのロジックからは、a→b→aのような直接的なループは出来ないのですが、時にa→b→c→aのような構造が出来てしまうことがあります。これは、<Cube>が隙間なく触れ合っている場合、または互いにめり込んでしまっているような場合に発生します。
これは<Cube>の座標を不適切に指定したり、過った回転移動を行った時にだけに起こりますので、バグとは言えないかも知れません。
しかし、同じプログラムの中とはいえ、他のモジュールの不備を検知できないのは、ロジックとしての堅牢性に欠けると言われても仕方がありません。
eclipseを作る目的の隠面消去のためには、このルービックキューブの図形の構造に着目すれば、もっと簡単で確実な方法があるはずです。
次のステップでは、9個のブロックごとの回転を実装しますが、その際には以上の点の改良も大きなテーマになります。
次回はまたしばらく時間が開いてしまうと思いますが、よろしくお願いします。
ルービックキューブ I - 6
ルービックキューブ Iもやっと最終回、今回は<MyWorld>クラスです。
<MyWorld>クラスは、<UIView>のサブクラスで、画面とRubicCubeの仲立ちをするクラスです。
<MyWorld.h>
#import <UIKit/UIKit.h> #import "RubicCube.h" #import "Ctm3d.h" @interface MyWorld : UIView { RubicCube *rubiccube; Ctm3d *ctm3d; CGPoint currMovePnt; } - (void)rotateX:(CGFloat)dx Y:(CGFloat)dy; @end
<MyWorld.h>はこれだけです。RubicCubeのインスタンスrubiccubeと、ワールド座標、ビュー座標を格納するCtm3dのインスタンスctm3d、それにタッチされた画面の座標を保存するcurrMovePntがインスタンス変数です。
<MyWorld.m>
#import "MyWorld.h" @implementation MyWorld - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // Initialization code ctm3d = [[Ctm3d alloc] initCtm3d]; rubiccube = [[RubicCube alloc] initRubicCube:ctm3d viewPoint:600.0]; //Viewpointの設定 [ctm3d addViewRotate:-M_PI/4.0 X:0.0 Y:10.0 Z:0.0]; [ctm3d addViewRotate:M_PI/8.0 X:10.0 Y:0.0 Z:0.0]; } return self; }
初期化メソッドinitWithFrameも単純です。
ctm3dとrubiccubeを初期化し、視点(Viewpoint)の設定を行っています。Viewpointはz軸上の+600.0とし、これにCtm3dのメソッドaddViewRotateで回転を与えています。この回転は、Ctm3dの(CATransform3D)viewに保存されます。
このルービックキューブ Iでは、画面をドラッグすることでルービックキューブ全体を回転させることができますが、
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint currentTouchPosition = [touch locationInView:self]; currMovePnt = currentTouchPosition; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { NSAutoreleasePool *arp = [[NSAutoreleasePool alloc] init]; UITouch *touch = [touches anyObject]; CGPoint currentTouchPosition = [touch locationInView:self]; CGFloat dx = currentTouchPosition.x - currMovePnt.x; CGFloat dy = currentTouchPosition.y - currMovePnt.y; [self rotateX:dx Y:dy]; currMovePnt = currentTouchPosition; [arp release]; [self setNeedsDisplay]; } - (void)isTouchesEnded:(int)no loc:(CGPoint)location { } - (void)rotateX:(CGFloat)dx Y:(CGFloat)dy { //dx,dyの反転に注意 Point3d *p = [[[Point3d alloc] Point3dMakeX:dy Y:dx Z:0.0] autorelease]; //ビュー座標をワールド座標に戻す Point3d *np = [ctm3d Point3dApplyCtm3dViewInvert:p]; CGFloat d = sqrt(dx*dx + dy*dy); [ctm3d addWorldRotate:(M_PI*d)/360.0 X:np.x Y:np.y Z:np.z]; }
touchesBeganは、ドラッグの始点をインスタンス変数currMovePntに保存するだけ。続くtouchesMovedでドラッグの移動量x,yをrotateX:Y:に渡しています。
rotateX:Y:では、渡されたx,yを加工してワールド座標上のベクトルに変換し、このベクトルを回転軸としてCtm3dのaddWorldRotateで回転を与えます。
コメントにもあるように、x方向のドラッグはy軸を中心とした回転に、y方向のドラッグはx軸を中心とした回転になりますので、回転軸を計算する際dx,dyを反転させています。また、この画面上の移動量はビュー座標上のものですから、Point3dApplyCtm3dViewInvertでワールド座標に変換しています。
rotateX:Y:でルービックキューブに回転を与えてから、touchesMovedは現在のドラッグ位置を保存し、setNeedsDisplay(UIViewオリジナルのメソッド)を呼んであとはシステムに任せます。
setNeedsDisplayが呼ばれ、再描画の必要があると判断するとシステムはdrawRectを実行します。これは先回りして前回説明しました。
drawRectから
- (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetLineWidth(context, 1.5); CGContextSetRGBStrokeColor(context, 0.0, 0.0, 0.0, 1.0); //ディスプレイ座標のセット CGAffineTransform xform; xform = CGAffineTransformMakeTranslation(320.0/2, 320.0/2); xform = CGAffineTransformScale(xform, 1.0, -1.0); CGContextConcatCTM(context, xform); [rubiccube drawCubes:context]; //後始末 xform = CGAffineTransformInvert(xform); CGContextConcatCTM(context, xform); } - (void)dealloc { [super dealloc]; } @end
<MyWorld>は以上です。
そうそう、最後に<MyWorld>をキックする部分が必要です。これは、View-based Applicationプロジェクトを作成した時に出来ている<RubicCubeViewController.m>(「RubicCube」の部分はプロジェクト名で異なります)のviewDidLoadに加えます。
- (void)viewDidLoad { [super viewDidLoad]; MyWorld *world = [[MyWorld alloc] initWithFrame:CGRectMake(0, 0, 320, 320)]; [world setBackgroundColor:[UIColor whiteColor]]; [self.view addSubview:world]; }
これでルービックキューブ Iの完成です。
ルービックキューブ I - 5
だいぶ核心に近づいてきました。今回は<RubicCube>クラスです。<RubicCube>は<Cube>によって作る27個の立方体を統括、制御する役割を果たしています。
まずは例によって<RubicCube.h>から。
#import <Foundation/Foundation.h> #import "Cube.h" #import "Ctm3d.h" @interface RubicCube : NSObject { Ctm3d *ctm3d; NSMutableArray *cubearray; NSMutableSet *rootEclipse; CGFloat viewZ; } - (id)initRubicCube:(Ctm3d *)ctm viewPoint:(CGFloat)vz; - (NSArray *)makeColorArray; - (void)drawCubes:(CGContextRef)context; - (void)makeEclipseInfo; - (NSMutableArray *)getCrossingPointsA:(Quad3d *)q0 B:(Quad3d *)q1; - (Point3d *)getCrossPointA1:(double)a1 B1:(double)b1 C1:(double)c1 A2:(double)a2 B2:(double)b2 C2:(double)c2; - (void)addUniqueObject:(NSMutableArray *)points point:(Point3d *)p; - (int)includeQuadP:(Point3d *)p Quad3d:(Quad3d *)q; - (Boolean)isFrontQ1:(Quad3d *)q0shape Q2:(Quad3d *)q1shape cross:(NSMutableArray *)points; @end
<Cube.h>と違って、インスタンス変数はわずかです。
ワールド座標とビュー座標を保存する<Ctm3d>クラスのインスタンスctm3dは<Cube>でも登場しました。
cubearrayは27個の<Cube>インスタンスを保存します。
rootEclipseは、<Cube>のeclipseの「根っこ」にあたります。
viewZは、視点(カメラ位置)のz座標です。
では<RubicCube.m>に入りましょう。まず、初期化メソッドinitRubicCubeです。
#import "RubicCube.h" @implementation RubicCube - (id)initRubicCube:(Ctm3d *)ctm viewPoint:(CGFloat)vz { [super init]; ctm3d = ctm; viewZ = vz; cubearray = [[NSMutableArray alloc] initWithCapacity:1]; rootEclipse = [[NSMutableSet alloc] initWithCapacity:1]; NSAutoreleasePool *arp = [[NSAutoreleasePool alloc] init]; NSArray *colorArray = [self makeColorArray]; int cubeSize = 55; int blockSize = cubeSize+1; for (int i=0; i<3; i++) { for (int j=0; j<3; j++) { for (int k=0; k<3; k++) { int number = i*9+j*3+k; Point3d *center = [[[Point3d alloc] Point3dMakeX:-blockSize+(k*blockSize) Y:blockSize-(j*blockSize) Z:blockSize-(i*blockSize)] autorelease]; Cube *c = [[[Cube alloc] cubeMake:center size:cubeSize cubeno:number colors:[colorArray objectAtIndex:number] Ctm3d:ctm3d viewZ:&viewZ Cubearray:cubearray] autorelease]; [cubearray insertObject:c atIndex:number]; } } } [arp release]; return self; }
呼び出し元の<MyWorld>から渡されたCtm3dとviewPointをインスタンス変数に保存したあと、前回説明した<Cube>の初期化メソッドcubeMakeを使って、ひたすら27個の立方体を作りcubearrayに保存しています。<Cube>の番号0〜26は、cubearrayのindexと一致させています。
中で使われているcubeSizeは<Cube>の辺のサイズ、blockSizeは<Cube>同士の間隔です。lockSizeは必ずcubeSizeより大きくないといけません。(同じサイズだと、eclipseの構造に無限ループが生じてしまいます。)
なお、NSAutoreleasePoolは、autoreleaseメソッドの呼び出しによってオブジェクトが登録される「プール」です。NSAutoreleasePoolにreleaseが送られると、プールに登録されたオブジェクトにも一斉にreleaseが送られ、メモリの解放が楽に行えるという仕組みです。
さて、cubeMakeには、各Cubeの面の色を:[colorArray objectAtIndex:number]として渡していますが、この配列を作るmakeColorArrayを見ましょう。
#define C_WHI [UIColor whiteColor] #define C_BLU [UIColor blueColor] #define C_ORA [UIColor orangeColor] #define C_YEL [UIColor yellowColor] #define C_GRE [UIColor greenColor] #define C_RED [UIColor redColor] #define C_NON [UIColor grayColor] - (NSArray *)makeColorArray { NSArray *colors = [[[NSArray alloc] initWithObjects: [[[NSArray alloc] initWithObjects: C_WHI, C_NON, C_ORA, C_NON, C_GRE, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_WHI, C_NON, C_ORA, C_NON, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_WHI, C_BLU, C_ORA, C_NON, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_WHI, C_NON, C_NON, C_NON, C_GRE, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_WHI, C_NON, C_NON, C_NON, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_WHI, C_BLU, C_NON, C_NON, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_WHI, C_NON, C_NON, C_NON, C_GRE, C_RED, nil] autorelease], [[[NSArray alloc] initWithObjects: C_WHI, C_NON, C_NON, C_NON, C_NON, C_RED, nil] autorelease], [[[NSArray alloc] initWithObjects: C_WHI, C_BLU, C_NON, C_NON, C_NON, C_RED, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_ORA, C_NON, C_GRE, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_ORA, C_NON, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_BLU, C_ORA, C_NON, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_NON, C_NON, C_GRE, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_NON, C_NON, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_BLU, C_NON, C_NON, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_NON, C_NON, C_GRE, C_RED, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_NON, C_NON, C_NON, C_RED, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_BLU, C_NON, C_NON, C_NON, C_RED, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_ORA, C_YEL, C_GRE, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_ORA, C_YEL, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_BLU, C_ORA, C_YEL, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_NON, C_YEL, C_GRE, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_NON, C_YEL, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_BLU, C_NON, C_YEL, C_NON, C_NON, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_NON, C_YEL, C_GRE, C_RED, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_NON, C_NON, C_YEL, C_NON, C_RED, nil] autorelease], [[[NSArray alloc] initWithObjects: C_NON, C_BLU, C_NON, C_YEL, C_NON, C_RED, nil] autorelease], nil] autorelease]; return colors; }
NSArrayの二重構造ですが中身は単純です。<Cube>1個分6面の色をNSArray化したものを27個、またNSArrayに入れているだけです。各<Cube>の色は、外側に面する部分は前回Cubeの展開図で示した色、内側はグレー( [UIColor grayColor])にしています。また、少しでもコードが見やすいように、#defineを使っています。
さあ、そろそろRubicCube全体の描画ロジックの説明に入りますが、その前にもういちど先ほど作った27個のCubeに振った番号を図で確認しておきましょう。

ローカル座標で正面にあたる白で塗られた面の左上を0として右下方向に8まで、中段は9から17、一番奥の段が18から26までとなっています。
この時点でビュー座標は図の通り、白と青とオレンジの3面を見渡す方向に設定されています。ここで、1,2,10,11の4つのCubeに注目してください。2は1,10,11のいずれよりも手前にありますから、2を描画する前に1,10,11を描画しておかなければなりません。また、1と11はいずれも10の手前にありますから、1と11を描画する前に10を描画しておかなければなりません。
このように、あるCubeを描画する前に描画する必要のあるCubeを格納しているのが各Cubeインスタンスのeclipse配列です。ただし、2→1→10(あるいは2→11→10)のような場合には、2には1と11だけを格納し、間接的な関係になる10は除きます。1,2,10,11の4つのCubeに限れば、Cube-2のeclipseには1,11が、Cube-1とCube-11のeclipseには10が格納されるというわけです。
こうして、図の初期状態で作られたeclipseを図にしたのが次です。

各Cubeの番号から出た青い線がそのeclipseに格納されたCubeの番号を示しています。rootはRubicCubeクラスのrootEclipseです。
各Cubeはeclipseに格納されたCubeを先に描画してから自分自身を描画します。図ではまず(2)に出された描画の指令が(5)に渡され、(4)、(13)、(22)、(21)と順に指令が下っていき、(24)ではじめて描画が行われます。(21)は(24)が描画されてもうeclipseが空なので自身を描画し、(22)に戻ります。(22)はeclipseの残りの(25)に描画の指示を出します。(25)はeclipseに入っている(24)に描画指示を出しますが、(24)は描画を終わっているので(displayedフラグがYESになっているので)何もせず戻り、(25)が描画されます。
このようにして、eclipse情報をたどって他のすべてのCubeが描画された後、最初に指令を受け取った(2)が描画されてめでたくRubicCubeが完成することになります。
それでは描画ロジックに入りましょう。まず、描画をキックするのは<MyWorld>クラスのdrawRectメソッドなのでこちらを覗きましょう。
<drawRect> (<MyWorld>クラス)
- (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetLineWidth(context, 1.5); CGContextSetRGBStrokeColor(context, 0.0, 0.0, 0.0, 1.0); //ディスプレイ座標のセット CGAffineTransform xform; xform = CGAffineTransformMakeTranslation(320.0/2, 320.0/2); xform = CGAffineTransformScale(xform, 1.0, -1.0); CGContextConcatCTM(context, xform); [rubiccube drawCubes:context]; //後始末 xform = CGAffineTransformInvert(xform); CGContextConcatCTM(context, xform); }
<MyWorld>は<UIView>のサブクラスで、drawRect はviewに再描画が必要になったときシステムから呼び出されます。ここではまずUIGraphicsGetCurrentContext()でcontextを作成し、CGContextSetLineWidthとCGContextSetRGBStrokeColorで全体の描画に共通な線の太さと色をセットし、CGAffineTransformにディスプレイ座標をセットし、<RubicCube>クラスのdrawCubesを呼んでいます。こうすることで、以下のルーチンで描かれる図形に、ディスプレイ座標が反映されることになります。
では、<RubicCube>クラスに戻ってdrawCubesメソッドです。
- (void)drawCubes:(CGContextRef)context { //eclipse情報を作り直す [self makeEclipseInfo]; for (NSNumber *num in rootEclipse) { Cube *c = [cubearray objectAtIndex:[num intValue]]; [c drawCube:context]; } }
これは簡単ですね。makeEclipseInfoで先に紹介したeclipse情報を作り直してから、rootEclipseに入っているCubeに描画指令を出すだけです。<Cube>のdrawCubeメソッドは前回紹介しました。
問題はmakeEclipseInfoです。
- (void)makeEclipseInfo { //以前のeclipse情報を消去する [rootEclipse removeAllObjects]; for (Cube *c in cubearray) { [c.eclipse removeAllObjects]; [c applyTransform]; [c makeFrontq]; c.displayed = NO; c.visited = 0; } NSAutoreleasePool *arp = [[NSAutoreleasePool alloc] init]; //cubearrayからCubeをひとつづつ取り出して隠蔽関係があるか調べる //隠蔽関係があればどちらかのecliseに情報を追加する for (int i=0; i<[cubearray count]; i++) { Cube *cube0 = [cubearray objectAtIndex:i]; for (int j=i+1; j<[cubearray count]; j++) { Cube *cube1 = [cubearray objectAtIndex:j]; //cube0とcube1のオモテを向いている面同士に隠蔽関係があるか調べる int result = 0; for (int k=0; result==0 && k<cube0.frontqSize; k++) { Quad3d *q0_pers = [cube0 getFrontqPers:k]; Quad3d *q0_orig = [cube0 getFrontqOrig:k]; for (int l=0; result==0 && l<cube1.frontqSize; l++) { Quad3d *q1_pers = [cube1 getFrontqPers:l]; Quad3d *q1_orig = [cube1 getFrontqOrig:l]; //q0_persとq1_persの交差する点を見つける NSMutableArray *points = [self getCrossingPointsA:q0_pers B:q1_pers]; if ([points count] > 2) { //交差する点が3つ以上の場合隠蔽関係があると判断し、どちらが前に位置するか調べる Boolean ret = [self isFrontQ1:q0_orig Q2:q1_orig cross:points]; if (ret == YES) { result = 1; } else { result = -1; } } } } if (result == 1) { //cube0がcube1を隠す場合 NSNumber *num = [NSNumber numberWithInt:cube1.cubeNo]; [cube0.eclipse addObject:num]; } else if (result == -1) { //cube1がcube0を隠す場合 NSNumber *num = [NSNumber numberWithInt:cube0.cubeNo]; [cube1.eclipse addObject:num]; } } } //各Cubeのeclipseの整理 for (int i=0; i<[cubearray count]; i++) { Cube *c0 = [cubearray objectAtIndex:i]; NSMutableSet *tempminus = [[NSMutableSet alloc] initWithCapacity:1]; for (NSNumber *num in c0.eclipse) { Cube *c1 = [cubearray objectAtIndex:[num intValue]]; if (! [c0 isEqual:c1]) { [tempminus unionSet:c1.eclipse]; } } [c0.eclipse minusSet:tempminus]; } //rootEclipseを作る for (int i=0; i<[cubearray count]; i++) { NSNumber *num = [NSNumber numberWithInt:i]; [rootEclipse addObject:num]; } //rootEclipseの整理 NSMutableSet *tempminus = [[NSMutableSet alloc] initWithCapacity:1]; for (NSNumber *num in rootEclipse) { Cube *c = [cubearray objectAtIndex:[num intValue]]; [tempminus unionSet:c.eclipse]; } [rootEclipse minusSet:tempminus]; [arp release]; }
makeEclipseInfoは、まず以前のeclipse情報をクリアしてから、各CubeのapplyTransformメソッドでビュー座標までを反映したtq_origとそれにパースペクティブ座標を加えたtq_persを作り直します。さらに、makeFrontqで現在オモテを向いている面をfrontq_origとfrontq_persに入れます。displayedフラグをクリアし、visitedカウンタを0にして準備完了、cubearrayからCubeをひとつづつ取り出しオモテを向いている面同士の重なり合いを調べて、重なり合いがあればどちらかのeclipseに入れます。後半、「各Cubeのeclipseの整理」「rootEclipseの整理」とあるのは、eclipseの構造の説明で触れた「間接的な重なり合いは除いておく」という処理です。
では、実際に面同士の重なり合いを調べるgetCrossingPointsAメソッドです。
- (NSMutableArray *)getCrossingPointsA:(Quad3d *)q0 B:(Quad3d *)q1 { //交差する点を格納する配列pointsの定義 NSMutableArray *points = [[[NSMutableArray alloc] initWithCapacity:1] autorelease]; //q0とq1の各辺が交差するか調べる for (int i=0; i<4; i++) { int j = (i+1)%4; Point3d *q0i = [q0 getP:i]; Point3d *q0j = [q0 getP:j]; //q0の辺の方程式 a1*x+b1*y+c1=0 の係数を計算 double a1, b1, c1; if ((q0j.x-q0i.x) == 0) { a1 = 1; b1 = 0; c1 = -q0i.x; } else { a1 = (q0j.y-q0i.y)/(q0j.x-q0i.x); b1 = -1; c1 = q0i.y - a1*q0i.x; } for (int k=0; k<4; k++) { int l = (k+1)%4; //Point3d *q1k = [q1 getPoint3dPars:k]; Point3d *q1k = [q1 getP:k]; Point3d *q1l = [q1 getP:l]; //q1の辺の方程式 a2*x+b2*y+c2=0 の係数を計算 double a2, b2, c2; if ((q1l.x - q1k.x) == 0) { a2 = 1; b2 = 0; c2 = -q1k.x; } else { a2 = (q1l.y-q1k.y)/(q1l.x-q1k.x); b2 = -1; c2 = q1k.y - a2*q1k.x; } //a1*x+b1*y+c1=0 と a2*x+b2*y+c2=0 の交差点を求める Point3d *crossP = [self getCrossPointA1:a1 B1:b1 C1:c1 A2:a2 B2:b2 C2:c2]; if (crossP.x != FLT_MAX && crossP.x >= MIN(q0i.x, q0j.x) && crossP.x <= MAX(q0i.x, q0j.x) && crossP.y >= MIN(q0i.y, q0j.y) && crossP.y <= MAX(q0i.y, q0j.y) && crossP.x >= MIN(q1k.x, q1l.x) && crossP.x <= MAX(q1k.x, q1l.x) && crossP.y >= MIN(q1k.y, q1l.y) && crossP.y <= MAX(q1k.y, q1l.y)) { //交差点が各辺の上にあれば、pointsに加える [self addUniqueObject:points point:crossP]; } } } //q0の各点が、q1の内部にあればpointsに加える for (int i=0; i<4; i++) { Point3d *q0p = [q0 getP:i]; int res = [self includeQuadP:q0p Quad3d:q1]; if (res == 1 || res == 0) { [self addUniqueObject:points point:q0p]; } } //q1の各点が、q0の内部にあればpointsに加える for (int i=0; i<4; i++) { Point3d *q1p = [q1 getP:i]; int res = [self includeQuadP:q1p Quad3d:q0]; if (res == 1 || res == 0) { [self addUniqueObject:points point:q1p]; } } return points; }
getCrossingPointsAは、渡された2つの四辺形の座標から、重なり合う点の位置を求めてpoints配列に入れて返します。
ここで、重なり合う点には、辺と辺が交差してできる点と、一方の四辺形の頂点が他方の四辺形に包含される場合の点の、2つの種類があることに注意してください。

図で×印を付けたのが辺と辺が交差してできる点、○印を付けたのが他の四辺形に包含される点です。getCrossingPointsAではまず×印のタイプの点を、次に○印のタイプの点を探してpoints配列に入れます。なお、このpointsが2個までの場合は(図の(d))重なり合いはなく、3個以上あれば重なり合いがあると判断します。
×印のタイプの、辺と辺の交差点を探すのが、次のgetCrossPointA1です。
- (Point3d *)getCrossPointA1:(double)a1 B1:(double)b1 C1:(double)c1 A2:(double)a2 B2:(double)b2 C2:(double)c2 { double x, y; double det = a1 * b2 - a2 * b1; if (det == 0) { return [[[Point3d alloc] Point3dMakeX:FLT_MAX Y:FLT_MAX Z:FLT_MAX] autorelease]; } x = (b1 * c2 - b2 * c1) / det; if (x == -0.0) { x = 0.0; } y = (a2 * c1 - a1 * c2) / det; if (y == -0.0) { y = 0.0; } //Z成分はdummy return [[[Point3d alloc] Point3dMakeX:x Y:y Z:0.0] autorelease]; }
そして、○印のタイプの、ある頂点が他の四辺形の内部にあるかを調べるのが次のincludeQuadPです。
- (int)includeQuadP:(Point3d *)p Quad3d:(Quad3d *)q { //面qの頂点(p0,p1)を順に取り出しp→p0とp→p1の成す角を求める //これを一回り足し合わせた時にM_PI*2になれば点pが面qの内側にある double theta = 0.0; for (int i=0; i<4; i++) { Point3d *p0 = [q getP:i]; Point3d *p1 = [q getP:(i+1)%4]; double x1 = p0.x - p.x; double y1 = p0.y - p.y; double x2 = p1.x - p.x; double y2 = p1.y - p.y; double n1 = sqrt(x1*x1 + y1*y1); double n2 = sqrt(x2*x2 + y2*y2); if ((n1*n2) == 0) { //点pが面qの辺上にある場合 return 1; } double t; if (ABS((x1*x2 + y2*y1)/(n1*n2)) >= 1.0) { t = 0.0; } else { t = acos((x1*x2 + y2*y1)/(n1*n2)); } if ((x1*y2 - y1*x2) < 0) { t = t * -1; } theta += t; } if (ABS(ABS(theta) - M_PI*2) < 0.1) { return 1; } return -1; }

includeQuadPは、左の図のように、ある点から四辺形の各頂点を線分で結んだとき、四辺形の内部にある点P0では線分の成す角の和が2πになり、外部にある点P1では差し引き0となるという性質を用いています。
さて、先ほどのgetCrossingPointsAでは、pointsに点を格納する際に、addUniqueObjectというメソッドを使っています。これは、上記のルーチンで同一点を検出してもpointsの重複をさけるためで、次のような内容です。
- (void)addUniqueObject:(NSMutableArray *)points point:(Point3d *)p { for (int i=0; i<[points count]; i++) { Point3d *q = [points objectAtIndex:i]; if (p.x == q.x && p.y == q.y) { return; } } [points addObject:p]; }
pointsに3点以上が返ってきたとき、makeEclipseInfoではその2つの面の前後関係を調べてeclipseに格納します。この2つの面の前後関係を調べるのが、最後のincludeQuadPです。
//pointsの各点についてq0平面、q1平面と交わる点を求め、q0とq1の前後関係を返す - (Boolean)isFrontQ1:(Quad3d *)q0 Q2:(Quad3d *)q1 cross:(NSMutableArray *)points { Boolean result = NO; NSAutoreleasePool *arp = [[NSAutoreleasePool alloc] init]; //q0とq1の平面の方程式を作る FlatEquation *q0flat = [[[FlatEquation alloc] makeFlatEquationP1:q0.p0 P2:q0.p1 P3:q0.p2] autorelease]; FlatEquation *q1flat = [[[FlatEquation alloc] makeFlatEquationP1:q1.p0 P2:q1.p1 P3:q1.p2] autorelease]; for (int i=0; i<[points count]; i++) { Point3d *p = [points objectAtIndex:i]; //p1は視点の座標 Point3d *p1 = [[[Point3d alloc] Point3dMakeX:0 Y:0 Z:viewZ] autorelease]; //p2はpointsから取り出した点のz平面上の座標 Point3d *p2 = [[[Point3d alloc] Point3dMakeX:p.x Y:p.y Z:0] autorelease]; //p1→p2がq0とq1の各平面と交わる点を求める Point3d *q0orig = [q0flat crossPoint3dP1:p1 P2:p2]; Point3d *q1orig = [q1flat crossPoint3dP1:p1 P2:p2]; if (q0orig.z < q1orig.z && abs(q1orig.z-q0orig.z) > 0.1) { result = NO; break; } else if (q1orig.z < q0orig.z && abs(q0orig.z-q1orig.z) > 0.1) { result = YES; break; } } [arp release]; return result; } @end

isFrontQ1は、視点P1とpointsに格納された点P2を通る直線が、q0、q1それぞれのオリジナルの平面と交わる点を求め、そのz座標によってq0、q1の前後関係を返します。
q0、q1の各平面と直線の交点は、平面の方程式を作る<FlatEquation>クラスで計算します。
今回は、この<FlatEquation>クラスを紹介して終わることにしましょう。
<FlatEquation.h>
#import <Foundation/Foundation.h> #import "Point3d.h" @interface FlatEquation : NSObject { //P1->P2ベクトル成分(sx,sy,sz) double sx; double sy; double sz; //P1->P3ベクトル成分(tx,ty,tz) double tx; double ty; double tz; //平面方程式(ax+by+cz+d=0)の係数 double a; double b; double c; double d; } - (id)makeFlatEquationP1:(Point3d *)p1 P2:(Point3d *)p2 P3:(Point3d *)p3; - (Point3d *)crossPoint3dP1:(Point3d *)p1 P2:(Point3d *)p2; @end
<FlatEquation.m>
#import "FlatEquation.h" @implementation FlatEquation - (id)makeFlatEquationP1:(Point3d *)p1 P2:(Point3d *)p2 P3:(Point3d *)p3 { sx = p2.x - p1.x; sy = p2.y - p1.y; sz = p2.z - p1.z; tx = p3.x - p1.x; ty = p3.y - p1.y; tz = p3.z - p1.z; a = sy*tz - sz*ty; b = sz*tx - sx*tz; c = sx*ty - sy*tx; d = (a*p1.x + b*p1.y + c*p1.z) * -1; return self; } - (Point3d *)crossPoint3dP1:(Point3d *)p1 P2:(Point3d *)p2 { //p1->p2の直線と平面の交点を求める //ベクトル(e, f, g) double e = p2.x - p1.x; double f = p2.y - p1.y; double g = p2.z - p1.z; //直線のパラメタ表示 // x = p1.x + t*e // y = p1.y + t*f // z = p1.z + t*g //平面の方程式に代入してtを求める double t = (a*p1.x + b*p1.y + c*p1.z + d) / (a*e + b*f + c*g) * -1.0; double x = p1.x + t*e; double y = p1.y + t*f; double z = p1.z + t*g; return [[[Point3d alloc] Point3dMakeX:x Y:y Z:z] autorelease]; } @end
ルービックキューブ I - 4
さて<Cube>クラスです。<Cube>はルービックキューブの27個の立方体を表しています。まず、
#import <Foundation/Foundation.h> #import <QuartzCore/CATransform3D.h> #import "Quad3d.h" #import "FlatEquation.h" #import "Ctm3d.h" @interface Cube : NSObject { Quad3d *q[6]; int cubeNo; UIColor *color[6]; Ctm3d *ctm3d; CATransform3D ctm3d_local; NSMutableSet *eclipse; CGFloat *viewZ; NSMutableArray *cubearray; Quad3d *tq_orig[6]; Quad3d *tq_pers[6]; Quad3d *frontq_orig[6]; Quad3d *frontq_pers[6]; UIColor *frontcolor[6]; int frontqSize; Boolean displayed; int visited; } @property (retain, readwrite) NSMutableSet *eclipse; @property (readwrite) int cubeNo; @property (readwrite) Boolean displayed; @property (readwrite) int visited; @property (readwrite) int frontqSize; - (id)cubeMake:(Point3d *)p size:(CGFloat)s cubeno:(int)no colors:(NSArray *)colorarray Ctm3d:(Ctm3d *)ctm viewZ:(CGFloat *)vz Cubearray:(NSMutableArray *)carray; - (void)applyTransform; - (Point3d *)point3dApplyCtm3d:(CATransform3D)ctm Point3d:(Point3d *)p; - (Point3d *)getPerspectivePoint3D:(Point3d *)p; - (void)makeFrontq; - (Boolean)isFront:(Quad3d *)q0; - (void)drawCube:(CGContextRef)context; - (void)findLoop; - (Quad3d *)getFrontqPers:(int)index; - (Quad3d *)getFrontqOrig:(int)index; - (void)add3DCtmRotate:(CGFloat)r X:(CGFloat)x Y:(CGFloat)y Z:(CGFloat)z; @end
ずいぶん沢山のインスタンス変数がありますが、ここでは一番大事な面<Quad3d>の情報についてだけ説明しましょう。
qは前々回説明した<Quad3d>6個で立方体の面の座標の初期値を保存します。これに対し、tq_origは、ローカル座標、ワールド座標、ビュー座標を適用した後の座標値がその都度格納されます。tq_persの方は、これにパースペクティブ変換を施した結果です。
また、frontq_origにはtq_origから、frontq_persにはtq_pers[]から、その時点でオモテを向いている面だけが格納されています。オモテを向いた面の数は多くても3つですが、ここでは念のため6つ配列を取って有効な要素数をfrontqSizeにセットしています。
残りのインスタンス変数については、それぞれの
#import "Cube.h" @implementation Cube @synthesize eclipse, cubeNo, displayed, visited, frontqSize; - (id)cubeMake:(Point3d *)p size:(CGFloat)s cubeno:(int)no colors:(NSArray *)colorarray Ctm3d:(Ctm3d *)ctm viewZ:(CGFloat *)vz Cubearray:(NSMutableArray *)carray { [super init]; CGFloat x = p.x-(s/2.0); CGFloat y = p.y+(s/2.0); CGFloat z = p.z+(s/2.0); Point3d *vertex[8]; vertex[0] = [[Point3d alloc] Point3dMakeX:x Y:y Z:z]; vertex[1] = [[Point3d alloc] Point3dMakeX:x Y:y-s Z:z]; vertex[2] = [[Point3d alloc] Point3dMakeX:x+s Y:y-s Z:z]; vertex[3] = [[Point3d alloc] Point3dMakeX:x+s Y:y Z:z]; vertex[4] = [[Point3d alloc] Point3dMakeX:x+s Y:y Z:z-s]; vertex[5] = [[Point3d alloc] Point3dMakeX:x+s Y:y-s Z:z-s]; vertex[6] = [[Point3d alloc] Point3dMakeX:x Y:y-s Z:z-s]; vertex[7] = [[Point3d alloc] Point3dMakeX:x Y:y Z:z-s]; q[0] = [[Quad3d alloc] quad3dMakeP0:vertex[0] P1:vertex[1] P2:vertex[2] P3:vertex[3]]; q[1] = [[Quad3d alloc] quad3dMakeP0:vertex[3] P1:vertex[2] P2:vertex[5] P3:vertex[4]]; q[2] = [[Quad3d alloc] quad3dMakeP0:vertex[0] P1:vertex[3] P2:vertex[4] P3:vertex[7]]; q[3] = [[Quad3d alloc] quad3dMakeP0:vertex[7] P1:vertex[4] P2:vertex[5] P3:vertex[6]]; q[4] = [[Quad3d alloc] quad3dMakeP0:vertex[0] P1:vertex[7] P2:vertex[6] P3:vertex[1]]; q[5] = [[Quad3d alloc] quad3dMakeP0:vertex[1] P1:vertex[6] P2:vertex[5] P3:vertex[2]]; cubeNo = no; for (int i=0; i<MIN([colorarray count], 6); i++) { color[i] = [colorarray objectAtIndex:i]; } ctm3d = ctm; viewZ = vz; cubearray = carray; ctm3d_local = CATransform3DMakeTranslation(0.0, 0.0, 0.0); eclipse = [[NSMutableSet alloc] initWithCapacity:1]; return self; }
最初の引数Point3d *)pは、作成するCubeの中心の座標を示します。size:(CGFloat)sは一辺の長さです。この2つのパラメタでQuad3dを6つ作り、インスタンス変数qに保存しています。6つのQuad3dの順番は次の展開図のようになっています。
[f:id:pincushion:20090830112746p:image]
色付けされた面の0から5が、q[0]からq[5]に対応しています。そして、頂点に振られたイタリックの0から7がプログラムのvertex[0]からvertex[7]に対応します。この頂点は、前回も述べたように、反時計周りに設定していることが展開図でわかると思います。
cubeno:(int)noは、ルービックキューブ全体の中でのこのCubeの番号です。0から26が渡されます。
colors:(NSArray *)colorarrayは、このCubeの各面に塗る色の配列です。白なら[UIColor whiteColor]が、青なら[UIColor blueColor]が入っているという具合です。各面に塗られる色は上の展開図の通りですが、各Cubeはすべての色が使用される訳ではなく、内側に隠れる面にはグレー([UIColor grayColor])が設定されています。
残りの引数、Ctm3d:(Ctm3d *)ctmは、前回説明したワールド座標とビュー座標を保存する<Ctm3d>クラスのインスタンスです。
viewZ:(CGFloat *)vzは、視点のz座標のポインタです。直接CGFloatの値ではなくポインタ[(CGFloat *)]を渡しているのは、呼び出し側であとになって変更ができるようにという意味です。
そして最後にCubearray:(NSMutableArray *)carray。これは、このインスタンスも含んだCube全部のオブジェクトを格納した配列へのポインタです。
これらの情報をインスタンス変数に格納して、Cubeオブジェクトが完成します。
次は、イニシャルルーチンで保存したQuad3dの配列から、これにローカル座標、ワールド座標、ビュー座標を適用したtq_origと、さらにパースペクティブ変換を施したtq_pers[]を作るメソッドapplyTransformです。
- (void)applyTransform { CATransform3D tmp = [ctm3d getCATransform3DConcat:ctm3d_local]; for (int i=0; i<6; i++) { Point3d *np0 = [self point3dApplyCtm3d:tmp Point3d:q[i].p0]; Point3d *np_pars0 = [self getPerspectivePoint3D:np0]; Point3d *np1 = [self point3dApplyCtm3d:tmp Point3d:q[i].p1]; Point3d *np_pars1 = [self getPerspectivePoint3D:np1]; Point3d *np2 = [self point3dApplyCtm3d:tmp Point3d:q[i].p2]; Point3d *np_pars2 = [self getPerspectivePoint3D:np2]; Point3d *np3 = [self point3dApplyCtm3d:tmp Point3d:q[i].p3]; Point3d *np_pars3 = [self getPerspectivePoint3D:np3]; tq_orig[i] = [[Quad3d alloc] quad3dMakeP0:np0 P1:np1 P2:np2 P3:np3]; tq_pers[i] = [[Quad3d alloc] quad3dMakeP0:np_pars0 P1:np_pars1 P2:np_pars2 P3:np_pars3]; } }
applyTransformでは、まず前回説明した<Ctm3d>のメソッドgetCATransform3DConcatで、ローカル座標、ワールド座標、ビュー座標を合成した変換行列を得て、qの各点をgetPerspectivePoint3Dで座標変換してtq_origに入れています。CATransform3DにはCGAffineTransformクラスにおけるCGPointApplyAffineTransformのようなメソッドがないため、これを自前で用意しているのがpoint3dApplyCtm3dです。
- (Point3d *)point3dApplyCtm3d:(CATransform3D)ctm Point3d:(Point3d *)p { CGFloat nx, ny, nz; nx = p.x * ctm.m11 + p.y * ctm.m21 + p.z * ctm.m31 + ctm.m41; ny = p.x * ctm.m12 + p.y * ctm.m22 + p.z * ctm.m32 + ctm.m42; nz = p.x * ctm.m13 + p.y * ctm.m23 + p.z * ctm.m33 + ctm.m43; Point3d *np = [[[Point3d alloc] Point3dMakeX:nx Y:ny Z:nz] autorelease]; return np; }
もうひとつ、pplyTransformではtq_origそれぞれに視点(カメラ座標)からのパースペクティブをかけてtq_pers に保存しています。
- (Point3d *)getPerspectivePoint3D:(Point3d *)p { CGFloat newX = (*viewZ)*(p.x)/((*viewZ)-p.z); CGFloat newY = (*viewZ)*(p.y)/((*viewZ)-p.z); CGFloat newZ = p.z; return [[[Point3d alloc] Point3dMakeX:newX Y:newY Z:newZ] autorelease]; }
この時点で、視点はz軸上のviewZです。getPerspectivePoint3Dは、現在の視点(viewZ)からX0をX軸上のX0'へ、X1をX1'へと投影していることになります。

計算式は次の通りです。
x0' : viewZ = x0 : (viewZ - z0)
x0' * (viewZ - z0) = viewZ * x0
x0' = (viewZ * x0) / (viewZ - z0)
なお、割り算の分母((*viewZ)-p.z)と((*viewZ)-p.z)は0になってはまずいのですが、ここではviewZに十分大きな値(実際には+600)を与えているので、0除算の際の処理はは省略しています。また、これ以降は二次元データとしてx、yの値しか用いませんがzにはオリジナルの値をそのまま入れています。
描画までもう一息ですが、その前にもう一つやっておくことがあります。tq_persからオモテを向いている面だけを取り出してfrontq_persに入れます。また同じ面を、tq_origからfrontq_origへと入れておきます。
- (void)makeFrontq { frontqSize = 0; for (int i=0; i<6; i++) { if ([self isFront:tq_pers[i]]) { frontq_pers[frontqSize] = tq_pers[i]; frontq_orig[frontqSize] = tq_orig[i]; frontcolor[frontqSize] = color[i]; frontqSize++; } } } - (Boolean)isFront:(Quad3d *)q0 { //p[0]=>p[1]とp[0]=>p[2]の外積で面のオモテ、ウラを判断する double v1x = q0.p1.x - q0.p0.x; double v1y = q0.p1.y - q0.p0.y; double v2x = q0.p2.x - q0.p0.x; double v2y = q0.p2.y - q0.p0.y; double crossp = (v1x * v2y) - (v1y * v2x); if (crossp < 0) { return NO; } return YES; }
面のオモテ、ウラはベクトルの外積の性質を利用してisFrontで行います。対象の面のp[0]=>p[1]とp[0]=>p[2]との外積がプラスのとき、面はオモテを向いています。
さて、やっと描画の準備が整いました。次はCubeを描画するdrawCubeです。
- (void)drawCube:(CGContextRef)context { //既にこのCubeが描画されていればスキップする if (displayed) { return; } //eclipseにループがあった場合のエラー処理 if (visited > 1) { [self findLoop]; exit(0); } visited++; //eclipseの中身を先に描画する for (NSNumber *c1 in eclipse) { Cube *c = [cubearray objectAtIndex:[c1 intValue]]; [c drawCube:context]; } //このCubeの描画 for (int i=0; i<frontqSize; i++) { CGContextMoveToPoint(context,frontq_pers[i].p0.x, frontq_pers[i].p0.y); CGContextAddLineToPoint(context,frontq_pers[i].p1.x, frontq_pers[i].p1.y); CGContextAddLineToPoint(context,frontq_pers[i].p2.x, frontq_pers[i].p2.y); CGContextAddLineToPoint(context,frontq_pers[i].p3.x, frontq_pers[i].p3.y); CGContextAddLineToPoint(context, frontq_pers[i].p0.x, frontq_pers[i].p0.y); CGContextClosePath(context); CGContextSetFillColorWithColor(context, frontcolor[i].CGColor); CGContextDrawPath(context, kCGPathFillStroke); } displayed = YES; } - (void)findLoop { NSLog([NSString stringWithFormat:@"!!! findLoop cubeNo=%d", cubeNo]); for (Cube *c0 in cubearray) { NSLog([NSString stringWithFormat:@" -- CubeNo=%d", c0.cubeNo]); for (NSNumber *num in c0.eclipse) { NSLog([NSString stringWithFormat:@" * eclipse=%d", [num intValue]]); } } exit(0); }
Cubeの描画には、隠面消去を考慮しなければなりません。他のCubeに隠されるCubeは先に描画する必要がありますが、その先に描く必要のあるCubeの番号を呼び出し元のクラスであらかじめeclipse配列に入れています。このdrawCubeでは、そのeclipse内のCubeを描画してから自身を描画します。
eclipseについてはここだけでは説明が難しいので、描画の全体像とあわせて(たぶん)次回の<RubicCube>クラスで解説します。
なお、eclipseには場合によって内部にループを作ってしまうという欠陥があります。findLoopは、このループを検出したときのためのデバッグルーチンです。
Cubeにはあといくつか<RubicCube>クラスから呼び出されるルーチンがあります。getFrontqPersはfrontq_persを、getFrontqOrigはfrontq_origをゲットするルーチンです。そしてadd3DCtmRotateは、ローカル座標に回転を加えるルーチンです。
- (Quad3d *)getFrontqPers:(int)index { return frontq_pers[index]; } - (Quad3d *)getFrontqOrig:(int)index { return frontq_orig[index]; } - (void)add3DCtmRotate:(CGFloat)r X:(CGFloat)x Y:(CGFloat)y Z:(CGFloat)z { ctm3d_local = CATransform3DConcat(ctm3d_local, CATransform3DMakeRotation(r, x, y, z)); } - (void)dealloc { for (int i=0; i<6; i++) { [q[i] release]; [color[i] release]; [tq_orig[i] release]; [tq_pers[i] release]; [frontq_orig[i] release]; [frontq_pers[i] release]; [frontcolor[i] release]; } [eclipse release]; [super dealloc]; } @end
以上でCubeクラスは終わり、次回は<RubicCube>クラスの予定です。
ルービックキューブ 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>クラスに進みたいと思います。
ルービックキューブ I - 2
今回は<Point3d>と<Quad3d>の2つのクラスを紹介します。
<Point3d>は、三次元の点[x, y, z]を表すクラスです。
まずヘッダファイル<Point3d.h>。
<Point3d.h>:
#import <Foundation/Foundation.h> @interface Point3d : NSObject { CGFloat x; CGFloat y; CGFloat z; } @property CGFloat x; @property CGFloat y; @property CGFloat z; - (id)Point3dMakeX:(CGFloat)x0 Y:(CGFloat)y0 Z:(CGFloat)z0; @end
続いて<Point3d.m>。
<Point3d.m>:
#import "Point3d.h" @implementation Point3d @synthesize x, y, z; - (id)Point3dMakeX:(CGFloat)x0 Y:(CGFloat)y0 Z:(CGFloat)z0 { [super init]; x = x0; y = y0; z = z0; return self; } @end
特に説明の必要はないでしょう。ただ一つのメソッド[- (id)Point3dMakeX:(CGFloat)x0 Y:(CGFloat)y0 Z:(CGFloat)z0]は、CGFloat型のx、y、zをインスタンス変数に保存してそのオブジェクトを返します。
x、y、zは他のクラスから直接アクセスしたいので、@propertyと@synthesizeを定義しています。
この<Point3d>を4つ使用して、三次元の面を表すのが次の
<Quad3d.h>:
#import <Foundation/Foundation.h> #import "Point3d.h" @interface Quad3d : NSObject { Point3d *p0; Point3d *p1; Point3d *p2; Point3d *p3; } @property (retain, readwrite) Point3d *p0; @property (retain, readwrite) Point3d *p1; @property (retain, readwrite) Point3d *p2; @property (retain, readwrite) Point3d *p3; - (id)quad3dMakeP0:(Point3d *)a P1:(Point3d *)b P2:(Point3d *)c P3:(Point3d *)d; - (Point3d *)getP:(int)index; @end
<Quad3d.m>:
#import "Quad3d.h" @implementation Quad3d @synthesize p0, p1, p2, p3; - (id)quad3dMakeP0:(Point3d *)a P1:(Point3d *)b P2:(Point3d *)c P3:(Point3d *)d { [super init]; p0 = a; p1 = b; p2 = c; p3 = d; return self; } - (Point3d *)getP:(int)index { switch (index) { case 0: return p0; break; case 1: return p1; break; case 2: return p2; break; case 3: return p3; break; default: return nil; break; } } - (void)dealloc { [p0 release]; [p1 release]; [p2 release]; [p3 release]; [super dealloc]; } @end
これも難しいところはありません。
ただ、Point3dと同じように、4点p0、p1、p2、p3を直接アクセスするため@propertyと@synthesizeを定義しているほかに、インデックス付けして順番に取り出したいこともあるので、別途[- (Point3d *)getP:(int)index]というメソッドも用意しています。
なお、[- (id)quad3dMakeP0:(Point3d *)a P1:(Point3d *)b P2:(Point3d *)c P3:(Point3d *)d]で面を作る際に、p0〜p3はそれぞれ三次元ですから、変な値を渡すと結果は<平面>になりません。平面になるように値を渡すのは呼び出し側の責任です。
また、これも呼び出し側の問題ですが、p0〜p3は反時計周りを表の面として指定します。後に、このQuad3dを二次元に投影した際に、p0〜p3の視点からの相対位置で表を向いているか裏を向いているかを判断するためです。
次回は、Quad3dを6つ使って作る立方体<Cube>を解説します。
ルービックキューブ I - 1
ずいぶん間が空いてしまいました。
既存のコードを作り直す過程で、書き換える予定のなかった部分に潜在的なバグが見つかったり、また15パズルを含めて以前は無視していたメモリ管理を多少はまともにしようと考えてはまり込んだり、ずるずると遅れてしまいました。
ようやくコードが安定してきたので、これから数回にわたって<ルービックキューブ I>としてコードを紹介していきたいと思います。
<ルービックキューブ I>には、ブロックを9つづつスライスして動かすルーチンは組み込まれていません。初期状態の27個のブロック全体をぐりぐりと回転させることができるだけです。<ルービックキューブ II>ではスライスを実装したいと思います。
さて、まず今回作成する7つのクラスについて簡単に説明しておきます。
◇Point3dクラス
これは三次元の点を表すクラスです。
NSObjectのsubclassです。◇Quad3dクラス
Point3d4個で三次元の面を表すクラスです。
NSObjectのsubclassです。◇Cubeクラス
Quad3d6個で立方体を表すクラスです。ルービックキューブの一つ一つのブロックを表しています。
NSObjectのsubclassです。
このCubeクラスでは図形の三次元変換操作のために<QuartzCore.framework>の<QuartzCore/CATransform3D.h>をimportしています。◇RubicCubeクラス
Cube27個で、ルービックキューブ全体を表すクラスです。
NSObjectのsubclassです。◇MyWorldクラス
RubicCubeを描画するビューです。
これだけがUIViewのsubclassです。
ここまでが基本的な図形を表現しているクラスで、後の2つは図形の演算のための補助的なクラスです。
◇Ctm3dクラス
ローカルな座標をワールド座標系へ、ワールド座標系をビュー座標系へ変換するための三次元変換行列(CTM)を保存するクラスです。
NSObjectのsubclassです。
Cubeクラスと同じく、<QuartzCore.framework>の<QuartzCore/CATransform3D.h>をimportしています。◇FlatEquationクラス
図形の前後関係を調べるために使う平面の方程式を表すクラスです。
NSObjectのsubclassです。
クラスは以上です。
projectは、15パズルと同じView-based Applicationを使用しています。
開発環境についても書いておきます。
マシンはMacBook(Early 2009) MB881J/A、本日Mac OS X 10.6 Snow Leopardにアップグレードしました。XCodeはSnow Leopard付属の3.2、iPhone SDKもそれに合わせて3.0(Snow Leopard)にしました。
では、次回よりクラスの説明に入ります。