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