深い深い訳がある

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

 

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

 色々事情があるんだよ。

 

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

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

 

 

 このためのアルゴリズムには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くらいの範囲になって綺麗にグラデーションになる。

 

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

 以下次回

 

リブートキャンプ by Swift 目次

 

 場所移動用のマップ画面なんかは、今までに得た知識で十分表示できるわけでして…

 要するに、今のself.viewに新しいUIView追加すりゃいいわけですよ。

 でもって、用がなくなれば画面を隠すなり、取り外すなりすればいいわけです。

 ちなみにUIViewの取り外しは、addSubviewの時のように親側UIViewじゃなく、取り外される子側のUIViewにremoveFromSuperview()メッセージを送ります。

 

取り付ける時:

  親UIViewオブジェクト.addSubview( 子になるUIViewオブジェクト )

 

取り外す時:

  子UIViewオブジェクト.removeFromSuperview()

 

 で、追加するUIViewの中に、サブ画面としてUIImageViewとか貼り付けて地図出すとか、そのまた上に別のUIImageView貼り付けて、キャラクタのアイコンなりを表示させて現在位置を示すとか、色々やればいいわけです。

 なんならアニメーション付きでマップ画面出しましょうか、くらいのこと言う猛者もいるとは思うし、それで正解ちゃ正解なんですが…

 

 そうやって調子こいて、今までみたいにViewControllerに処理追加していくと、どんどんViewControllerのソースコードが肥大化するわけですな。

 で、半年くらい時間あけて見直した時に、コードが込み入りすぎてて「何やりたかったんだ、俺?」みたいなことになっちゃう。

 今でも十分、リファクタイリング(再因子化。現行のソースコードの関数やクラス定義を見直して、見通しの良い構造に組み替える)したいレベルなんですが、これ以上悪化させないためにはどうするか?

 こういう場合、UIViewを派生させてマップ画面専用のUIView派生クラスを作ったりします。

 

 class MapView : UIView {

   ・・・

  }

 

 そのままViewController.swiftにMapViewクラスの定義を書いてもいいんだけど、クラス定義ってのは、ファイルを分離するのに、ちょうどいい感じの単位なんで、大概は別ファイルにします。

 新しいswiftファイルを作るには、File→New→File…メニューを選んで、出てきた画面でSwift Fileテンプレート選択てのが一番基本なんですが、UIKit系の派生クラス用ならCocoa Touch Classテンプレートを使いましょう。

 

 

 Cocoa Touch Classテンプレートを選ぶと、次の画面ではこんな感じでUIViewを派生させて、MapViewクラス用意しますってのを指定する。

 

 その次の画面は、ファイルをどこに置くかの指定。プロジェクトフォルダ内が表示されてると思うので、そのまま保存でOK。そうすっとナビゲーションエリアにMapView.swift項目が追加されます。

 

 

 あらかじめUIViewの派生としてMapViewクラスの定義まで書いてくれてるので、あとは必要な処理として、作成時に地図画像用のサブ画面準備とか、ユーザーのタップに対する対応とかの関連作業をやっちゃえば、ViewController側は、そのMapViewを作ってself.viewに追加する程度で地図画面対応ができるわけっす。

 

        let mapView = MapView(frame:self.view.bounds)

        mapView.frame.origin.y = mapView.frame.maxY

        self.view.superview?.addSubview(mapView)

        UIView.animate(withDuration: 1) {

            mapView.frame.origin.y = self.view.bounds.origin.y

        }

 

ちなみに作ったMapViewは、self.viewじゃなく、self.view.superviewとして、self.viewの親UIViewに追加してます。

 こんな感じで、superviewプロパティには親側UIViewの参照が設定されとります。どこにもaddSubviewされてないUIViewの場合はsuperviewはnilになるんでオプショナルっす。オプショナルがわからん人は「アンラップしてチン♪」を読みましょう。親UIViewに追加する理由は次回。

 

 この例では、単に追加じゃなく、下から迫り上がるアニメーションでMapViewを追加表示してる。

 でもって、表示されたMapViewは、下に向けてのスワイプで画面を閉じるようにしました。こっちもアニメーション付き。

 

サンプル:

http://tetera.jp/xcc/book-sample/modalview.zip

 

 なんですが…

 実はAppleはこの手の画面切り替えをUIViewController間で行うことを推奨してまして…

 こんな感じで全面にマップ画面を出すなら、UIViewControllerを派生させてMapViewControllerなんかを定義して、これとViewController間で切り替えを行う、ってのがiOSアプリでの王道ってことになっとります。

 

 そこんところや、今回のUIViewの派生方法なんかは次回!