最近、Facemotion3dというiOSアプリをリリースしました。
これは、iOSでフェイストラッキングが出来るアプリです。
このアプリの中には「UaaL」と呼ばれている技術が使われています。
#目次
1.UaaLとは
2.UaaLが使用されているアプリ
3.SceneKitとの併用
4.SceneKitの面倒くささ
5.アプリ起動直後ではない方法でUaaLを使用する
6.UaaL実装においてうまく行かなかった点
#1. UaaLとは
UaaLとは「Unity as a Library」の略で、その名の通りUnityをライブラリとして使うものです。
大元の記述をSwiftというiOSネイティブの言語で記述し、Unityをライブラリ(framework)として使用することで、UI部分はSwiftを使いつつ、3DCGの描画にはUnityを使うというようなSwiftとUnityの良い所取りが出来ます。
Unityでスクロール画面を作るよりも、Swiftでのスクロール速度の方が早かったりする利点があります。また、ARKitを使用する場合、「純正Unityだと30FPSしか出ないのに対して、Swiftだと60FPSを出せる...」という話をTwitter上で見かけたりしたので、本当かは分かりませんがそういう利点があります。
#2. UaaLが使用されているアプリ
偉大な先人としてnoppeさんのvearや、Realityなどで使用されています。
そういった記事を参考にさせて頂いてFacemotion3dにもUaaLを使っています。
以下の記事などを参考にさせて頂きました。
https://qiita.com/noppefoxwolf/items/b43d8554142e69c2ada6
https://qiita.com/tkyaji/items/7dbd56b41b6ac3e72635
https://forum.unity.com/threads/integration-unity-as-a-library-in-native-ios-app.685219/
#3. SceneKitとの併用
Facemotion3dでは、Apple標準のビューポートを使用する「SceneKit」とUnityを使用する「UaaL」の併用ということをやっています。
これはどうしてこうなったかというと開発していく流れの中で偶然こうなったという感じです。元々、SceneKitでアプリを作っていて、SceneKitでも十分見栄えのするモデルをモデラーさんが作ってくれました。途中からUaaLについても調べ始めたので後付けでUaaLを載せました。
正確な比較はしたことないので分かりませんが、恐らくメモリの消費量であったり電池の消費量などの面ではUnityを起動しない方が長時間使用する場合、パフォーマンス的にはApple標準のSceneKitを使う方が良いのではないかという気がします。
#4. SceneKitを使う上での面倒くささ
SceneKitにも利点はありますが、Apple標準のビューポートは機能が少なく、プログラミングする上でのネット上の情報が少ないので3DCG部分について記述する上では圧倒的にUnity(UaaL)で記述する方が楽です。
また、UnityではVRMなどを使うとプログラマーからしてみると簡単にデータをアプリ内に読み込めます。
しかし、SceneKitを使う場合、MayaなどのDCCツールでデザイナーからデータを受け取り、それを良い感じに原点付近に来るようにデータを配置して、ピボット情報を修正し、スケール情報を整え、オブジェクトの命名規則をちゃんとチェックしたりした上で、DAE_FBXなどという聞きなれない拡張子でデータをエクスポートする必要があります。
それだけでなく、DAE_FBXを使うだけではデータをアプリ内に読み込めないので、DAE_FBXをテキストファイルで開き、内部の記述をちょっと書き換える必要があります。
これだけでも面倒くさいのですが、MacのXCodeにDAE_FBXを読み込んだ後に.scnというファイル拡張子に変換し、全てのテクスチャパスを手動で張り替えるという面倒くさい手順が必要でした。
モデル変更などでデザイナーさんが頻繁にデータを更新するので、この作業を何度か繰り返す必要があります。
ここら辺の作業は、今後、AppleとPixarが共同で開発しているUSDZという拡張子が台頭することで簡略化されるのだと思われます。
#5. アプリ起動直後ではない方法でUaaLを使用する
参考にさせて頂いたこちらの記事では、アプリ起動直後にUnityが起動する方法が紹介されています。
Facemotion3dでも、前回アプリ終了時にUnityをロードしたまま終了していればアプリ起動直後にUnityが起動する仕組みになっていますが、基本はUaaLではなくSceneKitが起動するようになっています。
前置きが長くなりましたが、ここから実際にFacemotion3d内に記述されているコードを示します。基本的にはnoppeさんの記事の内容を拡張しながら場当たり的に書いていった上に、Swift歴も浅いので関数名の汚さや、記述の汚さが目立つかもしれません...。「適当に書いてたらなんか動いた...!」という類のコードであり、何ら参考にならないかもしれません。
AppDelegate.swift
import UnityFramework
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var application: UIApplication?
var launchOptions: [UIApplication.LaunchOptionsKey : Any]?
var firstLaunchUnity = true
var isUnityRunning = false
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
// see notes below for the meaning of Atomic / Non-Atomic
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
for purchase in purchases {
switch purchase.transaction.transactionState {
case .purchased, .restored:
if purchase.needsFinishTransaction {
// Deliver content from server, then:
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
// Unlock content
case .failed, .purchasing, .deferred:
break // do nothing
@unknown default:
print("unknown")
}
}
}
self.application = application
self.launchOptions = launchOptions
let storyboard = UIStoryboard(name: "ViewController", bundle: nil)
window?.rootViewController = storyboard.instantiateInitialViewController()
window?.makeKeyAndVisible()
return true
}
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "FACEMOJO for DAZ")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
func firstStartUnity()
{
Unity.shared.secondInit()
if let app = self.application
{
Unity.shared.application(app, didFinishLaunchingWithOptions: launchOptions)
window?.makeKeyAndVisible()
}
}
}
class Unity: NSObject, UnityFrameworkListener, NativeCallsProtocol {
static let shared = Unity()
var unityFramework: UnityFramework
//グローバル変数
var myVar = GlobalVar.shared
override init() {
let bundlePath = Bundle.main.bundlePath
let frameworkPath = bundlePath + "/Frameworks/UnityFramework.framework"
let bundle = Bundle(path: frameworkPath)!
if !bundle.isLoaded {
bundle.load()
}
let frameworkClass = bundle.principalClass as! UnityFramework.Type
let framework = frameworkClass.getInstance()!
if framework.appController() == nil {
var header = _mh_execute_header
framework.setExecuteHeader(&header)
}
unityFramework = framework
super.init()
}
//1度UnityをUnloadした後に読み込む場合に対応
func secondInit()
{
if !unityIsInitialized()
{
let bundlePath = Bundle.main.bundlePath
let frameworkPath = bundlePath + "/Frameworks/UnityFramework.framework"
let bundle = Bundle(path: frameworkPath)!
if !bundle.isLoaded {
bundle.load()
}
let frameworkClass = bundle.principalClass as! UnityFramework.Type
let framework = frameworkClass.getInstance()!
if framework.appController() == nil {
var header = _mh_execute_header
framework.setExecuteHeader(&header)
}
unityFramework = framework
}
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
unityFramework.register(self)
FrameworkLibAPI.registerAPIforNativeCalls(self)
unityFramework.setDataBundleId("com.unity3d.framework")
unityFramework.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: launchOptions)
}
private func unityIsInitialized() -> Bool
{
return ( unityFramework.appController() != nil )
}
func unloadUnity()
{
if unityIsInitialized()
{
unityFramework.unloadApplication()
if let appDelegate = UIApplication.shared.delegate as? AppDelegate
{
appDelegate.firstLaunchUnity = true
}
}
}
//Unity側からSwiftに対して送られてくる関数-関数名はUaaLサンプルの初期設定のまま使用
func showHostMainWindow( _ color: String!)
{
//Unityを読み込み終わったことを示すフラグ
self.myVar.UnityLoadFlag = true
}
func sendUnityMessage(objectName:String,functionName:String,message:String)
{
unityFramework.sendMessageToGO(withName: objectName, functionName: functionName, message: message)
}
var view: UIView
{
unityFramework.appController()!.rootView!
}
}
class GlobalVar {
private init() {}
static let shared = GlobalVar()
var UnityLoadFlag = false
var unityFirstLoadFlag = true
}
ViewController.swift
//グローバル変数
var myVar = GlobalVar.shared
//Unityを起動する関数,適当なところで呼び出す
func startUnity()
{
//GUI周りの表示切り替え
DispatchQueue.main.async
{
if self.myVar.unityFirstLoadFlag == true
{
//Unityロゴを隠さないようにするための処理
//UaaLを使うとUnityロゴを隠せてしまうが、それを避ける処理
//self.tappedFunction()
}
//ロード時のくるくる開始
//self.activityIndicator.startAnimating()
//self.activityIndicator.isHidden = false
}
//loadingUnity = true
var waitTimeAfterStopUnity = 0.0
if self.myVar.UnityLoadFlag == true
{
waitTimeAfterStopUnity = 0.5
}
stopUnity()
DispatchQueue.main.async
{
//SceneKitを非表示にする
//self.faceView.isHidden = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + waitTimeAfterStopUnity)
{
if let appDelegate = UIApplication.shared.delegate as? AppDelegate
{
appDelegate.firstStartUnity()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0)
{
DispatchQueue.main.async
{
let unityView:UIView = Unity.shared.view
unityView.isHidden = false
self.view?.insertSubview(unityView, at: 0)
//ロード時のくるくる停止
//self.stop_activityIndicator()
}
/*
//SceneKitのタッチが反応しないようにする(画面回転用)
self.faceView.isUserInteractionEnabled = false
//self.viewのタッチが反応するようにする(画面回転用)
self.view.isUserInteractionEnabled = true
self.view.isMultipleTouchEnabled = true
self.unityView?.isUserInteractionEnabled = true
self.unityView?.isMultipleTouchEnabled = true
*/
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0)
{
if self.myVar.unityFirstLoadFlag == true
{
self.myVar.unityFirstLoadFlag = false
}
}
}
}
}
}
//Unityをアンロードする関数,適当なところで呼び出す
func stopUnity()
{
if self.myVar.UnityLoadFlag == true
{
DispatchQueue.main.async
{
//Bool型をStringで記述するというあほな記述、無視してください;;
self.myVar.UnityLoadFlag = false
let unityView = Unity.shared.view
unityView.removeFromSuperview()
Unity.shared.unloadUnity()
unityView.isHidden = true
//SceneKitを非表示にする
//self.faceView.isHidden = true
}
}
}
#6. UaaL実装においてうまく行かなかった点
1.AVAudioSessionやAVAudioRecorderなどを使った録音、再生周りがうまく動作しなくなった
SceneKitのままだとAVAudioSessionなどがうまく機能しているのですが、UaaLを起動した途端に録音ができなくなりました。理由は不明です。
Unityの使用中だけでなく、Unityをアンロードした後でも同様に録音できないようです。
2.2本指ドラッグを反応させる方法が分からなかった
Unityの画面のズームをさせるために、
self.viewに対してPinchGestureRecognizerを使用しています。
そして、Unity画面の回転のために、以下の記事を参考に1本指の画面ドラッグをself.viewに実装しています。
https://i-app-tec.com/ios/image-drag.html
上記の記事には、画面の2本指ドラッグについても書かれており、SceneKitでは2本指ドラッグが正常に反映されたのですが、UaaLになると2本指ドラッグによるカメラ移動が反応しないという問題が回避できませんでした。原因は不明です。
ドワンゴが作るプラットフォームや規格について思うこと
https://note.com/pekochun/n/n8c5a8f115645
こういうのも書きました。
モーションキャプチャアプリを作っていて色々思うこと
https://note.com/pekochun/n/nf16643bd9f68
Amazonで欲しいものリストを公開しているので、もしお金持ちの方がいたら買ってください><
https://www.amazon.jp/hz/wishlist/ls/1S03PCOA5P7IE