#はじめに
HoloInkShooterがマルチプレイに対応して、みんなで床を塗れるようになったよ!
とゆーことで、今回はHoloLensのアプリにSharingを組み込む方法について、実際にサンプルアプリに組み込みながら、その手順とSharingの仕組みについてばっさり説明したいと思います。
開発環境
MixedRealityToolkit for Unity 2017.2.0p1 MRTP4
サンプルアプリ
Sharingを組み込むサンプルアプリとして、最近作った以下のアプリを生贄に捧げます。
https://github.com/arcsin16/HoloPseudoLighting/tree/master
これはAir-Tapをすると光る弾が発射されて、発射された弾の近くの床や壁に光が反射しているように見えるアプリです。
githubのプロジェクトにはMixedRealityToolkitは含めていないので、別途インポートしてください。
#Sharingについて
一言でいうと、HoloLens で複数人で同じアプリ(複合現実空間)を共有するための機能です。
せっかく現実空間を共有しているのだからバーチャルなオブジェクトも同じ場所に表示しようぜ!という一見常識的に見えるけど実際にはクレイジーな機能を当然の様に使えるHoloLensは最高にロックだと思います。
詳しくは MixedRealityToolkitのSharingの説明 や Mixed Reality Academyの Tutorial 240 や Tutorial 250 をご参照ください。
##Sharingの仕組み
複数のHoloLensで同じ空間を共有する仕組みについて、簡単に説明すると
- 基準となる座標を合わせる(空間を合わせる)
- 基準となる座標からの相対座標を複数のHoloLens間で通信してオブジェクトの表示を同期させる (オブジェクトの位置とタイミングを合わせる)
の2点になります。
極端な話HoloLensで現実空間の同じ場所に同じオブジェクトを配置できないのは、基準となる座標系が一致していないから。の1点に尽きます。
例えば、現実世界で緯度経度から場所が一意に求められるのは、みんなが同じ緯度経度という座標系を共有しているからです。そのためHoloLensのアプリについても同様に、基準となる座標系さえ共有できれば、同じ場所にオブジェクトを配置できるのです。
具体的にはUnityのプロジェクトでは入れ物となるオブジェクトを用意して、その中に共有するオブジェクトを配置して、入れ物の位置を基準とした相対座標をやり取りします。
イメージしづらい場合、左手でフレミングを作ってください。これが入れ物のオブジェクトになります。
ここで、親指方向に10cmの位置(相対座標)に何かオブジェクトがあると想像してください。この時点ではあなたのオブジェクトの位置と他の人のオブジェクトの位置は異なります。それはお互いの左手のある場所が異なるからです。しかし手の位置をピタリと重ねれば、基準となる座標系が一致するため、オブジェクトの位置も同じになる。とまぁこんな感じです。
ではその座標系を合わせるのをどうやっているかというと、MixedRealityToolkitでは空間アンカーを利用して、画像処理的に位置合わせを行なっています。処理としてはライブラリ内で実装されているため、使う側はあまり意識する必要はありませんが、興味があれば調べて見ると良いでしょう。
#HoloLensアプリにUNETベースのSharing機能を組み込む
さあではここからが本題です。実際にサンプルアプリにSharingを組み込んで行きましょう!
##Capabilityの設定
まず初めに、メニューの「Mixed Reality Toolkit」→「Configure」→「Apply UWP Capability Setting」を開き、「Internet Client Server」と「Private Network Client Server」のチェックを追加します。
##Sharing用モジュールの組み込み
ProjectビューからHoloToolkit/SharingWithUNET/SharingWithUnetExample シーンを開き、"UNETSharingStage", "UNETAnchorManager", "HologramCollection", "UIContainer"をコピーして、自分のシーンに貼り付けます。
この中の「HologramCollection」が、基準となる座標系を共有するために利用する入れ物となります。適当にこの中に、オブジェクトを置いておけば、その位置が共有されるようになります。
次に自分のアプリのシーンにManagersオブジェクトを作成し、WorldAnchorManagerコンポーネントとGenericNetworkTransmitterコンポーネントを追加します。
ManagersオブジェクトはSpatialMappingやらInputManagerやらを格納するオブジェクトで、既存のものがあればそちらに、なければ適当にオブジェクトを作ってもらえればよいです。
##既存のアプリのイベント処理の無効化
AirTapで弾を飛ばすような処理を実装している場合、一旦無効化しておきます。
→サンプルアプリではManagers/PseudoLightingManagerでそういった処理を実装しているので以下のように無効化します。
void Start () {
//無効化
//InteractionManager.InteractionSourcePressed += InteractionManager_InteractionSourcePressed;
posArray = new Vector4[32];
colorArray = new Color[32];
}
void Update()
{
//無効化
//if (Input.GetKeyDown(KeyCode.Space))
//{
// SpawnLightSource();
//}
}
以上で、Sharingの座標系を共有する機能は動くようになりました。
一度アプリを起動して動作を確認します。起動してAvailable Sessionウィンドウが表示されれば第一段階クリアです。
また、少し移動してアプリを起動した場所あたりを見ると、以下の様なテキストが表示されているはずです。これはSharing時に基準となる座標系の基準点を示すテキストになります。ただ、現時点ではまだSharingを開始していないので、ただ単にアプリの起動した位置に表示されています。
このテキストの上のボタンを押下すると、以下の様なデバッグテキストが表示されます。Sharingのデバッグを行う際にお世話になるので覚えておきましょう。
それでは最初に表示されていたAvailable Sessionウィンドウに戻り、左下のStartボタンを押下して新規セッションを開始します。問題なく動作すれば、Available Sessionウィンドウが消えて、アンカー位置が更新されているはずです。以下の様に"Anchored Here"と表示されれば成功です。
もし、この時点でNotAnchoredと表示される場合、一度アプリを起動しなおして再度実行してみたり、デバッグテキストで例外が発生していないか確認してみましょう。
次に、周りのHoloLensを持っている人を捕まえて下さい。
その人のHoloLensに同じアプリをデプロイして同じLANに繋いで起動すると、Available Sessionsウィンドウにあなたが立ち上げたセッションが表示されます。そちらを選択して、右下の Join ボタンでセッションに参加すると、そのセッション内でアンカー位置が共有されます。
以上でUNETベースのSharingの座標系の位置合わせができるようになりました。後はこれを元にアプリ毎の処理(弾を発射する)を実装して行きましょう。
#UNETのネットワーク処理の概要
その前にざっくりとUNETの処理の概要について説明しましょう。
詳細はUnityのドキュメントに詳しくまとまっていますのでそちらを参照ください。
ここではざっくりとSharingを実装するうえで抑えておいたほうが良いポイントをかいつまんで説明したいと思います。
UNETのネットワークシステムは1つのサーバと複数のクライアントから構成されるクライアント-サーバ型のネットワークとなります。UNETベースのSharingでは別途サーバを用意するわけではないので、1台のHoloLensがサーバーとクライアントを兼任するホストとなり、その他のHoloLensはクライアントとなります。
各プレイヤーやオブジェクトの位置姿勢などネットワーク内で共有する情報はサーバ上で管理されます。クライアンから操作したい場合は、サーバに対してコマンドを送信し、コマンドを受けたサーバが情報を更新し、クライアントに変更を通知するといった流れとなります。
- 1台はサーバを兼任してくれるよ
- サーバで情報が管理されるよ
- クライアントからサーバにコマンドを送信して、サーバが情報を更新、操作するよ
- 伝送経路は違うけど、ホストも他のクライアントも処理的に違いはないよ
##プレイヤーについて
Sharingする場合に最低限共有する必要がある情報として、各プレイヤーの位置姿勢があります。
UNETベースのSharingでは、UNETSharingStageオブジェクト内に設定された Player Prefab を使い、各プレイヤーのプレイヤーオブジェクトが生成され、プレイヤーオブジェクトを介してプレイヤーやその他の共有するオブジェクトなどの位置姿勢や処理について実装することになります。
プレイヤーオブジェクトは下図の様にあなたのプレイヤーオブジェクトがあなたのクライアントと他のクライアント、そしてサーバのそれぞれで生成、共有されます。
オブジェクトの生成や管理については、サーバーを介して行われます。位置姿勢の更新や、新規オブジェクトの配置などを行いたい場合、プレイヤーからサーバに対してコマンドを送信して、サーバ側でコマンドに応じて位置姿勢情報の更新やオブジェクトの生成を行い、それを各クライアントに通知、反映することで全体の状態を同期します。
具体的な実装については、Player PrefabのPlayer Controllerで定義されています。
既存のPlayerControllerは必要最低限の機能以外にいくつか機能が盛り込まれているため、本記事では分かりやすいように機能を削ったシンプルなPlayer Controllerを作成しつつ説明したいと思いますので、前準備として既存のPlayer Prefabを複製して、SimplePlayer Prefabを作成してください。
##Player Controllerを作成する。
今回、Player Controllerで実現する機能は、以下の2つとします。
- 位置姿勢を共有する
- AirTapで弾を撃つ
- オブジェクトの生成
- 位置姿勢の共有
そして以下が作成したPlayer Controllerになります。細かい処理は後程説明するのでひとまず読み流してOKです。
using HoloToolkit.Unity.InputModule;
using HoloToolkit.Unity.SharingWithUNET;
using UnityEngine;
using UnityEngine.Networking;
[NetworkSettings(sendInterval = 0.033f)]
public class SimplePlayerController : NetworkBehaviour,IInputClickHandler {
public GameObject bullet;
/// <summary>
/// The transform of the shared world anchor.
/// </summary>
private Transform sharedWorldAnchorTransform;
private NetworkDiscoveryWithAnchors networkDiscovery;
[SyncVar]
private Vector3 localPosition;
[SyncVar]
private Quaternion localRotation;
void Awake()
{
networkDiscovery = NetworkDiscoveryWithAnchors.Instance;
}
// Use this for initialization
void Start()
{
if (SharedCollection.Instance == null)
{
Debug.LogError("This script required a SharedCollection script attached to a gameobject in the scene");
Destroy(this);
return;
}
if (isLocalPlayer)
{
//イベントリスナーの登録
InputManager.Instance.AddGlobalListener(gameObject);
}
//位置合わせの基準となるアンカーのtransformを親に設定する
sharedWorldAnchorTransform = SharedCollection.Instance.gameObject.transform;
transform.SetParent(sharedWorldAnchorTransform);
}
private void OnDestroy()
{
if (isLocalPlayer)
{
//イベントリスナーの削除
InputManager.Instance.RemoveGlobalListener(gameObject);
}
}
void Update()
{
//他プレイヤーの位置反映
if (!isLocalPlayer)
{
//CmdTransformで通知された他のプレイヤーのlocalPosition, localRotationを反映する。
transform.localPosition = Vector3.Lerp(transform.localPosition, localPosition, 0.3f);
transform.localRotation = localRotation;
return;
}
// 自プレイヤーの位置更新
transform.position = Camera.main.transform.position;
transform.rotation = Camera.main.transform.rotation;
localPosition = transform.localPosition;
localRotation = transform.localRotation;
// サーバへ位置更新のコマンドを送信する
CmdTransform(localPosition, localRotation);
}
public void OnInputClicked(InputClickedEventData eventData)
{
if (isLocalPlayer)
{
//AirTapで弾を発射する
CmdFire();
}
}
//[Command]属性が付き、Cmdから始まるメソッドはサーバで実行される処理
//クライアントがこれらのメソッドを呼び出すと、クライアント側では何も処理されず
//サーバ側で処理が呼び出される。
//姿勢更新はマイフレーム発生するので、
//channel=1(QosType.UnreliableFragmented)として速度優先する。
[Command(channel = 1)]
public void CmdTransform(Vector3 postion, Quaternion rotation)
{
//クライアント -> サーバにクライアントのローカルプレイヤーの
//位置と姿勢(position, rotation)がコマンドの引数として通知される。
//サーバは[SyncVar]属性付きのlocalPosition、localRotationに値を保存
//することで、サーバ -> 全クライアントに反映する。
localPosition = postion;
localRotation = rotation;
}
/// <summary>
/// Called on the host when a bullet needs to be added.
/// This will 'spawn' the bullet on all clients, including the
/// client on the host.
/// </summary>
[Command]
void CmdFire()
{
Vector3 bulletDir = transform.forward;
Vector3 bulletPos = transform.position + bulletDir * 1.5f;
var color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f), 1);
var localPos = sharedWorldAnchorTransform.InverseTransformPoint(bulletPos);
var localDir = sharedWorldAnchorTransform.InverseTransformVector(bulletDir);
//サーバー側からアンカーを起点とした相対座標で位置、姿勢を共有する
//クライアント側ではオブジェクトが生成された際に、相対座標を元に実際の位置、姿勢を反映する
//→PseudoLightBehaviour::Startを参照
GameObject nextBullet = (GameObject)Instantiate(bullet, localPos, Quaternion.Euler(localDir));
//速度、色などはNetworkServer.Spawnしてもクライアント側には反映されないので
//[SyncVar]なフィールドを定義して共有しておいて、PseudoLightBehaviour::Startで反映する。
var behaviour = nextBullet.GetComponentInChildren<PseudoLightBehaviour>();
behaviour.color = color;
behaviour.localVelocity = localDir * 1.0f;
behaviour.localAngularVelocity = Random.onUnitSphere;
//ホストクライアントを含む全クライアントにオブジェクトを生成する。
//※ここで生成するオブジェクトは、UNETSharingStageのRegistered Spawnable Prefabに登録しておくこと。
NetworkServer.Spawn(nextBullet);
//8秒後に弾を破棄する
//サーバでDestroyされると、全クライアントに生成された弾も破棄される
Destroy(nextBullet, 8.0f);
}
}
public class SimplePlayerController : NetworkBehaviour,IInputClickHandler {
まず最初にBehaviourではなく、NetworkBehaviourを継承します。
その他に[SyncVar]、[Command]、isLocalPlayer、isServerなどの見慣れない表記がありますね。これらは、NetworkBehaviour で利用できるアトリビュートやフィールド変数となり、クライアントサーバ間の処理のコマンドの送信、データの同期のために利用されます。
各処理の説明の中で簡単に説明していきますが、詳細については公式ドキュメントのネットワークシステムの概要、ステートの同期、リモートアクションを参照することを推奨します。
###位置姿勢を共有する
プレイヤーの位置姿勢の共有について説明します。
位置姿勢を共有する処理の入り口はプレイヤーオブジェクトのUpdateメソッドになります。この中で自分の位置をCmdTransformを呼び出しサーバへ通知します。
void Update()
{
・・・
// 自プレイヤーの位置更新
transform.position = Camera.main.transform.position;
transform.rotation = Camera.main.transform.rotation;
localPosition = transform.localPosition;
localRotation = transform.localRotation;
// サーバへ位置更新のコマンドを送信する
CmdTransform(localPosition, localRotation);
}
CmdTransformの様に、[Command]属性が付与され、Cmdと接頭子のついたメソッドはクライアントから呼び出されると、クライアントでは処理は実行されずに、サーバ側で処理が実行されるようになります。
CmdTransformのサーバ側の処理ではlocalPosition、localRotationフィールドを更新しています。localPosition、localRotationは[SyncVar]属性が付与されているため、サーバで変更した値が、各クライアントのプレイヤーオブジェクトに反映されるようになります。
[SyncVar]
private Vector3 localPosition;
[SyncVar]
private Quaternion localRotation;
[Command(channel = 1)]
public void CmdTransform(Vector3 postion, Quaternion rotation)
{
//クライアント -> サーバにクライアントのローカルプレイヤーの
//位置と姿勢(position, rotation)がコマンドの引数として通知される。
//サーバは[SyncVar]属性付きのlocalPosition、localRotationに値を保存
//することで、サーバ -> 全クライアントに反映する。
localPosition = postion;
localRotation = rotation;
}
ここまではまだ変数の値が同期されたにすぎませんので、後は各クライアントがUpdateでlocalPosition/Rotationを参照して、プレイヤーの位置姿勢を表すtranformに反映すれば、プレイヤーオブジェクトの位置が更新されます。
void Update()
{
//他プレイヤーの位置反映
if (!isLocalPlayer)
{
//CmdTransformで通知された他のプレイヤーのlocalPosition, localRotationを反映する。
transform.localPosition = Vector3.Lerp(transform.localPosition, localPosition, 0.3f);
transform.localRotation = localRotation;
return;
}
・・・
isLocalPlayer変数はプレイヤーオブジェクトが自分自身かどうかを判別するための、NetworkBehaviourクラスで定義されたプロパティです。プレイヤーオブジェクトは自分のプレイヤーオブジェクト以外に、他プレイヤーのオブジェクトも存在するため、自分の位置姿勢を更新するために、isLocalPlayerを参照して自分のプレイヤーオブジェクトを識別する必要があります。
その他にも以下のプロパティが利用可能です。
- isServer - オブジェクトがサーバー(またはホスト)上にあり、既に生成されている場合は true。
- isClient - オブジェクトがクライアント上にあり、サーバーによって作成された場合は true。
- isLocalPlayer - オブジェクトが、該当クライアントのプレイヤーオブジェクトである場合は true。
- hasAuthority - オブジェクトがローカル処理によって所有されている場合は true。
###弾を発射する
次に弾の発射処理について説明します。
弾の発射処理の起点はプレイヤーオブジェクトに実装された入力イベント(Air-Tap)のイベントハンドラになります。
ここでも自分のプレイヤーオブジェクトから弾を発射させるために、isLocalPlayerで判別を行い、CmdFireを呼び出しサーバで弾の発射処理を実行してもらいます。
public void OnInputClicked(InputClickedEventData eventData)
{
if (isLocalPlayer)
{
//AirTapで弾を発射する
CmdFire();
}
}
コマンドを実行するサーバではまず、プレイヤーの位置、姿勢を基準に弾の発射位置(bulletPos)と方向(bulletDir)を決定します。
これらはHoloLensの起動位置を原点とした座標系での座標となりますので、アンカー位置を基準とした座標系の位置(localPos)と方向(localDir)に変換します。
位置の変換にはTransform::InverseTransform(Vector3)を方向(ベクトル)の変換にはTransform::InverseTransformVector(Vector3)を利用します。
この時に利用される sharedWorldAnchorTransformはStart()で初期化した SharedCollection のTransformになります。これはUnityのシーン内の「HologramCollection」に追加されたコンポーネントなので、HologramCollectionのTransformをアンカー位置として変換処理を行っていることになります。
void Start()
{
・・・
sharedWorldAnchorTransform = SharedCollection.Instance.gameObject.transform;
・・・
}
[Command]
void CmdFire()
{
Vector3 bulletDir = transform.forward;
Vector3 bulletPos = transform.position + bulletDir * 1.5f;
var color = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f), 1);
var localPos = sharedWorldAnchorTransform.InverseTransformPoint(bulletPos);
var localDir = sharedWorldAnchorTransform.InverseTransformVector(bulletDir);
//サーバー側からアンカーを起点とした相対座標で位置、姿勢を共有する
//クライアント側ではオブジェクトが生成された際に、相対座標を元に実際の位置、姿勢を反映する
//→PseudoLightBehaviour::Startを参照
GameObject nextBullet = (GameObject)Instantiate(bullet, localPos, Quaternion.Euler(localDir));
そして、Instantiateを呼び出してオブジェクトを生成しますが、この段階ではまだサーバ上でオブジェクトを生成しただけなので、各クライアントには何も生成されていない状態です。この後にNetworkServer.Spawnを呼び出すことで、各クライアントにオブジェクトが生成されることになります。
ここで一点気を付けないといけないことがあり、生成したオブジェクトのすべての情報がクライアント/サーバ間で共有されるわけではないのです。
生成したオブジェクトのRigidBodyを取得して、AddForceで移動させようとしても、その情報は共有されないため、明示的にで[SyncVar]なフィールドを使って情報を同期して、各クライアントのオブジェクトに反映させる必要があるのです。
UNETで、NetworkTransformといった速度や位置情報をできるコンポーネントがありますが、Sharingの場合共有しなければいけないのが、アンカー位置ベースの相対座標になるので、自前で処理したほうが多分シンプルです。
//速度、色などはNetworkServer.Spawnしてもクライアント側には反映されないので
//[SyncVar]なフィールドを定義して共有しておいて、PseudoLightBehaviour::Startで反映する。
var behaviour = nextBullet.GetComponentInChildren<PseudoLightBehaviour>();
behaviour.color = color;
behaviour.localVelocity = localDir * 1.0f;
behaviour.localAngularVelocity = Random.onUnitSphere;
//ホストクライアントを含む全クライアントにオブジェクトを生成する。
//※ここで生成するオブジェクトは、UNETSharingStageのRegistered Spawnable Prefabに登録しておくこと。
NetworkServer.Spawn(nextBullet);
//8秒後に弾を破棄する
//サーバでDestroyされると、全クライアントに生成された弾も破棄される
Destroy(nextBullet, 8.0f);
}
後は生成される弾のBehaviourでデータをオブジェクトに反映させればOKですが、その前に共有する弾の設定を行います。
生成される弾にはサンプルアプリでは、PseudoLighting/Prefabs/LightSourcePrefabを使用します。ネットワークを介して共有することになるので、PrefabにNetworkIdentityコンポーネントを追加します。
こちらをSimplePlayerControllerのプロパティと
UNetSharingStageのRegisteredSpawnable Prefabに設定しておきます。ついでにPlayerPrefabもSimplePlayerControllerを使ったものに置き換えておきましょう。
そして、弾の挙動を定義していた、PseudoLightBehaviour.csを以下の様に書き換えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using HoloToolkit.Unity.SharingWithUNET;
public class PseudoLightBehaviour : NetworkBehaviour
{
//サーバが設定した初期値をクライアント側に反映するため[SyncVar]とし、
//オブジェクトが各クライアントで生成されたタイミングで参照する。
[SyncVar]
public Color color;
[SyncVar]
public Vector3 localVelocity;
[SyncVar]
public Vector3 localAngularVelocity;
private Rigidbody rb;
void Start () {
Debug.Log("Start");
if (SharedCollection.Instance == null)
{
Debug.LogError("This script required a SharedCollection script attached to a gameobject in the scene");
Destroy(this);
return;
}
//サーバがオブジェクト生成時にlocalPositionを初期位置に設定しているので
//localPositionを維持したまま、Parentを設定する。
transform.SetParent(SharedCollection.Instance.transform, false);
//速度の反映
rb = GetComponentInChildren<Rigidbody>();
rb.velocity = transform.parent.TransformDirection(localVelocity);
rb.angularVelocity = transform.parent.TransformVector(localAngularVelocity);
//光源(Cube)のマテリアルに色を反映
var rs = GetComponentsInChildren<Renderer>();
if (rs != null)
{
foreach (var r in rs)
{
r.material.color = color;
}
}
//光源(Particle)のマテリアルに色を反映
var particles = GetComponentsInChildren<ParticleSystem>();
if (particles != null)
{
foreach (var p in particles)
{
ParticleSystem.MinMaxGradient gradient = new ParticleSystem.MinMaxGradient();
gradient.color = color;
gradient.mode = ParticleSystemGradientMode.Color;
var main = p.main;
main.startColor = gradient;
}
}
}
void Update () {
//光源の位置を更新
PseudoLightingManager.Instance.SetLightPosition(transform.position, color);
}
}
前述した通り、色や速度が[SyncVar]でサーバから同期されているため、オブジェクトを生成してStartが呼ばれた際にRigidbodyやMaterial、ParticleSystemに反映しています。
以上でSharingの適用ができました!アプリを起動して動作確認をしてみましょう。
#まとめ
Sharingの仕組みについて振り返ってみましょう。
- 基準となる座標を合わせる(空間を合わせる)
- 基準となる座標からの相対座標を複数のHoloLens間で通信してオブジェクトの表示を同期させる (オブジェクトの位置とタイミングを合わせる)
基準となる座標系を合わせる方法については、ゲームオブジェクトを持ってくるだけで良いので、思ったより簡単にできたかと思います。
オブジェクトの表示を同期させる方法については、[SyncVar]や[Command]、サーバ/クライアント、アンカーを基準とした相対座標など混乱しやすいところもあったかと思いますが、以下に気を付けると良いかと思います。
- UNETのサーバ⇔クライアント、ローカルプレイヤー⇔他プレイヤーの違いをしっかり認識する。
- クライアントからコマンドをサーバに送信して、サーバが変更を全クライアントに反映する。
- クライアント-サーバ間の座標のやり取りはアンカー位置を基準とした相対座標でやり取りする。
今回弾を飛ばす系のアプリを実装しましたが、これ以外のケースについても基本的な考え方は一緒ですので、これをきっかけに皆さんも自分のアプリにSharingを組み込んでみようと思ってもらえるとうれしいです。
なお、出来上がったものはこちらにありますので、うまくいかない場合などご参照ください。
https://github.com/arcsin16/HoloPseudoLighting/tree/sharing
#謝辞
最後にUNETのことを何も分からない状態で、HoloLensでUNETのSharingを実装、理解するにあたり、
Unityのドキュメント、@h-takauma氏の記事を参考にさせていただきました。ありがとうございます。