Rust/WinRT を試す (その 2)

はじめに

前回記事以降も順調にアップデートがされているようなので改めて調べてみました。

crates.io に登録

https://crates.io/crates/winrt

crates.io に登録されましたので、 Cargo.toml で GitHub の Git を参照するのではなく、 crates.io の登録バージョン指定で参照できるようになりました。

[dependencies]
winrt = "0.7.1"

build.rs 対応

前回記事で

端的に言うとコード補完がほぼききません。これは WindowsAPI 部分が winmd に定義されているためと思われます。

と書いた点が解消されそうな感じになってきました。

bindgen と同じように build.rs でビルド時に winmd から静的なコード生成をします。これにより IDE から binding コードの参照が可能になりました。

現時点では README.md 等にも記載がないのですが、次のようにすることで利用できました。

Cargo.toml の設定

ビルドプロセスで処理するので build-dependencies にも定義が必要になります。

[dependencies]
winrt = "0.7.1"

[build-dependencies]
winrt = "0.7.1"

build.rs の設定

プロジェクト直下 (Cargo.toml と同じ位置) に build.rs を作成し、次のコードを記述します。

winrt::build!(
    dependencies
        os
    types
        windows::data::xml::dom::*
        windows::foundation::*
        windows::ui::*
);

fn main() {
    build();
}

winrt::build マクロの中身は従来の import マクロのものと同じです。

main.rs / lib.rs の設定

コード側には include 定義を記述します。 main.rs か lib.rs の先頭に下記記述を追加します。

include!(concat!(env!("OUT_DIR"), "/winrt.rs"));

import マクロ定義は削除してください。

これで一回ビルドすると binding コードが生成されてエディッター上から参照できるようになります。

f:id:tan-y:20200721005032p:plain

前回を思うと結構感動します。ただ私の環境では VSCode ではうまく参照ができない事も多く、 CLion では全く見えませんでした。この辺りは IDE 側でも対応を進める必要があるのかもしれません。

NuGet 対応

NuGet からの import も可能になったようです。ただこれは若干特殊な操作を必要とします。

これは実装例が winrt-rs 下にあるのでそちらを参照してください。

まず "cargo winrt" というコマンドを使えるようにする必要があります。

$ cargo install cargo-winrt

※ cargo-winrt も crates.io に登録されています

NuGet も build.rs での前処理になるので Cargo.toml の dependencies, build-dependencies に winrt の参照を設定します。また、それに加えて参照する NuGet パッケージの定義もします。

[package.metadata.winrt.dependencies]
"Microsoft.AI.MachineLearning" = "1.3.0"
"Microsoft.Windows.SDK.Contracts" = "10.0.19041.1"

build.rs でも参照する NuGet のパッケージを指定していきます。

winrt::build!(
    dependencies
        nuget: Microsoft.AI.MachineLearning
        nuget: Microsoft.Windows.SDK.Contracts
    types
        microsoft::ai::machine_learning::*
        windows::foundation::Uri
);

ビルドは通常の "cargo build" ではなく、 cargo-winrt を通して "cargo winrt build" で行います。 NuGet の依存解決のために Cargo に拡張を加えたのではないかと思います。これだと IDE 上からショートカットキーでビルド、というわけにもいかなくなってしまうので (この方針で確定なら) IDE 側にも対応が求められそうです。

おわりに

(個人的に) 最大の懸案だった winmd の binding コードへの参照が可能になってきているのでなんとか使えそうになってきたかなあという感じです。開発もアクティブに進められているようなのでこれからも状況を見ていきたいと思います。

Windows で Unity as a Library

はじめに

Windows でも Unity as a Library ができる、と Twitter で見かけたので試してみました。

unity.com

Windows では Unity 2019.3 以降での対応のようです。この記事では Unity 2019.4.1f1 で行っています。

ホスト側は .NET Core 3.1 + Windows Forms で作りました。

f:id:tan-y:20200625073117p:plain
動作例 / © UTJ/UCL

github.com

手順

Windows でやる場合、方法が 3 通りあります。

  • a) Windows Standalone / Unity コードを外部プロセスとして起動する
  • b) Windows Standalone / Unity コードを DLL として自身のプロセスにロードする
  • c) UWP

ここでは a) と b) について取り上げます。

まず適当に Unity のプロジェクトを作成し、 Windows Standalone でビルドしておきます。

Unity コードを外部プロセスとして起動する

Unity のアプリを起動する際のコマンドラインオプションに "-ParentHWND" が追加されています。このオプションで親ウィンドウとなるウィンドウのハンドルを指定します。ウィンドウの親子関係は異なるプロセス間でも成立できます (これができないとウェブブラウザのプロセス分離ができないことになります) 。

原理的にはこれだけなのですが、実際には親ウィンドウの状態変更に応じた処理 (リサイズなど) が必要になります。 Unity によるサンプル を参考に汎用的な対応コードを実装しました。

  • UnityLibrary.cs
    • Unity のウィンドウハンドル取得
    • Unity ウィンドウのリサイズ
    • Unity ウィンドウの有効化 / 無効化
    • Unity ウィンドウのクローズ

Unity コードを DLL として自身のプロセスにロードする

Unity の実行ファイル本体は UnityPlayer.dll になります。ビルド時に生成されている .exe ではなく、自前で UnityPlayer.dll をロードし、エントリーポイントを呼び出す事でも Unity アプリを起動することができます。これ自体は以前から可能のようですが、ここで指定する起動パラメーターに先ほどの -ParentHWND を指定することで親ウィンドウをした上で起動することができるようになっています。

こちらの手法ではインプロセス実行になるため理屈的には Unity 側とホスト側でメモリ (ポインタ) を共有できることになります (実際にはどうやってポインタ値の受け渡しをするのか検討の必要があります) 。

UnityPlayer.dll をロードする場合、いくつか事前準備が必要になります。

必要なファイルのコピー

まず、ビルドした Unity アプリケーションをホスト側の実行ファイルと同じ位置にコピーする必要があります。厳密には Unity アプリの実行時は カレントディレクトリが Unity アプリと同じになっていなくてはならない という制約に準拠するためです。外部プロセス実行の場合はどこに置いてもそのプロセス下での制約になるのでホスト側は影響を受けません。

また、コピーの際は "???_Data" というフォルダはコピー後に ??? の部分をホスト側の実行ファイル名と同じものにリネームする必要があります。例えば

  • Unity ビルドの実行ファイル名: UnityApp.exe
  • ホストの実行ファイル名: Win32.exe

だった場合 "UnityApp_Data" というフォルダをコピー先で "Win32_Data" にリネームする必要があります。

UnityPlayer.dll のロード ~ 実行

delegate int FnUnityMain(IntPtr hInstance, IntPtr hPrevInstance, [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, int nShowCmd);

DLL をロードして "UnityMain" のエントリーポイントを取得、実行をします。

var p = GetProcAddress(_dll, "UnityMain");
var fnUnityMain = Marshal.GetDelegateForFunctionPointer<FnUnityMain>(p);
fnUnityMain(Process.GetCurrentProcess().Handle, IntPtr.Zero, arg, 1);

arg には "-ParentHWND" で親ウィンドウを指定するオプションを設定します。

Unity - ホスト間で通信をする

通常の Win32 ウィンドウの中に Unity アプリを表示することができるようになりました。しかし、これだけだと単に表示がされるだけで統合がされているような感じはしません。組み込むからにはホスト側から Unity の操作をしたり、 Unity の情報をホスト側に伝えたくなるわけです。

iOS ではそういうルートも用意されているっぽい ですが Windows では特にないので、別の方法を考えます。

ということで今回は MagicOnion を使ってみました。 MagicOnion は gRPC を通信基盤としているもののインターフェース定義を全て C# で行えるので使い勝手がとてもよいです。

MagicOnion の導入はこちらの記事がとても参考になります。ありがとうございます。

qiita.com

今回は Unity がライブラリ、つまり WinForms → Unity という呼び出しが主になりますが、 Unity は MagicOnion のサーバーになれないので WinForms 側をサーバーに仕立てます。この構成だと "サーバー → クライアント" という呼び出しが主になってしまいます。 MagicOnion は "サーバー → クライアント" 通信では登録済クライアントに対するブロードキャストのみですが、今回は 1 対 1 通信と決まっているのでブロードキャストでも結果ほぼ変わらないのでまあいいかなと。

public interface IUnityChanControllerReceiver
{
    void SetAnimation(AnimeType animeType);
    void SetMessageText(string msg);
}

public interface IUnityChanController : IStreamingHub<IUnityChanController, IUnityChanControllerReceiver>
{
    Task RegisterAsync();
    Task UnregisterAsync();

    Task SetAnimationAsync(AnimeType animeType);
    Task SetMessageTextAsync(string msg);
}

これに対する IUnityChanController (WinForms 側) の実装は次のようにしました。

private IGroup _group;

async Task IUnityChanController.RegisterAsync()
{
    _group = await Group.AddAsync("UnityLibrary");
}

Task IUnityChanController.UnregisterAsync()
{
    return _group?.RemoveAsync(Context).AsTask();
}

Task IUnityChanController.SetAnimationAsync(AnimeType animeType)
{
    Broadcast(_group).OnSetAnimation(animeType);
    return Task.CompletedTask;
}

Task IUnityChanController.SetMessageTextAsync(string msg)
{
    Broadcast(_group).OnSetMessageText(msg);
    return Task.CompletedTask;
}

SetAnimationAsync, SetMessageTextAsync は対応する Receiver に Broadcast でそのままたらしい回しをします。

WinForms 側の Receiver は何もしない空実装であるのに対し、 Unity 側はアニメーション制御だったり UI の更新をしたりしています。 使う時は Unity / WinForms 双方から StreamingHubClient.Connect で接続し、 RegisterAsync で Group の登録をして Broadcast の送受信をできるようにしておきます。実際に Broadcast を受けて何かするのは Unity 側のみ、ということになります。

WinForms の UI から SetAnimationAsync, SetMessageTextAsync を呼び出せば結果的に Unity 側の処理が呼び出されるという構造になりました。

おわりに

Windows での Unity as a Library は実際提供されている機能的には極々限られていて、特に標準で通信手段が用意されていないのはなかなかつらいものがあります。 Unity との通信には MagicOnion を使ってみましたが、 Broadcast を使うしかないですし実際はあんまりお勧めできないかと思います。他の方法といってもパイプとか泥臭い方法になりそうですが・・・

実際のアプリ開発でこの機能を利用しようとした場合、 Unity 側とホスト側で完全に分離してしまうので作業がなかなか大変なことになってしまうとは思います。そこまでしてネイティブ UI を使いたい場合は一考の余地ありといったところでしょうか。

Rust/WinRT を試す

はじめに

先日 Microsoft から preview がリリースされた Rust で WinRT API を使用できるようにするためのものです。 C++/WinRT の Rust 版、みたいなもののようです。 Microsoft は割と Rust に本気なのかもしれません。

個人的にかなり気になるものなので試してみました。

crate.io には以前から WinRT の Binding ライブラリがありますが、こちらとは別のものです。

WinRT とは

あまりご存じない方もいるかもしれないので軽く触れておきます。

WinRT は正確には "Windows Runtime" という Windows 8 と合わせて登場した (当時) 新しい API セットの総称で、現在 UWP と言われている "Windows ストアアプリ (Metro スタイルアプリ)" を開発するために提供されたのが最初でした。

ちなみに Windows 8 の ARM 版の名前が "Windows RT" であったため略称にするとどっちの事を言っているのよくわからないという問題がありました。

基本的な特徴としては

  • COM をベースにしたネイティブ API である (.NET ではない)
  • API 定義はテキスト (ヘッダーファイル) ではなくバイナリ (Windows Metadata / winmd) で提供される

かと思います (他にも非同期とか UI とかありますが、根幹はこの二つかなと) 。

winmd は COM 的には Type Library に相当するもので、バイナリ構造としては .NET と互換性のあるものとなっています。各言語から WinRT の API にアクセスする場合、この winmd からインターフェース情報を取得し、相互運用を実現しています。

Rust/WinRT を導入する

README.md の "Getting started" に書かれている通りです。

[dependencies]
winrt = { git = "https://github.com/microsoft/winrt-rs" }

Cargo.toml に winrt の crate を参照に追加するだけです。注意点として toolkit が msvc である必要があります。 gnu では通りません。これは UWP 用のバイナリをビルドするので当然かなあと思ったのですが、そういう理由ではなさそうです。

winmd ファイルのあるパスへの参照が解決できないため、というのが理由のようです。まあ確かに API 的に WinRT 互換で winmd で API 定義が公開されているならコンパイル時にはプラットフォーム依存はしていないと言えるのもしれません。いずれにせよ、現時点では toolkit に制約がありますので注意してください。

とりあえず README.md のサンプルをコピペしてビルド、実行をしてみると、動作することは確認できます。

コードを書いてみる

・・・ちょっと、いや大分厳しい。

端的に言うとコード補完がほぼききません。これは WindowsAPI 部分が winmd に定義されているためと思われます。

winmd のインポートは "winmd_macros" という crate に定義されている import マクロで行います。

winrt::import!(
    dependencies
        os
    modules
        "windows.data.xml.dom"
        "windows.foundation"
        "windows.ui"
);

この import というマクロですが、どうも "proc_macro" というもので定義されています。

"Procedural Macros" は通常のマクロのようにテンプレートに穴埋めするのではなく、コンパイル時に動的にコード生成をする仕組みの事のようです。なるほど。これでは winmd 部分を Rust の文法でコード補完をかけるのは専用の対応を入れないと不可能ですね・・・

関連した Issue は上がっているので、実用になるかどうかはこれ次第ですかね。

おわりに

現状ではコード補完がきかないのが致命的すぎるので、 Windows と Rust が相当好きでも使うにはかなりハードルが高いと思います。試すだけなら Rust らしくとても簡単ですが。

こちらのコードも読んでみましたが、ウィンドウを開くだけでもちょっと手続きが面倒くさそうな感じなので、また機会を改めて (Win32 での UWP UI の利用の仕方についての理解が必要そう?) 挑戦してみようと思います。

Rust で手作業で DLL の動的ロードをする (Windows)

はじめに

DLL を自前でロードする必要が出てきたのでやり方をまとめました。

  • 目的の API へのアクセスに #link 属性は使わない
  • win32-rs 等は使わない

要点

DLL ロードに必要な API だけ #link 属性で取得します。

use std::ffi::c_void;
type Error = u32;

#[link(name = "kernel32")]
#[no_mangle]
extern "stdcall" {
    fn GetLastError() -> Error;
    fn LoadLibraryExW(lpLibFileName: *const u16, hFile: *const c_void, dwFlags: u32) -> *const c_void;
    fn FreeLibrary(hLibModule: *const c_void) -> i32;
    fn GetProcAddress(hModule: *const c_void, lpProcName: *const u8) -> *const c_void;
}

Win32 の W 系 APIUTF-16 文字列なので、変換する関数を用意します。

trait IntoNullTerminatedU16 {
    fn to_nullterminated_u16(&self) -> Vec<u16>;
}

impl IntoNullTerminatedU16 for str {
    //  略
}

Win32 API の多くは GetLastError でエラー取得をします。のでそれに対応した関数も用意します。

trait ToResult: Sized {
    fn to_result(&self) -> Result<Self, Error>;
}

impl ToResult for *const c_void {
    //  略
}

この組み合わせで LoadLibrary は次のように行いました。

let h = LoadLibraryExW("user32.dll".to_nullterminated_u16().as_ptr(),
                        ptr::null(),
                        0x800).to_result().unwrap();

API の関数ポインタを Rust の関数ポインタに変換するには次のようにします。 Rust → FFI の関数ポインタ (生ポインタ) への変換は as でできますが、逆は transmute で行います。

type FnMessageBox = extern "stdcall" fn(hWnd: *const c_void, lpText: *const u16, lpCaption: *const u16, uType: u32) -> i32;

let p = GetProcAddress(h, "MessageBoxW\0".as_ptr()).to_result().unwrap();
let fn_message_box = std::mem::transmute::<_, FnMessageBox>(p);

fn_message_box(ptr::null(),
                "Hello, Rust!".to_nullterminated_u16().as_ptr(),
                "MessageBox".to_nullterminated_u16().as_ptr(),
                0);

コード

gist485b3a3703f95bccac5d1503e55db8b4

おわりに

やってる事は簡単なのですが、 Rust の作法がいま一つよくわかってなくてこれでよいのか自信がないのでつっこみをお待ちしてます。

Unity 2020.1 の HDR ディスプレイ出力機能を試す

はじめに

この記事は以前書いた記事の続きです。

Unity 2020.1 では HDR ディスプレイ出力機能が強化されています。

https://blogs.unity3d.com/jp/2020/03/17/unity-2020-1-beta-is-now-available-for-feedback/

上記記事によると

  • PS4 / Xbox OneHDR 出力に対応 (Desktop PC を含めて Single API で対応)
  • Unity Editor 上の HDR プレビューに対応
  • HDR サポートの C# API

といった機能が追加されたとのこと。 PS4 / XONE は試しようもないですが、それ以外について一つずつ見ていこうと思います。

この記事では Unity 2020.1b5 で確認しています。

HDR 出力 / Unity Editor 上の HDR プレビュー対応

We have added HDR display support for the Editor, allowing developers who use displays that support HDR to take advantage of it. This change supports DX12 and Metal.

との記載があり Unity Editor 上で HDR 出力が行えるようになっています。

Windows では DX12 モードで起動する必要があります。 現時点ではデフォルトでは DX11 なので -force-d3d12 オプションをつけて Unity Editor を起動する必要があります。ビルドしたアプリケーションについては DX11 でも HDR は機能しますので Graphics API を Direct3D11 にしてビルドしても OK です (DX12 は Editor 上での条件) 。

2019.3 より Project Settings に下記設定をすることで HDR ディスプレイ出力が有効にできます。

  • "Player - Rendering - Color Space" を "Linear" に設定する
  • "Player - Rendering - Use display in HDR mode" のチェックを ON にする

上記で HDR ディスプレイ出力が有効になりますが、条件を満たしている環境では Unity Editor 上でもそのまま HDR ディスプレイ出力が行われます。

f:id:tan-y:20200412105511p:plain

HDR mode ではさらに "Swap Chain Bit Depth" という指定があり、これが "Bit Depth 10" か "Bit Depth 16" で挙動が違うことが確認できています。この点については前回記事でも触れていて、 "Bit Depth 10" では色がおかしいと書きました。

前回記事では検証が不十分だったのですが Unity としては次のような動作になっているようです。

Mac で DisplayP3 の設定をした時の挙動は環境がないので未確認

Unity のドキュメントによると

  • Bit Depth 10 → Unity will use the R10G10B10A2 buffer format and Rec2020 primaries with ST2084 PQ encoding.
  • Bit Depth 16 → Unity will use the R16G16B16A16 buffer format and Rec709 primaries with linear color (no encoding).

と書かれています。これは Windows の DXGI の仕様に準じています (DXGI はこの 2 つのフォーマットで HDR 出力ができる) 。

これに先に記述した "HDR ディスプレイに出力 → 最終レンダリング結果をそのまま出力" を合わせて考えると、 Unity のエンジン側でなく、アプリの方で指定の Bit Depth に対応した色空間に変換する必要がある ということになります。

従来、 Unity で言う色空間 (Color Space) は ドキュメントの説明 によると

  • Color Space は Gamma と Linear の 2 種類である
  • 標準的な Gamma は sRGB である

ということになっているので、まあ次のような事だろうという事は想定できます。というか Unity に限らず暗黙的にそのような扱いだったはずと思います。

色域 伝達特性
Gamma sRGB BT.709 sRGB
Linear scRGB BT.709 Linear

色空間については下記記事もご覧ください。

ちなみに scRGB は有効な値の範囲が仕様として定義されている範囲がせまい (-0.5~7.5) ですが処理中においては特に気にする必要はないと思います。

HDR 出力有効時の注意点として (少なくとも 2020.1 時点では) Unity 標準の Post Process (PPS, URP, HDRP) の Tone mapping を適用すると 0.0~1.0 の範囲に丸め込まれるので HDR じゃなくなります。従って Tone mapping は標準 Post Process 以外の方法で行う必要があります。

Bit Depth 10 の場合

Bit Depth 10 は "Rec.2020 (BT.2020) かつ "ST 2084(PQ)" となっています。これは Unity の Color Space と互換性がありません。実は私は 2019.3 で試していた時は Unity 側で色空間変換をしてくれるものと思っていたのですがそうではありませんでした。

色空間変換をしてくれない、ということは自前でなんとかする必要があります。

Bit Depth 16 の場合

Bit Depth 16 は "Rec.709 (BT.709)" かつ "Linear" となっています。つまり Color Space = Linear に設定した際の中間レンダリングフォーマットそのもの ということになります。従ってこの設定にした場合は特に意識しないで正しい色になります。

HDR サポート の C# API

HDR の対応状況は SystemInfo.hdrDisplaySupportFlags より取得できます。

HDR 出力に関する詳細な設定は HDROutputSettings クラス で行えます。設定できるものは下記の 2 つです (SystemInfo.hdrDisplaySupportFlags の状況で設定の可否があると思います) 。

  • active → HDR が有効かどうか
  • automaticHDRTonemapping → 自動 HDR トーンマッピングを有効にするかどうか

active についてはプロパティ自体は get のみですが RequestHDRModeChange メソッド により切り替えができます。

上記以外のプロパティは available プロパティ が true じゃないと例外が出るので、まずこのプロパティを確認してから操作をするようにすべきでしょう。 avalilable プロパティは Player Settings の "Use display in HDR mode" が ON になっていると (かつ HDR が有効にできるプラットフォーム) true になります。 OFF にしていると対応環境でも available = false なので「Editor 上では SDR で作業したい」と OFF にしてそのままビルドするとはまりそうなので注意が必要です。

BitDepth は実行時に変更することはできません (まあ実行時に切り替えるようなものではないですが) 。 BitDepth に基づいた設定は displayColorGamut, format, graphicsFormat から判断できます。これらの情報を元に必要に応じて色空間変換処理を適用すればよいかと思います。

その他、ディスプレイの性能 (最大、平均輝度など) を取得するプロパティも存在します。が、経験上この値の信頼性はあまりない (ディスプレイが正しい値を返す保証がない) と思いますので参考程度に留め、性能に応じてトーンマッピングのパラメーターを調整するのであればキャリブレーション機能があった方が望ましいと思います。

automaticHDRToneMapping は具体的にどういった挙動をするのか試した感じではよくわかりませんでした・・・ (らしい絵を作ってみないとだめかも)

トーンマッピングと色空間変換

automaticHDRToneMapping のドキュメント によるとトーンマッピングを自前でやる場合には PostEffect や OnRenderImage で、と書いてあります (これがカスタムトーンマッピングの事?) 。 URP や HDRP で OnRenderImage は使えないので別の手段で対応する必要があります。ただ私があれこれ試してみたところ URP 8.0.1 では URP 組み込み PostEffect の後に独自の PostEffect を差し込むことはできません でした。前だったら差し込めましたが、トーンマッピングと色空間変換は一番最後に適用すべきなのであまり意味がないですね。私の調べ方が不十分なだけかもしれませんが。

BT.709 / Linear → BT.2020 / PQ の変換は Microsoft の D3D12 HDR sample にそのものずばりの PixelShader のコードがあるので、それを利用するのが手っ取り早いでしょう。

表示例

Bit Depth 設定と Post Process による色空間変換の適用によってレンダリング結果にどのような違いが生じるかを見ていきます。

画像は適当な Sphere に強めの Light をあてただけのものです。 Tone mapping はしていません。

画像 説明
1
f:id:tan-y:20200411141437p:plain
適切に sRGB 変換されたもの
2
f:id:tan-y:20200411141649p:plain
Bit Depth 10 にレンダリング結果を無変換でそのまま表示したもの
3
f:id:tan-y:20200411141737p:plain
レンダリング結果を BT.2020 / PQ に変換したものをそのまま SDR で表示したもの

1 はあからさまにおかしく見えますが、これは強い Light をあてて白飛びしているためです。

  • SDR (sRGB)
  • Bit Depth 16
  • Bit Depth 10 かつ Post Process で BT.709 / Linear → BT.2020 / PQ 変換したもの

を SDR キャプチャしたもので全て同じ表示になります。

f:id:tan-y:20200411142120p:plain
Bit Depth 16 出力映像の波形モニター

これは Bit Depth 16 でレンダリングしたものを Windows 10 の機能で JPEG XR に HDR キャプチャしたものを波形モニターで見たものです。 1.0 オーバーの部分は SDR 下では全て飛びます。 HDR 環境では綺麗な描画で見ることができます。

2 は単純に設定を Bit Depth 10 にしただけのものです。これだとわかりにくいですが色空間が合っていないので 1 と色合いが変わってしまっています。

3 は PQ のソースをそのまま sRGB に表示した場合の見栄えです。傾向として色合いが大分浅く見えます。これも色空間が合っていないので色は正しくありません。が 0~1 の範囲に値が収まるので SDR 環境で HDR のグラデーションのかかり具合が視認できます。

Unity Editor ではウィンドウ内の描画が丸ごと指定の色空間になります。

f:id:tan-y:20200411143847p:plain
Bit Depth 10 の UI
f:id:tan-y:20200411143922p:plain
Bit Depth 16 の UI

UI は BT.709 / Linear でレンダリングしていると思われます。そのため Bit Depth 16 の UI は通常の SDR と同じに見えますが、 Bit Depth 10 は白っぽくなってしまっています。これは UI 部分の色空間変換をせず、そのまま出力してしまっているためでしょう。せめて UI の色くらいはなんとかして欲しいですね・・・

Unity でのレンダリング色空間を考える

Unity の Player Settings で設定する "Color Space" は実際のところ "伝達特性" の定義であって色域については規定がされてなく、最終出力の sRGB から BT.709 を色域とする、といった形になっているのではないかと思います。実際のところは最終的な出力時に色空間変換を自前ですれば (Bit Depth 10 出力有効時はそもそも必要) レンダリング時の色空間はなんでもよいわけです。

まず前提として scRGB (BT.709 / Linear) は負の値を許容することで広色域になっていますが、そもそも負の値を扱えるバッファが必要になります。 Frame Debugger で観察してみるとレンダリングバッファは次のフォーマットのようです。 (ColorSpace = Linear)

GraphicsFormat
Legacy R16G16B16A16_SFloat
URP B10G11R11_UFloatPack32
HDRP B10G11R11_UFloatPack32 or R16G16B16A16_SFloat

B10G11R11_UFloatPack32 は 1 ピクセルあたり 32bit ですが符号ビットがないので負の値を持てません。一方 R16G16B16A16_SFloat は負の値が持てるものの 1 ピクセルあたり 64bit となりメモリ負荷が高いのと URP では選択できないようです。

BT.709 より広い色域のものにすれば 0~1 の範囲で扱うことが可能になります。一方で BT.709 と全く互換性がない事からワークフローも見直す必要が出てくると思います。もしも違う色域にするなら BT.2020 が現在の HDR コンテンツが採用している標準的な色域で表現範囲も一番広いのでよいのではないかと思います。

以上の事から次のように考えられます。

  • BT.709 は sRGB と色域が同じなので従来ワークフローと互換性が高いが、広色域にする場合は負の値が表現できる必要がある
    • 負の値が表現できる R16G16B16A16_SFloat はメモリ負荷が高めなのでそれを許容する必要がある
    • 色域については BT.709 の範囲で留め、 HDR 表現のみ採用するという判断も考えられる
  • BT.2020 などより広色域のものにすれば負の値を扱わずに広色域にすることが可能になるが、ワークフローの見直しが必要になる
    • RGB 値が BT.709 と異なるものになる (BT.709 下で色選択しても正しくなくなる)
    • テクスチャのインポート時にもあらかじめ色域を変換しておく必要がある
    • Editor 上でのプレビューが難しくなるかもしれない

BT.2020 は Unity 自体にサポートが入ってほしいかなあと思います。

おわりに

HDR 出力制御の C# API が追加されたことにより、 Unity でも現実的に HDR 出力に対応したアプリの開発が可能になってきたと思います。一方でレンダリング色空間や Unity Editor の UI の色空間の扱いなどアプリ開発者側的に不便なところもまだあると思います。 今後の拡張にも期待していきたいところです。

"[非公式] Unite Tokyo 2019 Eve2 LT Fes" (2019/9/23) で登壇しました

UniteEve2 で登壇させていただきました。

発表資料

めちゃくちゃ充実した会でとてもよかったです。時間的にはものすごい長いはずだったのですが、あっという間の感じでした。

発表のフォローなど

また発表やらかしてしまったなあと・・・ 結構削ったのですが、これ以上になると章単位で丸ごとになりそうだったのですがそれくらい削ってもよかったかなあと思いました。途中またテンパってしまってしまいましたし。

今回の発表に向けていろいろ検証しましたが、削ったりテンパったりでうまく伝えられていないところも多々あると思いますので、その辺りを書いていきたいと思います。

せっかくなので一部再検証しました。

Unity AOV Recorder

AOV は "Arbitrary Output Variable" の略だそうです。

Deferred Rendering に使用する GBuffer の各レンダリング結果をそのまま出力して、後段の Compositor の素材とするためのものですが、 Unite Tokyo 2019 の映像制作系のセッションではどれもコンポジット含めた最終レンダリングまでを Unity で完結させる話ばかりで「あれー」と思わなくもありませんでしたが技術的には新規性のある話ではないのでまあそれはそうなのかもしれません。

色化け問題 (明るさ)

アルファキャプチャ問題はバグとしか思えないのですが、明るさの件は (Texture Sampling モードを考えると若干一貫性に欠けますが) まあ概ね "仕様" と言い切っても差し支えのない挙動かなとは思います。

発表では時間の兼ね合いもあったので理屈抜きでざっくりとした説明だけになってしまいましたが、検証結果を整理してもう少し詳しく説明したいと思います。

↑ Gamma / Linear についての解説で個人的に一番わかりやすい解説記事だったのでお勧めです。

検証は数値的に Linear に遷移するグラデーションを Shader で記述し、そのレンダリング結果を Unity Recorder で様々な設定の組み合わせでキャプチャしたものを別のアプリで解析したものを見て行いました。

基本的に画像ファイルの Color Space は PNG は Gamma (sRGB) 、 EXR は Linear なので、Project Settings と画像ファイルの Color Space が一致すればグラフは Linear になるはずです (数値的に Linear グラデーションになるようにしているから) 。

Color Space Capture Format グラフ Result
1 Gamma Game View PNG (a) OK
2 EXR (a) NG
3 Render Texture PNG (a) OK
4 EXR (a) NG
5 Texture Sampling PNG (a) OK
6 EXR (a) NG
7 Linear Game View PNG (b) OK
8 EXR (b) NG
9 Render Texture PNG (a) NG
10 EXR (a) OK
11 Texture Sampling PNG (b) OK
12 EXR (a) OK

f:id:tan-y:20191005100007p:plain
(a) Linear
f:id:tan-y:20191005100120p:plain
(b) Gamma Correction
f:id:tan-y:20191005100258p:plain
(c) Display Gamma

  • 1, 3, 5, 10, 12 は入出力の色空間が一致していてグラフが Linear になっているので理解しやすいと思います。
  • 2, 4, 6 は Display Gamma のグラフにならないとおかしいです。
  • 7, 11 は Linear 空間の映像を Gamma 空間の PNG に書き出すので Gamma Correct のグラフになります。 9 は Linear のままなのでおかしい。
  • 8 は入出力の色空間が一致しているので Linear グラフになっていないとおかしいのに Gamma Correct になっています。これは補正が二重がけになるので非常に明るくなってしまっています。

上記から、おそらく次のような理屈に基づいて Unity Recorder では処理がされていると思われます。

  • Game View, Targeted Camera などはディスプレイに表示する映像を想定しているので "Gamma" 空間で出力する
    • Project Settings が Gamma ならそのまま出力し、 Linear ならガンマ補正をして出力する
  • Render Texture Assets は内部処理結果なのでそのまま出力する
  • 出力先ファイルフォーマットの色空間は考慮しない

"Texture Sampling" は問題ないかと思ったら Gamma EXR がダメでしたね (LT 発表時は未確認でした) 。

色化け問題 (色合い)

色空間には本来はいろいろな定義があり、 Gamma / Linear もその一部でしかありません。このうち Unity Recorder の Movie Recorder で発生する色化けは Unity では出て来ない "YUV 色空間" 問題です。

H.264 などのビデオコーデックは一般的には YUV という色空間にしてからエンコードします。その際の RGB - YUV 間の色空間変換の計算式 (変換マトリックス) にいくつか種類があります。エンコードされたビデオストリームにはヘッダーにどの変換マトリックスを用いているのか記録されていますが、ヘッダーに書かれている情報と実際に使われているものが異なっています。

具体的には HD 解像度では BT.709 、 SD 解像度では BT.601 (SMPTE 170M) で計算されていますがヘッダーにはどちらも BT.709 となっています。この挙動は割と想像がつきます (SD 解像度は BT.601 が標準なので、エンコーダーが解像度に応じてマトリックスを変える) 。

f:id:tan-y:20191006234714p:plain
H.264 480p
f:id:tan-y:20191006234717p:plain
H.264 1080p

ちょっとこれだと区別が付きにくいですが、下の 1080p の方は正しいです。グラデーションの右側の部分をスポイトで取得してみると確認できると思います。

元色(R,G,B) 480p 1080p
(1.0, 0.0, 0.0) (255, 25, 0) (255, 1, 0)
(0.0, 1.0, 0.0) (0, 215, 0) (0, 255, 0)
(0.0, 0.0, 1.0) (0, 14, 255) (1, 0, 255)

480p は誤差で済まない値になっています。

試した限りでは 720p 付近が境界っぽいので 1080p にしておけば間違いはないと思いますが、 Unity Recorder の Movie Recorder でのエンコードしながらのキャプチャーはパラメーター設定も細かくできないため画質面から元々あまりお勧めできません。この件に関わらず連番静止画でキャプチャーしてから別途エンコードした方がよいかと思います。

JXUGC #25 (2019/8/31) で LT してきました

先週の事ですが、 JXUG の勉強会で LT をさせていただきました。

発表について

ネタ選定の理由ですが、 Connpass のページで

Build 2019 の少し後に Xamarin.Forms 4.0 が発表になり、結構大きな追加機能(Shell など)が入りました。

とあったので Shell をネタにするかー、でもただ Shell にするとネタかぶりしそうだから Shell Renderer を実装しよう!、ということであのような内容にしました。

実際のところ Shell 自体がよくわかってなかったので Shell そのものから (表題の通り) 結構調べて、結果それだけで普通に発表ができる程度には理解が進んだように思います。 Shell は ShellItem のコレクションの組み方がキモだと思うのですが (この設定によってハンバーガーメニューの有無やタブの出方が決まります) その辺りの具体的な情報が私の調べた範囲では見当たりませんでした (API リファレンスくらい?) 。その辺の事を整理して発表した方が多くの方にはよいかなーとは思いましたが、まあ Platform ネタの方が自分らしいと思うので、そこはあえてということで・・・

Android との動作比較をしたかったのですが、 PC を修理に出していた関係で旧ノート PC を使っていたのですがスペックが低くてエミュレーターが動かなくて断念しました。

Shell Renderer の実装は組み方の基本がなんとなくわかったかなー程度で完全実装にはほど遠い感じです。まず先に MasterDetailPage をなんとかしないと話にならないとは思いますが・・・ なかなか時間とれなくて進められていないのですが、時間見つけてやっていきたいところです。今回久しぶりにそこそこやれたのでよかったです。

会を通して

JXUG で初めて LT して懇親会も最後まで参加させていただいて楽しい時間でした。また参加したいと思いますのでよろしくお願いいたします。