凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Oculus Avatar SDK を使って自分のアバターを Unity で利用する方法を調べてみた

はじめに

Oculus の Home アプリから以下のようにアバタを編集できます。

f:id:hecomi:20161224190715p:plain

このアバタは Oculus Avatar SDK を利用することで Unity 内でも利用することが出来ます。ただ、現状少し手順が多く大変です。というのも、このアバタ情報は Oculus のサーバ側に保存されており、そのデータを取ってくるのは Oculus Platform SDK という別の SDK になります。公式のドキュメントにも書かれていますが現状 Avatar SDK は Platform SDK からのユーザ情報(ID)を使ってアバタ情報を取ってくるような形になっていません。

本エントリでは、どうやれば自身のアバタが表示できるかを解説したいと思います。

なお、Oculus Rift のアプリ開発や Touch のハンドリングについては余り解説しないので、詳しくはフレームシンセシスの以下のエントリをご参照ください。

環境

  • Oculus Utilities for Unity 5 V1.10.0
  • Oculus Avatar SDK V1.10.1
  • Oculus Platform SDK v1.10.0

アバターの表示

Virtual Reality Supported

Unity の「Player Settings」から Virtual Reality Supported をチェックする必要があります。

.unitypackage のインポート

以下の Downloads ページから、「Oculus Utilities for Unity 5」と「Oculus Avatar SDK」をダウンロードし、「OculusUtilities.unitypackage」および「OvrAvatar.unityPackage」を Unity へインポートします。

サンプルの実行

OvrAvatar ディレクトリ下にアバターを表示するサンプルがあるので、いずれかを実行してみてください。例えば LocalAvatar シーンを実行すると以下のようなアバターが表示されます。

f:id:hecomi:20161222011335p:plain

ただし以下のようなエラーが表示されます。

No Oculus Rift App ID has been provided. Go to OvrAvatar > Edit Configuration to supply one UnityEngine.Debug:LogError(Object, Object) OvrAvatarSDKManager:Initialize() (at Assets/OvrAvatar/Scripts/OvrAvatarSDKManager.cs:40) OvrAvatarSDKManager:get_Instance() (at Assets/OvrAvatar/Scripts/OvrAvatarSDKManager.cs:28) OvrAvatar:Start() (at Assets/OvrAvatar/Scripts/OvrAvatar.cs:433)

App ID の取得

エラーによると App ID を入力しなければならないと書いてあります。アバターを始めとする Oculus に登録された情報を取得するためには、事前に登録されたアプリの ID を必要とします。アプリはダッシュボードにアクセスして「Create New App」から登録することが出来ます。アプリの名前は「Unity Avatar Demo Project」等、適当なテスト用のもので問題ないようなので登録してみましょう。

作成後、「API」タブから App ID を取得できます。

f:id:hecomi:20161222012701p:plain

これをメニューの「Oculus Avatars > Edit Configuration」から OvrAvatarSettings の画面(実体は Resources/OvrAvatarSettingsScriptableObject)を表示し、Oculus Rift App Id に先程の App ID を入力します。

f:id:hecomi:20161226154406p:plain

しかしながら、この状態で実行するとエラーは表示されなくなりますが、以前としてデフォルトのアバターのままです。

これはアバターと紐付けるための Oculus User IDstring ではなく 295109307540267 のような UInt64 型のもの)を指定していないためです。アバターを取得するためにはアプリケーションの ID とユーザの ID の 2 つが必要というわけです。具体的には、LocalAvatar にアタッチされている OvrAvatar コンポーネントOculus User ID フィールドを見ると 0 となっているのが分かると思います。この ID は同コンポーネントStart() 内で初期化(OvrAvatarSDKManager.RequestAvatarSpecification())に使われており、これより前に OvrAvatar に与えてあげる必要があります。

Oculus User ID の取得

Oculus User ID を取得するには Oculus Platform SDK が必要となります。Platform SDK は Oculus に登録された情報(フレンド、アチーブメント、ルーム管理やマッチメイキングなど)を取得する役割を担います。情報は DLL 内にあるノンブロッキングの C 言語で書かれた API を経由して得られる形のようです。時間があれば別途まとめたいと思います。

Platform SDK は先程のダウンロードページからカテゴリのフィルタを「Oculus Platform」にすることでダウンロードできます。

展開後、「Unity/OculusPlatform.unitypackage」をインポートすれば一連の API が使えるようになります。早速 ID を取得してみましょう。ドキュメントは以下になります。

まずは、Platform SDK を初期化する必要があります。この初期化には Platform SDK に App ID を教えて上げる必要があります。「Resources > OculusPlatformSettings」を選択し、インスペクタから App ID を Oculus Rift App Id に記入してください。

f:id:hecomi:20161222013537p:plain

そして以下のようなコードを実行してみます。

using UnityEngine;
using Oculus.Platform;

public class GetUserIdTest : MonoBehaviour
{
    void Start()
    {
        Core.Initialize();

        Users.GetLoggedInUser().OnComplete(msg => {
            Debug.Log(msg.GetUser().ID); // 自身の ID(16 桁の数字)を表示
            Debug.Log(msg.GetUser().OculusID); // 自身の ID(文字列)を表示
        });
    }
}

これでコンソールに ID が表示されると思います。API は非同期で実行される形式のため、呼び出し終了後にコールバックで結果が返ってくる形になっています*1。なお、いくつかアプリを作成してテストしてみましたが、作成直後だと結果が返ってこない挙動になっている気がしました(気のせいかもしれません)。

カスタムアバターの表示

では最後にカスタムしたアバターを表示してみましょう。

OvrAvatar.cs をいじる方法

まずは簡単に変わるかどうかを確認してみます。以下の行を OvrAvatar.cs から探します。

void Start() {
    ShowLeftController(StartWithControllers);
    ShowRightController(StartWithControllers);
    OvrAvatarSDKManager.Instance.RequestAvatarSpecification(
        oculusUserID, this.AvatarSpecificationCallback);
}

この oculusUserID を先程見た自分の Oculus User ID にしてみます。

void Start() {
    ShowLeftController(StartWithControllers);
    ShowRightController(StartWithControllers);
    OvrAvatarSDKManager.Instance.RequestAvatarSpecification(
        ****************, this.AvatarSpecificationCallback);
}

これで自分のアバターが出れば成功です。

OvrAvatar.cs をいじらない方法

ただこれだと自分のアバターだけしか固定で表示されないので、指定された oculusUserID で出るように、以下のようなコンポーネントを書いてみます。

using UnityEngine;
using Oculus.Platform;

public class MyOvrAvatarGenerator : MonoBehaviour
{
    [SerializeField] GameObject avatarPrefab;

    void Start()
    {
        Core.Initialize();

        Users.GetLoggedInUser().OnComplete(msg => {
            var avatarObject = Instantiate(avatarPrefab);
            var avatar = avatarObject.GetComponent<OvrAvatar>();
            if (avatar != null) {
                avatar.ShowThirdPerson = true;
                avatar.ShowFirstPerson = false;
                avatar.oculusUserID = msg.GetUser().ID;
            }
        });
    }
}

OvrAvatarStart のタイミングで初期化が行われてしまうので、ID を取得してから生成する形にしています。これで以下のように自身のアバターが表示されます。

f:id:hecomi:20161222014218p:plain

別アカウントの利用

この状態で別のアカウントに切り替えると、GetUser() の結果が Null になってしまいます。これはそのユーザがアプリへ登録されてないことが原因だと思われます。

これを回避するためには、Dashboard からビルドしたバイナリを ALPHA CHANNEL へアップロードし、該当のユーザを招待、そのユーザに届いたメールの招待リンクをクリック、そしてそのアプリをそのユーザがインストールすれば正常に表示されるようになると思います(何度か試したのでおそらくあってると思います...)。

f:id:hecomi:20161225150042p:plain

f:id:hecomi:20161225150049p:plain

ビルド後に手が表示されない

ビルドすると手が表示されない問題が生じます。これは後で見ていきますが、アバタ用のシェーダが Shader.Find() でランタイム時に文字列で検索され、そこからマテリアルが生成されるのですが、Unity の仕様上、ビルド時に使われていないシェーダ等のリソースは除外されてしまうからです。これは Avatar SDK のダウンロードページの「Known Issues」にも書かれています。これを修正するには「Edit > Project Settings > Graphics」で Always Included ShadersSize を文字列で検索する対象のシェーダ分(3 つ)増やし、OvrAvatar > Content > Materials にある、AvatarSurfaceShaderAvatarSurfaceShaderSelfOccludingAvatarSurfaceShaderPBS の 3 つを増えたフィールドへドラッグ&ドロップして追加することで修正されます。

f:id:hecomi:20161225151737p:plain

サンプルシーン

Avatar SDK に含まれるサンプルのシーンを解説しておきます。SDK には以下の 4 つのシーンが含まれています。

  • Controllers
    • 一人称視点で手がどう見えるかを示したデモ
  • GripPoses
    • カスタムグリップポーズを作成するためのシーン(後述)
  • LocalAvatar
    • 一人称と三人称両方のアバターを置いたシーン
    • マイク入力によるボイスの可視化および Platform SDK による自身のアバタのロードは未実装
  • RemoteLoopback
    • アバタの動きを記録し、再生するサンプル

OvrAvatar コンポーネント

次に OvrAvatar コンポーネントの設定を見ていきましょう。インスペクタは次のようになっています。

f:id:hecomi:20161224123452p:plain

ドライバ

OvrAvatarDriver が最初の項目です。これはどんなインプットでどうモデルを動かすか、というのを担うコントローラの役割を担っています。これを継承したクラスとして OvrLocalAvatarDriverOvrRemoteAvatarDriver があり、前者は自分の動きについてくるもので、先程見た LocalAvatar シーンで使われています。後者はネットワーク越しのプレイヤや予め録画された動きを流し込めるものになっているようです。RemoteLoopback シーンで使われていて、ここでは単に OvrLocalAvatarDriver で動かしたものを OvrRemoteAvatarDriver へ流し込んでいるだけのデモになっています。

もう少し具体的には、OvrAvatarDriver には GetCurrentPose() という virtual 関数が有り、これは PoseFrame というポーズ情報を返すものになっています。

public abstract class OvrAvatarDriver : MonoBehaviour 
{
    public struct ControllerPose
    {
        public ovrAvatarButton buttons;
        public ovrAvatarTouch touches;
        public Vector2 joystickPosition;
        public float indexTrigger;
        public float handTrigger;
        public bool isActive;
        ...
    }

    public struct PoseFrame
    {
        public Vector3 headPosition;
        public Quaternion headRotation;
        public Vector3 handLeftPosition;
        public Quaternion handLeftRotation;
        public Vector3 handRightPosition;
        public Quaternion handRightRotation;
        public float voiceAmplitude;
        public ControllerPose controllerLeftPose;
        public ControllerPose controllerRightPose;
        ...
    };

    public abstract bool GetCurrentPose(out PoseFrame pose);
}

OvrLocalAvatarDriver を見てみるとこんな感じになっています。

public class OvrAvatarLocalDriver : OvrAvatarDriver 
{
    ...
    ControllerPose GetControllerPose(OVRInput.Controller controller)
    {
        ovrAvatarButton buttons = 0;
        if (OVRInput.Get(OVRInput.Button.One, controller)) buttons |= ovrAvatarButton.One;
        ...

        ovrAvatarTouch touches = 0;
        if (OVRInput.Get(OVRInput.Touch.One, controller)) touches |= ovrAvatarTouch.One;
        ...

        return new ControllerPose
        {
            buttons = buttons,
            touches = touches,
            joystickPosition = OVRInput.Get(OVRInput.Axis2D.PrimaryThumbstick, controller),
            indexTrigger = OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger, controller),
            handTrigger = OVRInput.Get(OVRInput.Axis1D.PrimaryHandTrigger, controller),
            isActive = (OVRInput.GetActiveController() & controller) != 0,
        };
    }
    ...
    public override bool GetCurrentPose(out PoseFrame pose)
    {
        pose = new PoseFrame
        {
            voiceAmplitude = voiceAmplitude,
            headPosition = UnityEngine.VR.InputTracking.GetLocalPosition(UnityEngine.VR.VRNode.CenterEye),
            headRotation = UnityEngine.VR.InputTracking.GetLocalRotation(UnityEngine.VR.VRNode.CenterEye),
            handLeftPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.LTouch),
            handLeftRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.LTouch),
            handRightPosition = OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch),
            handRightRotation = OVRInput.GetLocalControllerRotation(OVRInput.Controller.RTouch),
            controllerLeftPose = GetControllerPose(OVRInput.Controller.LTouch),
            controllerRightPose = GetControllerPose(OVRInput.Controller.RTouch),
        };
        return true;
    }
}

こうして詰められたポーズ情報が OvrAvatar の中で処理され、頭の位置や指の動きに反映されます。

パーツ

アバタは各パーツに分かれています。顔を含む身体、左手、右手、左コントローラ、右コントローラなどです。全て、IAvatarPart を継承したコンポーネント(例. OvrAvatarBodyOvrAvatarHand)がアタッチされています。ここではアセットがロードされたタイミングで OnAssetsLoaded() が実行されたり、virtual な関数の UpdatePose() などが記述されていますが、コードを読む限り、現状余り使われてないように思われます。

Record Packets

このオプションを true にすると、PoseFrame データが記録され、OvrAvatar.PacketRecorded イベントハンドラに登録したコールバックが呼ばれるようになります。詳細は RemoteLoopback シーンの RemoteLoopbackManager コンポーネントを見てみてください。

Start With Controllers

これをチェックすると握るまでの間は Touch のモデルが、握った後は Touch のモデルと手が一緒に表示されるようになります。スティック操作で親指の動きも反映されるようになります。

f:id:hecomi:20161224213711p:plain

Show First Person

一人称視点用の表示です。自身の身体は描画されず、手やコントローラのみ表示されるようになります。

Show Third Person

リモートプレイヤなど、他の人の描画用です。身体のモデルも表示されるようになります。一人称でこれを使ってしまうと、HMD をかけたモデルなどではその HMD のモデルが視界の端に表示されてしまったり、半透明の描画がおかしくなったりします。

First Person Layer / Third Person Layer

一人称 / 三人称それぞれのモデルの描画レイヤを指定できます。

Capabilities

ネイティブ側の関数である ovrAvatar_Create() の引数に使う設定を変更できますが、ovrAvatarCapabilities.All 以外はエラーとなるため、特に変更しなくて良いと思います。

Left / Right Hand Custom Pose

カスタムグリップポーズの元になる Transform を指定します。次の項で説明します。

カスタムグリップポーズ

握ったときの手の形状はカスタマイズすることが出来ます。このカスタムポーズは GripPoses シーンで作成することが出来ます。解説は以下のドキュメントにも書いてあります。

まずシーンを開き、プレイボタンを押して実行してみましょう。シーンビューに以下のようにデバッグ表示つきで手が表示されていると思います。持ちながらだと大変なので Touch は適当な場所に置いておきましょう。

f:id:hecomi:20161225135135p:plain

次にヒエラルキを展開し、LocalAvatar > hand_left > LeftHandPoseEditHelper というゲームオブジェクトを見つけます。この配下にぶらさがっている手の各関節を動かすと手の形状もそれに合わせて変形されるようになります。

f:id:hecomi:20161225135544p:plain

f:id:hecomi:20161225135550p:plain

例えば図だと LeftHandPoseEditHelper > hands_l_hand_world > hands:b_l_middle1 > hands:b_l_middle2 の中指の第 1 関節を曲げた状態です。各関節を適当に曲げて試しにピースを作ってみましょう。

f:id:hecomi:20161225140301p:plain

この状態で hand_l_hand_world のゲームオブジェクトを選択し、Project ウィンドウへドラッグドロップして Prefab 化します。

f:id:hecomi:20161225140429p:plain

これでいったんこのシーンは終了し、試しに LocalAvatar シーンを開いてみてください。LocalAvatar ゲームオブジェクトにアタッチされている OvrAvatar コンポーネントLeft Hand Custom Pose(右手を編集した場合は Right Hand Custom Pose)に先程 Prefab 化したオブジェクトを指定してみます。

f:id:hecomi:20161225141340g:plain

この処理をスクリプトで行うようにすれば動的に切り替えることが出来ます。例えばものに合わせたグリップポーズを作っておき、つかめるオブジェクトにそのポーズを設定、掴んだ際にそのポーズを取得してカスタムポーズとして利用すれば、オブジェクトごとのグリップポーズを実現できます。

アバター生成の流れ

ではアバターはコード上でどんな流れで生成されているのか最後に見ておきたいと思います。知らなくても何ら問題ないように SDK が作られるべきなので実際知らなくても問題ありませんが、知っておくと参考になりますし、問題が出てきた時に対処しやすいかもしれないので解説したいと思います。必要ない方は読み飛ばしてください。

最初に流れをまとめておくと以下のような感じです。

  1. アバタ情報のリクエスト
  2. 取得したアバタ情報からアセットをロード
  3. 初回フレームで必要なパーツを生成、マテリアルを更新
  4. 毎フレーム位置姿勢は更新、以降は変更があればマテリアルを更新

ネイティブとの絡みでコードは一見複雑に見えますが、流れはそこまで複雑ではありません。メッシュやマテリアルの情報は Oculus のサーバ上に保存されているため、それをネイティブ側で取得し、適切なメッシュとマテリアルを選択・表示している形です。

まずはエントリポイントから見ていきます。OvrAvatar.Start() で以下のようにアバタ情報をリクエストします。

void Start()
{
    ShowLeftController(StartWithControllers);
    ShowRightController(StartWithControllers);
    OvrAvatarSDKManager.Instance.RequestAvatarSpecification(
        oculusUserID, this.AvatarSpecificationCallback);
}

このコールバックがマネージャ(実行時にマネージャ用のゲームオブジェクトが作られそこにアタッチされている)に登録され、ロードが非同期で裏で行われます。Unity の Update 毎に終了したかポーリングしてチェックを行い、終了したらこのコールバックが呼ばれます。

void AvatarSpecificationCallback(IntPtr avatarSpecification)
{
    // 得られたアバタ情報を使ってアバタを生成(ネイティブ側)
    sdkAvatar = CAPI.ovrAvatar_Create(avatarSpecification, Capabilities);
    ...

    // ネイティブ側で生成されたアバタを見て Unity 側でパーツを生成
    UInt32 assetCount = CAPI.ovrAvatar_GetReferencedAssetCount(sdkAvatar);
    for (UInt32 i = 0; i < assetCount; ++i)
    {
        UInt64 id = CAPI.ovrAvatar_GetReferencedAsset(sdkAvatar, i);
        if (OvrAvatarSDKManager.Instance.GetAsset(id) == null)
        {
            OvrAvatarSDKManager.Instance.BeginLoadingAsset(id, this.AssetLoadedCallback);
            assetLoadingIds.Add(id);
        }
    }
}

アバタのパーツ数は変動します(例えば髪の毛有無、HMD 有無など)。まずこの数を調べた後、各パーツの ID を取得しています。この ID に紐付いたアセットが未ロードであれば、マネージャにロードをしてくれるよう依頼します。この際、assetLoadingIds にロード中の ID を登録していて、この残り ID 数を Update() 内でチェックして 0 になれば処理が進む流れになっています。ロード後のコールバックではこの ID のリストからロード済みの ID を消すコードが書いてあります。ロード後の処理の該当箇所は以下のとおりです。

if (sdkAvatar != IntPtr.Zero && assetLoadingIds.Count == 0)
{
    UpdateSDKAvatarUnityState();
    ...
}

この UpdateSDKAvatarUnityState()コンポーネントの更新を行っています。最初の更新時に生成されていないパーツを生成するようなコードになっています。ちなみにここでいうコンポーネントとは、身体(顔や HMD など)や左手、右手といった Prefab 直下にぶら下がっているもの達です。分かりやすいようにゲームオブジェクト生成前・後のヒエラルキを見ると、以下のように各パーツを表すゲームオブジェクトの下にメッシュ描画などを担うゲームオブジェクトがぶら下がるようになります。

f:id:hecomi:20161224213144p:plain

この部分のコードを見てみます。

// ネイティブ側のコンポーネント
public struct ovrAvatarComponent
{
    public ovrAvatarTransform transform;
    public UInt32 renderPartCount;
    public IntPtr renderParts;

    [MarshalAs(UnmanagedType.LPStr)]
    public string name;
};

private void UpdateSDKAvatarUnityState()
{
    ...
    // コンポーネント(細かいパーツ)を全て見ていく
    for (UInt64 i = 0; i < componentCount; i++)
    {
        // ネイティブ側からコンポーネントの情報を取り出す
        IntPtr ptr = CAPI.ovrAvatarComponent_Get_Native(sdkAvatar, i);
        ovrAvatarComponent component = (ovrAvatarComponent)Marshal.PtrToStructure(ptr, typeof(ovrAvatarComponent));
        ...
        // そのコンポーネントに対応するオブジェクトがまだ生成されていない場合は生成して登録
        if (!trackedComponents.ContainsKey(component.name))
        {
            // どのパーツかを調べて親オブジェクト(base, body, hand_left, ... など)および
            // そこにアタッチされる予定のコンポーネントの型を取得
            GameObject componentObject = null;
            Type specificType = null;
            // ベース(トラッキング中心位置)か
            if (ptr == CAPI.ovrAvatarPose_GetBaseComponent(sdkAvatar).renderComponent)
            {
                specificType = typeof(OvrAvatarBase);
                if (Base != null)
                {
                    componentObject = Base.gameObject;
                }
            }
            // ボディか
            else if (ptr == CAPI.ovrAvatarPose_GetBodyComponent(sdkAvatar).renderComponent)
            {
                specificType = typeof(OvrAvatarBody);
                if (Body != null)
                {
                    componentObject = Body.gameObject;
                }
            }
            else if ... // その他(左手、右手、左コントローラ...)のコンポーネントを調査

            // 各コンポーネントに含まれるパーツ(ボディなら髪、身体、メガネなど)を生成する
            // AvatarComponent を該当のオブジェクトにアタッチする
            if (componentObject != null)
            {
                AddAvatarComponent(componentObject, component);
                ...
            }
        }

        // コンポーネントの更新
        UpdateAvatarComponent(component);
    }
    ...
}

余談ですが、コーディングルールとしてネイティブ側と結びつく型は最初のキャレットが小文字から始まり、C# 側の型は大文字から始まるようです。ここではアバタのコンポーネント(ボディ、左手、右手...)を登録し、それらを毎フレーム更新しています。ちょっと解説がややこしいですが、ここで言っているコンポーネントは Unity のコンポーネントとは別で、アバタのオブジェクトにぶら下がっているものを指しています。また、これらには更にパーツ(アバタの編集でスワップ可能なもの)がぶら下がる形になります。

ではこの登録部分の AddAvatarComponent() が何をしているか見てみましょう。

public class OvrAvatarComponent : MonoBehaviour
{
    public List<OvrAvatarRenderComponent> RenderParts = new List<OvrAvatarRenderComponent>();
}

private void AddAvatarComponent(GameObject componentObject, ovrAvatarComponent component)
{
    // パーツを管理するための OvrAvatarComponent を追加
    var ovrComponent = componentObject.AddComponent<OvrAvatarComponent>();
    trackedComponents.Add(component.name, ovrComponent);

    // パーツの数だけループ
    for (UInt32 renderPartIndex = 0; renderPartIndex < component.renderPartCount; renderPartIndex++)
    {
        // 子にゲームオブジェクトを追加
        var renderPartObject = new GameObject();
        renderPartObject.name = GetRenderPartName(component, renderPartIndex);
        renderPartObject.transform.SetParent(componentObject.transform);

        // どういったレンダラ(見た目)のパーツかを調べる
        IntPtr renderPart = GetRenderPart(component, renderPartIndex);
        ovrAvatarRenderPartType type = CAPI.ovrAvatarRenderPart_GetType(renderPart);
        OvrAvatarRenderComponent ovrRenderPart;

        // 該当のレンダラをアタッチ
        switch (type)
        {
            case ovrAvatarRenderPartType.SkinnedMeshRender:
                ovrRenderPart = AddSkinnedMeshRenderComponent(
                    renderPartObject,
                    CAPI.ovrAvatarRenderPart_GetSkinnedMeshRender(renderPart));
                break;
            case ovrAvatarRenderPartType.SkinnedMeshRenderPBS:
                ovrRenderPart = AddSkinnedMeshRenderPBSComponent(
                    renderPartObject,
                    CAPI.ovrAvatarRenderPart_GetSkinnedMeshRenderPBS(renderPart));
                break;
            case ovrAvatarRenderPartType.ProjectorRender:
                ovrRenderPart = AddProjectorRenderComponent(
                    renderPartObject,
                    CAPI.ovrAvatarRenderPart_GetProjectorRender(renderPart));
                break;
            default:
                throw new NotImplementedException(
                    string.Format("Unsupported render part type: {0}",
                                  type.ToString()));
        }

        // パーツを保存
        ovrComponent.RenderParts.Add(ovrRenderPart);
    }
}

ようやくゲームオブジェクト生成とレンダラを付与するコードが出てきました。身体の描画は PBR か Non-PBR のマテリアルがあり、どちらにするかによって switch 文でレンダラが切り替わっています。ovrAvatarRenderPartType.ProjectorRender はボイスの描画用みたいですが、まだサンプルが対応していないようなので飛ばします。

ここでは、Non-PBR 側の ovrAvatarRenderPartType.SkinnedMeshRenderer だけ追ってみます。コンポーネントの追加はこうなっています。やってることは単にアタッチして初期化です。

private OvrAvatarSkinnedMeshRenderComponent AddSkinnedMeshRenderComponent(
    GameObject gameObject,
    ovrAvatarRenderPart_SkinnedMeshRender skinnedMeshRender)
{
    var skinnedMeshRenderer = gameObject.AddComponent<OvrAvatarSkinnedMeshRenderComponent>();
    skinnedMeshRenderer.Initialize(
        skinnedMeshRender,
        ThirdPersonLayer.layerIndex,
        FirstPersonLayer.layerIndex,
        renderPartCount++);
    return skinnedMeshRenderer;
}

こうして取り付けられた OvrAvatarSkinnedMeshRenderComponent 側のコードを見てみます。

using UnityEngine;

public class OvrAvatarSkinnedMeshRenderComponent : OvrAvatarRenderComponent
{
    SkinnedMeshRenderer mesh;
    Transform[] bones;

    // Unity 組み込みの SkinnedMeshRenderer を生成
    internal void Initialize(
        ovrAvatarRenderPart_SkinnedMeshRender skinnedMeshRender,
        int thirdPersonLayer,
        int firstPersonLayer,
        int sortOrder)
    {
        mesh = CreateSkinnedMesh(
            skinnedMeshRender.meshAssetID, 
            skinnedMeshRender.visibilityMask, 
            false, 
            thirdPersonLayer, 
            firstPersonLayer, 
            sortOrder);
        bones = mesh.bones;
    }

    // ちなみにこの更新は先程見た UpdateSDKAvatarUnityState() から呼ばれる
    // 位置姿勢とマテリアルの更新を行う
    internal void UpdateSkinnedMeshRender(
        OvrAvatar avatar, 
        ovrAvatarRenderPart_SkinnedMeshRender meshRender)
    {
        UpdateSkinnedMesh(
            avatar, 
            mesh, 
            bones, 
            meshRender.localTransform, 
            meshRender.visibilityMask, 
            meshRender.skinnedPose);
        UpdateAvatarMaterial(
            mesh.sharedMaterial, 
            meshRender.materialState);
    }
}

この初期化では基底クラスの OvrAvatarRenderComponentCreateSkinnedMesh() を呼び出しています。ようやく出てきたこの OvrAvatarRenderComponent がモデルの描画を担うコンポーネントになっています。ネイティブから貰った ovrAvatarRenderPart_SkinnedMeshRender には「どんなメッシュを描くか」「どんなマテリアルで描くか」というような情報が入っていて、これを生成時・更新時に見て適切なメッシュとマテリアルでモデルが表示されるようにしています。この基底クラスを見てみましょう。

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

public class OvrAvatarRenderComponent : MonoBehaviour 
{
    ...
    protected SkinnedMeshRenderer CreateSkinnedMesh(
        ulong assetID, 
        ovrAvatarVisibilityFlags visibilityMask, 
        bool physicallyBasedShader, 
        int thirdPersonLayer, 
        int firstPersonLayer, 
        int sortingOrder)
    {
        // OvrAvatarAssetMesh はメッシュの情報を管理するクラス
        var meshAsset = (OvrAvatarAssetMesh)OvrAvatarSDKManager.Instance.GetAsset(assetID);
        ...

        // SkinnedMeshRenderer を追加して適切なメッシュを設定する
        var renderer = meshAsset.CreateSkinnedMeshRendererOnObject(gameObject);

        // マテリアルの生成
        renderer.sharedMaterial = CreateAvatarMaterial(
            gameObject.name + "_material", 
            physicallyBasedShader, 
            (visibilityMask & ovrAvatarVisibilityFlags.SelfOccluding) != 0);
        ...

        return renderer;
    }

    ...

    // シェーダの選択をしてマテリアルを生成
    protected Material CreateAvatarMaterial(
        string name,
        bool physicallyBasedShader, 
        bool selfOccluding)
    {
        string shaderPath;
        if (physicallyBasedShader)
        {
            // PBS なシェーダ
            shaderPath = "OvrAvatar/AvatarSurfaceShaderPBS";
        }
        else
        {
            // true  : 透明だがデプスを出力して前後関係が辺にならないようにしたもの
            // false : 透明
            shaderPath = selfOccluding ? 
                "OvrAvatar/AvatarSurfaceShaderSelfOccluding" :
                "OvrAvatar/AvatarSurfaceShader";
        }

        var s = Shader.Find(shaderPath);
        var mat = new Material(s);
        mat.name = name;
        materialStates.Add(mat, new ovrAvatarMaterialState());
        return mat;
    }

    // マテリアルの見た目を設定
    protected void UpdateAvatarMaterial(
        Material mat, 
        ovrAvatarMaterialState matState)
    {
        ...(変更があったときのみ以下更新)....
        mat.SetColor("_BaseColor", matState.baseColor);
        mat.SetInt("_BaseMaskType", (int)matState.baseMaskType);
        mat.SetVector("_BaseMaskParameters", matState.baseMaskParameters);
        mat.SetVector("_BaseMaskAxis", matState.baseMaskAxis);
        ...
    }
    ...
}

メッシュ生成のところでやっている OvrAvatarAssetMesh.CreateSkinnedMeshRendererOnObject() ですが、この中ではネイティブ側からメッシュ情報を取ってくる処理を行っています。Avatar SDK の中を見るとお分かりになるかもしれないですが、各パーツのメッシュはどこにもありません。これは直接 SDK から与えられるメッシュ情報を使っているからです。こういう設計にしておけばメッシュ情報をサーバ側で増やせば増やせる実装にもなっているのかな、と思います。ちょっと覗いてみましょう。

public class OvrAvatarAssetMesh : OvrAvatarAsset 
{
    public Mesh mesh;
    ...

    // コンストラクタはアセットのロード時に呼び出される
    public OvrAvatarAssetMesh(UInt64 _assetId, IntPtr asset) 
    {
        ...
        // メッシュ情報をネイティブから取得
        var meshAssetData = CAPI.ovrAvatarAsset_GetMeshData(asset);

        // Unity で理解できるようにメッシュを生成
        mesh = new Mesh();
        ...
        var vertexCount = (long)meshAssetData.vertexCount;
        var vertices    = new Vector3[vertexCount];
        var normals     = new Vector3[vertexCount];
        var tangents    = new Vector4[vertexCount];
        var uv          = new Vector2[vertexCount];
        var boneWeights = new BoneWeight[vertexCount];

        // それぞれの頂点データを一つずつ Marshal.PtrToStructure() で持ってきて
        // Mesh の情報に詰めていく
        var vertexSize = (long)Marshal.SizeOf(typeof(ovrAvatarMeshVertex));
        var vertexBufferStart = meshAssetData.vertexBuffer.ToInt64();

        for (long i = 0; i < vertexCount; i++)
        {
            var offset = vertexSize * i;
            var vertex = (ovrAvatarMeshVertex)Marshal.PtrToStructure(
                new IntPtr(vertexBufferStart + offset), 
                typeof(ovrAvatarMeshVertex));

            vertices[i] = new Vector3(vertex.x, vertex.y, -vertex.z);
            normals[i]  = new Vector3(vertex.nx, vertex.ny, -vertex.nz);
            tangents[i] = new Vector4(vertex.tx, vertex.ty, -vertex.tz, vertex.tw);
            uv[i]       = new Vector2(vertex.u, vertex.v);

            boneWeights[i].boneIndex0 = vertex.blendIndices[0];
            boneWeights[i].boneIndex1 = vertex.blendIndices[1];
            boneWeights[i].boneIndex2 = vertex.blendIndices[2];
            boneWeights[i].boneIndex3 = vertex.blendIndices[3];

            boneWeights[i].weight0 = vertex.blendWeights[0];
            boneWeights[i].weight1 = vertex.blendWeights[1];
            boneWeights[i].weight2 = vertex.blendWeights[2];
            boneWeights[i].weight3 = vertex.blendWeights[3];
        }

        mesh.vertices = vertices;
        mesh.normals = normals;
        mesh.uv = uv;
        mesh.tangents = tangents;
        mesh.boneWeights = boneWeights;

        // 後はもろもろ計算して完成
        ...
    }

    public SkinnedMeshRenderer CreateSkinnedMeshRendererOnObject(GameObject target)
    {
        // 生成しておいたメッシュを代入
        SkinnedMeshRenderer skinnedMeshRenderer = target.AddComponent<SkinnedMeshRenderer>();
        skinnedMeshRenderer.sharedMesh = mesh;
        ...
    }
}

長いですが、ネイティブの構造体を引っ張ってきて Unity の理解できる形式に変換しているだけです。

というわけで、ようやくメッシュ生成およびマテリアルの生成、そしてそれらのパラメタの設定までたどり着くことが出来ました。これで色々問題が起きても対処できると思います。

例えば先程の私のアバタ(緑色)の手が変に見えているのは自己遮蔽が false なマテリアルを使っていたからなので、これが嫌だな~ということになったら、selfOccluding による分岐をなくしたコードにしてみると次のように表示されるようになります。

f:id:hecomi:20161225132807p:plain

おわりに

現状では Platform SDK との兼ね合いで表示するまでは手順が少し多いですが、近いうちに改善されるとドキュメントに書いてあったので期待しましょう。

*1:API 毎に戻り値を変更できるようジェネリックにしてるのに他の API の結果も含めた共通の構造体にデータを詰める設計になってるのだけ頂けない...