macOS 用 UnityHub および Unity アプリケーションが起動しなくなった件

2023-10-25 UnityHub をアップデートしたところ、UnityHub および Unity アプリケーションが起動しなくなりました。

何とか起動する様になったので、行ったことの備忘録です。まずは注意事項です。

  • この記事に書かれた方法により何らかの問題が発生しても当方では責任を負えません。
  • この記事に書かれた方法で必ずしも解決するとは限りません。
  • この記事に書かれた方法の全てを行わなくても解決するかもしれません。

まず、最終的に解決に至るきっかけとなった操作を記します。

  • キーチェーンアクセスにおいて Unity で検索し、結果として表示された 2 項目を削除した。
    • 項目名はメモしていないが、片方はアカウントに関する情報だったと思う。

試行錯誤している段階で UnityHub と Unity を完全に(?)アンインストールしています。以下のディレクトリの内容を全て削除しました。

アプリケーションが起動しない時にアクティビティモニタを確認したところライセンス関係のプロセスが起動していたので、ライセンス認証まわりで問題があったのではないかと想像しています。

Kotlin コーディング規約

概要

Kotlin コーディング規約から個人的に忘れそうな部分を抜き出した備忘録です。 ここに記載されている以外の内容も有益なので原文の参照をおすすめします。

目次

参考情報

備忘録

Source code organization

Source file organization

複数の宣言を同じ1つのファイルに配置することは、以下の条件を全て満たす場合のみ推奨されます。宣言には、classes, top-level functions, top-level properties などが該当します。

  • これらの宣言が意味的に密接に関連している
  • ファイルサイズが妥当な範囲内(数百行を超えない)

Class layout

  1. Property declarations and initializer blocks
  2. Secondary constructors
  3. Method declarations
  4. Companion object

以下を行ってはいけません。

  • メソッドをアルファベット順に並べる
  • メソッドを可視性で並べる
  • 通常のメソッドと拡張メソッドを分離する

nested classes はそのクラスを使用するコードの次に置きます。nested classes が外部で使用されることを意図している場合は companion object の後に置きます。

Naming rules

package の命名では小文字のみを使用し _ は使用しません。複数語から成る名前は通常は推奨されませんが、使用する必要がある場合は単語を連結する (nameexample) か camel case (nameExample) を使用します。

Function names

クラスのインスタンスを生成する factory functions の場合は戻り値の型と同じ名前を使用します。例えば、戻り値の型が Foo の場合は fun Foo(): Foo { ... } とします。

Property names

以下の val properties には大文字を _ で接続した名前 (EXAMPLE_NAME) を使用します。

  • const が付けられている
  • top-level か object で、カスタムの get function を持たず、変更不可能なデータを保持している

シングルトンオブジェクトへの参照を持つ properties には object 宣言と同じ規則で名前を付けます。つまり、クラスと同様の名前を付けます。

enum constants は以下のいずれかの名前を使います。

  • uppercase underscore-separated names (例えば、RED, BLUE, GREEN)
  • upper camel case names (例えば、Red, Blue, Green)

Names for backing properties

プレフィックスに _ を付けます。

Choose good names

名前の一部に頭文字を使用している場合は以下の規則に従います。

  • 2文字の場合は全て大文字 (例えば、IOStream であり IoStream ではない)
  • 3文字以上の場合は先頭のみ大文字 (例えば、HttpInputStream であり HTTPInputStream ではない)

Formatting

Horizontal whitespace

// の後には空白を入れること。

水平方向で揃えることは避けること。識別子の名前の長さが変わった場合に影響を受けるべきではありません。

Colon

以下の場合は : の前に空白を入れます。

  • type と super type を分ける (例えば、class FooImpl : Foo())
  • 他の constructor に委譲する (例えば、constructor(x: Int) : this(x))
  • object キーワードの後 (例えば、object : IFoo)

Class headers

長いため省略。原文を参照のこと。

Modifiers order

長いため省略。原文を参照のこと。

Annotations

引数ありの注釈を複数並べるときは改行します。引数なし注釈を複数並べるときは1行に並べます。引数なし注釈が1つの場合は宣言と同じ行に書きます。

File annotations

ファイルコメントの後、package 宣言の前に書きます。但し、package 宣言との間に空行を入れ、package ではなくファイルに対する注釈であることを強調します。

Lambdas

パラメーターリストが長すぎて1行に収まらない場合は、-> を別の行に配置します。

Trailing commas

Kotlin スタイルガイドでは宣言時の引数末尾へのコンマ記載を推奨しています。

Documentation comments

通常は @param@return などのタグの使用は避けます。文中に [paramName] と記載します。

Avoid redundant constructs

「わかりやすくするため」だけに不要な構文要素をコードに残さない様にします。例えば、戻り値の型の Unitセミコロン、string template 中の {} などは不要な場合にはなくします。

Idiomatic use of language features

Lambda parameters

短く、ネストされていないラムダでは、it の使用を推奨します。

Named arguments

以下の場合には named argument の使用を推奨します。

  • 同一の基本型を複数とる
  • Boolean 型 (全ての引数が文脈より絶対的に明確である場合を除き)

Nullable Boolean values in conditions

nullable Boolean の比較は value == truevalue == false で比較する。

Loops

基本的には通常の for ループの使用を推奨します。但し、以下の場合は forEach 関数を使用します。

  • forEach のレシーバーが nullable
  • 長い call chain の一部として使う

Functions vs properties

以下の場合は functions より properties を優先して使用します。

  • 例外を投げない
  • 計算が安価、もしくは、一度だけ計算してキャッシュしている
  • オブジェクトの状態が変更されない限り、呼び出しの度に同じ結果を返す

原神(Genshin Impact)をDualSenceでプレイする

概要

原神はDualSenceを無線(Bluetooth)接続した場合に限りDualSenceを使用してプレイできると書かれた記事が見受けられます。 しかし、PC環境によって原神がコントローラーを認識したり、しなかったりといった状況がある様です。 認識されない場合の対処方法にはいくつかの方法がありますが、本記事ではSteamのBig Pictureモードを使用する方法を説明します。

尚、この記事の内容は動作を保証するものではありません。試す場合には自己責任でお願いします。 また、本記事の方法でも動作しない場合、当方では対応しかねます。

(2021-10-06追記:Ver.2.2でDualSenceに正式対応するとのことなので、こちらの方法は不要かもしれません。)

目次

確認環境

  • Windows 10 Pro (21H1)
  • Steam ビルド Sep 16 2021
  • 原神(Genshin Impact) 2.1.0

参考情報

解説

事前にBluetoothのペアリングは済ませてある前提で話を進めます。ペアリングが済んでいない場合には、他の記事を検索すると説明している記事が見つかると思います。以下の画像の様に、「Wireless Controller」が「ペアリング済み」になっている必要があります。

Steamを管理者として実行する

Steamを起動する際には「管理者として実行」を選んで起動する必要があります。

ライブラリに原神を登録する

ライブラリメニューから、「非Steamゲームを追加」を選択して Genshin Impact を追加します。

「参照」ボタンを押下して、exeファイルを選択するウィンドウを開きます。

C:\Program Files\Genshin Impact\Genshin Impact Game フォルダにある GenshinImpact.exe を選択します。

そして、「選択したプログラムを追加」ボタンを押下します。

以上で原神アプリがライブラリに追加されます。

Big Picture モードに切替え、コントローラを設定する

ゲームを起動する際には Big Picture モードから起動します。

まずは、Big Picture モードに切り替えます。

歯車アイコンから設定メニューを開き、コントローラー設定を開きます。

PlayStation 設定サポート」を有効にします。 ついでに「Big Picture 終了時に電源を切る」を有効にすると、Big Picture を終了した際にコントローラーの電源が切れます。

以上で設定は完了です。

ゲームを起動する

ゲームを起動する際には、Big Picture モードのライブラリメニューから GENSHINIMPACT を選択します。

ゲームが開始されるとコントローラーでプレイできる思います。

(管理者として実行されていれば Big Picture モードでなくてもプレイできるみたいです。)

Pythonのプログラムでピボットテーブルを使う

概要

PythonからExcelのピボットテーブルを使いたいが、難しいプログラムを記述したくない場合の方法です。 ピボットテーブルの設定はExcelで行い、Pythonのプログラムではピボットテーブルの値の範囲のみを変更します。

目次

確認環境

参考情報

解説

テンプレートとなるExcelファイルを作成する

まずピボットテーブルを含んだExcelファイルを用意します。 今回は疑似個人情報データ生成サービスを使って生成したExcelファイルに対してピボットテーブルを作成します。 ピボットテーブルの作り方は検索して調べてください。

作成したExcelファイルは以下の通りになっているとします。

  • ピボットテーブルは pivot シートに含まれている
  • データは dummy_data シートに含まれている
  • Excelファイルの名前はPivotTemplate.xlsx

Pythonでピボットテーブルの値の範囲を設定する

Pythonのプログラムで以下の2つを合わせます。

  • PivotTemplate.xlsxに含まれているピボットテーブル
  • 別途作成したデータシート

合わせるためのプログラムは以下のとおりです。

import openpyxl

# データシートを作成する
new_wb = openpyxl.Workbook()
ws = new_wb.create_sheet('data')

# ... wsにデータを追加する ...


def add_pivot(cache_id, data_sheet, destination_book):
    template_wb = openpyxl.load_workbook('PivotTemplate.xlsx')

    pivot_sheet = template_wb['pivot']  # ピボットテーブルが含まれたシート
    pivot = pivot_sheet._pivots[0]
    pivot.name = data_sheet.title + '_pivot'  # ピボットテーブルに名前を付ける
    pivot.cacheId = cache_id  # IDが重複するとエラーになる
    pivot.cache.refreshOnLoad = True  # 読み込み時にピボットテーブルを更新
    source = pivot.cache.cacheSource.worksheetSource
    print(source)

    print(data_sheet.dimensions)
    print(data_sheet.title)
    source.ref = data_sheet.dimensions
    source.sheet = data_sheet.title

    pivot_sheet = destination_book.create_sheet(pivot.name)
    pivot_sheet.add_pivot(pivot)
    template_wb.close()


add_pivot(1000, ws, new_wb)
new_wb.save('PivotSample.xlsx')

まず、はじめにdataという名前のシートをnew_bookに追加して、 このdataシート(ws)にデータを用意しています。

次に、add_pivot関数でピボットテーブルを含むシートを追加しています。 cache_idはピボットテーブルのIDです。 他のピボットテーブルと重複しない値にします。 ここでは重複しないであろう値として適当に1000にしています。 data_sheetはデータを含むシートです。 ここでは最初の段階で作成したdataシート(ws)を指定しています。 destination_bookはピボットテーブルを含むシートを追加する先のブックです。 ここではnew_wbを指定しています。

add_pivot関数で行っている内容を説明します。 まず、テンプレートとなるPivotTemplate.xlsxファイルを開き、ピボットテーブルが含まれるシートからピボットテーブルの情報を取得します。 そして、このピボットテーブルの情報(pivot)を変更しています。 nameは設定しなくて構いませんが、ここではシート名に基づいて名付けています。 cacheIdは他のピボットテーブルと重複しない値を設定する必要があります。 refreshOnLoadTrueに設定しておくとファイルを開いたときにピボットテーブルが更新されます。 次に、ピボットテーブルが参照するデータの範囲を変更します。 refにデータの範囲、sheetにシート名を設定することで行えます。 data_sheetのデータ範囲(data_sheet.dimensions)とシート名(data_sheet.title)を設定しています。 最後に、新規シートにピボットテーブルを追加します。

以上を行ってファイルを保存すれば、ピボットテーブルを含んだファイルが作成されます。

Unity Android Plugin の手間を軽減する管理方法(改)

概要

Unity Android Plugin の解説記事では JAR, AAR ファイルを手動コピーする例が多く見受けられます。 手動コピーでは依存ライブラリのバージョンを管理しづらくなるため、出来る限り手動コピーは避けたいです。 この記事では Gradle の Local リポジトリを経由して Android Plugin を Unity に公開する方法を説明します。

目次

確認環境

  • macOS 10.15.7
  • Android
    • Android Studio 4.1.2
    • minSdkVersion 19
    • buildToolsVersion 30.0.3
    • Gradle 6.5
  • Unity 2019.4.18f1
  • Rider 2020.3.2

参考情報

解説

Android Plugin (AAR) をビルドする

Android Library モジュールを作成し、モジュールの build.gradle を以下の様に記述します。 この例ではモジュール名を sample にしています。

plugins {
    id 'com.android.library'
    id 'kotlin-android'
    id 'maven-publish'
}

ext {
    groupId = "com.example"
    versionName = "1.0"
    unityVersion = "2019.4.18f1"
    unityJar = "/Applications/Unity/Hub/Editor/$unityVersion/PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes/classes.jar"
}

group = groupId
version = versionName

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 1
        versionName versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    compileOnly files(unityJar)
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

// see: https://developer.android.com/studio/build/maven-publish-plugin?hl=ja
afterEvaluate {
    publishing {
        publications {
            release(MavenPublication) {
                from components.release
            }
            debug(MavenPublication) {
                from components.debug
                artifactId = "${project.name}-debug"
            }
        }
    }
}

ポイントは以下の2点です。

  • Maven Publish Plugin を使用する
  • Unity の classes.jar をコピーせずに直接指定する

1つめのポイントとして、 Maven Publish Plugin を使用するために pluginsid 'maven-publish' を追加しています。 また、afterEvaluate 内に publishing の設定を記述しています。 この例の設定では、ビルドバリアント(debug, release)毎に AAR ファイルを生成します。 具体的には、sample-1.0.aar (release) ファイルと sample-debug-1.0.aar (debug) ファイルが生成されます。

2つめのポイントとして、 compileOnly files(unityJar) によって Unity の classes.jar を参照しています。 Windows の場合は unityJar のパスを適切なパスに変更してください。 Unity のバージョンを変える場合には unityVersion を変更します。 implementation ではなく compileOnly にしている理由は、 Unity から AAR ファイルを参照する際に Unity の classes.jar が重複して参照されることを防ぐためです。 implementation にすると Unity でビルドする際にエラーになります。

ビルド設定は以上です。

ビルドを実行する前に、今回は以下のサンプルプログラムを作成します。

package com.example.unity.sample

import com.unity3d.player.UnityPlayer

class KotlinObject {

    fun getActivityName(): String = UnityPlayer.currentActivity.localClassName

}

Android Plugin をビルドし、Local リポジトリに保存する

以下のコマンドを Terminal で実行するとビルドが行われ、その結果が Local リポジトリに保存されます。

project % ./gradlew publishToMavenLocal

Gradle のデフォルトでは、Local リポジトリ~/.m2/repository になっています。 この repository ディレクトリに以下のファイルといくつかの関連ファイルが作成されます。

  • com/example/sample/1.0/sample-1.0.aar
  • com/example/sample-debug/1.0/sample-debug-1.0.aar

以上で Android Plugin の作成と公開は終了です。

アプリに Android Plugin への依存を追加する

Unity で Project Settings > Player > Android > Publishing Settings > Build と辿っていくと以下の設定が表示されます。

f:id:nosix:20210225172356p:plain

画像の様に以下の2箇所にチェックを付けます。

  • Custom Main Gradle Template
  • Custom Base Gradle Template

チェックを付けることで Assets/Plugins/Android ディレクトリに以下の2つのファイルが作成されるはずです。 (作成されない場合は Build を実行してみると作成されると思います。)

  • mainTemplate.gradle
  • baseProjectTemplate.gradle

この2つのファイルを編集します。

// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN

apply plugin: 'com.android.library'
**APPLY_PLUGINS**

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.example:sample:1.0'
**DEPS**}

// ... 以下省略
// ... 以上省略
    repositories {**ARTIFACTORYREPOSITORY**
        google()
        jcenter()
        mavenLocal()
        flatDir {
            dirs "${project(':unityLibrary').projectDir}/libs"
        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

mainTemplate.gradle には implementation 'com.example:sample:1.0' を追加しています。 これは groupId:artifactId:version の形式になっており、groupId と version は Android Plugin の build.gradle で指定した値になっています。 artifactId はモジュール名が使われており、今回は sample (release) か sample-debug (debug) です。

baseProjectTemplate.gradle には mavenLocal()repositories に追加しています。 ファイル内に repositories が2箇所あるので注意して下さい。 buildscript 内の repositories はビルドスクリプト(Gradle)が使用するライブラリのリポジトリを指定しています。 buildscript ではない方の repositories は Android アプリをビルドする際に使用するライブラリのリポジトリ指定です。 ここに Local リポジトリを参照する指定 mavenLocal() を追加します。

アプリをビルドする

今回は以下の Script を作成して動作確認をしました。

using UnityEngine;
using UnityEngine.UI;

public class AndroidExecutor : MonoBehaviour
{
    [SerializeField] private Text result;

    void Start()
    {
        using (var o = new AndroidJavaObject("com.example.unity.sample.KotlinObject"))
        {
            result.text = o.Call<string>("getActivityName");
        }
    }
}

Androidバイスを PC に接続するか Android Emulator を起動した上で、 Build Settings で Android Platform に Switch して、 Run Device にデバイスか Emulator を指定します。 File メニューの Build And Run を選択すると APK ファイルのファイル名を訊ねられないので、 Build Settings の Build And Run は使わずに閉じて File メニューから実行しています。

実行するとText に以下の様に表示されます。

f:id:nosix:20210225175148p:plain

Sharing

概要

NRSDK (Nreal Light の Software Developer Kit) の Observer View を理解するために、 サンプルとして提供されている Sharing (Scene) のソースコードを解読してみます。 個人的な読解結果であるため誤りが含まれている可能性があることをご了承ください。

目次

確認環境

  • NRSDK 1.2.1

解説

Sharing (Scene) の Hierarchy とそこで使われている Scripts を網羅して確認しました。

Sharing 機能は NetWorkSession (Prefab) を Hierarchy に追加して使用します。 NetWorkSession (Prefab) には SharingManager (Component) が設定されており、SharingManager のプロパティでは Player と ObjectPool を設定します。 Player, ObjectPool のいずれにも NetworkBehaviour (Component) を設定した Prefab を設定します。

ObjectPool には共有データを定義した Prefab を登録します。 共有データを NetObject と呼ぶとすると、他ユーザーから受け取る NetObject と自分から送信する NetObject を登録します。 Player には自分が生成する NetObject を定義した Prefab を登録します。

ObjectPool には複数の Prefab が登録されます。 登録する際には NetworkBehaviour を継承したクラスを作成し、作成したクラスを Component にもつ Prefab を作成します。 ObjectPool は NetworkBehaviour を継承したクラスのクラス名を Key、Prefab を Value とする辞書を保持します。 Key は NetObjectInfo.Key として使われます。

Player には単一の Prefab が登録されます。 ObjectPool に登録した Prefab のいずれか 1 つを登録します。 ObjectPool に複数の Prefab を登録するケースは理解できませんでした。 SharingManager は NetWorkSession のシングルトンインスタンスを使用しているため、SharingManager を複数使用してはいけないのではないかと思います。 そのため、Player に登録される Prefab は 1 つだけです。

NetworkBehaviour を継承したクラスの例として TestNetBehaviour が用意されています。 TestNetBehaviour では、共有データを管理します。 共有データはシリアライズして送受信します。 SynObject を継承させたクラスを独自に定義すれば任意のデータを共有できます。 標準では以下の SynObject を継承したクラスが用意されています。

  • SynTransform
  • SynInt
  • SynVector2
  • SynVector3
  • SynQuaternion

また、NetworkBehaviour.IsOwner は自身のセッションで作成した NetObject である場合には true になります。 つまり、Player に指定して生成されたインスタンスでは IsOwner が true になります。

Hierarchy

  • Sharing

    • Main Camera
      • [C] Camera
      • [C] Audio Listener
    • Directional Light
      • [C] Light
    • NetWorkSession (Prefab)
  • NetWorkSession (Prefab) (in Scripts/Sharing/Prefabs)

  • Player (Prefab) (in Demos/Sharing/Prefabs)

Scripts

  • SharingManager (in Scripts/Sharing/Scripts)

    • MonoBehaviour を継承している
    • Inspector の設定項目
      • Player : NetworkBehaviour (Demos/Sharing/Prefabs/Player)
      • Object Pool : NetWorkObjectPool (Scripts/Sharing/Prefabs/NetWorkObjectPool)
    • OnEnable
    • Start
      • Wraper.Initialize
      • ObjectPool.Init
      • NetObjectManager.Init
      • NetWorkSession.SearchLocalServer
        • LocalServerSearcher.Search
          • IP 255.255.255.255 port 1989 UDP に RequestForServerIP を送信する
          • IP:port を受信する
          • LocalServerSearcher.m_LocalServer に IPEndPoint(IP, port) を設定する
    • NetWorkSession.OnGetServerIP
      • EasyClient.ConnectAsync(EndPoint)
    • NetWorkSession.OnClientConnected
      • MsgGroup.LoginEvent.Request(GUID)
      • SharingManager.OnNetConnected
    • OnNetConnected
      • Player が設定されている場合は CreateNetObjRequest(NetworkBehaviour) を呼び出す
    • CreateNetObjRequest
      • MsgGroup.CreateNetObjectEvent.Request(NetworkBehaviour.GetType().Name)
      • OnCreateNetObjectResp
        • NetObjectManager.Create
          • NetWorkObjectPool から NetObjectInfo.Key で NetworkBehaviour を検索する
          • NetworkBehaviour が見つかったら NetworkBehaviour を Instantiate して、Initialize を呼び出す
            • NetObjectInfo.Owner が NetWorkSession.GUID と一致したら、NetworkBehaviour.IsOwner を true にする
          • Instantiate で生成された NetworkBehaviour の GameObject の name を NetObjectInfo.Key に設定する
        • SynContextRequest
          • MsgGroup.SynContextEvent.Request();
    • SynContextRequest
      • MsgGroup.SynContextEvent.Request()
      • OnSynContextResp
        • NetObjectManager.SynObjects
          • 受け取った NetObjectInfo 毎に以下を行う
            • NetObjectInfo.Identify 毎に NetObjectManager.Create を呼び出して NetworkBehaviour インスタンスを生成する
              • NetObjectInfo.Identify が NetworkBehaviour インスタンスの識別子になる
              • 既にインスタンスが生成されている場合は NetObjectManager.Create を呼び出さない
    • SynDataRequest
      • MsgGroup.SynDataEvent.Request(bytes, Data.RequestType.Others)
      • OnSynDataResp
        • NetMsgType が SynValue なら NetworkBehaviour.DeserializeData
        • NetMsgType が Commond なら NetworkBehaviour.ReplyCommond
  • MsgGroup

    • SynContextEvent
    • CreateRoomEvent
    • CreateNetObjectEvent
    • DestroyNetObjectEvent
    • GetRoomStateEvent
    • JoinRoomEvent
    • LeaveRoomEvent
    • LoginEvent
    • DownLoadCommondListEvent
    • UpLoadCommondListEvent
    • SynDataEvent
  • TestNetBehaviour (in Demos/Sharing/Scripts)

    • NetworkBehaviour を継承している
    • Inspector の設定項目
      • SynTransform
      • SynInt
      • SynVector2
      • SynVector3
      • SynQuaternion
    • Update
      • IsOwner が true ならば SynObject を更新する
      • Space キー押下時、MsgGroup.SynDataEvent.Request(bytes, Data.RequestType.Everyone))
  • NetworkBehaviour (in Scripts/Sharing/Scripts/RunTime)

    • Updater コルーチン
      • MsgGroup.SynDataEvent.Request(bytes, Data.RequestType.Everyone))
  • NetWorkObjectPool (in Scripts/Sharing/Prefabs)

    • ScriptableObject を継承している
    • Inspector の設定項目
      • NetObjects : List
    • NetworkBehaviour (のサブクラス) の Type Name をKey、NetworkBehaviour (のサブクラス) を Component に持つ GameObject を Value とした Dictionary を持つ

NRPhotoCapture, NRVideoCapture を読み解いてみる

概要

RGBCamera-CaptureRGBCamera-Record を理解すると NRPhotoCapture クラスと NRVideoCapture クラスが主要クラスだということが分かります。 NRPhotoCapture, NRVideoCapture の両クラスの動作を理解することで RGBCamera への理解が深まると考え、これらのソースコードを解読してみます。 個人的な読解結果であるため誤りが含まれている可能性があることをご了承ください。

目次

確認環境

  • NRSDK 1.2.1

解説

拾い読み

例のごとく、NRPhotoCapture を拾い読みすると、以下が要点になるのかなと思います。

  • NRPhotoCapture
    • CreateAsync
      • NRPhotoCapture インスタンスを生成する
      • FrameCaptureContext インスタンスを生成する
        • FrameCaptureContext 生成時に AbstractFrameProvider を渡す
          • Unity Editor では EditorFrameProvider
          • それ以外では RGBCameraFrameProvider
    • StartPhotoModeAsync
      • FrameCaptureContext.StartCaptureMode(CameraParameters)
        • CameraParameters の camMode は PhotoMode に設定される
        • CaptureBehaviourBase インスタンスを生成する
          • camMode が PhotoMode の場合は NRCaptureBehaviour (Record/Prefabs/NRCaptureBehaviour)
        • IEncoder インスタンスを生成する
          • camMode が PhotoMode の場合は ImageEncoder
          • Config(CameraParameters) を行う
        • FrameBlender インスタンスを生成する
          • CaptureBehaviourBase.CaptureCamera, IEncoder, CameraParameters で初期化する
        • FrameProvider.OnUpdate に CaptureBehaviourBase.OnFrame を登録する
      • FrameCaptureContext.StartCapture()
        • IEncoder.Start()
        • FrameProvider.Play()
    • TakePhotoAsync
      • NRCaptureBehaviour.DoAsyn
        • CaptureTask インスタンスを生成して、CameraParameters を設定する
        • SystemInfo.supportsAsyncGPUReadback が true の場合、ImageEncoder.Commit(CaptureTask)
          • 非同期実行で画像データが取得されて CaptureTask.OnReceive が呼ばれる
        • false の場合、ImageEncoder.Encode(CaptureTask)
          • RenderTexture を使用して Texture2D から画像データが取得されて CaptureTask.OnReceive が呼ばれる
        • OnReceive では PhotoCaptureFrame インスタンスが生成される
    • StopPhotoModeAsync
      • FrameCaptureContext.StopCaptureMode()
        • FrameCaptureContext.Release() を呼び出す
    • Dispose
      • FrameCaptureContext.Release()
        • FrameProvider.OnUpdate から CaptureBehaviourBase.OnFrame を削除する
        • FrameProvider.Release()
        • FrameBlender.Dispose()
        • IEncoder.Release()
        • Destroy(CaptureBehaviourBase)
      • FrameCaptureContext を破棄する
  • NRCaptureBehaviour
    • ImageEncoder の Commit, Encode により画像データを取得する
    • OnFrame では
      • NRFrame.GetHeadPoseByTime に成功した場合に RGBCameraRig の localPosition, localRotation を更新する
      • FrameBlender.OnFrame を呼び出す
  • ImageEncoder
    • 画像データを取得する
  • FrameBlender
    • OnFrame で BlendMode (RGBOnly/VirtualOnly/Blend/WidescreenBlend) に応じて Blit を行い画像データを作る
      • 作った画像データは ImageEncoder に渡される
  • RGBCameraFrameProvider
    • NRRGBCamTexture を使う
      • Play, Pause, Stop で NRRGBCamTexture の Play, Pause, Stop を呼び出す
      • NRRGBCamTexture.OnUpdate に AbstractFrameProvider.OnUpdate を登録する
  • EditorFrameProvider
    • AbstractFrameProvider.OnUpdate に毎フレーム RGBTextureFrame を渡す
      • RGBTextureFrame.texture に Texture2D (Record/Textures/captureDefault) を設定する

同様に、NRVideoCapture を拾い読みすると、以下が要点になるのかなと思います。

  • NRVideoCapture
    • CreateAsync
    • StartVideoModeAsync
      • FrameCaptureContext.StartCaptureMode(CameraParameters)
        • CameraParameters の camMode は VideoMode に設定される
        • CameraParameters の hologramOpacity は 1 に設定される
        • 他は NRPhotoCapture と同様だが、camMode が異なるため以下が異なる
          • NRCaptureBehaviour は NRRecordBehaviour (Record/Prefabs/NRRecorderBehaviour) になる
          • IEncoder は VideoEncoder になる
    • StartRecordingAsync
      • IsRecording が true の場合は UnknownError とする (Unknown なんだ...)
      • NRRecordBehaviour.SetOutPutPath を行う
      • FrameCaptureContext.StartCapture()
        • NRPhotoCapture と同様
      • IsRecording を true にする
    • StopRecordingAsync
      • IsRecording が false の場合は UnknownError とする (Unknown なんだ...)
      • FrameCaptureContext.StopCapture()
        • IEncoder.Stop()
        • FrameProvider.Stop()
      • IsRecording を false にする
    • StopVideoModeAsync
      • NRPhotoCapture と同様
    • Dispose
      • NRPhotoCapture と同様

要点の整理

NRPhotoCapture, NRVideoCapture の基本的な使い方は似ています。以下の 5 つに分類して理解します。

  1. CreateAsync
  2. Start[Photo/Video]ModeAsync
  3. TakePhotoAsync or [Start/Stop]RecordingAsync
  4. Stop[Photo/Video]ModeAsync
  5. Dispose

CreateAsync

引数は showHolograms と callback の 2 つです。 showHolograms は使用されていません。 callback は処理が終了するときに呼び出されます。 非同期処理ではありません。 callback が呼び出されてから CreateAsync が終了します。

CreateAsync は NRPhotoCapture, NRVideoCapture インスタンスを生成します。 この時、FrameCaptureContext インスタンスを生成して設定します。 更に、FrameProvider (AbstractFrameProvider のサブクラス) インスタンスを生成して FrameCaptureContext インスタンスに設定します。 つまり、下記の階層構造になります。

  • NR[Photo/Video]Capture
    • FrameCaptureContext
      • FrameProvider

FrameProvider は実行環境により異なります。 Unity Editor では EditorFrameProvider、それ以外では RGBCameraFrameProvider が使われます。

Start[Photo/Video]ModeAsync

引数には CameraParameters, callback が渡されます。 Video では AudioState が追加で渡されます。 CameraParameters は諸々の場面で使用されます。 callback は処理が終了するときに呼び出されます。 非同期処理ではありません。 callback が呼び出されてから Start[Photo/Video]ModeAsync が終了します。 AudioState は使用されていません。

Start[Photo/Video]ModeAsync では PhotoMode/VideoMode で FrameCaptureContext を初期化します。 FrameCaptureContext.StartCaptureMode(CameraParameters) を呼び出し、以下に続くいくつかの処理を行います。

CameraParameters の値をいくつか上書きします。 Photo/Video に応じて、CameraParameters.camMode の値を PhotoMode/VideoMode で上書きします。 Video の時のみ、CameraParameters.hologramOpacity を 1 に上書きします。

Behaviour (CaptureBehaviourBase のサブクラス) インスタンス、Encoder (IEncoder を実装) インスタンス、FrameBlender インスタンスを生成します。 Behaviour と Encoder は Photo/Video により生成するインスタンスが異なります。

  • Photo
    • NRCaptureBehaviour (Record/Prefabs/NRCaptureBehaviour)
    • ImageEncoder
  • Video
    • NRRecordBehaviour (Record/Prefabs/NRRecorderBehaviour)
    • VideoEncoder

FrameProvider.OnUpdate に CaptureBehaviourBase.OnFrame を登録します。 FrameProvider.OnUpdate が呼び出されたときに Behaviour の OnFrame が呼び出される様になります。

終了時点では、階層構造は下記に更新されています。

  • NR[Photo/Video]Capture
    • FrameCaptureContext
      • FrameProvider
      • Behaviour
      • Encoder
      • FrameBlender

Photo の時のみ、FrameCaptureContext.StartCaptureMode() に続いて FrameCaptureContext.StartCapture() を呼び出します。 FrameCaptureContext.StartCapture の説明は StartRecordingAsync を参照してください。

Stop[Photo/Video]ModeAsync

引数には callback が渡されます。 callback は処理が終了するときに呼び出されます。 非同期処理ではありません。 callback が呼び出されてから Stop[Photo/Video]ModeAsync が終了します。

Stop[Photo/Video]ModeAsync では FrameCaptureContext を無効化します。 FrameCaptureContext.StopCaptureMode() を呼び出すことで行われます。

StopCaptureMode では FrameCaptureContext.Release() を呼び出します。 Release では以下を無効化します。

  • FrameProvider
  • Behaviour
  • Encoder
  • FrameBlender

また、FrameProvider.OnUpdate から CaptureBehaviourBase.OnFrame を削除します。

(Photo の時は FrameCaptureContext.StopCapture() を呼び出した方が良いと思うけどやっていない。)

Dispose

引数はありません。他とは違い Async が付かないため明確に同期呼び出しでの使用を想定していると思われます。

Dispose では FrameCaptureContext.Release() を呼び出した後に、FrameCaptureContext インスタンスを破棄します。 Release の処理内容は Stop[Photo/Video]ModeAsync を参照してください。

撮影/録画

StartRecordingAsync

引数には filename と callback が渡されます。 callback は処理が終了するときに呼び出されます。 非同期処理ではありません。 callback が呼び出されてから StartRecordingAsync が終了します。 録画中に呼び出したり、処理中に例外が発生すると callback に UnknownError が渡されます。

StartRecordingAsync では FrameCaptureContext.StartCapture() を呼び出して、録画を開始します。 StartCapture では、Encoder を開始(Start)し、FrameProvider を再生状態に(Play)します。

StartCapture の呼び出し前には NRRecordBehaviour に filename を設定します。 NRRecordBehaviour では設定された filename を VideoEncoder.EncodeConfig に設定します。

StopRecordingAsync

引数には callback が渡されます。 callback は処理が終了するときに呼び出されます。 非同期処理ではありません。 callback が呼び出されてから StopRecordingAsync が終了します。 未録画時に呼び出したり、処理中に例外が発生すると callback に UnknownError が渡されます。

StopRecordingAsync では FrameCaptureContext.StopCapture() を呼び出して、録画を停止します。 StopCapture では、Encoder を停止(Stop)し、FrameProvider を停止状態に(Stop)します。

TakePhotoAsync

引数のパターンが 2 種類あります。 filename と PhotoCaptureFileOutputFormat を指定するパターンと、指定しないパターンです。 いずれの場合も callback が渡されます。 filename を指定するパターンでは callback が使用されません。 filename を指定しないパターンでは画像が取得できたときに callback が呼ばれます。 SystemInfo.supportsAsyncGPUReadback が true の場合は非同期処理、false の場合は同期処理になります。

TakePhotoAsync は引数のパターンにより処理方法が異なります。

filename を指定するパターンでは、NRCaptureBehaviour.Do を呼び出します。 Do では ImageEncoder.Encode(width, height, captureFormat) を呼び出します。 Encode により画像データが取得し、ファイルに書き出します。

filename を指定しないパターンでは、NRCaptureBehaviour.DoAsyn を呼び出します。 DoAsyn では CaptureTask インスタンスを生成し、CaptureTask に応じた画像データ取得の処理を行います。 CaptureTask には OnReceive 関数を設定して、画像データの取得が完了した場合の処理を指定します。 OnReceive 関数は画像データが得られたときに PhotoCaptureFrame インスタンスを生成し、callback を呼び出します。 PhotoCaptureFrame は callback の引数として渡されます。

CaptureTask は SystemInfo.supportsAsyncGPUReadback の値により処理方法が異なります。

supportsAsyncGPUReadback が true の場合、CaptureTask をキューに積んで非同期に処理を行います。 CaptureTask は Update のタイミングで処理されます。 UnityEngine.Rendering.AsyncGPUReadbackRequest を使用して画像データを取得します。 画像データ (Texture2D) が取得されると OnReceive を呼び出します。 呼び出す際に CaptureTask.CaptureFormat に従って JPG/PNGエンコードしたデータが渡されます。

supportsAsyncGPUReadback が false の場合、ImageEncoder.Encode(width, height, captureFormat) を呼び出します。 width, height, captureFormat は CaptureTask から取得します。 Encode により画像データが取得されたら OnReceive を呼び出します。 呼び出す際に Encode で得たデータが渡されます。

ImageEncoder.Encode は filename を指定するパターンと filename を指定せずに supportsAsyncGPUReadback が false の場合に呼び出されます。 Encode では FrameBlender.OnFrame で生成された RenderTexture から得られた画像データ (Texture2D) を CaptureTask.CaptureFormat に従って JPG/PNGエンコードします。 FrameBlender.OnFrame については次に説明します。

FrameBlender

FrameBlender.OnFrame では BlendMode (RGBOnly/VirtualOnly/Blend/WidescreenBlend) に応じて Texture を Blit します。 OnFrame は引数に RGBTextureFrame を受け取ります。 Blit した結果の RenderTexture は Encoder に設定(Commit)されます。

FrameBlender.OnFrame は CaptureBehaviourBase.OnFrame から呼ばれます。 CaptureBehaviourBase.OnFrame は FrameProvider.OnUpdate に登録されるため、FrameProvider.OnUpdate が呼び出されたときに FrameBlender.OnFrame が呼ばれることになります。

FrameProvider.OnUpdate は引数に RGBTextureFrame を受け取っており、これが FrameBlender.OnFrame の引数になります。

RGBCameraFrameProvider

Unity Editor 以外で動かした場合に使われる FrameProvider です。 NRRGBCamTexture を使って RGBTextureFrame を取得します。

FrameCaptureContext の操作に応じて以下の様に NRRGBCamTexture の操作が使われます。

FrameCaptureContext RGBCameraFrameProvider NRRGBCamTexture
StartCapture Play Play
StopCapture Stop Pause
Release Release Stop

NRRGBCamTexture では NRRgbCamera.GetRGBFrame を呼び出して RGBTextureFrame を取得しています。 この RGBTextureFrame が FrameProvider.OnUpdate を呼ぶときに渡されます。

FrameProvider.OnUpdate は NRKernalUpdater.OnUpdate が呼ばれて NRRGBCamTexture.OnUpdate が呼ばれたときに呼ばれます。 OnUpdate, OnFrame の呼び出し関係を整理すると下記の様になります。

NRKernalUpdater.OnUpdate
  NRRGBCamTexture.OnUpdate
    RGBCameraFrameProvider.OnUpdate
      Behaviour.OnFrame
        FrameBlender.OnFrame
          Encoder.Commit

EditorFrameProvider

Unity Editor で動かした場合に使われる FrameProvider です。 RGBTextureFrame インスタンスを生成し、インスタンスに Texture2D (Record/Textures/captureDefault) を設定します。

コルーチンで FrameProvider.OnUpdate を呼んでいます。その際に生成した RGBTextureFrame を渡します。 但し、isPlay が true のときのみ呼ばれます。isPlay は FrameCaptureContext の操作に応じて以下の様に設定されます。

FrameCaptureContext EditorFrameProvider isPlay
StartCapture Play true
StopCapture Stop false
Release Release false