サイトトップ

Director Flash 書籍 業務内容 プロフィール
 
近日開講講座
4/15(水)JavaScriptベーシックトレーニング
4/16(木)JavaScriptインタラクティブアニメーション講座
CreateJS Style Book WebクリエイターのためのCreateJSスタイルブック Away3D TypeScript in gihyo.jp gihyo.jp連載
「Away3D TypeScriptではじめる3次元表現」
■Twitter: @FumioNonaka / Facebook Page: CreateJS / Away3D

Creators MeetUp

ベクトルふたたび − 外積と内積を使う

「ベクトル」というのは、とっつきにくい印象が強いらしい。とくに、「内積」とか「外積」とか、果たして何の積なのか。いずれも、式そのものは単なる四則演算だ。けれど、そもそもどんなときに使うのかわらないのが、近寄りがたい理由だろう[*1]。そこで、内積も加えて、最近小学校の算数教材の制作で使った例を採上げる。式は前述のとおり四則演算なので、考え方に重点を置いてご説明したい。

なお、このレジュメは、2015年3月21日土曜日に催された第26回Creators Meetupで務めた高座「ベクトルふたたび − 外積と内積を使う」の参加者向けとして書いた。当日のYouTube録画をつぎに掲げる。

[*1] 2014年1月の第12回Creators MeetUpでは、「珍味ベクトル外積3種盛り」と題して外積の使い道について一席務めた(15分間のYouTube録画も掲載)。本稿はその続編といえる。興味のある方は、リンクの第12回レジュメもご覧いただきたい。


01 座標でかたちを扱う

算数教材でベクトルの外積や内積を使ったのは、図形の上でマウスポインタが重なった面や辺をハイライトするという処理だ(図001)。以下のサンプル001は、もっとも簡単なかたちである三角形について、ポインタ座標の重なりを確かめている。

図001■図形の面や辺と座標との重なりを調べる
図001左図001右

サンプル001■EaselJS 0.8.0: Using the dot product and the cross product to search the line and the face under the mouse pointer

マウスポインタが重なるとハイライトするというのは、ボタンにも使われる動きだ。たとえば、Flashでは、予めハイライトのパーツを置いておき、初めは見えないようにしたうえで、ロールオーバーしたら表示するというつくり方がよく用いられる(図002)。

図002■予めハイライトのパーツを置いてロールオーバーで表示させる (Flash Professional CC)
図002

しかし、図形をパーツに分けて扱うのが適さない場合もある。立体図形をアニメーションさせるときは、3次元の頂点座標をデータにして計算し、辺や面は線と塗りで描く方が扱いやすい(図003)。すると、マウスポインタとの重なりも、座標にもとづいて確かめなければならない。

図003■立体をアニメーションさせるには3次元の頂点座標で扱う
図003

ただし、ここで気をつけなければならないのは、マウスポインタの座標は2次元(xとy)であり、重なりは2次元平面で調べるということだ。立体の頂点座標は3次元であっても、それらに遠近法を加えて2次元平面に投影する(図004)。「透視投影」と呼ばれる考え方だ(「遠近法が投影された座標を求める」参照)。

図004■遠近法を投影した図形は2次元平面で扱われる
図004


02 座標が面の中か外かを調べる

マウスポインタと面との重なり、つまり座標が面の中か外かを調べるにはベクトルの外積が使える。3次元ベクトルの外積A×Bはベクトルで表され、方向と大きさをもつ(図005)。そして、ベクトルの外積は交換法則が成立たない。掛ける順序が変わると、方向は逆になる。

図005■3次元ベクトルの外積は方向と大きさをもつ
図005左
方向は右ネジのベクトル
図005右
大きさは平行四辺形の面積

2次元ベクトルの外積の大きさは、3次元ベクトルと同じくふたつのベクトルを2辺とする平行四辺形の面積に等しい。しかし、方向はなく、そのかわりに正負が定められている。ふたつのベクトルの位置が右ネジなら+、左ネジなら-になる。

図006■2次元ベクトルの外積は方向のかわりに正負が定まる
図006左
Aの右ネジ側がBなら+
図006右
Aの左ネジ側がBなら-

2次元ベクトルA(ax, ay)とB(bx, by)の外積A×Bはつぎの式で導かれる。なお、外積は乗算ではないので、記号「×」は「クロス」と読む。前掲サンプル001には、以下のように2次元座標のクラス(Vector2D)に外積を求めるメソッド(crossProduct2D())が備わっている。

【2次元ベクトルA(ax, ay)とB(bx, by)の外積A×B】
A×B = axby - aybx

Vector2D.prototype.crossProduct2D = function(vector) {
  var crossProduct = this.x * vector.y - this.y * vector.x;
  return crossProduct;
};

2次元平面の三角形について、座標が内側にあるかどうかは、3辺と調べる座標の外積で確かめられる。3辺のベクトルは、向きを時計回りに決めておく(図007)。そして、各ベクトルの始点と調べる座標を結んだベクトルから、それぞれ外積を計算する。3つの外積がいずれも正なら、調べた座標は3辺すべてに対して右ネジの位置にある。つまり、三角形の内側に座標があるということになる。

図007■2次元平面上の座標が三角形の内側にあるかどうかを外積で調べる
図007上
図007下左 図007下中 図007下右

前掲サンプル001は、つぎのように三角形を定めるクラス(Triangle)に備えたメソッド(hitTestPoint())により、引数の座標が三角形の内側にあるかどうか調べている。なお、このクラスはプロパティ(vertex0とvertex1およびvertex2)に3頂点座標をもつ。また、ふたつの座標をメソッド(subtract())で引き算することにより、それらを結ぶベクトルが求まる。

Triangle.prototype.hitTestPoint = function(point) {
  var vertices = [this.vertex0, this.vertex1, this.vertex2];
  var vertex = vertices[2];
  for (var i = 0; i < 3; i++) {
    var target = point.subtract(vertex);
    var vector = vertices[i].subtract(vertex);
    var crossProduct = vector.crossProduct2D(target);
    if (crossProduct < 0) {
      return false;
    }
    vertex = vertices[i];
  }
  return true;
};


03 座標が線分に近いか遠いかを調べる

座標が辺つまり線分に近いか遠いかは、距離で調べることになる。そして、2次元平面における直線と点との距離は外積で求められる(図008)。線分(ab)と点(c)をベクトル(PとQ)として求めた外積の大きさ(|P×Q|)は平行四辺形の面積に等しい。したがって、その面積を底辺となる線分の長さ(ab = |P|)で割れば、高さ(cd = |Q|sinθ)は線分と点との距離になる。

図008■2次元平面における直線と点の距離は外積で求められる
図008上
図008下

【外積P×Qから直線との距離を求める】
|P×Q| = |P||Q|sinθ
|Q|sinθ = |P×Q| / |P|

ただし、外積から求めた距離は、線分(ab)ではなく直線に対する値になる。つまり、点が線分の外側にあっても、両端から伸ばした直線との距離が導かれる。しかし、線分の両端より外側の点(c'やc'')は省きたい(図009)。そこでベクトルの内積を使う。

図009■線分の両端で座標を3つの領域に分ける
図009

2次元ベクトルA(ax, ay)とB(bx, by)の内積A・Bはつぎの式で導かれる。なお、内積の記号「・」は「ドット」と読む。前掲サンプル001には、以下のように2次元座標のクラス(Vector2D)に内積を求めるメソッド(dotProduct2D())も備わっている。

【2次元ベクトルA(ax, ay)とB(bx, by)の内積A・B】
A・B = |A||B|cosθ = axbx + ayby

図010■ベクトルAとBの内積
図010

Vector2D.prototype.dotProduct2D = function(vector) {
  var dotProduct = this.x * vector.x + this.y * vector.y;
  return dotProduct;
};

内積A・B = |A||B|cosθの正負はcosθの値で決まる。ふたつのベクトルのなす角θは、内積が正なら鋭角、負は鈍角、0のとき直角になる(図011)。つまり、線分(ab)と点(cまたはc')をベクトルとして求めた内積が負なら、点(c')は線分の外にある。

図011■線分の端の点aを通る垂線の内側か外側かはcosθの正負でわかる
図011

内積P・Q = |P||Q|cosθを一方のベクトル(P)の絶対値(|P|)で割れば、他方のベクトル(Q)にそのベクトルを投影した大きさ(|Q|cosθ)が求まる(図010参照)。その大きさが一方のベクトル(P)よりも長ければ、やはりその点(c'')は線分の端(b)を超えている(図012)。

図012■点c"が点bを通る垂線の外側にあると|Q"|cosθ"は|P|より大きい
図012

前掲サンプル001では、つぎのように三角形を定めるクラス(Triangle)に備えたメソッド(getCloseLine())が、引数の座標の近くに辺を返す。近い辺がなければ、戻り値はnullとした。まず、座標のクラス(Vector2D)の外積を求めるメソッド(crossProduct2D())で、距離が一定値(threshold)より近いかどうかを調べる。近かったら、つぎに内積を求めるメソッド(dotProduct2D())で、座標が線分の両端より内側であることを確かめた[*2]

Triangle.prototype.getCloseLine = function(point) {
  var vertices = [this.vertex0, this.vertex1, this.vertex2];
  var vertex = vertices[2];
  for (var i = 0; i < 3; i++) {
    var target = point.subtract(vertex);
    var vector = vertices[i].subtract(vertex);
    var crossProduct = vector.crossProduct2D(target);
    var crossProductAbs = Math.abs(crossProduct);
    var vectorLength = vector.getLength();
    if (crossProductAbs < this.threshold * vectorLength) {
      var dotProduct = vector.dotProduct2D(target);
      if (dotProduct >= 0 && dotProduct <= vectorLength * vectorLength) {
        return [vertex, vertices[i]];
      }
    }
    vertex = vertices[i];
  }
  return null;
};

[*2] 前述のとおり、考え方としては外積の絶対値(crossProductAbs)を底辺のベクトルの長さ(vectorLength)で割れば距離が求まる。ただし、割る値が0でないことを確かめなければならない。しかし、距離の値を直接用いるのでなく、一定値(threshold)より小さいことを確かめたいだけなので、逆にその値に底辺のベクトルの長さを乗じて比べた。

if (crossProductAbs < this.threshold * vectorLength) {

内積(dotProduct)を線分のベクトルの長さ(vectorLength)で割って座標のベクトルの投影値と比較する処理についても、割り算をせずに比べる相手のベクトルの長さに乗じている。

if (dotProduct >= 0 && dotProduct <= vectorLength * vectorLength) {



作成者: 野中文雄
更新日: 2015年4月19日 第26回Creators MeetupのYouTube録画を掲載。
作成日: 2015年3月21日


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