PV3Dで色立体をつくる

 Flash上においてPV3Dを使用した色立体をつくり、解説していきます。※現時点での最新版であるPapervision 3D 2.0 Great Whiteをもとに説明していきます。

Papervision3Dについて

 Papervision3Dとは、Flash上で動作する3Dエンジンのライブラリのことです。

01.試作段階におけるスクリーンショット

 Action Script3.0に対応していて、本来2D表現しかできないFlash上で3Dでの表現が可能になります。また、数ある3Dカスタムクラスのなかでも、機能が充実しているのが特徴です。

PV3Dの入手&インストール方法

 最新版のPV3Dを入手するには、tortoisesvnというバージョン管理システムを用います。ダウンロード、設定、詳しい使用方法などはここを参照してください。

tortoisesvnをインストールした後、右クリックのメニューでSVNチェックアウトを選択し、リポジトリURLに「http://papervision3d.googlecode.com/svn/」と入力すると、ダウンロードがはじまります。

 つぎにクラスパスを通し方を説明します。パブリッシュする際に、PV3Dのクラスライブラリを参照するようにするためパスを設定します。

 まず始めに、customclassesという新規フォルダを作成します。その中に、おとしたsvn\trunk\branches\GreatWhite\src内の"com", "fl", "org"をコピペします。

 さらにAdobe Flash CS3を起動し、編集→環境設定→カテゴリ内のActionScript→ 言語:ActionScript3.0設定を開きます。ウインドウ右下にある+をおしてcustomclassesまでのディレクトリをうてば終了です。(例えば、Cドライブ直下にcustomclassesがある場合、C:\customclassesとうちます。)

これで、制作環境が整いました。

制作する

 それでは、実際制作していきたいと思います。※試作段階であるため、色立体の骨組のみとなります。表色系も色立体も再現はされてません。

円柱座標から直交座標へ変換するクラスを作る。

 まず、色立体を作るにあたって、座標の変換を考えます。色立体は円柱状の形状であるため、直交座標系よりも円柱座標系のほうが扱いやすいと思われます。そこでPV3DのコアクラスのNumber3Dを継承した新しい座標保持用のクラスを作成していきました。

Cylinder_crds.as


/////Cylinder_crds.as//////////////////////////////////////////////////
//円柱座標から直交座標へと変換し、座標を保持するクラス///////////////////////
package {
	//ドキュメントクラスであるため、flaファイルでは通っていたライブラリのパスも
	//書く必要があります。Pointクラスを使うのでインポートしておきます。
	import flash.geom.Point;
	//PV3Dライブラリもインポートします。
	//Number3Dは3次座標系を表すクラスです。これを拡張していきます。
	import org.papervision3d.core.math.Number3D;

	public class Cylinder_crds extends Number3D {
		public var len;
		public var angle;
		public var c_y;

		private var pt:Point;
		//Pointは2次座標を保持するクラスです。メソッドに極座標を直交座標へ変換する関数polar();があるので、
		//それを使います。
		public function Cylinder_crds(len:Number=0,angle:Number=0,c_y:Number=0) {
			//コンストラクタCylinder_crds(半径,角度,高さ);
			this.len = len;
			this.angle = angle;
			this.c_y = c_y;
			pt = Point.polar(len,angle);
			//原点から長さ座標”len”を角度”angle”だけ移動したものを直交座標にします。X=pt.x Y=pt.yです。
			//ただし、PV3Dの3次座標ではyが高さで、zが奥行であるため
			//Number3D( x: Number=0, y: Number=0, z: Number=0 )がコンストラクタである。
			//親バージョンに対し、Number3D(pt.x,c_y,pt.y);といれます。
			//
			//素直に座標変換の式を自分で書いたほうがいいかも知れません・・・
			//x = len*Math.cos(angle);
			//z = len*Math.sin(angle);
			//y = y;
			//みたいに・・・
			super(pt.x,c_y,pt.y);//最後にNumber3Dに値をいれておきます。
		}
	}
}

/////Colorplane.as//////////////////////////////////////////////////
// Planeクラスを拡張したプリミティブ形状クラスです。///////////////////////////

package {
	//ライブラリからクラスをインポートします。
	import org.papervision3d.objects.primitives.Plane;
	//↑このクラスの元となるクラスです。板形状のオブジェクトをシーン上に描きます。
	import org.papervision3d.materials.special.CompositeMaterial;
	import org.papervision3d.materials.ColorMaterial;
	//↑ポリゴンの表面材質を決定するマテリアルクラスの一種です。
	//ColorMaterialクラスはポリゴン上を単一色で描画します。


	public class Colorplane extends Plane {
		public var color:uint;
		//↑後々カラーを習得する必要があるのでパブリックプロパティとしておきます。
		private var mate:ColorMaterial;
		private var width:Number;
		private var height:Number;

		public function Colorplane(color:uint,width:Number=10,height:Number=10) {
			//コンストラクタColorplane(色,横幅,縦幅);
			this.color = color;
			this.width = width;
			this.height = height;
			mate = new ColorMaterial(color,1,true);
			//↑コンストラクタはColorMaterial(color:Number = 0xFF00FF, alpha:Number = 1, interactive:Boolean = false);です。
			//三番目の引数は3Dオブジェクトへのマウスアクションなどのイベントを処理するか、どうかを決めます。
			//マウスオーバー時の反応がほしかったため、trueとしておきます。
			mate.doubleSided = true;
			//裏面を表示するかをきめるプロパティです。
			super(mate,width, height,1,1,null);
			//↑親バージョンであるPlaneクラスへ値を入れます。
			//コンストラクタはPlane(material:MaterialObject3D = null, width:Number = 0, height:Number = 0, segmentsW:Number = 0, segmentsH:Number = 0, initObject:Object = null);
			//segmentsW, segmentsHはポリゴンの分割数を設定します。
			//数が大きいほどポリゴン割れが目立たなくなりますが、その分重くなります。
		}
	}
}

カラーチップをつくる

次に、色立体のカラーチップを作ります。これは、PV3Dのプリミティブ形状にあるPlaneクラスをそのまま使用します。ここでは、Planeクラスを継承させ、座標と色のみを入力すればいいようにしたColorplaneクラスを制作しました。

Colorplane.as

本体のドキュメントクラスをつくる

 ここまでで作ったクラスとPV3Dをもとに色立体を作っていきます。

最初にPV3Dのセットアップをします。PV3DはシーングラフをベースにしたAPIです。Scene3DでFlash上にバーチャルな3Dの空間を作り、そこに3Dのオブジェクト、camera3Dを設定します。そして、3Dオブジェクトとcamera3Dの関係を計算し変換された図形がViewport3Dに描き出されます。

そのため、まずは、Scene3D、camera3D、Viewport3D、renderer(レンダリング用クラス)のインスタンスを作っていきます。今回は常に原点方向を向き続ける通常のcamera3Dでやっていきたいと思います。(空間内を自由に映したい場合はFreeCamera3Dなどを使えばいいようです。)各クラスのコンストラクタやメソッドはソースのほうを見てください。

 環境の設定が済んだら、次はScene3D上に3Dオブジェクトを配置していきます。まずは、3DオブジェクトをaddChild()するために空のDisplayObject3Dをつくり、これをコンテナ代わりにします。全体を回転させたいとき、これだけ指定すればいいので便利です。※ただし、子ノードのグローバル座標(Scene3Dでの座標)の取り方がわかりませんでした。親が基準のローカル座標になるようです。さらに、先に作ったクラス達を使いつつオブジェクトを配置していきます。ドキュメントクラスにはMunsellと書いてありますが、とりあえず、RGB値を各値でグラデーションした図形を作ります。(マンセルは目視で色を分類するので、自分にはハードルが高いです。w)ループ文を使い半径、角度、高さ、RGB値を段々といれてインスタンスを作っていき、それを配列に加えます。また、ここでカラーチップにイベントリスナーを登録しておきます。あとは、フレームに入るたびにレンダリングするようにすれば、基本的には終了です。

  今回は、さらにドラッグに合わせカメラが移動する機能、マウスオーバー時にチップに色が16進数で表示する機能を追加します。

  まず、カメラの移動からなのですが、前に述べたように常に原点方向を向くカメラであるため、向く方向は考えず、移動方法を考えればいいだけとなります。具体的には、PV3DのMathクラスより回転行列を使用します。(Matrix3D)この時、マウスイベントの取り方と、角度の制限が重要となってきます。マウスオーバー時のカラープロパティ取得方法はコードを参照して下さい。

Munsell.as

  これで完成です。

package {
	//Flashのライブラリをインポートします。
	import flash.display.*;
	import flash.text.*;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.text.*;
	//PV3Dのライブラリをインポートします。

	import org.papervision3d.scenes.Scene3D;
	//↑仮想の3次元空間を定義します。
	import org.papervision3d.view.Viewport3D;
	//↑3次元にあったものを2次元に描画する際、ここに描画されます。Spriteを継承しています。
	import org.papervision3d.cameras.Camera3D;
	//↑カメラです。
	import org.papervision3d.render.BasicRenderEngine;
	//↑レンダリングエンジンです。
	import org.papervision3d.objects.DisplayObject3D;
	//↑PlaneやCamera3DなどScene3D上におかれる3Dオブジェクトの親バージョンにあたるクラスです。
	//これ自体は、EventDispatcherを元に作られているようです。
	//これをカラのノードとしこの中にColorplaneをいれていきます。

	import org.papervision3d.core.math.Matrix3D;
	import org.papervision3d.core.proto.GeometryObject3D;
	//↑行列式を扱うクラスたちです。3次元での回転に使います。

	import org.papervision3d.events.InteractiveScene3DEvent;
	import org.papervision3d.core.utils.InteractiveSceneManager;
	//↑マウスイベントを扱うクラスたちです。

	public class Munsell extends Sprite {
		//環境///////////////////////////////////////////////////////////////////////
		private var camera:Camera3D;
		private var scene:Scene3D;
		private var rootNode:DisplayObject3D;
		private var viewport:Viewport3D;
		private var renderer:BasicRenderEngine;
		private var chip:Colorplane;
		private var Arr_chip:Array = new Array();//これにColorplaneをいれていきます。

		//座標///////////////////////////////////////////////////////////////////////
		private var plane_w:Number =100;//Colorplaneの横幅。
		private var plane_h:Number = 100;//Colorplaneの縦幅。

		private var pt:Cylinder_crds;//円柱座標クラス。たいして使いませんでした。w

		//円柱のそれぞれの座標。
		private var hue:Number;//円の角度
		private var chroma:Number;//円の半径
		private var value:Number;//円柱の高さ

		//円柱のそれぞれの分割数。
		private var hue_num:int = 20;//円の角度の分割数
		private var chroma_num:int = 14;//円の半径におけるカラーチップの枚数
		private var chroma_inc:Number = 150;//円の半径におけるカラーチップ間の間隔
		private var value_num:int = 10;//円柱の高さにおけるカラーチップの枚数
		private var value_inc:Number = 150;//円柱の高さにおけるカラーチップ間の間隔

		//カメラの座標を移動するときに使う座標。
		private var camera_0X:int;//マウスのボタンを押したときのx座標
		private var camera_0Y:int;//マウスのボタンを押したときのy座標
		private var cameraX:int;//X軸に対するカメラの角度
		private var cameraY:int;//Y軸に対するカメラの角度

		//色///////////////////////////////////////////////////////////////////////
		private var color:uint;
		private var red:uint;
		private var green:uint;
		private var blue:uint;

		//文字///////////////////////////////////////////////////////////////////////
		private var txt_color:TextField;//色情報を画面に表示する際にしようする表示場所。
		private var posX_txt:Number = 100;//表示場所のx座標
		private var posY_txt:Number = 100;//表示場所のy座標


		public function Munsell():void {
			stage.frameRate = 60;
			stage.quality   = "HIGH";
			stage.scaleMode = "noScale";//使用する拡大 / 縮小モードを指定する。
			stage.align = StageAlign.TOP_LEFT;//Flash Player またはブラウザでのステージの配置を指定する
			this.addEventListener(Event.ENTER_FRAME, _enterframe);//フレームが読み込まれるたびに_enterframe()関数が実行されるよう登録します。
			this.stage.addEventListener(MouseEvent.MOUSE_DOWN,mouseDown);
			//↑マウスのボタンを押した際にmouseDown()関数が実行されます。
			init();
		}
		private function init():void {
			//________________________________________________________________________________________viewport
			viewport = new Viewport3D(0,0,true,true,true,true);//コンストラクタViewport3D(viewportWidth:Number = 640, viewportHeight:Number = 480, autoScaleToStage:Boolean = false, interactive:Boolean = false, autoClipping:Boolean = true, autoCulling:Boolean = true)
			//4番目の引数はマウスイベントを受け取るかどうかの設定、
			//5、6番目の引数は見えないポリゴンを計算するかどうかの設定です。trueで計算しません。
			//軽くなるのでtrueにしておきます。
			addChild(viewport);//viewportをステージに入れ子します。
			//________________________________________________________________________________________renderer
			renderer = new BasicRenderEngine();//レンダリングエンジンを設定します。
			//________________________________________________________________________________________camera
			camera = new Camera3D();//カメラの設定です。
			camera.x = camera.y = 0;
			camera.z = -5000;
			camera.focus = 300;
			camera.zoom = 2;
			//________________________________________________________________________________________scene
			scene = new Scene3D();//Scene3Dのインスタンスを宣言する。
			rootNode = new DisplayObject3D();//色立体を入れるためのコンテナをつくる。
			scene.addChild(rootNode);//3DオブジェクトはScene3Dへと入れ子していきます。
			//________________________________________________________________________________________text
			//色情報を表示する場所を用意しておく。
			txt_color = new TextField();
			txt_color.width = 100;
			//場所、フォーマットなどの設定です。適当に。
			txt_color.x = posX_txt - txt_color.width/2;
			txt_color.y = posY_txt - txt_color.height/2;
			txt_color.autoSize = TextFieldAutoSize.LEFT;
			txt_color.textColor = 0xFFFFFF;
			addChild(txt_color);//3Dオブジェクトではないので、ステージに入れ子します。

			//________________________________________________________________________________________color_chip

			//Colorplaneを円柱状に配置していくためのループ文です。ここで色も決めてしまいます。
			for (var i:int=0; i<hue_num; i++) {
				hue = i*360/hue_num/180*Math.PI;//円の角度を計算します。
				for (var k:int=0; k<value_num; k++) {
					value = k*value_inc-value_inc*value_num/2;//円柱の高さを計算します。
					for (var j:int=0; j<chroma_num; j++) {
						chroma = j*chroma_inc+1000;//円の半径を計算します。+1000で真ん中に空間を作りドーナツ状をつくっています。

						//RGB値の設定です。
						red = (i+1)*255/hue_num;//円の角度ごとに赤色が薄くしていきます。
						green = (j+1)*255/chroma_num;//円の半径ごとに緑色が薄くしていきます。
						blue = (k+1)*255/value_num;//円柱の高さごとに青色が薄くしていきます。

						color = Number("0x"+red.toString(16)+green.toString(16)+blue.toString(16));//それぞれの値を16進数に変換します

						pt = new Cylinder_crds(chroma,hue,value);//円柱座標から直交座標に変換します。

						chip = new Colorplane(color,plane_w,plane_h);
						//↓値を代入した後Colorplaneの向きを整えます。これで、上向きかつ中心方向に向くと思います。
						chip.rotationY -= i*360/hue_num + 90;
						chip.rotationX += 90;

						chip.addEventListener(InteractiveScene3DEvent.OBJECT_OVER,mouseOver);
						//イベントリスナーに登録します。
						//マウスオーバー以外のイベントを使いたい場合はInteractiveScene3DEvent.asを調べて見てください。
						//ここでさっきの座標の代入を行います。
						chip.x = pt.x;
						chip.z = pt.z;
						chip.y = pt.y;
						//DisplayObject3Dに入れ子した後、配列Arr_chipにchipを加えます。
						rootNode.addChild(chip);
						Arr_chip.push(chip);//push()は配列の最後にエレメントを追加するメソッドです。

					}
				}
			}

		}
		private function _enterframe( event:Event ):void {
			//rootNode.rotationY += 2;//rootNodeを回転させます。
			renderer.renderScene(scene,camera,viewport);//レンダリングを開始するメソッドです。これで、描画がはじまります。
		}
		private function mouseDown(event:Event):void {
			//マウスダウン時の座標を記録します。
			camera_0X =(stage.mouseX-(stage.width/2));
			camera_0Y =(stage.mouseY-(stage.height/2));
			//マウスダウンのイベントリスナーをはずし、マウスが移動した際とマウスのボタンが上がった時の
			//イベントを登録します。これでドラッグ&ドロップを再現します。
			stage.removeEventListener(MouseEvent.MOUSE_DOWN,mouseDown);
			stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMove);
			stage.addEventListener(MouseEvent.MOUSE_UP,mouseUp);
		}
		private function mouseMove(event:Event):void {
			//移動距離の差分をとり、それをカメラの角度にたしていきます。
			//これでマウスの移動量に合わせカメラの移動もできるようになります。
			cameraX += ((stage.mouseY-(stage.height/2)) - camera_0Y)/3;
			cameraY -= ((stage.mouseX-(stage.width/2)) - camera_0X)/3;
			//X軸に対してのカメラの角度で、上下に90度以上の角度がつくとカメラが上と下とで反転してしまうため、
			//90度でストップするようにします。
			if (-89<cameraX&&cameraX<89) {
			} else if (-89>=cameraX) {
				cameraX = -89;
			} else {
				cameraX = 89;
			}
			//行列成分とプロパティの対応表
			// Matrix3D(n11,n12,n13,n14,n21,n22,n23,n24,n31,n32,n33,n34)
			//_                        _
			//| n11,n12,n13,n14 |
			//| n21,n22,n23,n24 |
			//| n31,n32,n33,n34 |
			//-                       -
			var v:Matrix3D = new Matrix3D([0,0,-5000,1,0,0,0,0,0,0,0,0]);
			//_          _
			//| x,y,z, 1 |
			//| 0,0,0,0 |
			//| 0,0,0,0 |
			//-         -
			var vrx:Matrix3D = Matrix3D.rotationX(cameraX/180*Math.PI);
			//X軸の回転行列を作る。
			//|10 00|
			//|0cosθ sinθ0|
			//|0-sinθ cosθ0|
			//|00 01|
			var vry:Matrix3D = Matrix3D.rotationY(cameraY/180*Math.PI);
			//Y軸の回転行列を作る。
			//|cosθ0-sinθ0|
			//|0100|
			//|sinθ0cosθ0|
			//|0001|
			var va0:Matrix3D = Matrix3D.multiply(v,vrx);
			//行列vと行列vrxの積 
			var va1:Matrix3D = Matrix3D.multiply(va0,vry);
			//行列va0と行列vryの積
			//最終的な行列の値を代入。
			camera.x = va1.n11;
			camera.y = va1.n12;
			camera.z = va1.n13;

			//マウスの座標を更新します。
			camera_0X =(stage.mouseX-(stage.width/2));
			camera_0Y =(stage.mouseY-(stage.height/2));

		}

		private function mouseUp(event:Event):void {
			//マウスイベントを元の形へと戻す。
			stage.removeEventListener(MouseEvent.MOUSE_MOVE,mouseMove);
			stage.removeEventListener(MouseEvent.MOUSE_UP,mouseUp);
			stage.addEventListener(MouseEvent.MOUSE_DOWN,mouseDown);
		}
		private function mouseOver(event:InteractiveScene3DEvent):void {
			//Colorplaneマウスオーバー時に色を習得する。注意すべきはfor文の都合上Colorplaneの名前がずれていることである。
			txt_color.text = "0x"+Arr_chip[int(event.displayObject3D.name) - 4].color.toString(16);
		}
	}
}

パブリッシュする

ドキュメントクラスであるこのコードをパブリッシュするには、まず今まで作ったクラスを同一のディレクトリへといれます。その後、そのディレクトリ内に新規のflaファイルを作ります。※名前は何でもいいです。そして、ステージをクリックするとプロパティにドキュメントクラスという欄がでます。そこにMunsellと入力してパブリッシュをすれば、swfファイルが作成されます。

Munsell.swf

ソース一式(munsell.zip)