やじゅ@アプリケーション・ラボ わんくま支局

目次

Blog 利用状況

ニュース

プロフィール

MSMVP

[Javascript] SinとCosの誤差発生について

Javascriptに限らないのですが、コンピュータでは数学と違って誤差が発生するケースがあります。
これは、コンピュータが取り扱うデータは、ビット数の限られた有限のものだからです。
例えば、1÷3の結果である0.3333…という循環小数をコンピュータが正確に表すことはできません。

MathクラスのSinメソッドとCosメソッドについて
http://social.msdn.microsoft.com/Forums/ja/vsgeneralja/thread/fc03347d-c9a3-4e6f-b378-f781dd1d9ff4

下記の表は、各角度に対して左側SIN,COSが数学上での値、右側SIN,COSがコンピュータ(Javascript)での値となります。

角度

SIN

COS

SIN(角度*Math.PI/180)

COS(角度*Math.PI/180)

90

1

0

1

6.1232339957366e-17

180

0

-1

1.2246063538223773e-16

-1

270

-1

0

-1

-1.836909530733566e-16

360

0

1

-2.4492127076447545e-16

1


これに気が付いたのが、Javascriptでキャラクターを角度指定でとりあえず上下に動かすように作成していた時に、下方向から反転して上方向に移動したとたんに、キャラクターが横方向に動いてしまったからです。
Chromeでデバッグしてみると、COSが270度の時に本来0を想定していたのに、-1.836909530733566e-16という値が返ってきたために、x<0の条件で、角度が-90度されて180度の動きになってしまったのです。

function moveFish(no){
    // 移動量を求める
    var rot = fish[no].ang;
    var sp = fish[no].speed;
    dx = Math.cos(rot * Math.PI / 180) * sp;
    dy = Math.sin(rot * Math.PI / 180) * sp;

    fish[no].x += dx;
    fish[no].y += dy;

    var x = fish[no].x;
    var y = fish[no].y;

    if(x > (canvas.width-fish[no].image.width)){
        fish[no].ang += 90;
    }
    if(x < 0){
        fish[no].ang -= 90;
    }

    if(y > (canvas.height-fish[no].image.height)){
        fish[no].ang += 180;
    }
    if(y < 0){
        fish[no].ang -= 180;
    }
}

では、どうやって対処するのか?
今回の場合はキャラクターを画面に表示するのが目的なので、小数ではなく整数に変換します。
日経ソフトウェア5月号のHTML5ゲームプログラミング入門の記事によると「drawImage関数は小数を含む座標を渡してしまうと、処理が一気に数倍重くなります。これは小数を含む場合はアンチエイリシングをしてキレイに見せようとするためだと思われます。」と書いてあります。
このように処理速度も考慮しても整数化した方がいいわけです。

小数を整数化するメソッドとして代表的なものに、Math.floor、Math.ceil、parseIntですが、
これ以外にテクニックとして、論理演算(例 x | 0)を使って整数化する方法があります。
例では、ゼロで論理和を取ることで整数に変換します(Math.floor(x)とはマイナス時に違いがある)。
http://d.hatena.ne.jp/amachang/20070813/1186980089
また、Math.floor(x)を使うよりはブラウザによってはかなり高速になります。
http://d.hatena.ne.jp/htsign/20120211/1328970561
JavaScriptのビット演算の仕組みを理解する
http://d.hatena.ne.jp/mindcat/20091119/1258651717

■COS
rot = 90; //数学上では、1となる
alert(Math.cos(rot * Math.PI / 180))                  //6.1232339957366e-17
alert(Math.floor(Math.cos(rot * Math.PI / 180)))  //0
alert(Math.ceil(Math.cos(rot * Math.PI / 180)))    //1
alert(parseInt(Math.cos(rot * Math.PI / 180)))     //6
alert(Math.cos(rot * Math.PI / 180) | 0)             //1   

var rot = 270; //数学上では、0となる
alert(Math.cos(rot * Math.PI / 180))                  //-1.836909530733566e-16
alert(Math.floor(Math.cos(rot * Math.PI / 180)))  //-1
alert(Math.ceil(Math.cos(rot * Math.PI / 180)))   //0
alert(parseInt(Math.cos(rot * Math.PI / 180)))    //-1
alert(Math.cos(rot * Math.PI / 180) | 0)            //0

■SIN
var rot = 180; //数学上では、0となる
alert(Math.sin(rot * Math.PI / 180))           //1.2246063538223773e-16
alert(Math.floor(Math.sin(rot * Math.PI / 180)))  //0
alert(Math.ceil(Math.sin(rot * Math.PI / 180)))    //1
alert(parseInt(Math.sin(rot * Math.PI / 180)))     //1
alert(Math.sin(rot * Math.PI / 180) | 0)             //0

var rot = 360; //数学上では、0となる
alert(Math.sin(rot * Math.PI / 180))                  //-2.4492127076447545e-16
alert(Math.floor(Math.sin(rot * Math.PI / 180)))  //-1
alert(Math.ceil(Math.sin(rot * Math.PI / 180)))   //0
alert(parseInt(Math.sin(rot * Math.PI / 180)))    //-2
alert(Math.sin(rot * Math.PI / 180) | 0)            //0

   

結果からすると、正しい答えを出すのは、論理演算だけなんですね。
parseIntは、文字列を数値にする目的なだけあって、小数点以降を切るだけなので指数表示になると駄目ですね。

東大生が教えるビジュアル数学 三角関数
http://www24.atpages.jp/venvenkazuya/math1/trigonometric_ratio4.php

投稿日時 : 2012年4月7日 23:37

コメントを追加

No comments posted yet.
タイトル  
名前  
URL
コメント