e.blog

主にUnity/UE周りのことについてまとめていきます

Unityが起動しなくなってあれこれしたことを殴り書きしておく

概要

ある日突然、Unityが死んだ。

シーン読み込み時にフリーズして再起動したら、Unity自体が起動しなくなるという自体に遭遇。
結果的に4時間溶かしてなんとか解決した・・・。

ただ、正直謎が多すぎてこれで直るか分からないのだけど、同じ悩みを持っている人を助けられるかもしれないのでメモを残しておきます。

やったこと

ちなみにそのときに一番調べていた単語が「Native extension for iOS target not found」です。
これは、Unity Editorのログファイルに残されていた手がかりです。
(ちなみにログファイルはここ→ ~/Library/Logs/Unity/Editor.log

そのときのログファイル全文を残しておきます。

Initiating legacy licensing module
[Package Manager] Server::Start -- Port 61119 was selected
Launching external process: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/Server/UnityPackageManager

 COMMAND LINE ARGUMENTS:
/Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/MacOS/Unity
-projectpath
/Users/kazuyahiruma/MyDesktop/UnityProjects/hoge
-useHub
-hubIPC
-cloudEnvironment
production
-buildTarget
iOS
-hubSessionId
529ac9d0-f0ba-11e9-ba43-8bfa11da91e7

LICENSE SYSTEM [20191017 17:48:0] Next license update check is after 2019-10-18T07:36:42

Successfully changed project path to: /Users/kazuyahiruma/MyDesktop/UnityProjects/hoge
/Users/kazuyahiruma/MyDesktop/UnityProjects/hoge
Using Asset Import Pipeline V1.
Loading GUID <-> Path mappings...0.000048 seconds
Loading Asset Database...0.122003 seconds
AssetDatabase consistency checks...0.722986 seconds
[Package Manager] Done resolving packages in 1.14s seconds
[Package Manager] 
Registered 40 packages:
  Packages from [https://packages.unity.com]:
    com.unity.collab-proxy@1.2.16 (location: /Users/kazuyahiruma/MyDesktop/UnityProjects/hoge/Library/PackageCache/com.unity.collab-proxy@1.2.16)
    com.unity.ext.nunit@1.0.0 (location: /Users/kazuyahiruma/MyDesktop/UnityProjects/hoge/Library/PackageCache/com.unity.ext.nunit@1.0.0)
    com.unity.ide.rider@1.1.0 (location: /Users/kazuyahiruma/MyDesktop/UnityProjects/hoge/Library/PackageCache/com.unity.ide.rider@1.1.0)
    com.unity.ide.vscode@1.1.0 (location: /Users/kazuyahiruma/MyDesktop/UnityProjects/hoge/Library/PackageCache/com.unity.ide.vscode@1.1.0)
    com.unity.test-framework@1.0.13 (location: /Users/kazuyahiruma/MyDesktop/UnityProjects/hoge/Library/PackageCache/com.unity.test-framework@1.0.13)
    com.unity.textmeshpro@2.0.1 (location: /Users/kazuyahiruma/MyDesktop/UnityProjects/hoge/Library/PackageCache/com.unity.textmeshpro@2.0.1)
  Built-in packages:
    com.unity.package-manager-ui@2.2.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.package-manager-ui)
    com.unity.timeline@1.1.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.timeline)
    com.unity.ugui@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.ugui)
    com.unity.modules.ai@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.ai)
    com.unity.modules.androidjni@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.androidjni)
    com.unity.modules.animation@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.animation)
    com.unity.modules.assetbundle@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.assetbundle)
    com.unity.modules.audio@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.audio)
    com.unity.modules.cloth@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.cloth)
    com.unity.modules.director@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.director)
    com.unity.modules.imageconversion@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.imageconversion)
    com.unity.modules.imgui@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.imgui)
    com.unity.modules.jsonserialize@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.jsonserialize)
    com.unity.modules.particlesystem@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.particlesystem)
    com.unity.modules.physics@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.physics)
    com.unity.modules.physics2d@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.physics2d)
    com.unity.modules.screencapture@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.screencapture)
    com.unity.modules.terrain@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.terrain)
    com.unity.modules.terrainphysics@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.terrainphysics)
    com.unity.modules.tilemap@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.tilemap)
    com.unity.modules.ui@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.ui)
    com.unity.modules.uielements@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.uielements)
    com.unity.modules.umbra@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.umbra)
    com.unity.modules.unityanalytics@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.unityanalytics)
    com.unity.modules.unitywebrequest@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.unitywebrequest)
    com.unity.modules.unitywebrequestassetbundle@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.unitywebrequestassetbundle)
    com.unity.modules.unitywebrequestaudio@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.unitywebrequestaudio)
    com.unity.modules.unitywebrequesttexture@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.unitywebrequesttexture)
    com.unity.modules.unitywebrequestwww@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.unitywebrequestwww)
    com.unity.modules.vehicles@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.vehicles)
    com.unity.modules.video@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.video)
    com.unity.modules.vr@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.vr)
    com.unity.modules.wind@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.wind)
    com.unity.modules.xr@1.0.0 (location: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/PackageManager/BuiltInPackages/com.unity.modules.xr)

[XR] No new subsystems found in resolved package list.
[Package Manager] Done registering packages in 0.05s seconds
Targeting platform: iOS
Refreshing native plugins compatible for Editor in 206.28 ms, found 6 plugins.
Preloading 0 native plugins for Editor in 0.00 ms.
IsTimeToCheckForNewEditor: Update time 1571299758 current 1571302083
Initialize engine version: 2019.2.3f1 (8e55c27a4621)
[XR] Discovering subsystems at path /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Resources/UnitySubsystems
[XR] Discovering subsystems at path /Users/kazuyahiruma/MyDesktop/UnityProjects/hoge/Assets
GfxDevice: creating device client; threaded=1
Initializing Metal device caps: Intel(R) Iris(TM) Plus Graphics 640
[EnlightenBakeManager] m_Clear = false;
Initialize mono
Mono path[0] = '/Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/Managed'
Mono path[1] = '/Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/MonoBleedingEdge/lib/mono/unityjit'
Mono config path = '/Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/MonoBleedingEdge/etc'
Using monoOptions --debugger-agent=transport=dt_socket,embedding=1,server=y,suspend=n,address=127.0.0.1:56155
Begin MonoManager ReloadAssembly
Registering precompiled unity dll's ...
Register platform support module: /Applications/Unity/Hub/Editor/2019.2.3f1/Unity.app/Contents/PlaybackEngines/MacStandaloneSupport/UnityEditor.OSXStandalone.Extensions.dll
Register platform support module: /Applications/Unity/Hub/Editor/2019.2.3f1/PlaybackEngines/iOSSupport/UnityEditor.iOS.Extensions.dll
Registered in 0.002006 seconds.
[usbmuxd] Start listen thread
[usbmuxd] Listen thread started
[usbmuxd] Send listen message
Native extension for iOS target not found

問題の一文はログの最後に出てくるやつですね。

どう解決したか

さて、どう解決したかですが、結論から書くと、問題がありそうなファイルをRenameしてみた、です。

問題のファイル(っぽいもの)はこちら↓

/Applications/Unity/Hub/Editor/2019.2.3f1/PlaybackEngines/iOSSupport/UnityEditor.iOS.Extensions.dll

iOS target not found と書いてあったのでこれかなと。
でやったことはこれをRenameしました。(要はこれを読み込ませないようにしてみたということ)

これをスキップさせたらどういうエラーが出るのかなーと思って。
が、結果は意図に反してなぜかUnity起動するという謎。ファイルあるときはnot foundなのに、ファイル自体Renameしたらなんで起動するんだ・・。

そしてさらに謎なのが、これで無事起動したあと、変更したファイル名を元に戻したら普通に起動するという。

とはいえ、もしこのあたりの問題で悩んでいる人がいたら上記を試してみてください。
ただ、あまりにも謎すぎるので直る保証は一切ないのであしからず・・・。

その他メモ

ちなみに、これも功を奏したのか分かりませんが、「Unityゲーム開発者ギルド」というSlackのコミュニティに質問したところ以下のような対応も教えてもらいました。
上記の方法でもダメな場合はこちらも試してみるといいかもしれません。

  • Unityを起動している場合は終了し、アンインストール
  • ~/Library/Unity/ を削除
  • /Library/Application Support/Unity/ にある .ulf 拡張子のファイルを削除
  • PCを再起動
  • Unityを再インストール。インストーラーが壊れている可能性があるため、インストーラーも再度ダウンロードする

ちなみに再インストールでもプチハマり。なんと、この前アップデートがあったばかりのMacOS Catalina の場合、普通の手順でインストーラからインストールしようとするとインストールできない旨のアラートが。

ただ、こちらの記事を見るとUnity Hubを経由してインストールすると行けるよ、とのこと。
DownloadページのUnity Hubで開くボタンからやると無事にインストールできました。

ネイティブテクスチャ経由で画面キャプチャ(RenderTexture)を保存する

概要

今回はiOSのネイティブプラグインを利用してRenderTextureをそのままファイル保存するプラグインを作ったので、作る過程で得られた知見やハマりどころなどをメモしていきたいと思います。

RenderTextureにはTexture2Dが持っているEncodeToPNGEncodeToJPGメソッドがなく、さらにそもそもEncodeToPNGなどは非常に重い処理となっています。

なのでRenderTextureをそのままネイティブ側に渡してそれが保存できないかな、と思ったのが実装するに至った経緯です。

実際に実行したときの動画↓

今回の実装にあたり、ふじきさんには多大なご助力をいただきました。本当にありがとうございます。

ふじきさんが作られた動画キャプチャツールの投稿を見たのがきっかけで質問させていただき、色々ご教授いただきました。
そのツールはこちら↓

フロー

まずは全体の流れを把握するためにフローを概観してみます。

  1. RenderTextureを用意
  2. CommandBufferを利用して画面をキャプチャ
  3. Texture2DReadPixelsを利用してRenderTextureピクセル情報を読み出す(*1)
  4. RenderTextureGetNativeTexturePtrを使ってネイティブテクスチャのポインタを取得
  5. (4)のポインタをネイティブプラグインへ送信
  6. (5)のテクスチャを適切な形に変換する
  7. (6)のデータをUIImageにして保存

*1 ... ReadPixelsを行わないと(おそらく)GPUにデータがすぐにアップロードされず、画像保存に失敗します。もしかしたらGL.IssuePluginEventを利用するとうまくいくかもしれません。 色々調査した結果、1フレーム遅延させることでReadPixelsを使わなくても正常に保存することができました。

以上が大まかな処理の流れとなります。

今回実装したものはGitHubに上げてあるので動作を見たい方はそちらをご覧ください。

github.com

RenderTextureを用意

ここはむずかしいところはありません。
ファイルとして用意してもいいですし、ランタイムで用意してもよいです。
今回はランタイムで以下のように、Start時に用意するようにしています。

_buffer = new RenderTexture(Screen.width, Screen.height, 0);
_buffer.Create();

CommandBufferを用意する

コマンドバッファは画面をキャプチャする用途で利用しています。
セットアップは以下のように行っています。

_commandBuffer = new CommandBuffer();
_commandBuffer.name = "CaptureScreen";

_buffer = new RenderTexture(Screen.width, Screen.height, 0);
_buffer.Create();

_commandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, _buffer);

// スクショを撮るタイミングでカメラにコマンドバッファをアタッチ
Camera.main.AddCommandBuffer(CameraEvent.BeforeImageEffects, _commandBuffer);

コマンドバッファはシンプルに、現在のアクティブなレンダーバッファの内容をレンダーテクスチャにコピーしているだけです。
コマンドバッファをアタッチするタイミングはスクショを撮るタイミングです(スクショ撮影用メソッドを呼ぶタイミング)。

CommandBufferについては凹みさんがこちらの記事でとても詳しく解説されています。

tips.hecomi.com

ReadPixelsでピクセル情報を読み出す(※ 必要ありませんでした)

キャプチャを行ったらTexture2DReadPixelsピクセル情報を読み出します。
保存だけを行いたい場合はこれは不要な処理となりますが、GPUへのデータアップロードの関連なのか、これを行わないとネイティブ側の保存時に空白の画像が保存されてしまい、うまくいきませんでした。

フローのところでも書きましたが、もしかしたらGL.IssuePluginEventを利用して読み出すことでうまく動くかもしれません。(これについては後日調査します)

CommandBufferでキャプチャ後、その後すぐに保存するのではなく、1フレーム遅延させることで正常に保存できることを確認しました。

コード的には以下のようにしています。(ReadPixelsしていたコードを変更しています)

    private IEnumerator SaveTexture()
    {
        yield return _waitForEndOfFrame;

        Debug.Log("Save texture to the file.");

        Camera.main.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, _commandBuffer);

        _image.texture = _buffer;

        // To save the RenderTexture as file needs to wait one frame.
        yield return _waitForEndOfFrame;

        Debug.Log("Will show the texture.");

        _SaveTextureImpl(_buffer.GetNativeTexturePtr(), gameObject.name, nameof(CallbackFromSaver));
    }

RenderTextureのGetNativeTexturePtrを使ってネイティブテクスチャのポインタを取得

ここがひとつめの重要な点です。
UnityのTexture2DRenderTextureにはGetNativeTexturePtrというメソッドがあり、ネイティブ側のテクスチャのポインタを取得する方法があります。
今回のプラグインではネイティブ側で、この生成済みのテクスチャのポインタを利用して処理を行います。

取得して送信している箇所を抜粋すると以下のようになります。

// 第2、第3引数はネイティブ側からコールバックを受け取るためのもの
_SaveTextureImpl(_buffer.GetNativeTexturePtr(), gameObject.name, nameof(CallbackFromSaver));

GetNativeTexturePtrのドキュメントは以下です。

docs.unity3d.com

ドキュメントから抜粋すると以下のように書かれています。

On Direct3D-like devices this returns a pointer to the base texture type (IDirect3DBaseTexture9 on D3D9, ID3D11Resource on D3D11, ID3D12Resource on D3D12). On OpenGL-like devices the GL texture "name" is returned; cast the pointer to integer type to get it. On Metal, the id pointer is returned. On platforms that do not support native code plugins, this function always returns NULL.

要するにこれはプラットフォームによって返ってくる意味が違っているということです。(プラットフォームごとに適切なテクスチャへのポインタが返ってくる)
そして今回はiOS向けの話なのでid<MTLTexture>へのポインタが返ってくることが分かります。

On Metal, the id pointer is returned.

ネイティブプラグインへテクスチャのポインタを送る

そしてこちらが重要な点のふたつめです。
前段で取得したネイティブテクスチャのポインタをネイティブプラグイン側へ送り受け取る必要があるわけですが、適切にキャストして利用する必要があります。

これを間違えるとクラッシュしたり、ということがあるので気をつけてください。
ポインタを取得してキャストするコードは以下のようになります。

extern "C" void _SaveTextureImpl(unsigned char* mtlTexture, const char* objectName, const char* methodName)
{
    id<MTLTexture> tex = (__bridge id<MTLTexture>)(void*)mtlTexture;

    // 後略
}

まず、テクスチャのポインタはunsigned char*型で受け取ります。
そしてそれを(__bridge id<MTLTexture>)(void*)mtlTexture;のように、いったんvoid*型を経由してから最後にid<MTLTexture>型にキャストします。

加えて注意点として、C側からObjective-C側へのキャストには(__bridge)を利用してキャストを行う必要があります。

このブリッジにはいくつか種類があり、ARC管理下にいれるか、など状況に応じて使い分ける必要があります。

__bridgeを利用したキャストについては以下の記事を参考にしてみてください。

balunsoftware.jp

また今回はUnity側で生成したテクスチャなのでGC対象にもなっています。
そのため、ネイティブ側で管理対象にはせず、そのままキャストするだけに留めます。

これを、ARC管理対象などにしてしまうとBAD ACCESSでアプリがクラッシュするので注意が必要です。
(ここらへんまだ詳しくないのであれですが、万全を期すなら、Unity側でGC対象のポインタをロック(GCHandle.Alloc)してメモリ位置が変更されないようにするなどのケアは必要かもしれません。が、今回はとにかく保存するところまでを書くのでこのあたりには触れません)

テクスチャを適切な形に変換する

テクスチャのポインタを受け取り、テクスチャの情報にアクセスすることができるようになりましたが、このままだと各ピクセルのデータの並びが異なっているため色味が反転したような絵になって保存されてしまいます。

これに対して、適切に対処してくれるコードを公開してくれていた記事があったのでこちらを参考にさせていただきました。

qiita.com

具体的に何をしているかというと、取得したMTLTextureはRGBAではなくBGRAの並びになっているので、それをRGBAな形に並び替える処理をしています。


上下の反転について

また参考にした記事では、環境によっては上下が反転してしまうため、それを補う処理も同時に施されています。

該当のコードは以下のようになっています。

// flipping image vertically
let flippedBytes = bgraBytes // share the buffer
var flippedBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: flippedBytes),
            height: vImagePixelCount(self.height), width: vImagePixelCount(self.width), rowBytes: rowBytes)
vImageVerticalReflect_ARGB8888(&rgbaBuffer, &flippedBuffer, 0)

ただ今回のサンプルでは上下の反転は必要なかったのでコメントアウトしてあります。
上下の反転があったら上のコードを試してみてください。


この工程を経ることで無事、望んだ形のデータが手に入ります。

上の記事ではMTLTextureの拡張として書かれているのでこれを少しだけ改変してコンバータクラスとして実装しました。

実際に利用しているコードは以下です。

//
//    MTLTexture+Z.swift
//    ZKit
//
//    The MIT License (MIT)
//
//    Copyright (c) 2016 Electricwoods LLC, Kaz Yoshikawa.
//
//    Permission is hereby granted, free of charge, to any person obtaining a copy
//    of this software and associated documentation files (the "Software"), to deal
//    in the Software without restriction, including without limitation the rights
//    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//    copies of the Software, and to permit persons to whom the Software is
//    furnished to do so, subject to the following conditions:
//
//    The above copyright notice and this permission notice shall be included in
//    all copies or substantial portions of the Software.
//
//    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//    THE SOFTWARE.
//

import Foundation
import CoreGraphics
import MetalKit
import GLKit
import Accelerate

class MTLTextureConverter : NSObject {
    
    @objc static func convert(texture: MTLTexture) -> UIImage?
    {
        
        assert(texture.pixelFormat == .bgra8Unorm)
        
        // read texture as byte array
        let rowBytes = texture.width * 4
        let length = rowBytes * texture.height
        let bgraBytes = [UInt8](repeating: 0, count: length)
        let region = MTLRegionMake2D(0, 0, texture.width, texture.height)
        texture.getBytes(UnsafeMutableRawPointer(mutating: bgraBytes), bytesPerRow: rowBytes, from: region, mipmapLevel: 0)
        
        // use Accelerate framework to convert from BGRA to RGBA
        var bgraBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: bgraBytes),
                                       height: vImagePixelCount(texture.height), width: vImagePixelCount(texture.width), rowBytes: rowBytes)
        
        let rgbaBytes = [UInt8](repeating: 0, count: length)
        var rgbaBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: rgbaBytes),
                                       height: vImagePixelCount(texture.height), width: vImagePixelCount(texture.width), rowBytes: rowBytes)
        let map: [UInt8] = [2, 1, 0, 3]
        vImagePermuteChannels_ARGB8888(&bgraBuffer, &rgbaBuffer, map, 0)
        
        // flipping image virtically
        // let flippedBytes = bgraBytes // share the buffer
        // var flippedBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: flippedBytes),
        //                                   height: vImagePixelCount(texture.height), width: vImagePixelCount(texture.width), rowBytes: rowBytes)
        // vImageVerticalReflect_ARGB8888(&rgbaBuffer, &flippedBuffer, 0)
        
        // create CGImage with RGBA
        let colorScape = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
        guard let data = CFDataCreate(nil, rgbaBytes, length) else { return nil }
        guard let dataProvider = CGDataProvider(data: data) else { return nil }
        let cgImage = CGImage(width: texture.width, height: texture.height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rowBytes,
                              space: colorScape, bitmapInfo: bitmapInfo, provider: dataProvider,
                              decode: nil, shouldInterpolate: true, intent: .defaultIntent)
        
        return UIImage(cgImage: cgImage!)
    }
}

UIImageとして保存する

前段までで無事にUnityからテクスチャを受け取ることができました。あとはこれをUIImageとしてファイルに保存すれば終了です。

シンプルに保存するだけでいいのであれば以下のように1行で書くことができます。

UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);

なお、保存後にコールバックを受け取りたい場合は適切にセットアップして、コールバックを受け取るオブジェクトを生成する必要があります。
具体的には以下のシグネチャを持つオブジェクトを生成し、セレクタを渡してやることで実現できます。

期待されるシグネチャ

- (void)image:(UIImage *)image
    didFinishSavingWithError:(NSError *)error
                 contextInfo:(void *)contextInfo;

これを実装したオブジェクトを作ると以下のようになります。
(Unityへのコールバックをする部分も一緒に実装した例です)

#import "CaptureCallback.h"
 
@implementation CaptureCallback

- (id)initWithObjectName:(NSString *)_objectName
              methodName:(NSString *)_methodName;
{
    if (self = [super init])
    {
        self.objectName = _objectName;
        self.methodName = _methodName;
    }
    return self;
}
 
- (void)savingImageIsFinished:(UIImage *)_image didFinishSavingWithError:(NSError *)_error contextInfo:(void *)_contextInfo
{
    const char *objectName = [self.objectName UTF8String];
    const char *methodName = [self.methodName UTF8String];

    if (_error != nil)
    {
        NSLog(@"Error occurred with %@", _error.description);
        UnitySendMessage(objectName, methodName, [_error.description UTF8String]);
    }
    else
    {
        UnitySendMessage(objectName, methodName, "success");
    }
}
 
@end

これを実際に使うと以下のようになります。

CaptureCallback *callback = [[CaptureCallback alloc] initWithObjectName:@"obj" methodName:@"method"];

UIImageWriteToSavedPhotosAlbum(image, callback, @selector(savingImageIsFinished:didFinishSavingWithError:contextInfo:), nil);

詳細は以下のドキュメントをご覧ください。

developer.apple.com

保存パスをコールバックで受け取る

上記の関数では保存と保存後のコールバックを受け取れても、保存したファイルのパスを知ることができません。
これを実現するためにはPHPhotoLibraryperformChangesと、PHImageManagerrequestImageDataForAsset:を利用します。

コードは以下のようになります。
ポイントはいくつかの非同期関数を連続で呼び出し、最終的にファイルパスを取得している点です。

__block NSString* localId;

// Add it to the photo library
[PHPhotoLibrary.sharedPhotoLibrary performChanges:^{
    PHAssetChangeRequest *assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    
    localId = assetChangeRequest.placeholderForCreatedAsset.localIdentifier;
} completionHandler:^(BOOL success, NSError *err) {
    
    if (!success)
    {
        NSLog(@"Error saving image: %@", err.localizedDescription);
        [callback savingImageIsFinished:nil
                didFinishSavingWithError:err];
    }
    else
    {
        PHFetchResult* assetResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[localId] options:nil];
        PHAsset *asset = assetResult.firstObject;
        [PHImageManager.defaultManager requestImageDataForAsset:asset
                                                        options:nil
                                                    resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
                                                        
                                                        NSURL *fileUrl = [info objectForKey:@"PHImageFileURLKey"];
                                                        
                                                        if (fileUrl)
                                                        {
                                                            NSLog(@"Image path: %@", fileUrl.relativePath);
                                                            [callback savingImageIsFinished:fileUrl
                                                                    didFinishSavingWithError:nil];
                                                        }
                                                        else
                                                        {
                                                            NSLog(@"Error retrieving image filePath, heres whats available: %@", info);
                                                            [callback savingImageIsFinished:nil
                                                                    didFinishSavingWithError:nil];
                                                        }
                                                    }];
    }
}];

やや複雑ですが、これで保存先のパスを得ることができます。

まとめ

ひとまずここまでで、Unity側で生成したテクスチャをネイティブ側に送りファイルに保存することができました。
今回は保存することを目的に作成しましたが、ネイティブ側で加工をしたり、ネイティブ側で生成したものをUnity側に送るなど、活用の幅は広いと思います。

ネイティブと友だちになれるとやれることが格段に増えるのでぜひともマスターしておきたいですね。

そして以下からは、今回のプラグインを作るにあたってハマった点や試したことなど、後々役に立ちそうなものをメモとして残しておきます。
興味がある方は読んでみてください。


その他の役立ちそうなメモ

Native Plugin Interfaceについて

最後に、今回の実装とは直接は関係ありませんが、デバイス情報(MTLDevice)などを利用したいケースがある場合にUnityのAPIからそれらデバイスへの参照の取得方法を紹介していきます。

これらはNative Plugin InterfaceとしてUnityから提供されているAPIになります。

バイスへの参照を取得する

バイスへの参照などを得るためにUnityはIUnityInterfacesというインターフェースを用意しています。
これはプラグインが読み込まれた際に呼ばれるコールバック内で取得することができます。

ドキュメントにはextern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoadを公開しておけば自動的に呼ばれる、と記載されているのですがiOSだとダメなのかこれだけでは呼び出されませんでした。

そこで別の方法を利用してこのインターフェースを取得するようにしました。

具体的には以下の関数をあらかじめ呼び出すことで対応しました。

UnityRegisterRenderingPluginV5(&UnityPluginLoad, &UnityPluginUnload);

名前から分かる通り、プラグインが読み込まれた際に、引数に渡したコールバックが呼ばれる仕組みになっています。
この登録処理を、C#側から呼べるようにしておき、Startなどのタイミングで呼び出しておきます。

これら諸々を記述したコードは以下のようになります。

extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces);
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginUnload();

static IUnityGraphicsMetal* s_MetalGraphics = 0;
static IUnityInterfaces*    s_UnityInterfaces  = 0;
static IUnityGraphics*      s_Graphics = 0;

static bool initialized = false;

static void UNITY_INTERFACE_API OnGraphicsDeviceEvent(UnityGfxDeviceEventType eventType)
{
    switch (eventType)
    {
        case kUnityGfxDeviceEventInitialize:
        {
            // s_RendererType = s_Graphics->GetRenderer();
            initialized = false;
            break;
        }
        case kUnityGfxDeviceEventShutdown:
        {
            // s_RendererType = kUnityGfxRendererNull;
            initialized = false;
            break;
        }
        case kUnityGfxDeviceEventBeforeReset:
        {
            // TODO: User Direct3D 9 code
            break;
        }
        case kUnityGfxDeviceEventAfterReset:
        {
            // TODO: User Direct3D 9 code
            break;
        }
    };
}

extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces)
{
    s_UnityInterfaces = unityInterfaces;
    s_Graphics = s_UnityInterfaces->Get<IUnityGraphics>();
    s_MetalGraphics   = s_UnityInterfaces->Get<IUnityGraphicsMetal>();

    s_Graphics->RegisterDeviceEventCallback(OnGraphicsDeviceEvent);
    OnGraphicsDeviceEvent(kUnityGfxDeviceEventInitialize);
}

extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginUnload()
{
    s_Graphics->UnregisterDeviceEventCallback(OnGraphicsDeviceEvent);
}

///
/// Attach the functions to the callback of plugin loaded event.
///
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API _AttachPlugin()
{
    UnityRegisterRenderingPluginV5(&UnityPluginLoad, &UnityPluginUnload);
}

特に大事な点は以下です。

extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces)
{
    s_UnityInterfaces = unityInterfaces;
    s_Graphics = s_UnityInterfaces->Get<IUnityGraphics>();
    s_MetalGraphics   = s_UnityInterfaces->Get<IUnityGraphicsMetal>();

    s_Graphics->RegisterDeviceEventCallback(OnGraphicsDeviceEvent);
    OnGraphicsDeviceEvent(kUnityGfxDeviceEventInitialize);
}

コールバックにはIUnityInterfacesが引数として渡されてくるので、さらにそこからunityInterfaces->Get<IUnityGraphicsMetal>();を呼び出すことでデバイスへの参照を持つオブジェクトを取得することができます。

UNITY_INTERFACE_EXPORTとUNITY_INTERFACE_API

ちなみにAPIを利用するにはUNITY_INTERFACE_EXPORTUNITY_INTERFACE_APIのマクロを付ける必要があります。
ぱっと見はなにをしてくれるものか分かりづらいので、定義元をメモしておきます。

定義を見てみると以下のように分岐されています。

#if defined(__CYGWIN32__)
    #define UNITY_INTERFACE_API __stdcall
    #define UNITY_INTERFACE_EXPORT __declspec(dllexport)
#elif defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(_WIN64) || defined(WINAPI_FAMILY)
    #define UNITY_INTERFACE_API __stdcall
    #define UNITY_INTERFACE_EXPORT __declspec(dllexport)
#elif defined(__MACH__) || defined(__ANDROID__) || defined(__linux__)
    #define UNITY_INTERFACE_API
    #define UNITY_INTERFACE_EXPORT
#else
    #define UNITY_INTERFACE_API
    #define UNITY_INTERFACE_EXPORT
#endif

プラットフォームによっては関数をDLLにexportするために必要な指定が必要なため、また指定方法が異なるためこういうスイッチが入っています。

iOSなどその他のプラットフォームでは特になにも出力されないので、読むときは単純に関数の宣言と見ておいて大丈夫です。

ちなみに__stdcallは関数の呼び出し規約となっていて、アセンブラレベルでは関数に対して引数をどう渡すか、また戻り値をどう受け取るか、という取り決めを事前にしておく必要があります。

その取り決めを明示するものであり、これがないと引数がうまく渡せなかったり、など問題がおきます。(多分、誤った指定だとエラーになるか、実行時にクラッシュします。試したこと無いので推測ですが・・・)

このあたりについては以下の記事が参考になるかもしれません。

qiita.com

ハマった点

Photo Libraryへのアクセス権

Photo Libraryに保存するためにアクセス権を取得する必要があります。
実行時にアクセス権を確認し、アクセス権がなければ適切にリクエストする必要があります。

ちなみに確認せずに実行するとクラッシュして以下のようなエラーが出力されます。

$ Photos Access not allowed

このあたりについては以下の記事を参考に修正しました。

superhahnah.com

その他メモ

Build Settingsをポストプロセスで更新する

Swiftコードも含んでいるためBuild Settingsを修正する必要があり、それを自動化するために以下の記事を参考にさせていただきました。

uwanosora22.hatenablog.com

MTLTextureをコピーする

実装する過程でクラッシュが発生したため、いったんテクスチャをネイティブ側でコピーして保持したらどうだ、っていうことで作ったものがあるのでメモとして残しておきます。

id<MTLTexture> CopyTexture(id<MTLTexture> source)
{
    MTLTextureDescriptor *descriptor = [[MTLTextureDescriptor alloc] init];
    descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
    descriptor.width = source.width;
    descriptor.height = source.height;
    
    id<MTLTexture> texture = [s_MetalGraphics->MetalDevice() newTextureWithDescriptor:descriptor];

    id<MTLCommandQueue> queue = [s_MetalGraphics->MetalDevice() newCommandQueue];
    id<MTLCommandBuffer> buffer = [queue commandBuffer];
    id<MTLBlitCommandEncoder> encoder = [buffer blitCommandEncoder];
    [encoder copyFromTexture:source
                 sourceSlice:0
                 sourceLevel:0
                sourceOrigin:MTLOriginMake(0, 0, 0)
                  sourceSize:MTLSizeMake(source.width, source.height, source.depth)
                   toTexture:texture
            destinationSlice:0
            destinationLevel:0
           destinationOrigin:MTLOriginMake(0, 0, 0)];
    [encoder endEncoding];
    [buffer commit];
    [buffer waitUntilCompleted];

    return texture;
}

MTLTextureをネイティブ側で生成してポインタを受け取る

今回の実装をしていく過程で、試しにネイティブ側でMTLTextureを生成してUnity側に渡したらうまくいくかも、と思って実装したのでメモとして残しておきます。

extern "C" uintptr_t UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API _GetNativeTexturePtr(int width, int height)
{
    MTLTextureDescriptor *descriptor = [[MTLTextureDescriptor alloc] init];
    descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
    descriptor.width = width;
    descriptor.height = height;
    
    id<MTLTexture> texture = [s_MetalGraphics->MetalDevice() newTextureWithDescriptor:descriptor];

    return (uintptr_t)texture;
}

その他参考にした記事

tips.hecomi.com

Unity AR FoundationでPeople Occlusionをやってみる

この記事はUnityゆるふわサマーアドベントカレンダー 2019 #ゆるふわアドカレの8/11の記事です。

概要

UnityのPackage Managerで提供されているAR Foundation。最近はARKit3のサポートが入ってだいぶ熱くなってきてますね。

ということで、今回はAR Foundationを使ってPeople Occlusionをやってみたのでそのメモです。

AR Foundationのドキュメントはこちら↓

docs.unity3d.com

AR Foundationについてはサンプルなどを含めたものがGitHubで公開されています。

github.com

このサンプルを使ってPeople Occlusion的なものを実装してみたのが以下↓

処理フロー

今回は上のtweetのような簡単な見た目のPeople Occlusionを行うまでを解説したいと思います。
大まかなフローはそこまで複雑なことはしていません。

  • 1) People Occlusionに必要なテクスチャを集める
    • 1-1) ステンシル / デプス / カメラ映像のテクスチャ
  • 2) ARオブジェクトと人との深度を比較する
  • 3) ポストエフェクトとして描画する

People Occlusionに必要なテクスチャを集める

フローの(1)についてはAR Foundationが提供してくれているARHumanBodyManagerクラスとARCameraBackgroundから得ることができます。

ARHumanBodyManagerARCameraBackgroundについてはインスペクタなどから設定し、以下のようにすることでテクスチャを得ることができます。

ステンシルとデプス用のテクスチャを得る

// 人の位置と推測された位置が`1`となるマスク用テクスチャ
Texture2D humanStencil = _humanBodyManager.humanStencilTexture;

// 人の位置と推測された位置の深度値を格納しているテクスチャ(単位はメートルの模様)
Texture2D humanDepth = _humanBodyManager.humanDepthTexture;

ステンシルとデプスはARHumanBodyManagerのプロパティから簡単に得ることができます。

カメラからの映像については少し手間を掛ける必要があります。

ARCameraの映像を得る

カメラからの映像についてはARCameraBackgroundクラスを利用します。
ちなみにカメラからの映像が必要な理由は、レンダリング結果にはARオブジェクトも含まれてしまっているので、カメラの映像も別途必要なのです。(もしかしたらデプステクスチャをデプスバッファとして応用することができればこの処理はいらないかもしれません)

対象の映像を得るための方法がドキュメントに記載されています。それを引用すると以下のように説明されています。

Copying the Camera Texture to a Render Texture

The camera textures are likely External Textures and may not last beyond a frame boundary. It can be useful to copy the camera image to a Render Texture for persistence or further processing. This code will blit the camera image to a render texture of your choice:

Graphics.Blit(null, m_MyRenderTexture, m_ARBackgroundCamera.material);

このコード断片が示すように、自前で生成したレンダーテクスチャとARCameraBackgroundクラスのmaterialプロパティを利用して、現在の状態をそのレンダーテクスチャに書き出すことができます。

ARオブジェクトと人との深度を比較する

さて、必要なデータを集めたらそれを利用して「人と思しき場所」の深度を比較し、必要であればARオブジェクトの前面に人の体を描画します。

比較に関してはシェーダを利用し以下のようにします。

// Check depth delta. If delta is over zero, it means pixels that estimated like human is in front of AR objects.
float sceneZ = LinearEyeDepth(tex2D(_CameraDepthTexture, i.uv));
float delta = saturate(sceneZ - depth);
if (delta > 0.0)
{
    return tex2D(_BackgroundTex, i.uv);
}
else
{
    return col;
}

最初の行で行っているのは、3Dシーンで描画されたデプスバッファからの値をリニアに変換しています。

ここでリニアに変換しているのは以下の記事で言及されている以下の理由からです。

https://forum.unity.com/threads/how-to-setup-people-occlusion.691789/

The values in the depth buffer are in meters with the range [0, infinity) and need to be converted into the view space with the depth value [0, 1] mapped between the near & far clip plane.

正確には「デプステクスチャ側を0 - 1に正規化する」と書かれていますが、Unityの単位もメートルなのでそのままリニアに変換することでメートルとして利用できるかなと思ってこうしています。

が、floatの精度などの問題でもしかしたら上で言及されているように、デプステクスチャ側をしっかり0 - 1に正規化したほうがよりきれいになるかもしれません。(それは追って調査)

なお、このあたりの変換については以前書いた記事が理解に役立つかもしれません。

edom18.hateblo.jp

ポストエフェクトとして描画する

最後に、これらの情報を元にしてポストフェクトとしてPeople Occlusionを実現していきます。
イメージ的にはCGシーンとカメラ映像を、深度値を元に切り分けてレンダリングする、という感じです。

ということでセットアップを含めてコード全文を載せておきます。
まずはシェーダから。

Shader "Hidden/PeopleOcclusion"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;
            sampler2D _BackgroundTex;
            sampler2D _DepthTex;
            sampler2D _StencilTex;

            UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);

                float2 uv = i.uv;

                // Flip x axis.
                uv.x = 1.0 - uv.x;

                // Correcting textures ratio that can be got by ARHumanBodyManager to the screen ratio.
                float ratio = 1.62;
                uv.y /= ratio;
                uv.y += 1.0 - (ratio * 0.5);

                float stencil = tex2D(_StencilTex, uv).r;
                if (stencil < 0.9)
                {
                    return col;
                }

                // Check depth delta. If delta is over zero, it means pixels that estimated like human is in front of AR objects.
                float depth = tex2D(_DepthTex, uv).r;
                float sceneZ = LinearEyeDepth(tex2D(_CameraDepthTexture, i.uv));
                float delta = saturate(sceneZ - depth);
                if (delta > 0.0)
                {
                    return tex2D(_BackgroundTex, i.uv);
                }
                else
                {
                    return col;
                }
            }
            ENDCG
        }
    }
}

コードは深度値比較とフェッチする対象を変えるだけなのでそんなに長くないです。
前述の通り、重要な箇所は以下。

// Check depth delta. If delta is over zero, it means pixels that estimated like human is in front of AR objects.
float depth = tex2D(_DepthTex, uv).r;
float sceneZ = LinearEyeDepth(tex2D(_CameraDepthTexture, i.uv));
float delta = saturate(sceneZ - depth);
if (delta > 0.0)
{
    return tex2D(_BackgroundTex, i.uv);
}

ひとつ注意点として、UV値を少しだけ加工しています。
理由は、AR Foundationから得られるステンシルとデプステクスチャのサイズがデバイスの解像度と合っていないためです。(さらに左右反転しているのでそれも合わせて行っています)

そのため少しだけUVの値を加工して縦横比が合うように補正しています。
補正のためのコードは以下。

// Correcting textures ratio that can be got by ARHumanBodyManager to the screen ratio.
float ratio = 1.62;
uv.y /= ratio;
uv.y += 1.0 - (ratio * 0.5);

1.62の根拠は256x192の比率から2688x1242の比率へ変換するためのものです。
ちなみに、ステンシル/デプステクスチャの解像度はstandard resolutionhalf resolution, full resolutionの3つが選べますが、解像度が違えどどれも比率は同様のものが渡されるのでこの計算で問題なさそうです。

あとはAR Foundationから受け取ったデプステクスチャの値とレンダリングされた3Dシーンの深度値を比較して、人と思われる位置のピクセルの深度が3Dシーンより手前だと判断されたらカメラの映像を利用し、そうでなければそのまま3Dシーンの映像をレンダリングするという形です。

これをセットアップしているC#側のコードは以下のようになります。

using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;

public class PeopleOcclusion : MonoBehaviour
{
    [SerializeField, Tooltip("The ARHumanBodyManager which will produce frame events.")]
    private ARHumanBodyManager _humanBodyManager;

    [SerializeField]
    private Material _material = null;

    [SerializeField]
    private ARCameraBackground _arCameraBackground = null;

    [SerializeField]
    private RawImage _captureImage = null;

    private RenderTexture _captureTexture = null;

    public ARHumanBodyManager HumanBodyManager
    {
        get { return _humanBodyManager; }
        set { _humanBodyManager = value; }
    }

    [SerializeField]
    private RawImage _rawImage;

    /// <summary>
    /// The UI RawImage used to display the image on screen.
    /// </summary>
    public RawImage RawImage
    {
        get { return _rawImage; }
        set { _rawImage = value; }
    }

    [SerializeField]
    private Text _imageInfo;

    /// <summary>
    /// The UI Text used to display information about the image on screen.
    /// </summary>
    public Text ImageInfo
    {
        get { return _imageInfo; }
        set { _imageInfo = value; }
    }

    #region ### MonoBehaviour ###
    private void Awake()
    {
        Camera camera = GetComponent<Camera>();
        camera.depthTextureMode |= DepthTextureMode.Depth;

        _rawImage.texture = _humanBodyManager.humanDepthTexture;

        _captureTexture = new RenderTexture(Screen.width, Screen.height, 0);
        _captureImage.texture = _captureTexture;
    }
    #endregion ### MonoBehaviour ###

    private void LogTextureInfo(StringBuilder stringBuilder, string textureName, Texture2D texture)
    {
        stringBuilder.AppendFormat("texture : {0}\n", textureName);
        if (texture == null)
        {
            stringBuilder.AppendFormat("   <null>\n");
        }
        else
        {
            stringBuilder.AppendFormat("   format : {0}\n", texture.format.ToString());
            stringBuilder.AppendFormat("   width  : {0}\n", texture.width);
            stringBuilder.AppendFormat("   height : {0}\n", texture.height);
            stringBuilder.AppendFormat("   mipmap : {0}\n", texture.mipmapCount);
        }
    }

    private void Update()
    {
        var subsystem = _humanBodyManager.subsystem;

        if (subsystem == null)
        {
            if (_imageInfo != null)
            {
                _imageInfo.text = "Human Segmentation not supported.";
            }
            return;
        }

        StringBuilder sb = new StringBuilder();
        Texture2D humanStencil = _humanBodyManager.humanStencilTexture;
        Texture2D humanDepth = _humanBodyManager.humanDepthTexture;
        LogTextureInfo(sb, "stencil", humanStencil);
        LogTextureInfo(sb, "depth", humanDepth);

        if (_imageInfo != null)
        {
            _imageInfo.text = sb.ToString();
        }

        _material.SetTexture("_StencilTex", humanStencil);
        _material.SetTexture("_DepthTex", humanDepth);
        _material.SetTexture("_BackgroundTex", _captureTexture);
    }

    private void LateUpdate()
    {
        if (_arCameraBackground.material != null)
        {
            Graphics.Blit(null, _captureTexture, _arCameraBackground.material);
        }
    }

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        Graphics.Blit(src, dest, _material);
    }
}

こちらのコードはAR FoundationのTestDepthImage.csに手を加えたものです。
元々あったステンシルとデプスを取得する部分にカメラからの映像取得を追加したものです。

そしてこれらのテクスチャ情報をマテリアルにセットしOnRenderImageのタイミングで合成している、というわけです。

ちなみにOnRenderImageを利用していることからも分かる通り、これはCameraコンポーネントがついているオブジェクトにアタッチして利用することを想定しています。

まとめ

色々調整が必要な部分が多々ありますが、ひとまずAR Foundationの機能を用いてPeople Occlusionが実現できました。
これを調整していけば実用的なものになりそうです。

People Occlusionは個人的にARKit3で一番注目している機能なので、いち早く取り入れて実際のコンテンツに利用していきたいですね。

ARで影を描画する(影だけをレンダリングするシェーダ)

概要

ARコンテンツを作っているとキャラやオブジェクトの影をつけたくなります。
ただ、通常地面はARKitの機能で認識した平面に対して透明(か半透明)のメッシュを配置して表現します。

そしてUnityの標準のシェーダでは透明なオブジェクトには影が落ちません。

UnityのARKit Pluginには影を落とすためのシェーダが最初から用意されているのでそれを床面として配置したプレーンに設定してやれば影が落ちるようになります。

ただ、AR以外でも透明なオブジェクトに影を落としたいケースもあると思うので、その中で使われているシェーダを見つつ、ARKitではどうやって透明なオブジェクトに影を落としているかをメモしておきたいと思います。

ということで、用意されているシェーダは以下になります。
(英語で書かれているコメントをふんわり翻訳してあります)

コード全容

//This is based on a shader from https://alastaira.wordpress.com/2014/12/30/adding-shadows-to-a-unity-vertexfragment-shader-in-7-easy-steps/

Shader "Custom/MobileARShadow"
{
    SubShader
    {
        Pass
        {
            // 1.) This will be the base forward rendering pass in which ambient, vertex, and
            // main directional light will be applied. Additional lights will need additional passes
            // using the "ForwardAdd" lightmode.
            // see: http://docs.unity3d.com/Manual/SL-PassTags.html
            //
            // 1.) forwardレンダリングパスのアンビエント、頂点とメインディレクショナルライトで利用されます。
            //     追加ライトの場合は、addtionalパス、ForwardAddライトモードが必要です。
            Tags
            {
                "LightMode" = "ForwardBase" "RenderType"="Opaque" "Queue"="Geometry+1" "ForceNoShadowCasting"="True"
            }

            LOD 150
            Blend Zero SrcColor
            ZWrite On
        
            CGPROGRAM
 
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
 
            // 2.) This matches the "forward base" of the LightMode tag to ensure the shader compiles
            // properly for the forward bass pass. As with the LightMode tag, for any additional lights
            // this would be changed from _fwdbase to _fwdadd.
            //
            // 2.) "forward base"ライトモードにマッチし、シェーダがforward baseパスで正常にコンパイルされるようにします。
            //     他の追加ライトの場合は_fwdbaseを_fwdaddに変更する必要があります。
            #pragma multi_compile_fwdbase
 
            // 3.) Reference the Unity library that includes all the lighting shadow macros
            // 3.) すべてのライティングシャドウマクロのUnityライブラリをインクルードします。
            #include "AutoLight.cginc"
 
 
            struct v2f
            {
                float4 pos : SV_POSITION;
                 
                // 4.) The LIGHTING_COORDS macro (defined in AutoLight.cginc) defines the parameters needed to sample 
                // the shadow map. The (0,1) specifies which unused TEXCOORD semantics to hold the sampled values - 
                // As I'm not using any texcoords in this shader, I can use TEXCOORD0 and TEXCOORD1 for the shadow 
                // sampling. If I was already using TEXCOORD for UV coordinates, say, I could specify
                // LIGHTING_COORDS(1,2) instead to use TEXCOORD1 and TEXCOORD2.
                //
                // 4.) LIGHTING_COORDSマクロ(AutoLight.cgincで定義)は、シャドウマップをサンプルするパラメータを定義しています。
                //     (0, 1)はサンプルした値を保持するために、未使用のTEXCOORDを指定します。
                //     このシェーダでは、texcoordsは使用していないため、TEXCOORD0とTEXCOORD1をシャドウのサンプリングのために使用できます。
                //     もしUV座標のためにTEXCOORDを利用していたら、LIGHTHING_COORDS(1, 2)をTEXCOORD1とTEXCOORD2の代わりに使用します。
                LIGHTING_COORDS(0,1)
            };
 
 
            v2f vert(appdata_base v) {
                v2f o;
                o.pos = UnityObjectToClipPos (v.vertex);
                 
                // 5.) The TRANSFER_VERTEX_TO_FRAGMENT macro populates the chosen LIGHTING_COORDS in the v2f structure
                // with appropriate values to sample from the shadow/lighting map
                //
                // 5.) TRANSFER_VERTEX_TO_FRAGMENTマクロは、LIGHTING_COORDSで選択したものをv2f構造体の中で、shadow/lightingマップのサンプル用に適切な値に設定します。
                TRANSFER_VERTEX_TO_FRAGMENT(o);
                 
                return o;
            }
 
            fixed4 frag(v2f i) : COLOR {
             
                // 6.) The LIGHT_ATTENUATION samples the shadowmap (using the coordinates calculated by TRANSFER_VERTEX_TO_FRAGMENT
                // and stored in the structure defined by LIGHTING_COORDS), and returns the value as a float.
                //
                // 6.) LIGHT_ATTENUATIONは、シャドウマップからサンプルします。(TRANSFER_VERTEX_TO_FRAGMENTによって計算された座標を使って、LIGHTHING_COORDSによって定義された構造体に保持します)
                //     そしてfloatの値を返します。
                float attenuation = LIGHT_ATTENUATION(i);
                return fixed4(1.0,1.0,1.0,1.0) * attenuation;
            }
 
            ENDCG
        }
    }
     
    // 7.) To receive or cast a shadow, shaders must implement the appropriate "Shadow Collector" or "Shadow Caster" pass.
    // Although we haven't explicitly done so in this shader, if these passes are missing they will be read from a fallback
    // shader instead, so specify one here to import the collector/caster passes used in that fallback.
    //
    // 7.) レシーブまたはキャストシャドウのために、シェーダは"Shadow Collector"か"Shadow Caster"パスを適切に実装しなければなりません。
    //     しかしこのシェーダでは明示的にそれをしていませんが、これらのパスが見つからない場合はフォールバックシェーダが代わりに読み込まれます。
    //     フォールバックで使用されるcollector/casterパスをインポートするには、ここでそれを指定します。
    Fallback "VertexLit"

}

コメントによってだいぶボリュームが増えていますが、実際のシェーダコードはそんなに多くはありません。
このシェーダによってなにが行われているかを簡単に説明すると、通常の頂点変換とライトの強さなどを知るための値を構造体に設定してフラグメントシェーダに送り、その値を元に影の強さを元にフラグメントの色を決める、という処理になります。

基本的に影になっていない部分は影の濃さが0になるため、結果的に「なにも描かない」ことになり、逆に影になっている部分は0以外の値になるため、濃さに応じて色が描かれる、というわけです。

その他の細かな点については、コメントをご覧ください。
元の英語のコメントに加え、自分が英訳したものも追記してあります。

ソフトパーティクルの仕組みを応用した表現

概要

ソフトパーティクルの仕組みを応用した表現について色々とメモしておこうと思います。
(よく利用しようと思うこともあるものの、毎回調べたり、というのがめんどくさいので)

ちなみにソフトパーティクルとは、いわゆるパーティクルが通常のオブジェクトと重なる場合に、深度値を用いて重なり具合に応じてフェードを掛けることで、パーティクルの重なり部分をソフトに見せる(エッジを目立たなくさせる)目的で使われるものです。

これをパーティクル以外に利用することで、オブジェクトが他のオブジェクトに近い場合にフェードさせじんわりと重ねることができるようになります。

なお、今回も下記の凹みさんの記事を大いに参考にさせていただきました。
(いつもありがとうございます)

tips.hecomi.com

実際に実行すると以下のようになります。(右はワイヤーフレームを追加したもの)

f:id:edo_m18:20190730081733p:plain

コード解説

さて、まずはコード全容から。
コード量はそこまで多くありません。

Shader "Unlit/SoftParticle"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _InvFade("Inv fade", Range(0, 1)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 100

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float4 projPos : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _InvFade;

            // デプステクスチャの宣言
            UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);

                // ワールド空間座標を元に、スクリーンスペースでの位置を求める
                o.projPos = ComputeScreenPos(o.vertex);

                // 求めたスクリーンスペースでの位置のz値からビュー座標系での深度値を求める
                COMPUTE_EYEDEPTH(o.projPos.z);

                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);

                // 深度バッファからフェッチした値を使って、リニアな深度値に変換する
                float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
                float partZ = i.projPos.z;

                // フェード処理
                float fade = saturate(_InvFade * (sceneZ - partZ));
                col.a *= fade;

                return col;
            }
            ENDCG
        }
    }
}

ソフトパーティクルを実現するための処理を、順を追って見ていきましょう。

ComputeScreenPos

ComputeScreenPosの定義は以下。

inline float4 ComputeScreenPos(float4 pos) {
    float4 o = ComputeNonStereoScreenPos(pos);
#if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    return o;
}

// -------------------

inline float4 ComputeNonStereoScreenPos(float4 pos) {
    float4 o = pos * 0.5f;
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
}

シングルパスステレオレンダリングかどうかで分岐が入っていますが、今回はソフトパーティクルの説明なのでこちらは割愛します。(基本概念が分かればVR対応も容易いと思うので)

ComputeNonStereoScreenPosの引数に渡ってくるposは、UnityObjectToClipPosでクリップ座標空間に変換された位置、すなわち-w ~ wの範囲(グラフィックスAPIによっては0 ~ w)に変換された値が渡ってきます。

冒頭の計算が行っていることを、細かいことを省いてやっていることだけに着目すると、

float4 o = pos * 0.5f;
o.xy = o.xy + o.w;

ということです。
ここで行っているのは-w ~ wを半分にして(-0.5w ~ 0.5w)、0.5wを足す、つまり0 ~ wの範囲に変換している、ということです。

ちなみに_ProjectionParams.xyに掛けているのはプラットフォームごとの違いを吸収する目的で行っているのみで、計算の本質に違いはありません。

そして最後に、半分にしてしまったzwの値をもとに戻す目的でo.zw = pos.zw;を再代入しています。


余談

wの意味。

3Dグラフィクスでは3D空間を正規化デバイス座標系という座標系に変換し、そののちにビューポート(つまりディスプレイ)座標に変換して画面に映像を出力します。

この正規化デバイス座標系では頂点xyzの値が-1 ~ 1に変換されます。 (グラフィクスAPIによっては0 ~ 1になる) そのために正規化と呼ばれるわけですね。

そしてこの-1 ~ 1に変換するために使われる値がwなのです。
具体的にはwで除算することで-1 ~ 1に変換します。

本来ならこのwで除算の部分はGPU側が自動で行ってくれるため、シェーダ内では-w ~ wの間になる状態までの計算に留めておいています。

ただ今回の例では、さらにそこからシェーダ内で深度値を利用するため自分でwで除算して値を求めているというわけです。

このあたりの詳しい説明はマルペケさんのこちらの記事(その70 完全ホワイトボックスなパースペクティブ射影変換行列)に詳しく書かれているのでご覧ください。

またパースペクティブ行列関連については以前、Qiitaにまとめているので詳しくはそちらをご覧ください。

qiita.com


COMPUTE_EYEDEPTH

次にCOMPUTE_EYEDEPTHの定義を見てみましょう。

#define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z

inline float3 UnityObjectToViewPos( in float3 pos )
{
    return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz;
}

COMPUTE_EYEDEPTHはマクロとして定義されており、定義からは引数に指定された変数へ計算結果を格納する形になっています。
そして計算自体はUnityObjectToViewPosによって行われ、その結果のz値が代入されています。

UnityObjectToViewPos

続けてUnityObjectToViewPosを見てみましょう。

こちらはインライン関数になっていて引数にfloat3を受け取ります。
注意してほしいのは、COMPUTE_EYEDEPTHのマクロではv.vertexという変数が宣言されている前提で計算が行われている点です。

これは頂点シェーダに渡ってきた頂点そのものです。
そしてこれを加工した結果のz値を使っているわけです。

計算式を改めて見てみると、

return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz;

となっています。
ワールド座標への変換とビュー座標への変換を行っています。
つまり、引数に与えられた頂点位置をビュー座標空間へ変換しているだけですね。

そしてCOMPUTE_EYEDEPTHは「そのz値を格納している」と書きました。

これはカメラから見た頂点のz値を計算しているわけです。

デプスの算出

次に見るのはデプスの算出部分です。
コードを抜粋すると以下の箇所。

float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));

UNITY_PROJ_COORD

順番に見ていきましょう。
UNITY_PROJ_COORDの定義は以下です。なにもせずにそのまま値を返しています。
凹みさんの記事によるとPS Vitaのときだけ違う計算が行われるようですが、基本的にここは無視してよさそうです。

#define UNITY_PROJ_COORD(a) a

SAMPLE_DEPTH_TEXTURE_PROJ

次はSAMPLE_DEPTH_TEXTURE_PROJ
これも定義を見てみましょう。

#   define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv).r)

内容はシンプルですね。
tex2Dprojのラッパーになっていて、さらにフェッチしたテクスチャの値のr成分だけを返しています。

元々引数に取るテクスチャはデプステクスチャなので、対象のr成分だけで大丈夫なわけですね。

ちなみにtex2Dprojについてですが、以前書いた以下の記事で少しだけ言及しています。

edom18.hateblo.jp

引用すると以下のような計算を行っています。

tex2Dprojは、該当オブジェクトにテクスチャを投影するような形でテクセルをフェッチします。 つまり、同次座標系で見た場合に、該当のテクセルがどうなるか、を計算しているわけです。

具体的には、以下のように自前で計算することでも同じ結果を得ることができます。

float2 uv = i.uvgrab.xy / i.uvgrab.w;
half4 col = tex2D(_GrabTexture, uv);

要は、Z方向の膨らみを正規化することで2D平面(ディスプレイ)のどの位置に、該当オブジェクトのピクセルがくるのか、を計算しているわけですね。

イメージ的には以下の図のような感じです。

f:id:edo_m18:20190729114442p:plain

これは以前書いた以下の記事から引用したものです。

qiita.com

これでテクスチャからの値を取得することができました。
しかしまだこのままの値では、前段で説明した「ビュー空間でのz値」と組み合わせて計算を行うことができません。

なぜなら、テクスチャからフェッチした値はまだビュー空間での値ではないからです。
そこで利用するのが最後のLinearEyeDepthです。

LinearEyeDepth

こちらもさっそく定義を見てみましょう。

inline float LinearEyeDepth( float z )
{
    return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

計算自体はシンプルですが、ぱっと見なにをしているか分かりませんね。
まず_ZBufferParamsがなにかを突き止めましょう。ドキュメントは以下です。

docs.unity3d.com

ドキュメントによると以下のように記述されています。

Z バッファ値をリニア化するために使用します。 x は (1-far/near)、 y は (far/near)、 z は (x/far)、 w は (y/far) です。

これを展開して整理すると以下のような計算を行っていることになります。


\begin{eqnarray}
d &=& \frac{1.0}{\frac{x}{far} * z + \frac{y}{far}}  \\\
&=& \frac{1.0}{\frac{xz + y}{far}}  \\\
&=& \frac{far}{xz + y}
\end{eqnarray}

値の意味は分かりましたが、なぜこんな計算になるのでしょうか。
これを紐解くために、いつもお世話になっているマルペケさんの以下の記事を参考に考えてみます。

marupeke296.com

マルペケさんの記事にも書かれている通り、深度値は線形な変化をしません。むしろ極端な変化をします。
マルペケさんの記事から画像を引用させていただくと、以下のように極端なグラフになります。

深度値グラフ

だいぶ極端なグラフです。
これを線形に戻すということは、このグラフを求める計算の逆関数を求めればいいことになります。

ただ、マルペケさんの記事で紹介されていた計算式の逆関数を求めてもLinearEyeDepthの実装のようにはならず、クロスプラットフォーム対応など他の要素の兼ね合いでこうなっていると思うので詳細までは分かりませんでした;(詳しい方いたらコメントください・・・)

とはいえ、ここで行っているのはこの極端なグラフを線形に変換するのでLinearEyeDepthという名前がついているということですね。


余談

ちなみにマルペケさんの記事で紹介されていた式は以下です。


d = \frac{fZ}{z} \biggl(\frac{z - nZ}{fZ - nZ}\biggr)

これの逆関数は以下になりました。


f^{-1}(x) = \frac{-nZfZ}{x(fZ - nZ) - fZ}

デプスを用いたフェード処理

最後の部分は実際に計算を行っている部分です。
コードを抜粋すると以下の部分ですね。

float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos)));
float partZ = i.projPos.z;

// フェード処理
float fade = saturate(_InvFade * (sceneZ - partZ));
col.a *= fade;

LinearEyeDepthを用いて深度バッファから得た深度値を線形に変換し、さらに今計算中のフラグメントの深度値を利用してフェードする係数として利用しています。(sceneZ - partZ

_InvFadeはどの程度フェードさせるかをプロパティから設定するための変数です。
これを0 ~ 1の範囲にクランプし、アルファに乗算してやることで今回のフェードを実現している、というわけです。

depthTextureModeをオンに

プラットフォームの設定などによってはなにもしないとDepth Textureが取れない場合があるので、その場合は明示的にオンにしてやる必要があります。
具体的には以下のようにスクリプトから設定してやる必要があります。
(なにか適当なスクリプトから、メインカメラに対して以下を実行してやればOKです)

GetComponent().depthTextureMode |= DepthTextureMode.Depth;

最後に

色々と深度値周りの計算について見てきました。
利用するにあたっては以下の2点の違いに注意が必要です。

  • 通常の計算で出た深度値
  • 深度バッファから得られる値

ただ、これが分かってしまえばあとは単純な数値比較のみになるので、実際の計算自体はむずかしくないと思います。

またソフトパーティクル以外でも、深度値を取得してなにかをするというのは利用価値があると思うので覚えておくといいテクニックですね。

Magic Leap One開発入門

概要

Magic Leap Oneの開発を始めたのでそれに対する諸々をメモしておこうと思います。
モバイル開発同様、ちょっとした設定や証明書の設定など初回しかやらないことなどは忘れがちなので。

※ なお、本記事はUnity2019の、Lumin OS向けの開発がデフォルトで採用されたあとの話となります。
※ 今回追記した内奥はUnity2019.2.15f1で実行し、Magic Leapのバージョンは0.98.0です。

まずはCreator Portalにログイン

まずはMagic Leapの開発者サイトであるCreator Portalにログインします。
ドキュメントやMagicLeap向け開発のセットアップ方法などの情報、またSDKのダウンロードなどが行えるので必須です。

creator.magicleap.com

UnityのプロジェクトをMagic Leap向けに設定する

公式のドキュメントは以下を参照ください。

developer.magicleap.com

またUnityに対するドキュメントはこちらにあります。

Unity2019以降をインストール

Magic Leap開発をするにはUnity2019以降のバージョンが必要です。それ以降のバージョンであればサポートプラットフォームにLumin OSが含まれています。

Settingsを調整

プラットフォームをLumin OSに

新規プロジェクトを作成、Unityを起動する際、プラットフォームをLumin OSにします。

ライブラリ・ツールをインストール

The Labをインストール

以前はMagic Leap Package Managerというアプリから諸々インストールなどを行っていたのですが、現在はThe Labというアプリからインストールやダウンロードを行うようになっています。
なのでまずはこのアプリをインストールします。

パッケージをインストール

XR Managementをインストール

まず、Package Manager WindowからXR Managementをインストールします。

f:id:edo_m18:20200128162530j:plain

インストールを行うと「Project Settings」に以下のような項目が追加されます。

f:id:edo_m18:20200128162645j:plain

ここから、Magic Leap Loaderという項目を追加します。(初回はダウンロードが必要です)
また同時に、Input Handlerという項目を選択すると必要なパッケージがまだインストールされていない場合はボタンが表示されるのでそこからインストールを行います。

MLRemote用ライブラリをインポート

もし以前のものを使っている場合は新しくインポートし直したほうがいいかもしれません。
以下のように、Magic Leap > MLRemote > Import Support Librariesから必要なライブラリをインポートします。

f:id:edo_m18:20200128162856j:plain

ここまでのセットアップが終わったら、Launch Zero Iterationを実行することでZero Iterationアプリが起動します。

起動後、実機を持っている人はターゲットを実機に変更する必要があります。以下の図の部分を対象デバイスに変更してください。

f:id:edo_m18:20200128163301j:plain

あとはEditor上でプレイボタンを押すと実機で確認することができるようになります。

Lumin SDKをPreferenceに設定

次に、Preferenceから、Magic Leap向けのSDKを設定します。

f:id:edo_m18:20190710133652p:plain

Build Settingsを設定

Player SettingsColor SpaceLinearに変更します。
次に、XR SettingsVirtual Reality Supportedをオンにし、プラットフォームにLuminを追加します。
またStereo Rendering ModeSingle Pass Instancedに変更します。

パッケージのインポートとPrefabの配置

Magic LeapのUnityパッケージをインポートする

Magic LeapのUnityパッケージは、上記ポータルからDownload / InstallしたMagic Leap Package Managerを起動するとパッケージのDownloadなどができます。

起動し、インストールが済むと以下のような画面にUnityパッケージが保存されている場所が表示 されるので、そこからパッケージをインポートします。

f:id:edo_m18:20190710132640p:plain

Magic Leap向けのカメラPrefabをシーン内に配置する

該当のPrefabはAssets/MagicLeap/CoreComponents/内にあります。

f:id:edo_m18:20190528133825p:plain

アプリに署名する

Magic Leapのアプリをビルドするために、アプリに署名をする必要があります。
署名するためには証明書を作成し、適切に設定します。

Magic Leapのポータルにログインすると、証明書を作成するページがあるのでそこでIDなどを登録します。

f:id:edo_m18:20190710171053p:plain

すると、秘密鍵などがまずダウンロードされます。
その後しばらくしてページをリロードすると、上記画像のように右側のダウンロードボタンから証明書をダウンロードすることができるようになります。

それを最初にダウンロードされたフォルダに入れ、そのフォルダごとUnityのプロジェクトに追加します。
ちなみにAssets配下である必要はないので、同階層などに置いておくといいと思います。

その後、Player SettingsのPublishing Settingsで上記の証明書を設定します。

f:id:edo_m18:20190710171211p:plain

コントローラを使う

コントローラのイベントをトラッキングする

UnityEngine.XR.MagicLeap namespaceにあるMLInputを利用します。
以下は簡単に、トリガーのDown / Upのイベントを購読する例です。

using UnityEngine.XR.MagicLeap;

private void Start()
{
    MLInput.Start();
    MLInput.OnTriggerDown += OnTriggerDown;
    MLInput.OnControllerButtonDown += OnButtonDown;
}

private void OnTriggerDown(byte controllerId, float triggerValue)
{
    // do anything.
}

private void OnButtonDown(byte controllerId, MLInputControllerButton button)
{
    // do anything.
}

コントローラのバイブレーションを利用する

バイブレーションを利用するにはMLInputControllerStartFeedbackPatternVibeメソッドを使います。

private MLInputControllerFeedbackPatternVibe _pattern = MLInputControllerFeedbackPatternVibe.ForceDown;
private MLInputControllerFeedbackIntensity _intensity = MLInputControllerFeedbackIntensity.Medium;

// ... 中略 ...

MLInputController controller = _controllerConnectionHandler.ConnectedController;
controller.StartFeedbackPatternVibe(_pattern, _intensity);

コントローラを使ってuGUIを操作する

MagicLeapのSDKの中にExamplesがあるので、それをベースにセットアップするのが早いでしょう。

CanvasのRender ModeをWorld Spaceに変更する

Magic Leapでは、uGUIを空間に配置する必要があるため、uGUIのCanvasのRender ModeをWorld Spaceに変更する必要があります。

ポイントとしては、uGUIのEventSystemオブジェクトにMLInputModuleコンポーネントを追加します。
またそのコンポーネントに、対象となるCanvasを設定します。
どうやら、Lumin SDK 0.21.0からはこの設定はいらなくなったようです。

ちなみに、0.20.0の場合は以下のように設定項目があります。

f:id:edo_m18:20190529133115p:plain

また、対象シーンにあるControllerオブジェクトをシーン内に配置します。
いちおうこれだけでも動作しますが、レーザーポインタみたいなオブジェクトなどは表示されないのでちょっと操作しづらいです。
なので、同シーンに配置されているInputExampleコンポーネントを利用するとそれらが視覚化されます。

ただ、Exampleと名前がついているので、これを複製して独自にカスタムしたほうがよいでしょう。

複数Canvasを使う場合

前述のように、MLInputModuleCanvasを設定する必要があります。
しかし複数のCanvasがシーン内にある場合は、そららを設定することができません。

こちらも前述のように、Lumin SDK 0.21.0からは不要となりました。

0.20.0時代に調べていたら、Magic Leapのフォーラムでまさに同様なことが語られていました。(フォーラムは以下)

forum.magicleap.com

どうやら、Canvasに対してMLInputRaycasterをアタッチすることで複数Canvasでも問題なく動作させることができるようです。
このMLInputRaycasterをアタッチするのは0.21.0でも同様に必要なようです。

ハンドトラッキングを使う

MLにはハンドトラッキングの機能も標準で搭載されています。

ハンドトラッキングを開始する

まずはハンドトラッキングを開始するためにMLHands.Start();を実行します。
実行に失敗したかをチェックして、問題がなければハンドトラッキングが開始されます。

MLResult result = MLHands.Start();

if (!result.IsOk)
{
    Debug.LogErrorFormat("Error: HandTrackingVisualizer failed starting MLHands, disabling script. Reason: {0}", result);
    enabled = false;
    return;
}

ハンドトラッキングを検知する

まず、(必要であれば)MLHandType型のプロパティを用意し、どちらの手のトラッキングをするかを決められるようにしておきます。

private MLHand Hand
{
    get
    {
        if (_handType == MLHandType.Left)
        {
            return MLHands.Left;
        }
        else
        {
            return MLHands.Right;
        }
    }
}

こんな感じ。

そして、対象の手の状態がenumで取得できるので、以下のように評価します。

if (Hand.KeyPose == MLHandKeyPose.Thumb)
{
    // do something.
}

上の例ではサムズアップの状態になったら呼ばれるようにしています。
こんな感じで、MagicLeapが用意してくれている手の形を検知するとそれを知ることができるので、それに応じて処理を分岐させます。

レンダリング

MagicLeapではStereo Rendering ModeにSingle Pass Instancedを使うことができます。
ただこれを利用すると、自作シェーダなどの場合はSingle Pass Instancedに対応した形にしないと正常に動作しなくなります。

このあたりについては凹みさんの記事に詳細が書かれているのでそれを参考にさせてもらいました。
ここではポイントだけ記述します。詳細について知りたい方は凹みさんの記事をご覧ください。

tips.hecomi.com

tips.hecomi.com

シングルパス対応のためにIDを適切に扱う

そもそもなぜ、Single Pass Instancedにすると正常に描画されないのでしょうか。
その理由は、左右の目用のレンダリングを一度、つまりシングルパスで行うためそれに対応する処理を追加しなければならないためです。

より具体的に言えば、GPU Instancingを利用してオブジェクトを1ドローコールで両目用にレンダリングします。また、レンダーターゲットアレイというものを利用してレンダーターゲットを複数(左右の目分)用意しそれを利用して描画します。

つまり左右の目それぞれのオブエジェクトごとに固有の行列などを利用する必要があり、それを適切にセットアップしないとならないのがその理由です。

シェーダ内部ではunity_InstanceIDというstatic変数経由で、現在レンダリング中のオブジェクトの配列のインデックスを取得します。
つまりはこれを適切にセットアップし、配列から情報を取り出すことができれば正常にレンダリングされるようになる、というわけです。

コードセットアップ

なぜこれらの処理が必要なのかは上で紹介した凹みさんの記事にとてもとても詳しく書いてあるので、内部的にどういうことをやっているのかを知りたい方は凹みさんの記事を参考にしてください。

ここではベースとなるシンプルなシェーダに追記していく形で、ざっくりとだけまとめます。

ということで、まずはUnlitなシンプルなシェーダを載せます。
これは、Unityで「Create > Shader > UnlitShader」として生成されたものから、Fog関連の記述を消したものです。

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

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

これをそのままマテリアルにして適用するとSingle Pass Instancedの設定の場合は片目になにも描画されなくなります。

pragmaを設定する

まず#pragma multi_compile_instancingを追加します。

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing // 追加

これを追加すると、以下のようにマテリアルのインスペクタにEnable GPU Instancingという項目が追加されます。(当然チェックを入れます)

f:id:edo_m18:20190711165358p:plain

これでGPU Instancingを利用する準備ができました。
以下から、このインスタンシングを利用するためのコードに変更していきます。

コードをインスタンシング対応のものにする

手始めに頂点/フラグメントシェーダの入力の構造体にマクロを追加します。

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

    UNITY_VERTEX_INPUT_INSTANCE_ID // 追加
};

struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;

    UNITY_VERTEX_INPUT_INSTANCE_ID // 追加
    UNITY_VERTEX_OUTPUT_STEREO     // 追加
};

これらはGPU Instancingを利用するにあたってインスタンスのIDを適切に扱うためのものになります。

そして次に頂点シェーダにもマクロを加えます。

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);
    return o;
}

インスタンスIDのセットアップとフラグメントシェーダへの出力を設定します。

次に、コンスタントバッファの宣言を追加し、フラグメントシェーダで利用できるようにします。

UNITY_INSTANCING_BUFFER_START(Props)
    UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)

ちなみにコンスタントバッファ(定数バッファ)は、GLSLで言うところのuniform変数やuniform blockに相当するものです。(以下の記事を参考に)

docs.microsoft.com

なので、C#側から送る値だったりインスペクタで設定するプロパティだったりは、個別に必要なデータに関してはこのコンスタントバッファの定義方法を用いて適切に設定する必要があります。

具体的には、uniformとして定義する変数はほぼそれで定義しておくと考えるといいと思います。

最後にフラグメントシェーダです。

UNITY_SETUP_INSTANCE_ID(i);

// ... 中略 ...

col *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

頂点シェーダから送られてきたインスタンスIDを取り出し、適切にパラメータを扱います。
上記のコンスタントバッファのところでも説明しましたが、通常のシェーダであればuniformな変数_Colorを定義しそれを利用するだけでよかったものを、上記のようにマクロを経由して使う必要がある、というわけです。

余談

ちなみに、凹みさんが記事を書いているときのUnityのバージョンの問題なのか、Unity2019.1.4f1では以下のようにしないとエラーになっていたので書き換えました。

        UNITY_INSTANCING_BUFFER_START(Props)
            UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
        UNITY_INSTANCING_BUFFER_END(Props)

// ... 中略 ...

col *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

マクロ展開後のコード例

最後に、上記のマクロを展開したらどうなるかをコメントしたコード全体を載せておきます。
なお、以下のコードの展開例はあくまで一例です。グラフィクスAPIやその他の設定に応じていくつかの分岐が存在するため、詳細について知りたい方はUnityInstancing.cgincHLSLSupport.cgincを適宜参照してください。

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

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing

            #include "UnityCG.cginc"

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

                // uint instanceID : SV_InstanceID;が追加される
                UNITY_VERTEX_INPUT_INSTANCE_ID // 追加
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;

                // uint instanceID : SV_InstanceID;が追加される
                UNITY_VERTEX_INPUT_INSTANCE_ID // 追加

                // 以下のマクロを経由して「stereoTargetEyeIndexSV, stereoTargetEyeIndex」が追加される。
                // #define UNITY_VERTEX_OUTPUT_STEREO DEFAULT_UNITY_VERTEX_OUTPUT_STEREO
                // #define DEFAULT_UNITY_VERTEX_OUTPUT_STEREO uint stereoTargetEyeIndexSV : SV_RenderTargetArrayIndex; uint stereoTargetEyeIndex : BLENDINDICES0;
                UNITY_VERTEX_OUTPUT_STEREO     // 追加
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            // #define UNITY_INSTANCING_BUFFER_START(buf) CBUFFER_START(buf)
            // #define CBUFFER_START(name) cbuffer name { 
            UNITY_INSTANCING_BUFFER_START(Props)

                // #define UNITY_DEFINE_INSTANCED_PROP(type, var)  type var;
                UNITY_DEFINE_INSTANCED_PROP(float4, _Color)

            // #define CBUFFER_END };
            UNITY_INSTANCING_BUFFER_END(Props)

           // これを展開して整理すると以下のような形になります。
           // cbuffer UnityInstancing_Props { struct {
           //    fixed4 _Color;
           // } PropsArray[2]; }

            v2f vert (appdata v)
            {
                v2f o;

                // DEFAULT_UNITY_SETUP_INSTANCE_IDはいくつか定義が分散しているので詳細は「UnityInstancing.cginc」を参照。
                // #define UNITY_SETUP_INSTANCE_ID(input) DEFAULT_UNITY_SETUP_INSTANCE_ID(input)
                // #define DEFAULT_UNITY_SETUP_INSTANCE_ID(input) { UnitySetupInstanceID(UNITY_GET_INSTANCE_ID(input)); UnitySetupCompoundMatrices(); }
                UNITY_SETUP_INSTANCE_ID(v);

                UNITY_INITIALIZE_OUTPUT(v2f, o);

                // DEFAULT_UNITY_INITIALIZE_VERTEX_OUTPUT_STEREOもいくつか定義があるので「UnityInstancing.cginc」を参照。
                // #define UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output) DEFAULT_UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output)
                // #define DEFAULT_UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output) output.stereoTargetEyeIndexSV = unity_StereoEyeIndex; output.stereoTargetEyeIndex = unity_StereoEyeIndex;
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);

                fixed4 col = tex2D(_MainTex, i.uv);

                // #define UNITY_ACCESS_INSTANCED_PROP(arr, var) arr##Array[unity_InstanceID].var
                col *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);

                return col;
            }
            ENDCG
        }
    }
}

開発環境についてのメモ

Magic Leap Remote for Unityを使って実機でプレビューする

まずはドキュメント。

creator.magicleap.com

Magic Leap Remoteを起動する

Remoteでの確認をするのに、専用のアプリを利用します。
ドキュメントは以下。

Developer Portal | Magic Leap

適切にセットアップが終わっていれば、Magic Leap Package Managerがインストールされているはずなので、その中のLumin SDKをリストから選択、さらにその後Use ML Remoteボタンを押してアプリを起動します。

f:id:edo_m18:20190528161618j:plain

また、Unity Editorから実機に転送できるようセットアップを行います。

Player Settingsから、Windows向け設定のAuto Graphics API for Windowsのチェックをはずし、OpenGL Coreをリストに追加します。

f:id:edo_m18:20190528162003g:plain

続いて、必要なパッケージをインポートします。
Magic Leap向けのプラグインをインポートしている場合は、メニューにMagic Leap用のものが追加されているので、そこから必要なパッケージをインポートすることができます。

Magic Leap > ML Remote > Import Support Librariesに該当のライブラリがあります。

f:id:edo_m18:20190528162232p:plain

そして同じメニュー内にMagic Leap > ML Remote > Launch MLRemoteと、MLRemoteを起動する項目があるので起動します。

起動すると以下のようなウィンドウが表示されるので、Start Deviceボタンを押下して実機に接続します。

f:id:edo_m18:20190528163228j:plain

あとはUnity Editorのプレイモードに入れば、自動的に描画結果が実機に転送されプレビューできるようになります。

コマンドラインを扱う

Magic Leap OneのSDKにはコマンドラインツールも含まれています。

利用するのはmldb.exeです。
コマンド自体は以下のようなパスに保存されています。(デフォルト設定の場合)

C:\Users\{USER_NAME}\MagicLeap\mlsdk\{VERSION}\tools\mldb\

接続されているデバイスのリストを表示する

$ mldb devices

コマンドラインからmpkファイルをインストールする

mpkファイルをコマンドラインからインストールするには以下のようにします。

$ mldb install /path/to/any.mpk

また、すでにインストール済のものを上書きインストールする場合は-uオプションを使用します。

$ mldb install -u /path/to/any.mpk

コマンドでできることをまとめてくれているサイトがあったので紹介しておきます。

littlewing.hatenablog.com

UnityのProfilerにつなぐ

こちらのフォーラムの質問にありました。

引用させてもらうと、以下のようにすることでUnityのプロファイラに接続することが出来ます。

Here’s my steps to profile:

  1. In a console: mldb forward tcp:55000 tcp:55000
  2. Build Settings: Check Development Build and Autoconnect Profiler
  3. Build app, deploy and run on device
  4. Open Profiler (Window > Analysis > Profiler) Editor > 127.0.0.1

その他Tips

Lumin OSを選択している場合のPlatform Dependent Compilation

PLATFORM_LUMINを利用する。

#if PLATFORM_LUMIN
// for lumin
#endif

Magic Leap Oneの映像をモバイルのコンパニオンアプリにミラーリングする

まだbeta版のようですが、コンパニオンアプリを使うことでミラーリングすることができます。
(ただし、コンパニオンアプリはアメリカのStoreでしか落とせないのでちょっとごにょごにょしないと手に入りません。無料です)

www.magicleap.care

ハマった点

ARKit関連でLinker error

ARKitを使っているARプロジェクトなどをMagic Leapに移植しようとして、ARKit関連のSDKが残ったままだと以下のようなエラーが出てしまいます。

In function `UnityARVideoFormat_EnumerateVideoFormats_m1076262586' : undefined reference to `EnumerateVideoFormats'

利用している箇所で#if UNITY_EDITORのみとなっている箇所が、Luming OSプラットフォームだとDLLを参照しにいこうとしてコケるやつです。
なので、分岐を追加することで回避できます。

こちらの記事にも似たようなことが書かれています。
(ただ、バージョン違いなのか自分の環境ではARVideoFormat.csというファイル名でした)

bitbucket.org

PlacenoteなどのライブラリをSymboliclinkで参照を作る

上記と似たような問題ですが、今回の開発では元々ARKit向けに作っていたものを改修する形で対応しました。
なので元々ARKit用のプラグインなどが入っていて、いくつかのライブラリに関してはそのままでも大丈夫だったのですが、場合によってはビルドがまったくできなくなってしまいます。

そこで、各プラットフォームごとに必要なSDKなどをSymboliclinkにして読み込ませる、という方法を取りました。

もっとスマートなやり方がある気もしますが、ビルド時に対象フォルダを外す、などはあまり簡単にできそうではなかったのでこの方法を選択しました。
(もし他の方法を知っている人いたら教えてください)

$ new-item -itemtype symboliclink -path D:\MyDesktop\UnityProjects\078_ar_city\Assets -name Placen
ote -value D:\MyDesktop\UnityProjects\078_ar_city\Placenote

satococoa.hatenablog.com

ネットワークで通信ができない

最初にビルドしたときはできていた気がしたんですが、途中からなぜかネットワークに接続できない現象が。

調べてみたら、以下の記事がヒット。
ただ、最終的にはマニフェストファイルで解決したんですが、ランタイムで個別に権限の確認などが行えるスクリプトが最初から用意されているらしく、メモとして残しておきます。

medium.com

簡単に説明しておくと、Privilege RequesterというコンポーネントをつけてLocal Area Networkを追加するだけでいいようです。

OSやSDKのupdateに伴う変更

Magic LeapにはPackageManagerがあるので、それでSDKなどのバージョン管理などを行います。
なのでアップデートがあったときはそこからインスールし、さらにバージョン管理も(フォルダ分けも)自動的に行ってくれるので非常に楽です。

が、Unity側で指定しているSDKのパスは、エディタ上で設定を変更しないとなりません。
設定自体は特にむずかしいことはないのですが、しばらく開発をしていて久々にアップデートした際に忘れがちになるので、メモとして残しておきます。

設定自体はAndroid SDKなどと同様に、Editor > Preferencesから開く設定画面で、以下の箇所に設定項目があります。

f:id:edo_m18:20190603140823p:plain

イベントを登録しているとuGUIへのイベントが発火しない?

ちょっとまだしっかりと調査していないのですが、ちょっとハマったのでメモ。
MLInputの各種イベントを購読して処理をしていたら、その処理を追加したあとなぜかuGUIへのイベントの発火が止まってしまい、uGUIを操作できなくなった。

処理終了後にそれらのイベントを購読解除したら正常に動いた。

最後に

ARKitを用いたモバイルARと、いわゆるARグラスを用いたAR体験はだいぶ色々なものが異なるので、プロジェクト的に一緒に開発していくのはややきびしいかもなーというのが正直な感想でした。

一番感じた点としては、ARKitなどのモバイルAR開発であったとしても、ARグラス向けを意識して開発しておくと後々幸せになれるかもしれません。(Hololens2も控えているし)

Swiftでネイティブプラグインを作る

概要

以前、Objective-Cを用いたプラグイン生成については記事を書きました。

edom18.hateblo.jp

今回はSwiftを用いたプラグイン作成方法です。
Unity側で準備する内容は基本的には大きく違いませんが、Xcode側でやることあったり、SwiftとObjective-Cとの連携周りについての調整が多く、そのあたりを中心にまとめておきたいと思います。

ちなみに、大本はこちらの記事を参考にさせていただきました。

qiita.com

SwiftからObjCを呼び出すためのBridge用ファイルを生成・設定する

通常、Xcodeプロジェクトから生成したものであればブリッジが必要になったタイミングでXcodeが聞いて自動的に生成してくれるようですが、Unityからだとそれが行われないので自分で追加、設定する必要があります。

ファイル名は任意で、必要な項目にそのファイルを設定するだけです。

設定箇所は「Swift Compiler - General > Objective-C Bridging Header」です。

$(SRCROOT)とするとプロジェクトフォルダまでのパスを設定してくれるので、あとは作成したファイル名を続けて入力すると無事に設定されます。

$(SRCROOT)/Bridging-Header.h

f:id:edo_m18:20190606170511p:plain

ヘッダファイルの中身は、Swiftから呼ばれる可能性のあるものをインポートしておきます。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "UnityInterface.h"

Objective-CからSwiftのメソッドを呼び出す

Swiftのバージョンなどによって挙動が違うようですが、Swift4から(?)明示的に書かないとObjCに対して公開されないようになったようです。
そのため、ObjCに対して公開する場合は@objcをつけて宣言する必要があるようです。

class ExampleClass : NSObject {
    @objc static func swiftMethod(_ message: String) {
        print("\(#function) is called with message: \(message)")
    }
}

ObjCから呼び出している箇所はこんな感じ↓

extern "C" {
    void _ex_callSwiftMethod(const char* message) {
        [ExampleClass swiftMethod:[NSString stringWithUTF8String:message]];
    }
}

ちなみに、この@objcが必要な理由はこちらの記事で解説されているので、興味がある人は読んでみるといいかもです。

qiita.com

Objective-CからSwiftのクラスをイニシャライザを使って生成する

例えば以下のようなSwiftクラスを定義したとします。

public class SwiftClass : NSObject {
    var name: String?
    var age: Int?
    @objc public init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

これを、Objective-Cで利用するには以下のようにします。

SwiftClass *aClass = [[SwiftClass alloc] initWithName:@"hoge" age:@20]

Tips

< Product_Module_Name>-Swift.hCommand + Left Clickすると定義に飛べるので、自動生成したヘッダファイルを見て、Swiftのメソッドがどうエクスポートされているかを確認するとエラー解消が早いかもしれません。
Product_Module_NameはBuild Settingsで確認することができます)

ちなみに、以下のようなクラス・メソッドの場合のエクスポート例です。

Swiftの定義は以下。

import Foundation

public class Callback : NSObject {
    var objectName: String?
    var methodName: String?
    
    @objc public init(objectName: String, methodName: String) {
        self.objectName = objectName
        self.methodName = methodName
    }
    
    @objc public func Picked(date: String) {
        UnitySendMessage(objectName, methodName, date)
    }
}

ヘッダには以下のように書き出される。

@interface Callback : NSObject
- (nonnull instancetype)initWithObjectName:(NSString * _Nonnull)objectName methodName:(NSString * _Nonnull)methodName OBJC_DESIGNATED_INITIALIZER;
- (void)PickedWithDate:(NSString * _Nonnull)date;
- (nonnull instancetype)init SWIFT_UNAVAILABLE;
+ (nonnull instancetype)new SWIFT_DEPRECATED_MSG("-init is unavailable");
@end

Objective-Cからはこんな感じで呼び出します。

Callback *callback = [[Callback alloc] initWithObjectName:objStr methodName:methodStr];
[callback PickedWithDate:@"2019/06/07"];

その他気になったところ

Swift Versionの設定

上記設定をしてビルドをしようとすると、Swiftのバージョンについての問題を指摘されてエラーとなりビルドが失敗します。

設定項目があってそこを設定するだけなので、該当エラーが出たら以下の箇所を適切に設定してください。

f:id:edo_m18:20190606172936p:plain

stackoverflow.com