Unity as a Libraryと SwiftUI で作るARアプリ (後編)
この記事は、前編の続きになります。
この記事のゴールは、SwiftUIからUnityオブジェクトを操作することがターゲットになります。 具体的にはSwiftUIでCubeを10度ずつ回せるようにします。
とはいえ、普通のNativePluginを使った開発と変わりはないです。sendUnityMessageToGOとdelegateを使った2つの方法をここでは紹介します。
(その1)sendUnityMessageToGOを使ったやり方
Cubeを回転させるためのスクリプトを作成する
Manipulator.csというスクリプトを作って、Cubeにこれをアタッチします。
using UnityEngine; public class Manipulator: MonoBehaviour { /// <summary> /// sendMessageToGOを使ってメッセージを受け取る場合、 /// 引数はstring型である必要がある。 /// </summary> /// <param name="message"></param> public void Rotate(string message) { if (float.TryParse(message, out var degree)) { this.transform.Rotate(Vector3.up * degree); } } }
- Unityプロジェクトをビルドします。
SwiftUIからCubeを回転させる
UnityBridge.swiftにsendMessageToGOをラップしたメソッドを追加する
/// 指定のGame Object で呼び出し可能なメソッドを呼び出す internal func sendMessageToGO(withName: String, functionName: String, message: String){ ufw.sendMessageToGO( withName: withName, functionName: functionName, message: message) }
ContentView.swiftに回転させるボタンを追加する
struct ContentView: View { var body: some View { ZStack{ UnityView() HStack{ Button(action: { UnityBridge.getInstance().sendMessageToGO(withName: "Cube", functionName: "Rotate", message: "10") }){ Image(systemName: "rotate.right") .frame(width: 60, height: 60) .imageScale(.large) .background(Color.black) .foregroundColor(.white) .clipShape(Circle()) } Spacer() Button(action: { UnityBridge.getInstance().sendMessageToGO(withName: "Cube", functionName: "Rotate", message: "-10") }){ Image(systemName: "rotate.left") .frame(width: 60, height: 60) .imageScale(.large) .background(Color.black) .foregroundColor(.white) .clipShape(Circle()) } } } } }
- ビルドする
これだけです! あとは実行画面に追加されたボタンを押すと、Cubeが回転してくれるはずです。
(その2) NativePluginを使ったやり方
sendMessageToGOは組むのは簡単ですが、同名メソッドを避ける必要やstringでしか値を渡せない点が不便です。
そこでDelegateを使ったメッセージをやり取りする方法がブログでは紹介されています。
Delegateを定義する
NativeCallsProxy.hにfloat値を受け取りたいので次のようなDelegateを定義します。
#import <Foundation/Foundation.h> // float値を受け取ってvoidを返すDelegate typedef void (*RotateDelegate)(const float degree);
- 定義したDelegateをUnityから登録してもらうためのメソッド
SetRotateDelegateを定義します。
#import <Foundation/Foundation.h> // float値を受け取ってvoidを返すDelegate typedef void (*RotateDelegate)(const float degree); // NativeCallsProtocol は iOS側からUnityメソッドに登録する型 @protocol NativeCallsProtocol @required - (void) onUnityStateChange:(const NSString*) state; // Unityのメソッドを登録する - (void) setRotateDelegate:(RotateDelegate) delegate; @end __attribute__ ((visibility("default"))) @interface FrameworkLibAPI : NSObject // UnityFrameworkLoadの後に呼び出す // iOS側はこのメソッドから、デリゲートを登録する +(void) registerAPIforNativeCalls:(id<NativeCallsProtocol>) aApi; @end
NativeCallsProxy.mmに実装を追加します。
// Unity側からiOSのメソッドを伝達する部分 // apiに登録されているデリゲートを呼び出している extern "C" { void sendUnityStateUpdate(const char* state) { const NSString* str = @(state); [api onUnityStateChange: str]; } void setRotateDelegate(RotateDelegate delegate) { [api setRotateDelegate: delegate]; } }
NativePluginからUnityのメソッドを呼び出せるようにする
HostNativeAPI.csにDllを呼び出すためのメソッドを追加する
public class HostNativeAPI { public delegate void RotateDelegate(float degree); [DllImport("__Internal")] public static extern void sendUnityStateUpdate(string state); [DllImport("__Internal")] public static extern void setRotateDelegate(RotateDelegate rotateDelegate); }
UnityNativeAPI.csを追加します。
NativePluginから呼び出すメソッドはstaticではないとダメという制約があるためGameObjectを直接回転させるようなことはできません。そのため、回転リクエストがあったことをDelegateで飛ばすようにしています。
using System; using AOT; public static class UnityNativeAPI { public static Action<float> OnRotate; [MonoPInvokeCallback(typeof(HostNativeAPI.RotateDelegate))] public static void Rotate(float degree) { OnRotate?.Invoke(degree); } }
Manipulator.csというスクリプトを作ってこれをCubeにアタッチします。(※sendMessageToGOで作ったものとは別物だと思ってください)
using System; using UnityEngine; public class Manipulator: MonoBehaviour { private void Start() { if (Application.platform == RuntimePlatform.IPhonePlayer) { HostNativeAPI.setRotateDelegate(UnityNativeAPI.Rotate); UnityNativeAPI.OnRotate += Rotate; } } public void Rotate(float degree) { this.transform.Rotate(Vector3.up * degree); } private void OnDestroy() { UnityNativeAPI.OnRotate -= Rotate; } }
- ビルドします。
SwiftUIからCubeを回転させる
API.csに実装を追加する。
NativeCallsProxyでsetRotateDelegateを追加定義したため、これを実装する。
APIオブジェクトでUnityから渡されたメソッドをデリゲートに登録しておく。また、このメソッドをSwiftから呼び出せるようにrotateメソッドを定義する。
import Foundation import UnityFramework class API: NativeCallsProtocol { internal weak var bridge: UnityBridge! private var rotateCallback: RotateDelegate! internal func onUnityStateChange(_ state: String) { switch (state) { case "ready": self.bridge.unityGotReady() default: return } } internal func setRotateDelegate(_ delegate: RotateDelegate!) { self.rotateCallback = delegate } public func rotate(_ value: Float) { self.rotateCallback(value) } }
ContentView.swiftに回転させるボタンを追加する
struct ContentView: View { var body: some View { ZStack{ UnityView() HStack{ Button(action: { UnityBridge.getInstance().api.rotate(10) }){ Image(systemName: "rotate.right") .frame(width: 60, height: 60) .imageScale(.large) .background(Color.black) .foregroundColor(.white) .clipShape(Circle()) } Spacer() Button(action: { UnityBridge.getInstance().api.rotate(-10) }){ Image(systemName: "rotate.left") .frame(width: 60, height: 60) .imageScale(.large) .background(Color.black) .foregroundColor(.white) .clipShape(Circle()) } } } } }
- ビルドする
その1で紹介したsendMessageToGOと異なり、こちらの方法では文字列をパースしたりせず、型をそのままUnityとSwiftの双方で扱えるのが利点です。
まとめ
今回は、ミニマムでARKitとSwiftUIを組み合わせる例を紹介しました。 ただ、ミニマムとはいえ内容がまだまだお粗末なので、flutterとUnityを組み合わせたflutter-unity-view-widget をベースにSwiftUI版を作ってみようかなーとかなんとか考えています。
Unity as a Libraryと SwiftUI で作るARアプリ (前編)

- NOTE
- 登場要素
- Unity アプリの準備
- Swiftアプリの準備
- ワークスペースを作成する
- SwiftUI と Unity アプリを接続する
- ビルド
- (追記) Native-Plugin もSwiftで書く場合
- 次について
NOTE
記事について
- この記事は更新される可能性があります。
- 画像の更新コストが重いので、テキストベースで手順を記載しています。
- この記事は、ARFoundationとSwiftUIを組み合わせたいなーというモチベーションで書いていますが、ARFoundationは別になくても問題なく成立します。
元ネタについて
- この記事は、下記のブログポストのコードを分割したり日本語のコメントを入れて整理した内容です。詳しい内容はリンク先をご参照ください。 # Unity 2020 Integration With SwiftUI
- ブログでは書いていない細かい引っかかりなどは追記しています。
書いている人の知識について
- Objective-C や Swift は全然詳しくないです。
- 完全に理解したい方はこちらの記事がおすすめです。 # 【Unity】iOSネイティブプラグイン開発を完全に理解する
登場要素
- Unityプロジェクト
- Unityビルドした後に生成されるIL2CPPプロジェクト
- Swiftプロジェクト
- UnityビルトとSwiftプロジェクトを一緒にまとめるワークスペース
Unity アプリの準備
開発環境: Unity 2020.3
1. MyARApp (なんでもいい) でアプリを作ります
2. Build Settings > Switch Platform で iOS に切り替える
AR利用環境の設定
- Package Manager から
ARKit XR PluginとAR Foundaitonを入れる - Hierarchy から
MainCameraを削除し、ARSessionOriginとARSessionを作成する - Unity でレンダリングができているかを確認するために、Cube を (0, 0, 3) に作成・配置する
- Project Settings > XR Plug-in Management の
Plug-in Providers の ARKit にチェックを入れる
(Option)
このアプリをUnityアプリとしても使いたい場合は、
Project Settings > Player Settings > Other Settings > Camera Useage Description にfor ARKit (なんでもいい) を入れる。
Camera の許可設定はSwiftアプリの方で設定してあげる必要がある。
Swift との連携部の作成
Swift アプリからは Objective-C 経由でUnityアプリのメソッド等を呼んでもらう
NativeCallProxy.h は後にSwift アプリと情報をやり取りする場所になる。C# 的に言うならば interface みたいなもの。
// 重要: このファイルのターゲットメンバーシップに UnityFrameworkを設定し、パブリックヘッダーの可視性を設定する必要がある #import <Foundation/Foundation.h> // NativeCallsProtocol は iOS側からUnityメソッドに登録する型 @protocol NativeCallsProtocol @required - (void) onUnityStateChange:(const NSString*) state; // 上は一例 // このブロックにのメソッドを定義していく. @end __attribute__ ((visibility("default"))) @interface FrameworkLibAPI : NSObject // UnityFrameworkLoadの後に呼び出す // iOS側はこのメソッドから、デリゲートを登録する +(void) registerAPIforNativeCalls:(id<NativeCallsProtocol>) aApi; @end
- 同様に、
NativeCallProxy.mmを追加する
NativeCallProxy.h がインタフェースならば、NativeCallProxy.mm はその実装部となる。
#import <Foundation/Foundation.h> #import "NativeCallProxy.h" @implementation FrameworkLibAPI id<NativeCallsProtocol> api = NULL; +(void) registerAPIforNativeCalls:(id<NativeCallsProtocol>) aApi { api = aApi; } @end // Unity側からiOSのメソッドを伝達する部分 // apiに登録されているデリゲートを呼び出している extern "C" { void sendUnityStateUpdate(const char* state) { const NSString* str = @(state); [api onUnityStateChange: str]; } }
- Assets に
HostNativeAPI.csを追加する
Unity から Native Plugin を呼び出せるようにスクリプトを追加する。
using System.Runtime.InteropServices; public class HostNativeAPI { [DllImport("__Internal")] public static extern void sendUnityStateUpdate(string state); }
- Assets に
API.csを追加する
このスクリプトをHierarychy内の任意のGameObjectにアタッチする。後に出てくる iOS 側のプロジェクトは、Unityの起動タイミングを知る手段が必要となる。そこで、このスクリプトで"ready"メッセージを送っている。
using UnityEngine; public class API : MonoBehaviour { void Start() { #if UNITY_IOS if (Application.platform == RuntimePlatform.IPhonePlayer { HostNativeAPI.sendUnityStateUpdate("ready"); } #endif } }
Unity アプリのビルドとビルド後の設定
- Build Settings から Build する。 ここでは名前を
MyARAppExportとする。 - ビルドしたプロジェクトに生成される
Unity-iPhone.xcodeprojを開く。 - 画面左のプロジェクトの階層から、
Dataを選択し、画面右のインスペクタのTarget MembershipのUnityFrameworkにチェックを入れる。 - 続いてプロジェクトの階層から、
Libralies/Plugins/iOS/NativeCallProxy.hを選択し、Target MembershipのUnityFrameworkのprojectをpublicにする。public にすることで、Unity-iPhoneと別のプロジェクトからこのNativeCallProxyを呼び出せるようになる。 - 設定したらプロジェクトを閉じる。(開いたままだと、ワークスペースの追加時にエラーになる)
Swiftアプリの準備
- XCode から New > Project で
Appを選択する。名前はMyNativeApp(なんでもいい)とする。インタフェースはSwiftUI、ライフサイクルはSwiftUI Appを選択する。 - MyNativeApp に LaunchScreen を追加する。
- MyNativeApp/Info.plist の
Launch screen interface file base nameにLaunchScreenを指定する。 (Unity のロゴが表示できるようにSplash Screenを参照している模様) - ARKit を使う場合は、カメラの利用許可が必要となるため、 MyNativeApp/Info.plist に
Privacy - Camera Usage Descriptionを追加し、Value にはfor ARKit(なんでもいい) を入れる - 作成したら、プロジェクトを閉じる。(開いたままだと、ワークスペースの追加時にエラーになる)
ワークスペースを作成する
xcproj を一箇所にまとめておくと便利なのでワークスペースを作って、そこに作成したUnityビルドとSwiftアプリを統合する。
- XCode から New > Workspace で Workspace を作成する。名前は
MyWorkspaceとする。 - File > Add Files... から、
MyAppExport/Unity-iPhone.xcodeprojを追加する。MyNativeApp/MyNativeApp.xcodeprojも同様についかする。
SwiftUI と Unity アプリを接続する
フレームワークの追加
Unityのビルド物に含まれるUnityFrameworkを通して、Swiftはコミュニケーションを行う。そのため、下記の手順でSwiftプロジェクトにUnityFrameworkを追加する。
- 画面左のプロジェクト一覧の階層からMyNativeAppを選択し、Targets から MyNativeApp を選択する。
- General タブに切り替え、
Frameworks, Libralies, and Embedded Contentの+ボタンを押して、Workspace/Unity-iPhone/UnityFramework.frameworkを追加する。
NativeCallProxy のインポート
- 利用するライブラリを定義する NativeCallProxy-Bridging-Header.h を追加する。
NativeCallProxy-Bridging-Header
#ifndef APISwiftBridge_h #define APISwiftBridge_h // Swiftからobjective-c のAPIを呼べるようにする #include <UnityFramework/NativeCallProxy.h> #endif /* APISwiftBridge_h */
- 前項目と同様に、MyNativeApp を選択し、Targets から MyNativeApp を選択する。
- Build Settings のタブに切り替え、
Swift Compiler - GeneralのObjective-C Biding HeadersにNativeCallProxy-Bridging-Header.hを追加する。
コールバックを定義する
- API.swift を追加する。(UnityBridgeは後述)
API は Unity側で定義したNativeCallsProtocolの実装。ここで定義したメソッドが、registerAPIforNativeCalls 経由で登録される。
先ほど、起動時にreadyを呼ぶように設定した部分は、ここで呼び出される。
import Foundation import UnityFramework class API: NativeCallsProtocol { internal weak var bridge: UnityBridge! // Unityからのメッセージを受け取るコールバックメソッド // UnityBridge内で登録する internal func onUnityStateChange(_ state: String) { switch (state) { case "ready": // Unityの画面が呼び出し可能になったことを伝える self.bridge.unityGotReady() default: return } } }
- UnityBridge.swift を追加する。
UnityBridge は、Unity の起動やアンロードといったライフサイクルの操作。また、上で作ったメソッドをregisterAPIforNativeCallsを通じてswiftで受け取りたいUnityからの情報を取得するメソッドをデリゲートに登録する。
import Foundation import UnityFramework /// Unityとネイティブアプリを繋ぐシングルトンクラス /// Unityフレームワークの初期化・読み込みを行える class UnityBridge: UIResponder, UIApplicationDelegate, UnityFrameworkListener { public internal(set) var isReady: Bool = false public var api: API public var onReady: () -> Void = {} private static var instance: UnityBridge? /// UnityFramework instance private let ufw: UnityFramework /// UnityFramework root view public var view: UIView? { ufw.appController()?.rootView } public static func getInstance() -> UnityBridge { if UnityBridge.instance == nil { UnityBridge.instance = UnityBridge() } return UnityBridge.instance! } /// bundleパスからUnityFrameworkを読み込む /// /// - Returns: The UnityFramework instance private static func loadUnityFramework() -> UnityFramework? { let bundlePath: String = Bundle.main.bundlePath + "/Frameworks/UnityFramework.framework" let bundle = Bundle(path: bundlePath) if bundle?.isLoaded == false { bundle?.load() } let ufw = bundle?.principalClass?.getInstance() if ufw?.appController() == nil { let machineHeader = UnsafeMutablePointer<MachHeader>.allocate(capacity: 1) machineHeader.pointee = _mh_execute_header ufw!.setExecuteHeader(machineHeader) } return ufw } internal override init() { self.ufw = UnityBridge.loadUnityFramework()! self.ufw.setDataBundleId("com.unity3d.framework") self.api = API() super.init() self.api.bridge = self self.ufw.register(self) NSClassFromString("FrameworkLibAPI")?.registerAPIforNativeCalls(self.api) //FrameworkLibAPI.registerAPIforNativeCalls(self.api) ufw.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: nil) } public func show(controller: UIViewController) { if self.isReady { self.ufw.showUnityWindow() } if let view = self.view { controller.view?.addSubview(view) } } public func unload() { self.ufw.unloadApplication() } internal func unityGotReady() { self.isReady = true onReady() } /// フレームワークがアンロードされた時に `UnityFrameworkListener`を通してUnityにトリガーされます。 internal func unityDidUnload(_: Notification!) { ufw.unregisterFrameworkListener(self) UnityBridge.instance = nil } } //#endif
View に Unity を組み込む
- MyNativeApp に SwiftUIファイルを追加する。
Unityの画面自体はUIKitのView形式で生成されるので UIViewControllerRepresentable を実装して SwiftUI に変換する。
import UIKit import SwiftUI // UIViewControllerRepresentableを実装すると、UIKitのViewをSwiftUIのViewとして返すことができる struct UnityView: UIViewControllerRepresentable { func makeUIViewController(context _: Context) -> UIViewController { let vc = UIViewController() let unity = UnityBridge.getInstance() // Unityが呼び出し可能になったらUnity画面を表示する UnityBridge.getInstance().onReady = { UnityBridge.getInstance().show(controller: vc) } return vc } func updateUIViewController(_: UIViewController, context _: Context) { // Empty. } }
- UnityView を ContentView に組み込む
import SwiftUI struct ContentView: View { var body: some View { // UnityのViewにSwiftUIのテキストを重畳している UnityView() Text("Hello, world!") .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
ビルド
あとはビルドしたらアプリが開始されます!
(追記) Native-Plugin もSwiftで書く場合
headerファイルのプロトコル定義やFrameworkLibAPIの記述をswiftで置き換えることもできます。
Unity側の変更
- NativeCallsProxy.h を swift で置き換える
Assets/Plugins/iOS/NativeCallsProxy.h を 下記のswiftで置き換えます。
(参考) [# Swift class with only class methods and a delegate?]
NativeCallsProxy.swift
import Foundation @objc public protocol NativeCallsProtocol { // Unityからのステートの変更をSwiftUIへ通知する. func onUnityStateChange(_ state:String) } public class FrameworkLibAPI: NSObject{ @objc public static weak var api: NativeCallsProtocol? = nil @objc public static func registerAPIforNativeCalls(_ aAPI: NativeCallsProtocol?){ api = aAPI } }
- NativeCallsProxy.mm を一部変更する
Swiftの方でクラスを実装しているため、Objective-C++で実装していた部分を除去しています。
#import <UnityFramework/UnityFramework-Swift.h> extern "C" { void sendUnityStateUpdate(const char* state) { NSString * const str = @(state); [FrameworkLibAPI.api onUnityStateChange:str]; } }
- Assets/Editorの下にXcodePostProcessにSwiftを利用するためにビルド後の設定を行ってくれるファイルを追加する
(参考) 【Unity】iOSネイティブプラグインをSwiftで実装する際には、2019.3前後で設定方法が変わる
using System.IO; using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.iOS.Xcode; sealed class XcodePostProcess { [PostProcessBuild] static void OnPostProcessBuild(BuildTarget target, string path) { if (target != BuildTarget.iOS) return; var projectPath = PBXProject.GetPBXProjectPath(path); var project = new PBXProject(); project.ReadFromString(File.ReadAllText(projectPath)); var targetGuid = project.GetUnityFrameworkTargetGuid(); project.AddBuildProperty(targetGuid, "SWIFT_VERSION", "5.0"); File.WriteAllText(projectPath, project.WriteToString()); } }
SwiftUI側の変更
Protocolの定義が変わったので、NSStringからStringに変更します。
API.swift
internal func onUnityStateChange(state: String) { ・・・ }
- NativeCallsProxy-Bridging-Header.h から不要な部分を削除する
#ifndef APISwiftBridge_h #define APISwiftBridge_h // NativeCallProxy.hはもうないので、下記の部分を削除数する // #include <UnityFramework/NativeCallProxy.h> #endif /* APISwiftBridge_h */
あとはビルドするだけです。
次について
今回は、最低限重畳するところまででしたが次回はSwiftのUIからUnityのオブジェクトを操作するところを書こうかなと考えています。(いつ書き終わるかは未定)
2020年振り返り
毎年恒例の振り返りです
去年のブログの振り返りから
デザイン
あんましエンジニア(or専門外)だから〜とかで食わず嫌いせず、まずはやってみるというのを今年はより積極的にやっていくつもりです。
記憶にないw けどまあ今年はOOUIの本を読んで以降ずっとこの話をしていた気がします。(たぶん社でもまたこいつ同じ話してるよ、、、と思われていたに違いない)

オブジェクト指向UIデザイン──使いやすいソフトウェアの原理 WEB+DB PRESS plus
- 作者:ソシオメディア株式会社,上野 学,藤井 幸多
- 発売日: 2020/06/05
- メディア: Kindle版
何をする機能かではなく、「構成する要素はなにか」「どうあるか」「どうふるまうか」で考えて言語化しようというのも繰り返し言ってた気がします。
まあでもメンタルモデルをプログラムに落とすのはまた別の技術なので、そこの課題は残したまま来年は改めて向かい直す旅かなーと思う次第。OOUI is Justice かはまだわからんですが、また一周回って悟ったりすると思うのでそういうのも醍醐味ですよねエンジニア人生。
設計
HoloLens 2 からはいっそう、業務用途に振り切った背景もあり、Domain Driven Design に片足突っ込んでいたのが昨年。といっても、業務知識が弱く戦術的な実装に引きずられてしまったのが反省点です。モデリングがまだまだ下手なので、継続的に向き合う課題です。
LightweightDDDのようなCleanのような作りになったんですが、まどろっこしいのと無駄インターフェースが生えまくったので今年最大の反省of反省。 単方向にしたいモチベはわかりつつも、もう少し軽量な実装はできると思うんですけどね。これも来年の課題。
そういえばGUI周りはUIToolkitの登場もあって作業の分離のしやすさやPrefab地獄からの脱却できるんですかね。UnityAsLibraryという選択肢もありますが、チャレンジング項目ですね。(ちゃんとリリースされればですが)
組織
一方、会社方面は人が増えて、人間・チーム・心理的安全性なんもわからん…な年でした。その中でもあれやってみよう、これやってみようと少しずつ取り組みが始まるも、全体的に意思決定力が弱かったのが反省点です。なので今年は、主張と決定がテーマです。
スーパースター型(?)組織なのかはわからんのですが、個性派ぞろいなのであんまり踏み込んでこなかったのですが、今年は思い切ってOOUIの輪読会を開きました。結果的に好評だったのでよかったです。えらい。(過激派だのなんだの言われるようになったのは気のせい)
そしてこのご時世なのに、人が更に増えました。景気が良い。
優秀な腕のエンジニア、さらにお待ちしております。(育てられてぇ。我ながら可愛気がないのが良くないんでしょうね。。。)
HoloLensアプリの開発
今年は5%くらいなんじゃないでしょうか。 GUIをどう作るかってところ以外はPCやiPadとそうは変わらんだろってな感じであんまり気にしてなかったり。(申し訳ないという話をされたけど当の本人はWebやりてぇなぁとか言っていたり)
まだ開発プロセス周りの学びが足りない
HoloLensは関係なく、ここが課題なので来年こそは。。。
プライベート
身体的にも精神的にも無理がきかなくなって、途中何度かガタがきてしまった。 27歳、もう若くない、なんていうといろんな人から怒られそうですが、徹夜パワーはもうないですw
というわけで今年は、身の回りに投資しました。
クルーズ: 22万 www.okamura.co.jp
バロンチェア: 14万 item.rakuten.co.jp
43インチモニタ: 6万

LG モニター ディスプレイ 43UN700-B 42.5インチ/4K/HDR対応/IPS非光沢/HDMI×4,DP,USB Type-C/スピーカー/ブルーライト低減、フリッカーセーフ/リモコン付属
- 発売日: 2019/11/15
- メディア: Personal Computers
30代でリタイアしてーなー。(ボソッ
副業とかも考えたいっすね
来年の抱負
そんなこんなで来年の抱負は、
今まで避けてきたことと向き合う
っていうのが大目標です。
他の開発環境を学ぼうってのは、今の開発に新しい考え方を取り入れたいなあってのがモチベです。
今年は成長を実感できるような大きな転換もなく、来月で28歳を迎えるのでそこそこに焦り候
OpenXR Plugin for Unity を HoloLens で試す
UnityのOpenXRサポートのPreview版が公開され、HoloLensのサンプルが公開されていたので動かしました (中身は全然追いかけていないです。動かしただけ。)
セットアップ
Edit > Project Settings > Package Manager から Scoped Registries を登録します
- Name : Microsoft Mixed Reality
- URL : https://pkgs.dev.azure.com/aipmr/MixedReality-Unity-Packages/_packaging/Unity-packages/npm/registry/
- Scope(s) : com.microsoft.mixedreality
Enable Preview Package にチェックをいれます
今回はめんどいのでGUIから入れちゃいます。
Windos -> PackageManger->add package from git url
com.microsoft.mixedreality.openxr
- Projecte Settings -> XR Plugin -Management から
- OpenXR
- Microsoft HoloLens feature set
にチェックをいれます Fix のサジェスチョンがでるので、Fix All で修正します

Microsoft HoloLens feature set はどうやら下の階層のHoloLensに必要なパラメータを自動セットしてくれるみたいです

サンプル
Package Manager からサンプルシーンをインポートします

サンプルがどうやらARFoundationに依存しているようなので、ARFoundationをインポートします


あとは普通にビルドします
うごいた #Unity #OpenXR #HoloLens https://t.co/zvOz91OloX pic.twitter.com/rePN9J9teI
— Decoc (@deco_c_) 2020年12月17日
テキストエディタを使わずGUIからMRTKをUPMインポートする
MRTKは2.5よりUPMサポートが入り、パッケージのバージョン管理がしやすくなりました.
UPMはドキュメントにある通り、VSCodeなどのテキストエディタでScoped Registories や Dependencies の登録をします。
が、こちらはテキストエディタだけではなくGUIからも登録することができます。
Scoped Registories の登録
Project Settings の Package Manager で Scoped Registories の登録ができます。
{ "scopedRegistries": [ { "name": "Microsoft Mixed Reality", "url": "https://pkgs.dev.azure.com/aipmr/MixedReality-Unity-Packages/_packaging/Unity-packages/npm/registry/", "scopes": [ "com.microsoft.mixedreality", "com.microsoft.spatialaudio" ] } ],
上記のname, url, scopes は下図のような対応になります。

Dependencies の登録
Dependenciesについても同様です。
Window -> Package Manager -> +ボタン -> Add package from git url....


のURL欄に下記を入れます。
com.microsoft.mixedreality.toolkit.foundation
以上です!
HoloLensで片目しか描画されないとき
観測範囲内でこの現象にハマる人が多いようです
結論から言うと、 Single Pass Instancing に対応していない Shader をつかっているがためです.
Single Pass Instancing とは?
Single Pass Instancing とは、左右それぞれのディスプレイに効率よくレンダリングする技術の1つです。Multi Pass に比べ、パフォーマンスが良いのが利点です。仕組みをより詳しく知りたい方は、下記のリンク先がオススメです。

Unity では、XR Settings -> Stereo Rendering Mode でレンダリング手法を切り替えることができます。MRTKでは気を利かせてダイアログで有効にしてくれているので、気づかない人も多いのかな?という印象です。

MRTKの設定
Single Pass Instancing と Shader
先の記事内でも紹介されているように、Single Pass Instancing を使う場合は、対応するShaderを用意する必要 があります。購入したアセットがSingle Pass Instancing に対応していなかったり、UnityChanShaderを使うと、記事の題名のようにHoloLensで片目しか表示されないということになってしまいます。
HoloLens のシングルパスステレオレンダリング - Unity マニュアル
MRTK のShaderは、Single Pass Instancing に対応しているので問題なく利用できます。
MRTKの解説
さらっと書きましたがMRTKではこの辺りをもう少し詳しく書いてあるので、興味がある人は読んでみるとよいでしょう。
Performance | Mixed Reality Toolkit Documentation
UnityのXRのデフォルトのレンダリング設定は、マルチパスです。この設定では、Unity がレンダリング パイプライン全体を 2 回、各アイに対して 1 回実行するように指示します。これは、代わりにシングルパス インスタンスレンダリングを選択することで最適化することができます。この設定では、レンダーターゲットの配列を利用して、各アイに適したレンダーターゲットにインスタンス化する1回の描画呼び出しを実行できるようにします。さらに、このモードでは、すべてのレンダリングをレンダリング パイプラインの 1 回の実行で行うことができます。このように、ミックスリアリティアプリケーションのレンダリングパスとしてシングルパスインスタンスレンダリングを選択すると、CPUとGPUの両方で大幅に時間を節約することができ、推奨されるレンダリング構成となります。
しかし、各メッシュに対して各目に単一の描画コールを発行するためには、すべてのシェーダでGPUインスタンス化がサポートされている必要があります。インスタンス化により、GPUは両目にまたがって描画コールを多重化することができます。UnityのビルトインシェーダとMRTKスタンダードシェーダはデフォルトではシェーダコードに必要なインスタンシング命令が含まれています。ただし、Unity用にカスタムシェーダーを書く場合は、これらのシェーダーを更新してシングルパスのイン スタンスレンダリングをサポートする必要があるかもしれません。 www.DeepL.com/Translator(無料版)で翻訳しました。
まとめ
- Single Pass Instancing を使いたい場合は、MRTKのStandardShaderを使う、あるいはShaderを Single Pass Instancingに対応させる
- ShaderのSingle Pass Instancing 対応がどうしても困難な場合は、パフォーマンスを諦めて Multi Pass を使う
HoloLensアプリは必ずパッケージ名を指定しよう
HoloLensアプリを作る際は、必ずパッケージ名を指定しましょう。
デフォルトでは「Template3D」が指定されており、他にHoloLensでTemplate3Dをパッケージ名としてインストールされているアプリがあるとアプリを上書きしてしまったり、配置に失敗するケースがあります。
パッケージ名は、Project Settings -> Player -> Publishing Settingsから設定することができます。

また、Unityが書き出したプロジェクトからもこの名前は変更することができます。
Package.appmanifest->パッケージ化からパッケージ名を変更することができます。

