問題
静的メソッド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を返す
原因
内部的な計算から生じる誤差により、角度が導けなくなっているバグだと考えられます。
角度の計算には、ベクトルの内積を用います。ふたつのベクトル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]。
作成者: 野中文雄
更新日: 2011年4月23日 注[*1]のテストと補足説明を追加。
作成日: 2011年4月12日