これはOculus Rift Advent Calendar2015の5日目です。
今回は夏のOcuFesに出展した、UnboundedSpace3(US3)の技術的な内容を書きました。US3とは、VR空間に日常やっていることを持ち込んでみるための、UIの実験プロジェクトです。
これまでの1と2は写真やドキュメントを見てみる、ということがメインでしたが、3になってウェブブラウジングの機能をつけました。
詳細は以下の記事または動画をご確認ください。
http://magicbullet.hatenablog.jp/entry/OcuFesSummer2015_JP
ここでは、Unbounded Space3の技術的な観点を2つにわけて、それぞれ説明していきます。
1. Webから記事を取得する方法
1-1 htmlページの解析ライブラリ「Html Agility Pack」の導入
Html Agility Packとは、OSSのhtml解析ライブラリです。読み込んだhtmlのタグを解析することで、htmlから特定の情報を抽出することができます。
Html Agility Packの基本的な使い方は以下が詳しいです。ご参照ください。
Html Agility Packを使ってWebページをスクレイピングするには?[C#、VB]:.NET TIPS - @IT
Unbounded Space3では、アクセスしたページから特定のタグを抽出するのに使っています。開発にはUnity5を使いました。上記の紹介ページはC#アプリケーションでの利用方法ですが、Unityでも使用できます。
以下、手順です。
1. ダウンロード
Html Agility Packのページにて、Downloadを選択して、必要な一式をダウンロードします。
次に、ダウンロードした中に入っている "Net20"フォルダに含まれている、
HtmlAgilityPack.dll
HtmlAgilityPack.xml
の2つをUnityで作ったプロジェクトのPluginsに入れます。
私の場合、後でわかりやすくするため、Plugins/HtmlAgilityPack の中に入れました。
導入はこれで完了です。注意すべき点として、netXXは使用する.NET環境のバージョンに依存するようです。当初、net40を入れたところエラーが出てしまいました。Unity5の.NETバージョンは4.0以下です。今回はnet20の中身を入れたところエラーが出なくなりました。
1-2 Html Agility Packを使った記事の抽出
Html Agility Pack(以下、HAP)を使うには、まずusing で宣言をします。
using HAP= HtmlAgilityPack;
もちろん using HAPでもよいのですが、WindowsにはHAPだと何か別のライブラリとかぶるようです。では、htmlページを読み込んで指定タグの文字列を抽出するコードの一例を示します。
/*HAPを使って宣言しているので、"HAP."を付ける*/ HAP.HtmlDocument hDoc; HAP.HtmlWeb htmlweb = new HAP.HtmlWeb(); var htmlwebdoc = htmlweb.Load("http://test.com"); /*ここまででhtmlwebdocに指定URLのページ情報がストアされる*/ /*htmlページの中から任意のタグの中身だけ抽出*/ HAP.HtmlNodeCollection nodes = null; nodes = htmlwebdoc.DocumentNode.SelectNodes("//div[@id='aaa']//a[@href]"); (1) /*foreachで回して、同じタグを持った情報を抽出して、別の変数に格納する*/ foreach(HAP.HtmlNode node in nodes){ if(Regex.IsMatch(node.Attributes["href"].Value,"http://article.com/.*?")){ webList.addTitle(node.InnerText); (2) webList.addUrl(node.Attributes["href"].Value); (3) } }
上記について、(1),(2),(3)を説明します。
(1) 抽出式
SelectNodesの引数として、htmlタグの中から抽出条件を書きます。抽出条件は、xPathという方式を使います。
xPathとは、xmlに準拠した、文字列の特定の構文を見つける仕組みです。どんな書き方があるのか、などはW3Cのサイトに記載があります。
正規表現なども使用可能で、ちょっと使うとだいたい使い方がわかる感じです。
ただ、私の場合、何となく使えそうと思ってxPathの説明をよく読まずに試したため、以下の内容ではまりました。
たとえば、下記のようなHtmlコードがあったとします。
<div id="aaa"> <div id="bbb"> <div id="ccc"> <a href="http://abc.com"> </div></div></div>
ここで、
<a href="http://aaa.com"></a>
のリンクの中身を取りたいとき、(1)のように、
//div[@id='aaa']//a[@href]
と指定すればURLの中身を取り出すことができます。
一方、
//div[@id='aaa']/a[@href]
とすると、
<div id="aaa"> <a href="http://abc.com"> ・・・ </div>
のように、直下に
<a>
があるかないか、という解析をします。
つい、再帰的に見てくれるような勘違いをしてまして、気づくのにずいぶんかかりました。
改めて説明しますと、HAPの中では、
"//" : そのタグ以下にあるタグ全てを検索
"/" : そのタグ直下にあるタグのみを検索
という概念があります。
(2) タグの中身の取り出し
HAP.HtmlNode node変数に格納した値から、innerTextを使うと、タグの中身を取得することができます。
webList.addTitle(node.InnerText); (2)
(3) タグ内のアトリビュートの取り出し
HAP.HtmlNode node変数の中から 特定のアトリビュート(属性)を取り出すには、下記のように取り出したいアトリビュートを指定し、Valueを付けます。
webList.addUrl(node.Attributes["href"].Value); (3)
1-3 非同期処理中にUnity関数利用を可能にするアセット Spicy Pixel Concurrency Kitの活用
こちらについては、Unity 2 Advent Calendar2015に投稿いたしました。
下記よりご確認ください。
非同期処理中にUnityAPIを使えるSpicyPixel Concurrency Kitのご紹介 - CrossRoad
2. uGUIを使って記事を並べたり、操作する方法
2-1 uGUIのオブジェクトを空間に配置
uGUIはそのまま使うとスクリーンに固定表示されますが、Render ModeをWorld Spaceにすると3次元空間の好きな位置に配置できるようになります。
あとはこれをInstantiateなどでVector3で好きな位置に配置することで、3次元空間に配置できます。Unbounded Space3では、頭の動きに追随するようにマーカーを置き、このマーカによって記事を選択しています。
uGUI向けにはPhysics.Raycasterというメソッドをうまく使うとuGUIオブジェクトを選択することができるようですが、開発当時はそこを調べる時間がなかったため、従来通りのRaycastHitを使いました。
このように準備しました。
1. OVRPlayerControllerの中に照準用オブジェクトをくっつける。
ここのTargetMarkerが該当します。これはPlaneにテクスチャを付け、shaderをUlintにしただけです。
2. 照準の方向にrayを飛ばす関数を作成する。
/*照準方向にrayを飛ばす関数*/ private RaycastHit findObject(){ RaycastHit rch; Ray ray; Vector3 direction = new Vector3(0,0,0); /*本来はずっとこのままなので、Start()の中で宣言した方がよいですが、便宜上ここで宣言してます*/ GameObject TargetMarkerObj = GameObject.Find ("OVRPlayerController/OVRCameraRig/TrackingSpace/CenterEyeAnchor"); /**/ direction = Vector3.Normalize(TargetMarkerObj.transform.position - CenterEyeAnchor.transform.position); ray = new Ray(TargetMarkerObj.transform.position,direction ); Physics.Raycast(ray, out rch, 5000); return rch; }
3. プレーヤーの操作で実行できるようにする。
/*例として、マウスの左クリックで呼ぶようにしてみました*/ void Update(){ if(Input.GetMouseButtonDown(0)){ executeSomething(); } } /*rayを飛ばして当たったオブジェクトに任意の処理を実行させる関数*/ private void executeSomething(){ RaycastHit rch = findObject(); if((rch.collider !=null) &&(rch.transform.gameObject.name == "<指定したオブジェクトの名前>")){ /*好きな処理を書く*/ }
2-2 iTweenによる記事選択時のアニメーション
冒頭に紹介した動画の、開始20秒付近のようなアニメーションです。これも特に難しいことはなく、2-1の処理を実行後、
GameObject go = Instantiate("<Prefabの名前>","<生成したい位置>", Quatenion.Identify); iTween.MoveTo(go,"オブジェクトの移動先",1.0f); iTween.ScaleTo(go,new Vector3(1.0f,1.0f,1.0f),2.5f);
を実行しているだけです。
Prefabのscale(x,y,z)を0.01にしておき、Instantiate後にScaleToで2.5秒かけて通常のscaleに拡大しています。これをMoveToと同時に実行することで、動画のようなアニメーションを実現しています。
2-3 DK2使用時に表示が欠けてしまう現象の対応方法
当初DK2をかぶって周辺を見回していると、このように向きによって表示が欠けてしまうことがありました。
何かPlaneでも挟んでたのかと思って、OVRPlayerControllerの中身を色々調べたのですが解決せず、、、
最終的に、@needleさんに原因と対策方法を教えていただきました。 @needleさん、ありがとうございます。
原因
描画範囲より遠いところにオブジェクトがあったため
対策
Clipping Planesを1000より大きくする。
今回の場合、作っているうちにOVRPlayerControllerとオブジェクトの距離を相当遠い位置にしていました。そのため、遠すぎて描画できなくなっていたようです。
これでUnbounded Space3の作り方解説は終わりです。
明日はwaffle_makerさんによる投稿です。