深い深い訳がある

物理シミュレーション目次

 

 今回は、物理シミュレーションじゃなく、移動ロボットの障害物回避シミュレーションやるっす。

 色々事情があるんだよ。

 

 扱うのは、センサベースドナビゲーション。

 こいつは、移動ロボットが走行中にまわりの障害物の情報(位置や形状など)を 外界センサ(ビジョンや超音波センサなど)で獲得し,それに基づいて 目的地までの経路をリアルタイムで探し出すってやつです。

 

 

 このためのアルゴリズムにはLumelskyとかSankaranarayananとか色々あって、それぞれに長所短所があるみたい。

 で、実物だと、超音波センサなんかで距離測定して、目的地方向に障害物がないか調べて、障害物があったら回避だ〜って話なんだけど、シミュレーションじゃ超音波センサないわけですよ。

 どうしようかって話なんだけど、正統派なら、ロボットから目的地に向けての視線を飛ばして、各障害物との交差判定ってことになります。

 なんだけど、これって結構めんどくさいわけで…

 障害物を構成する形状平面と直線の交差判定を、延々繰り返すことになるわけで、障害物が多いと、色々工夫(障害物を視点からの距離でソートしたりとか)しないと処理がムッチャ遅くなるんですな。

 

 

 ま、そういう正統派もありなんですが、せっかくWebGL使ってるわけだし、ここはひとつ、実際にロボットから目的地への視線で画像を作り出して、その際に副産物で出来上がる深度バッファ利用しちゃおうかなと考えるわけです。

 

深度バッファ

 てのは、知らない人は、なんすか、それ?って話なんだけど、3DCGを描くときに、物体の前後関係を知るために利用されるバッファのことです。

 具体的には「WebGLでいきますぜい」で描いた立方体と球体の図なら

 

 

 次のような、深度バッファが自動的に副産物として作成されてます。

 実際には、深度は0.0〜1.0で表されるんだけど、これをグレー階調で表現するとこうなるわけです。白くなるほど視線からの距離がある。

 

 なんでまた自動的に副産物として深度バッファなんてものが作られるかというと、現在の3DCG描画処理の主流が、物体の形状表面を2D画面に描く時に

 

 1)すでに自分より手前にある形状表面によって画面が描かれていたら、そこには自分の形状表面を描かない。

 2)描く描かないは、画素単位で判定する。

 

 

というルールのもとに行われるようになってるからです。

 これで、どの形状表面を先に描こうが、前後関係が正しくなるわけなんですな。

 注意)厳密には形状表面を細かな三角形平面に分けて、その三角形単位で行う

 

 Zバッファ法といって、「iPhoneアプリ開発、その(98) 」で、もうちょっと詳しく説明してます。興味がある人はよんでみてね。

 

 で、このルールを実践するには、1つの形状表面を描くたびに、画素単位でその画素に描かれた形状表面の視点からの深度を記録しないと、次の形状表面を描くときに評価できないわけですよ。そのために深度バッファが必要になる。

 600x600の2D画面に表示するなら、600x600個の深度を記憶するバッファが必要になります。

 

 現在のコンピュータは3D画像は基本、描画専用の装置が担当することになってて、コンピュータの中央処理装置(CPU:Central Processing Unit)にちなんで、グラフィックの処理装置(GPU:Graphics Processing Unit)て呼ばれてるんですが、こいつが行う3DCG描画処理には、いくつかある描画方法のうちから、この深度バッファを使う方法が採用されているんですな。

 

 ちなみに「WebGLでいきますぜい」のサンプル:cube-600.htmlの以下の部分

 

        cube = new THREE.Mesh(

            new THREE.CubeGeometry(50,50,50),

            new THREE.MeshLambertMaterial({color: 0xffffff}));

 

 こいつを

 

        cube = new THREE.Mesh(

            new THREE.CubeGeometry(50,50,50),

            new THREE.MeshDepthMaterial());

 

と書き換えると、立方体だけ深度バッファをグレー階調化した描画になります。

 このさいに、カメラ側の設定もちょっといじらないと、綺麗なグレーのグラデーションじゃなく、真っ白になる。

 綺麗なグレーのグラデーションにしたいなら

 

        camera = new THREE.PerspectiveCamera( 45 , 

                frame.clientWidth / frame.clientHeight , 1 , 10000 );

 

 

        camera = new THREE.PerspectiveCamera( 45 , 

                frame.clientWidth / frame.clientHeight , 50 , 100 );

 

に変更だ!

 こうしないと、70〜90くらいの深度にある立方体の表面が

 

 1(最近)〜10000(最遠)

 

の範囲の中で、0.0〜1.0の間に正規化されるので

 

 (70〜90 -1) / (10000 - 1)

 

で、0.007〜0.009くらいの範囲になって、ほとんど変化しなくなっちゃうんですな。

 これを

 

 50(最近)〜100(最遠)

 

とするのが、上の修正。

 

 (70〜90 -50) / (100 - 50)

 

で、0.4〜0.8くらいの範囲になって綺麗にグラデーションになる。

 

 で、表示的には、こんなお手軽な方法で深度バッファを表示できるんだけど、私がやりたいのは、この深度バッファの深度情報を取り出すことなわけでして、そのためにやったのが…

 以下次回

 

AD

全方位に投げつけろ!

 昔、うんこ投げるゴリラいたな。

 いや、そうじゃない。

 

 前回の実装は運動方程式に基づいて動かしてるので、当然、上に投げると重力で落ちてくるようになってます。

 

 ただ、カメラポジションとか立方体の大きさなんかも、初期高度を示す変数altitudeに依存させちゃってるので、altitudeを0mとかにすると画面が変になります。なので、元々のaltitudeは画面最大高度ということにし、新しく高度用変数positionを追加することにしました。で、このpositionを逐次更新することにしdisplacementは廃止します。

	var altitude = 828;		//	初期高度画面最大高度 m
	var startDate;			//	開始時刻
	var lastDate;			//	最後の移動処理の時刻
	var velocity = 0;		//	初速度 m/s
	var displacement = 0;		//	変位 m
	var position = 0;		//	高度 m

 それと、空気抵抗も落ちるの前提だったので、これを進行方向に逆らう形に直す。

	function sign(value) {
		return (value < 0) ? -1 : 1;
	}

	function updateTime(deltasec) {		
		・・・
		var spv2 = s / 2 * p * velocity * velocity * sign(velocity);
		・・・
		velocity += (-9.8 + (-a * Lnv - b * spv2) / m) * deltasec;
		・・・
	}

 こうしないと、投げ上げる時に空気抵抗で上方向に加速しちゃうことになるんでな。

 新しく追加したsignメソッドは、引数で受け取ったvalueの値が正なら1、負なら-1を返すというもの。その中でやってる

		return (value < 0) ? -1 : 1;

 は三項演算子( ? : )を使ったもので、条件によって値を変えたい時に使うもんです。

 

    条件 ? 条件が真の時の値 : 条件が偽の時の値

 

 結果、以下のように書いたことと同じ意味になります。

	if (value < 0) {
		return -1;
	} else {
		return 1;
	}

 後は、displacementの削除、positionの追加に合わせた細かな調整なので、説明は省略。重要なのはupdateTime関数の計算の変更で、こうすることで速度が上向きなら下向きに、下向きなら上向きに空気抵抗が働くことになる。

 これで準備OKなんで、例えば20m/sの初速度を与えれば…

 

    var velocity = 20;                    //    初速度 m/s

 ↓こうなる

 

http://www.tetera.jp/xcc/ameba/free-throw.html

 

 なかなかいい動きします。

 で、ここまできたら、せっかくの3次元表示なんで、縦横斜め、八方になげられるようにしたいですよね。私はしたいです。

 

 全然関係無いけど、よく言われる四方八方の8や、八極拳の8は3次元空間のx,y,z3軸、左右、上下、前後の組み合わせの8だから、平面上で8方向想像してた人、間違いだからそれ。一極集中(ビッグクランチ)の太極拳と、全方位膨張(ビッグバン)の八極拳です。小学校の頃、二極拳〜七極拳とかもあるのかと、そんな風に考えていた時期が俺にもありました。

 
 なので高度を扱うだけの上下方向のみだった速度や加速度、位置を3次元に拡張します。

 スカラ:scalarからベクタ:vectorへ。

 スカラってのは、これまで使ってきた単独の量のことね。

 

  5とか、2とか

 

 ベクタってのは、複数のスカラで構成される量。

 例えば、x軸、y軸、2つの座標値をペアにした2次元ベクトルってのを習ったと思いますが、あれがベクタです。

 1つの点の位置を表現するのに2つのスカラ値(x、y)を使っていたでしょ。

 

  (5, 2)とか

 

 

 ベクトルもベクタも、英語のvectorをどう読むかであって、全く同じだけど、日本では、ベクトルは「向き」ってイメージが強いっすね。

 

 でも複素数をベクタで表現(実数部, 虚数部)したりもするんで、一概に「向き」って言っちゃうのはどうかと思われ。スカラ、ベクタ混在で構成するベクタとかもあるし。そもそもスカラ値のプラスマイナスからして数直線上の向きって言えなくもないし…

 ちなみに複素数の複素平面(ガウス平面というのじゃよ)を見て、あれ、これ2次元表現するのにちょうどよくね?って極座標使ってeのなんちゃらってやってsin、cosの式を簡潔に書いたりするとか、3次元でも使える複素数あるんじゃねとか言ってハミルトンが4元数とか言い出す話はまた今度。

 

 まあどっちでもいい。

 

 で、Three.jsにも3次元ベクタがオブジェクトとして定義されてるんで、こいつを使って、先のプログラムを書き直しました。

	var velocity = new THREE.Vector3( 0.7, 20, 0);	//	初速度 m/s
	var position = new THREE.Vector3( 0, 1.7, 0 );	//	初期位置 m

 THREE.Vector3はx,y,zプロパティを持つオブジェクトです。

 プロパティ?オブジェクト?って人は「走れJavaScriptちゃん!」から出直してこい!

 ま、それはいいとして、上のように書くと、velocityは(x、y、z)要素の順に(0.7, 10, 0)が設定されたTHREE.Vector3オブジェクトを示すようになり、positionは( 0, 1.7, 0 )が設定されたTHREE.Vector3オブジェクトを示すようになります。

 で、このpositionのx、y、z値をsphereのpositionにcopyメソッドを使って設定しています。実はTHREE.Meshの持つpositionプロパティもTHREE.Vector3なのでこんなことができる。「超高校級のMikuMikuDance」で話した「positionはカメラオブジェクトのプロパティで、3次元空間でのカメラ座標を示す。この3D座標を表現するプロパティ自身もオブジェクト」もTHREE.Vector3。

	function arrengeObjects() {
		・・・
		sphere.position.copy(position);

 それと床に立方体を使うのはやめて平面(THREE.PlaneGeometry)にしました。ここら辺は自分で調べてみてください。

 updateTimeメソッドもTHREE.Vector3に合わせて変更してるけど、x、y、zそれぞれの要素に対して運動方程式で計算してるだけで違いはないです。

 加速度のy成分だけ重力加速度が追加されるわけやね。

		var F = new THREE.Vector3();
		F.x = - nR * velocity.x 
			- D * (velocity.x * velocity.x) * sign(velocity.x);
		F.y = - nR * velocity.y 
			- D * (velocity.y * velocity.y) * sign(velocity.y); 
		F.z = - nR * velocity.z 
			- D * (velocity.z * velocity.z) * sign(velocity.z); 

		var acc = F.divideScalar(m);
		acc.y += -9.8;

 それと人間を投げあげるのは豪快すぎるので、バレーボール投げあげることにします。

 

  半径:10cm

  質量:200g

 

としました。それに合わせ、粘性抵抗

 

 

については、バレーボールにしたことだし素直にナビエストークの式使って

 

    F = 6πrηv

 

とします。結局人間用のαがわからんかったからね。

 

 THREE.Vector3のaddメソッドは、引数で渡したTHREE.Vector3のx,y,z値を加算するというものです。

 例えば、a、b、二つのTHREE.Vector3オブジェクトがあれば

 

        a.add(b)

 

でaのx,y,zは、それぞれbのx,y,zが加算された状態になります。

 

   a.x += b.x;

   a.y += b.y;

   a.z += b.z;

 

 こんな感じ。「+=」は左辺の変数(またはプロパティ)に右辺の値を加算という意味ね。

 multiplyScalarの場合は、引数で渡した値がx,y,z値にかけられます。

 

        a.multiplyScalar(2)

 

なら

 

        a.x *= 2;

        a.y *= 2;

        a.z *= 2;

 

とやったことと同じです。「*=」は「+=」の掛け算版です。

 でもってdivideScalarはその割り算版。

 cloneメソッドは複製で

 

    b = a.clone();

 

でbは、aのx,y,z値を持つ、新しいTHREE.Vector3オブジェクトを示すことになります。

 

 詳細はupdateTimeメソッド見てもらうとして、これでようやく斜め発射もできるようになる。

 で、ここまできたら床で跳ね返らせたいわけですよ。

 なので、床に当たると速度ベクタを運動エネルギー保存則使って変換します。

 

   v' = v - 2(v・n)n

 

   v':衝突後の速度ベクタ

   v:衝突時の速度ベクタ

   n:床の法線ベクタ

   

 v・nってのは内積です。

 THREE.Vector3にはdotメソッドとして用意されてます。便利。

 完全弾性衝突なんで、エネルギー減らず、減るのは空気中の移動による抵抗だけってことで、ボールはしつこく跳ね続けます。

 

 ↓それが、これだ!

http://www.tetera.jp/xcc/ameba/free-throw-3d.html

 

    ↓jsdo.it版

http://jsdo.it/reborn_xcc/ULWR

 

 完全弾性衝突とか内積とか、法線ベクタとか、詳細は次回!

AD