デコシノニッキ

ホロレンジャーの戦いの記録

Unity as a Libraryと SwiftUI で作るARアプリ (後編)

この記事は、前編の続きになります。

この記事のゴールは、SwiftUIからUnityオブジェクトを操作することがターゲットになります。 具体的にはSwiftUIでCubeを10度ずつ回せるようにします。

とはいえ、普通のNativePluginを使った開発と変わりはないです。sendUnityMessageToGOdelegateを使った2つの方法をここでは紹介します。

(その1)sendUnityMessageToGOを使ったやり方

Cubeを回転させるためのスクリプトを作成する

  1. 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);
        }
    }
}
  1. Unityプロジェクトをビルドします。

SwiftUIからCubeを回転させる

  1. UnityBridge.swiftsendMessageToGOをラップしたメソッドを追加する
    /// 指定のGame Object で呼び出し可能なメソッドを呼び出す
    internal func sendMessageToGO(withName: String, functionName: String, message: String){
        ufw.sendMessageToGO(
                withName: withName,
                functionName: functionName,
                message: message)
    }
  1. 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())
                }
            }
        }
    }
}
  1. ビルドする

これだけです! あとは実行画面に追加されたボタンを押すと、Cubeが回転してくれるはずです。

(その2) NativePluginを使ったやり方

sendMessageToGOは組むのは簡単ですが、同名メソッドを避ける必要やstringでしか値を渡せない点が不便です。 そこでDelegateを使ったメッセージをやり取りする方法がブログでは紹介されています。

Delegateを定義する

  1. NativeCallsProxy.hにfloat値を受け取りたいので次のようなDelegateを定義します。
#import <Foundation/Foundation.h>

// float値を受け取ってvoidを返すDelegate
typedef void (*RotateDelegate)(const float degree);
  1. 定義した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
  1. 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のメソッドを呼び出せるようにする

  1. 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);
}
  1. 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);
    }
}
  1. 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;
    }
}
  1. ビルドします。

SwiftUIからCubeを回転させる

  1. 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)
    }
}
  1. 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. ビルドする

その1で紹介したsendMessageToGOと異なり、こちらの方法では文字列をパースしたりせず、型をそのままUnityとSwiftの双方で扱えるのが利点です。

まとめ

今回は、ミニマムでARKitとSwiftUIを組み合わせる例を紹介しました。 ただ、ミニマムとはいえ内容がまだまだお粗末なので、flutterとUnityを組み合わせたflutter-unity-view-widget をベースにSwiftUI版を作ってみようかなーとかなんとか考えています。

Unity as a Libraryと SwiftUI で作るARアプリ (前編)

f:id:haikage1755:20210625234251p:plain

!Image from iOS.jpg

NOTE

記事について

  • この記事は更新される可能性があります。
  • 画像の更新コストが重いので、テキストベースで手順を記載しています。
  • この記事は、ARFoundationとSwiftUIを組み合わせたいなーというモチベーションで書いていますが、ARFoundationは別になくても問題なく成立します。

元ネタについて

  • この記事は、下記のブログポストのコードを分割したり日本語のコメントを入れて整理した内容です。詳しい内容はリンク先をご参照ください。 # Unity 2020 Integration With SwiftUI
  • ブログでは書いていない細かい引っかかりなどは追記しています。

書いている人の知識について

登場要素

  1. Unityプロジェクト
  2. Unityビルドした後に生成されるIL2CPPプロジェクト
  3. Swiftプロジェクト
  4. UnityビルトとSwiftプロジェクトを一緒にまとめるワークスペース

Unity アプリの準備

開発環境: Unity 2020.3 1. MyARApp (なんでもいい) でアプリを作ります 2. Build Settings > Switch Platform で iOS に切り替える

AR利用環境の設定

  1. Package Manager から ARKit XR PluginAR Foundaiton を入れる
  2. Hierarchy からMainCameraを削除し、 ARSessionOriginARSession を作成する
  3. Unity でレンダリングができているかを確認するために、Cube を (0, 0, 3) に作成・配置する
  4. 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アプリのメソッド等を呼んでもらう

  1. Assets/Plugins/iOS ディレクトリを作る
  2. NativeCallProxy.hディレクトリに追加する

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
  1. 同様に、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];
  }
}
  1. Assets に HostNativeAPI.cs を追加する

Unity から Native Plugin を呼び出せるようにスクリプトを追加する。

using System.Runtime.InteropServices;

public class HostNativeAPI 
{
    [DllImport("__Internal")]
    public static extern void sendUnityStateUpdate(string state);
}
  1. 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 アプリのビルドとビルド後の設定

  1. Build Settings から Build する。 ここでは名前をMyARAppExport とする。
  2. ビルドしたプロジェクトに生成される Unity-iPhone.xcodeproj を開く。
  3. 画面左のプロジェクトの階層から、Dataを選択し、画面右のインスペクタの Target Membership の UnityFramework にチェックを入れる。
  4. 続いてプロジェクトの階層から、Libralies/Plugins/iOS/NativeCallProxy.hを選択し、Target MembershipUnityFrameworkprojectpublicにする。public にすることで、Unity-iPhoneと別のプロジェクトからこのNativeCallProxyを呼び出せるようになる。
  5. 設定したらプロジェクトを閉じる。(開いたままだと、ワークスペースの追加時にエラーになる)

Swiftアプリの準備

  1. XCode から New > Project で App を選択する。名前はMyNativeApp(なんでもいい)とする。インタフェースはSwiftUI、ライフサイクルはSwiftUI Appを選択する。
  2. MyNativeApp に LaunchScreen を追加する。
  3. MyNativeApp/Info.plist の Launch screen interface file base nameLaunchScreen を指定する。 (Unity のロゴが表示できるようにSplash Screenを参照している模様)
  4. ARKit を使う場合は、カメラの利用許可が必要となるため、 MyNativeApp/Info.plist に Privacy - Camera Usage Description を追加し、Value には for ARKit (なんでもいい) を入れる
  5. 作成したら、プロジェクトを閉じる。(開いたままだと、ワークスペースの追加時にエラーになる)

ワークスペースを作成する

xcproj を一箇所にまとめておくと便利なのでワークスペースを作って、そこに作成したUnityビルドとSwiftアプリを統合する。

  1. XCode から New > Workspace で Workspace を作成する。名前はMyWorkspace とする。
  2. File > Add Files... から、MyAppExport/Unity-iPhone.xcodeprojを追加する。MyNativeApp/MyNativeApp.xcodeprojも同様についかする。

SwiftUI と Unity アプリを接続する

フレームワークの追加

Unityのビルド物に含まれるUnityFrameworkを通して、Swiftはコミュニケーションを行う。そのため、下記の手順でSwiftプロジェクトにUnityFrameworkを追加する。

  1. 画面左のプロジェクト一覧の階層からMyNativeAppを選択し、Targets から MyNativeApp を選択する。
  2. General タブに切り替え、Frameworks, Libralies, and Embedded Content+ボタン を押して、Workspace/Unity-iPhone/UnityFramework.frameworkを追加する。

NativeCallProxy のインポート

  1. 利用するライブラリを定義する NativeCallProxy-Bridging-Header.h を追加する。

NativeCallProxy-Bridging-Header

#ifndef APISwiftBridge_h
#define APISwiftBridge_h

// Swiftからobjective-c のAPIを呼べるようにする
#include <UnityFramework/NativeCallProxy.h>

#endif /* APISwiftBridge_h */
  1. 前項目と同様に、MyNativeApp を選択し、Targets から MyNativeApp を選択する。
  2. Build Settings のタブに切り替え、Swift Compiler - GeneralObjective-C Biding HeadersNativeCallProxy-Bridging-Header.h を追加する。

コールバックを定義する

  1. 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
        }
    }
}
  1. 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 を組み込む

  1. 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.
    }
}
  1. 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側の変更

  1. 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
    }
}
  1. 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];
    }
}
  1. 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側の変更

  1. API.swift をプロトコルの型に合わせて変更する

Protocolの定義が変わったので、NSStringからStringに変更します。

API.swift

    internal func onUnityStateChange(state: String) {
    ・・・
}
  1. NativeCallsProxy-Bridging-Header.h から不要な部分を削除する
#ifndef APISwiftBridge_h
#define APISwiftBridge_h

// NativeCallProxy.hはもうないので、下記の部分を削除数する
// #include <UnityFramework/NativeCallProxy.h>

#endif /* APISwiftBridge_h */

あとはビルドするだけです。

次について

今回は、最低限重畳するところまででしたが次回はSwiftのUIからUnityのオブジェクトを操作するところを書こうかなと考えています。(いつ書き終わるかは未定)

2020年振り返り

毎年恒例の振り返りです

去年のブログの振り返りから

www.tattichan.work

デザイン

あんましエンジニア(or専門外)だから〜とかで食わず嫌いせず、まずはやってみるというのを今年はより積極的にやっていくつもりです。

記憶にないw けどまあ今年はOOUIの本を読んで以降ずっとこの話をしていた気がします。(たぶん社でもまたこいつ同じ話してるよ、、、と思われていたに違いない)

何をする機能かではなく、「構成する要素はなにか」「どうあるか」「どうふるまうか」で考えて言語化しようというのも繰り返し言ってた気がします。

まあでもメンタルモデルをプログラムに落とすのはまた別の技術なので、そこの課題は残したまま来年は改めて向かい直す旅かなーと思う次第。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万

30代でリタイアしてーなー。(ボソッ

副業とかも考えたいっすね

来年の抱負

そんなこんなで来年の抱負は、

今まで避けてきたことと向き合う

っていうのが大目標です。

  • Webクラインアント・バックエンド(Unity&C#以外の開発環境)
  • 開発プロセスの改善

他の開発環境を学ぼうってのは、今の開発に新しい考え方を取り入れたいなあってのがモチベです。

今年は成長を実感できるような大きな転換もなく、来月で28歳を迎えるのでそこそこに焦り候

OpenXR Plugin for Unity を HoloLens で試す

UnityのOpenXRサポートのPreview版が公開され、HoloLensのサンプルが公開されていたので動かしました (中身は全然追いかけていないです。動かしただけ。)

t.co

セットアップ

  1. Edit > Project Settings > Package Manager から Scoped Registries を登録します

  2. Enable Preview Package にチェックをいれます

  3. 今回はめんどいのでGUIから入れちゃいます。

Windos -> PackageManger->add package from git url

com.microsoft.mixedreality.openxr

www.tattichan.work

  1. Projecte Settings -> XR Plugin -Management から
  2. OpenXR

にチェックをいれます Fix のサジェスチョンがでるので、Fix All で修正します

f:id:haikage1755:20201217122012p:plain:w250

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

サンプル

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

f:id:haikage1755:20201217122447p:plain:w250

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

f:id:haikage1755:20201217122558p:plain:w250

f:id:haikage1755:20201217122702p:plain:w250

あとは普通にビルドします 

テキストエディタを使わずGUIからMRTKをUPMインポートする

MRTKは2.5よりUPMサポートが入り、パッケージのバージョン管理がしやすくなりました.

UPMはドキュメントにある通り、VSCodeなどのテキストエディタでScoped Registories や Dependencies の登録をします。

microsoft.github.io

が、こちらはテキストエディタだけではなく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 は下図のような対応になります。

f:id:haikage1755:20201216115306p:plain:w250

Dependencies の登録

Dependenciesについても同様です。

Window -> Package Manager -> +ボタン -> Add package from git url....

f:id:haikage1755:20201216115708p:plain:w250

f:id:haikage1755:20201216115807p:plain:w250

のURL欄に下記を入れます。

com.microsoft.mixedreality.toolkit.foundation 

以上です!

HoloLensで片目しか描画されないとき

観測範囲内でこの現象にハマる人が多いようです

結論から言うと、 Single Pass Instancing に対応していない Shader をつかっているがためです.

Single Pass Instancing とは?

Single Pass Instancing とは、左右それぞれのディスプレイに効率よくレンダリングする技術の1つです。Multi Pass に比べ、パフォーマンスが良いのが利点です。仕組みをより詳しく知りたい方は、下記のリンク先がオススメです。

tips.hecomi.com

f:id:haikage1755:20200814213958p:plain:w450

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

f:id:haikage1755:20200814215635p:plain:w450
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をパッケージ名としてインストールされているアプリがあるとアプリを上書きしてしまったり、配置に失敗するケースがあります。

githubより抜粋 f:id:haikage1755:20200705192034p:plain

パッケージ名は、Project Settings -> Player -> Publishing Settingsから設定することができます。

f:id:haikage1755:20200705190523p:plain

また、Unityが書き出したプロジェクトからもこの名前は変更することができます。

Package.appmanifest->パッケージ化からパッケージ名を変更することができます。

f:id:haikage1755:20200705191127p:plain

参考

qiita.com

[デコシノニッキ]は、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、Amazonアソシエイト・プログラムの参加者です。」