サイトトップ

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

Macromedia Flash非公式テクニカルノート

Vector3D.angleBetween()メソッドの戻り値がNaNになる

ID: FN1104001 Product: Flash CS4 and above Platform: All Version: 10 and above/ActionScript 3.0

問題
静的メソッドVector3D.angleBetween()は、ふたつのVector3Dオブジェクトがなす角をラジアン値で返します。ところが、ふたつのVector3Dオブジェクトのなす角がπ(180度)または0のとき、戻り値はNaNになることがあります。たとえば、つぎのスクリプトがその例です(図001)。

var a:Vector3D = new Vector3D(1, 1, 1);
var b:Vector3D = new Vector3D(-1, -1, -1);
var c:Vector3D = a.clone();
trace(Vector3D.angleBetween(a, b), Vector3D.angleBetween(a, c));   // 出力: NaN NaN

図001■Vector3D.angleBetween()メソッドがNaNを返す
図001上図

図001下図


原因
内部的な計算から生じる誤差により、角度が導けなくなっているバグだと考えられます。

角度の計算には、ベクトルの内積を用います。ふたつのベクトルaとbがなす角をθとすると、内積a・bはつぎの式で表されます(後出Vector3D.dotProduct()メソッドのリンク参照)。なお、ベクトルの絶対値(|a|および|b|)は、ベクトルの長さ(大きさ)として定められます。

a・b = |a||b|cosθ

この式から、cosθを求めることができます。そして、cosの値を角度に変えるのは、逆三角関数cos-1です。つまり、角度はつぎのように導かれます。

cosθ = a・b / |a||b|
θ = cos-1(a・b / |a||b|)

この式にもとづき、ふたつのVector3Dオブジェクトを引数に渡すと角度が返される関数は、つぎのスクリプト001のように定義されます。ベクトルの内積はVector3D.dotProduct()メソッド、長さはVector3D.lengthプロパティで得られます。

スクリプト001■ふたつのVector3Dオブジェクトを引数に角度を返す関数

function xAngleBetween(a:Vector3D, b:Vector3D):Number {
  var nDotProduct:Number = a.dotProduct(b);
  var nMultipliedLength:Number = a.length * b.length;
  var nCos:Number = nDotProduct / nMultipliedLength;
  return Math.acos(nCos);
}

上記「問題」の項で掲げたふたつのVector3Dオブジェクトa(Vector3D(1, 1, 1))とb(Vector3D(-1, -1, -1))について、スクリプト001の関数(xAngleBetween())が計算の過程で求めた値は下表001のとおりです。

表001■
求める式 スクリプトの式 求めた値
内積a・b a.dotProduct(b) -3
絶対値の積|a||b| a.length * b.length 2.9999999999999996
cosθ = |a||b| / a・b a.length * b.length / a.dotProduct(b) -1.0000000000000002

Vector3Dオブジェクトの長さ(Vector3D.lengthプロパティ)はaとbともに√3ですので、その積|a||b|は3です。しかし、表001では誤差が生じて3に達しません(2.9999999999999996)。そのため、cosθは-1をわずかに超えています(-1.0000000000000002)。ところが、逆三角関数cos-1(Math.acos()メソッド)は±1の範囲でのみ定義されています。その範囲を外れれば計算できず、NaNを返してしまうのです。


対処法
誤差は防げません。cos値が定義域(±1)を超えるかどうか判定して、処理する必要があるでしょう。つまり、cos値が1を超えたら1にして角度は0、-1を超えたら-1で角度π(180度)とします(スクリプト002)。

var a:Vector3D = new Vector3D(1, 1, 1);
var b:Vector3D = new Vector3D(-1, -1, -1);
var c:Vector3D = a.clone();
trace(xAngleBetween(a, b), xAngleBetween(a, c));   // 出力: 3.141592653589793 0

スクリプト002■ふたつのVector3Dオブジェクトを引数に角度を返す修正した関数

function xAngleBetween(a:Vector3D, b:Vector3D):Number {
  var nDotProduct:Number = a.dotProduct(b);
  var nMultipliedLength:Number = a.length * b.length;
  var nCos:Number = nDotProduct / nMultipliedLength;
  if (nCos > 1) {
    return 0;
  } else if (nCos < -1) {
    return Math.PI;
  } else {
    return Math.acos(nCos);
  }
}

スクリプト002の関数xAngleBetween()は、ふたつのVector3Dオブジェクトから内積を求め、cos値で振分けたうえで、Math.acos()メソッドにより角度を返しています。けれど、その計算の速さは誤差の問題を判定しないVector3D.angleBetween()メソッドと比べて遜色はありません[*1]

[*1] ご参考までに、スクリプト002と同じ処理の関数とVector3D.angleBetween()メソッドの速さを、wonderflのサンプルスクリプトで比べてみました(念のため同じテストを2回行っています)。

Vector3D.angleBetween() vs Math.acos() + Vector3D.dotProduct() methods - wonderfl build flash online


作成者: 野中文雄
更新日: 2011年4月23日 注[*1]のテストと補足説明を追加。
作成日: 2011年4月12日


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