VRならではのUI設計・実装や、視線やコントローラを使用した操作の実装を全3回に分けて紹介していきます。プログラミング初心者の方もOKです!
第1回の記事を読んでいない方は コチラ から始めてください!
第3回は コチラ です!
第2回の内容は「VRに適したUI設計〜前編〜」です。
UIの基礎
1-1. ステージを作成
新規のScene を File > New Scene で作成してそこで作業を行います。念のため、一旦 Scene を保存しましょう。
(Sceneの名前はご自由に)
Unity – マニュアル: シーン
Assets / VRSampleScenes / Models / Menu から MenuBG を Hierarchy(ヒエラルキー )にドラッグアンドドロップします。
Window > Lighting > Setting の項目を以下の画像のように調整します。
Unity – マニュアル: ライティングウィンドウ
今回は DirectionalLight は不要なので削除します。
すると以下の画像のようなステージが描画出来るのを確認してください。
Unity – マニュアル: ライトの種類
1-2. VRManagerを実装
VRのレンダリング品質をスクリプトで変更できるように実装します。
空のGameObject(Hierarchy で右クリックをして Create Empty を選択)を生成しましょう。その際、この 空のGameObject の名前を VR Manager に変更しましょう。
この VR Manager に Assets / VEStandardAssets / Scrits にある VRDeviceManager.cs をアタッチしましょう。
このスクリプト(VRDeviceManager.cs)は 環境によってグラフィックパフォーマンスを変更する 処理を作成するときに呼ぶことができます。
/// <summary> | |
/// VR device manager. | |
/// @copyright Copyright © 2017 Unity Technologies. | |
/// @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0 | |
/// </summary> | |
using UnityEngine; | |
using UnityEngine.VR; | |
using System.Collections; | |
namespace VRStandardAssets.Utils { | |
//レンダリングパフォーマンスを調整するためのスクリプト | |
public class VRDeviceManager : MonoBehaviour { | |
[SerializeField] private float m_RenderScale = 1.4f; | |
private static VRDeviceManager s_Instance; | |
public static VRDeviceManager Instance { | |
get { | |
if (s_Instance == null) { | |
s_Instance = FindObjectOfType<VRDeviceManager> (); | |
//新しいシーンを読み込んでもオブジェクトが自動で破壊されないように登録 | |
DontDestroyOnLoad (s_Instance.gameObject); | |
} | |
return s_Instance; | |
} | |
} | |
private void Awake () { | |
if (s_Instance == null) { | |
s_Instance = this; | |
DontDestroyOnLoad (this); | |
} | |
else if (this != s_Instance) { | |
Destroy (gameObject); | |
} | |
} | |
} | |
} |
※補足1:なぜレンダリング品質変更するの?
Q. 画質を下げることでパフォーマンスを上げられるからです。
デフォルト値は1です。
1以下になると画質が下がり、パフォーマンスが向上します。
1以上にすると画質が上がり、パフォーマンスが低下します。
基本0.5〜1.5の間を調整するのが経験上望ましい結果が出やすいです。
/// <summary> | |
/// ExampleRenderScale. | |
/// @copyright Copyright © 2017 Unity Technologies. | |
/// @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0 | |
/// </summary> | |
using UnityEngine; | |
using UnityEngine.VR; | |
namespace VRStandardAssets.Examples { | |
public class ExampleRenderScale : MonoBehaviour { | |
[SerializeField] private float m_RenderScale = 1.5f; | |
void Start () { | |
VRSettings.renderScale = m_RenderScale; | |
} | |
} | |
} |
Unity – スクリプトリファレンス: VR.VRSettings.renderScale
※補足2:なぜ自分自身をインスタンス化してるの?
Q. Singleton パターン というデザインパターンの1種です。
Singleton パターン を用いると、そのクラスのインスタンスが1つしか生成されないことを「保証」できます。
Singleton – Unify Community Wiki
1-3. 3D UIを作成
HUD型 ( ヘッドアップディスプレイ ) を作成します。
これを通してVRに適したUIの作成を学びましょう。
まず、Hierarchy にUIを格納するための 空のGameObject を作成して GUI と名付けます。
そして、この GameObject(GUI)の座標を初期化しておきましょう。(Transform の右上の歯車をクリックして Reset を選択するのが1番簡単です)
次に、GUI の直下に 新しい空のGameObject を作成して IntroUI と名付けます。そして、このGameObject ( IntroUI ) の座標を初期化しておきましょう。
その次に IntroUI の中に「メッセージを表示する部分」と「スライダー」を作成します。
まず IntroUI を右クリックして UI > Canvas を押下して Canvas を生成します。この Canvas の名前を IntroMessage に変更しましょう。
さらに、IntroMessage の Inspector(インスペクター)を表示して、Canvasn の RenderMode をWorldSpace に変更した後、下記の画像のように数値を変更してください。
ここで重要となる Canvasの中身 について解説します。
Unity – マニュアル: Canvas
① RectTransform
Canvas のサイズを設定する項目です。 この数値を調整してカメラの中にUI全体が含まれるように設定します。
VR空間上でのUIとカメラとの距離は2〜3m以上離すのをOculusでは推奨しています。
※今回は約”8m”離しています。
Unity – スクリプトリファレンス: RectTransform
② Canvas – RenderMode
UIの描画方法を変更します。WorldScale に変更すると Canvas を 3D空間上に配置することが出来ます。VR空間でUIを作成する際、基本的には3D空間上に配置し、カメラとUIに距離が出来るように設計します。
もしUIとの間に距離がないと目の前にUIが表示されてしまい、気持ち悪いUIになってしまいます。
(イメージとしてはメガネの上に大きなゴミがついた状態で見ているのと近い体験)
Unity – スクリプトリファレンス: RenderMode
③ CanvasScaler
Dynamic Pixels Per Unit という項目を編集します。 この設定項目は、誤解を恐れずに言うならテキストの解像度を設定することが出来きます。デフォルト数値の場合、キャンバスサイズに対して解像度が非常に低いのでこれを上げることで文字を鮮明に表示可能です。しかし、解像度を上げることで処理の負荷も上がるので必要十分な数値にしましょう。
※今回は1ユニット毎のピクセル数を”4″に設定しました。
Unity – マニュアル: Canvas Scaler
ここまで出来たら GUI / IntroGUI / IntroMessage の直下に移動し、Hierarchy で右クリックをして UI > Text を2つ追加します。1つは Title、もう1つは Body を名付けます。
以下の画像のように Title と Body を変更しましょう。
1-4. レイティクルを実装
レティクルとは「視線マーカー」のことです。
レティクルは視線がどこを向いているのか表すので、 HMDのトラッキングに追従する必要があります。
そのために、レイティクルは Main Camera の 子オブジェクト として設定する必要があります。Assets / VRSampleScenes / Prefabs に VRCameraUI という Prefab(プレハブ)があります。 これを MainCamera にドラッグアンドドロップします。
下記画像のように Gameビュー の中央に赤いマーカーが表示されていればOKです。
ついでに Main Camera の Transform – Positionを X=0, Y=1, Z=0 に変更しましょう。
視線を使った入力
2-1. はじめに
Assets / VRSampleScenes / Scripts / Utils にある Reticle.cs をMainCameraにアタッチします。
Reticle.cs は今回の内容では特に効果はありませんが、カメラの正面からみた物体の傾きに合わせてレティクルを回転させるときに使えます。
/// <summary> | |
/// Reticle. | |
/// @copyright Copyright © 2017 Unity Technologies. | |
/// @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0 | |
/// </summary> | |
using UnityEngine; | |
using UnityEngine.UI; | |
namespace VRStandardAssets.Utils { | |
public class Reticle : MonoBehaviour { | |
[SerializeField] private float m_DefaultDistance = 5f; | |
[SerializeField] private bool m_UseNormal; | |
[SerializeField] private Image m_Image; | |
[SerializeField] private Transform m_ReticleTransform; | |
[SerializeField] private Transform m_Camera; | |
private Vector3 m_OriginalScale; | |
private Quaternion m_OriginalRotation; | |
public bool UseNormal { | |
get { return m_UseNormal; } | |
set { m_UseNormal = value; } | |
} | |
public Transform ReticleTransform { get { return m_ReticleTransform; } } | |
private void Awake() { | |
// 元のレティクルの大きさを格納しておく. | |
m_OriginalScale = m_ReticleTransform.localScale; | |
m_OriginalRotation = m_ReticleTransform.localRotation; | |
} | |
public void Hide() { | |
m_Image.enabled = false; | |
} | |
public void Show() { | |
m_Image.enabled = true; | |
} | |
// VREyeRayCasterから何もヒット情報が返ってこないときに実行される処理. | |
// 初期に設定された位置情報アイコンの位置をリセットする. | |
public void SetPosition () { | |
m_ReticleTransform.position = m_Camera.position + m_Camera.forward * m_DefaultDistance; | |
m_ReticleTransform.localScale = m_OriginalScale * m_DefaultDistance; | |
m_ReticleTransform.localRotation = m_OriginalRotation; | |
} | |
// 何かに衝突したとき、レティクルの向きをヒットした物体を基準にするかしないかを選択. | |
public void SetPosition (RaycastHit hit) { | |
m_ReticleTransform.position = hit.point; | |
m_ReticleTransform.localScale = m_OriginalScale * hit.distance; | |
if (m_UseNormal) { | |
m_ReticleTransform.rotation = Quaternion.FromToRotation (Vector3.forward, hit.normal); | |
} else { | |
m_ReticleTransform.localRotation = m_OriginalRotation; | |
} | |
} | |
} | |
} |
そして、Reticle.csと同じ場所 ( Assets / VRSampleScenes / Scripts / Utils ) にある VRInput.cs も MainCamera にアタッチ。これはクリックを含めたVR上で入力処理を実装するために必要な機能が一通り揃ったスクリプトになります。さらに SelectionRadial.cs もアタッチします。これはもう一つのレティクルを制御するスクリプトです。
MainCamera にアタッチした3つのスクリプト ( Reticle.cs, VRInput.cs, SelectionRadial.cs ) は下記の画像のように設定してください。
次にボタン部分を実装します。今回はボタンの実装がメインではないので 詳細を省きます。 Assets / VRSampleScenes / Prefabs に InstructionsSelectionSlider という Prefab があります。 これを IntroGUI にドラッグアンドドロップして InstructionsSelectionSlider の RectTransform – PosY = 0 にします。これで以下の画像のようになればOKです。
2-2. 視線からビームを出す(当たり判定 )
視線によってUIを選択するための当たり判定処理を作成します。
実際に視線が当たったらアクションを起こすために、まず 入力を検知したらアクションを行うための雛形を用意します。
Assets / VRStandardAssets / Scritps の中に VRInteractiveItem.cs という イベントハンドラ の塊のスクリプトがあります。
UIを操作中、視線をが重なった GameObject を「赤く光らせる」「アニメーションを開始」など何かしらの視覚効果を入れたい場合に VRInteractiveItem.cs を利用することで任意の コルーチン処理 を呼び出すことが可能です。
VRInteractiveItem.cs は既に InstructionsSelectionSlider にアタッチされています。これを後述する当たり判定 スクリプトのトリガーが発火した際に呼び出すことで、様々なアクションを起こすことが可能です。
さて、いよいよ今回のコアとなる当たり判定処理に入ります。
Assets / VRStandardAssets / Scripts の中に VREyeRayCaster.cs というファイルがあります。それを MainCamera にアタッチしてください。
そして Inspector上 で VREyeRayCaster.cs の各項目を下記の画像のように設定してください。
さっそく VREyeRayCaster.cs の中身を見てみます。 まず冒頭に
1 | public event Action<RaycastHit> OnRaycasthit; |
で、デリゲート処理を作成しています。
※ デリゲートはデザインパターンの1種です。詳細は wiki で確認してください。
1 2 | private VRInteractiveItem m_CurrentInteractible; //The current interactive item private VRInteractiveItem m_LastInteractible; |
そして先ほど説明した VRInteractiveItem の変数を作成しています。 なぜ2個作成しているのでしょうか?確認してみましょう。
1 2 3 | private void Update(){ EyeRaycast(); } |
Update内で EyeRayCast() が毎フレーム毎に呼び出しています。 EyeRayCast() の中身を見てましょう。
1 2 | Ray ray = new Ray(m_Camera.position, m_Camera.forward); RaycastHit hit; |
この処理はカメラの座標からカメラの前方方向(すなわち画面の真ん中)に向けてRay を作成しています。
そして Ray がヒットしたオブジェクトを格納するための RaycastHit を用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | if (Physics.Raycast(ray, out hit, m_RayLength, ~m_ExclusionLayers)) { VRInteractiveItem interactible = hit.collider.GetComponent<VRInteractiveItem>(); //HitアイテムからVRInteractiveItemのスクリプトを取得 m_CurrentInteractible = interactible; if (interactible && interactible != m_LastInteractible) interactible.Over(); if (interactible != m_LastInteractible) DeactiveLastInteractible(); m_LastInteractible = interactible; // ヒットしたオブジェクト情報をレティクルに渡す. if (m_Reticle) m_Reticle.SetPosition(hit); //登録されたActionを実行 if (OnRaycasthit != null ) OnRaycasthit(hit); } } |
あとは Physics.Raycast で Ray を飛ばし、当たったオブジェクトからVRInteractiveItemを取得しています。 これではじめて当たったオブジェクトに対して VRInteractiveItem.Over() を呼び出し、前回のフレームで既にそのオブジェクトを呼び出していたらなら実行しない。 そのために VRInteractiveItem の変数を2個用意してあるのです。
これで当たり判定処理の雛形が出来ました。
あとはこのハンドラーをよしなに使うことで様々なオブジェクトに視線の処理を渡すことが出来るようになります。
2-3. トリガーに視線を合わせた状態でクリックするとゲージが溜まる処理の実装
前項で視線を検知するための処理を作成しました。
では実際にこれを使ったインタラクションな処理を実装してみましょう。
この章の目的は視線が「Look Here!」に重なった時、カードボードやトリガーをクリックするとゲージがたまる処理を実装してみたいと思います。
Assets / VRSampleScenes / Scripts / Utils にある SelectionSlider.cs が今回の目的のスクリプトです。
SelectionSlider.cs は InstructionsSelectionSlider にアタッチされているので確認しましょう。
そして SelectionSlider の設定項目 VRInput に MainCamera をドロップアンドドラッグをしてください。
各行の役割はコメントを参照してください。
/// <summary> | |
/// SelectionSlider. | |
/// @copyright Copyright © 2017 Unity Technologies. | |
/// @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0 | |
/// </summary> | |
using System; | |
using UnityEngine; | |
using System.Collections; | |
using UnityEngine.UI; | |
namespace VRStandardAssets.Utils { | |
public class SelectionSlider : MonoBehaviour { | |
// Barのゲージが満タンになったら呼ばれる処理. | |
public event Action OnBarFilled; | |
// Barが満タンになるまでの時間. | |
[SerializeField] private float m_Duration = 2f; | |
[SerializeField] private AudioSource m_Audio; | |
// バーを見たときに再生される音. | |
[SerializeField] private AudioClip m_OnOverClip; | |
// バーが満タンになったときに再生する音. | |
[SerializeField] private AudioClip m_OnFilledClip; | |
[SerializeField] private Slider m_Slider; | |
// Barが満タンになったときに通知するためのクラス. | |
[SerializeField] private VRInteractiveItem m_InteractiveItem; | |
// ボタンなどの入力を検知するためのクラス. | |
[SerializeField] private VRInput m_VRInput; | |
[SerializeField] private GameObject m_BarCanvas; | |
[SerializeField] private Renderer m_Renderer; | |
[SerializeField] private SelectionRadial m_SelectionRadial; | |
// Canvasグループの切り替えの際、画面がフェードアウトをするためのクラス. | |
[SerializeField] private UIFader m_UIFader; | |
[SerializeField] private Collider m_Collider; | |
[SerializeField] private bool m_DisableOnBarFill; | |
[SerializeField] private bool m_DisappearOnBarFill; | |
private bool m_BarFilled; | |
private bool m_GazeOver; | |
private float m_Timer; | |
private Coroutine m_FillBarRoutine; | |
private const string k_SliderMaterialPropertyName = "_SliderValue"; | |
private void OnEnable () { | |
m_VRInput.OnDown += HandleDown; | |
m_VRInput.OnUp += HandleUp; | |
m_InteractiveItem.OnOver += HandleOver; | |
m_InteractiveItem.OnOut += HandleOut; | |
} | |
private void OnDisable () { | |
m_VRInput.OnDown -= HandleDown; | |
m_VRInput.OnUp -= HandleUp; | |
m_InteractiveItem.OnOver -= HandleOver; | |
m_InteractiveItem.OnOut -= HandleOut; | |
} | |
private void Update () { | |
if (m_UIFader) { | |
m_Collider.enabled = m_UIFader.Visible; | |
} | |
} | |
// IntroManager上で実行され, UI遷移を管理. | |
public IEnumerator WaitForBarToFill () { | |
if (m_BarCanvas && m_DisappearOnBarFill) { | |
m_BarCanvas.SetActive(true); | |
} | |
// 初期化. | |
m_BarFilled = false; | |
m_Timer = 0f; | |
SetSliderValue (0f); | |
// バーのゲージが満タンになるまでループさせる. | |
while (!m_BarFilled) { | |
yield return null; | |
} | |
// ゲージが満タンになったら一度描画をオフにする. | |
// これをしないと次のBarの当たり判定が取得出来ない. | |
if (m_BarCanvas && m_DisappearOnBarFill) { | |
m_BarCanvas.SetActive(false); | |
} | |
} | |
private IEnumerator FillBar () { | |
// タイマーのリセット. | |
m_Timer = 0f; | |
float fillTime = m_SelectionRadial != null ? m_SelectionRadial.SelectionDuration : m_Duration; | |
// 満タンまでの設定した時間までループ処理. | |
while (m_Timer < fillTime) { | |
// フレーム毎の時間を加算. | |
m_Timer += Time.deltaTime; | |
// Barのゲージの値を入力. | |
SetSliderValue(m_Timer / fillTime); | |
// 次のフレーム処理まで待つ. | |
yield return null; | |
// 次のフレーム時にまだ視線がBarと重なっていたらループを継続 | |
if (m_GazeOver) { | |
continue; | |
} | |
// 視線が途切れてしまったら下記内容を実行し値をリセット後, Break. | |
m_Timer = 0f; | |
SetSliderValue (0f); | |
yield break; | |
} | |
// Barが満タンになったのでtrueを返す | |
m_BarFilled = true; | |
// Barが満タンになったのでAction関数を実行 | |
if (OnBarFilled != null) { | |
OnBarFilled (); | |
} | |
// 満タンになったことを知らせるサウンドを再生 | |
m_Audio.clip = m_OnFilledClip; | |
m_Audio.Play(); | |
if (m_DisableOnBarFill) { | |
enabled = false; | |
} | |
} | |
private void SetSliderValue (float sliderValue) { | |
if (m_Slider) { | |
m_Slider.value = sliderValue; | |
} | |
if (m_Renderer) { | |
m_Renderer.sharedMaterial.SetFloat (k_SliderMaterialPropertyName, sliderValue); | |
} | |
} | |
private void HandleDown () { | |
if (m_GazeOver) { | |
m_FillBarRoutine = StartCoroutine(FillBar()); | |
} | |
} | |
private void HandleUp () { | |
if (m_FillBarRoutine != null) { | |
StopCoroutine (m_FillBarRoutine); | |
} | |
m_Timer = 0f; | |
SetSliderValue(0f); | |
} | |
// ユーザの視線がBarに重なっている間の処理 | |
private void HandleOver () { | |
m_GazeOver = true; | |
m_Audio.clip = m_OnOverClip; | |
m_Audio.Play(); | |
} | |
// 視線がBarから外れてるときの処理 | |
private void HandleOut () | |
{ | |
m_GazeOver = false; | |
if (m_FillBarRoutine != null) { | |
StopCoroutine(m_FillBarRoutine); | |
} | |
m_Timer = 0f; | |
SetSliderValue(0f); | |
} | |
} | |
} |
2-4. テスト
これで視線を使った入力処理の一連の処理が出来ました。
最後に新規のスクリプト(UIManager.cs )を作成し、そこでコルーチンを実行します。
こうすることで、遷移処理が管理しやすくなります。
using UnityEngine; | |
using System.Collections; | |
using VRStandardAssets.Utils; | |
public class UIManager : MonoBehaviour { | |
[SerializeField] private Reticle m_Reticle; | |
[SerializeField] private SelectionRadial m_Radial; | |
[SerializeField] private UIFader m_HowToUseFader; | |
[SerializeField] private SelectionSlider m_HowToUseSlider; | |
private IEnumerator Start () { | |
m_Reticle.Show (); | |
m_Radial.Hide (); | |
yield return StartCoroutine (m_HowToUseFader.InteruptAndFadeIn ()); | |
yield return StartCoroutine (m_HowToUseSlider.WaitForBarToFill ()); | |
yield return StartCoroutine (m_HowToUseFader.InteruptAndFadeOut ()); | |
Debug.Log("Clear"); | |
} | |
} |
上のスクリプトを参考に UIManager.cs を作成し、Hierarchy で UIManager と名付けた 新しい空のGameObject に UIManager.cs をアタッチしましょう。
さて、ようやくここで一度実行をします。
ただし、動作確認はモバイル端末上で行なってください。
1. iOS or Android向けにビルドをしてアプリを起動 ※ まだ用意できていないです… 「unity android ios ビルド」でググってくださいm(_ _)m
2. Unity Remote5 で動作確認(簡単)記事はこちら
※ 「Unity Remote 5」をHMDに装着した状態で利用する場合
下記のコードを完コピした CameraController.cs を新規に作成して MainCamera にアタッチしましょう。CameraController.cs によってモバイル端末の傾きを再生中のシーンに反映することができます。さらに、Editor > Project Settings > Player > PlayerSettings (Settings for PC, Mac & Linux Standalone) > Other Settings > Rendering:Virtual Reailty Supported を “ON”かつ、Virtual Reality SDKs を Split Stereo Display (non head-mounted) に設定してください。すると、再生中のシーンが2分割されます。
using UnityEngine; | |
public class InputMobileGyro : MonoBehaviour { | |
public bool isUnityRemote; | |
private Quaternion gyro; | |
void Start () { | |
if (isUnityRemote) { | |
Input.gyro.enabled = true; | |
} | |
} | |
void Update () { | |
if (Input.gyro.enabled) { | |
gyro = Input.gyro.attitude; | |
transform.localRotation = Quaternion.Euler(90, 0, 0) * (new Quaternion(-gyro.x,-gyro.y, gyro.z, gyro.w)); | |
} | |
} | |
} |
赤い点が 「Look Here!」に重なった状態で 画面をタップ し続け「ピロリンッ♪」と音が鳴ればOKです!
※ 画像なので音は出ていませんが、実際は音が鳴ります。
第2回の内容は以上となります。次回はこの続きとなります。
第3回の更新日は9/20(水)予定です。
【10/7(土) スタート!】VRプロフェッショナルアカデミー第2期募集中!
VRアカデミーに入学して本格的にVR開発を学んでみませんか?
☆オススメ☆ VRエンジニアコース【エキスパート】
高度なプログラミング技術を持つエンジニアが、
VRコンテンツをつくるための知識と技術を習得するためのコースです。
VRを構成する基本要素のコントロールシステムやUIをはじめ、SpatialSoundやShaderなど、コンテンツを開発する上でニーズの高い技術もまとめて学習することで、様々なVRコンテンツ開発に対応出来るエンジニアを目指します。
更にUnityでのAR開発についても代表的なツールを用いた開発方法を学ぶことが出来ます。
VR Professional Academy | 日本初のVR/AR専門の学校 – VRエンジニアコース【エキスパート】
詳しく説明を聞きたい方は…
無料個別説明会 in 中目黒VRプライベートサロン ☆参加者限定の割引特典あり☆ 申し込みはコチラ