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

第51回ベクトルの内積で面の向きを調べる

今回から、おもに3次元空間で回すネタを深堀りすると予告した。少し手の込んだ回転のアニメーションを考えると、どうしても立ちはだかってくるのは、ベクトルと三角関数だ。これから数回にわたって、ベクトルの内積・外積と三角関数を使ったお題に取り組む。

といっても、数学的にきっちり理解しなくてもよい。エンジンやサスペンションの仕組みは知らなくても、機能さえわかれば車は運転できる。もっと実感しやすい例でいえばフィルタだ。きわめて高度な数学演算の結果を、筆者も含めて多くのユーザーは内部処理を知らぬまま使っている。ただ、どのパラメータが結果にどう反映されるかさえわかっていればよいからだ。

もっとも、できれば内部的な処理を知りたいマニアックな読者もいるだろう。筆者もその気持ちはわかる。そこで、順序を逆転して数学的な計算や理由づけは、基本的に後から解説するかたちをとる。苦手な読者は、文脈を見失わない程度に流し読みしてもらって構わない。

お題の説明と準備

本論に入る前に、とくにマニアックな読者にひとつご注意したい。ベクトルの内積や外積が、何を意味するのかという疑問は、取りあえず先送りしてほしい。結論からいえば、筆者はその適切な答えをもっていない。だが、それは円周率πでも同じだろう。πとは何かと問われたとき、それは約3.14だとか、もう少し厳密にいうなら円周の直径に対する比率と一応は答えられる。

けれど、質問者が訊きたいのは、その数値の具体的な意味だ。この問いには、円の面積が求められるというくらいしか答えづらい。筆者はそれでよいと考える。Photoshopの初心者に、⁠雲模様」のフィルタは何をするものなのかと尋ねられたときと同じだ。数学でもPhotoshopでも、それをツールとして使うのであれば、何ができるのか、どのように使うのかがわかれば足りる。

今回採上げるベクトルの内積では、面が表か裏かを見分ける。ネタとしては、第48回「テクスチャに遠近法を適用する - uvt座標の指定」スクリプト2に手を加えることにしよう第48回図2再掲⁠⁠。

第48回 図2 回転のMatrix3Dオブジェクトに透視(遠近法)投影の変換を加える(再掲)
第48回 図2 回転のMatrix3Dオブジェクトに透視(遠近法)投影の変換を加える(再掲)
画像 画像 画像 画像

第48回スクリプト2は、3次元空間で正方形の頂点座標をマウスポインタの位置に合わせて水平に回し、テクスチャマッピングした(前掲第48回図2⁠。今回はビットマップをもうひとつ加え、正方形の裏と表で異なるテクスチャをマッピングする図1⁠。

図1 3次元空間で水平に回る正方形の裏と表に異なるテクスチャをマッピングする
図1 3次元空間で水平に回る正方形の裏と表に異なるテクスチャをマッピングする

まず準備として、回る正方形の裏に貼るビットマップを[ライブラリ]に加える。そして、スクリプトからインスタンスがつくれるように、⁠クラス]を設定しておく図2⁠。今回のサンプルでは「Image2」とした(クラスを設定する手順については、第34回3次元空間における回転「ビットマップのインスタンスを動的に配置する」参照⁠⁠。

図2 ⁠ライブラリ]に加えたビットマップに[クラス]を設定
図2 [ライブラリ]に加えたビットマップに[クラス]を設定

ベクトルの内積から面の向きがわかる

ベクトルは方向をもった大きさで、矢印により表現されることが多い図3⁠。矢印の長さが大きさ、矢じりは方向を示す。そして、内積はふたつのベクトルによる計算だ。計算結果は数値(実数)で、その値はふたつのベクトル同士の向きや角度に関わる。取りあえず、これくらいの情報を頭に入れておけばよい。

図3 ふたつのベクトルを矢印で示す
図3 ふたつのベクトルを矢印で示す

つまり、面の裏表を内積で調べようとすると、ふたつのベクトルが必要だということになる。ひとつは面の向きを示すベクトル。もうひとつは視線のベクトルだ。今回の正方形を回す場合なら、ふたつのベクトルの方向はすぐにわかる。面はz軸手前方向、視線は逆のz軸奥向きである。実は、面の向きを調べるだけであれば、ベクトルの大きさはどうでもよい。そういうときは、大きさを1に決めておくと、後あと計算が楽になる。

3次元空間のベクトルを表すために、ActionScript 3.0ではVector3Dオブジェクトを用いる。ということで、ふたつのベクトルの3次元空間座標は次の表1のように定められる。もちろん、面の向きは初期値で、アニメーションによって変わる。それに対して、視線は動かない。

表1視線と面のベクトルの3次元座標
ベクトル座標
視線(0, 0, 1)
面(初期値)(0, 0, -1)

そして、本題の内積だ。面の裏表を調べるには、視線と面のふたつのベクトルの内積から、その値の符号(正負)だけ取出せばよい表2⁠。図4は水平に回る面をy軸の方向、つまり3次元空間の真上から見ている。内積の値は、視線と面のベクトルのなす角が鋭角のときは正、鈍角なら負の値になる。前者は面が裏向き、後者は表向きということだ。そして、内積がちょうど0のとき、面は真横を向いている。

表2視線と面のベクトルの内積となす角
面の向きふたつのベクトルのなす角内積の符号
90度より小さい(鋭角)+(正)
真横90度(直角)0
90度より大きい(鈍角)-(負)
図4 視線と面のふたつのベクトルから面の裏表を調べる
図4 視線と面のふたつのベクトルから面の裏表を調べる

ここまでくれば、内積の計算は簡単だ。Vector3D.dotProduct()メソッドを呼出せばよい。なお、参照する(ターゲットの)Vector3Dオブジェクトと引数のVector3Dオブジェクトは、入替えても内積の値は同じになる。

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

面の表と裏を塗替える

では、いよいよ第48回スクリプト2に手を加える。第1に、視線と面のベクトルをVector3Dオブジェクトとして変数(viewVector3DとfaceVector3D)に定める。視線のベクトルの座標(0, 0, 1)は、定数Vector3D.Z_AXISと同じだ。面のベクトルは、視線のベクトルの向きを逆にした(0, 0, -1)だった(前掲表1⁠。

第2は、面の表裏を調べる関数(xIsFront())の定義だ。表のときtrue裏ならfalseを返すことにする。この関数の引数はふたつ、面のVector3Dオブジェクトと変換行列のMatrix3Dオブジェクトにした。なぜなら、視線はもちろん、面のベクトルを示すVector3Dオブジェクトも動かないからだ。第48回につぎのように説明したことを想い起こしてほしい(⁠Utils3D.projectVectors()メソッドを使うの項⁠⁠。

今回3次元空間の頂点座標は動かさない。変換のMatrix3Dオブジェクトに回転を加えて、その結果のオブジェクトを変数(worldMatrix3D)に残す。つまり、座標ではなく変換行列で座標変換を管理するのだ。

したがって、面のベクトルが今どちらを向いているかを調べるには、変換のMatrix3Dオブジェクトが欠かせない。面の表裏を調べる関数(xIsFront())は、まずMatrix3D.transformVector()メソッドを用いて今の面の向きをVector3Dオブジェクト(directionVector3D)として得る(メソッドについては、第42回「Vector3Dクラスの3次元空間座標とインスタンスへの描画」Vector3DインスタンスをMatrix3Dオブジェクトで座標変換するの項参照⁠⁠。つぎに、その面の向きのVector3Dオブジェクトと視線(viewVector3D)の内積を求めて、値が負なら表向きを意味するtrue0以上ならfalseを返す。

// フレームアクションに追加
var viewVector3D:Vector3D = Vector3D.Z_AXIS;   // 視線のベクトル
var faceVector3D:Vector3D = new Vector3D(0, 0, -1);   // 面のベクトル

function xRotate(eventObject:Event):void {
  var nRotationY:Number = mySprite.mouseX * nDeceleration;
  var vertices2D:Vector.<Number> = new Vector.<Number>();
  xTransform(vertices2D, nRotationY);
  var bFront:Boolean = xIsFront(faceVector3D, worldMatrix3D);  // 面の表裏を調べる
  trace(bFront);  // 確認用
  xDraw(vertices2D);
}

// 面の表裏を調べる関数の定義
function xIsFront(myVector3D:Vector3D, myMatrix3D:Matrix3D):Boolean {
  var directionVector3D:Vector3D = myMatrix3D.transformVector(myVector3D);
  var bFront:Boolean = (viewVector3D.dotProduct(directionVector3D) < 0);
  return bFront;
}

面の表裏を調べる関数(xIsFront())は、正方形を回すDisplayObject.enterFrameイベントのリスナー関数(xRotate())から呼出す。実際に表裏のテクスチャを切替えるには、面を塗る関数(xDraw())にも手を加えなければならない。さしあたり確認用のtrace()関数で、戻り値(bFront)[出力]している。実際に[ムービープレビュー]で確かめるときには、フレームレートを落とした方が見やすい図5⁠。

図5 関数の戻り値を[出力]するときフレームレートは落とした方が見やすい
図5 関数の戻り値を[出力]するときフレームレートは落とした方が見やすい

それでは、面の表と裏を塗り分ける仕上げにかかろう。第1に、⁠ライブラリ]に加えたビットマップ(クラスImage2)をインスタンスにして、裏面用の変数(backTexture)に納めておく。

第2に、正方形を回すDisplayObject.enterFrameイベントのリスナー関数(xRotate())から呼出す面を塗る関数(xDraw())に、前掲の関数(xIsFront())で調べた面の表裏を引数として渡す。

そして第3に、面を塗る関数(xDraw())は、引数(bFront)のブール値により表裏のテクスチャ(texture)を切替えて、回転する正方形にマッピングしている。これで、回る正方形の裏表によって、塗られるビットマップが切り替わる(前掲図1参照⁠⁠。

// フレームアクションに追加
var backTexture:BitmapData = new Image2();   // 追加

function xRotate(eventObject:Event):void {
  var nRotationY:Number = mySprite.mouseX * nDeceleration;
  var vertices2D:Vector.<Number> = new Vector.<Number>();
  xTransform(vertices2D, nRotationY);
  var bFront:Boolean = xIsFront(faceVector3D, worldMatrix3D);
  // trace(bFront);  // 確認用
  // xDraw(vertices2D);
  xDraw(vertices2D, bFront);
}

// function xDraw(vertices2D:Vector.<Number>):void {
function xDraw(vertices2D:Vector.<Number>, bFront:Boolean):void {
  var texture:BitmapData = bFront ? myTexture : backTexture;   // 追加
  myGraphics.clear();
  // myGraphics.beginBitmapFill(myTexture);
  myGraphics.beginBitmapFill(texture);
  myGraphics.drawTriangles(vertices2D, indices, uvtData);
  myGraphics.endFill();
}

修正し終えたフレームアクション全体を、スクリプト1として掲げる。

スクリプト1 水平に回す立方体の裏表を異なるテクスチャでマッピング
// フレームアクション
var nUnit:Number = 100 / 2;
var mySprite:Sprite = new Sprite();
var myTexture:BitmapData = new Image();
var backTexture:BitmapData = new Image2();
var viewVector3D:Vector3D = Vector3D.Z_AXIS;
var faceVector3D:Vector3D = new Vector3D(0, 0, -1);
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();
viewMatrix3D.prependTranslation(0, 0, myPerspective.focalLength);
mySprite.x = stage.stageWidth / 2;
mySprite.y = stage.stageHeight / 2;
vertices.push(-nUnit, -nUnit, 0);
vertices.push(nUnit, -nUnit, 0);
vertices.push(nUnit, nUnit, 0);
vertices.push(-nUnit, nUnit, 0);
indices.push(0, 1, 3);
indices.push(1, 2, 3);
uvtData.push(0, 0, 0);
uvtData.push(1, 0, 0);
uvtData.push(1, 1, 0);
uvtData.push(0, 1, 0);
addChild(mySprite);
addEventListener(Event.ENTER_FRAME, xRotate);
function xRotate(eventObject:Event):void {
  var nRotationY:Number = mySprite.mouseX * nDeceleration;
  var vertices2D:Vector.<Number> = new Vector.<Number>();
  xTransform(vertices2D, nRotationY);
  var bFront:Boolean = xIsFront(faceVector3D, worldMatrix3D);
  xDraw(vertices2D, bFront);
}
function xTransform(vertices2D:Vector.<Number>, myRotation:Number):void {
  worldMatrix3D.prependRotation(myRotation, Vector3D.Y_AXIS);
  var myMatrix3D:Matrix3D = worldMatrix3D.clone();
  myMatrix3D.append(viewMatrix3D);
  Utils3D.projectVectors(myMatrix3D, vertices, vertices2D, uvtData);
}
function xDraw(vertices2D:Vector.<Number>, bFront:Boolean):void {
  var texture:BitmapData = bFront ? myTexture : backTexture;
  myGraphics.clear();
  myGraphics.beginBitmapFill(texture);
  myGraphics.drawTriangles(vertices2D, indices, uvtData);
  myGraphics.endFill();
}
function xIsFront(myVector3D:Vector3D, myMatrix3D:Matrix3D):Boolean {
  var directionVector3D:Vector3D = myMatrix3D.transformVector(myVector3D);
  var bFront:Boolean = (viewVector3D.dotProduct(directionVector3D) < 0);
  return bFront;
}

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

ベクトルの内積について、数学的な説明を簡単に加えよう。ベクトル同士の掛け算は定義されていない。しかし、乗算つまり積と似た計算に「内積」がある。ベクトルAとBの内積は「A・B」で表される。結果は実数であり、ベクトルではないので注意してほしい。

ベクトルAとBの内積は、ふたつのベクトルのなす角をθとすると、つぎの式で求められる(三角関数のcosについては、⁠座標と三角関数と、時々、ベクトル」の2.三角関数で角度と距離から位置を計算するをお読みいただきたい⁠⁠。なお、|A|はベクトルAの大きさで、⁠絶対値」とも呼ばれる。

  • A・B = |A||B|cosθ

  • ベクトルAとBの始点を結んだとき、ベクトルAの終点からBに下ろした垂線との交点までの長さをAの射影といい、|A|cosθになる。この長さとベクトルBの大きさ|B|の積が内積A・Bだ図6⁠。なお、ふたつのベクトルのなす角θは、小さい方の角度つまり0以上π(180度)以下の範囲で定める。また、ベクトルAとBのどちらかひとつでも絶対値が0のとき、内積A・B = 0とされる。

    図6 内積はベクトルAのBへの射影と|B|との積
    図6 内積はベクトルAのBへの射影と|B|との積

    ベクトルAとBの絶対値は、どちらも0ではないとしよう。すると、ふたつのベクトルの絶対値の積は、つねに正になる。

    • A||B| > 0 (|A|≠0、|B|≠0とする)

    つまり、内積A・Bの値の正負は、cosθによって決まる。cosθは角θが0からπ(180度)までの範囲について、鋭角のとき正、直角であれば0、そして鈍角では負の値になる表3⁠。したがって、内積からふたつのベクトルの互いの向きがわかるのだ。

    表3 ベクトルのなす角と内積の正負
    内積の値ベクトルのなす角(θ) cosθ
    +(正)90度より小さい(鋭角)+(正)画像
    090度(直角)0
    負(-)90度より大きい(鈍角)負(-)

    内積は、ふたつのベクトルの座標(⁠⁠成分」ともいう)から導くこともできる。まず、ベクトルAとBを2次元平面で考えたとき、座標をそれぞれ(ax, ayおよび(bx, byとする。そのとき、内積A・Bはつぎの式で導かれる[1]⁠。

    • A・B = axbx + ayby

    3次元空間のベクトルA(ax, ay, azとB(bx, by, bzについても、同じように求められる。つまり、各座標ごとに掛合わせてそれらの和をとる。Vector3D.dotProduct()メソッドは、内部的にはこの計算をしているはずだ。

    • A・B = axbx + ayby + azbz

    次回は、立方体を上下左右に自由に回す前回第50回のネタに戻る。ただし、回転軸をx軸とかy軸とせず、自由に定めてみたい。その場合には、ベクトルの外積を使う。

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

おすすめ記事

記事・ニュース一覧