ルービックキューブ 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>クラスの予定です。