Mercari Advent Calendar 2018 の11日目はMercariのR&D部門 R4DでXR Researcherをしている @satomi がお送りします。
私は主にVR(Virtual Reality)やAR(Augmented Reality)方面の研究開発を行っているのですが、それ以外にも直接的に仮想空間を現実世界に召喚する技術として、ホログラム等の3Dディスプレイ技術にも興味を持っています。
今年の8月頃に一部の3Dディスプレイマニアの間で話題になった製品 Looking Glass をご存じの方も多いと思うのですが、私自身 Kickstarter 経由で出資をしたものの
予定の12月になってもまだ届かないという事で、本記事では手元に有った半球面型ディスプレイを用いて擬似的な3Dディスプレイを作ってみました。
レシピ
ハードウェア
- 半球面型ディスプレイ 1個 (今回は、学研から発売されている WORLDEYE を使用しました)
- 3Dセンサー (Intel製の RealSense D435 を使用しましたが、Kinectの様な3Dセンサーでも大丈夫だと思います)
- Windows PC (Windows 10が動作するノートPCでもデスクトップPCでも大丈夫ですがHDMI出力が必要になります)
- HDMI to mini HDMI Cable (WORLDEYEがmini HDMI入力の為に必要になります)
ソフトウェア
- Unity
- NUITRACK SDK (最新のRealSense SDK 2.0では、Skelton Tracking機能が利用できなくなった為、その代わりに同様の機能を持つNUITRACKを利用します)
- 表示させたい3Dモデル (今回は ユニティちゃん を使用させていただきました。 © Unity Technologies Japan/UCL)
基本的な実装方針
半球面型ディスプレイを用いて擬似的な3Dディスプレイを実現するためには、観察者の頭部(正確には、眼球の)3次元位置を3Dセンサーを用いて推定し、その位置から見られる画像をリアルタイムで計算機で生成し、それを半球面ディスプレイに表示する事により実現することが出来ます。
この考え方は、現実世界で行われるプロジェクションマッピングと非常に似ており、言わばUnity内の仮想空間の中で次の手順を実装する事により実現可能です。
- 仮想空間内の原点付近に表示させたい3Dモデルを配置します。
- 仮想空間内のCameraを観察者の頭部の位置に配置し、その視点から原点方向を見るように調整します。
- Cameraから見た画像をRender Texture経由でProjectorコンポーネントを用いてCameraと同方向に投射します。
- 現実空間のディスプレイと同形状(今回はSphereオブジェクト)のScreenオブジェクトを仮想空間に配置しておき、Projectorからの画像を投射させます。
- Screenオブジェクトの表面の画像をそのままHDMI経由で半球面型ディスプレイに表示します。
鑑賞したい3Dモデルを配置
半球面型ディスプレイの中心に3Dモデルが表示できるように自分の推しの3Dモデルを配置します。
この3Dオブジェクトは、観察者のCameraだけに見えるように新しくAudience
Layerを生成し、そのLayerに配置するようにInspectorで設定します。
WORLDEYEの直径は実測で約24[cm]程度であるので、そのサイズに合わせて3Dモデルの大きさを調整しておきます。
今回のユニティちゃんでは、Scaleを大体 0.12 程度に設定しました。
仮想空間に観察者のCameraを配置
まず、観察者のCameraは3Dセンサー(RealSense)からの相対位置になるためにRealSenseに対する相対座標を調整可能にするためにRealSenseCoord
のEmptyオブジェクトを作成し、実空間におけるWORLDEYEとRealSenseの相対位置に併せて位置を設定します。
このRealSenseCoord
の下にCameraを配置するCameraPos
Emptyオブジェクトを作成しCameraコンポーネントを追加します。
CameraコンポーネントのInspectorを開いて次の設定をします。
- Culling Mask を
Audience
Layerだけに設定します。 - FoV(Field of View)を45度に設定します。
Audience View
というRender Texture (サイズ4096×4096)を作成し、それをTarget Textureに設定します。
NUITRACKから取得した位置をCameraPos
オブジェクトに反映する為に、NUITRACK SDKのUnity packageをインポート後、新しく次のC#スクリプト(ProjectorCalibration.cs
)を作成しアタッチします。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ProjectorCalibration : MonoBehaviour { public GameObject target; void Start() { } void Update() { if (CurrentUserTracker.CurrentSkeleton != null) ProcessSkeleton(CurrentUserTracker.CurrentSkeleton); transform.LookAt(target.transform); } void ProcessSkeleton(nuitrack.Skeleton skeleton) { //Calculate the model position: take the Torso position and invert movement along the Z axis Vector3 torsoPos = Quaternion.Euler(0f, 180f, 0f) * (0.001f * skeleton.GetJoint(nuitrack.JointType.Head).ToVector3()); transform.localPosition = torsoPos; } }
このスクリプトのパラメータのtarget
には、後ほど仮想スクリーンとして作成するSphereオブジェクトを設定してください。
仮想スクリーンに投射する為のProjectorを配置
CameraPos
オブジェクトに新しくProjectorコンポーネントを追加し、Inspectorを開いて次の設定をします。
- FoVをCameraコンポーネントと同じ値(45度)に設定します。
Audience View
Render Textureを持つ新しいProjector
Materialを作成し、Materialに設定します。
このProjectorからの投射に関しては、減衰などを無視した特殊なProjector
Shaderを新規に作成してProjector
MaterialのShaderに設定します。
Shader "Custom/Projector" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } Pass { Offset -1, -1 CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; float4x4 unity_Projector; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; float4 _MainTex_ST; v2f vert (appdata_base v) { v2f o; o.pos = UnityObjectToClipPos (v.vertex); float4 p = mul(unity_Projector, v.vertex); o.uv = p.xy / p.w; return o; } half4 frag (v2f i) : COLOR { return tex2D (_MainTex, i.uv); } ENDCG } } FallBack Off }
仮想スクリーンを配置
原点にWORLDEYEに相当するサイズ(Scale 0.24)のSphereオブジェクトを配置します。
但し、このままでは最終的なWORLDEYE用のCameraがSphereオブジェクトの中に入ってしまう関係で、うまくProjectorからの投射が反映されません。
具体的には、CullingをOFFにしたStandard Surface Shader(cullOffShader.shader
)を作成して球体オブジェクトのMaterialに適応します。
SubShader { Tags { "RenderType"="Opaque" } Cull Off // <-- Add this line. LOD 200
更に、Mesh Colliderを反転させるために次のC#スクリプト(ReverseCollider.cs
)を作成してアタッチします。
using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; public class ReverseCollider : MonoBehaviour { public bool removeExistingColliders = true; private void Start() { CreateInvertedMeshCollider(); } public void CreateInvertedMeshCollider() { if (removeExistingColliders) RemoveExistingColliders(); InvertMesh(); gameObject.AddComponent<MeshCollider>(); } private void RemoveExistingColliders() { Collider[] colliders = GetComponents<Collider>(); for (int i = 0; i < colliders.Length; i++) DestroyImmediate(colliders[i]); } private void InvertMesh() { Mesh mesh = GetComponent<MeshFilter>().mesh; mesh.triangles = mesh.triangles.Reverse().ToArray(); } }
Screenオブジェクトの表面の画像をWORLDEYEに出力
以上の処理で球体オブジェクト表面に必要な画像が投射された状態になります。
WORLDEYEへ投射するには、さらに画像を補正する必要がありますが、ここは先人の知恵を借りようと思います。
この記事に載っている正距方位図法を用いた方式でSphereオブジェクトに投影された画像をHDMIに出力すれば擬似的な3Dディスプレイの出来上がりとなります。
完成デモ
最終的なWORLDEYEとRealSenseの配置については、色々試行錯誤した末に下記図のようにWORLDEYEの背後に三脚を立ててRealSenseを斜め後ろの位置に配置しました。
実際にユニティちゃんを動作させた動画が次のようになります。
まとめ
本記事では、WORLDEYEという学研の半球面型ディスプレイ装置と3Dセンサーを組み合わせて、観察者から見て3Dディスプレイの様に振る舞う装置を作成してみました。
もっと簡単に実装できそうな予想をしていたのですが、Unityで実装するためには、技術的にも色々な隘路事項を乗り越える必要がありましたので、
この記事ではその点についてより詳細にまとめてみました。
このアイデア自身は、数年前から温めていて平面型のディスプレイで試作してみた経験があったのですが、今回より立体的な半球面型ディスプレイ装置を用いることにより、
観察可能な範囲( 例えば真上からも観察可能)が広がりより3Dディスプレイっぽい表現が出来るようになったのではないかと感じています。
今後の課題としては…
- 利用した半球面型ディスプレイ装置であるWORLDEYEの解像度が低い為、ユニティちゃんの詳細な姿が拝見できなかった。
- 小型の3Dセンサー(RealSense)を用いた為、動作検出範囲と精度が思ったよりも限定されてしまった(Kinectを利用すれば改善される気がします)。
- 最終的なCalibrationとして、WORLDEYEとRealSenseの位置調整がなかなか難しかった。
などが有りましたが、(PCを除けば)予算3万円程度で実現することが出来ました。もし、後に続く方がいらっしゃいましたら、本記事のノウハウを活用していただけると嬉しい限りです。
明日 12 日目の執筆担当は @urahiroshi さんの「Chrome DevToolsを用いたメルカリWebのパフォーマンス計測」です。引き続きお楽しみください〜