MRが楽しい

MRやVRについて学習したことを書き残す

アプリ起動/終了時に呼び出されるMonoBehaviourの関数の順序を確認する

本日は Unity の調査枠です。
アプリ起動/終了時に呼び出されるMonoBehaviourの関数の順序を確認することがあったので記事にします。

アプリ起動/終了時に呼び出される関数

MonoBehaviourを継承したスクリプトは特定のタイミングで特定の関数がキックされます。
docs.unity3d.com

今回はアプリ起動/終了時に呼び出される以下の関数を一つのスクリプトにまとめ、その呼び出し順序を確認しました。
・Awake()
・Start()
・OnEnable()
・OnDisable()
・OnApplicationFocus(bool)
・OnApplicationPause(bool)
・OnDestroy()
・OnApplicationQuit()

確認用スクリプトを作成する

呼び出しタイミングでデバッグログを出力する以下のスクリプトを作成しました。
・CheckScript.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CheckScript : MonoBehaviour {

    /// <summary>
    /// スクリプトのインスタンスがロードされたときに呼び出されます
    /// </summary>
    private void Awake()
    {
        Debug.Log("Awake");
    }

    /// <summary>
    /// 最初のUpdateメソッドが呼び出される前にスクリプトが有効になると
    /// フレーム上でStartが呼び出されます。
    /// </summary>
    private void Start()
    {
        Debug.Log("Start");
    }

    /// <summary>
    /// オブジェクトが有効/アクティブになったときに呼び出されます
    /// </summary>
    private void OnEnable()
    {
        Debug.Log("OnEnable");
    }

    /// <summary>
    /// オブジェクトが無効/非アクティブになったときに呼び出されます
    /// </summary>
    private void OnDisable()
    {
        Debug.Log("OnDisable");
    }

    /// <summary>
    /// プレイヤーがフォーカスを取得、または、失ったときに呼び出されます
    /// </summary>
    /// <param name="focus">フォーカス有無</param>
    private void OnApplicationFocus(bool focus)
    {
        Debug.Log("OnApplicationFocus : " + focus);
    }

    /// <summary>
    /// プレイヤーが一時停止したときに呼び出されます
    /// </summary>
    /// <param name="pause">一時停止有無</param>
    private void OnApplicationPause(bool pause)
    {
        Debug.Log("OnApplicationPause : " + pause);
    }

    /// <summary>
    /// オブジェクトが破棄されたときに呼び出されます
    /// </summary>
    private void OnDestroy()
    {
        Debug.Log("OnDestroy");
    }

    /// <summary>
    /// アプリケーションが終了する前に呼び出されます
    /// </summary>
    private void OnApplicationQuit()
    {
        Debug.Log("OnApplicationQuit");
    }
}

Empty オブジェクトを作成し、本スクリプトをアタッチしました。
f:id:bluebirdofoz:20190503230650j:plain

UnityEditorでの確認

それぞれ[再生]ボタンをクリックして、アプリを起動したときと、アプリを終了したときのデバッグログを確認しました。
・アプリ起動時
f:id:bluebirdofoz:20190503230709j:plain

Awake()
↓
OnEnable()
↓
OnApplicationPause(false)
↓
OnApplicationFocus(true)
↓
Start()

・アプリ終了時
f:id:bluebirdofoz:20190503230721j:plain

OnApplicationQuit()
↓
OnDisable()
↓
OnDestroy()

シングルパスインスタンシングレンダリングを使って左右のディスプレイで異なる画像を出力する

本日は HoloLens の技術調査枠です。
シングルパスインスタンシングレンダリングを使って左右のディスプレイで異なる画像を出力してみます。

以下のページを参考に実施します。
docs.unity3d.com

マルチパスとシングルパスの違いについて

本記事ではマルチパスとシングルパスの仕組みの違いについては述べません。未だに理解が至っていないため。
以下の記事や公式ページが参考になります。
tips.hecomi.com
docs.unity3d.com
docs.unity3d.com

プロジェクトとシーンの準備

以下の記事を元にHoloLens(WindowsMR)プロジェクトを作成します。
bluebirdofoz.hatenablog.com

今回は Panel に配置した画像ファイルで動作を確認します。
このため、Sphere オブジェクトを Plane オブジェクトに差し替えています。
f:id:bluebirdofoz:20190502234255j:plain

レンダリング設定の切り替え

メニューから Edit -> ProjectSettings -> Player を選択し、PlayerSettings の Inspector ビューを開きます。
f:id:bluebirdofoz:20190502234310j:plain

[XR Settings]パネルを開き、[Stereo Rendering Method]を[Single Pass Instanced (Preview]に変更します。
これでシングルパスインスタンシングレンダリングが有効になります。
f:id:bluebirdofoz:20190502234323j:plain

カスタムシェーダの作成

インスタンシングを利用するカスタムシェーダを作成します。
Assets フォルダで右クリックから Create -> Shader -> Unlit Shader で新規シェーダを作成します。
f:id:bluebirdofoz:20190502234335j:plain

参考ページのサンプルシェーダを元に、テクスチャの右半分と左半分を右目と左目で別々に描画するシェーダを作成しました。
・UnitOffsetShader.shader

Shader "Unlit/UnitOffsetShader"
{
  Properties
  {
    _MainTex ("Texture", 2D) = "white" {}
  }
  SubShader
  {
    Tags { "RenderType"="Opaque" }
    LOD 100

    Pass
    {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
      // make fog work
      #pragma multi_compile_fog
      
      #include "UnityCG.cginc"

      struct appdata
      {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;

        // シングルパスインスタンシング対応のため挿入
        UNITY_VERTEX_INPUT_INSTANCE_ID
      };

      struct v2f
      {
        float2 uv : TEXCOORD0;
        UNITY_FOG_COORDS(1)
        float4 vertex : SV_POSITION;

        // シングルパスインスタンシング対応のため挿入
        UNITY_VERTEX_OUTPUT_STEREO
      };

      sampler2D _MainTex;
      float4 _MainTex_ST;
      
      v2f vert (appdata v)
      {
        v2f o;

        // シングルパスインスタンシング対応のため挿入
        UNITY_SETUP_INSTANCE_ID(v);
        UNITY_INITIALIZE_OUTPUT(v2f, o);
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
        // 左目と右目でテクスチャの左右の半分を分けて描画する
        o.uv.x *= .5;
        // unity_StereoEyeIndex の値は
        // 左目のレンダリングは 0、右目のレンダリングは 1
        if (unity_StereoEyeIndex != 0)
        {
          o.uv.x += .5;
        }
        UNITY_TRANSFER_FOG(o,o.vertex);
        return o;
      }
      
      fixed4 frag (v2f i) : SV_Target
      {
        // シングルパスインスタンシング対応のため挿入
        UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);

        fixed4 col = tex2D(_MainTex, i.uv);
        // apply fog
        UNITY_APPLY_FOG(i.fogCoord, col);
        return col;
      }
      ENDCG
    }
  }
}

作成したシェーダを参照するマテリアルを作成します。
Assets フォルダで右クリックから Create -> Material で新規マテリアルを作成します。
f:id:bluebirdofoz:20190502234349j:plain

作成した新規マテリアルのシェーダとして先ほど作成した Unlit/UnitOffsetShader を設定します。
f:id:bluebirdofoz:20190502234403j:plain

最後に描画するテクスチャを設定します。
左右で異なる部分が描画されていることが分かる横方向のグラデーション画像を取り込みました。
[Texture Type]を[Sprite (2D and UI)]に変更して[Apply]ボタンをクリックします。
f:id:bluebirdofoz:20190502234415j:plain

マテリアルにテクスチャを設定して、マテリアルの設定は完了です。
f:id:bluebirdofoz:20190502234428j:plain

完成したマテリアルを Plane オブジェクトに適用します。
f:id:bluebirdofoz:20190502234439j:plain

後は HoloLens 向けにプロジェクトをビルドしてインストールします。
UnityプロジェクトのビルドとHoloLensへのインストール手順については以下を参照してください。
bluebirdofoz.hatenablog.com

HoloLensでの動作確認

HoloLens 上で動作確認を行います。
成功していれば右目と左目で異なる画像が描画されています。
キャプチャを行うと、右目の映像がキャプチャされます。
f:id:bluebirdofoz:20190502234458j:plain

HoloLensで両手の位置を検出して立方体を作成する

本日は HoloLens の技術調査枠です。
HoloLensで手の位置を検出する手順を記事にします。
前回記事の続きです。
bluebirdofoz.hatenablog.com

両手の位置を元に立方体を生成する

両手の検出してその位置情報を元に立方体を生成するようスクリプトを修正してみました。
・HandPointGetter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using HoloToolkit.Unity.InputModule;

public class HandPointGetter : MonoBehaviour, ISourcePositionHandler
{
    /// <summary>
    /// オブジェクト有効時の実行処理
    /// </summary>
    private void OnEnable()
    {
        // 常にハンドトラッキングのイベントを取得する
        InputManager.Instance.PushFallbackInputHandler(this.gameObject);
    }

    /// <summary>
    /// オブジェクト無効時の実行処理
    /// </summary>
    private void OnDisable()
    {
        if (InputManager.Instance)
        {
            // イベントの取得を無効化する
            InputManager.Instance.PopFallbackInputHandler();
        }
    }

    /// <summary>
    /// 定期実行処理
    /// </summary>
    private void Update()
    {
        // 追加対象のハンドIDがあれば追加処理を実行
        if (createList.Count > 0)
        {
            for (int loop = 0; loop < createList.Count; loop++)
            {
                uint id = createList[loop];
                if (handStates.ContainsKey(id))
                {
                    // Sphereオブジェクトを生成
                    GameObject posObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                    posObject.transform.localScale = new Vector3(0.05f, 0.05f, 0.05f);
                    // オブジェクトの参照をハンドトラッキング情報に追加
                    handStates[id].Object = posObject;
                }
            }
            createList.Clear();
        }
        // 削除対象のハンドIDがあれば削除処理を実行
        if (lostList.Count > 0)
        {
            for (int loop = 0; loop < lostList.Count; loop++)
            {
                uint id = lostList[loop];
                if (handStates.ContainsKey(id))
                {
                    // Sphereオブジェクトを破棄
                    Destroy(handStates[id].Object);
                    handStates.Remove(id);
                }
            }
            lostList.Clear();
        }
        float currentKey01Time = 0.0f;
        float currentKey02Time = 0.0f;
        // ロストが正常に検出されない場合があるため時間経過でも削除を行う
        foreach (KeyValuePair<uint, HandState> pair in handStates)
        {
            // 最終検出からの経過時間を取得
            float lostTime = Time.time - pair.Value.DetectTime;
            if (lostTime > deleteTime)
            {
                uint id = pair.Key;
                // Sphereオブジェクトを破棄
                Destroy(handStates[id].Object);
                handStates.Remove(id);
            }
            else
            {
                // より最新のキーを2つ取得する
                float checkTime = pair.Value.DetectTime;
                if (checkTime > currentKey02Time)
                {
                    if (checkTime > currentKey01Time)
                    {
                        currentkey01 = pair.Key;
                        currentKey01Time = checkTime;
                    }
                    else
                    {
                        currentkey02 = pair.Key;
                        currentKey02Time = checkTime;
                    }
                }
            }
        }
        // 最新のハンドステータスが2つあれば立方体を生成する
        if (handStates.ContainsKey(currentkey01) &&
            handStates.ContainsKey(currentkey02))
        {
            if (cubeObject == null)
            {
                // Cubeオブジェクトを生成
                cubeObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
            }
            // 手の位置を元にCubeの位置と傾き、大きさを変更
            Vector3 posOne = handStates[currentkey01].Position;
            Vector3 posTwo = handStates[currentkey02].Position;
            CalculateCube(posOne, posTwo);
        }
        else
        {
            // Sphereオブジェクトを破棄
            Destroy(cubeObject);
        }
    }

    private bool checkflg = false;

    /// <summary>
    /// 立方体オブジェクト参照
    /// </summary>
    private GameObject cubeObject = null;

    // 追加ハンドID
    private List<uint> createList = new List<uint>();
    // 削除ハンドID
    private List<uint> lostList = new List<uint>();
    // 自動削除までの待機時間(秒)
    private float deleteTime = 3.0f;

    /// <summary>
    /// 手毎のハンドトラッキング情報
    /// </summary>
    private Dictionary<uint, HandState> handStates = new Dictionary<uint, HandState>();
    private class HandState
    {
        // 位置情報
        public Vector3 Position { get; set; }
        // オブジェクト参照
        public GameObject Object;
        // 最終検出時刻
        public float DetectTime;
    }

    /// <summary>
    /// ハンドトラッキング情報の取得
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    private HandState GetHandState(uint id)
    {
        if (handStates.ContainsKey(id))
        {
            // 既に情報がある場合は返却
            return handStates[id];
        }
        else
        {
            // 未検出のハンドIDならば情報を作成
            Debug.Log("GetHandState make: id = " + id);
            HandState handState = new HandState();
            // 更新時刻を登録
            handState.DetectTime = Time.time;
            // ステータスをリスト追加
            handStates.Add(id, handState);
            // 追加リストに登録
            createList.Add(id);
            return handState;
        }
    }

    /// <summary>
    /// ハンド検出イベント
    /// </summary>
    /// <param name="eventData"></param>
    public void OnPositionChanged(SourcePositionEventData eventData)
    {
        HandState getHandState = GetHandState(eventData.SourceId);
        getHandState.Position = eventData.GripPosition;
        if (getHandState.Object != null)
        {
            // ハンドトラッキング情報の位置情報を更新
            getHandState.Position = eventData.GripPosition;
            // 更新時刻を更新
            getHandState.DetectTime = Time.time;
        }
    }

    /// <summary>
    /// ハンドロストイベント
    /// </summary>
    /// <param name="eventData"></param>
    public void OnSourceLost(SourceStateEventData eventData)
    {
        if (handStates.ContainsKey(eventData.SourceId))
        {
            // 既にハンドIDが登録されていれば削除リストに登録
            lostList.Add(eventData.SourceId);
        }
    }

    /// <summary>
    /// 頂点に合わせて立方体を変形
    /// </summary>
    /// <param name="posOne"></param>
    /// <param name="posTwo"></param>
    public void CalculateCube(Vector3 posOne, Vector3 posTwo)
    {
        // 2つの頂点の中央に位置を修正
        cubeObject.transform.localPosition = Vector3.Lerp(posOne, posTwo, 0.5f);
        // 頂点の方向を向くように傾きを修正
        cubeObject.transform.LookAt(posOne);
        // 2つの頂点の距離でサイズを調整
        float size = Vector3.Distance(posOne, posTwo);
        cubeObject.transform.localScale = new Vector3(size, size, size);
    }
}

f:id:bluebirdofoz:20190501232446j:plain

前回記事と同様に、アプリケーションをビルドして HoloLens 上で動作確認してみました。
f:id:bluebirdofoz:20190501232345g:plain

オブジェクトの直感的なトランスフォーム調整として良いかなと思いましたが。
よくよく考えると[Two Hand Manipulatable]でのつまみ操作と理屈は同じです。
bluebirdofoz.hatenablog.com

HoloLensで手の位置を検出する

本日は HoloLens の技術調査枠です。
HoloLensで手の位置を検出する手順を記事にします。
f:id:bluebirdofoz:20190430220049j:plain

プロジェクトとシーンの準備

以下の記事を元にHoloLens(WindowsMR)プロジェクトを作成します。
bluebirdofoz.hatenablog.com

2019/4/30現在、MRTK 2017 の最新バージョンは 2017.4.3.0 です。
f:id:bluebirdofoz:20190430220036j:plain

ハンドトラッキングイベント

手の位置を検出するには ISourcePositionHandler を利用します。
OnPositionChanged 関数で検出した手のIDとPosition情報が取得できます。

HoloLensでは人差し指を立てた状態で手を検出できます。
また、同時に複数の検出が可能です。トラッキングIDを識別することで複数の手の位置を追跡できます。

実際に検出位置を確認するため、手の位置にSphereオブジェクトを表示するスクリプトを作成しました。
・HandPointGetter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using HoloToolkit.Unity.InputModule;

public class HandPointGetter : MonoBehaviour, ISourcePositionHandler
{
    /// <summary>
    /// オブジェクト有効時の実行処理
    /// </summary>
    private void OnEnable()
    {
        // 常にハンドトラッキングのイベントを取得する
        InputManager.Instance.PushFallbackInputHandler(this.gameObject);
    }

    /// <summary>
    /// オブジェクト無効時の実行処理
    /// </summary>
    private void OnDisable()
    {
        if (InputManager.Instance)
        {
            // イベントの取得を無効化する
            InputManager.Instance.PopFallbackInputHandler();
        }
    }

    /// <summary>
    /// 定期実行処理
    /// </summary>
    private void Update()
    {
        // 追加対象のハンドIDがあれば追加処理を実行
        if (createList.Count > 0)
        {
            for (int loop = 0; loop < createList.Count; loop++)
            {
                uint id = createList[loop];
                if (handStates.ContainsKey(id))
                {
                    // Sphereオブジェクトを生成
                    GameObject posObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                    posObject.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
                    // オブジェクトの参照をハンドトラッキング情報に追加
                    handStates[id].Object = posObject;
                }
            }
            createList.Clear();
        }
        // 削除対象のハンドIDがあれば削除処理を実行
        if (lostList.Count > 0)
        {
            for (int loop = 0; loop < lostList.Count; loop++)
            {
                uint id = lostList[loop];
                if (handStates.ContainsKey(id))
                {
                    // Sphereオブジェクトを破棄
                    Destroy(handStates[id].Object);
                    handStates.Remove(id);
                }
            }
            lostList.Clear();
        }
        // ロストが正常に検出されない場合があるため時間経過でも削除を行う
        foreach (KeyValuePair<uint, HandState> pair in handStates)
        {
            float lostTime = Time.time - pair.Value.DetectTime;
            if (lostTime > deleteTime)
            {
                uint id = pair.Key;
                // Sphereオブジェクトを破棄
                Destroy(handStates[id].Object);
                handStates.Remove(id);
            }
        }
    }

    // 追加ハンドID
    private List<uint> createList = new List<uint>();
    // 削除ハンドID
    private List<uint> lostList = new List<uint>();
    // 自動削除までの待機時間(秒)
    private float deleteTime = 3.0f;

    /// <summary>
    /// 手毎のハンドトラッキング情報
    /// </summary>
    private Dictionary<uint, HandState> handStates = new Dictionary<uint, HandState>();
    private class HandState
    {
        // 位置情報
        public Vector3 Position { get; set; }
        // オブジェクト参照
        public GameObject Object;
        // 最終検出時刻
        public float DetectTime;
    }

    /// <summary>
    /// ハンドトラッキング情報の取得
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    private HandState GetHandState(uint id)
    {
        if (handStates.ContainsKey(id))
        {
            // 既に情報がある場合は返却
            return handStates[id];
        }
        else
        {
            // 未検出のハンドIDならば情報を作成
            Debug.Log("GetHandState make: id = " + id);
            HandState handState = new HandState();
            // 更新時刻を登録
            handState.DetectTime = Time.time;
            // ステータスをリスト追加
            handStates.Add(id, handState);
            // 追加リストに登録
            createList.Add(id);
            return handState;
        }
    }

    /// <summary>
    /// ハンド検出イベント
    /// </summary>
    /// <param name="eventData"></param>
    public void OnPositionChanged(SourcePositionEventData eventData)
    {
        HandState getHandState = GetHandState(eventData.SourceId);
        getHandState.Position = eventData.GripPosition;
        if (getHandState.Object != null)
        {
            // ハンドトラッキング情報のオブジェクトの位置情報を更新
            getHandState.Object.transform.position = eventData.GripPosition;
            // 更新時刻を更新
            getHandState.DetectTime = Time.time;
        }
    }

    /// <summary>
    /// ハンドロストイベント
    /// </summary>
    /// <param name="eventData"></param>
    public void OnSourceLost(SourceStateEventData eventData)
    {
        if (handStates.ContainsKey(eventData.SourceId))
        {
            // 既にハンドIDが登録されていれば削除リストに登録
            lostList.Add(eventData.SourceId);
        }
    }
}

新規の EmptyObject を作成し、作成したスクリプトをアタッチします。
f:id:bluebirdofoz:20190430220112j:plain

手の位置にSphereオブジェクトを表示するため、ニアクリップの設定を短めにしておく必要があります。
MixedRealityCameraParent/MixedRealityCamera を開き、NearClip の設定を 0.2 に設定しました。
f:id:bluebirdofoz:20190430220127j:plain

後は HoloLens 向けにプロジェクトをビルドしてインストールします。
UnityプロジェクトのビルドとHoloLensへのインストール手順については以下を参照してください。
bluebirdofoz.hatenablog.com

HoloLensでの動作確認

HoloLens 上で動作確認を行います。
人差し指を立てて手を出すと、その位置にSphereオブジェクトが表示されます。
拳の中心部分辺りの位置が検出されていることが分かります。
f:id:bluebirdofoz:20190430220200j:plain

BlueTooth利用のサンプルプログラムを試す

本日は UWP アプリの調査枠です。
UWP アプリで BlueTooth を利用する方法を確認します。
f:id:bluebirdofoz:20190429221258j:plain

UWP アプリで様々な技術要素を試したい場合、GitHub にサンプルプログラムが公開されています。
docs.microsoft.com
github.com

今回はこの Windows-universal-samples の中から BluetoothAdvertisement を試してみます。
github.com

サンプルのダウンロード

Windows-universal-samples-master のサンプルを利用する場合、個々のサンプルを利用する場合でも全てのプロジェクトをダウンロードする必要があります。
また、現在の master ブランチは Windows 10 SDK のバージョンが 10.0.18362 のものになっています。
これは Windows 10 のビルドバージョンでは Update 1903 に当たります。
2019/04/29現在、通常 Windows 10 のビルドバージョンはまだ 1809 のものなので、ローカルのPCで動作確認を行いたい場合はブランチから古いバージョンを取得します。

[branches]からブランチを選択可能です。
f:id:bluebirdofoz:20190429221326j:plain

今回は PC にインストール済みの Windows SDK のバージョンに合わせ、[win10-1803]のブランチを選択しました。
これは Update 1803 のバージョンので Windows 10 SDK のバージョンが 10.0.17134 のものです。
f:id:bluebirdofoz:20190429221343j:plain

ブランチを切り替えた後、[Download ZIP]からサンプルプロジェクトを取得します。
f:id:bluebirdofoz:20190429221419j:plain

読み込み時のエラー解消(インストール)

以下のエラーが発生する場合は、実行中の PC に必要なバージョンの Windows 10 SDK がインストールされていません。
Visual Studio 更新プログラムが必要」
f:id:bluebirdofoz:20190429221458j:plain

以下から必要なバージョンの SDK を取得してインストールを行うと、エラーが解消されます。
developer.microsoft.com
f:id:bluebirdofoz:20190429221541j:plain

プロジェクトのビルドとインストール

ダウンロードした Windows-universal-samples-win10-1803.zip を展開します。
Samples\BluetoothAdvertisement\cs 配下の BluetoothAdvertisement.sln を起動します。
f:id:bluebirdofoz:20190429221609j:plain

プロジェクトを開いたらビルド設定を行います。
今回は[Release],[x86],[ローカルコンピュータ]を指定しました。
f:id:bluebirdofoz:20190429221619j:plain

メニューから デバッグ -> デバッグなしで開始 を実行します。
f:id:bluebirdofoz:20190429221632j:plain

ローカルPCにアプリケーションがインストールされ、自動で実行されます。
f:id:bluebirdofoz:20190429221653j:plain

アプリの動作確認

異なる PC に同じアプリケーションをインストールして動作確認を行います。
アプリには publisher(送信側)の機能と、watcher(受信側)の機能があります。
[Foreground publisher]のタブを開き、[Run]を実行すると、BlueTooth のアドバタイズが実行されます。
f:id:bluebirdofoz:20190429221713j:plain

その状態で、もう一方の PC で[Foreground watcher]のタブを開き、[Run]を実行します。
すると、送信側のアドバタイズを受信することができます。
f:id:bluebirdofoz:20190429221921j:plain

Blenderで編集したユニティちゃんをUnityに取り込む その2(Unityでの取り込み設定)

本日は Blender と Unity とユニティちゃんの調査枠です。
前回記事の続きです。
bluebirdofoz.hatenablog.com

Blender で読み込んだユニティちゃんを Unity に取り込む手順を記事にします。
今回は Unity での取り込み設定です。
f:id:bluebirdofoz:20190428081529j:plain

Unity での取り込み設定

最初に参照するテクスチャファイルを Unity のアセットフォルダにドラッグして取り込みます。
既に UnityChan_1_2_1.unitypackage を取り込み済みの Unity プロジェクトの場合、この手順は不要です。
f:id:bluebirdofoz:20190428081541j:plain

前回出力した FBX ファイルを Unity のアセットフォルダにドラッグして取り込みます。
f:id:bluebirdofoz:20190428081557j:plain

Humanoidリグの設定

ユニティちゃんのアニメーションなど Humanoid リグのアニメーションを共通利用できるようにします。
Inspector ビューの[Rig]タブを開き、[Animation Type]を[Humanoid]に変更します。
[Apply]ボタンをクリックして変更を反映します。
f:id:bluebirdofoz:20190428081611j:plain

自動で Humanoid リグとの対応付けが行われます。
しかし、編集前のユニティちゃんと同じリグ構成にするには幾つかの調整が必要です。
[Configure..]ボタンをクリックして[Humanoid]リグの手動調節を開きます。
f:id:bluebirdofoz:20190428081620j:plain

[Body]と[Head]の修正が必要です。
[Body]では[Spine]に[Character1_Spine1]を、[Chest]に[Character1_Spine2]を割り当て直します。
[UperChest]は[None]を設定します。
f:id:bluebirdofoz:20190428081633j:plain

次に[Head]を開きます。
[Left Eye]、[Right Eye]、[Jaw]の3つに[None]を設定します。
[Apply]ボタンを押して変更を反映し、[Done]ボタンで編集を終了します。
f:id:bluebirdofoz:20190428081647j:plain

これで Humanoid リグの設定は完了です。

マテリアルの設定

次にマテリアルの設定を行います。
元のユニティちゃんと同じシェーダを利用したいのであれば、マテリアルを再設定するのが手っ取り早いです。
ユニティちゃんのマテリアルは UnityChan_1_2_1.unitypackage を読み込んだ以下のアセットフォルダにあります。
・Assets/UnityChan/Models/Materials
f:id:bluebirdofoz:20190428081659j:plain

取り込んだ FBX ファイルの Inspector ビューから[Materials]タブを開きます。
[Remapped Materials]の設定で、同名のマテリアルを再設定し、[Apply]ボタンで反映します。
f:id:bluebirdofoz:20190428081709j:plain

これでマテリアルが再設定され、元のユニティちゃんと同じ見え方になりました。
f:id:bluebirdofoz:20190428081720j:plain

動作確認

これで設定は完了です。
FBX ファイルを Hierachy にドラッグして UnityEditor 上で動作確認してみます。
アニメーションファイルやスクリプトを設定すると、ユニティちゃんがアニメーションを行いました。
f:id:bluebirdofoz:20190428081730j:plain
ユニティちゃんのアニメーションを流用する際、Humanoid リグのアニメーションは流用できますが、表情変化などはブレンドシェイプの参照が切れているため反映されません。

Blenderで編集したユニティちゃんをUnityに取り込む その1(ボーンの修正)

本日は Blender と Unity とユニティちゃんの調査枠です。
前回記事の続きです。
bluebirdofoz.hatenablog.com

今回からは Blender で読み込んだユニティちゃんを Unity に取り込む手順を記事にします。
これらの手順でユニティちゃんを Blender で編集して Unity に再反映するといったことができます。
f:id:bluebirdofoz:20190427202355j:plain

Unityに取り込むと発生する問題

前回記事までに作成したユニティちゃんをそのまま FBX に出力して Unity に取り込みます。
すると、以下の問題が発生します。

ボーンの位置がズレる

画像だと分かり辛いかもしれませんが、取り込んだ際に胸部やスカートなどの位置が少しズレています。
f:id:bluebirdofoz:20190427202406j:plain

ユニティちゃんのアニメーションが正常に再生できない

Humanoid リグを反映した後、ユニティちゃんのアニメーションを試すと、髪やスカートが荒ぶります。
f:id:bluebirdofoz:20190427202418j:plain

Blenderでの修正作業

Blender側の編集作業でまずはこれらの問題に対処する必要があります。

ボーンの位置がズレる問題の修正

ボーンの位置がズレる問題はアーマチュアオブジェクトの[回転]と[拡大縮小]を適用した際の位置ズレが原因です。
例えば、胸部ボーンを確認すると[位置]情報が僅かにズレていることが分かります。
f:id:bluebirdofoz:20190427202433j:plain

[位置]情報を現在の位置で適用し、現在のポーズをデフォルトのポーズとすることで問題は解消できます。
まずはアーマチュアを選択し、[オブジェクトモード]に切り替えます。
メニューから オブジェクト -> 適用 -> 位置 を実行します。
f:id:bluebirdofoz:20190427202445j:plain

次にポーズオブジェクトを選択し、[ポーズモード]に切り替えます。
メニューから ポーズ -> 適用 -> デフォルトのポーズに適用 を実行します。
f:id:bluebirdofoz:20190427202455j:plain

現在のポーズのまま、ボーンの[位置]情報が[0.0]にリセットされます。
これでデフォルトのポーズで発生するズレを修正できました。
f:id:bluebirdofoz:20190427202516j:plain

アニメーションが正常に再生できない問題の修正

これはユニティちゃんのアニメーションを利用すると、同じボーン名のボーンが干渉することが原因です。
ユニティちゃんのアニメーションを利用する場合、Humanoid リグ以外のボーンは名前を変更する必要があります。
[J_***]という名前のボーンが対象です。
f:id:bluebirdofoz:20190427202526j:plain

これらのボーンについて全て名前の[J_]の部分を削除するなど、ボーン名を変更します。
ネストの下のものも確認して名前を修正します。
f:id:bluebirdofoz:20190427202537j:plain

念のため、[ポーズモード]でボーンを動かして関連付けに問題が発生していないか確認します。
f:id:bluebirdofoz:20190427202549j:plain

モデルをFBX形式でエクスポート

これで Blender での編集作業は完了です。
メニューから ファイル -> エクスポート -> FBX(.fbx) を選択します。
f:id:bluebirdofoz:20190427202559j:plain

出力対象に[アーマチュア]と[メッシュ]を選択して[FBXをエクスポート]を実行します。
f:id:bluebirdofoz:20190427202610j:plain

記事が長くなったので分けます。
次回は Unity の取り込み設定です。
bluebirdofoz.hatenablog.com