ActionScript 3.0で始めるオブジェクト指向スクリプティング

第52回ベクトルの外積で回転の軸を定める

前回のベクトルの内積に続いて、今回は外積を学ぶ。外積はふたつのベクトルのどちらにも、垂直なベクトルを求める計算だ。ふたつの(平行でない)ベクトルで、ひとつの面が定められる。したがって、ベクトルの外積は、面に垂直なベクトルを導くともいえる。お題としては、第50回「立方体の4面にテクスチャを貼って上下左右に回す」スクリプト3に手を加える。

回転の軸を定める

第50回スクリプト3は、⁠3次元空間で上下左右に回す立方体の4面にテクスチャマッピング」した第50回図1再掲⁠⁠。今回、この動きはそのままに、回転の内部処理を書替える。第50回のスクリプトは、マウスポインタを水平に動かすとy軸で、垂直に動かすとx軸で回した。そして、斜めの動きは水平軸と垂直軸に分けて、3次元空間座標を回転した。今回はふたつを分けずに、回転軸そのものを動かそう。

図1 上下左右に回した立方体に面の前後を整えて塗る(再掲)
図1 上下左右に回した立方体に面の前後を整えて塗る(再掲) 図1 上下左右に回した立方体に面の前後を整えて塗る(再掲)

幸いにして、Matrix3D.appendRotation()メソッドに渡す第2引数の回転軸はxyz軸(Vector3Dクラスの定数)にかぎらない。長さ1のVector3Dオブジェクトで回転軸を自由に定められる。

Matrix3D.appendRotation(回転の角度, 回転軸のVector3Dオブジェクト)

そこで考えなければならないのは、回転軸をどのようにして決めるかだ。マウスポインタを動かした方向だなどと、早とちりしてはいけない。マウスポインタに合わせるのは回転の動きであって、求めたいのは回す中心軸なのだ。

現に、マウスポインタをx軸方向に水平に動かすとy軸で回し、y軸の垂直方向に動かしたときはx軸で回転した。つまり、マウスポインタの動きと回転軸は、互いに垂直になる。もっとも、まだこれでは足りない。3次元空間では1本の直線と(ある点で)垂直に交わる直線は無数に考えられるからだ。たとえば、xy平面上の原点(0, 0)を通る直線なら、3次元空間ではすべてz軸と原点(0, 0, 0)で垂直に交わる。

初めにほのめかしたように、ふたつの(平行でない)ベクトルでひとつの面が定められる。そして、その平面と(ある点で)垂直に交わる直線は1本に決まるのだ。つまり、マウスポインタを動かした方向に加えて、ベクトルがもうひとつ要る。それはz軸だ。

ベクトルの外積から垂直な線を求める

マウスポインタの動きとz軸というふたつのベクトルが決まった。これで外積が求められる。ふたつのベクトルをAとBとすると、外積は「A×B」で表される。ただし、ベクトルの掛け算ではない。外積A×Bは、ベクトルAとBのどちらにも垂直なベクトルを表す図1⁠。

図1 外積はふたつのベクトルに垂直なベクトルを表す)
図1 外積はふたつのベクトルに垂直なベクトルを表す

ベクトルは大きさと方向をもつ。もっとも、今回のお題では、外積は回転軸を定めるために使う。すると、大きさはどうでもよい。だが、方向は気にしなければならない。ふたつのベクトルに垂直な直線は1本に決まっても、ふたつの方向がありえるからだ。回転軸の方向が逆になると、回る向きも反対になってしまう。

したがって、ベクトルの外積では、計算の順序が大切になる。外積A×Bでは、ベクトルAからBの方向にドライバーを回すと想像してほしい。普通使われる右ネジは、時計回りに締める。その右ネジの進む向きに、外積A×Bのベクトルは定められている(前掲図1⁠。

さて、外積を求めるのはVector3D.crossProduct()メソッドだ。参照するVector3Dオブジェクトに対して、もうひとつのVector3Dオブジェクトを引数として呼出し、戻り値は外積のVector3Dオブジェクトになる。

Vector3Dオブジェクト.crossProduct(もうひとつのVector3Dオブジェクト)

ここまでのところで、前回学んだ内積を求めるVector3D.dotProduct()メソッドとの違いはふたつある。まず、Vector3D.dotProduct()メソッドの戻り値は数値で、Vector3D.crossProduct()メソッドは外積のベクトルであるVector3Dオブジェクトを返す。つぎに、メソッドの参照と引数のふたつのVector3Dオブジェクトを入替えたとき、内積の戻り値は変わらないのに対して、外積が返すVector3Dオブジェクトはベクトルの方向が逆になる。

第50回スクリプト3の中で今回手を加えるのは、DisplayObject.enterFrameイベントのリスナー関数(xRotate())と座標変換の関数(xTransform())のふたつだ。修正を終えたフレームアクション全体は、後でスクリプト1として掲げる。

まず、DisplayObject.enterFrameイベントのリスナー関数(xRotate())は、回転の軸と回転角の大きさを計算する。そのために、マウスポインタのxy座標から、回す方向のVector3Dオブジェクト(mouseVector3D)を求める。そして、z軸(定数Vector3D.Z_AXISと回す方向のふたつのベクトルの外積から、回転軸のVector3Dオブジェクト(axisVector3D)を得る。回転角の大きさは、回す方向のベクトルの長さVector3D.lengthプロパティ)をもとに計算した(nRotation⁠⁠。

こうして求めた回転の軸(axisVector3D)と回転角の大きさ(nRotation)は、座標変換の関数(xTransform())に渡して呼出す。したがって、この関数の引数と処理内容を少し変えることになる。なお、Vector3D.normalize()メソッドは参照するVector3Dオブジェクトの方向はそのままで、長さVector3D.lengthプロパティ)を1にする。

// フレームアクションを修正
function xRotate(eventObject:Event):void {
  // var nRotationY:Number = mySprite.mouseX * nDeceleration;
  // var nRotationX:Number = mySprite.mouseY * nDeceleration;
  // 回す方向のベクトルを求める
  var mouseVector3D:Vector3D = new Vector3D(mySprite.mouseX, -mySprite.mouseY, 0);
  // z軸と回す方向のベクトルとの外積を求める
  var axisVector3D:Vector3D = Vector3D.Z_AXIS.crossProduct(mouseVector3D); 
  var vertices2D:Vector.<Number> = new Vector.<Number>();
  var nRotation:Number = mouseVector3D.length * nDeceleration;
  axisVector3D.normalize();   // ベクトルの長さを1にする
  // xTransform(vertices2D, nRotationY, nRotationX);
  xTransform(vertices2D, nRotation, axisVector3D);   // 引数を変更
  xSetOrder();
  xDraw(vertices2D);
}

つぎに、座標変換の関数(xTransform())は、前述のとおりDisplayObject.enterFrameイベントのリスナー関数(xRotate())から受取る引数が変わる。第2引数が回転角の数値、第3引数は回転軸のVector3Dオブジェクトにした。このふたつの引数をMatrix3D.appendRotation()メソッドに渡せば、指定した回転軸でMatrix3Dオブジェクト(worldMatrix3D)に回転が加えられる。

なお、Matrix3D.appendRotation()メソッドの第2引数として渡すVector3Dオブジェクトは長さVector3D.lengthプロパティ)が1でなければならない。そのため前掲の関数(xRotate())では、回転軸のVector3Dオブジェクトに対してVector3D.normalize()メソッドを呼出したのだ。

修正前の関数では、y軸とx軸のふたつに分けて行っていたMatrix3Dオブジェクトの回転が、任意の軸を定めたことによりMatrix3D.appendRotation()メソッド1回の呼出しで済んだことに注目してほしい。

// フレームアクションを修正
// function xTransform(vertices2D:Vector.<Number>, myRotationY:Number, myRotationX:Number):void {
function xTransform(vertices2D:Vector.<Number>, myRotation:Number, axisVector3D:Vector3D):void {
  // worldMatrix3D.appendRotation(myRotationY, Vector3D.Y_AXIS);
  // worldMatrix3D.appendRotation(myRotationX, Vector3D.X_AXIS);
  // Matrix3D.appendRotation()メソッドの呼出しは回転軸を定めて1度だけ
  worldMatrix3D.appendRotation(myRotation, axisVector3D);
  var myMatrix3D:Matrix3D = worldMatrix3D.clone();
  myMatrix3D.append(viewMatrix3D);
  Utils3D.projectVectors(myMatrix3D, vertices, vertices2D, uvtData);
}

細かいことにひとつ触れておこう。外積を計算するとき、ふたつのベクトルの順序が入替わると、外積のベクトルの向きは逆になると注意した。回転軸の方向が逆になれば、回る向きも反転するからだった。そこで、回転軸と回る向きの関係が気になっていた人もいるだろう。

Matrix3D.appendRotation()メソッドに正の角度と回転軸のVector3Dオブジェクトを渡すと、回転軸の方向にドライバーで右ネジを締める向きに回る図2⁠。もっとも、この関係はいちいち覚えなくてもよい。Flashの座標空間における回転は、すべてこの規則で定められているからだ。

図2 回転軸の方向と回る向き
図2 回転軸の方向と回る向き

たとえば、[変形]パネルで何かエレメントを正の方向に回してみよう。2次元平面の回転は、3次元空間ではz軸で回すことを意味する。z軸は奥向きに(正と)定められており、エレメントは時計回り、つまり右ネジの向きに回転することが確かめられる。

さらに実も蓋もないことをいうなら、回る向きが意図と逆だったら、角度の変数にマイナス(-)をつければ済む。要は、外積を使って回転軸が正しく求められ、方向が反転すると回転は逆になると知ってさえいればよいのだ。

第50回スクリプト3に前述したふたつの関数の修正を加えたフレームアクション全体が以下のスクリプト1だ。これで第50回スクリプト3と見た目は同じように、テクスチャマッピングした立方体がマウスポインタの位置に応じて回る(前掲第50回図1参照⁠⁠。

スクリプト1 テクスチャマッピングした立方体をマウスポインタの位置に応じた軸で回す
var nUnit:Number = 100 / 2;
var mySprite:Sprite = new Sprite();
var myTexture:BitmapData = new Image();
var vertices:Vector.<Number> = new Vector.<Number>();
var indices:Vector.<int> = new Vector.<int>();
var uvtData:Vector.<Number> = new Vector.<Number>();
var nDeceleration:Number = 0.3;
var myGraphics:Graphics = mySprite.graphics;
var myPerspective:PerspectiveProjection = transform.perspectiveProjection;
var worldMatrix3D:Matrix3D = new Matrix3D();
var viewMatrix3D:Matrix3D = myPerspective.toMatrix3D();
var nFaces:uint = 4;
var faces:Vector.<Vector.<uint>> = new Vector.<Vector.<uint>>();
var centers:Vector.<Vector3D> = new Vector.<Vector3D>();
faces.push(new <uint>[0, 1, 2, 3]);
faces.push(new <uint>[1, 5, 6, 2]);
faces.push(new <uint>[5, 4, 7, 6]);
faces.push(new <uint>[4, 8, 9, 7]);
centers.push(new Vector3D(0, 0, -nUnit));
centers.push(new Vector3D(nUnit, 0, 0));
centers.push(new Vector3D(0, 0, nUnit));
centers.push(new Vector3D(-nUnit, 0, 0));
viewMatrix3D.prependTranslation(0, 0, myPerspective.focalLength);
mySprite.x = stage.stageWidth / 2;
mySprite.y = stage.stageHeight / 2;
vertices.push(-nUnit, -nUnit, -nUnit);
vertices.push(nUnit, -nUnit, -nUnit);
vertices.push(nUnit, nUnit, -nUnit);
vertices.push(-nUnit, nUnit, -nUnit);
vertices.push(-nUnit, -nUnit, nUnit);
vertices.push(nUnit, -nUnit, nUnit);
vertices.push(nUnit, nUnit, nUnit);
vertices.push(-nUnit, nUnit, nUnit);
vertices.push(-nUnit, -nUnit, -nUnit);
vertices.push(-nUnit, nUnit, -nUnit);
for (var i:uint = 0; i < nFaces; i++) {
  addRectangleIndices(faces[i]);  
}
uvtData.push(0, 0, 0);
uvtData.push(1/4, 0, 0);
uvtData.push(1/4, 1, 0);
uvtData.push(0, 1, 0);
uvtData.push(3/4, 0, 0);
uvtData.push(2/4, 0, 0);
uvtData.push(2/4, 1, 0);
uvtData.push(3/4, 1, 0);
uvtData.push(1, 0, 0);
uvtData.push(1, 1, 0);
addChild(mySprite);
addEventListener(Event.ENTER_FRAME, xRotate);
function xRotate(eventObject:Event):void {
  var mouseVector3D:Vector3D = new Vector3D(mySprite.mouseX, -mySprite.mouseY, 0);
  var axisVector3D:Vector3D = Vector3D.Z_AXIS.crossProduct(mouseVector3D); 
  var vertices2D:Vector.<Number> = new Vector.<Number>();
  var nRotation:Number = mouseVector3D.length * nDeceleration;
  axisVector3D.normalize();
  xTransform(vertices2D, nRotation, axisVector3D);
  xSetOrder();
  xDraw(vertices2D);
}
function xTransform(vertices2D:Vector.<Number>, myRotation:Number, axisVector3D:Vector3D):void {
  worldMatrix3D.appendRotation(myRotation, axisVector3D);
  var myMatrix3D:Matrix3D = worldMatrix3D.clone();
  myMatrix3D.append(viewMatrix3D);
  Utils3D.projectVectors(myMatrix3D, vertices, vertices2D, uvtData);
}
function xDraw(vertices2D:Vector.<Number>):void {
  myGraphics.clear();
  myGraphics.beginBitmapFill(myTexture);
  myGraphics.drawTriangles(vertices2D, indices, uvtData);
  myGraphics.endFill();
}
function addRectangleIndices(face:Vector.<uint>):void {
  indices.push(face[0], face[1], face[3]);
  indices.push(face[1], face[2], face[3]);
}
function xSetOrder():void {
  var transformedFaces:Vector.<Array> = new Vector.<Array>();
  for (var i:uint = 0; i < nFaces; i++) {
    var transformedVector3D:Vector3D = worldMatrix3D.transformVector(centers[i]);
    transformedFaces[i] = [transformedVector3D, faces[i]];
  }
  transformedFaces.sort(compare);
  indices.length = 0;
  for (var j:uint = 0; j < nFaces; j++) {
    addRectangleIndices(transformedFaces[j][1]);
  }
}
function compare(a:Array, b:Array):Number {
  var nA:Number = a[0].z;
  var nB:Number = b[0].z;
  if (nA < nB) {
    return 1;
  } else if (nA > nB) {
    return -1;
  } else {
    return 0;
  }
}

ベクトルの外積を数学的に理解する

ベクトルの外積について、数学的な説明をしよう。数学が嫌いな読者は、流し読みしてもらって構わない。ベクトルAとBの外積は「A×B」で表される[1]⁠。求まる結果は、内積が数値だったのに対して、外積はベクトルである。ベクトルには大きさと方向があった。

まず、外積A×Bの方向については、角度がベクトルAとBのどちらにも垂直になる。そして、ベクトルAからBに向かう回転を考えたとき、その回し方で右ネジの進む向きに定められる(下表1参照⁠⁠。つぎに、外積A×Bの大きさ|A×B|は、ベクトルAとBのなす角をθとしたとき、つぎの式で表される。

  • |A×B| = |A||B|sinθ

ベクトルAとBを隣合う2辺とする平行四辺形を考え、2辺のなす角をθとすると、底辺を|A|としたとき|B|sinθは平行四辺形の高さになる。したがって、外積の大きさ|A||B|sinθは、この平行四辺形の面積に等しい。このようにして定められる外積A×Bのベクトルをまとめたのが次表1だ。

表1 外積A×Bで定められるベクトル
外積の要素求められた外積のベクトルとふたつのベクトルAとBとの関係
方向角度 ふたつのベクトルAとBのどちらにも垂直画像
向きベクトルAからBに向かう回転で右ネジの進む向き
大きさ|A||B|sinθ画像

前回の内積の説明で述べたとおり、ふたつのベクトルのなす角θは、小さい方の角度つまり0以上π(180度)以下の範囲で定める(第51回ベクトルの内積で面の向きを調べる「ベクトルの内積を数学的に理解する」の項参照⁠⁠。ふたつのベクトルAとBが平行、つまり互いのなす角θが0またはπ(180度)のときsinθ = 0となり、外積の大きさ|A×B| = 0になる[2]⁠。

逆に、ふたつのベクトルAとBの外積の大きさ|A×B|が0なら、それらは互いに平行だということになる。他方、前回ご説明した内積A・Bは、値が0のときふたつのベクトルは互いに垂直だった。つまり、ふたつのベクトルのなす角が垂直とか平行という特別な場合については、内積や外積から直ちに確かめられるのだ表2⁠。

表2 内積や外積からふたつのベクトルのなす角が特別な場合を確かめる
演算特別な場合互いになす角
内積内積が0垂直
外積外積の大きさが0平行

3次元空間のベクトルAとBの外積A×Bは、内積A・Bと同じく、それぞれの座標から計算することもできる。ベクトルAとBの座標がそれぞれ(ax, ay, azおよび(bx, by, bzである場合、外積A×Bはつぎの式で導かれる。

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

次回は再び内積をお題として、すでにできあがったスクリプトの書替えではなく、新たなサンプルをひとつつくってみたい。

今回解説した次のサンプルファイルがダウンロードできます。

おすすめ記事

記事・ニュース一覧