読者です 読者をやめる 読者になる 読者になる

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

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

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

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

magicbullet.hatenablog.jp




ここでは、Unbounded Space3の技術的な観点を2つにわけて、それぞれ説明していきます。

以下、目次です。

1. Webから記事を取得する方法

1-1 htmlページの解析ライブラリ「Html Agility Pack」の導入
1-2 Html Agility Packを使った記事の抽出
1-3 非同期処理中にUnity関数利用を可能にするアセット Spicy Pixel Concurrency Kitの活用


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

2-1 uGUIのオブジェクトを空間に配置
2-2 iTweenによる記事選択時のアニメーション
2-3 DK2使用時に表示が欠けてしまう現象の対応方法



1. Webから記事を取得する方法

1-1 htmlページの解析ライブラリ「Html Agility Pack」の導入


Html Agility Packとは、OSSのhtml解析ライブラリです。
読み込んだhtmlのタグを解析することで、htmlから特定の情報を抽出することができます。


Html Agility Packの基本的な使い方は以下が詳しいです。ご参照ください。

www.atmarkit.co.jp



Unbounded Space3では、アクセスしたページから特定のタグを抽出するのに使っています。


開発にはUnity5を使いました。上記の紹介ページはC#アプリケーションでの利用方法ですが、
Unityでも使用できます。

以下、手順です。

1. Html Agility Packのページにて、Downloadを選択して、必要な一式をダウンロードします

f:id:Takyu:20151201145400p:plain

ダウンロードした中に入っている "Net20"フォルダに含まれている、

HtmlAgilityPack.dll
HtmlAgilityPack.xml

の2つをUnityで作ったプロジェクトのPluginsに入れます。

私の場合、後でわかりやすくするため、Plugins/HtmlAgilityPack の中に入れました。

f:id:Takyu:20151201145729p:plain


導入はこれで完了です。


注意すべき点として、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に投稿いたしました。

下記よりご確認ください。

magicbullet.hatenablog.jp




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

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

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

f:id:Takyu:20151204193428p:plain


あとはこれをInstantiateなどでVector3で好きな位置に配置することで、3次元空間に配置できます。


Unbounded Space3では、頭の動きに追随するようにマーカーを置き、このマーカによって記事を選択しています。

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

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

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

f:id:Takyu:20151204222449p:plain

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

f:id:Takyu:20151205073154p:plain


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

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

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

対策

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

f:id:Takyu:20151205143241p:plain


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



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


最後に、技術的な内容ではありませんが、もう1つ紹介させてください。

このブログでは英語関係についても活動してまして、VR向けの英語表現を紹介しています。

magicbullet.hatenablog.jp

Twitterでは #VR英語 というタグでtweetしてますので、よかったらご覧ください。


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