すぎしーのXRと3DCG

主にXR, Unity, 3DCG系の記事を投稿していきます。

Git for Windowsをコマンドラインからインストールする方法

概要

今回はWindowsコマンドラインでGit for Windows (Git Bash) をインストールする方法を紹介します。

Git for WindowsWindows上でGitやBashを使用する場合にインストールしておくと何かと役に立つパッケージですが、
GUI経由でのインストールは必要オプションの取捨選択が必要であったり、なによりインストール状態の再現性に乏しいところがあります。

コマンドライン経由のインストール方法を確立することで、再現性があり開発環境インフラ構築などにも応用させることができますためよかったらご活用ください!

動作環境

  • Windows 10
    • 筆者はPro版ですが、Homeでも問題ないと思います

注意事項

  • 以下を試す場合は自己責任でお願いします!
  • 以下の処理は管理者権限で実行しますが、当方は事故や故障などの一切の責任を負いません!

上記の件、ご留意ください

変更履歴

  • 2022/05/17
    • 「2022/05/16 23:00以前のExampleを試した場合は以下を実施をお願いします」を追加
    • 「(任意)PATHの追加」に注意文を追記
  • 2023/02/05
    • 「install-git.ps1 を実行」を PowerShell -ExecutionPolicy Bypass を使ったコマンドに修正

2022/05/16 23:00以前のExampleを試した方は以下を実施を推奨

環境変数の設定に使用していたコマンド ([Environment]::SetEnvironmentVariable) に少し難点がありました。
WindowsInfrastructureExample 上のスクリプトは修正済みです。

もし、すでに試していてパスが通らなくなったり、コマンドが実行できなくなった方は以下の「余談、PATHのREG_SZ登録をREG_EXPAND_SZに切り替える方法」を試してみてください。

余談、PATHのREG_SZ登録をREG_EXPAND_SZに切り替える方法

詳細も上記記事に記載しています。


インストール方法

インストールオプションについて

  • 以下のコマンドでのGit for Windowsのインストールオプションは筆者が普段使用しているオプションを使用
    • 詳細は後述しますが、各自で git-for-windows.inf 都合の良いように改変してください

Gitからps1ファイルをクローン

github.com

PowerShellを管理者権限で開く

※開き方は好きなやり方で問題ありません

今回はWindows 10標準搭載のPowerShellを使用

  • Windows キーを押して、「powershell」を検索
  • 「管理者として実行する」をクリック

PowerShellでクローンしたWindowsInfrastructureExample上に移動

> cd <path>\WindowsInfrastructureExample

Git-2.x.x-64-bit.exe を配置

コマンドでダウンロードする場合

> Invoke-WebRequest https://github.com/git-for-windows/git/releases/download/v2.36.1.windows.1/Git-2.36.1-64-bit.exe -OutFile Git-2.36.1-64-bit.exe

※インフラ構築のスクリプトを書く場合は、このコマンド自体をps1スクリプトに組み込んでも良い

ブラウザでダウンロードする場合

以下より Git-2.x.x-64-bit.exeをダウンロードし、WindowsInfrastructureExample フォルダ以下に配置

github.com

install-git.ps1 を実行

> PowerShell -ExecutionPolicy Bypass .\install-git.ps1

エラーログが出なければ問題なく Git for Windowsのインストール完了

インストールの確認 (コマンドラインの場合)

  • Dir 'C:\Program Files\Git\cmd\git.exe'でエラーが出なければインストール完了
> Dir 'C:\Program Files\Git\bin\git.exe'


    ディレクトリ: C:\Program Files\Git\bin


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2022/05/09     13:28          45584 git.exe

(任意)PATHの追加

(追記)ローカルPCの環境変数 PATH が変更されます!ご注意ください!

> .\set-git-path.ps1

以上でインストール及び設定は完了です。


スクリプト解説

install-git.ps1

InnoSetup

Git for WindowsはInnoSetupというフリーのWindows向けインストーラーが使用されています

パラメータの詳細は Setup Command Line Parameters で確認できます

jrsoftware.org

  • /ALLUSERS はその通りWindows上の全部のユーザーにインストール
  • /VERYSILENT はダイアログなどを抑制
  • /Log xxx.log でログ出力
    • 標準出力に直接ログを流し込めなかったため、一度ログファイルに落として最後に出力に流しています

/LOADINF

/LOADINF=git-for-windows.inf

オプションファイルの指定で、実質的にGit for Windowsのオプション指定方法になります

  • Gitのcommit時の改行にLFを指定 (CRLFOption)
  • Credential Managerの使用 (UseCredentialManager)
  • コマンドプロンプトでgitを使用可能にするかの指定 (PathOption)
  • etc...

手順で使用したファイルは git-for-windows.inf となります

後述の SAVEINF からオプションファイルを作成できます。

自分はインストーラーのスクリプト (install.iss) から割り出してました。。。

余談、git-for-windows.inf の方針

  • CLI特化
  • UNIX系と互換性を意識
    • OpenSSHの使用
    • LFOnly指定など
  • Credential Manager を有効化
    • これがあるとGitHubとの連携が楽になります! (別記事でいずれ紹介します!)
  • デフォルトブランチ名はmain (昨今の流れを汲み取って)
  • git LFS を有効化

/SAVEINF

/LOADINF=git-for-windows_save.inf

install-git.ps1では非使用

今回は直接は使用していませんが、指定するとインストール時のオプションを確認可能です!

| Out-Null

コマンドの最後に | Out-Null を入れることで、インストール完了までコマンド終了待ちになります。
PowerShellだとメジャーなテクニックのようです。

余談、今回の調査で躓いたところ

  • PowerShellで変数に実行ファイルを仕込む場合は & ./$PackageFileのように記載
    • &なしで ./$PackageFile とするとエラーになる
  • Set-ExecutionPolicy を実行しないと PowerShell上でps1ファイルが実行できない
  • オプションの探し方
    • Foo.exe /? or Foo.exe /HELP などで確認できる場合があるようです
    • Windowsのダイアログ出でるのでコピーできないなど面倒。。。
  • 別ps1ファイルのfunctionをコールする方法

自分は元々UNIX系のエンジニアだったので、WindowsPowerShellコマンドライン化には結構手こずりました。
再現性のあるインフラの構築にはIaC (Infrastructure as Code) は欠かせないのですが、Windowsに慣れていないせいかほとんどPowerShell調査みたいになってしまいました。

雑感

前回の記事投稿から2ヶ月経ってしまいましたね。。。

しかも今回はXRと3DCGから程遠いCLIに関する記事に。

「3DCGとかUnityやる気あるの?」と思われた方もいらっしゃるかもしれませんが、ちゃんと普段は触ってます!
地味なことですが、再現性のある開発環境の構築方法は何かと便利だったりします。

また、Git for Windowsってインストールしておくと、UnityのPackage ManagerでGitHub経由のパッケージが取得しやすくなったり、
GitHubHTTPS経由での連携が安定したりと、何気に非エンジニアの方にもメリットがあったりします!

さて、次回は「PowerShell7をコマンドラインからインストールする方法」にする予定です!

それでは~

【Unity, XR】Single Pass Instanced向けURP Render FeatureのTips 【2020.3, 2021.2】

概要

UnityのUniversal Render Pipelineには Render Feature という描画処理を差し込む機能があります。
この Render Feature ですが、XRのSingle Pass Instanced 対応をする場合はクセがあります。

今回はそんなSingle Pass Instanced向けのRender FeatureのTipsを共有します。

追記

  • 2022/03/27 「おまけ、IntermediateTextureMode.Alwaysの指定なしでも機能させる方法」項目を追加

略語

用語が長いので、以下では略語で紹介します。

用語 略語
Universal Render Pipeline URP
Render Feature RF
Single Pass Instanced SPI
Render Texture RT

動作環境

  • Unity 2020.3.30f1 + Universal RP 10.8.1
  • Unity 2021.2.16f1 + Universal RP 12.1.6

Tips Unity 2020

ポストエフェクトでは cmd.Blit は使用不可で cmd.DrawMesh を使用

詳細は以下のドキュメントに記載されています。

docs.unity3d.com

※以下、上記ドキュメントを 「SPI Blit Example」と呼びます

NOTE: Do not use the cmd.Blit method in URP XR projects because that method has compatibility issues with the URP XR integration.

互換性の問題でSPIでは cmd.Blit は正しく機能しないようで、
もし、ポストエフェクト的なことを実施したい場合は cmd.DrawMesh + _CameraColorTarget から画面キャプチャを取得して Blit を実現します。

サンプルのColorBlitRenderFeature について

ちなみに SPI Blit Example の ColorBlitRenderFeature のサンプルは描画の緑成分を抽出するポストエフェクトになっています。

f:id:tsgcpp:20220321235931p:plainf:id:tsgcpp:20220321235957p:plain

(2021.2以降) SwapBufferはSPIでは実質的に使用不可

Unity 2021.2 ではRFでポストエフェクトを実装しやすいようにSwapBufferという機能が追加されています。

残念ながらSwapBufferはSPIでは使用できません。。。(もし使用できる場合は教えていただけるとありがたいです!!)。
原因はSwapBufferの処理が隠蔽されていること、SwapBufferを使用するBlitメソッドが cmd.Blit を使用するためです。

※SwapBufferについては以下の動画をご参照ください。

www.youtube.com

画面キャプチャは不透明オブジェクトのみ

さて、ポストエフェクトと言いましたが、URP + SPIにおいては欠点があります。

不透明オブジェクトの描画結果しか取得できません

これは、シェーダーに渡されるテクスチャの名前 (_CameraOpaqueTexture) の通り、
不透明オブジェクトのみ描画した画面キャプチャが渡されるためです。

f:id:tsgcpp:20220323003420p:plain

f:id:tsgcpp:20220322000310p:plain
renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing の場合

f:id:tsgcpp:20220322012404p:plain
renderPassEvent = RenderPassEvent.BeforeRenderingTransparents の場合

SwapBufferも使用できないため、残念ながらSPIでのポストエフェクトは透過オブジェクトを対象にできないなど制限が強いです。。。

完全な描画結果が取れない理由

SPIにおいて、スクリーン向けRTにアクセスする術がないためです。

  • ScriptableRenderer.GetCameraColorFrontBuffer が internalメソッドのためアクセスできない
  • (2021.2以降)SwapBuffer用のRTにアクセスできない

Unity 2021.2以降でのポストエフェクトにはIntermediateTextureMode.Alwaysの指定が必要

Unity2021.2から IntermediateTextureMode という設定項目が追加されました。

IntermediateTextureMode.Alwaysを指定しないとSPI Blit Exampleの ColorBlitRenderFeature は機能しません!

docs.unity3d.com

名前の通り、中間テクスチャ(IntermediateTexture)を使用するかの指定になります。

f:id:tsgcpp:20220322004654p:plain

IntermediateTextureMode.Alwaysの指定が必要な理由

指定していない場合は cmd.DrawMesh の描画先が中間テクスチャではなくスクリーンのRTに直接描画されるのですが、
その後、FinalBlitPassによってスクリーンのRTが上書きされてしまうためです。

以下のコードにIntermediateTextureModeによる条件分岐があります。 github.com

SPI Blit Example には、注意書きがないのでIntermediateTextureMode.Alwaysの指定を忘れると Unity 2021.2 では実質的に描画結果に現れません。

判明するまで数時間かかりました。。。一応Report投げときました。

ちなみに非SPI(Multi Pass) や NonXRではSwapBufferが使用可能なので問題になりません。

Unity 2020.3 以前での中間テクスチャ

Unity 2020.3 + Universal RP 10 では IntermediateTextureMode は有りませんが、
実は RF が Forward Rendererに1つでも追加されている場合に、中間テクスチャが自動で有効になっていたようです。

以下のコードの rendererFeatures.Count != 0 という判定からわかります。

github.com

おまけ、IntermediateTextureMode.Alwaysの指定なしでも機能させる方法

IntermediateTextureMode.Alwaysを指定する方が確実と思われますが、一応紹介します。

SPI Blit ExampleのColorBlitPassを以下のようにrenderingData.cameraData.renderer.cameraColorTargetを指定することで、 IntermediateTextureMode.Alwaysを指定していない場合でも機能させることができます。

--- a/Assets/ColorBlitPass.cs
+++ b/Assets/ColorBlitPass.cs
@@ -34,7 +34,7 @@ internal class ColorBlitPass : ScriptableRenderPass
         using (new ProfilingScope(cmd, m_ProfilingSampler))
         {
             m_Material.SetFloat("_Intensity", m_Intensity);
-            cmd.SetRenderTarget(new RenderTargetIdentifier(m_CameraColorTarget, 0, CubemapFace.Unknown, -1));
+            cmd.SetRenderTarget(new RenderTargetIdentifier(renderingData.cameraData.renderer.cameraColorTarget, 0, CubemapFace.Unknown, -1));
             //The RenderingUtils.fullscreenMesh argument specifies that the mesh to draw is a quad.
             cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, m_Material);
         }

IntermediateTextureMode.Alwaysの指定なしでも機能する理由

  • renderingData.cameraData.renderer.cameraColorTargetは基本的に中間テクスチャとなっているため
    • 以下のコードを読む限りcreateColorTextureがtrueの場合に中間テクスチャが描画先にになるのですが、スクリーン(非SceneView)向け描画は基本的にtrueになるみたいです

github.com

createColorTexture == falseになる(中間テクスチャを描画先にしない)パターンのほうが珍しいですが、
SPIの場合で中間テクスチャを描画先にする場合は、renderingData.cameraData.renderer.cameraColorTargetを使用するならIntermediateTextureMode.Alwaysを指定する方が確実だと思います。

URP + SPI で可能な表現の例

不透明オブジェクトの描画結果を使用したポストエフェクト

透明オブジェクトは対象にできませんが、不透明オブジェクトならポストエフェクトをかけることができます。

f:id:tsgcpp:20220321235957p:plain

アルファブレンドによるフェードイン・フェードアウト

シェーダーのアルファブレンドは問題なく使用できるため、フェードイン・フェードアウトは実現できます。

            Blend SrcAlpha OneMinusSrcAlpha
...
            half4 frag (Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
                return half4(0, 0, 0, saturate(_Intensity));
            }

雑感

なんとか3月中に1つ記事が書けました! 本当はAddressablesに関する記事を書いていたんですが、検証の自動テストがGameCIで実現できず。。。

Unity2020でRFを使用すると中間テクスチャが必ず使用されるようになるのも知るきっかけになってよかったです(RF使用する場合のパフォーマンスの懸念自体は増えましたが)。

世間は卒業シーズン、新卒入社で新生活を始める人も多そうですね。 これからUnity、XR始める人にも興味を持っていただければ幸いです!

それでは~

【Moq】Moqはメソッドチェーンでまるごとモック化できるよ

メソッドチェーンのモック化

IFoo -> IBar -> IBaz のようにinterfaceのプロパティが連結されている場合、 以下の2つのモック化は同等になります。

            // setup
            var fooMock = new Mock<IFoo>();
            var barMock = new Mock<IBar>();
            var bazMock = new Mock<IBaz>();

            // when
            fooMock.Setup(m => m.Bar).Returns(barMock.Object);
            barMock.Setup(m => m.Baz).Returns(bazMock.Object);
            bazMock.Setup(m => m.Message).Returns("Hello Redundant");

            // then
            IFoo target = fooMock.Object;
            Assert.That(target.Bar.Baz.Message, Is.EqualTo("Hello Redundant"));
            Assert.That(target.Bar.Baz, Is.Not.Null);

            // setup
            var fooMock = new Mock<IFoo>();

            // when
            fooMock.Setup(m => m.Bar.Baz.Message).Returns("Hello Chain");

            // then
            IFoo target = fooMock.Object;
            Assert.That(target.Bar.Baz.Message, Is.EqualTo("Hello Chain"));

上部(以後Aパターン)のようにわざわざ、IFoo, IBar, IBazのMockを作る必要はなく、

下部(以後Bパターン)の fooMock.Setup(m => m.Bar.Baz.Message).Returns("Hello Chain"); のようにIFooからメソッドチェーンでまとめてモック化してくれるという話でした。

何が起こっているの?

どうやらAパターンの宣言でBパターンと同等になるようです。

後で検証コードを記載しますが、
target.BarMock<IBar>.Object, target.Bar.BazMock<IBaz>.Objectインスタンスが割り当てられていました。

Setupで指定しない場合はdefault

以下の用にSetupを実施しない場合はdefault、つまりnullを返します。

            // setup
            var fooMock = new Mock<IFoo>();

            // when
            // fooMock.Setup(m => m.Bar.Baz.Message).Returns("Hello Chain");

            // then
            IFoo target = fooMock.Object;
            Assert.That(() => target.Bar.Baz.Message, Throws.TypeOf<NullReferenceException>());

検証コード

  • NUnitで検証
  • IFoo, IBar, IBaz のinterfaceも定義

github.com

雑感

Moqを使ったことない同僚の方に、自分のコードを参考にMoqを使ってもらったら、
上記のようにメソッドチェーンでまるごとモック化する使い方をされていて発覚しました。

Moq使いだして2年以上、全然気づきませんでした。。。 先入観って怖いですね(笑)。

それでは~

【Unity, VSCode】Visual Studio Code Editor 1.2.5 への更新のススメ

概要

Visual Studio Code Editor 1.2.5 が 02/09 (日本だと2/10) に公開されたため、 1.2.4の問題と合わせて、1.2.5を紹介したいと思います。

今回はVSCode愛用者向けの記事となります。

対象Unity

  • Unity 2020 or later
  • Unity 2021 or later

1.2.4の問題

Windowsの場合に Package Manager経由で取り込んだ(Packages/ 以下の)ソースコードへの参照が機能しないバグがあったこと!

github.com

Find All Referencesなどでinterfaceや実装の確認もできず、コードのSuggestionも機能しないため、 VSCodeユーザーにとって、コーディングの効率を損なう問題がありました。。。

f:id:tsgcpp:20220212193601p:plain

一応、1.2.3 にダウングレードすれば上記問題は回避はできましたが、
ただ、最新のUnity2020とUnity2021では標準で1.2.4がインストールされるため、煩わしさがありました。

ちなみにですが、1.2.4は 2021/09/01 にリリースされたので5ヶ月は放置されていた問題になります。。。

1.2.5のススメ

っということで結論として1.2.5へ更新しましょう!

f:id:tsgcpp:20220212193246p:plain

ちゃんとWindowsで参照の問題が解決されていました。

f:id:tsgcpp:20220212192238p:plain

パッケージ更新後は Preferences -> External Tools -> Regenerate project files を忘れずに!

f:id:tsgcpp:20220212193812p:plain

ちなみに以下のプルリクが問題の修正箇所です。

github.com

この1行の修正を5ヶ月待ったことになります。。。

雑感

今Addressablesに関する記事を書いているんですが、 資料をまとめるのに時間がかかりそうなので、1つ軽めの記事を書いてみました。

多分、この記事のタイトル見て「さっさとRiderにしたら?」って思った方もいらっしゃるかもですね。

正直VSCodeに出会ってから、VSCodeの魅力に取りつかれて手放せなくなっています。

ちなみに僕のVSCodeを主な用途は以下です。

特にBlenderのアドオンの作りやすさは感動を覚えるレベルです!

もちろん、豊富なアドオンがあることも理由の一つですね。

そんなVSCodeですが、Unityでのバグが数ヶ月放置されていたのでやっと解消されてホッとしてます。

みなさんも、良いVSCodeライフを!

それでは~

【Unity】Transparentシェーダー レンダリングTips (RenderQueue編)

f:id:tsgcpp:20211227024159j:plain

概要

UnityにおけるTransparentシェーダー(透過シェーダー)に関するTipsです。

Transparentシェーダーは3DCGにおいてよく使われる一方で昔から非常に悩ましい存在であったりします。
特に描画順の関係で思ったような絵にならなかったというのはよくあるパターンだと思います。

Unityでの透過オブジェクトを利用する場合の参考にしていただければと思います。

動作環境

  • Windows 10
  • Unity 2021.2.6f1
  • Universal RP 12.1.2

検証対象のTransparentシェーダー

  • Shader Graphを使用
  • _Color プロパティのみを持ち、アルファも使用

f:id:tsgcpp:20211226235733p:plain

Transparentシェーダーの特徴

アルファブレンドの場合、描画の順番で結果が異なる

デフォルトのブレンド設定であるアルファブレンド(Blending Mode が Alpha)の場合、オブジェクトの描画順で結果が異なります。

アルファ0.4の赤と緑のシェーダーを使ったオブジェクトを描画順を変えて見てみましょう。

f:id:tsgcpp:20211227011002j:plain
緑 -> 赤

f:id:tsgcpp:20211227011020j:plain
赤 -> 緑

上記のように描画順で描画結果が異なります。

Transparentシェーダーはアルファブレンドの場合に描画順で結果が変わるということを覚えておきましょう!
基本的なことですが、とても重要な性質になります。

アルファブレンドは描画順で結果が変わる理由

簡単に説明するとアルファブレンドの計算式のためです。

R * a + G * (1 - a) => 0.4R + 0.6G (緑 -> 赤の順番)
G * a + R * (1 - a) => 0.6R + 0.4G (赤 -> 緑の順番)

※上記はイメージのための計算式で実際は R * Ra + (G * Ga + Dest * (1 - Ga)) * (1 - Ra) のような計算

加算ブレンドのみの場合、描画の順番に関わらず結果が同じ

加算ブレンド(Blending Mode が Additive)のマテリアル同士の場合は描画順に関わらず結果が同じになります。
加算ブレンドのみの場合ということに注意してください。

f:id:tsgcpp:20211227012441j:plain

アルファの乗算色を単純に加算するため、計算の順番で結果が変わらないからです。

R * Ra + G * Ga(緑 -> 赤の順番) = G * Ga + R * Ra(赤 -> 緑の順番)
* Ra: 赤のアルファ値, Ga: 緑のアルファ値

RenderQueueが2501以上の場合、ソートが発生

RenderQueue 2501以上かつ同一のRenderQueueの場合はカメラから遠いオブジェクトから近いオブジェクトの順番で描画する処理が発生します。

透明オブジェクトはカメラから遠いオブジェクトから近いオブジェクトの順番で描画したほうが結果が安定します。
Unityはデフォルトでソートを実施して、遠->近の順番でオブジェクトを描画してくれています。

※どちらもアルファは1.0

ポイントは以下です。

  • RenderQueue 2501以上かつ同一のRenderQueueのマテリアルを持つRendererが複数ある場合はソートが発生
    • カメラから見て遠いオブジェクトから近いオブジェクトの順番で描画
    • ソートの基準位置はオブジェクトのTransformのposition (オブジェクトのピボット)
  • 異なるRenderQueueのマテリアル間ではソートは発生しない
    • 異なる場合はRenderQueueが小 -> 大の順番で描画
  • RenderQueue 2500以下の場合はソートは発生しない
    • 不透明オブジェクトはデプステストにより基本的に描画結果が安定するため

TransparencySortMode の説明にも以下のように記載されています。

By default, perspective cameras sort objects based on distance from camera position to the object center;

おまけ1: BlendingMode Additiveの場合

描画順に関わらず結果が一定になります。

※どちらもアルファは1.0

おまけ2: 赤がAlpha, 緑がAdditiveの場合

全体がAdditiveではない場合は結果が不安定になります。

マテリアルにBlendingMode Alphaが存在する場合はRenderQueueを意識する必要があります。

※どちらもアルファは1.0

デプス (深度) を通常は書き込まない

Transparentシェーダーはデプスを書き込まないように設定することが一般的です。
こちらも描画順で結果が不安定になるためです。

  • Transparentシェーダーはデフォルト(Depth Write: Auto)の場合はデプスを書き込まない
  • 意図的に書き込むことは可能
    • ZWrite On (Depth Write: ForceEnabled) をシェーダー or マテリアルで指定

デプスを書き込まない場合と書き込む場合との違い

例えば以下のようにオブジェクトを配置した場合を見てみましょう。

  • 赤Cubeと緑Sphereを重なるように配置
  • 両マテリアルのアルファは0.4
  • 両マテリアルのRenderQueueは3000
  • Sphereのほうがカメラに近い
    • 遠い赤Cube -> 近い緑Sphereの順番で描画

f:id:tsgcpp:20211227232124p:plain

f:id:tsgcpp:20211227232327j:plain
デプスを書き込まない場合
f:id:tsgcpp:20211227232356j:plain
デプスを書き込む場合

デプスを書き込んだ場合、重なった部分の緑側が描画されていません。
これは赤 -> 緑と描画されて、赤側でデプスが書き込まれたため後続の緑側の深度テストによりピクセルシェーダーがスキップされたためです。

意図的にこちらの現象を利用することもありますが、基本的にはTransparentシェーダーはデプスを書き込まないのが一般的です。

Transparentシェーダーパフォーマンス

※本記事では詳しくは取り上げません。

ここまで読んだ方の中にはパフォーマンス面が気になった方もいらっしゃるかもしれません。
Transparentシェーダーはピクセルシェーダー処理が発生しやすい性質上、パフォーマンス面で注意が必要です。

簡単に言えばオーバードローという現象が確実に発生するようなものです。
安易にUIやパーティクルなどで使用すると描画負荷が一気に上がりますためご注意ください!

また、ソート処理も発生するためCPU側にも影響があると考えられます。

パフォーマンスに関しては別の機会に取り上げようと思います。

サンプルプロジェクト

github.com

雑感

今年最後の技術ブログになります。
今回は自分の復習も兼ねて3DCGっぽい記事にしてみました。

というかここ数ヶ月、3DCGっぽくない記事が多かったですね。。。

昔のUnityでは透明マテリアルはRenderQueue 2450だった気がしますが、いつの間にか3000になってましたね。
パフォーマンス面については別の機会に紹介しようとは思いますが、いつになることやら。。。

Unity 2021.2ではShader Graphに Allow Material Override でBlending Modeを切り替えれたり、URPでVFXが使えたりと2020.3と比べて一気に使いやすさが上がっている気がしています。

そういえば、サンプルプロジェクトにTimelineでマテリアルをいじる簡易的なカスタムTrackも入れているので良かったら参考にどうぞ(Preview機能とか色々微妙ですが)。

来年もUnityを追求していきたいです!

それでは、良いお年を~

【Unity C#】 IReadOnlyListとアロケーション

概要

今回はIReadOnlyListアロケーションについて深堀りしようと思います。

前回の記事でも述べましたが、IReadOnlyListアロケーションを回避したい場合は注意が必要なinterfaceになっていたりします。
そんなIReadOnlyListアロケーションについて調べたので共有します。

IReadOnlyListについては前回の記事で少し紹介していますので合わせてどうぞ!

tsgcpp.hateblo.jp

環境

IReadOnlyList の アロケーション発生ポイント

foreach使用時などのIEnumerable.GetEnumerator

ListIReadOnlyList に変換してforeachに回すと使用するたびにアロケーションが発生します。

        List<Foo> list = new List<Foo>();
        foreach (var item in list) { }  // アロケーションは発生しない

        IReadOnlyList<Foo> readonlyList = list;
        foreach (var item in readonlyList) { }  // アロケーションが発生する

アロケーションの原因

Listの場合はEnumerable(値型)のboxingが原因です。

        public Enumerator GetEnumerator() {
            return new Enumerator(this);
        }

list.cs,569 より

ListGetEnumeratorが返すEnumerableは値型のため、Listを直接使用する場合はアロケーションは発生しません。

一方で、IReadOnlyListに変換した場合のGetEnumeratorIEnumerableとしてEnumerableを返します。

        IEnumerator<T> IEnumerable<T>.GetEnumerator() {
            return new Enumerator(this);
        }

list.cs,574 より

この戻り値のEnumerableからIEnumerableは値型から参照型への変換、つまりboxingが発生するためアロケーションが発生します。

アロケーションの回避方法

foreach ではなく for を使用すれば回避可能です。

        for (int i = 0; i < readonlyList.Count; ++i)
        {
            var item = readonlyList[i];
        }

IEnumerableと異なりIReadOnlyListlist[i]が配列の要素に直接アクセスする形となっています。

list.cs,1107

ListをIEnumerable(or IReadOnlyCollection)に変換してしまうとforが使用不可でGetEnumeratorによるアロケーションは避けられなくなりますが  IReadOnlyListへの変換であれば読込専用を保ちつつforが使用可能なため結果的にアロケーションの回避が可能です。

コーディングの煩わしさはありますが、メモリ的には優しくなります。

IEquatable<T>を実装しない値型(struct)でEquals

IEquatable<T>を継承していない値型はEqualsの挙動によりアロケーションが発生するパターンが多いです。

IReadOnlyListとは直接は関係ありませんが、後述するLinq.Enumerable.Containsに関わってくるため紹介します。

    public struct SimpleData
    {
        public int value;
    }

アロケーションの原因

C#の型に定義されているValueType.Equals(Object)は引数にobject型を受け取ります。

docs.microsoft.com

つまり、このEqualsに値型を渡すと値型から参照型への変換でboxingが発生しアロケーションに繋がります。

アロケーションの回避方法

主に2つあります。

  • structにIEquatable<T>を継承して実装
    • Equals(T value)で型指定となるためboxingが発生しない
  • IEqualityComparer<T>を継承したオブジェクトを実装して使用
    • Equals(T x, T y)で型指定となるためboxingが発生しない
    public struct EquatableData : IEquatable<EquatableData>
    {
        public int value;

        public bool Equals(EquatableData other)
        {
            return this.value == other.value;
        }
        ...
    }

IReadOnlyListでLinq.Enumerable.Contains

using System.Linq;
...
        readonlyList.Contains(item);

IReadOnlyListは残念ながらContainsメソッドを持っていません(List.Containsは定義されている)。
拡張メソッドLinq.Enumerable.Containsを使用することで同様の機能を使用できます。

しかし、Linq.Enumerable.Containsアロケーションが発生するパターンが多いです。

docs.microsoft.com

アロケーションの回避方法

Linq.Enumerable.Containsアロケーションは細かい話が多いため先に回避方法を紹介します。
これに関してはIReadOnlyList向けのContainsを実装する必要がありました。

github.com

後述する「アロケーションの原因」を アロケーション回避の検証も含めUnitTestも作成しています。

余談ですが、汎用性を保つためwhere T : structのような制約は入れていません。
その代わりtypeof(T).IsValueTypeによる条件分岐が入っています。

アロケーションの原因

共変性変換のIReadOnlyListに対してLinq.Enumerable.Contains

「共変性を使用したIReadOnlyList」とは要するにList<Foo> -> IReadOnlyList<IFoo>みたいな変換のことです。 共変性については前回の記事を参照。

ポイントは 共変性を利用して変換したIReadOnlyList にあります。

実は List<Foo> -> IReadOnlyList<Foo> のように不変(TをFooのまま変換)の場合はアロケーションは発生しません。
共変性変換の場合にアロケーションが発生する要因はLinq.Enumerable.Containsの定義にあります。

       public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value) {
            ICollection<TSource> collection = source as ICollection<TSource>;  // ICollectionは不変性のため、IReadOnlyList<IFoo>へのキャストは不可のためnullを返す
            if (collection != null) return collection.Contains(value);
            return Contains<TSource>(source, value, null);
        }
 
        public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value, IEqualityComparer<TSource> comparer)
        {
            if (comparer == null) comparer = EqualityComparer<TSource>.Default;
            if (source == null) throw Error.ArgumentNull("source");
            foreach (TSource element in source)  // <- GetEnumerator使用によりboxingによるアロケーションが発生
                if (comparer.Equals(element, value)) return true;
            return false;
        }

Enumerable.cs,1364 より

ICollection<T>は共変性(out T)を持たず不変性のため、List<Foo> -> ICollection<IFoo>は不可となります。
よって IReadOnlyList<IFoo> -> ICollection<IFoo>の変換も不可のため、上記Containsのコードでsource as ICollection<TSource>はnullを返します。

その後foreachにたどり着き、「foreachなどIEnumerableを必要とする処理」で述べたアロケーションが発生します。

逆にIReadOnlyList<Foo>は不変性を満たすためICollection<Foo>にキャストができforeachに到達しないためアロケーションが発生しません。

なんともややこしい。。。

IEquatable<T>非継承のstructを型とするListでLinq.Enumerable.Contains

IEquatable<T>非継承のstructをLinq.Enumerable.Containsに使用した場合はアロケーションが発生します。

        private static EqualityComparer<T> CreateComparer() {
            ...

            if (typeof(IEquatable<T>).IsAssignableFrom(t)) {
                return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(GenericEqualityComparer<int>), t);
            }

            ...

            return new ObjectEqualityComparer<T>();
        }

equalitycomparer.cs,40 より

IEquatable<T>継承の場合は型指定のEqualityComparer<T>が生成されますが、
IEquatable<T>非継承の場合はObjectEqualityComparer<T>が使用されてboxingが発生するんですね。

IEqualityComparer<T>指定有りでLinq.Enumerable.Contains

Linq.Enumerable.Containsオーバーロードで第2引数にIEqualityComparer<T>指定可能なメソッドがあります。
Equalsboxingは回避できるんですが、残念ながらその後で使用されるforeachboxingは回避できません。。。

Enumerable.cs,1370

アロケーションの回避方法」にて記載したコードには、IEqualityComparer<T>指定可能なContainsも実装して記載しています。

Unity Test Runnerによるアロケーション確認

今回のアロケーション有無の検証ですがUnity Test Runnerを利用しました。

github.com

実装開始時はIs.AllocatingGCMemory()Is.Not.AllocatingGCMemory()を使い分けていましたが、 再利用性(TestBaseクラスからの派生)と視認性のためにIs.Not.AllocatingGCMemory()指定で統一しました。

以下の例だと✅でアロケーション発生無し、🚫でアロケーション発生有りという見方になります。
テストコード的にはおかしいですが、一旦視認性を重視しました(別の視認性が確保しやすい方法が思いついたら修正しておきます)。

また、1st, 2ndはインスタンスごとの初回と2回目でアロケーションの変化があるかを確認するために入れています。

List<Foo>

f:id:tsgcpp:20211116022853p:plain

List<Foo> -> IReadOnlyList<Foo>

foreach のみで発生

f:id:tsgcpp:20211116022823p:plain

List<Foo> -> IReadOnlyList<IFoo>

foreach および 共変性変換のためLinq.Enumerable.Containsで発生

f:id:tsgcpp:20211116022932p:plain

List<SimpleData> -> IReadOnlyList<SimpleData>

IEquatable非実装のstruct

f:id:tsgcpp:20211120235310p:plain

List<EquatableData> -> IReadOnlyList<EquatableData>

IEquatable実装のstruct

f:id:tsgcpp:20211120235152p:plain

サンプルコード

github.com

  • IReadOnlyList向けContainsなども含む

追記

Package Manager対応版を用意しました。

github.com

参考

雑感

世間ではFacebookからMetaへと社名変更、新しいOculusが発表、メタバースへの注目など、
XR界隈で目まぐるしい変化が来そうなときに、
「なんでXRとは程遠いC#の話してるの?」って言われそうですねw。

アプリケーションの楽しさに直接関連するものでは確かに有りませんが、
一方で快適なXRアプリケーションの実現において、過剰なアロケーションの回避や削減は大事な要素であると考えています。

もちろんUnityにおいて完全にアロケーションを避けることは難しいため、
こだわり過ぎず、避けれるなら避けるぐらいがちょうど良いのではと思います。

マニアックな話では有りましたが、少しでも面白いと思っていただけたなら幸いです。
それでは~

【Unity C#】 IReadOnlyListの紹介

概要

今回の主役は IReadOnlyList です!

docs.microsoft.com

Listでもなく、IReadOnlyCollectionでもありません!

個人的には パフォーマンスポリモーフィズムを併せ持った良いinterfaceだと思っています。

しかし、アロケーションの発生を避けたいときにはなかなか注意が必要 な存在だったりします。。。
そんなIReadOnlyListを紹介していきたいと思います。

ちなみにアロケーションについては別記事にする予定です。

IReadOnlyList について

注意事項

  • ダウンキャスト (readonlyList as List<>など)は禁止という前提で説明
    • ダウンキャストを許した場合は読込専用が簡単に崩壊するため

追記

2022/02/25

  • 「Unityでの使用例」で共変性を利用したアップキャストになるようにコード例を修正

2022/02/26

IReadOnlyListは読込専用のList

  • 名前の通り 読込専用のList 向けinterface
    • 標準配列(Array)やListなどが継承している
  • 各要素の変更が不可になっており list[0] = default などはコンパイルエラーとなる
  • 主な利用用途は変更不可のListとして提供したいときなど
    [Tooltip("ラベル一覧")]
    [SerializeField] private List<string> _labelList;

    // 読込専用としてラベル一覧をクラス外に提供
    public IReadOnlyList<string> LabelList => _labelList;

C#標準の一次元配列はIReadOnlyListにキャスト可能

        string[] strList = new string[] { "foo", "bar", "baz" };
        IReadOnlyList<string> readonlyStrList = strList;
  • 公式ドキュメントにも 「一次元配列は IList<T>IEnumerable<T> を実装している」と明記されている
    • Single-dimensional arrays also implement IList<T> and IEnumerable<T>.

docs.microsoft.com

IReadOnlyCollectionとの違いは index指定で要素にアクセスが可能なこと

  • list[i]List同様に使用可能
  • そもそもT this[int index] { get; }の定義は IReadOnlyList由来
  • IReadOnlyCollectionと異なりindexさえわかれば O(1)の計算量でアクセス可能

※個人的にパフォーマンス的観点で有効性を感じるところ

IReadOnlyListのTに値型を宣言すれば要素含め読込専用となる

    [SerializeField] private List<Vector3> _vectorList;
    public IReadOnlyList<Vector3> VectorList => _vectorList;
  • 上記のようにTが値型でIReadOnlyListに変換した場合は完全な読込専用として提供される

IReadOnlyListは参照型の要素を読込専用にはしない

  • IReadOnlyListはあくまでList自体の変更を不可にしたもので、要素本体はT型に従う
    • 要素の方がGameObjectの場合は引き続きnameやtransformを変更することは可能
        IReadOnlyList<GameObject> readonlyObjectList = new List<GameObject> { ... };
        readonlyObjectList[0].name = "Renamed_" + readonlyObjectList[0].name;

但し、後述する共変性を利用することで参照型の要素本体も読込専用としての提供も可能

IReadOnlyList<out T>のため共変性持つ

public interface IReadOnlyList<out T> : System.Collections.Generic.IEnumerable<out T>, System.Collections.Generic.IReadOnlyCollection<out T>
  • 上記のように out T で宣言されているため、IReadOnlyListは共変性がある

例えば以下のような参照型クラスFoo、interface INameHolder があったとする。

public interface INameHolder
{
    string Name { get; }
}

public class Foo : INameHolder
{
    public string Name { get; set; } = "Default Name";
}

上記Foo使ったList<Foo>は共変性を使って以下のような変換が可能。

        IReadOnlyList<INameHolder> readonlyNameHolderList = new List<Foo> { ... };
        // コンパイルエラー(INameHolderはget_Nameのみ提供のため)
        readonlyNameHolderList[1].Name = "Melon";

readonlyの継承型として提供することで参照型要素もreadonlyとして提供可能

Unityでの使用例

MonoBehaviourやScriptableObjectを依存逆転の法則を当てはめて提供

  • Unityとは疎結合にしたまま、Unityの機能を利用した読込専用データ群を配布に利用するなど

※以下はあくまで実装イメージ

[CreateAssetMenu(fileName = nameof(ScriptableObjectNameHolder), menuName = "ScriptableObjects/" + nameof(ScriptableObjectNameHolder), order = 1)]
public sealed class ScriptableObjectNameHolder : ScriptableObject, INameHolder
{
    [SerializeField] private string _name;

    // ScriptableObject側で定義された名前を読込専用として提供
    public string Name => _name;
}

f:id:tsgcpp:20220225231223p:plain

以下の様に List<ScriptableObjectNameHolder> から IReadOnlyList<INameHolder> に変換し、Unityとは疎結合インスタンス公開が可能。

    [SerializeField] private List<ScriptableObjectNameHolder> _nameHolders;

    // 共変性を利用してUnityの存在を隠蔽し、読み込み専用として公開
    public IReadOnlyList<INameHolder> NameHolders => _nameHolders;

インスタンスの公開はExtenjectやVContainerなどを利用することが多いですが詳細は割愛。

関連

本題記事

tsgcpp.hateblo.jp

雑感

久々の投稿です。
最近、環境が変わって色々ドタバタしていました。

世間ではFacebookのMetaへ社名変更、新しいQuestが発表、メタバースへの注目など、
XR界隈で目まぐるしい変化が来そうなときに、
「なんでC#の話してるの?」って言われそうですねw。

IReadOnlyListはUnityでの開発でも結構役立つことが多いと感じています。
DI, SOLID原則, テスタビリティ, etc...

ちなみに今回の記事は前座で本題は次回の「IReadOnlyListのアロケーション(関連にリンク)」だったりします!
なんでアロケーションなんかを調べたのかは次回にお話できればと思います。

それでは~