Cross Technology

Unity、VR、MR、ARを中心とした技術ブログ

VR向けUI体験プロジェクト UnboundedSpace3の開発Tips

これはOculus Rift Advent Calendar2015の5日目です。

今回は夏のOcuFesに出展した、UnboundedSpace3(US3)の技術的な内容を書きました。US3とは、VR空間に日常やっていることを持ち込んでみるための、UIの実験プロジェクトです。
これまでの1と2は写真やドキュメントを見てみる、ということがメインでしたが、3になってウェブブラウジングの機能をつけました。

詳細は以下の記事または動画をご確認ください。

G-Tune×AMD OcuFes 2015夏 に出展します - Cross Technology


ここでは、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のご紹介 - Cross Technology

2. uGUIを使って記事を並べたり、操作する方法

2-1 uGUIのオブジェクトを空間に配置

uGUIはそのまま使うとスクリーンに固定表示されますが、Render ModeをWorld Spaceにすると3次元空間の好きな位置に配置できるようになります。

あとはこれをInstantiateなどでVector3で好きな位置に配置することで、3次元空間に配置できます。Unbounded Space3では、頭の動きに追随するようにマーカーを置き、このマーカによって記事を選択しています。

uGUI向けにはPhysics.Raycasterというメソッドをうまく使うとuGUIオブジェクトを選択することができるようですが、開発当時はそこを調べる時間がなかったため、従来通りのRaycastHitを使いました。

このように準備しました。

1. OVRPlayerControllerの中に照準用オブジェクトをくっつける。

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をかぶって周辺を見回していると、このように向きによって表示が欠けてしまうことがありました。

DK2で表示が欠ける例
何かPlaneでも挟んでたのかと思って、OVRPlayerControllerの中身を色々調べたのですが解決せず、、、

最終的に、@needleさんに原因と対策方法を教えていただきました。 @needleさん、ありがとうございます。

原因

描画範囲より遠いところにオブジェクトがあったため

対策

Clipping Planesを1000より大きくする。

Clipping Planesの設定画面
今回の場合、作っているうちにOVRPlayerControllerとオブジェクトの距離を相当遠い位置にしていました。そのため、遠すぎて描画できなくなっていたようです。

これでUnbounded Space3の作り方解説は終わりです。

明日はwaffle_makerさんによる投稿です。