サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

EaselJSのMatrix2Dクラスで3次元空間の回転を表現する

ID: FN1211001 Technique: HTML5 and JavaScript

EaselJSのMatrix2Dクラスを使ったインスタンスの変形」で、矩形のインスタンスを変換行列により任意の3頂点が定める平行四辺形に変えてみました。この考え方を応用すれば、6つの正方形の面から立方体を組立てて、3次元空間で回す表現ができます(図001)。本稿は学習の進んだ方向けの解説になりますので、スクリプトも一部クラスを用いました。

図001■頂点座標に合わせて変換した平行四辺形を組合わせると3次元空間の立方体が表現できる
図001


01 面のインスタンスをひとつCanvasに置く

立方体は、8つの頂点で定められます。もっとも、Matrix2Dオブジェクトでは、矩形は平行四辺形にしか変換できず、台形にはなりません。そのため、3頂点でひとつの面が決まります。すると、立方体は6頂点でつくれます。そこで、立方体の6つの頂点に0から始まる整数インデックスを与え、ひとつの面は3頂点で表します。原点(0, 0, 0)を立方体の中心に置き、1辺の半分を変数(unit)にすれば、立方体の6面と各頂点座標は下図002のように定められます。

図002■立方体の頂点番号と座標を決める
図002左 図002右

3次元座標空間で考えるなら、正方形を回したからといって平行四辺形にかたちが変わったりなどしません。変形して見えるのは、2次元の平面に投影するからです。よりリアルな投影では遠近法が加わり、同じ2点間が遠いほど短く見えます。けれど、矩形を台形に変形できないMatrix2Dクラスによる変換では、遠くても近くても同じ2点間の距離は等しくなります。つまり、3次元空間座標のz座標値は0にして、xy座標をそのまま2次元平面に投影します。

3次元座標変換
(x, y, z)
 投影 
2次元平面
(x, y)

まずは、立方体6面のうち前面ひとつだけBitmapインスタンスでつくってCanvasに置きます(図003)。まだ、回転もさせません。けれども、後あと画像を6面を置いて3次元空間で回すという「野望」があるために、今の段階では大掛かりに見える仕掛けを組込んでおきます。

図003■前面の頂点番号と座標
図010左

script要素全体は、後にコード001として掲げます。画像をひとつ読込んでCanvasに置くところから始めて、順に処理を加えていきましょう。抜書きするステートメントの行番号は、後掲コード001にもとづきます。

ファイルの読込みについては「PreloadJSで外部画像ファイルの読込みを待つ」に解説しました。このお題では、6面の画像ファイルを読込むことになりますので、PreloadJS.loadManifest()メソッドとAbstractLoader.onCompleteイベントを用いています(HTMLドキュメント第31〜33行目)。

6面のインスタンスはまとめて扱えるように、親となるContainerインスタンス(holder)をCanvasの真ん中に置き(HTMLドキュメント第34〜36行目)、面のインスタンス(face)はその表示リストに加えています(第69および第71行目)。なお、CreateJSのクラスには、新たなバージョンから加わった名前空間createjsを添えて参照しています(「CreateJS Suiteのクラスに名前空間が設定される」参照)。

●HTMLドキュメント
  1. <script src="easeljs/utils/UID.js"></script>
  2. <script src="easeljs/geom/Matrix2D.js"></script>
  3. <script src="easeljs/geom/Point.js"></script>
  4. <script src="easeljs/events/MouseEvent.js"></script>
  5. <script src="easeljs/display/DisplayObject.js"></script>
  6. <script src="easeljs/display/Container.js"></script>
  7. <script src="easeljs/display/Stage.js"></script>
  8. <script src="easeljs/display/Bitmap.js"></script>
  1. <script src="preloadjs/AbstractLoader.js"></script>
  2. <script src="preloadjs/PreloadJS.js"></script>
  3. <script src="preloadjs/TagLoader.js"></script>
  4. <script src="preloadjs/XHRLoader.js"></script>
  1. <script src="Face3D.js"></script>
  1. <script>
  2. var stage;
  3. var manifest = [
      "images/000.png"
      ];
  4. var holderPositon;
  1. var holder;
  1. var faces_array = [];
  2. function initialize() {
  3.   var canvasElement = document.getElementById("myCanvas");
  4.   stage = new createjs.Stage(canvasElement);
  1.   var loader = new createjs.PreloadJS(false);
  2.   loader.onComplete = drawCube;
  3.   loader.loadManifest(manifest);
  4.   holderPositon = new createjs.Point(canvasElement.width / 2, canvasElement.height / 2);
  5.   holder = new createjs.Container();
  6.   setAppearance(holder, holderPositon.x, holderPositon.y);
  7.   setCubeData();
  8.   initializeCube();
  1. }
  2. function setAppearance(instance, nX, nY) {
  3.   instance.x = nX;
  4.   instance.y = nY;
  5.   stage.addChild(instance);
  6. }
  1. function setCubeData() {
  1.   faces_array.push([0, 1, 2]);
  1. }
  2. function initializeCube() {
  1.   var i = 0;
  2.   var face = new Face3D(manifest[i], faces_array[i]);
  3.   faces_array[i] = face;
  4.   holder.addChild(face);
  1. }
  2. function drawCube() {
  1.   stage.update();
  2. }
  1. </script>

画像ファイルは、Bitmapインスタンスに納めて扱えます。さらにプロパティやメソッドを加えられるように、Bitmapのサブクラス(Face3D)を定めました。JavaScriptでは、継承したいクラスのオブジェクトはFunction.prototypeプロパティに上書きします。すると、そのオブジェクトをつくったクラスのプロパティとメソッドがすべて継承されます

コンストラクタ関数.prototype = new スーパクラスのコンストラクタ()

取りあえず、面の頂点番号を配列でプロパティ(indices)にもたせました(クラスFace3D第3行目)。メインのスクリプトからは、画像ファイルのURLと頂点番号の配列を引数で渡されます(前掲HTMLドキュメント第69行目)。各頂点の座標は、後でまた別のクラスを設けて扱います。

●Face3D.js
  1. function Face3D(file, indices) {
  2.   this.initialize(file);
  3.   this.indices = indices;
  4. }
  5. Face3D.prototype = new createjs.Bitmap();

これで、画像を読込んだ面の(Face3D)インスタンスがひとつCanvasに置かれます(図004)。座標はまったく触れていないので、親ContainerインスタンスのあるCanvas中央が、面のインスタンスのデフォルトの基準点である左上角になります。

図004■面のインスタンスがCanvasの中央にデフォルト状態で配置
図004


02 頂点座標に合わせて面のインスタンスを変換する

つぎに、立方体の頂点座標の情報を加えます。座標はxyzの3次元ですので、その値を入れるクラスは新たに定めます(クラスPoint3D第1〜5行目)。

●Point3D.js
  1. function Point3D(nX, nY, nZ) {
  2.   this.x = nX;
  3.   this.y = nY;
  4.   this.z = nZ;
  5. }

立方体の6頂点座標は変数(cubePoints_array)の配列に納めて(HTMLドキュメント第23および第53〜55行目)、面には座標をもたせません。頂点は複数の面が共有するため、座標の変換はまとめて行い、その後各面に割り振った方が効率的だからです。

面を描く関数(drawCube())が、面の3頂点の座標を調べ、それらの座標に合わせてMatrix2D.decompose()メソッドにより面を変換します(HTMLドキュメント第79〜86行目)。面の頂点座標を配列で返すメソッド(getVertices())は、面のクラス(Face3D)に加えました。また、3頂点座標から変換行列のMatrix2Dオブジェクトを求めるメソッド(transform())も新たなクラス(MathUtils)に定めます。それらのメソッドを順に見ましょう。

●HTMLドキュメント
  1. <script src="MathUtils.js"></script>
  2. <script src="Face3D.js"></script>
  3. <script src="Point3D.js"></script>
  4. <script>
  1. var unit = 100 / 2;
  2. var cubePoints_array = [];
  1. function setCubeData() {
  2.   cubePoints_array[0] = new Point3D(unit, -unit, -unit);
  3.   cubePoints_array[1] = new Point3D(-unit, -unit, -unit);
  4.   cubePoints_array[2] = new Point3D(-unit, unit, -unit);
  1. }
  1. function drawCube() {
  1.   var i = 0;
  2.   var face = faces_array[i];
  3.   var vertices = face.getVertices(cubePoints_array);
  4.   var point0 = vertices[0];
  5.   var point1 = vertices[1];
  6.   var point2 = vertices[2];
  1.   var size = face.getSize();
  2.   var matrix = MathUtils.transform(size, point0, point1, point2);
  3.   matrix.decompose(face);
  1. }
  1. </script>

クラスのインスタンスにメソッドを加えるときも、Function.prototypeプロパティを用います。Function.prototypeプロパティに関数を定めれば、すべてのインスタンスにメソッドとして備わります。

コンストラクタ関数.prototype.メソッド = function() {}

面を定めるクラス(Face3D)には、引数に受取った頂点座標の配列から、自らの面の頂点番号の座標を配列にして返すメソッド(getVertices())が加わりました。引数の配列には、頂点番号のインデックスにその頂点座標が納められているので、プロパティ(indices)の頂点番号のエレメントを順に取出して、戻り値の配列に入れています(クラスFace3D第8〜11行目)。また、もとイメージの幅と高さをPointオブジェクトで返すメソッド(getSize())も定めました(第13〜17行目)。

●Face3D.js
  1. Face3D.prototype.getVertices = function(points2D) {
  2.   var vertices = [];
  3.   for (var i = 0; i < 3; i++) {
  4.     vertices[i] = points2D[this.indices[i]];
  5.   }
  6.   return vertices;
  7. };
  8. Face3D.prototype.getSize = function() {
  9.   var myImage = this.image;
  10.   var size = new createjs.Point(myImage.width, myImage.height);
  11.   return size;
  12. };

座標を変換するメソッド(transform())は、数学的な座標変換のための新たなクラス(MathUtils)を定義して加えました。3頂点座標を定めた変換の演算については、前出「EaselJSのMatrix2Dクラスを使ったインスタンスの変形」で詳しく解説しました。ただし、ここで定めたメソッドは、インスタンスを直接変形はせず、変換行列のMatrix2Dオブジェクトを返します(クラスMathUtils第7〜14行目)。この方が、変換対象のインスタンスをかぎることなく、メソッドがより幅広く使い回せるからです。

なお、Function.prototypeプロパティでなく、コンストラクタ関数に直接定められたメソッドは、クラスを参照して静的に呼出します(前掲HTMLドキュメント第85行目)。

コンストラクタ関数.静的メソッド = function() {}
●MathUtils.js
  1. function MathUtils() {}
  2. MathUtils.transform = function(size, point0, point1, point2) {
  3.   var point1_0_x = point0.x - point1.x;
  4.   var point1_0_y = point0.y - point1.y;
  5.   var point1_2_x = point2.x - point1.x;
  6.   var point1_2_y = point2.y - point1.y;
  7.   var matrix = new createjs.Matrix2D();
  8.   matrix.tx = point1.x;
  9.   matrix.ty = point1.y;
  10.   matrix.a = point1_0_x / size.x;
  11.   matrix.b = point1_0_y / size.x;
  12.   matrix.c = point1_2_x / size.y;
  13.   matrix.d = point1_2_y / size.y;
  14.   return matrix;
  15. };

これで、Canvasに置かれた画像のインスタンスは、定められた3頂点の座標に変換されて表示されます(図005)。複数画像を面のインスタンスとして読込み、立方体の頂点座標に合わせて変換して貼りつける仕組みが整いました。

図005■読込んだ画像がCanvasの中央に表示
図005


03 マウスポインタの位置に応じてインスタンスをy軸とx軸で回す

複数の面を加える前に、3次元座標の回転を組入れましょう。まずは、2次元平面で原点を中心に、座標(x, y)を角度θ回した座標(x', y')はつぎの式で導けます。

【座標(x, y)を角度θ回転する】
x' = x cosθ - y sinθ
y' = x sinθ + y cosθ

3次元空間で回転する場合はどうかというと、さほど複雑にはなりません。2次元の回転は、3次元から見れば、z軸で回すことを意味します。ですから、あと2軸、つまりx軸とy軸周りの回転を加えればよいのです。そして、どの軸で回そうと、座標の求め方は変わりません。ただ、軸と座標を読替えるだけです。

普通の地球儀は、南北を軸として東西方向に回転します。けれど、ちょっと高級な地球儀には、もうひとつ回転軸を加えて南北にも自由に回せるものがあります。それと同じで、3次元の座標は、yz平面をx軸で回し、zx平面をy軸で回し、さらにxy平面をz軸で回せば、原点(0, 0, 0)を中心とした回転が表せるのです。

では、3次元空間で座標を回転するスクリプトに移ります。まず、2次元平面における座標の回転を、数学的な座標変換のクラス(MathUtils)にメソッド(rotatePoint2D())として加えました(第19〜26行目)。引数にはラジアン角とxy座標を受取ります。その角度のsinおよびcosを求めたら、前述の式により回転したxy座標を計算して、Pointオブジェクトで返しています。

●MathUtils.js
  1. MathUtils.rotatePoint2D = function(nRadians, nX, nY) {
  2.   var nSin = Math.sin(nRadians);
  3.   var nCos = Math.cos(nRadians);
  4.   var point2D = new createjs.Point();
  5.   point2D.x = nCos * nX - nSin * nY;
  6.   point2D.y = nSin * nX + nCos * nY;
  7.   return point2D;
  8. };

つぎに、3次元座標のクラス(Point3D)には、3次元空間で座標を回転するメソッド(rotate())を加えます(第6行目)。メソッドは、xyz軸それぞれを中心としたラジアンの回転角を引数に受取ります(第6行目)。後の処理は前述のとおり、オブジェクトのxyz座標値を、x軸(第10行目)、y軸(第13行目)、z軸(第16行目)で順に回転し、結果のxyz座標値を自らのプロパティに設定し直します(第19〜21行目)。

●Point3D.js
  1. Point3D.prototype.rotate = function(nRadianX, nRadianY, nRadianZ) {
  2.   var nX = this.x;
  3.   var nY = this.y;
  4.   var nZ = this.z;
      // X軸で回転
  5.   var pointX = MathUtils.rotatePoint2D(nRadianX, nY, nZ);
  6.   nY = pointX.x;
  7.   nZ = pointX.y;
      // Y軸で回転
  8.   var pointY = MathUtils.rotatePoint2D(nRadianY, nZ, nX);
  9.   nZ = pointY.x;
  10.   nX = pointY.y;
      // Z軸で回転
  11.   var pointZ = MathUtils.rotatePoint2D(nRadianZ, nX, nY);
  12.   nX = pointZ.x;
  13.   nY = pointZ.y;
  14.   this.x = nX;
  15.   this.y = nY;
  16.   this.z = nZ;
  17. };

インスタンスを、マウスポインタの位置に応じて、3次元空間で回しましょう。マウスポインタの座標を調べる関数(getRotations())は、Stage.onMouseMoveイベントのハンドラに定めました(第30行目)。ポインタの座標から求めるxyz各軸回りの回転角(ラジアン)は、Objectインスタンス(rotations)のプロパティx、y、zとして記録します(第21行目)。そして、イベントハンドラ(getRotations())は、マウスポインタのxy座標を調べたら、それぞれCanvas中央からの差を求めて調整係数(sensitivity)を乗じたうえで、xy各軸回りの回転角とします(第25および第47〜50行目)。

Tickerクラスには、アニメーションの関数(drawCube())をリスナーに加えました(第39行目)。リスナー関数は、頂点座標の配列(cubePoints_array)と3次元の回転角のオブジェクト(rotations)を、2次元に投影する関数(transformCubePoints)の引数に渡し、投影後の座標が納められた配列(points2d)を受取ります(第75行目)。その配列を引数として面のメソッド(getVertices())に渡せば、面の3頂点を定める配列(vertices)が戻り値として得られます(第79行目)。

後は、すでにご説明した処理にしたがって、面のインスタンス(face)は3次元空間で回転した頂点座標に変換されることになります。なお、今回のお題ではz軸による回転はありません。ですから、z軸で回す処理(前掲クラスPoint3D第16〜18行目)は除いた方が効率は高まります。けれど、必要があればz軸による回転が加えられる仕様として残すことにします。

●HTMLドキュメント
  1. <script src="easeljs/utils/Ticker.js"></script>
  1. var rotations = {x:0, y:0, z:0};
  1. var sensitivity = 1 / 500;
  1. function initialize() {
  1.   stage.onMouseMove = getRotations;
  1.   createjs.Ticker.addListener(drawCube);
  2. }
  1. function getRotations(eventObject) {
  2.   var mouseX = eventObject.stageX;
  3.   var mouseY = eventObject.stageY;
  4.   rotations.x = (mouseY - holderPositon.y) * sensitivity;
  5.   rotations.y = (mouseX - holderPositon.x) * sensitivity;
  6. }
  1. function drawCube() {
  2.   var points2d = transformCubePoints(cubePoints_array, rotations);
      // var vertices = face.getVertices(cubePoints_array);
  1.   var vertices = face.getVertices(points2d);
  1. }
  2. function transformCubePoints(points_array, myRotations) {
  3.   var points2D_array = [];
  4.   var nRadianX = myRotations.x;
  5.   var nRadianY = myRotations.y;
  6.   var nRadianZ = myRotations.z;
  7.   var nLength = points_array.length;
  8.   for (var i = 0; i < nLength; ++i) {
  9.     var myPoint3D = points_array[i];
  10.     myPoint3D.rotate(nRadianX, nRadianY, nRadianZ);
  11.     points2D_array[i] = new createjs.Point(myPoint3D.x, myPoint3D.y);
  12.   }
  13.   return points2D_array;
  14. }

これでマウスポインタの水平方向の動きはy軸、垂直方向の動きはx軸回りに、3次元空間で面のインスタンスを回転させます(図006)。面がひとつだけですので、どちらに回っているのか少しわかりにくいかもしれません。

図006■マウスポインタの水平・垂直方向の位置によりインスタンスは3次元空間で回転
図006左   図006右


04 立方体の6面をマウスポインタの位置に応じてy軸とx軸で回す

読込む画像を6つに増やし(images/000.png〜005.png)、6面で立方体にしましょう。頂点番号とそれぞれの座標は、前述01「面のインスタンスをひとつCanvasに置く」で考えた(前掲図002)とおりに定めます(図007)。すると、頂点座標と頂点番号の設定は立方体の6頂点・6面分に増やし、座標変換も6つの面に対して行うよう手をくわえることになります。

図007■立方体の6頂点の番号と座標を定める
図007

まず、読込む画像のURLは配列に納めておきます(第19行目)。つぎに、立方体6頂点の座標と6面それぞれの3頂点番号の組を変数に与える関数(setCubeData())です(第52〜65行目)。各面の3頂点番号は子配列にして、親配列の変数(faces_array)に納めます(第9および第59〜64行目)。また、面をつくる関数(initializeCube())は、for文で6面をつくって配列(faces_array)に入れます(第68〜72行目)。

そして、AbstractLoader.onCompleteイベントのハンドラでもあり、Ticker.addListener()メソッドでアニメーションのリスナーとして加えた立方体を描画する関数(drawCube())です(第32および第39行目)。for文で6面すべての大きさ(size)と3頂点座標(point0、point1、point2)を調べ、それらの座標に合わせて変換しています(第77〜86行目)。

●HTMLドキュメント
  1. var manifest = [
      "images/000.png",
      "images/001.png",
      "images/002.png",
      "images/003.png",
      "images/004.png",
      "images/005.png"
      ];
  1. function initialize() {
  1.   loader.onComplete = drawCube;
  1.   createjs.Ticker.addListener(drawCube);
  2. }
  1. function setCubeData() {
  2.   cubePoints_array[0] = new Point3D(unit, -unit, -unit);
  3.   cubePoints_array[1] = new Point3D(-unit, -unit, -unit);
  4.   cubePoints_array[2] = new Point3D(-unit, unit, -unit);
  5.   cubePoints_array[3] = new Point3D(unit, -unit, unit);
  6.   cubePoints_array[4] = new Point3D(unit, unit, unit);
  7.   cubePoints_array[5] = new Point3D(-unit, unit, unit);
  8.   faces_array.push([0, 1, 2]);   // 前面
  9.   faces_array.push([3, 4, 5]);   // 後面
  10.   faces_array.push([4, 3, 0]);   // 右側面
  11.   faces_array.push([5, 2, 1]);   // 左側面
  12.   faces_array.push([1, 0, 3]);   // 上面
  13.   faces_array.push([2, 5, 4]);   // 底面
  14. }
  15. function initializeCube() {
  16.   var nLength = faces_array.length;
      // var i = 0;
  17.   for (var i = 0; i < nLength; i++) {
  18.     var face = new Face3D(manifest[i], faces_array[i]);
  19.     faces_array[i] = face;
  20.     holder.addChild(face);
  21.   }
  22. }
  23. function drawCube() {
  24.   var points2d = transformCubePoints(cubePoints_array, rotations);
  25.   var nLength = faces_array.length;
      // var i = 0;
  26.   for (var i = 0; i < nLength; i++) {
  27.     var face = faces_array[i];
  28.     var vertices = face.getVertices(points2d);
  29.     var point0 = vertices[0];
  30.     var point1 = vertices[1];
  31.     var point2 = vertices[2];
  32.     var size = face.getSize();
  33.     var matrix = MathUtils.transform(size, point0, point1, point2);
  34.     matrix.decompose(face);
  35.   }
  36.   stage.update();
  37. }

これで、マウスポインタの位置に応じて立方体がy軸とx軸で回転し、6面のインスタンスはそれぞれの頂点座標に合わせて変形されます。したがって、立方体が3次元空間で回転しているような表現にはなります。

ただし、ひとつ大きな問題が残っています。それは、面の重ね順です。初めにインスタンスをStageオブジェクトの表示リストに加えた重なりは、回転してもそのまま変わらず、3次元表現における正しい面の並べ替えが行われません(図008)。この面の重なりが正しく整えられれば、本稿のお題は整います。

図008■面の重なりが3次元表現の正しい順序にならない
図008


05 立方体の裏向きの面は隠す

面の重ね順をどう決めるかは、3次元表現で重要な課題です。コンテンツによっていくつかの考え方があり、処理の重さや精確さも変わってきます。ただ、本稿のお題の場合、簡単なのは裏返った面を表示しないことです。すると、表向きの面同士が重なり合うことはないので、それらの重ね順は考えずに済みます。では、面の裏表をどう確かめるかです。実は、その布石はすでに売ってあります。それは、各面の頂点番号の決め方です(setCubeData())。

●HTMLドキュメント
  1. function setCubeData() {
  1.   faces_array.push([0, 1, 2]);   // 前面
  2.   faces_array.push([3, 4, 5]);   // 後面
  3.   faces_array.push([4, 3, 0]);   // 右側面
  4.   faces_array.push([5, 2, 1]);   // 左側面
  5.   faces_array.push([1, 0, 3]);   // 上面
  6.   faces_array.push([2, 5, 4]);   // 底面
  7. }

面の3つの頂点番号は、右上・左上・左下の順で、半時計回りに定めました(図008)。すると、2次元平面に投影したとき、3頂点番号が時計回りの位置になっていたら、その面は裏返っていることになるのです。そして、頂点番号の回る向きは、2次元ベクトルの外積で確かめられます。

図008■立方体の各面の3頂点番号は反時計回りの順序で定めた
図008左
立方体に定めた6頂点番号
図008右
前面・右側面・底面の頂点番号

インスタンスの右上角頂点から左上角頂点のベクトルと、右上角から左下角の頂点のベクトルのふたつの外積を求めます。その値が負のとき、ふたつのベクトルは反時計回りの位置にあり、面は手前向きです(図009)。2次元平面のベクトルをそれぞれA(ax, ay)およびB(bx, by)とすると、外積A×Bはつぎの式で導かれます。

A×B = axby - aybx

図009■ふたつの2次元ベクトルの外積が負なら手前向き
図009

各面の3頂点座標を取出したら、変換する前にその座標の面が表向きかどうか新たな関数(isFront())で確かめます(HTMLドキュメント第83行目)。そして、表向きの面のみ、3頂点座標に合わせて変換しています(第84〜86行目)。なお、面の向きを確かめる関数が返す表裏のブール(論理)値は、面のDisplayObject.visibleプロパティに与えられるので、裏向きの面は見えなくなります(第83行目)。

面の向きを確かめる関数(isFront())は、引数で受取った3頂点の座標から、前述のとおり右上角から左上角(point0_1)と右上角から左下角(point0_2)のふたつのベクトルの外積を求め、その値が負かどうかをブール値で返します(HTMLドキュメント第105〜108行目)。外積を求めるメソッド(crossProduct2D())の計算式は前述のとおりです(クラスMathUtils第16〜18行目)。

●HTMLドキュメント
  1. function drawCube() {
  2.   var points2d = transformCubePoints(cubePoints_array, rotations);
  3.   var nLength = faces_array.length;
  4.   for (var i = 0; i < nLength; i++) {
  5.     var face = faces_array[i];
  6.     var vertices = face.getVertices(points2d);
  7.     var point0 = vertices[0];
  8.     var point1 = vertices[1];
  9.     var point2 = vertices[2];
  10.     if (face.visible = isFront(point0, point1, point2)) {
  11.       var size = face.getSize();
  12.       var matrix = MathUtils.transform(size, point0, point1, point2);
  13.       matrix.decompose(face);
  14.     }
  15.   }
  16.   stage.update();
  17. }
  1. function isFront(point0, point1, point2) {
  2.   var point0_1 = new createjs.Point(point1.x - point0.x, point1.y - point0.y);
  3.   var point0_2 = new createjs.Point(point2.x - point0.x, point2.y - point0.y);
  4.   var bResult = MathUtils.crossProduct2D(point0_1, point0_2) < 0;
  5.   return bResult;
  6. }

●MathUtils.js
  1. MathUtils.crossProduct2D = function(point0, point1) {
  2.   return point0.x * point1.y - point0.y * point1.x;
  3. };

これで、マウスポインタの位置に応じて立方体の頂点座標がy軸とx軸で回転し、表向きの面のみが変換されて描画されます(図010)。裏向きの面は消されるので、重ね順の問題が起こりません。スクリプト全体は、つぎのコード001のとおりです。

図010■マウスポインタの位置に応じて3次元空間で立方体が回転する
図010

コード001■マウスポインタの位置に応じて3次元空間で立方体が回る
●HTMLドキュメント
  1. <script src="easeljs/utils/UID.js"></script>
  2. <script src="easeljs/geom/Matrix2D.js"></script>
  3. <script src="easeljs/geom/Point.js"></script>
  4. <script src="easeljs/events/MouseEvent.js"></script>
  5. <script src="easeljs/display/DisplayObject.js"></script>
  6. <script src="easeljs/display/Container.js"></script>
  7. <script src="easeljs/display/Stage.js"></script>
  8. <script src="easeljs/display/Bitmap.js"></script>
  9. <script src="easeljs/utils/Ticker.js"></script>
  10. <script src="preloadjs/AbstractLoader.js"></script>
  11. <script src="preloadjs/PreloadJS.js"></script>
  12. <script src="preloadjs/TagLoader.js"></script>
  13. <script src="preloadjs/XHRLoader.js"></script>
  14. <script src="MathUtils.js"></script>
  15. <script src="Face3D.js"></script>
  16. <script src="Point3D.js"></script>
  17. <script>
  18. var stage;
  19. var manifest = [
      "images/000.png",
      "images/001.png",
      "images/002.png",
      "images/003.png",
      "images/004.png",
      "images/005.png"
      ];
  20. var holderPositon;
  21. var rotations = {x:0, y:0, z:0};
  22. var unit = 100 / 2;
  23. var cubePoints_array = [];
  24. var holder;
  25. var sensitivity = 1 / 500;
  26. var faces_array = [];
  27. function initialize() {
  28.   var canvasElement = document.getElementById("myCanvas");
  29.   stage = new createjs.Stage(canvasElement);
  30.   stage.onMouseMove = getRotations;
  31.   var loader = new createjs.PreloadJS(false);
  32.   loader.onComplete = drawCube;
  33.   loader.loadManifest(manifest);
  34.   holderPositon = new createjs.Point(canvasElement.width / 2, canvasElement.height / 2);
  35.   holder = new createjs.Container();
  36.   setAppearance(holder, holderPositon.x, holderPositon.y);
  37.   setCubeData();
  38.   initializeCube();
  39.   createjs.Ticker.addListener(drawCube);
  40. }
  41. function setAppearance(instance, nX, nY) {
  42.   instance.x = nX;
  43.   instance.y = nY;
  44.   stage.addChild(instance);
  45. }
  46. function getRotations(eventObject) {
  47.   var mouseX = eventObject.stageX;
  48.   var mouseY = eventObject.stageY;
  49.   rotations.x = (mouseY - holderPositon.y) * sensitivity;
  50.   rotations.y = (mouseX - holderPositon.x) * sensitivity;
  51. }
  52. function setCubeData() {
  53.   cubePoints_array[0] = new Point3D(unit, -unit, -unit);
  54.   cubePoints_array[1] = new Point3D(-unit, -unit, -unit);
  55.   cubePoints_array[2] = new Point3D(-unit, unit, -unit);
  56.   cubePoints_array[3] = new Point3D(unit, -unit, unit);
  57.   cubePoints_array[4] = new Point3D(unit, unit, unit);
  58.   cubePoints_array[5] = new Point3D(-unit, unit, unit);
  59.   faces_array.push([0, 1, 2]);   // 前面
  60.   faces_array.push([3, 4, 5]);   // 後面
  61.   faces_array.push([4, 3, 0]);   // 右側面
  62.   faces_array.push([5, 2, 1]);   // 左側面
  63.   faces_array.push([1, 0, 3]);   // 上面
  64.   faces_array.push([2, 5, 4]);   // 底面
  65. }
  66. function initializeCube() {
  67.   var nLength = faces_array.length;
  68.   for (var i = 0; i < nLength; i++) {
  69.     var face = new Face3D(manifest[i], faces_array[i]);
  70.     faces_array[i] = face;
  71.     holder.addChild(face);
  72.   }
  73. }
  74. function drawCube() {
  75.   var points2d = transformCubePoints(cubePoints_array, rotations);
  76.   var nLength = faces_array.length;
  77.   for (var i = 0; i < nLength; i++) {
  78.     var face = faces_array[i];
  79.     var vertices = face.getVertices(points2d);
  80.     var point0 = vertices[0];
  81.     var point1 = vertices[1];
  82.     var point2 = vertices[2];
  83.     if (face.visible = isFront(point0, point1, point2)) {
  84.       var size = face.getSize();
  85.       var matrix = MathUtils.transform(size, point0, point1, point2);
  86.       matrix.decompose(face);
  87.     }
  88.   }
  89.   stage.update();
  90. }
  91. function transformCubePoints(points_array, myRotations) {
  92.   var points2D_array = [];
  93.   var nRadianX = myRotations.x;
  94.   var nRadianY = myRotations.y;
  95.   var nRadianZ = myRotations.z;
  96.   var nLength = points_array.length;
  97.   for (var i = 0; i < nLength; ++i) {
  98.     var myPoint3D = points_array[i];
  99.     myPoint3D.rotate(nRadianX, nRadianY, nRadianZ);
  100.     points2D_array[i] = new createjs.Point(myPoint3D.x, myPoint3D.y);
  101.   }
  102.   return points2D_array;
  103. }
  104. function isFront(point0, point1, point2) {
  105.   var point0_1 = new createjs.Point(point1.x - point0.x, point1.y - point0.y);
  106.   var point0_2 = new createjs.Point(point2.x - point0.x, point2.y - point0.y);
  107.   var bResult = MathUtils.crossProduct2D(point0_1, point0_2) < 0;
  108.   return bResult;
  109. }
  110. </script>

●Face3D.js
  1. function Face3D(file, indices) {
  2.   this.initialize(file);
  3.   this.indices = indices;
  4. }
  5. Face3D.prototype = new createjs.Bitmap();
  6. Face3D.prototype.getVertices = function(points2D) {
  7.   var vertices = [];
  8.   for (var i = 0; i < 3; i++) {
  9.     vertices[i] = points2D[this.indices[i]];
  10.   }
  11.   return vertices;
  12. };
  13. Face3D.prototype.getSize = function() {
  14.   var myImage = this.image;
  15.   var size = new createjs.Point(myImage.width, myImage.height);
  16.   return size;
  17. };

●Point3D.js
  1. function Point3D(nX, nY, nZ) {
  2.   this.x = nX;
  3.   this.y = nY;
  4.   this.z = nZ;
  5. }
  6. Point3D.prototype.rotate = function(nRadianX, nRadianY, nRadianZ) {
  7.   var nX = this.x;
  8.   var nY = this.y;
  9.   var nZ = this.z;
      // X軸で回転
  10.   var pointX = MathUtils.rotatePoint2D(nRadianX, nY, nZ);
  11.   nY = pointX.x;
  12.   nZ = pointX.y;
      // Y軸で回転
  13.   var pointY = MathUtils.rotatePoint2D(nRadianY, nZ, nX);
  14.   nZ = pointY.x;
  15.   nX = pointY.y;
      // Z軸で回転
  16.   var pointZ = MathUtils.rotatePoint2D(nRadianZ, nX, nY);
  17.   nX = pointZ.x;
  18.   nY = pointZ.y;
  19.   this.x = nX;
  20.   this.y = nY;
  21.   this.z = nZ;
  22. };

●MathUtils.js
  1. function MathUtils() {}
  2. MathUtils.transform = function(size, point0, point1, point2) {
  3.   var point1_0_x = point0.x - point1.x;
  4.   var point1_0_y = point0.y - point1.y;
  5.   var point1_2_x = point2.x - point1.x;
  6.   var point1_2_y = point2.y - point1.y;
  7.   var matrix = new createjs.Matrix2D();
  8.   matrix.tx = point1.x;
  9.   matrix.ty = point1.y;
  10.   matrix.a = point1_0_x / size.x;
  11.   matrix.b = point1_0_y / size.x;
  12.   matrix.c = point1_2_x / size.y;
  13.   matrix.d = point1_2_y / size.y;
  14.   return matrix;
  15. };
  16. MathUtils.crossProduct2D = function(point0, point1) {
  17.   return point0.x * point1.y - point0.y * point1.x;
  18. };
  19. MathUtils.rotatePoint2D = function(nRadians, nX, nY) {
  20.   var nSin = Math.sin(nRadians);
  21.   var nCos = Math.cos(nRadians);
  22.   var point2D = new createjs.Point();
  23.   point2D.x = nCos * nX - nSin * nY;
  24.   point2D.y = nSin * nX + nCos * nY;
  25.   return point2D;
  26. };

06 03-02-05 ベクトルの外積で面の向きを調べる

ベクトルの外積からどうして面の向きがわかるのかについて、興味のある方のために簡単に説明を補います。

3次元空間におけるふたつのベクトルAとBの外積A×Bは、A×Bで表され、ふたつのベクトルにともに垂直で、ベクトルAからBへの回転で右ネジが進む向きのベクトルとして定められます(図011)。そして、その絶対値(大きさ)はつぎの式で表されます。

|A×B| = |A||B|sinθ
図011■ベクトルの外積はどちらにも垂直でその回転により右ネジの進む方向のベクトル

2次元平面に投影した面の右上角から左上角、および右上角から左下角のふたつのベクトルの外積を求めたとき、面が表なら右ネジは手前に向くことになります。z軸は奥向きに正ですので、外積のz座標値が負のとき面は手前、つまり表を向いています。

3次元空間のベクトルAとBの各成分がそれぞれ(ax, ay, az)および(bx, by, bz)だとすると、外積A×Bはつぎの式で導かれます。

A×B = (aybz - azby, azbx - axbz, axby - aybx)

2次元平面のベクトルはz座標値が0です。すると、ベクトルAとBはそれぞれ(ax, ay, 0)および(bx, by, 0)となりますので、外積A×Bはxy座標が0でz座標値のみのz軸に平行なベクトルを表します。

A×B = (0, 0, axby - aybx)

そのため、このz座標値(axby - aybx)をもって、2次元ベクトルの外積とすることがあります。本稿で用いた外積を求める関数(crossProduct2D())はこの考え方にもとづいて、3次元ベクトル(座標)でなくz座標値のみを返しています。



作成者: 野中文雄
作成日: 2012年11月1日


Copyright © 2001-2012 Fumio Nonaka.  All rights reserved.