koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

List<T>の内部の配列の参照を取得する

はじめに

C#List<T> はいわゆる可変長配列のことであり,内部的には配列を持っている. 要素追加の度に残り容量を確認し,不足するようであればより大きな配列を確保し,そこに既存の要素をコピーした上で新しい要素を入れるという実装になっている.

List<T> のインデックスアクセサ自体はとても単純なものであり,おそらくインライン展開されるため,速度面で気になることはほぼない.

しかし,P/Invoke等でどうしても List<T> の内部の配列が欲しい場合もある. 本記事では List<T> の内部の配列の参照を得る方法を紹介する.

リフレクション

一番愚直にリフレクションを用いる方法である. 当然のことだが遅いため,何度も用いる場合には向かない.

using System;
using System.Collections.Generic;
using System.Reflection;


/// <summary>
/// Provides some utility methods of <see cref="List{T}"/>.
/// </summary>
public static class ListUtils
{
    /// <summary>
    /// Get internal array of <see cref="List{T}"/>.
    /// </summary>
    public static T[] GetArray<T>(List<T> list)
    {
        return (T[])(typeof(List<T>).GetField(
                "_items",
                BindingFlags.GetField
                    | BindingFlags.NonPublic
                    | BindingFlags.Instance)
                ?? throw new ArgumentException("FieldInfo not found: System.Collections.Generic.List<T>._items"))
            .GetValue(list);
    }
}

List<T> の内部の配列のメンバ名は _items であり,シリアライズのこともあるため,メンバ名変更は禁止というコメントが見られたため,近い将来で名前変更されることはまずないと考えていいと思われる.

IL生成

ILを生成し,デリゲートとしてキャッシュすることで,リフレクションの高速化を図る. 2回目以降はリフレクションを用いるより速くなるはずで,何度も List<T> の内部の配列を得たい場合はこの方法が良い.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;



/// <summary>
/// Provides some utility methods of <see cref="List{T}"/>.
/// </summary>
public static class ListUtils
{
    /// <summary>
    /// Get internal array of <see cref="List{T}"/>.
    /// </summary>
    public static T[] GetArray<T>(List<T> list)
    {
        return ListUtils<T>.GetArray(list);
    }
}


/// <summary>
/// Provides some utility methods of <see cref="List{T}"/>.
/// </summary>
public static class ListUtils<T>
{
    /// <summary>
    /// Cache of delegate of <see cref="CreateGetArrayFunc"/>.
    /// </summary>
    private static Func<List<T>, T[]>? _getArray;

    /// <summary>
    /// Get internal array of <see cref="List{T}"/>.
    /// </summary>
    /// <param name="list">Target list.</param>
    /// <returns>Internal array of <see cref="List{T}"/>.</returns>
    public static T[] GetArray(List<T> list)
    {
        return (_getArray ??= CreateGetArrayFunc())(list);
    }

    /// <summary>
    /// Create method which gets internal array of <see cref="List{T}"/>.
    /// </summary>
    /// <typeparam name="T">Element type of <see cref="List{T}"/>.</typeparam>
    private static Func<List<T>, T[]> CreateGetArrayFunc()
    {
        var dynMethod = new DynamicMethod(
            "GetListArray",
            typeof(T[]),
            new [] { typeof(List<T>) },
            true);

        var ilGen = dynMethod.GetILGenerator();
        ilGen.Emit(OpCodes.Ldarg_0);
        ilGen.Emit(
            OpCodes.Ldfld,
            typeof(List<T>).GetField(
                "_items",
                BindingFlags.GetField
                    | BindingFlags.NonPublic
                    | BindingFlags.Instance)
                ?? throw new ArgumentException("FieldInfo not found: System.Collections.Generic.List<T>._items"));
        ilGen.Emit(OpCodes.Ret);

        return (Func<List<T>, T[]>)dynMethod.CreateDelegate(typeof(Func<List<T>, T[]>));
    }
}

List<T> という型引数を持つクラスを対象にするので,Static Type Cachingという手法を適用している. コンパイル時に型が解決するため, Type 型をキー,デリゲートを値にした Dictionary を用いるよりも高速である.

ジェネリック版の ListUtils型推論により型引数の指定を省略するためだけにある(呼び出し側で,例えば ListUtils.GetArray<int>(list) ではなく, ListUtils.GetArray(list) と書くだけでよい). ジェネリック版のみだと, ListUtils<int>.GetArray(list) のように書かなければならなくなり,面倒である.

式木

単にフィールドにアクセスするだけなので,IL版でも十分に保守性は高い(3命令しかないので)が,どうしても式木でやりたい場合は下記のようになる. 最終的にはIL生成していると思うが,生成までにやっていることは多いため,IL版より初回呼び出しは時間がかかるのではないだろうか?

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;


/// <summary>
/// Provides some utility methods of <see cref="List{T}"/>.
/// </summary>
public static class ListUtils
{
    /// <summary>
    /// Get internal array of <see cref="List{T}"/>.
    /// </summary>
    public static T[] GetArray<T>(List<T> list)
    {
        return ListUtils<T>.GetArray(list);
    }
}


/// <summary>
/// Provides some utility methods of <see cref="List{T}"/>.
/// </summary>
public static class ListUtils<T>
{
    /// <summary>
    /// Cache of delegate of <see cref="CreateGetArrayFunc"/>.
    /// </summary>
    private static Func<List<T>, T[]>? _getArray;

    /// <summary>
    /// Get internal array of <see cref="List{T}"/>.
    /// </summary>
    /// <param name="list">Target list.</param>
    /// <returns>Internal array of <see cref="List{T}"/>.</returns>
    public static T[] GetArray(List<T> list)
    {
        return (_getArray ??= CreateGetArrayFunc())(list);
    }

    /// <summary>
    /// Create method which gets internal array of <see cref="List{T}"/>.
    /// </summary>
    /// <typeparam name="T">Element type of <see cref="List{T}"/>.</typeparam>
    private static Func<List<T>, T[]> CreateGetArrayFunc()
    {
        // Arguments.
        var pList = Expression.Parameter(typeof(List<T>), "list");

        return Expression.Lambda<Func<List<T>, T[]>>(
            Expression.Field(
                pList,
                typeof(List<T>).GetField(
                    "_items",
                    BindingFlags.GetField
                        | BindingFlags.NonPublic
                        | BindingFlags.Instance)
                    ?? throw new ArgumentException("FieldInfo not found: System.Collections.Generic.List<T>._items")),
            "GetListArray",
            new []
            {
                pList
            }).Compile();
    }
}

lilToonのカスタムシェーダーを作るときのTips

基本

基本的なカスタムシェーダーの情報は下記公式ドキュメントを参照すること.

lilToon カスタムシェーダーの作り方

ドキュメント中にある下記のファイルがテンプレートであり,この中の数ファイルを編集する.

目次

custom.hlsl, custom_insert.hlsl について

知っておくべき文法

カスタムシェーダーはプリプロセッサを用いて,本体側の特定位置にカスタムシェーダーで記述した処理を差し込んだシェーダーを作成するようになっている. そのため,プリプロセスについてC/C++の文献等をあたり,基本的な文法は知っておいた方がよい.

プリプロセッサとともによく併用されるのが行末の \ である. これは次の行もその行にあるように扱う「行継続」の意味を持っている. 換言するならば改行の打ち消しである.

#define#if 等のプリプロセッサ指令は1行のみ有効であるため,本来は1行で記述しなければならない. しかし長い1行というのは視認性が悪い. 行継続を用いることで,視認性のために改行を行いつつも,コンパイラには1行として扱うように指示することができる.

custom.hlsl の注意点

基本的には処理置き換えのマクロにとどめておくこと. 関数定義もできるが,インクルード位置がuniform変数の宣言位置(custom.hlsl 内で記述している LIL_CUSTOM_PROPERTIES のマクロの展開箇所も含む)よりも前なので,uniform変数に依存する処理は書けないuniform変数に依存する かどうかに関わらず,関数定義は custom_insert.hlsl で行うように統一すると,問題は起こらないとも言える(好みの問題).

フラグメントシェーダーにおけるデフォルトの各処理

各処理の挿入位置

custom.hlsl, custom_insert.hlsl の変更が反映されない

Reimportを行うこと. 以前にコンパイルエラーがあった場合は,下記のコンパイルエラーがキャッシュされている件の解消方法を試した後にReimportを行うこと.

シェーダーのコンパイルエラー内容が古い内容のまま発生する

カスタムシェーダーは変にキャッシュが残ることがあり,custom.hlsl, custom_insert.hlsl を正しく修正してもコンパイルエラーが取れないことがある. この現象が発生すると,インスペクタの値の変更がプレビューに反映されない,インスペクタでエラーとなっているバリエーションのシェーダーが表示されず選択できない,等の現象が発生する.

この現象を解決するためには Library/ShaderCache.db を適当な sqlite3 クライアントで開き,下記のSQLを実行する.

DELETE FROM shadererrors;

sqlite3のコマンドラインツールなら下記のコマンドの実行でよい. (echoで '.exit' を出力するのはWindowsのため.空文字列を出力する方法がないため,受理されるコマンドを出力している.Linuxであれば空文字列でよい)

$ echo .exit | sqlite3 --cmd "DELETE FROM shadererrors;" ShaderCache.db

面倒であれば, Library/ShaderCache.db のファイル削除でもよいのだが,Unity起動中は削除することができない.

winsqlite3.dllが含まれる最近のWindows10や sqlite3 の動的ライブラリにパスが通っているMac/Linux限定にはなるが,下記の処理を設けることで,SQLite3のライブラリの同梱なしに右クリックメニューから DELETE FROM shadererrors を実行できるようにすることもできる.

/// <summary>
/// Callback method for menu item which refreshes shader cache and reimport.
/// </summary>
[MenuItem("Assets/TemplateFull/Refresh shader cache", false, 2000)]
private static void RefreshShaderCacheMenu()
{
    var result = NativeMethods.Open("Library/ShaderCache.db", out var dbHandle);
    if (result != 0)
    {
        Debug.LogError($"Failed to open Library/ShaderCache.db [{result}]");
        return;
    }

    try
    {
        result = NativeMethods.Execute(dbHandle, "DELETE FROM shadererrors", IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
        if (result != 0)
        {
            Debug.LogError($"SQL failed [{result}]");
            return;
        }
    }
    finally
    {
        result = NativeMethods.Close(dbHandle);
        if (result != 0)
        {
            Debug.LogError($"Failed to close database [{result}]");
        }
    }

    AssetDatabase.ImportAsset("Assets/TemplateFull/Shaders", ImportAssetOptions.ImportRecursive);
}


/// <summary>
/// Provides some native methods of SQLite3.
/// </summary>
internal static class NativeMethods
{
#if UNITY_EDITOR && !UNITY_EDITOR_WIN
    /// <summary>
    /// Native library name of SQLite3.
    /// </summary>
    private const string LibraryName = "sqlite3";
    /// <summary>
    /// Calling convention of library functions.
    /// </summary>
    private const CallingConvention CallConv = CallingConvention.Cdecl;
#else
    /// <summary>
    /// Native library name of SQLite3.
    /// </summary>
    private const string LibraryName = "winsqlite3";
    /// <summary>
    /// Calling convention of library functions.
    /// </summary>
    private const CallingConvention CallConv = CallingConvention.StdCall;
#endif
    /// <summary>
    /// Open database.
    /// </summary>
    /// <param name="filePath">SQLite3 database file path.</param>
    /// <param name="db">SQLite db handle.</param>
    /// <returns>Result code.</returns>
    /// <remarks>
    /// <seealso href="https://www.sqlite.org/c3ref/open.html"/>
    /// </remarks>
    [DllImport(LibraryName, EntryPoint = "sqlite3_open", CallingConvention = CallConv)]
    public static extern int Open(string filename, out IntPtr dbHandle);

    /// <summary>
    /// Close database.
    /// </summary>
    /// <param name="filePath">Database filename.</param>
    /// <param name="db">SQLite db handle.</param>
    /// <returns>Result code.</returns>
    /// <remarks>
    /// <seealso href="https://www.sqlite.org/c3ref/close.html"/>
    /// </remarks>
    [DllImport(LibraryName, EntryPoint = "sqlite3_close", CallingConvention = CallConv)]
    public static extern int Close(IntPtr db);

    /// <summary>
    /// Execute specified SQL.
    /// </summary>
    /// <param name="db">An open database.</param>
    /// <param name="sql">SQL to be evaluated.</param>
    /// <param name="callback">Callback function.</param>
    /// <param name="callbackArg">1st argument to callback.</param>
    /// <param name="pErrMsg">Error msg written here.</param>
    /// <returns>Result code.</returns>
    /// <remarks>
    /// <seealso href="https://www.sqlite.org/c3ref/exec.html"/>
    /// </remarks>
    [DllImport(LibraryName, EntryPoint = "sqlite3_exec", CallingConvention = CallConv)]
    public static extern int Execute(IntPtr dbHandle, string sql, IntPtr callback, IntPtr callbackArg, IntPtr pErrMsg);
}

ただ単にDELETEを実行するだけなので最小限の処理にしている. 真面目にやるのであれば下記のような点も考慮するといい感じになる.

  • sqlite3_exec() の第5引数でエラーメッセージを受け取る
    • sqlite3_free() での解放処理が必要
  • SQLite3のハンドルは SafeHandle の継承クラスで扱う
  • 戻り値の enum を定義する
  • まず sqlite3.dll のロードを試みて,見つからなければ winsqlite3.dll のロードを試みる
    • ユーザにdllを用意してもらうことで使用できるようにする想定
    • x86を考慮して呼び出し規約の切り分け
      • winsqlite3.dll なら stdcall
      • sqlite3.dll なら cdecl
  • SQLite3 のライブラリが見つからない場合はメニューに追加しない
  • Reimport対象のアセットパス直書きではなくGUID経由で取得する
    • unitypackage/VPMの両対応のため

頂点シェーダー→フラグメントシェーダーの受け渡しメンバを定義する

メンバの追加は LIL_CUSTOM_V2F_MEMBER を,値の設定処理は LIL_CUSTOM_VERT_COPY を利用する.

  • custom.hlsl
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float emissionWavePos : TEXCOORD ## id0;

// Add vertex copy
#define LIL_CUSTOM_VERT_COPY \
    LIL_V2F_OUT.emissionWavePos = pickupPosition(getEmissionPos(input.positionOS)) \
        + (2.0 * rand(float2((float)input.vertexID, LIL_TIME)) - 1.0) * _EmissionWaveNoiseAmp;

#define BEFORE_BLEND_EMISSION \
    const float uDiff = frac(LIL_TIME * _EmissionWaveTimeScale + _EmissionWaveTimePhase) - remap01(_EmissionPosMin, _EmissionPosMax, input.emissionWavePos); \
    const float sDiff = 2.0 * uDiff - 1.0; \
    const float eFact = pow(0.5 * cos(clamp(sDiff * _EmissionWaveParam.x, -1.0, 1.0) * UNITY_PI) + 0.5, _EmissionWaveParam.y); \
    fd.emissionColor += _EmissionWaveColor * eFact;

LIL_CUSTOM_V2F_MEMBER の引数はTEXCOORDのIDとなるため,## を用いて字句結合を行う. 頂点シェーダー内での出力構造体変数は LIL_V2F_OUT を指定,フラグメントシェーダー内での入力構造体変数は input を指定する.

LIL_CUSTOM_V2F_MEMBER の展開箇所は例えば Assets/lilToon/Shader/Includes/lil_pass_forward_normal.hlsl を参照するとよい. 頂点シェーダーは例えば Assets/lilToon/Shader/Includes/lil_common_vert.hlsl 等を, フラグメントシェーダーは Assets/lilToon/Shader/Includes/lil_pass_forward_normal.hlsl 等を参照するとよい.

lilToon 1.4.0 でのバグとワークアラウンド

本体側で TEXCOORD のIDと重複するIDが LIL_CUSTOM_V2F_MEMBERid0 として渡されているため,コンパイルエラーとなるパスが存在する. ShadowCasterパスでは id0 の使用を避けるようにする. 面倒であればすべてのパスで id0 の使用を避けても良いと思う.

  • NG
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float customMember01 : TEXCOORD ## id0; \
    float4 customMember02 : TEXCOORD ## id1; \
    float3 customMember03 : TEXCOORD ## id2;
  • OK
#ifdef UNITY_PASS_SHADOWCASTER
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float emissionWavePos : TEXCOORD ## id1; \
    float4 customMember02 : TEXCOORD ## id2; \
    float customMember03 : TEXCOORD ## id3;
#else
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float emissionWavePos : TEXCOORD ## id0; \
    float4 customMember02 : TEXCOORD ## id1; \
    float customMember03 : TEXCOORD ## id2;
#endif

Ver.1.4.1で直った

本体側のバージョンに応じてカスタムシェーダーの条件コンパイルを行う

前述の特定のバージョン向けのワークアラウンド等で本体のバージョンに応じてカスタムシェーダーの処理を変更したいことがある. 本体のシェーダーのマクロ定義にはバージョンに関するものがないが,C#側には存在する. これを利用して,下記のようにしてバージョン値定義マクロを定義したファイルを生成する.

GuidShaderDir はカスタムシェーダーの Shaders ディレクトリのGUIDにすること.

using System.IO;
using UnityEditor;
using UnityEngine;
using lilToon;


namespace lilToon
{
    /// <summary>
    /// Startup method provider.
    /// </summary>
    internal static class Startup
    {
        /// <summary>
        /// GUID of shader directory.
        /// </summary>
        private const string GuidShaderDir = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";  // TODO

        /// <summary>
        /// A method called at Unity startup.
        /// </summary>
        [InitializeOnLoadMethod]
        private static void OnStartup()
        {
            AssetDatabase.importPackageCompleted += Startup_ImportPackageCompleted;
            UpdateVersionDefFile();
        }

        /// <summary>
        /// Update definition file of version value of lilToon, lil_current_version_value.hlsl
        /// </summary>
        private static void UpdateVersionDefFile()
        {
            var dstDirPath = AssetDatabase.GUIDToAssetPath(GuidShaderDir);
            var line = $"#define LIL_CURRENT_VERSION_VALUE {lilConstants.currentVersionValue}";
            var dstFilePath = Path.Combine(dstDirPath, "lil_current_version_value.hlsl");
            if (File.Exists(dstFilePath) && ReadFirstLine(dstFilePath) == line)
            {
                return;
            }

            using (var fs = new FileStream(dstFilePath, FileMode.Create, FileAccess.Write, FileShare.Read))
            using (var sw = new StreamWriter(fs))
            {
                sw.Write(line);
                sw.Write('\n');
            }

            Debug.Log($"Update {dstFilePath}");
        }

        /// <summary>
        /// Read first line of the specified file.
        /// </summary>
        /// <param name="filePath">File to read.</param>
        /// <returns>First line of <paramref name="filePath"/>.</returns>
        private static string ReadFirstLine(string filePath)
        {
            using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (var sr = new StreamReader(fs))
            {
                return sr.ReadLine();
            }
        }

        /// <summary>
        /// A callback method for <see cref="AssetDatabase.importPackageCompleted"/>.
        /// </summary>
        /// <param name="packageName">Imported package name.</param>
        private static void Startup_ImportPackageCompleted(string packageName)
        {
            if (!packageName.StartsWith("lilToon"))
            {
                return;
            }
            UpdateVersionDefFile();
        }
    }
}

上記のコードで生成されるのは lil_current_version_value.hlsl というファイルで,下記のようなたった1行のファイルである.

#define LIL_CURRENT_VERSION_VALUE 34

lil_current_version_value.hlslcustom.hlsl の先頭でインクルードする.

#include "lil_current_version_value.hlsl"

これを用いると前章の1.4.0向けのワークアラウンドは下記のように書き直せる.

#if LIL_CURRENT_VERSION_VALUE == 34 && defined(UNITY_PASS_SHADOWCASTER)
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float emissionWavePos : TEXCOORD ## id1; \
    float4 customMember02 : TEXCOORD ## id2; \
    float customMember03 : TEXCOORD ## id3;
#else
#define LIL_CUSTOM_V2F_MEMBER(id0,id1,id2,id3,id4,id5,id6,id7) \
    float emissionWavePos : TEXCOORD ## id0; \
    float4 customMember02 : TEXCOORD ## id1; \
    float customMember03 : TEXCOORD ## id2;
#endif

バージョンとバージョン値(上記の例だと34)の対応関係は以下の表の通り. これは Assets/lilToon/Editor/lilConstants.cs の変更履歴を見ればわかる.

lilToonのバージョン(currentVersionName バージョン値(currentVersionValue
1.3.0 26
1.3.1 27
1.3.2 28
1.3.3 29
1.3.4 30
1.3.5 31
1.3.6 32
1.3.7 33
1.4.0 34
1.4.1 35
1.5.0 36
1.5.1 37
1.6.0 38
1.6.1 39
1.7.0 40
1.7.1 41
1.7.2 42

シェーダーキーワードを使用する

下記5ファイルに #pragma multi_compile#pragma shader_feature_local を記述する. multi版以外ではキーワードがインスペクタの処理で削除されるため,記述しても意味がない.

  • ltsmulti.lilcontainer
  • ltsmulti_fur.lilcontainer
  • ltsmulti_gem.lilcontainer
  • ltsmulti_o.lilcontainer
  • ltsmulti_ref.lilcontainer
    HLSLINCLUDE
        #pragma shader_feature_local _ _TOGGLEPROP_ON
        #pragma shader_feature_local _ENUMKEYWORD_FOO _ENUMKEYWORD_BAR _ENUMKEYWORD_BAZ
        #include "custom.hlsl"
    ENDHLSL

multi版以外でもどうしてもシェーダーキーワードを使用する

lilToonの設計思想に真っ向から対立していると思うが....

まず,前述の5ファイルの代わりに下記ファイルにキーワードのpragmaを記述する.

  • lilCustomShaderInsert.lilblock
#pragma shader_feature_local _ _TOGGLEPROP_ON
#pragma shader_feature_local _ENUMKEYWORD_FOO _ENUMKEYWORD_BAR _ENUMKEYWORD_BAZ
#include "custom_insert.hlsl"

次にインスペクタのコードにて,OnGUI() をオーバーライドし,親クラスの OnGUI() を呼び出し後に,対象のマテリアルにキーワードを設定する処理を追加する. キーワードは DrawCustomProperties() で保存しておく.

/// <summary>
/// Keywords to preserve.
/// </summary>
private List<string> _shaderKeywords = new List<string>();

/// <summary>
/// Draw property items.
/// </summary>
/// <param name="me">The <see cref="MaterialEditor"/> that are calling this <see cref="OnGUI(MaterialEditor, MaterialProperty[])"/> (the 'owner').</param>
/// <param name="mps">Material properties of the current selected shader.</param>
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)
{
    base.OnGUI(materialEditor, props);

    foreach (var shaderKeyword in shaderKeywords)
    {
        material.EnableKeyword(keyword);
    }
    _shaderKeywords.Clear();
}

/// <summary>
/// Draw custom properties.
/// </summary>
/// <param name="material">Target material.</param>
protected override void DrawCustomProperties(Material material)
{
    // ...

    _shaderKeywords.Add($"_TOGGLEPROP_{(prop.floatValue >= 0.5f ? "ON" : "OFF")}");

    // ...
}

末検証であるが,lilToonMultiで使用されるキーワードを参考に,雑にやるなら下記のようにしてもよいと思う.

/// <summary>
/// Keywords to preserve.
/// </summary>
private List<string> _shaderKeywords = new List<string>();

/// <summary>
/// Draw property items.
/// </summary>
/// <param name="me">The <see cref="MaterialEditor"/> that are calling this <see cref="OnGUI(MaterialEditor, MaterialProperty[])"/> (the 'owner').</param>
/// <param name="mps">Material properties of the current selected shader.</param>
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)
{
    base.OnGUI(materialEditor, props);

    foreach (var shaderKeyword in shaderKeywords)
    {
        material.EnableKeyword(keyword);
    }
    _shaderKeywords.Clear();
}

/// <summary>
/// Draw custom properties.
/// </summary>
/// <param name="material">Target material.</param>
protected override void DrawCustomProperties(Material material)
{
    // ...

    // MUSE BE write the end of this method.
    foreach (var keyword in material.shaderKeywords)
    {
        if (!IsMultiKeyword(keyword))
        {
            _shaderKeywords.Add(keyword);
        }
    }
}

/// <summary>
/// Check keyword is used in lilToonMulti or not.
/// </summary>
/// <param name="keyword">Shader keyword.</param>
/// <returns>True if the keyword is used in lilToonMulti, otherwise false.</returns>
private static bool IsMultiKeyword(string keyword)
{
    switch (keyword)
    {
        case "ANTI_FLICKER":
        case "EFFECT_BUMP":
        case "EFFECT_HUE_VARIATION":
        case "ETC1_EXTERNAL_ALPHA":
        case "GEOM_TYPE_BRANCH":
        case "GEOM_TYPE_BRANCH_DETAIL":
        case "GEOM_TYPE_FROND":
        case "GEOM_TYPE_LEAF":
        case "GEOM_TYPE_MESH":
        case "PIXELSNAP_ON":
        case "UNITY_UI_ALPHACLIP":
        case "UNITY_UI_CLIP_RECT":
        case "_COLORADDSUBDIFF_ON":
        case "_COLORCOLOR_ON":
        case "_COLOROVERLAY_ON":
        case "_DETAIL_MULX2":
        case "_EMISSION":
        case "_FADING_ON":
        case "_GLOSSYREFLECTIONS_OFF":
        case "_MAPPING_6_FRAMES_LAYOUT":
        case "_METALLICGLOSSMAP":
        case "_NORMALMAP":
        case "_PARALLAXMAP":
        case "_REQUIRE_UV2":
        case "_SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A":
        case "_SPECGLOSSMAP":
        case "_SPECULARHIGHLIGHTS_OFF":
        case "_SUNDISK_HIGH_QUALITY":
        case "_SUNDISK_NONE":
        case "_SUNDISK_SIMPLE":
            return true;
        default:
            return false;
    }
}

AudioLinkの処理を書きたい

lilToonの本体側での _AudioTexture の宣言の有無はAudioLink機能が有効か無効であるかに依存する.

本体側のAudioLink機能が有効か無効であるかに左右されないようにするためには, custom_insert.hlsl 内で下記のように宣言すること.

  • custom_insert.hlsl
// _AudioTexture is declared in lil_common_input.hlsl.
#ifndef LIL_FEATURE_AUDIOLINK
TEXTURE2D_FLOAT(_AudioTexture);
float4 _AudioTexture_TexelSize;
#endif  // LIL_FEATURE_AUDIOLINK

uniform変数の宣言は custom.hlsl 内の LIL_CUSTOM_PROPERTIESLIL_CUSTOM_TEXTURES マクロで行うべきと思うかもしれないが, custom.hlsl の段階では LIL_FEATURE_AUDIOLINK マクロが定義されていないため不可能である.

VRChatのカメラ判定,ミラー判定のuniform変数

下記3つの変数はVRChat側から提供されているuniform変数であり,どのワールドでも使用することができる.

  • _VRChatCameraMode
  • _VRChatMirrorMode
  • _VRChatMirrorCameraPos

lilToon 1.7.2現在では本体側シェーダーで宣言されていないため,custom.hlslLIL_CUSTOM_PROPERTIES を用いて宣言して問題ない.

#define LIL_CUSTOM_PROPERTIES \
    float _VRChatCameraMode; \
    float _VRChatMirrorMode; \
    float3 _VRChatMirrorCameraPos;

もし本体側で宣言された場合はAudioLinkと同じ対応をとること. 本体のissueにミラー表示制御を実装してほしいという要望があるため,将来的に追加される可能性がある.

シェーダー側でmultiシェーダーかどうかを判定する

マクロ LIL_MULTI が定義されているかどうかを調べる. ただし,このマクロは custom.hlsl の段階では定義されておらず, custom_insert.hlsl の段階でないと使用できないことに注意.

#ifdef LIL_MULTI
// マルチシェーダー用の処理
#endif

非multiとmulti版である程度コードを共有する

非multi版でif文を用い,multi版で条件コンパイルを用いると,同じコードを2度書くことになる.

  • lilCustomShaderProperties.lilblock
        //----------------------------------------------------------------------------------------------------------------------
        // Custom Properties
        [Toggle] _ToggleProp ("Toggle Property", Int) = 0
        [KeywordEnum(Foo, Bar, Baz)] _KeywordEnumProp ("Keyword enum property", Int) = 0
  • custom.hlsl
#define LIL_CUSTOM_PROPERTIES \
    bool _ToggleProp; \
    int _KeywordEnumProp;
  • custom_insert.hlsl
float4 getColor()
{
#if !defined(LIL_MULTI)
    if (_ToggleProp) {
        return float4(1.0, 0.0, 0.0, 1.0);
    } else {
        return float4(0.0, 1.0, 0.0, 1.0);
    }
#elif defined(_TOGGLEPROP_ON)
    return float4(1.0, 0.0, 0.0, 1.0);
#else
    return float4(0.0, 1.0, 0.0, 1.0);
#endif
}

float selectElement(float3 v)
{
#if !defined(LIL_MULTI)
    if (_KeywordEnumProp == 0) {
        return v.x;
    } else if (_KeywordEnumProp == 1) {
        return v.y;
    } else {
        return v.z;
    }
#elif defined(_KEYWORDENUMPROP_FOO)
    return v.x;
#elif defined(_KEYWORDENUMPROP_BAR)
    return v.y;
#elif defined(_KEYWORDENUMPROP_BAZ)
    return v.z;
#endif
}

[Toggle][KeywordEnum] に対するuniform変数を用意し,if文で条件分岐を記述する. マルチシェーダー,すなわち LIL_MULTI が定義されている場合のみ,uniform変数をマクロによって定数に置換し, if文の条件分岐がコンパイル時に確定するようにし,プリプロセス段階ではなくコンパイル段階での不要な処理の除去をコンパイラに任せる.

なお, custom.hlsl の段階では LIL_MULTI が定義されていないので,マルチシェーダーのときはuniform変数を定義しない,ということは諦める.

  • custom_insert.hlsl
#ifdef LIL_MULTI
#    ifdef _TOGGLEPROP_ON
#        define _ToggleProp true
#    else
#        define _ToggleProp false
#    endif  // _TOGGLEPROP_ON
#    if defined(_KEYWORDENUMPROP_FOO)
#        define _KeywordEnumProp 0
#    elif defined(_KEYWORDENUMPROP_BAR)
#        define _KeywordEnumProp 1
#    elif defined(_KEYWORDENUMPROP_BAZ)
#        define _KeywordEnumProp 2
#    endif
#endif  // LIL_MULTI

float4 getColor()
{
    if (_ToggleProp) {
        return float4(1.0, 0.0, 0.0, 1.0);
    } else {
        return float4(0.0, 1.0, 0.0, 1.0);
    }
}

float selectElement(float3 v)
{
    if (_KeywordEnumProp == 0) {
        return v.x;
    } else if (_KeywordEnumProp == 1) {
        return v.y;
    } else {
        return v.z;
    }
}

SV_POSITION に NaN

頂点シェーダーの出力構造体で SV_POSITION に相当するメンバに NaN を代入することで,頂点に関連するポリゴンを消去するテクニック. フラグメントシェーダーに渡ってから discard するよりおそらくGPUにやさしい.

custom.hlsl で下記のようにする(VRChatのカメラに写らなくするシェーダー例).

#define LIL_CUSTOM_VERT_COPY
    if (_VRChatCameraMode != 0.0) { \
        LIL_INITIALIZE_STRUCT(v2f, LIL_V2F_OUT_BASE); \
        LIL_V2F_OUT_BASE.positionCS = 0.0 / 0.0; \
        return LIL_V2F_OUT; \
    }

LIL_INITIALIZE_STRUCT を入れておくことで,コンパイラの最適化処理により,頂点シェーダーの先頭あたりに上記のコードを記述したのと同一のコードが生成される. LIL_V2F_OUT_BASE.positionCSfloat4 であるが, 0.0 / 0.0 は全要素 0.0 / 0.0float4 に暗黙的に変換されるのを利用している. 後続の処理は不要なので,returnしておく.

NaNの警告回避

本体側で4008番の警告を無効化するpragma定義がされているので不要.

0.0 / 0.0 と書きたくないなら asfloat(0x7fc00000) と書いてもよい.

マテリアルエディタ

キーワードについて

マルチシェーダーでない限りキーワードは削除されるので注意.

マルチシェーダーかどうかの判定

lilToon.lilInspector に定義されている静的メンバ isMulti を参照する.

if (isMulti)
{
    material.EnableKeyword("_TOGGLEPROP_ON");
}

シェーダー名に Multi が含まれるかどうかで判定する手もある. 自前で定義したDrawer内では isMulti は参照できないため,シェーダー名で判断するしかない?

protected readonly string _keyword;

public override void OnGUI(Rect position, MaterialProperty prop, GUIContent label, MaterialEditor editor)
{
    var isOn = prop.floatValue >= 0.5f;
    var kw = string.IsNullOrEmpty(_keyword) ? prop.name.ToUpperInvariant() + "_ON" : _keyword;

    foreach (Material material in prop.targets.Where(material => material.shader.name.IndexOf("Multi", material.shader.name.LastIndexOf('/')) != -1))
    {
        if (isOn)
        {
            material.EnableKeyword(kw);
        }
        else
        {
            material.DisableKeyword(kw);
        }
    }
}

多言語対応

公式の作例の lilToonGeometryFX を参照.

多言語ファイルは下記のような1行目がヘッダ行(言語名),2行目以降がデータ行のTSVファイル. データ行の1列名はキーで,2列目以降が各言語に応じた文言である.

ファイル名は何でもよい(GUIDで参照するため). 作例に習うなら lang_custom.txt . GUIDは lang_custom.txt.meta を参照すること.

Language  English Japanese    Korean  Chinese Simplified  Chinese Traditional
sCustomGeometryAnimation    Geometry Animation  ジオメトリアニメーション    지오메트리 애니메이션 Geometry Animation  Geometry Animation
sCustomBase Base Setting    基本設定    기본 설정   基本设置    基本設置
sCustomVector   Vector  向き  방향  向量  向量
sCustomDelay    Delay   ディレイ    딜레이   延迟  延遲
sCustomSpeed    Speed   速度  속도  速度  速度
sCustomRandomize    Randomize   ランダム化 임의화   随机化   隨機化
sCustomNormal   Normal  法線  노멀  法线  法線
sCustomOffset   Offset  オフセット Offset  Offset  Offset
sCustomNormalMap    Normal Map  ノーマルマップ   노멀 맵  法线贴图    法線貼圖
sCustomStrength Strength    強度  강도  强度  強度
sCustomShrink   Shrink  縮小  축소  缩减  縮減
sCustomMotionNormal Motion Normal   モーション法線   모션 법선   运动法线    運動法線
sCustomShadingNormal    Shading Normal  シェーディング法線 셰이딩 법선    着色法线    著色法線
sCustomGenerateSide Generate Side   側面を生成 측면 생성   生成侧面    生成側面

C# 側では LoadCustomLanguage() メソッドでファイルを読み込み, GetLoc() メソッドでキーを指定してローカライズされた文言を取得する. もし,定義されていないキーであった場合.GetLoc()キー名をそのまま返す

protected override void LoadCustomProperties(MaterialProperty[] props, Material material)
{
    // ...

    LoadCustomLanguage("a5875813c34e16a49ae1c8e1a846ea75");

    // ...
}

protected override void DrawCustomProperties(Material material)
{
    // ...

    var label = GetLoc("sCustomGeometryAnimation");

    // ...
}

ToggleLeftによる折り畳みとキーワード

シェーダー側で [Toggle] を指定しているプロパティ(MaterialToggleDrawer)について,lilToon本体の折り畳みに合わせ,なおかつキーワードを定義したい場合の解決法. (当たり前のことではあるが,Drawerを定義して,そのDrawerを指定する方が良いとは思う.)

下記のように記載した場合, EditorGUI.ToggleLeft が使用されないため不恰好になる.

m_MaterialEditor.ShaderProperty(_toggleProp, "Label for toggle property");
  • UnityEditor.MaterialEditor.ShaderProperty
    • UnityEditor.MaterialEditor.ShaderPropertyInternal

しかし,UnityEditor.MaterialEditor.ShaderProperty() で行われている処理である MaterialProperty に設定されている Drawer を取得し,その DrawerOnGUI() を呼び出すのは, 使用されているクラス・メソッド類が外部からは private となっているため,リフレクションを活用する必要がある.

// 下記のusing必要
using System.Reflection;

// ...

/// <summary>
/// Enable or disable keyword of <see cref="MaterialProperty"/> which has MaterialToggleUIDrawer.
/// </summary>
/// <param name="shader">Target <see cref="Shader"/>.</param>
/// <param name="prop">Target <see cref="MaterialProperty"/>.</param>
private static void SetToggleKeyword(Shader shader, MaterialProperty prop)
{
    SetToggleKeyword(shader, prop, prop.floatValue >= 0.5f);
}

/// <summary>
/// Enable or disable keyword of <see cref="MaterialProperty"/> which has MaterialToggleUIDrawer.
/// </summary>
/// <param name="shader">Target <see cref="Shader"/>.</param>
/// <param name="prop">Target <see cref="MaterialProperty"/>.</param>
private static void SetToggleKeyword(Shader shader, MaterialProperty prop, bool isOn)
{
    // Get assembly from public class.
    var asm = Assembly.GetAssembly(typeof(UnityEditor.MaterialPropertyDrawer));

    // Get type of UnityEditor.MaterialPropertyHandler which is the internal class.
    var typeMph = asm.GetType("UnityEditor.MaterialPropertyHandler")
        ?? throw new InvalidOperationException("Type not found: UnityEditor.MaterialPropertyHandler");
    var miGetHandler = typeMph.GetMethod(
        "GetHandler",
        BindingFlags.NonPublic
            | BindingFlags.Static)
        ?? throw new InvalidOperationException("MethodInfo not found: UnityEditor.MaterialPropertyHandler.GetHandler");

    // Instance of UnityEditor.MaterialPropertyHandler.
    var handler = miGetHandler.Invoke(null, new object[]
    {
        shader,
        prop.name
    });

    var pi = typeMph.GetProperty(
        "propertyDrawer",
        BindingFlags.GetProperty
            | BindingFlags.Public
            | BindingFlags.Instance)
        ?? throw new InvalidOperationException("PropertyInfo not found: UnityEditor.MaterialPropertyHandler.propertyDrawer");
    var drawer = pi.GetValue(handler)
        ?? throw new InvalidOperationException("Field not found: UnityEditor.MaterialPropertyHandler.propertyDrawer");


    // Check if drawer is instance of UnityEditor.MaterialToggleUIDrawer or not.
    var typeMtd = asm.GetType("UnityEditor.MaterialToggleUIDrawer")
        ?? throw new InvalidOperationException("Type not found: UnityEditor.MaterialToggleUIDrawer");
    if (!drawer.IsSubClassOf(typeMtd))
    {
        throw new ArgumentException($"{nameof(prop)} is not instance of UnityEditor.MaterialToggleUIDrawer.");
    }

    var miSetKeyword = typeMtd.GetMethod(
        "SetKeyword",
        BindingFlags.NonPublic
            | BindingFlags.Instance)
        ?? throw new InvalidOperationException("MethodInfo not found: UnityEditor.MaterialToggleUIDrawer.SetKeyword");
    miSetKeyword.Invoke(drawer, new object[]
    {
        prop,
        isOn
    });
}

リフレクション結果のキャッシュを作るのであれば下記のようにするとよい(クリックで開く閉じる).

// 下記のusing必要
using System.Linq.Expression;
using System.Reflection;

// ...

/// <summary>
/// Cache of reflection result of following lambda.
/// </summary>
/// <remarks><seealso cref="CreateToggleKeywordDelegate"/></remarks>
private static Action<Shader, MaterialProperty, bool> _toggleKeyword;

// ...

/// <summary>
/// Enable or disable keyword of <see cref="MaterialProperty"/> which has MaterialToggleUIDrawer.
/// </summary>
/// <param name="shader">Target <see cref="Shader"/>.</param>
/// <param name="prop">Target <see cref="MaterialProperty"/>.</param>
private static void SetToggleKeyword(Shader shader, MaterialProperty prop)
{
    SetToggleKeyword(shader, prop, prop.floatValue >= 0.5f);
}

/// <summary>
/// Enable or disable keyword of <see cref="MaterialProperty"/> which has MaterialToggleUIDrawer.
/// </summary>
/// <param name="shader">Target <see cref="Shader"/>.</param>
/// <param name="prop">Target <see cref="MaterialProperty"/>.</param>
/// <param name="isOn">True to enable (define) keyword, false to disable (undefine) keyword.</param>
private static void SetToggleKeyword(Shader shader, MaterialProperty prop, bool isOn)
{
    try
    {
        (_toggleKeyword ?? (_toggleKeyword = CreateSetKeywordDelegate()))(shader, prop, isOn);
    }
    catch (Exception ex)
    {
        Debug.LogError(ex.ToString());
    }
}

/// <summary>
/// <para>Create delegate of reflection results about UnityEditor.MaterialToggleUIDrawer.</para>
/// <code>
/// (Shader shader, MaterialProperty prop, bool isOn) =>
/// {
///     MaterialPropertyHandler mph = UnityEditor.MaterialPropertyHandler.GetHandler(shader, prop.name);
///     if (mph is null)
///     {
///         throw new ArgumentException("Specified MaterialProperty does not have UnityEditor.MaterialPropertyHandler");
///     }
///     MaterialToggleUIDrawer mpud = mph.propertyDrawer as MaterialToggleUIDrawer;
///     if (mpud is null)
///     {
///         throw new ArgumentException("Specified MaterialProperty does not have UnityEditor.MaterialToggleUIDrawer");
///     }
///     mpud.SetKeyword(prop, isOn);
/// }
/// </code>
/// </summary>
private static Action<Shader, MaterialProperty, bool> CreateSetKeywordDelegate()
{
    // Get assembly from public class.
    var asm = Assembly.GetAssembly(typeof(UnityEditor.MaterialPropertyDrawer));

    // Get type of UnityEditor.MaterialPropertyHandler which is the internal class.
    var typeMph = asm.GetType("UnityEditor.MaterialPropertyHandler")
        ?? throw new InvalidOperationException("Type not found: UnityEditor.MaterialPropertyHandler");
    var typeMtud = asm.GetType("UnityEditor.MaterialToggleUIDrawer")
        ?? throw new InvalidOperationException("Type not found: UnityEditor.MaterialToggleUIDrawer");

    var ciArgumentException = typeof(ArgumentException).GetConstructor(new[] {typeof(string)});

    var pShader = Expression.Parameter(typeof(Shader), "shader");
    var pMaterialPropertyHandler = Expression.Parameter(typeMph, "mph");
    var pMaterialToggleUIDrawer = Expression.Parameter(typeMtud, "mtud");
    var pMaterialProperty = Expression.Parameter(typeof(MaterialProperty), "mp");
    var pBool = Expression.Parameter(typeof(bool), "isOn");

    var cNull = Expression.Constant(null);

    return Expression.Lambda<Action<Shader, MaterialProperty, bool>>(
        Expression.Block(
            new[]
            {
                pMaterialPropertyHandler,
                pMaterialToggleUIDrawer
            },
            Expression.Assign(
                pMaterialPropertyHandler,
                Expression.Call(
                    typeMph.GetMethod(
                        "GetHandler",
                        BindingFlags.NonPublic
                            | BindingFlags.Static)
                        ?? throw new InvalidOperationException("MethodInfo not found: UnityEditor.MaterialPropertyHandler.GetHandler"),
                    pShader,
                    Expression.Property(
                        pMaterialProperty,
                        typeof(MaterialProperty).GetProperty(
                            "name",
                            BindingFlags.GetProperty
                                | BindingFlags.Public
                                | BindingFlags.Instance)))),
            Expression.IfThen(
                Expression.Equal(
                    pMaterialPropertyHandler,
                    cNull),
                Expression.Throw(
                    Expression.New(
                        ciArgumentException,
                        Expression.Constant("Specified MaterialProperty does not have UnityEditor.MaterialPropertyHandler")))),
            Expression.Assign(
                pMaterialToggleUIDrawer,
                Expression.TypeAs(
                    Expression.Property(
                        pMaterialPropertyHandler,
                        typeMph.GetProperty(
                            "propertyDrawer",
                            BindingFlags.GetProperty
                                | BindingFlags.Public
                                | BindingFlags.Instance)
                            ?? throw new InvalidOperationException("PropertyInfo not found: UnityEditor.MaterialPropertyHandler.propertyDrawer")),
                    typeMtud)),
            Expression.IfThen(
                Expression.Equal(
                    pMaterialToggleUIDrawer,
                    cNull),
                Expression.Throw(
                    Expression.New(
                        ciArgumentException,
                        Expression.Constant("Specified MaterialProperty does not have UnityEditor.MaterialToggleUIDrawer")))),
            Expression.Call(
                pMaterialToggleUIDrawer,
                typeMtud.GetMethod(
                    "SetKeyword",
                    BindingFlags.NonPublic
                        | BindingFlags.Instance)
                    ?? throw new InvalidOperationException("MethodInfo not found: UnityEditor.MaterialToggleUIDrawer.SetKeyword"),
                pMaterialProperty,
                pBool)),
        "SetKeyword",
        new []
        {
            pShader,
            pMaterialProperty,
            pBool
        }).Compile();
}

上記の SetToggleKeyword() メソッドを利用して下記のように記述する.

protected override void DrawCustomProperties(Material material)
{
    // ...

    using (new EditorGUILayout.VerticalScope(boxOuter))
    {
        DrawToggleLeft(material, _toggleProp, GetLoc("sToggleProp"));
        if (_enableWorldPos.floatValue >= 0.5f)
        {
            // 関連するプロパティの描画
        }
    }

    // ...
}

/// <summary>
/// Draw ToggleLeft property.
/// </summary>
/// <param name="material">Target <see cref="Material"/>.</param>
/// <param name="prop">Target <see cref="MaterialProperty"/>.</param>
/// <param name="label">Label for this toggle button.</param>
private static void DrawToggleLeft(Material material, MaterialProperty prop, string label)
{
    using (var ccScope = new EditorGUI.ChangeCheckScope())
    {
        EditorGUI.showMixedValue = prop.hasMixedValue;
        var isChecked = EditorGUI.ToggleLeft(
            EditorGUILayout.GetControlRect(),
            label,
            prop.floatValue >= 0.5f,
            customToggleFont);
        EditorGUI.showMixedValue = false;
        if (ccScope.changed)
        {
            prop.floatValue = isChecked ? 1.0f : 0.0f;
            if (isMulti)
            {
                SetToggleKeyword(material.shader, prop);
            }
        }
    }
}

オリジナルのlilToonからの移行を簡単にする

テンプレートのインスペクタのコード末尾のコメントアウト部分を解除すると,マテリアルの右クリックメニューが追加される. 名前は適切に置きかえること.

[MenuItem("Assets/TemplateFull/Convert material to custom shader", false, 1100)]
private static void ConvertMaterialToCustomShaderMenu()
{
    if(Selection.objects.Length == 0) return;
    TemplateFullInspector inspector = new TemplateFullInspector();
    for(int i = 0; i < Selection.objects.Length; i++)
    {
        if(Selection.objects[i] is Material)
        {
            inspector.ConvertMaterialToCustomShader((Material)Selection.objects[i]);
        }
    }
}

ただし,上記コードはCtrl-Zが考慮されていない,C# のコードとしてイマイチ,inspector.ConvertMaterialToCustomShader の処理が大袈裟である(lilToon本体とカスタムシェーダーの全てのバリエーションについて Shader.Find() を呼び出す)ので,下記のようにするのがオススメである.

/// <summary>
/// Try to replace the shader of the selected material to custom lilToon shader.
/// </summary>
[MenuItem("Assets/TemplateFull/Convert material to custom shader", false, 1100)]
private static void ConvertMaterialToCustomShaderMenu()
{
    foreach (var obj in Selection.objects)
    {
        var material = obj as Material;
        if (material == null)
        {
            continue;
        }

        var shader = GetCorrespondingCustomShader(material.shader);
        if (shader == null)
        {
            Debug.LogWarningFormat($"Ignore {0}. \"{1}\" is not original lilToon shader.", AssetDatabase.GetAssetPath(material), material.shader.name);
            continue;
        }

        Undo.RecordObject(material, "TemplateFull/ConvertMaterialToCustomShaderMenu");

        var renderQueue = lilMaterialUtils.GetTrueRenderQueue(material);
        material.shader = shader;
        material.renderQueue = renderQueue;
    }
}

/// <summary>
/// Get a custom lilToon shader which is corresponding to specified original lilToon shader.
/// </summary>
/// <param name="originalShader">Original lilToon shader.</param>
/// <returns>null if no custom lilToon shader is found, otherwise the one found.</returns>
private static Shader GetCorrespondingCustomShader(Shader originalShader)
{
    var customShaderName = GetCorrespondingCustomShaderName(originalShader.name);
    return customShaderName == null ? null : Shader.Find(customShaderName);
}

/// <summary>
/// Get a custom lilToon shader name which is corresponding to specified original lilToon shader name.
/// </summary>
/// <param name="originalShaderName">Original lilToon shader name.</param>
/// <returns>null if no custom lilToon shader name is found, otherwise the one found.</returns>
private static string GetCorrespondingCustomShaderName(string originalShaderName)
{
    switch (originalShaderName)
    {
        case "lilToon": return shaderName + "/lilToon";
        case "Hidden/lilToonCutout": return "Hidden/" + shaderName + "/Cutout";
        case "Hidden/lilToonTransparent": return "Hidden/" + shaderName + "/Transparent";
        case "Hidden/lilToonOnePassTransparent": return "Hidden/" + shaderName + "/OnePassTransparent";
        case "Hidden/lilToonTwoPassTransparent": return "Hidden/" + shaderName + "/TwoPassTransparent";
        case "Hidden/lilToonOutline": return "Hidden/" + shaderName + "/OpaqueOutline";
        case "Hidden/lilToonCutoutOutline": return "Hidden/" + shaderName + "/CutoutOutline";
        case "Hidden/lilToonTransparentOutline": return "Hidden/" + shaderName + "/TransparentOutline";
        case "Hidden/lilToonOnePassTransparentOutline": return "Hidden/" + shaderName + "/OnePassTransparentOutline";
        case "Hidden/lilToonTwoPassTransparentOutline": return "Hidden/" + shaderName + "/TwoPassTransparentOutline";
        case "_lil/[Optional] lilToonOutlineOnly": return shaderName + "/[Optional] OutlineOnly/Opaque";
        case "_lil/[Optional] lilToonCutoutOutlineOnly": return shaderName + "/[Optional] OutlineOnly/Cutout";
        case "_lil/[Optional] lilToonTransparentOutlineOnly": return shaderName + "/[Optional] OutlineOnly/Transparent";
        case "Hidden/lilToonTessellation": return "Hidden/" + shaderName + "/Tessellation/Opaque";
        case "Hidden/lilToonTessellationCutout": return "Hidden/" + shaderName + "/Tessellation/Cutout";
        case "Hidden/lilToonTessellationTransparent": return "Hidden/" + shaderName + "/Tessellation/Transparent";
        case "Hidden/lilToonTessellationOnePassTransparent": return "Hidden/" + shaderName + "/Tessellation/OnePassTransparent";
        case "Hidden/lilToonTessellationTwoPassTransparent": return "Hidden/" + shaderName + "/Tessellation/TwoPassTransparent";
        case "Hidden/lilToonTessellationOutline": return "Hidden/" + shaderName + "/Tessellation/OpaqueOutline";
        case "Hidden/lilToonTessellationCutoutOutline": return "Hidden/" + shaderName + "/Tessellation/CutoutOutline";
        case "Hidden/lilToonTessellationTransparentOutline": return "Hidden/" + shaderName + "/Tessellation/TransparentOutline";
        case "Hidden/lilToonTessellationOnePassTransparentOutline": return "Hidden/" + shaderName + "/Tessellation/OnePassTransparentOutline";
        case "Hidden/lilToonTessellationTwoPassTransparentOutline": return "Hidden/" + shaderName + "/Tessellation/TwoPassTransparentOutline";
        case "Hidden/lilToonLite": return shaderName + "/lilToonLite";
        case "Hidden/lilToonLiteCutout": return "Hidden/" + shaderName + "/Lite/Cutout";
        case "Hidden/lilToonLiteTransparent": return "Hidden/" + shaderName + "/Lite/Transparent";
        case "Hidden/lilToonLiteOnePassTransparent": return "Hidden/" + shaderName + "/Lite/OnePassTransparent";
        case "Hidden/lilToonLiteTwoPassTransparent": return "Hidden/" + shaderName + "/Lite/TwoPassTransparent";
        case "Hidden/lilToonLiteOutline": return "Hidden/" + shaderName + "/Lite/OpaqueOutline";
        case "Hidden/lilToonLiteCutoutOutline": return "Hidden/" + shaderName + "/Lite/CutoutOutline";
        case "Hidden/lilToonLiteTransparentOutline": return "Hidden/" + shaderName + "/Lite/TransparentOutline";
        case "Hidden/lilToonLiteOnePassTransparentOutline": return "Hidden/" + shaderName + "/Lite/OnePassTransparentOutline";
        case "Hidden/lilToonLiteTwoPassTransparentOutline": return "Hidden/" + shaderName + "/Lite/TwoPassTransparentOutline";
        case "Hidden/lilToonRefraction": return "Hidden/" + shaderName + "/Refraction";
        case "Hidden/lilToonRefractionBlur": return "Hidden/" + shaderName + "/RefractionBlur";
        case "Hidden/lilToonFur": return "Hidden/" + shaderName + "/Fur";
        case "Hidden/lilToonFurCutout": return "Hidden/" + shaderName + "/FurCutout";
        case "Hidden/lilToonFurTwoPass": return "Hidden/" + shaderName + "/FurTwoPass";
        case "_lil/[Optional] lilToonFurOnly": return shaderName + "/[Optional] FurOnly/Transparent";
        case "_lil/[Optional] lilToonFurOnlyCutout": return shaderName + "/[Optional] FurOnly/Cutout";
        case "_lil/[Optional] lilToonFurOnlyTwoPass": return shaderName + "/[Optional] FurOnly/TwoPass";
        case "Hidden/lilToonGem": return "Hidden/" + shaderName + "/Gem";
        case "_lil/lilToonFakeShadow": return shaderName + "/[Optional] FakeShadow";
        case "_lil/[Optional] lilToonOverlay": return shaderName + "/[Optional] Overlay";
        case "_lil/[Optional] lilToonOverlayOnePass": return shaderName + "/[Optional] OverlayOnePass";
        case "_lil/[Optional] lilToonLiteOverlay": return shaderName + "/[Optional] LiteOverlay";
        case "_lil/[Optional] lilToonLiteOverlayOnePass": return shaderName + "/[Optional] LiteOverlayOnePass";
        case "_lil/lilToonMulti": return shaderName + "/lilToonMulti";
        case "Hidden/lilToonMultiOutline": return "Hidden/" + shaderName + "/MultiOutline";
        case "Hidden/lilToonMultiRefraction": return "Hidden/" + shaderName + "/MultiRefraction";
        case "Hidden/lilToonMultiFur": return "Hidden/" + shaderName + "/MultiFur";
        case "Hidden/lilToonMultiGem": return "Hidden/" + shaderName + "/MultiGem";
        default: return null;
    }
}

テンプレートに従っているならば, shaderName はクラス内で下記のように宣言されているはずであり,これを用いるようにしている.

private const string shaderName = "TemplateFull";

カスタムシェーダーからオリジナルのlilToonへ簡単に戻せるようにする

前述のものと逆の動作を行うメソッドを用意し,右クリックメニューとして登録する.

/// <summary>
/// Try to replace the shader of the material to original lilToon shader.
/// </summary>
[MenuItem("Assets/TemplateFull/Convert material to original shader", false, 1101)]
private static void ConvertMaterialToOriginalShaderMenu()
{
    foreach (var obj in Selection.objects)
    {
        var material = obj as Material;
        if (material == null)
        {
            continue;
        }

        var shader = GetCorrespondingOriginalShader(material.shader);
        if (shader == null)
        {
            Debug.LogWarningFormat($"Ignore {0}. \"{1}\" is not custom lilToon shader, \"" + shaderName + "\".", AssetDatabase.GetAssetPath(material), material.shader.name);
            continue;
        }

        Undo.RecordObject(material, "TemplateFull/ConvertMaterialToOriginalShaderMenu");

        var renderQueue = lilMaterialUtils.GetTrueRenderQueue(material);
        material.shader = shader;
        material.renderQueue = renderQueue;
    }
}

/// <summary>
/// Get a original lilToon shader which is corresponding to specified custom lilToon shader.
/// </summary>
/// <param name="customShader">Custom lilToon shader.</param>
/// <returns>null if no original lilToon shader is found, otherwise the one found.</returns>
private static Shader GetCorrespondingOriginalShader(Shader customShader)
{
    var customShaderName = GetCorrespondingOriginalShaderName(customShader.name);
    return customShaderName == null ? null : Shader.Find(customShaderName);
}

/// <summary>
/// Get a original lilToon shader name which is corresponding to specified custom lilToon shader name.
/// </summary>
/// <param name="customShaderName">Custom lilToon shader name.</param>
/// <returns>null if no original lilToon shader name is found, otherwise the one found.</returns>
private static string GetCorrespondingOriginalShaderName(string customShaderName)
{
    switch (customShaderName)
    {
        case shaderName + "/lilToon": return "lilToon";
        case "Hidden/" + shaderName + "/Cutout": return "Hidden/lilToonCutout";
        case "Hidden/" + shaderName + "/Transparent": return "Hidden/lilToonTransparent";
        case "Hidden/" + shaderName + "/OnePassTransparent": return "Hidden/lilToonOnePassTransparent";
        case "Hidden/" + shaderName + "/TwoPassTransparent": return "Hidden/lilToonTwoPassTransparent";
        case "Hidden/" + shaderName + "/OpaqueOutline": return "Hidden/lilToonOutline";
        case "Hidden/" + shaderName + "/CutoutOutline": return "Hidden/lilToonCutoutOutline";
        case "Hidden/" + shaderName + "/TransparentOutline": return "Hidden/lilToonTransparentOutline";
        case "Hidden/" + shaderName + "/OnePassTransparentOutline": return "Hidden/lilToonOnePassTransparentOutline";
        case "Hidden/" + shaderName + "/TwoPassTransparentOutline": return "Hidden/lilToonTwoPassTransparentOutline";
        case shaderName + "/[Optional] OutlineOnly/Opaque": return "_lil/[Optional] lilToonOutlineOnly";
        case shaderName + "/[Optional] OutlineOnly/Cutout": return "_lil/[Optional] lilToonCutoutOutlineOnly";
        case shaderName + "/[Optional] OutlineOnly/Transparent": return "_lil/[Optional] lilToonTransparentOutlineOnly";
        case "Hidden/" + shaderName + "/Tessellation/Opaque": return "Hidden/lilToonTessellation";
        case "Hidden/" + shaderName + "/Tessellation/Cutout": return "Hidden/lilToonTessellationCutout";
        case "Hidden/" + shaderName + "/Tessellation/Transparent": return "Hidden/lilToonTessellationTransparent";
        case "Hidden/" + shaderName + "/Tessellation/OnePassTransparent": return "Hidden/lilToonTessellationOnePassTransparent";
        case "Hidden/" + shaderName + "/Tessellation/TwoPassTransparent": return "Hidden/lilToonTessellationTwoPassTransparent";
        case "Hidden/" + shaderName + "/Tessellation/OpaqueOutline": return "Hidden/lilToonTessellationOutline";
        case "Hidden/" + shaderName + "/Tessellation/CutoutOutline": return "Hidden/lilToonTessellationCutoutOutline";
        case "Hidden/" + shaderName + "/Tessellation/TransparentOutline": return "Hidden/lilToonTessellationTransparentOutline";
        case "Hidden/" + shaderName + "/Tessellation/OnePassTransparentOutline": return "Hidden/lilToonTessellationOnePassTransparentOutline";
        case "Hidden/" + shaderName + "/Tessellation/TwoPassTransparentOutline": return "Hidden/lilToonTessellationTwoPassTransparentOutline";
        case shaderName + "/lilToonLite": return "Hidden/lilToonLite";
        case "Hidden/" + shaderName + "/Lite/Cutout": return "Hidden/lilToonLiteCutout";
        case "Hidden/" + shaderName + "/Lite/Transparent": return "Hidden/lilToonLiteTransparent";
        case "Hidden/" + shaderName + "/Lite/OnePassTransparent": return "Hidden/lilToonLiteOnePassTransparent";
        case "Hidden/" + shaderName + "/Lite/TwoPassTransparent": return "Hidden/lilToonLiteTwoPassTransparent";
        case "Hidden/" + shaderName + "/Lite/OpaqueOutline": return "Hidden/lilToonLiteOutline";
        case "Hidden/" + shaderName + "/Lite/CutoutOutline": return "Hidden/lilToonLiteCutoutOutline";
        case "Hidden/" + shaderName + "/Lite/TransparentOutline": return "Hidden/lilToonLiteTransparentOutline";
        case "Hidden/" + shaderName + "/Lite/OnePassTransparentOutline": return "Hidden/lilToonLiteOnePassTransparentOutline";
        case "Hidden/" + shaderName + "/Lite/TwoPassTransparentOutline": return "Hidden/lilToonLiteTwoPassTransparentOutline";
        case "Hidden/" + shaderName + "/Refraction": return "Hidden/lilToonRefraction";
        case "Hidden/" + shaderName + "/RefractionBlur": return "Hidden/lilToonRefractionBlur";
        case "Hidden/" + shaderName + "/Fur": return "Hidden/lilToonFur";
        case "Hidden/" + shaderName + "/FurCutout": return "Hidden/lilToonFurCutout";
        case "Hidden/" + shaderName + "/FurTwoPass": return "Hidden/lilToonFurTwoPass";
        case shaderName + "/[Optional] FurOnly/Transparent": return "_lil/[Optional] lilToonFurOnly";
        case shaderName + "/[Optional] FurOnly/Cutout": return "_lil/[Optional] lilToonFurOnlyCutout";
        case shaderName + "/[Optional] FurOnly/TwoPass": return "_lil/[Optional] lilToonFurOnlyTwoPass";
        case "Hidden/" + shaderName + "/Gem": return "Hidden/lilToonGem";
        case shaderName + "/[Optional] FakeShadow": return "_lil/lilToonFakeShadow";
        case shaderName + "/[Optional] Overlay": return "_lil/[Optional] lilToonOverlay";
        case shaderName + "/[Optional] OverlayOnePass": return "_lil/[Optional] lilToonOverlayOnePass";
        case shaderName + "/[Optional] LiteOverlay": return "_lil/[Optional] lilToonLiteOverlay";
        case shaderName + "/[Optional] LiteOverlayOnePass": return "_lil/[Optional] lilToonLiteOverlayOnePass";
        case shaderName + "/lilToonMulti": return "_lil/lilToonMulti";
        case "Hidden/" + shaderName + "/MultiOutline": return "Hidden/lilToonMultiOutline";
        case "Hidden/" + shaderName + "/MultiRefraction": return "Hidden/lilToonMultiRefraction";
        case "Hidden/" + shaderName + "/MultiFur": return "Hidden/lilToonMultiFur";
        case "Hidden/" + shaderName + "/MultiGem": return "Hidden/lilToonMultiGem";
        default: return null;
    }
}

shaderNameconst string であるため,文字列リテラルとの結合結果もまたコンパイル時定数となり,caseのラベルとして使用できる.

その他

本体のソースコードリーディングを楽にする

Vim等のctagsに対応しているエディタを使っている人向け. lilToon本体のソースコードGitHubからクローンし,タグファイルを作成しておくと,本体の関数やマクロへの定義ジャンプが可能になり,色々と楽になる.

以下は ~/github/lilToon に本体のソースコードをcloneしたものとしたタグファイル(フルパスのタグファイル)の生成方法である.

  • シェーダーファイル向け
$ ctags -f shaderlab.lilToon.tags --languages=c --langmap=c:+.shader,c:+.hlsl -R ~/lilToon/Assets/lilToon/Shader/
$ ctags -f cs.lilToon.tags --languages=c# -R ~/lilToon/Assets/lilToon/

上記のタグファイルを ~/.vim/tagfiles/ に配置し,~/.vimrc に下記のように記述するとよい.

augroup MyCtags
  autocmd!
  autocmd FileType hlsl,shaderlab setlocal tags+=~/.vim/tagfiles/shaderlab.lilToon.tags
  autocmd FileType cs setlocal tags+=~/.vim/tagfiles/win32unix/cs.lilToon.tags
augroup END

デフォルトだとファイルタイプがhlsl,shaderlabの判定がされないため,下記2ファイルを用意する必要がある. 別途プラグインを導入して判別できている場合はしなくてよい.

  • ~/.vim/ftdetect/hlsl.vim
au BufNewFile,BufRead *.hlsl setfiletype shaderlab
  • ~/.vim/ftdetect/shaderlab.vim
au BufNewFile,BufRead *.shaderlab setfiletype shaderlab

記事内容全部入りのインスペクタテンプレート

記事内容+自分好みに修正したインスペクタテンプレート.

長いので折り畳み(クリックで開く閉じる).

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using System.Runtime.InteropServices;

namespace lilToon
{
    /// <summary>
    /// <see cref="ShaderGUI"/> for the custom shader variations of lilToon.
    /// </summary>
    public class TemplateFullInspector : lilToonInspector
    {
        // Custom properties
        //private MaterialProperty customVariable;

        /// <summary>
        /// A flag whether to fold custom properties or not.
        /// </summary>
        private static bool isShowCustomProperties;

        /// <summary>
        /// Name of this custom shader.
        /// </summary>
        private const string shaderName = "TemplateFull";

        /// <summary>
        /// Load custom language file and make cache of shader properties.
        /// </summary>
        /// <param name="props">Properties of the material.</param>
        /// <param name="material">Target material.</param>
        protected override void LoadCustomProperties(MaterialProperty[] props, Material material)
        {
            isCustomShader = true;

            // If you want to change rendering modes in the editor, specify the shader here
            ReplaceToCustomShaders();
            isShowRenderMode = !material.shader.name.Contains(shaderName + "/[Optional] ");

            // If not, set isShowRenderMode to false
            //isShowRenderMode = false;

            //LoadCustomLanguage("");
            //customVariable = FindProperty("_CustomVariable", props);
        }

        /// <summary>
        /// Draw custom properties.
        /// </summary>
        /// <param name="material">Target material.</param>
        protected override void DrawCustomProperties(Material material)
        {
            // GUIStyles Name   Description
            // ---------------- ------------------------------------
            // boxOuter         outer box
            // boxInnerHalf     inner box
            // boxInner         inner box without label
            // customBox        box (similar to unity default box)
            // customToggleFont label for box

            isShowCustomProperties = Foldout("Custom Properties", "Custom Properties", isShowCustomProperties);
            if (!isShowCustomProperties)
            {
                return;
            }

            using (new EditorGUILayout.VerticalScope(boxOuter))
            {
                EditorGUILayout.LabelField(GetLoc("Custom Properties"), customToggleFont);
                using (new EditorGUILayout.VerticalScope(boxInnerHalf))
                {
                    //m_MaterialEditor.ShaderProperty(customVariable, "Custom Variable");
                }
            }
        }

        /// <summary>
        /// Replace shaders to custom shaders.
        /// </summary>
        protected override void ReplaceToCustomShaders()
        {
            lts         = Shader.Find(shaderName + "/lilToon");
            ltsc        = Shader.Find("Hidden/" + shaderName + "/Cutout");
            ltst        = Shader.Find("Hidden/" + shaderName + "/Transparent");
            ltsot       = Shader.Find("Hidden/" + shaderName + "/OnePassTransparent");
            ltstt       = Shader.Find("Hidden/" + shaderName + "/TwoPassTransparent");

            ltso        = Shader.Find("Hidden/" + shaderName + "/OpaqueOutline");
            ltsco       = Shader.Find("Hidden/" + shaderName + "/CutoutOutline");
            ltsto       = Shader.Find("Hidden/" + shaderName + "/TransparentOutline");
            ltsoto      = Shader.Find("Hidden/" + shaderName + "/OnePassTransparentOutline");
            ltstto      = Shader.Find("Hidden/" + shaderName + "/TwoPassTransparentOutline");

            ltsoo       = Shader.Find(shaderName + "/[Optional] OutlineOnly/Opaque");
            ltscoo      = Shader.Find(shaderName + "/[Optional] OutlineOnly/Cutout");
            ltstoo      = Shader.Find(shaderName + "/[Optional] OutlineOnly/Transparent");

            ltstess     = Shader.Find("Hidden/" + shaderName + "/Tessellation/Opaque");
            ltstessc    = Shader.Find("Hidden/" + shaderName + "/Tessellation/Cutout");
            ltstesst    = Shader.Find("Hidden/" + shaderName + "/Tessellation/Transparent");
            ltstessot   = Shader.Find("Hidden/" + shaderName + "/Tessellation/OnePassTransparent");
            ltstesstt   = Shader.Find("Hidden/" + shaderName + "/Tessellation/TwoPassTransparent");

            ltstesso    = Shader.Find("Hidden/" + shaderName + "/Tessellation/OpaqueOutline");
            ltstessco   = Shader.Find("Hidden/" + shaderName + "/Tessellation/CutoutOutline");
            ltstessto   = Shader.Find("Hidden/" + shaderName + "/Tessellation/TransparentOutline");
            ltstessoto  = Shader.Find("Hidden/" + shaderName + "/Tessellation/OnePassTransparentOutline");
            ltstesstto  = Shader.Find("Hidden/" + shaderName + "/Tessellation/TwoPassTransparentOutline");

            ltsl        = Shader.Find(shaderName + "/lilToonLite");
            ltslc       = Shader.Find("Hidden/" + shaderName + "/Lite/Cutout");
            ltslt       = Shader.Find("Hidden/" + shaderName + "/Lite/Transparent");
            ltslot      = Shader.Find("Hidden/" + shaderName + "/Lite/OnePassTransparent");
            ltsltt      = Shader.Find("Hidden/" + shaderName + "/Lite/TwoPassTransparent");

            ltslo       = Shader.Find("Hidden/" + shaderName + "/Lite/OpaqueOutline");
            ltslco      = Shader.Find("Hidden/" + shaderName + "/Lite/CutoutOutline");
            ltslto      = Shader.Find("Hidden/" + shaderName + "/Lite/TransparentOutline");
            ltsloto     = Shader.Find("Hidden/" + shaderName + "/Lite/OnePassTransparentOutline");
            ltsltto     = Shader.Find("Hidden/" + shaderName + "/Lite/TwoPassTransparentOutline");

            ltsref      = Shader.Find("Hidden/" + shaderName + "/Refraction");
            ltsrefb     = Shader.Find("Hidden/" + shaderName + "/RefractionBlur");
            ltsfur      = Shader.Find("Hidden/" + shaderName + "/Fur");
            ltsfurc     = Shader.Find("Hidden/" + shaderName + "/FurCutout");
            ltsfurtwo   = Shader.Find("Hidden/" + shaderName + "/FurTwoPass");
            ltsfuro     = Shader.Find(shaderName + "/[Optional] FurOnly/Transparent");
            ltsfuroc    = Shader.Find(shaderName + "/[Optional] FurOnly/Cutout");
            ltsfurotwo  = Shader.Find(shaderName + "/[Optional] FurOnly/TwoPass");
            ltsgem      = Shader.Find("Hidden/" + shaderName + "/Gem");
            ltsfs       = Shader.Find(shaderName + "/[Optional] FakeShadow");

            ltsover     = Shader.Find(shaderName + "/[Optional] Overlay");
            ltsoover    = Shader.Find(shaderName + "/[Optional] OverlayOnePass");
            ltslover    = Shader.Find(shaderName + "/[Optional] LiteOverlay");
            ltsloover   = Shader.Find(shaderName + "/[Optional] LiteOverlayOnePass");

            ltsm        = Shader.Find(shaderName + "/lilToonMulti");
            ltsmo       = Shader.Find("Hidden/" + shaderName + "/MultiOutline");
            ltsmref     = Shader.Find("Hidden/" + shaderName + "/MultiRefraction");
            ltsmfur     = Shader.Find("Hidden/" + shaderName + "/MultiFur");
            ltsmgem     = Shader.Find("Hidden/" + shaderName + "/MultiGem");
        }


        /// <summary>
        /// Try to replace the shader of the selected material to custom lilToon shader.
        /// </summary>
        [MenuItem("Assets/" + shaderName + "/Convert material to custom shader", false, 1100)]
        private static void ConvertMaterialToCustomShaderMenu()
        {
            foreach (var obj in Selection.objects)
            {
                var material = obj as Material;
                if (material == null)
                {
                    continue;
                }

                var shader = GetCorrespondingCustomShader(material.shader);
                if (shader == null)
                {
                    Debug.LogWarningFormat($"Ignore {0}. \"{1}\" is not original lilToon shader.", AssetDatabase.GetAssetPath(material), material.shader.name);
                    continue;
                }

                Undo.RecordObject(material, "TemplateFull/ConvertMaterialToCustomShaderMenu");

                var renderQueue = lilMaterialUtils.GetTrueRenderQueue(material);
                material.shader = shader;
                material.renderQueue = renderQueue;
            }
        }

        /// <summary>
        /// Menu validation method for <see cref="ConvertMaterialToCustomShaderMenu"/>.
        /// </summary>
        /// <returns>True if <see cref="ConvertMaterialToCustomShaderMenu"/> works, otherwise false.</returns>
        [MenuItem("Assets/" + shaderName + "/Convert material to custom shader", true)]
        private static bool ValidateConvertMaterialToCustomShaderMenu()
        {
            var count = 0;
            foreach (var obj in Selection.objects)
            {
                var material = obj as Material;
                if (material == null)
                {
                    continue;
                }

                if (GetCorrespondingCustomShaderName(material.shader.name) != null)
                {
                    count++;
                }
            }
            return count > 0;
        }

        /// <summary>
        /// Try to replace the shader of the material to original lilToon shader.
        /// </summary>
        [MenuItem("Assets/" + shaderName + "/Convert material to original shader", false, 1101)]
        private static void ConvertMaterialToOriginalShaderMenu()
        {
            foreach (var obj in Selection.objects)
            {
                var material = obj as Material;
                if (material == null)
                {
                    continue;
                }

                var shader = GetCorrespondingOriginalShader(material.shader);
                if (shader == null)
                {
                    Debug.LogWarningFormat($"Ignore {0}. \"{1}\" is not custom lilToon shader, \"" + shaderName + "\".", AssetDatabase.GetAssetPath(material), material.shader.name);
                    continue;
                }

                Undo.RecordObject(material, "TemplateFull/ConvertMaterialToOriginalShaderMenu");

                var renderQueue = lilMaterialUtils.GetTrueRenderQueue(material);
                material.shader = shader;
                material.renderQueue = renderQueue;
            }
        }

        /// <summary>
        /// Menu validation method for <see cref="ValidateConvertMaterialToOriginalShaderMenu"/>.
        /// </summary>
        /// <returns>True if <see cref="ValidateConvertMaterialToOriginalShaderMenu"/> works, otherwise false.</returns>
        [MenuItem("Assets/" + shaderName + "/Convert material to original shader", true)]
        private static bool ValidateConvertMaterialToOriginalShader(string customShaderCommonName)
        {
            var count = 0;
            foreach (var obj in Selection.objects)
            {
                var material = obj as Material;
                if (material == null)
                {
                    continue;
                }

                if (GetCorrespondingOriginalShaderName(material.shader.name, customShaderCommonName) != null)
                {
                    count++;
                }
            }
            return count > 0;
        }

        /// <summary>
        /// Callback method for menu item which refreshes shader cache and reimport.
        /// </summary>
        [MenuItem("Assets/" + shaderName + "/Refresh shader cache", false, 2000)]
        private static void RefreshShaderCacheMenu()
        {
            var result = NativeMethods.Open("Library/ShaderCache.db", out var dbHandle);
            if (result != 0)
            {
                Debug.LogError($"Failed to open Library/ShaderCache.db [{result}]");
                return;
            }

            try
            {
                result = NativeMethods.Execute(dbHandle, "DELETE FROM shadererrors", IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
                if (result != 0)
                {
                    Debug.LogError($"SQL failed [{result}]");
                    return;
                }
            }
            finally
            {
                result = NativeMethods.Close(dbHandle);
                if (result != 0)
                {
                    Debug.LogError($"Failed to close database [{result}]");
                }
            }

            AssetDatabase.ImportAsset("Assets/TemplateFull/Shaders", ImportAssetOptions.ImportRecursive);
        }

        /// <summary>
        /// Menu validation method for <see cref="RefreshShaderCacheMenu"/>.
        /// </summary>
        /// <returns>True if <see cref="RefreshShaderCacheMenu"/> works, otherwise false.</returns>
        [MenuItem("Assets/" + shaderName + "Refresh shader cache", true)]
        private static bool ValidateRefreshShaderCacheMenu()
        {
            try
            {
                NativeMethods.Close(IntPtr.Zero);
                return true;
            }
            catch (DllNotFoundException)
            {
                return false;
            }
        }

        /// <summary>
        /// Get a custom lilToon shader which is corresponding to specified original lilToon shader.
        /// </summary>
        /// <param name="originalShader">Original lilToon shader.</param>
        /// <returns>null if no custom lilToon shader is found, otherwise the one found.</returns>
        private static Shader GetCorrespondingCustomShader(Shader originalShader)
        {
            var customShaderName = GetCorrespondingCustomShaderName(originalShader.name);
            return customShaderName == null ? null : Shader.Find(customShaderName);
        }

        /// <summary>
        /// Get a custom lilToon shader name which is corresponding to specified original lilToon shader name.
        /// </summary>
        /// <param name="originalShaderName">Original lilToon shader name.</param>
        /// <returns>null if no custom lilToon shader name is found, otherwise the one found.</returns>
        private static string GetCorrespondingCustomShaderName(string originalShaderName)
        {
            switch (originalShaderName)
            {
                case "lilToon": return shaderName + "/lilToon";
                case "Hidden/lilToonCutout": return "Hidden/" + shaderName + "/Cutout";
                case "Hidden/lilToonTransparent": return "Hidden/" + shaderName + "/Transparent";
                case "Hidden/lilToonOnePassTransparent": return "Hidden/" + shaderName + "/OnePassTransparent";
                case "Hidden/lilToonTwoPassTransparent": return "Hidden/" + shaderName + "/TwoPassTransparent";
                case "Hidden/lilToonOutline": return "Hidden/" + shaderName + "/OpaqueOutline";
                case "Hidden/lilToonCutoutOutline": return "Hidden/" + shaderName + "/CutoutOutline";
                case "Hidden/lilToonTransparentOutline": return "Hidden/" + shaderName + "/TransparentOutline";
                case "Hidden/lilToonOnePassTransparentOutline": return "Hidden/" + shaderName + "/OnePassTransparentOutline";
                case "Hidden/lilToonTwoPassTransparentOutline": return "Hidden/" + shaderName + "/TwoPassTransparentOutline";
                case "_lil/[Optional] lilToonOutlineOnly": return shaderName + "/[Optional] OutlineOnly/Opaque";
                case "_lil/[Optional] lilToonCutoutOutlineOnly": return shaderName + "/[Optional] OutlineOnly/Cutout";
                case "_lil/[Optional] lilToonTransparentOutlineOnly": return shaderName + "/[Optional] OutlineOnly/Transparent";
                case "Hidden/lilToonTessellation": return "Hidden/" + shaderName + "/Tessellation/Opaque";
                case "Hidden/lilToonTessellationCutout": return "Hidden/" + shaderName + "/Tessellation/Cutout";
                case "Hidden/lilToonTessellationTransparent": return "Hidden/" + shaderName + "/Tessellation/Transparent";
                case "Hidden/lilToonTessellationOnePassTransparent": return "Hidden/" + shaderName + "/Tessellation/OnePassTransparent";
                case "Hidden/lilToonTessellationTwoPassTransparent": return "Hidden/" + shaderName + "/Tessellation/TwoPassTransparent";
                case "Hidden/lilToonTessellationOutline": return "Hidden/" + shaderName + "/Tessellation/OpaqueOutline";
                case "Hidden/lilToonTessellationCutoutOutline": return "Hidden/" + shaderName + "/Tessellation/CutoutOutline";
                case "Hidden/lilToonTessellationTransparentOutline": return "Hidden/" + shaderName + "/Tessellation/TransparentOutline";
                case "Hidden/lilToonTessellationOnePassTransparentOutline": return "Hidden/" + shaderName + "/Tessellation/OnePassTransparentOutline";
                case "Hidden/lilToonTessellationTwoPassTransparentOutline": return "Hidden/" + shaderName + "/Tessellation/TwoPassTransparentOutline";
                case "Hidden/lilToonLite": return shaderName + "/lilToonLite";
                case "Hidden/lilToonLiteCutout": return "Hidden/" + shaderName + "/Lite/Cutout";
                case "Hidden/lilToonLiteTransparent": return "Hidden/" + shaderName + "/Lite/Transparent";
                case "Hidden/lilToonLiteOnePassTransparent": return "Hidden/" + shaderName + "/Lite/OnePassTransparent";
                case "Hidden/lilToonLiteTwoPassTransparent": return "Hidden/" + shaderName + "/Lite/TwoPassTransparent";
                case "Hidden/lilToonLiteOutline": return "Hidden/" + shaderName + "/Lite/OpaqueOutline";
                case "Hidden/lilToonLiteCutoutOutline": return "Hidden/" + shaderName + "/Lite/CutoutOutline";
                case "Hidden/lilToonLiteTransparentOutline": return "Hidden/" + shaderName + "/Lite/TransparentOutline";
                case "Hidden/lilToonLiteOnePassTransparentOutline": return "Hidden/" + shaderName + "/Lite/OnePassTransparentOutline";
                case "Hidden/lilToonLiteTwoPassTransparentOutline": return "Hidden/" + shaderName + "/Lite/TwoPassTransparentOutline";
                case "Hidden/lilToonRefraction": return "Hidden/" + shaderName + "/Refraction";
                case "Hidden/lilToonRefractionBlur": return "Hidden/" + shaderName + "/RefractionBlur";
                case "Hidden/lilToonFur": return "Hidden/" + shaderName + "/Fur";
                case "Hidden/lilToonFurCutout": return "Hidden/" + shaderName + "/FurCutout";
                case "Hidden/lilToonFurTwoPass": return "Hidden/" + shaderName + "/FurTwoPass";
                case "_lil/[Optional] lilToonFurOnly": return shaderName + "/[Optional] FurOnly/Transparent";
                case "_lil/[Optional] lilToonFurOnlyCutout": return shaderName + "/[Optional] FurOnly/Cutout";
                case "_lil/[Optional] lilToonFurOnlyTwoPass": return shaderName + "/[Optional] FurOnly/TwoPass";
                case "Hidden/lilToonGem": return "Hidden/" + shaderName + "/Gem";
                case "_lil/lilToonFakeShadow": return shaderName + "/[Optional] FakeShadow";
                case "_lil/[Optional] lilToonOverlay": return shaderName + "/[Optional] Overlay";
                case "_lil/[Optional] lilToonOverlayOnePass": return shaderName + "/[Optional] OverlayOnePass";
                case "_lil/[Optional] lilToonLiteOverlay": return shaderName + "/[Optional] LiteOverlay";
                case "_lil/[Optional] lilToonLiteOverlayOnePass": return shaderName + "/[Optional] LiteOverlayOnePass";
                case "_lil/lilToonMulti": return shaderName + "/lilToonMulti";
                case "Hidden/lilToonMultiOutline": return "Hidden/" + shaderName + "/MultiOutline";
                case "Hidden/lilToonMultiRefraction": return "Hidden/" + shaderName + "/MultiRefraction";
                case "Hidden/lilToonMultiFur": return "Hidden/" + shaderName + "/MultiFur";
                case "Hidden/lilToonMultiGem": return "Hidden/" + shaderName + "/MultiGem";
                default: return null;
            }
        }

        /// <summary>
        /// Get a original lilToon shader which is corresponding to specified custom lilToon shader.
        /// </summary>
        /// <param name="customShader">Custom lilToon shader.</param>
        /// <returns>null if no original lilToon shader is found, otherwise the one found.</returns>
        private static Shader GetCorrespondingOriginalShader(Shader customShader)
        {
            var customShaderName = GetCorrespondingOriginalShaderName(customShader.name);
            return customShaderName == null ? null : Shader.Find(customShaderName);
        }

        /// <summary>
        /// Get a original lilToon shader name which is corresponding to specified custom lilToon shader name.
        /// </summary>
        /// <param name="customShaderName">Custom lilToon shader name.</param>
        /// <returns>null if no original lilToon shader name is found, otherwise the one found.</returns>
        private static string GetCorrespondingOriginalShaderName(string customShaderName)
        {
            switch (customShaderName)
            {
                case shaderName + "/lilToon": return "lilToon";
                case "Hidden/" + shaderName + "/Cutout": return "Hidden/lilToonCutout";
                case "Hidden/" + shaderName + "/Transparent": return "Hidden/lilToonTransparent";
                case "Hidden/" + shaderName + "/OnePassTransparent": return "Hidden/lilToonOnePassTransparent";
                case "Hidden/" + shaderName + "/TwoPassTransparent": return "Hidden/lilToonTwoPassTransparent";
                case "Hidden/" + shaderName + "/OpaqueOutline": return "Hidden/lilToonOutline";
                case "Hidden/" + shaderName + "/CutoutOutline": return "Hidden/lilToonCutoutOutline";
                case "Hidden/" + shaderName + "/TransparentOutline": return "Hidden/lilToonTransparentOutline";
                case "Hidden/" + shaderName + "/OnePassTransparentOutline": return "Hidden/lilToonOnePassTransparentOutline";
                case "Hidden/" + shaderName + "/TwoPassTransparentOutline": return "Hidden/lilToonTwoPassTransparentOutline";
                case shaderName + "/[Optional] OutlineOnly/Opaque": return "_lil/[Optional] lilToonOutlineOnly";
                case shaderName + "/[Optional] OutlineOnly/Cutout": return "_lil/[Optional] lilToonCutoutOutlineOnly";
                case shaderName + "/[Optional] OutlineOnly/Transparent": return "_lil/[Optional] lilToonTransparentOutlineOnly";
                case "Hidden/" + shaderName + "/Tessellation/Opaque": return "Hidden/lilToonTessellation";
                case "Hidden/" + shaderName + "/Tessellation/Cutout": return "Hidden/lilToonTessellationCutout";
                case "Hidden/" + shaderName + "/Tessellation/Transparent": return "Hidden/lilToonTessellationTransparent";
                case "Hidden/" + shaderName + "/Tessellation/OnePassTransparent": return "Hidden/lilToonTessellationOnePassTransparent";
                case "Hidden/" + shaderName + "/Tessellation/TwoPassTransparent": return "Hidden/lilToonTessellationTwoPassTransparent";
                case "Hidden/" + shaderName + "/Tessellation/OpaqueOutline": return "Hidden/lilToonTessellationOutline";
                case "Hidden/" + shaderName + "/Tessellation/CutoutOutline": return "Hidden/lilToonTessellationCutoutOutline";
                case "Hidden/" + shaderName + "/Tessellation/TransparentOutline": return "Hidden/lilToonTessellationTransparentOutline";
                case "Hidden/" + shaderName + "/Tessellation/OnePassTransparentOutline": return "Hidden/lilToonTessellationOnePassTransparentOutline";
                case "Hidden/" + shaderName + "/Tessellation/TwoPassTransparentOutline": return "Hidden/lilToonTessellationTwoPassTransparentOutline";
                case shaderName + "/lilToonLite": return "Hidden/lilToonLite";
                case "Hidden/" + shaderName + "/Lite/Cutout": return "Hidden/lilToonLiteCutout";
                case "Hidden/" + shaderName + "/Lite/Transparent": return "Hidden/lilToonLiteTransparent";
                case "Hidden/" + shaderName + "/Lite/OnePassTransparent": return "Hidden/lilToonLiteOnePassTransparent";
                case "Hidden/" + shaderName + "/Lite/TwoPassTransparent": return "Hidden/lilToonLiteTwoPassTransparent";
                case "Hidden/" + shaderName + "/Lite/OpaqueOutline": return "Hidden/lilToonLiteOutline";
                case "Hidden/" + shaderName + "/Lite/CutoutOutline": return "Hidden/lilToonLiteCutoutOutline";
                case "Hidden/" + shaderName + "/Lite/TransparentOutline": return "Hidden/lilToonLiteTransparentOutline";
                case "Hidden/" + shaderName + "/Lite/OnePassTransparentOutline": return "Hidden/lilToonLiteOnePassTransparentOutline";
                case "Hidden/" + shaderName + "/Lite/TwoPassTransparentOutline": return "Hidden/lilToonLiteTwoPassTransparentOutline";
                case "Hidden/" + shaderName + "/Refraction": return "Hidden/lilToonRefraction";
                case "Hidden/" + shaderName + "/RefractionBlur": return "Hidden/lilToonRefractionBlur";
                case "Hidden/" + shaderName + "/Fur": return "Hidden/lilToonFur";
                case "Hidden/" + shaderName + "/FurCutout": return "Hidden/lilToonFurCutout";
                case "Hidden/" + shaderName + "/FurTwoPass": return "Hidden/lilToonFurTwoPass";
                case shaderName + "/[Optional] FurOnly/Transparent": return "_lil/[Optional] lilToonFurOnly";
                case shaderName + "/[Optional] FurOnly/Cutout": return "_lil/[Optional] lilToonFurOnlyCutout";
                case shaderName + "/[Optional] FurOnly/TwoPass": return "_lil/[Optional] lilToonFurOnlyTwoPass";
                case "Hidden/" + shaderName + "/Gem": return "Hidden/lilToonGem";
                case shaderName + "/[Optional] FakeShadow": return "_lil/lilToonFakeShadow";
                case shaderName + "/[Optional] Overlay": return "_lil/[Optional] lilToonOverlay";
                case shaderName + "/[Optional] OverlayOnePass": return "_lil/[Optional] lilToonOverlayOnePass";
                case shaderName + "/[Optional] LiteOverlay": return "_lil/[Optional] lilToonLiteOverlay";
                case shaderName + "/[Optional] LiteOverlayOnePass": return "_lil/[Optional] lilToonLiteOverlayOnePass";
                case shaderName + "/lilToonMulti": return "_lil/lilToonMulti";
                case "Hidden/" + shaderName + "/MultiOutline": return "Hidden/lilToonMultiOutline";
                case "Hidden/" + shaderName + "/MultiRefraction": return "Hidden/lilToonMultiRefraction";
                case "Hidden/" + shaderName + "/MultiFur": return "Hidden/lilToonMultiFur";
                case "Hidden/" + shaderName + "/MultiGem": return "Hidden/lilToonMultiGem";
                default: return null;
            }
        }


        /// <summary>
        /// Provides some native methods of SQLite3.
        /// </summary>
        internal static class NativeMethods
        {
#if UNITY_EDITOR && !UNITY_EDITOR_WIN
            /// <summary>
            /// Native library name of SQLite3.
            /// </summary>
            private const string LibraryName = "sqlite3";
            /// <summary>
            /// Calling convention of library functions.
            /// </summary>
            private const CallingConvention CallConv = CallingConvention.Cdecl;
#else
            /// <summary>
            /// Native library name of SQLite3.
            /// </summary>
            private const string LibraryName = "winsqlite3";
            /// <summary>
            /// Calling convention of library functions.
            /// </summary>
            private const CallingConvention CallConv = CallingConvention.StdCall;
#endif
            /// <summary>
            /// Open database.
            /// </summary>
            /// <param name="filePath">SQLite3 database file path.</param>
            /// <param name="db">SQLite db handle.</param>
            /// <returns>Result code.</returns>
            /// <remarks>
            /// <seealso href="https://www.sqlite.org/c3ref/open.html"/>
            /// </remarks>
            [DllImport(LibraryName, EntryPoint = "sqlite3_open", CallingConvention = CallConv)]
            public static extern int Open(string filename, out IntPtr dbHandle);

            /// <summary>
            /// Close database.
            /// </summary>
            /// <param name="filePath">Database filename.</param>
            /// <param name="db">SQLite db handle.</param>
            /// <returns>Result code.</returns>
            /// <remarks>
            /// <seealso href="https://www.sqlite.org/c3ref/close.html"/>
            /// </remarks>
            [DllImport(LibraryName, EntryPoint = "sqlite3_close", CallingConvention = CallConv)]
            public static extern int Close(IntPtr db);

            /// <summary>
            /// Execute specified SQL.
            /// </summary>
            /// <param name="db">An open database.</param>
            /// <param name="sql">SQL to be evaluated.</param>
            /// <param name="callback">Callback function.</param>
            /// <param name="callbackArg">1st argument to callback.</param>
            /// <param name="pErrMsg">Error msg written here.</param>
            /// <returns>Result code.</returns>
            /// <remarks>
            /// <seealso href="https://www.sqlite.org/c3ref/exec.html"/>
            /// </remarks>
            [DllImport(LibraryName, EntryPoint = "sqlite3_exec", CallingConvention = CallConv)]
            public static extern int Execute(IntPtr dbHandle, string sql, IntPtr callback, IntPtr callbackArg, IntPtr pErrMsg);
        }
    }
}
#endif

作例

レイマーチングにおけるマーチングループのbreakについて

shaderlabでレイマーチングのシェーダーを書き,マーチングループの部分のアセンブリコードを確認して不満に思う点があった. 本記事ではマーチングその不満に思う点を解消する手法について述べる.

扱うシェーダーコード

この記事では下記のシェーダーコードを題材として取り上げる.

レイマーチングによる単純なスフィアトレーシングのshaderlabのコードであり,ForwardBaseパスのみサポートしている. ライティングは単純なLambert反射のみの計算であり,鏡面反射や環境光の計算は除外している. 出力アセンブリコードの増加を防ぐため,#pragma multi_compile_fog#pragma multi_compile_fwdbase は宣言していない.

Shader "koturn/RayMarching/LoopBreak"
{
    Properties
    {
        [IntRange]
        _MaxLoop ("Maximum loop count", Range(8, 1024)) = 128

        _MinRayLength ("Minimum length of the ray", Float) = 0.01
        _MaxRayLength ("Maximum length of the ray", Float) = 1000.0

        _MarchingFactor ("Marching Factor", Range(0.5, 1.0)) = 1.0

        _Color ("Color of the objects", Color) = (1.0, 1.0, 1.0, 1.0)

        [KeywordEnum(Post If Break, Post Flatten If Break, Pre If Break, Pre Flatten If Break, Use Loop Continuous, Use Index Update, Post Update Index)]
        _BreakMethod ("Break method of the marching loop", Int) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue" = "AlphaTest"
            "RenderType" = "Transparent"
            "DisableBatching" = "True"
            "IgnoreProjector" = "True"
            "VRCFallback" = "Hidden"
        }

        Cull Front

        CGINCLUDE
        #pragma target 3.0

        // keywords:
        //   FOG_LINEAR
        //   FOG_EXP
        //   FOG_EXP2
        // #pragma multi_compile_fog
        #pragma multi_compile_local_fragment _BREAKMETHOD_POST_IF_BREAK _BREAKMETHOD_POST_FLATTEN_IF_BREAK _BREAKMETHOD_PRE_IF_BREAK _BREAKMETHOD_PRE_FLATTEN_IF_BREAK _BREAKMETHOD_USE_LOOP_CONTINUOUS _BREAKMETHOD_USE_INDEX_UPDATE _BREAKMETHOD_POST_UPDATE_INDEX

        #include "UnityCG.cginc"
        #include "UnityStandardUtils.cginc"
        #include "AutoLight.cginc"


        /*!
         * @brief Input of the vertex shader, vert().
         */
        struct appdata
        {
            //! Local position of the vertex.
            float4 vertex : POSITION;
            //! Lightmap coordinate.
            float2 texcoord1 : TEXCOORD1;
        };

        /*!
         * @brief Output of the vertex shader, frag()
         * and input of fragment shader.
         */
        struct v2f
        {
            //! Clip space position of the vertex.
            float4 pos : SV_POSITION;
            //! Ray origin in object space (Camera position in object space).
            nointerpolation float3 localRayOrigin : TEXCOORD0;
            //! Unnormalized ray direction in object space.
            float3 localRayDirVector : TEXCOORD1;
            //! Lighting and shadowing parameters.
            UNITY_LIGHTING_COORDS(2, 3)
        };

        /*!
         * @brief Output of fragment shader.
         */
        struct fout
        {
            //! Output color of the pixel.
            half4 color : SV_Target;
            //! Depth of the pixel.
            float depth : SV_Depth;
        };


        float map(float3 p);
        float sdSphere(float3 p, float r);
        half4 calcLighting(half4 color, float3 worldPos, float3 worldNormal, half atten);
        float3 getNormal(float3 p);
        float getDepth(float4 projPos);


        #ifndef UNITY_LIGHTING_COMMON_INCLUDED
        fixed4 _LightColor0;
        #endif  // UNITY_LIGHTING_COMMON_INCLUDED

        //! Color of the objects.
        uniform half4 _Color;
        //! Maximum loop count.
        uniform int _MaxLoop;
        //! Minimum length of the ray.
        uniform float _MinRayLength;
        //! Maximum length of the ray.
        uniform float _MaxRayLength;
        //! Marching Factor.
        uniform float _MarchingFactor;


        /*!
         * @brief Vertex shader function for ForwardBase and ForwardAdd Pass.
         * @param [in] v  Input data
         * @return Output for fragment shader (v2f).
         */
        v2f vert(appdata v)
        {
            v2f o;
            UNITY_INITIALIZE_OUTPUT(v2f, o);

            o.pos = UnityObjectToClipPos(v.vertex);
            o.localRayOrigin = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1.0)).xyz;
            o.localRayDirVector = v.vertex - o.localRayOrigin;

            UNITY_TRANSFER_LIGHTING(o, v.texcoord1);

            return o;
        }

        /*!
         * @brief Fragment shader function.
         * @param [in] fi  Input data from vertex shader.
         * @return Output of each texels (fout).
         */
        fout frag(v2f fi)
        {
            const float3 ro = fi.localRayOrigin;
            const float3 rd = normalize(fi.localRayDirVector);

            float t = 0.0;

        #if defined(_BREAKMETHOD_POST_IF_BREAK)
            for (int i = 0; i < _MaxLoop; i++) {
                const float d = map(ro + rd * t);
                t += d * _MarchingFactor;

                if (d < _MinRayLength || t > _MaxRayLength) {
                    break;
                }
            }
        #elif defined(_BREAKMETHOD_POST_FLATTEN_IF_BREAK)
            for (int i = 0; i < _MaxLoop; i++) {
                const float d = map(ro + rd * t);
                t += d * _MarchingFactor;

                UNITY_FLATTEN
                if (d < _MinRayLength || t > _MaxRayLength) {
                    break;
                }
            }
        #elif defined(_BREAKMETHOD_PRE_IF_BREAK)
            float d = _MaxRayLength;
            for (int i = 0; i < _MaxLoop; i++) {
                if (d < _MinRayLength || t > _MaxRayLength) {
                    break;
                }

                d = map(ro + rd * t);
                t += d * _MarchingFactor;
            }
        #elif defined(_BREAKMETHOD_PRE_FLATTEN_IF_BREAK)
            float d = _MaxRayLength;
            for (int i = 0; i < _MaxLoop; i++) {
                UNITY_FLATTEN
                if (d < _MinRayLength || t > _MaxRayLength) {
                    break;
                }

                d = map(ro + rd * t);
                t += d * _MarchingFactor;
            }
        #elif defined(_BREAKMETHOD_USE_LOOP_CONTINUOUS)
            float d = _MaxRayLength;
            for (int i = 0; i < _MaxLoop && d >= _MinRayLength && t <= _MaxRayLength; i++) {
                d = map(ro + rd * t);
                t += d * _MarchingFactor;
            }
        #elif defined(_BREAKMETHOD_USE_INDEX_UPDATE)
            float d = _MaxRayLength;
            for (int i = 0; i < _MaxLoop; i = (d < _MinRayLength || t > _MaxRayLength) ? 0x7fffffff : i + 1) {
                d = map(ro + rd * t);
                t += d * _MarchingFactor;
            }
        #else  // defined(_BREAKMETHOD_POST_UPDATE_INDEX)
            for (int i = 0; i < _MaxLoop; i++) {
                const float d = map(ro + rd * t);
                t += d * _MarchingFactor;

                UNITY_FLATTEN
                if (d < _MinRayLength || t > _MaxRayLength) {
                    i = 0x7ffffffe;
                }
            }
        #endif
            clip(_MaxRayLength - t);

            const float3 localFinalPos = ro + rd * t;
            const float3 worldFinalPos = mul(unity_ObjectToWorld, float4(localFinalPos, 1.0).xyz);

            UNITY_LIGHT_ATTENUATION(atten, fi, worldFinalPos);

            half4 color = calcLighting(
                _Color,
                worldFinalPos,
                UnityObjectToWorldNormal(getNormal(localFinalPos)),
                atten);

            const float4 projPos = UnityWorldToClipPos(worldFinalPos);

            UNITY_APPLY_FOG(projPos.z, color);

            fout fo;
            UNITY_INITIALIZE_OUTPUT(fout, fo);
            fo.color = color;
            fo.depth = getDepth(projPos);

            return fo;
        }

        /*!
         * @brief SDF (Signed Distance Function) of objects.
         * @param [in] p  Position of the tip of the ray.
         * @return Signed Distance to the nearest object.
         */
        float map(float3 p)
        {
            return sdSphere(p, 0.5);
        }

        /*!
         * @brief SDF of Sphere.
         * @param [in] p  Position of the tip of the ray.
         * @param [in] r  Radius of sphere.
         * @return Signed Distance to the Sphere.
         */
        float sdSphere(float3 p, float r)
        {
            return length(p) - r;
        }

        /*!
         * Calculate lighting.
         * @param [in] color  Base color.
         * @param [in] worldPos  World coordinate.
         * @param [in] worldNormal  Normal in world space.
         * @param [in] atten  Light attenuation.
         * @return Color with lighting applied.
         */
        half4 calcLighting(half4 color, float3 worldPos, float3 worldNormal, half atten)
        {
            const float3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
        #ifdef USING_DIRECTIONAL_LIGHT
            const float3 worldLightDir = UnityWorldSpaceLightDir(worldPos);
        #else
            const float3 worldLightDir = normalize(UnityWorldSpaceLightDir(worldPos));
        #endif  // USING_DIRECTIONAL_LIGHT
            const fixed3 lightCol = _LightColor0.rgb * atten;

            // Lambertian reflectance.
            const float nDotL = dot(worldNormal, worldLightDir);
            const half3 diffuse = lightCol * pow(nDotL * 0.5 + 0.5, 2.0);

            const half4 outColor = half4(diffuse * _Color.rgb, _Color.a);

            return outColor;
        }

        /*!
         * @brief Calculate normal of the objects.
         *
         * @param [in] p  Position of the tip of the ray.
         * @return Normal of the objects.
         * @see https://iquilezles.org/articles/normalsSDF/
         */
        float3 getNormal(float3 p)
        {
            static const float h = 0.0001;
            static const float2 s = float2(1.0, -1.0);  // used only for generating k.
            static const float3 k[4] = {s.xyy, s.yxy, s.yyx, s.xxx};

            float3 normal = float3(0.0, 0.0, 0.0);

            UNITY_LOOP
            for (int i = 0; i < 4; i++) {
                normal += k[i] * map(p + h * k[i]);
            }

            return normalize(normal);
        }

        /*!
         * @brief Get depth from projected position.
         * @param [in] projPos  Projected position.
         * @return Depth value.
         */
        float getDepth(float4 projPos)
        {
            const float depth = projPos.z / projPos.w;
        #if defined(SHADER_API_GLCORE) \
            || defined(SHADER_API_OPENGL) \
            || defined(SHADER_API_GLES) \
            || defined(SHADER_API_GLES3)
            return depth * 0.5 + 0.5;
        #else
            return depth;
        #endif
        }
        ENDCG

        Pass
        {
            Name "FORWARD_BASE"
            Tags
            {
                "LightMode" = "ForwardBase"
            }

            Blend Off
            ZTest LEqual

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // keywords:
            //   DIRECTIONAL
            //   LIGHTMAP_ON
            //   DIRLIGHTMAP_COMBINED
            //   DYNAMICLIGHTMAP_ON
            //   LIGHTMAP_SHADOW_MIXING
            //   VERTEXLIGHT_ON
            //   LIGHTPROBE_SH
            // #pragma multi_compile_fwdbase
            ENDCG
        }  // ForwardBase
    }
}

目次

末尾if-break

まず最も典型的なマーチングループのシェーダーコードを示す. レイの衝突判定とレイの長さの上限の判定をループ末尾で行うコードと得られるDirect3D11のアセンブリコードである.

float t = 0.0;
for (int i = 0; i < _MaxLoop; i++) {
    const float d = map(ro + rd * t);
    t += d * _MarchingFactor;

    if (d < _MinRayLength || t > _MaxRayLength) {
        break;
    }
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_POST_IF_BREAK
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 6 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r0.w, l(0)
   4: mov r1.x, l(0)
   5: loop
   6:   ige r1.y, r1.x, cb0[4].x
   7:   breakc_nz r1.y
   8:   mad r1.yzw, r0.xxyz, r0.wwww, v1.xxyz
   9:   dp3 r1.y, r1.yzwy, r1.yzwy
  10:   sqrt r1.y, r1.y
  11:   add r1.y, r1.y, l(-0.500000)
  12:   mad r1.z, r1.y, cb0[4].w, r0.w
  13:   lt r1.y, r1.y, cb0[4].y
  14:   lt r1.w, cb0[4].z, r1.z
  15:   or r1.y, r1.w, r1.y
  16:   if_nz r1.y
  17:     mov r0.w, r1.z
  18:     break
  19:   endif
  20:   iadd r1.x, r1.x, l(1)
  21:   mov r0.w, r1.z
  22: endloop
  23: add r1.x, -r0.w, cb0[4].z
  24: lt r1.x, r1.x, l(0.000000)
  25: discard_nz r1.x

このアセンブリコードを見て思った問題点は下記の2つ.

  1. 16行目と21行目に同じ処理がある
    • 15行目と16行目の前で行ってもよいのでは?(元々のシェーダーコードがそうしているように)
  2. if ~ break ではなく,breakc_nz 命令を生成してほしい

末尾if-break (flatten)

前述の問題を解決できるかもしれないと思い,if文に [flatten] を指定した. 下記のコードでは UNITY_FLATTEN としているが,これは環境差吸収用のマクロで,HLSLコンパイラ向けなら [flatten] に置換される.

float t = 0.0;
for (int i = 0; i < _MaxLoop; i++) {
    const float d = map(ro + rd * t);
    t += d * _MarchingFactor;

    UNITY_FLATTEN
    if (d < _MinRayLength || t > _MaxRayLength) {
        break;
    }
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_POST_FLATTEN_IF_BREAK
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 5 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r0.w, l(0)
   4: mov r1.x, l(0)
   5: loop
   6:   ige r1.y, r1.x, cb0[4].x
   7:   breakc_nz r1.y
   8:   mad r1.yzw, r0.xxyz, r0.wwww, v1.xxyz
   9:   dp3 r1.y, r1.yzwy, r1.yzwy
  10:   sqrt r1.y, r1.y
  11:   add r1.y, r1.y, l(-0.500000)
  12:   mad r1.z, r1.y, cb0[4].w, r0.w
  13:   lt r1.y, r1.y, cb0[4].y
  14:   lt r1.w, cb0[4].z, r1.z
  15:   or r1.y, r1.w, r1.y
  16:   mov r0.w, r1.z
  17:   breakc_nz r1.y
  18:   iadd r1.x, r1.x, l(1)
  19:   mov r0.w, r1.z
  20: endloop
  21: add r1.x, -r0.w, cb0[4].z
  22: lt r1.x, r1.x, l(0.000000)
  23: discard_nz r1.x

breakc_nz 命令は生成されるようになったが,依然として冗長な mov 命令が生成される問題が残っている.

先頭if-break

そこでbreak判定を先頭に持っていくことにした. 初回は必ずfalseになるため,ループ末尾にif ~ breakを記述するのと意味は変わらない.

float d = _MaxRayLength;
for (int i = 0; i < _MaxLoop; i++) {
    if (d < _MinRayLength || t > _MaxRayLength) {
        break;
    }

    d = map(ro + rd * t);
    t += d * _MarchingFactor;
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_PRE_IF_BREAK
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 6 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r1.x, l(0)
   4: mov r1.y, cb0[4].z
   5: mov r0.w, l(0)
   6: loop
   7:   ige r1.z, r0.w, cb0[4].x
   8:   breakc_nz r1.z
   9:   lt r1.z, r1.y, cb0[4].y
  10:   lt r1.w, cb0[4].z, r1.x
  11:   or r1.z, r1.w, r1.z
  12:   if_nz r1.z
  13:     break
  14:   endif
  15:   mad r2.xyz, r0.xyzx, r1.xxxx, v1.xyzx
  16:   dp3 r1.z, r2.xyzx, r2.xyzx
  17:   sqrt r1.z, r1.z
  18:   add r1.y, r1.z, l(-0.500000)
  19:   mad r1.x, r1.y, cb0[4].w, r1.x
  20:   iadd r0.w, r0.w, l(1)
  21: endloop

if内の mov 命令が消えた! (これなら [flatten] 指定しなくても breakc_nz 命令を生成するぐらい気を利かせてもよいと思うが...)

先頭break (flatten)

[flatten] 指定をすることで breakc_nz 命令となった.

float d = _MaxRayLength;
for (int i = 0; i < _MaxLoop; i++) {
    UNITY_FLATTEN
    if (d < _MinRayLength || t > _MaxRayLength) {
        break;
    }

    d = map(ro + rd * t);
    t += d * _MarchingFactor;
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_PRE_FLATTEN_IF_BREAK
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 5 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r1.x, l(0)
   4: mov r1.y, cb0[4].z
   5: mov r0.w, l(0)
   6: loop
   7:   ige r1.z, r0.w, cb0[4].x
   8:   breakc_nz r1.z
   9:   lt r1.z, r1.y, cb0[4].y
  10:   lt r1.w, cb0[4].z, r1.x
  11:   or r1.z, r1.w, r1.z
  12:   breakc_nz r1.z
  13:   mad r2.xyz, r0.xyzx, r1.xxxx, v1.xyzx
  14:   dp3 r1.z, r2.xyzx, r2.xyzx
  15:   sqrt r1.z, r1.z
  16:   add r1.y, r1.z, l(-0.500000)
  17:   mad r1.x, r1.y, cb0[4].w, r1.x
  18:   iadd r0.w, r0.w, l(1)
  19: endloop
  20: add r0.w, -r1.x, cb0[4].z
  21: lt r0.w, r0.w, l(0.000000)
  22: discard_nz r0.w

かなり理想に近い形となったが,ループカウンタのための breakc_nz 命令と1つにまとめたい気持ちが出てくる.

forループ継続条件追加

for文の条件判定部分にbreak条件を折り込んだところ,breakc_nz 命令が1つにまとまった.

float d = _MaxLoop;
for (int i = 0; i < _MaxLoop && d >= _MinRayLength && t <= _MaxRayLength; i++) {
    d = map(ro + rd * t);
    t += d * _MarchingFactor;
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_USE_LOOP_CONTINUOUS
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 63 math, 4 temp registers, 1 textures, 4 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r1.x, l(0)
   4: mov r1.y, cb0[4].z
   5: mov r0.w, l(0)
   6: loop
   7:   ilt r1.z, r0.w, cb0[4].x
   8:   ge r1.w, r1.y, cb0[4].y
   9:   and r1.z, r1.w, r1.z
  10:   ge r1.w, cb0[4].z, r1.x
  11:   and r1.z, r1.w, r1.z
  12:   breakc_z r1.z
  13:   mad r2.xyz, r0.xyzx, r1.xxxx, v1.xyzx
  14:   dp3 r1.z, r2.xyzx, r2.xyzx
  15:   sqrt r1.z, r1.z
  16:   add r1.y, r1.z, l(-0.500000)
  17:   mad r1.x, r1.y, cb0[4].w, r1.x
  18:   iadd r0.w, r0.w, l(1)
  19: endloop
  20: add r0.w, -r1.x, cb0[4].z
  21: lt r0.w, r0.w, l(0.000000)
  22: discard_nz r0.w

欲を言うなら,先頭で判定するのは直感に反するので,末尾判断と同じようにしたいところである.

繰り返し時ループカウンタ操作

break条件を満たすとき,ループカウンタを大きな値にすることでbreakを実現するコードにしてみた. 0x7fffffffであればいかなる _MaxLoop の値に対しても i < _MaxLoop はfalseとなる. i = _MaxLoop としてもよかったのだが,後述の方法と足並を揃えるためと,即値命令の方が定数バッファへのアクセスよりよさそうだと根拠なく思ったためである.

float d = _MaxRayLength;
for (int i = 0; i < _MaxLoop; i = (d < _MinRayLength || t > _MaxRayLength) ? 0x7fffffff : i + 1) {
    d = map(ro + rd * t);
    t += d * _MarchingFactor;
}

// for (int i = 0; i < _MaxLoop; i = d < _MinRayLength ? 0x7fffffff : i++) {
//     const float d = map(ro + rd * t);
//     t += d * _MarchingFactor;
//     clip(_MaxRayLength - t);
// }
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_USE_INDEX_UPDATE
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 4 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r0.w, l(0)
   4: mov r1.x, l(0)
   5: loop
   6:   ige r1.y, r1.x, cb0[4].x
   7:   breakc_nz r1.y
   8:   mad r1.yzw, r0.xxyz, r0.wwww, v1.xxyz
   9:   dp3 r1.y, r1.yzwy, r1.yzwy
  10:   sqrt r1.y, r1.y
  11:   add r1.y, r1.y, l(-0.500000)
  12:   mad r0.w, r1.y, cb0[4].w, r0.w
  13:   lt r1.y, r1.y, cb0[4].y
  14:   lt r1.z, cb0[4].z, r0.w
  15:   or r1.y, r1.z, r1.y
  16:   iadd r1.z, r1.x, l(1)
  17:   movc r1.x, r1.y, l(0x7fffffff), r1.z
  18: endloop
  19: add r1.x, -r0.w, cb0[4].z
  20: lt r1.x, r1.x, l(0.000000)
  21: discard_nz r1.x

ループカウンタの条件判断レジスタと別レジスタmovc 命令の判定を行うコードのため,命令数が1つ少なくなった.

末尾ループカウンタ操作

for文の継続条件部分,更新部分にはループカウンタに関するもの以外書きたくない気持ちがある. そこで,下記のようにifの部分でループカウンタをいじることにしてみる. _MaxLoop - 1 の設定でよいのだが,1の減算命令が生成されてしまうかもしれないため,int型の最大値 - 1を設定することにしている. (実際には _MaxLoop - 1 でも減算命令は生成されなかった)

for (int i = 0; i < _MaxLoop; i++) {
    const float d = map(ro + rd * t);
    t += d * _MarchingFactor;

    UNITY_FLATTEN
    if (d < _MinRayLength || t > _MaxRayLength) {
        i = 0x7ffffffe;
    }
}
Global Keywords: <none>
Local Keywords: _BREAKMETHOD_POST_UPDATE_INDEX
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 62 math, 4 temp registers, 1 textures, 4 branches

   0: dp3 r0.x, v2.xyzx, v2.xyzx
   1: rsq r0.x, r0.x
   2: mul r0.xyz, r0.xxxx, v2.xyzx
   3: mov r0.w, l(0)
   4: mov r1.x, l(0)
   5: loop
   6:   ige r1.y, r1.x, cb0[4].x
   7:   breakc_nz r1.y
   8:   mad r1.yzw, r0.xxyz, r0.wwww, v1.xxyz
   9:   dp3 r1.y, r1.yzwy, r1.yzwy
  10:   sqrt r1.y, r1.y
  11:   add r1.y, r1.y, l(-0.500000)
  12:   mad r0.w, r1.y, cb0[4].w, r0.w
  13:   lt r1.y, r1.y, cb0[4].y
  14:   lt r1.z, cb0[4].z, r0.w
  15:   or r1.y, r1.z, r1.y
  16:   iadd r1.z, r1.x, l(1)
  17:   movc r1.x, r1.y, l(0x7fffffff), r1.z
  18: endloop
  19: add r1.x, -r0.w, cb0[4].z
  20: lt r1.x, r1.x, l(0.000000)
  21: discard_nz r1.x

繰り返し時ループカウンタ操作と同じコードとなった. 可読性を重んじるならこのコードにすべきかもしれない. あと,d の宣言をループ内に持ってこれる点も大きい.

まとめ

HLSLコンパイラはループ内のbreakの扱いが下手であり,特に末尾にif ~ breakを書いた場合,かなり冗長なコードを生成することがわかった.

本記事で示した各手法について,アセンブリ中のmathとbranch数を示すと下記の通り. 基本的にこの値が小さいほどよいコードであると言えると思う.

手法 math branches
末尾if-break 62 6
末尾if-break (flatten) 62 5
先頭if-break 62 6
先頭break (flatten) 62 5
forループ継続条件追加 63 4
繰り返し時ループカウンタ操作 62 4
末尾ループカウンタ操作 62 4

branchは if ~ endif で1つ, breakc_nz 命令で1つ計上されているようだ. この値から繰り返し時ループカウンタ操作,または末尾ループカウンタ操作の出力アセンブリが良いアセンブリと言えるのではないだろうか?

モデル行列に関するメモ

モデル行列の調理

モデル行列とはオブジェクトをローカル座標からワールド座標に移動する行列であり,具体的には平行移動,回転,拡大縮小の3つを行う行列である.

Unityのシェーダーとしては unity_ObjectToWorld というuniform変数で与えられる.

この行列から情報を抜き出すことを考える.

モデル行列

モデル行列 $\boldsymbol{M}$ の各成分は下記のようになっている.

\begin{equation} \boldsymbol{M} = \begin{pmatrix} m_{00} & m_{01} & m_{02} & t_x \\ m_{10} & m_{11} & m_{12} & t_y \\ m_{20} & m_{21} & m_{22} & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix} \label{ModelMatrix} \end{equation}

また,モデル行列 $\boldsymbol{M}$ は平行移動($\boldsymbol{T}$),回転($\boldsymbol{R}$),拡大縮小($\boldsymbol{S}$)を組み合わせたものであり,拡大縮小,回転,平行移動の順に適用することから,下記のようにも表現できる.

\begin{equation} \boldsymbol{M} = \boldsymbol{T} \boldsymbol{R} \boldsymbol{S} \end{equation}

平行移動行列

平行移動行列 $\boldsymbol{T}$ に関しては $t_x, t_y, t_z$ を用いて表現することが可能である.

\begin{equation} \boldsymbol{T} = \begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix} \end{equation}

Unityのshaderlabとしては平行移動成分は下記のようになる.

float3 translate = unity_ObjectToWorld._m03_m13_m23;

1始まりのswizzle演算子を用いるなら,

float3 translate = unity_ObjectToWorld._14_24_34;

インデックスアクセスを行った後,ベクトルのswizzle演算子を用いるなら,

float3 translate = unity_ObjectToWorld[3].xyz;

ベクトルの積とで求めるなら

float3 translate = mul(unity_ObjectToWorld, float4(0.0, 0.0, 0.0, 1.0)).xyz;

である.

この平行移動成分はオブジェクトのワールド座標であるが,オブジェクトの各頂点のワールド座標ではない. オブジェクトの中心というべきローカル座標の原点がワールド座標においてどこに位置するか,である.

なので,座標表示シェーダー等に用いるにはよいかもしれない.

回転行列

回転行列はX軸まわりの回転行列 $\boldsymbol{R}_x$,Y軸まわりの回転行列 $\boldsymbol{R}_y$,Z軸まわりの回転行列 $\boldsymbol{R}_z$ から成り,UnityはZ軸まわりの回転,Y軸まわりの回転,X軸まわりの回転の順に適用することから,$\boldsymbol{R}$は

\begin{equation} \boldsymbol{R} = \boldsymbol{R}_x \boldsymbol{R}_y \boldsymbol{R}_z \end{equation}

と表現できる.

X軸まわりの回転角を $\theta_x$,Y軸まわりの回転角を $\theta_y$,Z軸まわりの回転角を $\theta_z$ とすると,$\boldsymbol{R}_x$,$\boldsymbol{R}_y$,$\boldsymbol{R}_z$ はそれぞれ,

\begin{equation} \boldsymbol{R}_x = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos \theta_x & -\sin \theta_x & 0 \\ 0 & \sin \theta_x & \cos \theta_x & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \end{equation}

\begin{equation} \boldsymbol{R}_y = \begin{pmatrix} \cos \theta_y & 0 & \sin \theta_y & 0 \\ 0 & 1 & 0 & 0 \\ -\sin \theta_y & 0 & \cos \theta_y & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \end{equation}

\begin{equation} \boldsymbol{R}_z = \begin{pmatrix} \cos \theta_z & -\sin \theta_z & 0 & 0 \\ \sin \theta_z & \cos \theta_z & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \end{equation}

と表現できる.

拡大縮小行列

X軸方向の拡大率を $s_x$,Y軸方向の拡大率を $s_y$,Z軸方向の拡大率を $s_z$ とおくと,拡大縮小行列 $\boldsymbol{S}$ は

\begin{equation} \boldsymbol{S} = \begin{pmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \end{equation}

と表現できる.

回転行列と拡大縮小行列の積

モデル行列 $\boldsymbol{M} = \boldsymbol{TRS}$ のうち,平行移動は $\boldsymbol{T}$ が担当しており,回転行列 $\boldsymbol{R}$ と 拡大縮小行列 $\boldsymbol{S}$ は左上3x3の成分で構成されるので,

\begin{equation} \boldsymbol{RS} = \begin{pmatrix} m_{00} & m_{01} & m_{02} & 0 \\ m_{10} & m_{11} & m_{12} & 0 \\ m_{20} & m_{21} & m_{22} & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \end{equation}

となる. すなわち,(頑張れば) $m_{ij}$ は $\theta_x$,$\theta_y$,$\theta_z$,$s_x$,$s_y$,$s_z$ を用いて表すことができるはずであるが,とても大変なことは想像に難くない.

モデル行列から拡大縮小行列の各成分を求める

モデル行列 $\boldsymbol{M}$ から拡大縮小行列 $\boldsymbol{S}$ の各成分 $s_x, s_y, s_z$ を求めることを考える. すなわち,$s_x, s_y, s_z$ を $m_{ij}$ で表現することを考える.

ここでは平行移動成分は扱わないため同次座標で考える必要はない. ごちゃごちゃした記述にしないため,3x3の行列で考えることにする.

まず,回転行列 $\boldsymbol{R}$ は任意のベクトル $\boldsymbol{v} = \begin{pmatrix} x & y & z \end{pmatrix}^T$の大きさには影響しない. すなわち,

\begin{equation} \| \boldsymbol{Rv} \| = \| \boldsymbol{v} \| \label{RotateVector} \end{equation}

である. これは回転行列の操作の意味を考えれば自明なことであるが,一応証明する.

まずX軸まわりの回転行列 $\boldsymbol{R}_x$ について,

\begin{equation} \boldsymbol{R}_x \boldsymbol{v} = \begin{pmatrix} x \\ y \cos \theta_x - z \sin \theta_x \\ y \sin \theta_x + z \cos \theta_x \\ \end{pmatrix} \end{equation}

となるので,

\begin{eqnarray} \| \boldsymbol{R}_x \boldsymbol{v} \| & = & \sqrt{x^{2} + (y \cos \theta_x - z \sin \theta_x)^{2} + (y \sin \theta_x + z \cos \theta_x)^{2}} \nonumber \\ & = & \sqrt{x^{2} + (y^{2} \cos^{2} \theta_x - 2yz \cos \theta_x \sin \theta_x + z^{2} \sin^{2} \theta_x) + (y^{2} \sin^{2} \theta_x + 2yz \cos \theta_x \sin \theta_x + z^{2} \cos^{2} \theta_x)} \nonumber \\ & = & \sqrt{x^{2} + y^{2} (\cos^{2} \theta_x + \sin^{2} \theta_x) + z^{2} (\cos^{2} \theta_x + \sin^{2} \theta_x) + 2yz(\cos \theta_x \sin \theta_x - \cos \theta_x \sin \theta_x)} \nonumber \\ & = & \sqrt{x^{2} + y^{2} + z^{2}} \nonumber \\ & = & \| \boldsymbol{v} \| \end{eqnarray}

$\boldsymbol{R}_y$,$\boldsymbol{R}_z$ についても同様に(式の対称性から自明),

\begin{equation} \| \boldsymbol{R}_y \boldsymbol{v} \| = \| \boldsymbol{v} \| \end{equation}

\begin{equation} \| \boldsymbol{R}_z \boldsymbol{v} \| = \| \boldsymbol{v} \| \end{equation}

従って,

\begin{eqnarray} \| \boldsymbol{Rv} \| & = & \| \boldsymbol{R}_x \boldsymbol{R}_y \boldsymbol{R}_z \boldsymbol{v} \| \nonumber \\ & = & \| \boldsymbol{R}_y \boldsymbol{R}_z \boldsymbol{v} \| \nonumber \\ & = & \| \boldsymbol{R}_z \boldsymbol{v} \| \nonumber \\ & = & \| \boldsymbol{v} \| \end{eqnarray}

さて,式\eqref{RotateVector}は任意のベクトル $\boldsymbol{v}$ について成り立つので,$\boldsymbol{v} = \boldsymbol{S} \boldsymbol{e}$ とおくと($\boldsymbol{e}$ もまた任意のベクトル),

\begin{equation} \| \boldsymbol{RSe} \| = \| \boldsymbol{Se} \| \end{equation}

すなわち,

\begin{equation} \left\| \begin{pmatrix} m_{00} & m_{01} & m_{02} \\ m_{10} & m_{11} & m_{12} \\ m_{20} & m_{21} & m_{22} \end{pmatrix} \boldsymbol{e} \right\| = \left\| \begin{pmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & s_z \end{pmatrix} \boldsymbol{e} \right\| \end{equation}

となる. $\boldsymbol{e}$ は任意のベクトルなので,都合良く $\boldsymbol{e} = \begin{pmatrix} 1 & 0 & 0 \end{pmatrix}^T$ を選出すると,

\begin{eqnarray} \left\| \begin{pmatrix} m_{00} & m_{10} & m_{20} \end{pmatrix}^T \right\| & = & \left\| \begin{pmatrix} s_x & 0 & 0 \end{pmatrix}^T \right\| \nonumber \\ s_x & = & \pm \sqrt{m_{00}^2 + m_{10}^2 + m_{20}^2} \end{eqnarray}

となる. 同様に,$\boldsymbol{e} = \begin{pmatrix} 0 & 1 & 0 \end{pmatrix}^T$,$\boldsymbol{e} = \begin{pmatrix} 0 & 0 & 1 \end{pmatrix}^T$ を選出すると,

\begin{equation} s_y = \pm \sqrt{m_{01}^2 + m_{11}^2 + m_{21}^2} \end{equation}

\begin{equation} s_z = \pm \sqrt{m_{02}^2 + m_{12}^2 + m_{22}^2} \end{equation}

となる. Unityとしては負のスケールも許容しているが,計算からはスケール成分の正負までを求めることはできないことに注意.

UnityのShaderlabとしては下記のようになる.

float3 scales = float3(
    length(unity_ObjectToWorld._m00_m10_m20),
    length(unity_ObjectToWorld._m01_m11_m21),
    length(unity_ObjectToWorld._m02_m12_m22));

あるいは,1始まりのswizzle演算子を用いるなら,

float3 scales = float3(
    length(unity_ObjectToWorld._11_21_31),
    length(unity_ObjectToWorld._12_22_32),
    length(unity_ObjectToWorld._13_23_33));

となる.

CustomRenderTextureVertexShader()を改善する

はじめに

カスタムレンダーテクスチャの頂点シェーダーはUInityの標準ライブラリ CustomRenderTextureVertexShader から提供されており、これを用いることになっている。 しかし、実装を読み、なおかつDirect3D11の出力アセンブリを読んだところ、あまりよくない実装になっていると感じた個所がある.

この記事では当該箇所を修正することにより,より良い出力アセンブリが得られたことを示す. また修正案をカスタムレンダーテクスチャ用のシェーダーに取り入れる方法についても示す.

CustomRenderTextureVertexShader の問題点

Uniy 2022.2.2の UnityCustomRenderTexture.cginc は下記のようになっている. (古いバージョンでも同じ)

// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

#ifndef UNITY_CUSTOM_TEXTURE_INCLUDED
#define UNITY_CUSTOM_TEXTURE_INCLUDED

#include "UnityCG.cginc"
#include "UnityStandardConfig.cginc"

// Keep in sync with CustomRenderTexture.h
#define kCustomTextureBatchSize 16

struct appdata_customrendertexture
{
    uint    vertexID    : SV_VertexID;
};

// User facing vertex to fragment shader structure
struct v2f_customrendertexture
{
    float4 vertex           : SV_POSITION;
    float3 localTexcoord    : TEXCOORD0;    // Texcoord local to the update zone (== globalTexcoord if no partial update zone is specified)
    float3 globalTexcoord   : TEXCOORD1;    // Texcoord relative to the complete custom texture
    uint primitiveID        : TEXCOORD2;    // Index of the update zone (correspond to the index in the updateZones of the Custom Texture)
    float3 direction        : TEXCOORD3;    // For cube textures, direction of the pixel being rendered in the cubemap
};

float2 CustomRenderTextureRotate2D(float2 pos, float angle)
{
    float sn = sin(angle);
    float cs = cos(angle);

    return float2( pos.x * cs - pos.y * sn, pos.x * sn + pos.y * cs);
}

// Internal
float4      CustomRenderTextureCenters[kCustomTextureBatchSize];
float4      CustomRenderTextureSizesAndRotations[kCustomTextureBatchSize];
float       CustomRenderTexturePrimitiveIDs[kCustomTextureBatchSize];

float4      CustomRenderTextureParameters;
#define     CustomRenderTextureUpdateSpace  CustomRenderTextureParameters.x // Normalized(0)/PixelSpace(1)
#define     CustomRenderTexture3DTexcoordW  CustomRenderTextureParameters.y
#define     CustomRenderTextureIs3D         CustomRenderTextureParameters.z

// User facing uniform variables
float4      _CustomRenderTextureInfo; // x = width, y = height, z = depth, w = face/3DSlice

// Helpers
#define _CustomRenderTextureWidth   _CustomRenderTextureInfo.x
#define _CustomRenderTextureHeight  _CustomRenderTextureInfo.y
#define _CustomRenderTextureDepth   _CustomRenderTextureInfo.z

// Those two are mutually exclusive so we can use the same slot
#define _CustomRenderTextureCubeFace    _CustomRenderTextureInfo.w
#define _CustomRenderTexture3DSlice     _CustomRenderTextureInfo.w

sampler2D   _SelfTexture2D;
samplerCUBE _SelfTextureCube;
sampler3D   _SelfTexture3D;

float3 CustomRenderTextureComputeCubeDirection(float2 globalTexcoord)
{
    float2 xy = globalTexcoord * 2.0 - 1.0;
    float3 direction;
    if(_CustomRenderTextureCubeFace == 0.0)
    {
        direction = normalize(float3(1.0, -xy.y, -xy.x));
    }
    else if(_CustomRenderTextureCubeFace == 1.0)
    {
        direction = normalize(float3(-1.0, -xy.y, xy.x));
    }
    else if(_CustomRenderTextureCubeFace == 2.0)
    {
        direction = normalize(float3(xy.x, 1.0, xy.y));
    }
    else if(_CustomRenderTextureCubeFace == 3.0)
    {
        direction = normalize(float3(xy.x, -1.0, -xy.y));
    }
    else if(_CustomRenderTextureCubeFace == 4.0)
    {
        direction = normalize(float3(xy.x, -xy.y, 1.0));
    }
    else if(_CustomRenderTextureCubeFace == 5.0)
    {
        direction = normalize(float3(-xy.x, -xy.y, -1.0));
    }

    return direction;
}

// standard custom texture vertex shader that should always be used
v2f_customrendertexture CustomRenderTextureVertexShader(appdata_customrendertexture IN)
{
    v2f_customrendertexture OUT;

#if UNITY_UV_STARTS_AT_TOP
    const float2 vertexPositions[6] =
    {
        { -1.0f,  1.0f },
        { -1.0f, -1.0f },
        {  1.0f, -1.0f },
        {  1.0f,  1.0f },
        { -1.0f,  1.0f },
        {  1.0f, -1.0f }
    };

    const float2 texCoords[6] =
    {
        { 0.0f, 0.0f },
        { 0.0f, 1.0f },
        { 1.0f, 1.0f },
        { 1.0f, 0.0f },
        { 0.0f, 0.0f },
        { 1.0f, 1.0f }
    };
#else
    const float2 vertexPositions[6] =
    {
        {  1.0f,  1.0f },
        { -1.0f, -1.0f },
        { -1.0f,  1.0f },
        { -1.0f, -1.0f },
        {  1.0f,  1.0f },
        {  1.0f, -1.0f }
    };

    const float2 texCoords[6] =
    {
        { 1.0f, 1.0f },
        { 0.0f, 0.0f },
        { 0.0f, 1.0f },
        { 0.0f, 0.0f },
        { 1.0f, 1.0f },
        { 1.0f, 0.0f }
    };
#endif

    uint primitiveID = IN.vertexID / 6;
    uint vertexID = IN.vertexID % 6;
    float3 updateZoneCenter = CustomRenderTextureCenters[primitiveID].xyz;
    float3 updateZoneSize = CustomRenderTextureSizesAndRotations[primitiveID].xyz;
    float rotation = CustomRenderTextureSizesAndRotations[primitiveID].w * UNITY_PI / 180.0f;

#if !UNITY_UV_STARTS_AT_TOP
    rotation = -rotation;
#endif

    // Normalize rect if needed
    if (CustomRenderTextureUpdateSpace > 0.0) // Pixel space
    {
        // Normalize xy because we need it in clip space.
        updateZoneCenter.xy /= _CustomRenderTextureInfo.xy;
        updateZoneSize.xy /= _CustomRenderTextureInfo.xy;
    }
    else // normalized space
    {
        // Un-normalize depth because we need actual slice index for culling
        updateZoneCenter.z *= _CustomRenderTextureInfo.z;
        updateZoneSize.z *= _CustomRenderTextureInfo.z;
    }

    // Compute rotation

    // Compute quad vertex position
    float2 clipSpaceCenter = updateZoneCenter.xy * 2.0 - 1.0;
    float2 pos = vertexPositions[vertexID] * updateZoneSize.xy;
    pos = CustomRenderTextureRotate2D(pos, rotation);
    pos.x += clipSpaceCenter.x;
#if UNITY_UV_STARTS_AT_TOP
    pos.y += clipSpaceCenter.y;
#else
    pos.y -= clipSpaceCenter.y;
#endif

    // For 3D texture, cull quads outside of the update zone
    // This is neeeded in additional to the preliminary minSlice/maxSlice done on the CPU because update zones can be disjointed.
    // ie: slices [1..5] and [10..15] for two differents zones so we need to cull out slices 0 and [6..9]
    if (CustomRenderTextureIs3D > 0.0)
    {
        int minSlice = (int)(updateZoneCenter.z - updateZoneSize.z * 0.5);
        int maxSlice = minSlice + (int)updateZoneSize.z;
        if (_CustomRenderTexture3DSlice < minSlice || _CustomRenderTexture3DSlice >= maxSlice)
        {
            pos.xy = float2(1000.0, 1000.0); // Vertex outside of ncs
        }
    }

    OUT.vertex = float4(pos, 0.0, 1.0);
    OUT.primitiveID = asuint(CustomRenderTexturePrimitiveIDs[primitiveID]);
    OUT.localTexcoord = float3(texCoords[vertexID], CustomRenderTexture3DTexcoordW);
    OUT.globalTexcoord = float3(pos.xy * 0.5 + 0.5, CustomRenderTexture3DTexcoordW);
#if UNITY_UV_STARTS_AT_TOP
    OUT.globalTexcoord.y = 1.0 - OUT.globalTexcoord.y;
#endif
    OUT.direction = CustomRenderTextureComputeCubeDirection(OUT.globalTexcoord.xy);

    return OUT;
}

struct appdata_init_customrendertexture
{
    float4 vertex : POSITION;
    float2 texcoord : TEXCOORD0;
};

// User facing vertex to fragment structure for initialization materials
struct v2f_init_customrendertexture
{
    float4 vertex : SV_POSITION;
    float3 texcoord : TEXCOORD0;
    float3 direction : TEXCOORD1;
};

// standard custom texture vertex shader that should always be used for initialization shaders
v2f_init_customrendertexture InitCustomRenderTextureVertexShader (appdata_init_customrendertexture v)
{
    v2f_init_customrendertexture o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.texcoord = float3(v.texcoord.xy, CustomRenderTexture3DTexcoordW);
    o.direction = CustomRenderTextureComputeCubeDirection(v.texcoord.xy);
    return o;
}

#endif // UNITY_CUSTOM_TEXTURE_INCLUDED

問題に感じたのが CustomRenderTextureComputeCubeDirection() である. ここだけ抜粋して再掲する.

float3 CustomRenderTextureComputeCubeDirection(float2 globalTexcoord)
{
    float2 xy = globalTexcoord * 2.0 - 1.0;
    float3 direction;
    if(_CustomRenderTextureCubeFace == 0.0)
    {
        direction = normalize(float3(1.0, -xy.y, -xy.x));
    }
    else if(_CustomRenderTextureCubeFace == 1.0)
    {
        direction = normalize(float3(-1.0, -xy.y, xy.x));
    }
    else if(_CustomRenderTextureCubeFace == 2.0)
    {
        direction = normalize(float3(xy.x, 1.0, xy.y));
    }
    else if(_CustomRenderTextureCubeFace == 3.0)
    {
        direction = normalize(float3(xy.x, -1.0, -xy.y));
    }
    else if(_CustomRenderTextureCubeFace == 4.0)
    {
        direction = normalize(float3(xy.x, -xy.y, 1.0));
    }
    else if(_CustomRenderTextureCubeFace == 5.0)
    {
        direction = normalize(float3(-xy.x, -xy.y, -1.0));
    }

    return direction;
}

if文を用いているが,uniform変数に対してであるのと,この程度であれば movc 命令を生成するため問題とは思わない. しかし,各if文中で normalize() を用いていることが問題であると思う. normalize() を用いるならループ外にすべきではないだろうか.

実際に生成されたコードからも何度も normalize() の呼び出しが行われていることがわかる(dp3, rsq, mul が1回の normalize() に対応).

Global Keywords: <none>
Local Keywords: <none>
-- Hardware tier variant: Tier 1
-- Vertex shader for "d3d11":
// Stats: 46 math, 8 temp registers, 2 branches
Constant Buffer "$Globals" (848 bytes) on slot 0 {
  Vector4 CustomRenderTextureCenters[16] at 32
  Vector4 CustomRenderTextureSizesAndRotations[16] at 288
  Vector1 CustomRenderTexturePrimitiveIDs[16] at 544
  Vector4 CustomRenderTextureParameters at 800
  Vector4 _CustomRenderTextureInfo at 816
}

Shader Disassembly:
//
// Generated by Microsoft (R) D3D Shader Disassembler
//
//
// Input signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_VertexID              0   x           0   VERTID    uint   x
//
//
// Output signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_POSITION              0   xyzw        0      POS   float   xyzw
// TEXCOORD                 0   xyz         1     NONE   float   xyz
// TEXCOORD                 1   xyz         2     NONE   float   xyz
// TEXCOORD                 2   x           3     NONE    uint   x
// TEXCOORD                 3   xyz         4     NONE   float   xyz
//
      vs_4_0
      dcl_immediateConstantBuffer { { -1.000000, 1.000000, 0, 0},
                              { -1.000000, -1.000000, 0, 1.000000},
                              { 1.000000, -1.000000, 1.000000, 1.000000},
                              { 1.000000, 1.000000, 1.000000, 0},
                              { -1.000000, 1.000000, 0, 0},
                              { 1.000000, -1.000000, 1.000000, 1.000000} }
      dcl_constantbuffer CB0[52], dynamicIndexed
      dcl_input_sgv v0.x, vertex_id
      dcl_output_siv o0.xyzw, position
      dcl_output o1.xyz
      dcl_output o2.xyz
      dcl_output o3.x
      dcl_output o4.xyz
      dcl_temps 8
   0: udiv r0.x, r1.x, v0.x, l(6)
   1: mul r0.y, l(0.017453), cb0[r0.x + 18].w
   2: lt r0.zw, l(0.000000, 0.000000, 0.000000, 0.000000), cb0[50].xxxz
   3: div r1.yz, cb0[r0.x + 2].xxyx, cb0[51].xxyx
   4: div r2.yz, cb0[r0.x + 18].xxyx, cb0[51].xxyx
   5: mul r3.x, cb0[51].z, cb0[r0.x + 2].z
   6: mul r3.w, cb0[51].z, cb0[r0.x + 18].z
   7: movc r1.yz, r0.zzzz, r1.yyzy, cb0[r0.x + 2].xxyx
   8: mov r2.x, cb0[r0.x + 2].z
   9: mov r2.w, cb0[r0.x + 18].z
  10: mov r3.yz, cb0[r0.x + 18].xxyx
  11: movc r2.xyzw, r0.zzzz, r2.xyzw, r3.xyzw
  12: mad r1.yz, r1.yyzy, l(0.000000, 2.000000, 2.000000, 0.000000), l(0.000000, -1.000000, -1.000000, 0.000000)
  13: mul r2.yz, r2.zzyz, icb[r1.x + 0].yyxy
  14: sincos r3.x, r4.x, r0.y
  15: mul r0.yz, r2.yyzy, r3.xxxx
  16: mad r0.y, r2.z, r4.x, -r0.y
  17: mad r0.z, r2.y, r4.x, r0.z
  18: add r3.xy, r1.yzyy, r0.yzyy
  19: mad r0.y, -r2.w, l(0.500000), r2.x
  20: ftoi r0.z, r0.y
  21: ftoi r1.y, r2.w
  22: iadd r0.z, r0.z, r1.y
  23: round_z r0.y, r0.y
  24: lt r0.y, cb0[51].w, r0.y
  25: itof r0.z, r0.z
  26: ge r0.z, cb0[51].w, r0.z
  27: or r0.y, r0.z, r0.y
  28: movc r0.yz, r0.yyyy, l(0,1000.000000,1000.000000,0), r3.xxyx
  29: movc r0.yz, r0.wwww, r0.yyzy, r3.xxyx
  30: mad r2.xy, r0.yzyy, l(0.500000, 0.500000, 0.000000, 0.000000), l(0.500000, 0.500000, 0.000000, 0.000000)
  31: add r2.z, -r2.y, l(1.000000)
  32: mad r3.xy, r2.xzxx, l(2.000000, 2.000000, 0.000000, 0.000000), l(-1.000000, -1.000000, 0.000000, 0.000000)
  33: eq r0.w, cb0[51].w, l(0.000000)
  34: if_nz r0.w
  35:   mov r4.yz, -r3.yyxy
  36:   mov r4.x, l(1.000000)
  37:   dp3 r0.w, r4.xyzx, r4.xyzx
  38:   rsq r0.w, r0.w
  39:   mul o4.xyz, r0.wwww, r4.xyzx
  40: else
  41:   mov r3.z, l(-1.000000)
  42:   mov r3.w, -r3.y
  43:   mul r1.yzw, r3.xxzy, l(0.000000, 1.000000, 1.000000, -1.000000)
  44:   dp3 r0.w, r3.xzwx, r1.yzwy
  45:   rsq r0.w, r0.w
  46:   mul r4.xyz, r0.wwww, r1.zwyz
  47:   mov r5.xz, r3.xxyx
  48:   mov r5.y, l(1.000000)
  49:   dp3 r0.w, r5.xyzx, r5.xyzx
  50:   rsq r0.w, r0.w
  51:   mul r6.xyz, r0.wwww, r5.xyzx
  52:   dp3 r0.w, r1.yzwy, r1.yzwy
  53:   rsq r0.w, r0.w
  54:   mul r1.yzw, r0.wwww, r1.yyzw
  55:   eq r7.xyzw, cb0[51].wwww, l(1.000000, 2.000000, 3.000000, 4.000000)
  56:   mov r5.w, -r3.y
  57:   dp3 r0.w, r5.xywx, r5.xywx
  58:   rsq r0.w, r0.w
  59:   mul r5.xyz, r0.wwww, r5.xwyx
  60:   mul r3.xyz, r3.xwzx, l(-1.000000, 1.000000, 1.000000, 0.000000)
  61:   dp3 r0.w, r3.xyzx, r3.xyzx
  62:   rsq r0.w, r0.w
  63:   mul r3.xyz, r0.wwww, r3.xyzx
  64:   movc r3.xyz, r7.wwww, r5.xyzx, r3.xyzx
  65:   movc r1.yzw, r7.zzzz, r1.yyzw, r3.xxyz
  66:   movc r1.yzw, r7.yyyy, r6.xxyz, r1.yyzw
  67:   movc o4.xyz, r7.xxxx, r4.xyzx, r1.yzwy
  68: endif
  69: mov o0.xy, r0.yzyy
  70: mov o0.zw, l(0,0,0,1.000000)
  71: mov o1.xy, icb[r1.x + 0].zwzz
  72: mov o1.z, cb0[50].y
  73: mov r2.w, cb0[50].y
  74: mov o2.xyz, r2.xzwx
  75: mov o3.x, cb0[r0.x + 34].x
  76: ret
// Approximately 0 instruction slots used

改善案

normalize() をifを抜けた後で呼び出すようにする.

float3 CustomRenderTextureComputeCubeDirection(float2 globalTexcoord)
{
    float2 xy = globalTexcoord * 2.0 - 1.0;
    float3 direction;
    if(_CustomRenderTextureCubeFace == 0.0)
    {
        direction = float3(1.0, -xy.y, -xy.x);
    }
    else if(_CustomRenderTextureCubeFace == 1.0)
    {
        direction = float3(-1.0, -xy.y, xy.x);
    }
    else if(_CustomRenderTextureCubeFace == 2.0)
    {
        direction = float3(xy.x, 1.0, xy.y);
    }
    else if(_CustomRenderTextureCubeFace == 3.0)
    {
        direction = float3(xy.x, -1.0, -xy.y);
    }
    else if(_CustomRenderTextureCubeFace == 4.0)
    {
        direction = float3(xy.x, -xy.y, 1.0);
    }
    else if(_CustomRenderTextureCubeFace == 5.0)
    {
        direction = float3(-xy.x, -xy.y, -1.0);
    }

    return normalize(direction);
}

movc 命令っぽさを出したいのであれば,下記のように条件演算子を用いてもよいだろう.

float3 CustomRenderTextureComputeCubeDirectionEx(float2 globalTexcoord)
{
    float2 xy = globalTexcoord * 2.0 - 1.0;
    return normalize(_CustomRenderTextureCubeFace == 0.0 ? float3(1.0, -xy.y, -xy.x)
        : _CustomRenderTextureCubeFace == 1.0 ? float3(-1.0, -xy.y, xy.x)
        : _CustomRenderTextureCubeFace == 2.0 ? float3(xy.x, 1.0, xy.y)
        : _CustomRenderTextureCubeFace == 3.0 ? float3(xy.x, -1.0, -xy.y)
        : _CustomRenderTextureCubeFace == 4.0 ? float3(xy.x, -xy.y, 1.0)
        : float3(-xy.x, -xy.y, -1.0));
}

このように改善することで下記のコードが得られた. movc の後にnormalizeが行われていることがわかる. (if文を用いたものと条件演算子を用いたものとで生成コードは同じ)

//////////////////////////////////////////////////////
Global Keywords: <none>
Local Keywords: <none>
-- Hardware tier variant: Tier 1
-- Vertex shader for "d3d11":
// Stats: 30 math, 5 temp registers
Constant Buffer "$Globals" (848 bytes) on slot 0 {
  Vector4 CustomRenderTextureCenters[16] at 32
  Vector4 CustomRenderTextureSizesAndRotations[16] at 288
  Vector1 CustomRenderTexturePrimitiveIDs[16] at 544
  Vector4 CustomRenderTextureParameters at 800
  Vector4 _CustomRenderTextureInfo at 816
}

Shader Disassembly:
//
// Generated by Microsoft (R) D3D Shader Disassembler
//
//
// Input signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_VertexID              0   x           0   VERTID    uint   x
//
//
// Output signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_POSITION              0   xyzw        0      POS   float   xyzw
// TEXCOORD                 0   xyz         1     NONE   float   xyz
// TEXCOORD                 1   xyz         2     NONE   float   xyz
// TEXCOORD                 2   x           3     NONE    uint   x
// TEXCOORD                 3   xyz         4     NONE   float   xyz
//
      vs_4_0
      dcl_immediateConstantBuffer { { -1.000000, 1.000000, 0, 0},
                              { -1.000000, -1.000000, 0, 1.000000},
                              { 1.000000, -1.000000, 1.000000, 1.000000},
                              { 1.000000, 1.000000, 1.000000, 0},
                              { -1.000000, 1.000000, 0, 0},
                              { 1.000000, -1.000000, 1.000000, 1.000000} }
      dcl_constantbuffer CB0[52], dynamicIndexed
      dcl_input_sgv v0.x, vertex_id
      dcl_output_siv o0.xyzw, position
      dcl_output o1.xyz
      dcl_output o2.xyz
      dcl_output o3.x
      dcl_output o4.xyz
      dcl_temps 5
   0: lt r0.xy, l(0.000000, 0.000000, 0.000000, 0.000000), cb0[50].xzxx
   1: udiv r1.x, r2.x, v0.x, l(6)
   2: div r3.yz, cb0[r1.x + 18].xxyx, cb0[51].xxyx
   3: mul r4.x, cb0[51].z, cb0[r1.x + 2].z
   4: mul r4.w, cb0[51].z, cb0[r1.x + 18].z
   5: mov r3.x, cb0[r1.x + 2].z
   6: mov r3.w, cb0[r1.x + 18].z
   7: mov r4.yz, cb0[r1.x + 18].xxyx
   8: movc r3.xyzw, r0.xxxx, r3.xyzw, r4.xyzw
   9: mad r0.z, -r3.w, l(0.500000), r3.x
  10: ftoi r0.w, r0.z
  11: round_z r0.z, r0.z
  12: lt r0.z, cb0[51].w, r0.z
  13: ftoi r1.y, r3.w
  14: mul r1.zw, r3.zzzy, icb[r2.x + 0].yyyx
  15: iadd r0.w, r0.w, r1.y
  16: itof r0.w, r0.w
  17: ge r0.w, cb0[51].w, r0.w
  18: or r0.z, r0.w, r0.z
  19: mul r0.w, l(0.017453), cb0[r1.x + 18].w
  20: sincos r3.x, r4.x, r0.w
  21: mul r2.yz, r1.zzwz, r3.xxxx
  22: mad r0.w, r1.w, r4.x, -r2.y
  23: mad r1.y, r1.z, r4.x, r2.z
  24: div r1.zw, cb0[r1.x + 2].xxxy, cb0[51].xxxy
  25: movc r1.zw, r0.xxxx, r1.zzzw, cb0[r1.x + 2].xxxy
  26: mad r1.zw, r1.zzzw, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000)
  27: add r3.x, r0.w, r1.z
  28: add r3.y, r1.w, r1.y
  29: movc r0.xz, r0.zzzz, l(1000.000000,0,1000.000000,0), r3.xxyx
  30: movc r0.xy, r0.yyyy, r0.xzxx, r3.xyxx
  31: mov o0.xy, r0.xyxx
  32: mad r0.xy, r0.xyxx, l(0.500000, 0.500000, 0.000000, 0.000000), l(0.500000, 0.500000, 0.000000, 0.000000)
  33: mov o0.zw, l(0,0,0,1.000000)
  34: mov o1.xy, icb[r2.x + 0].zwzz
  35: mov o3.x, cb0[r1.x + 34].x
  36: mov o1.z, cb0[50].y
  37: add r0.z, -r0.y, l(1.000000)
  38: mov r0.w, cb0[50].y
  39: mov o2.xyz, r0.xzwx
  40: mad r0.xy, r0.xzxx, l(2.000000, 2.000000, 0.000000, 0.000000), l(-1.000000, -1.000000, 0.000000, 0.000000)
  41: mov r0.z, -r0.y
  42: mul r1.xy, r0.xzxx, l(-1.000000, 1.000000, 0.000000, 0.000000)
  43: eq r1.w, cb0[51].w, l(4.000000)
  44: mov r1.z, l(-1.000000)
  45: mov r0.w, l(1.000000)
  46: movc r1.xyz, r1.wwww, r0.xzwx, r1.xyzx
  47: mov r2.yz, r0.zzxz
  48: mul r3.xyz, r0.wzxw, l(1.000000, 1.000000, -1.000000, 0.000000)
  49: mov r2.x, l(-1.000000)
  50: eq r4.xyzw, cb0[51].wwww, l(0.000000, 1.000000, 2.000000, 3.000000)
  51: movc r1.xyz, r4.wwww, r2.zxyz, r1.xyzx
  52: movc r0.xyz, r4.zzzz, r0.xwyx, r1.xyzx
  53: movc r0.xyz, r4.yyyy, r2.xyzx, r0.xyzx
  54: movc r0.xyz, r4.xxxx, r3.xyzx, r0.xyzx
  55: dp3 r0.w, r0.xyzx, r0.xyzx
  56: rsq r0.w, r0.w
  57: mul o4.xyz, r0.wwww, r0.xyzx
  58: ret
// Approximately 0 instruction slots used

カスタムレンダーテクスチャ用シェーダーへの適用

改善したからには自分のカスタムレンダーテクスチャ用のシェーダーに取り入れたいものである. 影響が少なく,使い回しができ,すぐに元に戻せる方法としては以下の通りである.

まず,下記のインクルード用ファイル CustomRenderTextureEx.cginc を用意する.

#ifndef CUSTOM_TEXTURE_EX_INCLUDED
#define CUSTOM_TEXTURE_EX_INCLUDED

#include "UnityCustomRenderTexture.cginc"


float3 CustomRenderTextureComputeCubeDirectionEx(float2 globalTexcoord)
{
    float2 xy = globalTexcoord * 2.0 - 1.0;
    return normalize(_CustomRenderTextureCubeFace == 0.0 ? float3(1.0, -xy.y, -xy.x)
        : _CustomRenderTextureCubeFace == 1.0 ? float3(-1.0, -xy.y, xy.x)
        : _CustomRenderTextureCubeFace == 2.0 ? float3(xy.x, 1.0, xy.y)
        : _CustomRenderTextureCubeFace == 3.0 ? float3(xy.x, -1.0, -xy.y)
        : _CustomRenderTextureCubeFace == 4.0 ? float3(xy.x, -xy.y, 1.0)
        : float3(-xy.x, -xy.y, -1.0));
}


// standard custom texture vertex shader that should always be used
v2f_customrendertexture CustomRenderTextureVertexShaderEx(appdata_customrendertexture IN)
{
    v2f_customrendertexture OUT;

#if UNITY_UV_STARTS_AT_TOP
    const float2 vertexPositions[6] =
    {
        { -1.0f,  1.0f },
        { -1.0f, -1.0f },
        {  1.0f, -1.0f },
        {  1.0f,  1.0f },
        { -1.0f,  1.0f },
        {  1.0f, -1.0f }
    };

    const float2 texCoords[6] =
    {
        { 0.0f, 0.0f },
        { 0.0f, 1.0f },
        { 1.0f, 1.0f },
        { 1.0f, 0.0f },
        { 0.0f, 0.0f },
        { 1.0f, 1.0f }
    };
#else
    const float2 vertexPositions[6] =
    {
        {  1.0f,  1.0f },
        { -1.0f, -1.0f },
        { -1.0f,  1.0f },
        { -1.0f, -1.0f },
        {  1.0f,  1.0f },
        {  1.0f, -1.0f }
    };

    const float2 texCoords[6] =
    {
        { 1.0f, 1.0f },
        { 0.0f, 0.0f },
        { 0.0f, 1.0f },
        { 0.0f, 0.0f },
        { 1.0f, 1.0f },
        { 1.0f, 0.0f }
    };
#endif

    uint primitiveID = IN.vertexID / 6;
    uint vertexID = IN.vertexID % 6;
    float3 updateZoneCenter = CustomRenderTextureCenters[primitiveID].xyz;
    float3 updateZoneSize = CustomRenderTextureSizesAndRotations[primitiveID].xyz;
    float rotation = CustomRenderTextureSizesAndRotations[primitiveID].w * UNITY_PI / 180.0f;

#if !UNITY_UV_STARTS_AT_TOP
    rotation = -rotation;
#endif

    // Normalize rect if needed
    if (CustomRenderTextureUpdateSpace > 0.0) // Pixel space
    {
        // Normalize xy because we need it in clip space.
        updateZoneCenter.xy /= _CustomRenderTextureInfo.xy;
        updateZoneSize.xy /= _CustomRenderTextureInfo.xy;
    }
    else // normalized space
    {
        // Un-normalize depth because we need actual slice index for culling
        updateZoneCenter.z *= _CustomRenderTextureInfo.z;
        updateZoneSize.z *= _CustomRenderTextureInfo.z;
    }

    // Compute rotation

    // Compute quad vertex position
    float2 clipSpaceCenter = updateZoneCenter.xy * 2.0 - 1.0;
    float2 pos = vertexPositions[vertexID] * updateZoneSize.xy;
    pos = CustomRenderTextureRotate2D(pos, rotation);
    pos.x += clipSpaceCenter.x;
#if UNITY_UV_STARTS_AT_TOP
    pos.y += clipSpaceCenter.y;
#else
    pos.y -= clipSpaceCenter.y;
#endif

    // For 3D texture, cull quads outside of the update zone
    // This is neeeded in additional to the preliminary minSlice/maxSlice done on the CPU because update zones can be disjointed.
    // ie: slices [1..5] and [10..15] for two differents zones so we need to cull out slices 0 and [6..9]
    if (CustomRenderTextureIs3D > 0.0)
    {
        int minSlice = (int)(updateZoneCenter.z - updateZoneSize.z * 0.5);
        int maxSlice = minSlice + (int)updateZoneSize.z;
        if (_CustomRenderTexture3DSlice < minSlice || _CustomRenderTexture3DSlice >= maxSlice)
        {
            pos.xy = float2(1000.0, 1000.0); // Vertex outside of ncs
        }
    }

    OUT.vertex = float4(pos, 0.0, 1.0);
    OUT.primitiveID = asuint(CustomRenderTexturePrimitiveIDs[primitiveID]);
    OUT.localTexcoord = float3(texCoords[vertexID], CustomRenderTexture3DTexcoordW);
    OUT.globalTexcoord = float3(pos.xy * 0.5 + 0.5, CustomRenderTexture3DTexcoordW);
#if UNITY_UV_STARTS_AT_TOP
    OUT.globalTexcoord.y = 1.0 - OUT.globalTexcoord.y;
#endif
    OUT.direction = CustomRenderTextureComputeCubeDirectionEx(OUT.globalTexcoord.xy);

    return OUT;
}


#endif  // CUSTOM_TEXTURE_EX_INCLUDED

そして,カスタムレンダーテクスチャ用のシェーダーにおいて,

#include "UnityCustomRenderTexture.cginc"
#pragma vert CustomRenderTextureVertexShader

としている2行それぞれを

#include "CustomRenderTextureEx.cginc"
#pragma vert CustomRenderTextureVertexShaderEx

に置き換えるとよい.

まとめ

標準ライブラリであってもその実装を過信せずに改善を試みてもよいかもしれない.

参考文献

レイマーチングにおける正規化法線ベクトルの計算と実装

たまには趣向を変えて,ですます調で記事を書いてみようと思います.

レイマーチングではライティングを行うために正規化法線ベクトルの計算が必要となります. この記事ではいくつかの正規化法線ベクトルの計算手法について述べることにします.

なお,僕は普段Unityに触れている機会が多いため,本記事ではshaderlab(Cg/HLSL)をシェーダー言語として取り上げることにします. GLSLについては,記事中ではわずかに言及するのみですが,ShadertoyにGLSLでの実装コードを置いています

陰関数の法線

$\boldsymbol{p} = \begin{pmatrix} x & y & z \end{pmatrix}^T$ とします. これは3次元空間における座標の表現となるわけですね.

よく知られた事実として,陰関数 $f(\boldsymbol{p}) = 0$ の正規化法線ベクトル $\boldsymbol{n}(\boldsymbol{p})$ は以下のようになります.

\begin{eqnarray} \boldsymbol{n}(\boldsymbol{p}) & = & normalize(\nabla f(\boldsymbol{p})) \nonumber \\ & = & normalize \left( \begin{pmatrix} \dfrac{\partial f}{\partial x}(\boldsymbol{p}) & \dfrac{\partial f}{\partial y}(\boldsymbol{p}) & \dfrac{\partial f}{\partial z}(\boldsymbol{p}) \end{pmatrix}^T \right) \label{NormalizedNormal} \end{eqnarray}

レイマーチングの符号付き距離関数はまさしく陰関数であるため,式\eqref{NormalizedNormal}を基に正規化法線ベクトルを求めることができるわけです.

正規化法線ベクトルの計算と実装

本章でいくつかの正規化法線ベクトルの計算と実装を紹介します.

目次兼リンクを作ってみたのですが,大きくわけて4個,細かくわけて14個というなかなかの数となりました.

中心差分による法線計算

最初に中心差分による計算手法を紹介します. 中心差分による正規化法線ベクトルの計算は以下に基づいています. $h$ は微小な正の実数です.

\begin{eqnarray} \boldsymbol{n}(\boldsymbol{p}) & = normalize \left( \begin{pmatrix} \dfrac{f \left(\boldsymbol{p} + \begin{pmatrix} h & 0 & 0 \end{pmatrix}^T \right) - f \left(\boldsymbol{p} - \begin{pmatrix} h & 0 & 0 \end{pmatrix}^T \right)}{2h} \\ \dfrac{f \left(\boldsymbol{p} + \begin{pmatrix} 0 & h & 0 \end{pmatrix}^T \right) - f \left(\boldsymbol{p} - \begin{pmatrix} 0 & h & 0 \end{pmatrix}^T \right)}{2h} \\ \dfrac{f \left(\boldsymbol{p} + \begin{pmatrix} 0 & 0 & h \end{pmatrix}^T \right) - f \left(\boldsymbol{p} - \begin{pmatrix} 0 & 0 & h \end{pmatrix}^T \right)}{2h} \end{pmatrix} \right) \nonumber \\ & = normalize \left( \begin{pmatrix} f \left(\boldsymbol{p} + \begin{pmatrix} h & 0 & 0 \end{pmatrix}^T \right) - f \left(\boldsymbol{p} - \begin{pmatrix} h & 0 & 0 \end{pmatrix}^T \right) \\ f \left(\boldsymbol{p} + \begin{pmatrix} 0 & h & 0 \end{pmatrix}^T \right) - f \left(\boldsymbol{p} - \begin{pmatrix} 0 & h & 0 \end{pmatrix}^T \right) \\ f \left(\boldsymbol{p} + \begin{pmatrix} 0 & 0 & h \end{pmatrix}^T \right) - f \left(\boldsymbol{p} - \begin{pmatrix} 0 & 0 & h \end{pmatrix}^T \right) \end{pmatrix} \right) \label{CentralDifference} \end{eqnarray}

正規化を行うので定数倍の項である $\dfrac{1}{2h}$ は除外できます. これは明らかな事実で,任意のベクトル $\boldsymbol{v}$ と $a$ ($a \neq 0$)の乗算を考えると,正規化について下記のようになるからですね.

\begin{equation} normalize(a \boldsymbol{v}) = \dfrac{a \boldsymbol{v}}{\| a \boldsymbol{v} \|} = \dfrac{a \boldsymbol{v}}{a \| \boldsymbol{v} \|} = \dfrac{\boldsymbol{v}}{\| \boldsymbol{v} \|} = normalize(\boldsymbol{v}) \end{equation}

中心差分

式\eqref{CentralDifference}をそのままシェーダーのレイマーチングの正規化法線ベクトルの計算関数に落とし込むと以下のようになります. 数式をそのままコードにしましたといえるものになっていますね.

/*!
 * @brief Calculate normal of the objects using central differences.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal01(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float2 d = float2(h, 0.0);

    return normalize(
        float3(
            map(p + d.xyy) - map(p - d.xyy),
            map(p + d.yxy) - map(p - d.yxy),
            map(p + d.yyx) - map(p - d.yyx)));
}

上記から生成されるDirect3D11用のアセンブリコードのうち,上記の部分のみを抜粋すると下記の通りです. map() 関数は後述する半径0.5の球を描画するだけのコードとしています. 同じような処理が6回並んでいるのが確認できると思います. ループで書いていないので勝手にループ処理にされるわけはないですが,ここが map() の呼び出し6回分に相当しています.

  30: add r1.xyz, r1.xyzx, cb3[3].xyzx
  31: add r2.xyz, r0.xyzx, l(0.000100, 0.000000, 0.000000, 0.000000)
  32: dp3 r0.w, r2.xyzx, r2.xyzx
  33: sqrt r0.w, r0.w
  34: add r0.w, r0.w, l(-0.500000)
  35: add r2.xyz, r0.xyzx, l(-0.000100, -0.000000, -0.000000, 0.000000)
  36: dp3 r1.w, r2.xyzx, r2.xyzx
  37: sqrt r1.w, r1.w
  38: add r1.w, r1.w, l(-0.500000)
  39: add r2.x, r0.w, -r1.w
  40: add r3.xyz, r0.xyzx, l(0.000000, 0.000100, 0.000000, 0.000000)
  41: dp3 r0.w, r3.xyzx, r3.xyzx
  42: sqrt r0.w, r0.w
  43: add r0.w, r0.w, l(-0.500000)
  44: add r3.xyz, r0.xyzx, l(-0.000000, -0.000100, -0.000000, 0.000000)
  45: dp3 r1.w, r3.xyzx, r3.xyzx
  46: sqrt r1.w, r1.w
  47: add r1.w, r1.w, l(-0.500000)
  48: add r2.y, r0.w, -r1.w
  49: add r3.xyz, r0.xyzx, l(0.000000, 0.000000, 0.000100, 0.000000)
  50: dp3 r0.w, r3.xyzx, r3.xyzx
  51: sqrt r0.w, r0.w
  52: add r0.xyzw, r0.xyzw, l(-0.000000, -0.000000, -0.000100, -0.500000)
  53: dp3 r0.x, r0.xyzx, r0.xyzx
  54: sqrt r0.x, r0.x
  55: add r0.x, r0.x, l(-0.500000)
  56: add r2.z, -r0.x, r0.w
  57: dp3 r0.x, r2.xyzx, r2.xyzx
  58: rsq r0.x, r0.x
  59: mul r0.xyz, r0.xxxx, r2.xyzx

このコードにおける map() は符号付き距離関数を統合する関数です. 符号付き距離関数そのものと考えてもよいです.

例えば単純な球を描画するなら以下のようになります. 先程の出力アセンブリコード例では map() を下記の実装にしていました.

/*!
 * @brief SDF of Sphere.
 * @param [in] p  Position of the tip of the ray.
 * @param [in] r  Radius of the sphere.
 * @return Signed Distance to the Sphere.
 */
float3 sdSphere(float3 p, float r)
{
    return length(p) - r;
}

/*!
 * @brief SDF integration function.
 * @param [in] p  Position of the tip of the ray.
 * @return Signed Distance to the one of object.
 */
float3 map(float3 p)
{
    reutrn sdSphere(p, 0.5);
}

発展させて,2つの球を合体させるなら下記のようになります.

/*!
 * @brief SDF of Sphere.
 * @param [in] r  Radius of the sphere.
 * @return Signed Distance to the Sphere.
 */
float3 sdSphere(float3 p, float r)
{
    return length(p) - r;
}

/*!
 * @brief SDF integration function.
 * @param [in] p  Position of the tip of the ray.
 * @return Signed Distance to the one of object.
 */
float3 map(float3 p)
{
    reutrn min(
        sdSphere(p - float3(0.25, 0.0, 0.0), 0.5),
        sdSphere(p + float3(0.25, 0.0, 0.0), 0.5));
}

中心差分(ループ)

以下のように6つのベクトルを用意します.

\begin{equation} \begin{cases} \boldsymbol{k}_0 & = & \begin{pmatrix}1 & 0 & 0\end{pmatrix}^T \\ \boldsymbol{k}_1 & = & \begin{pmatrix}-1 & 0 & 0\end{pmatrix}^T \\ \boldsymbol{k}_2 & = & \begin{pmatrix}0 & 1 & 0\end{pmatrix}^T \\ \boldsymbol{k}_3 & = & \begin{pmatrix}0 & -1 & 0\end{pmatrix}^T \\ \boldsymbol{k}_4 & = & \begin{pmatrix}0 & 0 & 1\end{pmatrix}^T \\ \boldsymbol{k}_5 & = & \begin{pmatrix}0 & 0 & -1\end{pmatrix}^T \end{cases} \label{SelectorVector01} \end{equation}

これを用いると,式\eqref{CentralDifference}は以下のように総和の形で記述し直すことができます.

\begin{equation} \boldsymbol{n}(\boldsymbol{p}) = normalize \left( \sum_{i} \boldsymbol{k}_i f(\boldsymbol{p} + h \boldsymbol{k}_i) \right) \label{CentralDifferenceSum} \end{equation}

$\boldsymbol{k}_i$ は各次元ごとに独立しており,正負の符号を与えるものになっています. そのため,このベクトル,定数倍のベクトルの和はどれか1つの次元のみの加算になるわけなので,式\eqref{CentralDifference}を総和の形に変形できたわけですね.

さて,式\eqref{CentralDifferenceSum}はシェーダープログラムとしてはループを用いて記述でき,以下のようになります.

/*!
 * @brief Calculate normal of the objects using central differences with loop.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal01Loop(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float3 s = float3(1.0, -1.0, 0.0);  // used only for generating k.
    static const float3 k[6] = {s.xzz, s.yzz, s.zxz, s.zyz, s.zzx, s.zzy};

    float3 normal = float3(0.0, 0.0, 0.0);

    UNITY_LOOP
    for (int i = 0; i < 6; i++) {
        normal += k[i] * map(p + h * k[i]);
    }

    return normalize(normal);
}

変数 s というのはsignの意味で s という名前にしています. 都合上, 0も必要なので,第3要素を 0.0 にしています. どの位置にどの値を入れるかは迷いどころでしたが,0を第3要素としているとswizzle演算子を用いたときに, s.z と書くことができ,zからZeroを連想することができるからこの順にしようと考えた次第です.

UNITY_LOOP を指定していますが,これは環境差吸収用のマクロであり,例えば [loop] に展開されます. この指示があるとコンパイラは積極的にループ命令を生成してくれるようになります.

  31: mov r2.xyz, l(0,0,0,0)
  32: mov r0.w, l(0)
  33: loop 
  34:   ige r1.w, r0.w, l(6)
  35:   breakc_nz r1.w
  36:   mad r3.xyz, icb[r0.w + 0].xyzx, l(0.000100, 0.000100, 0.000100, 0.000000), r0.xyzx
  37:   dp3 r1.w, r3.xyzx, r3.xyzx
  38:   sqrt r1.w, r1.w
  39:   add r1.w, r1.w, l(-0.500000)
  40:   mad r2.xyz, icb[r0.w + 0].xyzx, r1.wwww, r2.xyzx
  41:   iadd r0.w, r0.w, l(1)
  42: endloop 
  43: dp3 r0.x, r2.xyzx, r2.xyzx
  44: rsq r0.x, r0.x
  45: mul r0.xyz, r0.xxxx, r2.xyzx

逆にUNITY_UNROLL[unroll]) を指定するとコンパイラは積極的にループ展開を行ってくれるようになります. この場合に生成されるコードは 中心差分 のものとほぼ同一になります.

  30: add r1.xyz, r1.xyzx, cb3[3].xyzx
  31: add r2.xyz, r0.xyzx, l(0.000100, 0.000000, 0.000000, 0.000000)
  32: dp3 r0.w, r2.xyzx, r2.xyzx
  33: sqrt r0.w, r0.w
  34: add r0.w, r0.w, l(-0.500000)
  35: add r2.xyz, r0.xyzx, l(-0.000100, 0.000000, 0.000000, 0.000000)
  36: dp3 r1.w, r2.xyzx, r2.xyzx
  37: sqrt r1.w, r1.w
  38: add r1.w, r1.w, l(-0.500000)
  39: mul r2.xyz, r1.wwww, l(-1.000000, 0.000000, 0.000000, 0.000000)
  40: mad r2.xyz, r0.wwww, l(1.000000, 0.000000, 0.000000, 0.000000), r2.xyzx
  41: add r3.xyz, r0.xyzx, l(0.000000, 0.000100, 0.000000, 0.000000)
  42: dp3 r0.w, r3.xyzx, r3.xyzx
  43: sqrt r0.w, r0.w
  44: add r0.w, r0.w, l(-0.500000)
  45: mad r2.xyz, r0.wwww, l(0.000000, 1.000000, 0.000000, 0.000000), r2.xyzx
  46: add r3.xyz, r0.xyzx, l(0.000000, -0.000100, 0.000000, 0.000000)
  47: dp3 r0.w, r3.xyzx, r3.xyzx
  48: sqrt r0.w, r0.w
  49: add r0.w, r0.w, l(-0.500000)
  50: mad r2.xyz, r0.wwww, l(0.000000, -1.000000, 0.000000, 0.000000), r2.xyzx
  51: add r3.xyz, r0.xyzx, l(0.000000, 0.000000, 0.000100, 0.000000)
  52: dp3 r0.w, r3.xyzx, r3.xyzx
  53: sqrt r0.w, r0.w
  54: add r0.xyzw, r0.xyzw, l(0.000000, 0.000000, -0.000100, -0.500000)
  55: mad r2.xyz, r0.wwww, l(0.000000, 0.000000, 1.000000, 0.000000), r2.xyzx
  56: dp3 r0.x, r0.xyzx, r0.xyzx
  57: sqrt r0.x, r0.x
  58: add r0.x, r0.x, l(-0.500000)
  59: mad r0.xyz, r0.xxxx, l(0.000000, 0.000000, -1.000000, 0.000000), r2.xyzx
  60: dp3 r0.w, r0.xyzx, r0.xyzx
  61: rsq r0.w, r0.w
  62: mul r0.xyz, r0.wwww, r0.xyzx

何も指示しなかった場合, map() のコードサイズに応じてループ展開を行うかどうかをコンパイラが決定してくれます. 単純なスフィアトレーシングであればループ展開され,処理の多い複雑な map() だとループ命令が生成されることは確認しました.

わざわざループにするのはプログラムの高速化の考えに反しますが,一般的に map() は長大な関数になりがちであるため,ループにすることで出力コードサイズの削減になります. また,どの部分が法線計算であるか明確になるため,出力コードが読みやすくなるというメリットもあります. そもそもマーチングループの中で map() を呼び出しているため,法線計算のための数回の呼び出しをループにすることはそこまでの抵抗を感じるものではないかなと思います.

ところで,下記のように hk の積のルックアップテーブルを作成する実装も考えることができます.

/*!
 * @brief Calculate normal of the objects using central differences with loop.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal01LoopEx(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float3 s = float3(1.0, -1.0, 0.0);  // used only for generating hs, k and hk.
    static const float3 hs = h * s;  // used only for generating hk.
    static const float3 k[6] = {s.xzz, s.yzz, s.zxz, s.zyz, s.zzx, s.zzy};
    static const float3 hk[6] = {hs.xzz, hs.yzz, hs.zxz, hs.zyz, hs.zzx, hs.zzy};

    float3 normal = float3(0.0, 0.0, 0.0);

    UNITY_LOOP
    for (int i = 0; i < 6; i++) {
        normal += k[i] * map(p + hk[i]);
    }

    return normalize(normal);
}

しかし,p + h * k[i] のような積和の計算は単一のmad命令となるため,p + hk[i] のような単一の加算命令と比較しても速度的な差はないでしょう.

中心差分(LUT無しループ)

ルックアップテーブルを用いた形のコードを提示しましたが,ルックアップテーブル用のConstant Bufferが作成されることになります. その領域を削りたい場合はループインデックス $i$ から係数ベクトル $\boldsymbol{k}_i$ を作るとよいでしょう.

/*!
 * @brief Calculate normal of the objects using central differences with loop without Look Up Table.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal01LoopNoLut(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value

    float3 normal = float3(0.0, 0.0, 0.0);

    UNITY_LOOP
    for (int i = 0; i < 6; i++) {
        const int j = i >> 1;
        const float4 v = float4(int4((int3(j + 3, i, j) >> 1), i) & 1);
        const float3 k = v.xyz * (v.w * 2.0 - 1.0);
        normal += k * map(p + h * k);
    }

    return normalize(normal);
}

float3 6つ分のConstant Bufferを削りたいモチベーションはそこまでなく,このコードは単に興味の一環で作成したというものになります. もちろん k の計算を行う分,ループ内の命令数がいくつか増えることになります. DirectX11向けのアセンブリコードの当該箇所を抜粋すると下記の通りで,8命令になっていることがわかります(あらかじめ i >> 1 の一時変数を用意したり,整数ベクトルにまとめてビットシフトとビット論理積を行ったり,mad命令が生成されるように浮動小数点ベクトルに変換してから * 2.0 - 1.0 を行って命令数が少なくなるようにしています).

  36:   ishr r3.y, r0.w, l(1)
  37:   iadd r3.x, r3.y, l(3)
  38:   mov r3.w, r0.w
  39:   ishr r3.xyz, r3.xwyx, l(1)
  40:   and r3.xyzw, r3.xyzw, l(1, 1, 1, 1)
  41:   itof r3.xyzw, r3.xyzw
  42:   mad r1.w, r3.w, l(2.000000), l(-1.000000)
  43:   mul r3.xyz, r1.wwww, r3.xyzx

やや複雑なビット演算を行っているので,テクニカルなコード書ける俺スゲー感を得たい場合にはよいかもしれませんね.

中心差分(2重ループ)

最初に言っておくと,これは採用する理由のない計算手法になります.

式\eqref{SelectorVector01}を眺めてみると,

\begin{equation} \begin{cases} \boldsymbol{k}_0 & = & -\boldsymbol{k}_1 \\ \boldsymbol{k}_2 & = & -\boldsymbol{k}_3 \\ \boldsymbol{k}_4 & = & -\boldsymbol{k}_5 \end{cases} \end{equation}

となっていることがわかります. そこで,

\begin{equation} \begin{cases} \boldsymbol{k}'_0 & = & \begin{pmatrix}1 & 0 & 0\end{pmatrix}^T \\ \boldsymbol{k}'_1 & = & \begin{pmatrix}0 & 1 & 0\end{pmatrix}^T \\ \boldsymbol{k}'_2 & = & \begin{pmatrix}0 & 0 & 1\end{pmatrix}^T \end{cases} \label{SelectorVector02} \end{equation}

\begin{equation} \begin{cases} s_0 & = & 1 \\ s_1 & = & -1 \end{cases} \end{equation}

とおけば,式\eqref{CentralDifferenceSum}は以下のように2重の総和の形になります.

\begin{equation} \boldsymbol{n}(\boldsymbol{p}) = normalize \left( \sum_{i} \sum_{j} s_j \boldsymbol{k}'_i f(\boldsymbol{p} + h s_j \boldsymbol{k}'_i) \right) \end{equation}

プログラムとしては,以下のような2重ループの形になります.

/*!
 * @brief Calculate normal of the objects using central differences with double loop.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal01LoopW(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float3 s = float3(1.0, -1.0, 0.0);
    static const float3 k[3] = {s.xzz, s.zxz, s.zzx};

    float3 normal = float3(0.0, 0.0, 0.0);

    UNITY_LOOP
    for (int i = 0; i < 3; i++) {
        UNITY_LOOP
        for (int j = 0; j < 2; j++) {
            const float3 sk = s[j] * k[i];
            normal += sk * map(p + h * sk);
        }
    }

    return normalize(normal);
}

しかし,これは $s_j$ と $\boldsymbol{k}'_i$ の積を計算する必要があったり,二重ループのためループ指示を2箇所に書く必要があるなど,採用する理由がない計算手法となります.

前方差分による法線計算

中心差分による関数 $f$ すなわち,map() の評価回数は6回でした. レイマーチングにおいて map() は大部分を占める関数であり,基本的に高コストな関数であるため,極力呼び出し回数は減らしたいものです.

そこで,計算誤差の増加と引き換えに,前方差分により法線計算を行うことを考えてみます.

\begin{eqnarray} \boldsymbol{n}(\boldsymbol{p}) & = normalize \left( \begin{pmatrix} \dfrac{f \left(\boldsymbol{p} + \begin{pmatrix} h & 0 & 0 \end{pmatrix}^T \right) - f \left(\boldsymbol{p} \right)}{2h} \\ \dfrac{f \left(\boldsymbol{p} + \begin{pmatrix} 0 & h & 0 \end{pmatrix}^T \right) - f \left(\boldsymbol{p} \right)}{2h} \\ \dfrac{f \left(\boldsymbol{p} + \begin{pmatrix} 0 & 0 & h \end{pmatrix}^T \right) - f \left(\boldsymbol{p} \right)}{2h} \end{pmatrix} \right) \nonumber \\ & = normalize \left( \begin{pmatrix} f \left(\boldsymbol{p} + \begin{pmatrix} h & 0 & 0 \end{pmatrix}^T \right) - f \left(\boldsymbol{p} \right) \\ f \left(\boldsymbol{p} + \begin{pmatrix} 0 & h & 0 \end{pmatrix}^T \right) - f \left(\boldsymbol{p} \right) \\ f \left(\boldsymbol{p} + \begin{pmatrix} 0 & 0 & h \end{pmatrix}^T \right) - f \left(\boldsymbol{p} \right) \end{pmatrix} \right) \label{ForwardDifference} \end{eqnarray}

$f(p)$ の値を保持すれば,関数 $f$ の評価回数は4回となりますね.

前方差分

式\eqref{ForwardDifference}に従い,愚直に実装すると以下のようになります. 数式通りのコードですね.

/*!
 * @brief Calculate normal of the objects using forward differences.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal02(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float2 d = float2(h, 0.0);

    const float mp = map(p);

    return normalize(
        float3(
            map(p + d.xyy) - mp,
            map(p + d.yxy) - mp,
            map(p + d.yyx) - mp));
}

前方差分(ループ)

式\eqref{ForwardDifference}に対し,式\eqref{SelectorVector02}の $\boldsymbol{k}'_i$ を用いると,以下のようになります.

\begin{eqnarray} \boldsymbol{n}(\boldsymbol{p}) & = & normalize \left( \sum_{i} \boldsymbol{k}'_i \left\{ f(\boldsymbol{p} + h \boldsymbol{k}'_i) - f(\boldsymbol{p}) \right\} \right) \nonumber \\ & = & normalize \left( \sum_{i} \boldsymbol{k}'_i f(\boldsymbol{p} + h \boldsymbol{k}'_i) - \sum_{i} \boldsymbol{k}'_i f(\boldsymbol{p}) \right) \nonumber \\ & = & normalize \left( \sum_{i} \boldsymbol{k}'_i f(\boldsymbol{p} + h \boldsymbol{k}'_i) - \begin{pmatrix} f(\boldsymbol{p}) & f(\boldsymbol{p}) & f(\boldsymbol{p}) \end{pmatrix}^T \right) \end{eqnarray}

/*!
 * @brief Calculate normal of the objects using forward differences with loop.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal02Loop(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float3 s = float3(1.0, -1.0, 0.0);  // used only for generating k.
    static const float3 k[3] = {s.xzz, s.zxz, s.zzx};

    float3 normal = (-map(p)).xxx;

    UNITY_LOOP
    for (int i = 0; i < 3; i++) {
        normal += k[i] * map(p + h * k[i]);
    }

    return normalize(normal);
}

shaderlab(Cg/HLSL)ではスカラーに対してswizzle演算子を用いることができるため,

const float nmp = -map(p);
float3 normal = float3(nmp, nmp, nmp);

と書かなくても

float3 normal = (-map(p)).xxx;

のように1行で記述できるのが楽でよいですね.

GLSLではスカラーに対してswizzle演算子を用いることができませんが,ベクトルの生成に1要素のみを指定することで,全要素が同じ値のベクトルを作ることができるので,これも便利な文法ですね.

vec3 normal = vec3(-map(p));

前方差分(LUT無しループ)

これもループインデックス $i$ から係数ベクトル $\boldsymbol{k}'_i$ を作ることができます.

/*!
 * @brief Calculate normal of the objects using forward differences with loop without Look Up Table.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal02LoopNoLut(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value

    float3 normal = (-map(p)).xxx;

    UNITY_LOOP
    for (int i = 0; i < 3; i++) {
        const float3 k = float3(int3((i + 3) >> 1, i, i >> 1) & 1);
        normal += k * map(p + h * k);
    }

    return normalize(normal);
}

前方差分(NGループ)

この計算手法も採用する理由がないものとなります.

前方差分(ループ)ではループの形にしたものの, map(p) の計算をループ外に置かざるをえず,コンパイラがループを生成したとしても,map(p) の呼び出し箇所がループの外と中の2箇所となり,コードサイズ面で中心差分(ループ)より不利になります.

ここで思考実験として, map(p) の計算を無理矢理ループ内に納めることを考えてみましょう. 以下の6つの係数を設けると,

\begin{equation} \begin{cases} z_0 & = & 1 \\ z_1 & = & 0 \\ z_2 & = & 1 \\ z_3 & = & 0 \\ z_4 & = & 1 \\ z_5 & = & 0 \end{cases} \end{equation}

式\eqref{SelectorVector01}の $\boldsymbol{k}_i$ と併用し,以下のように正規化法線ベクトルを計算することができます.

\begin{equation} \boldsymbol{n}(\boldsymbol{p}) = normalize \left( \sum_{i} \boldsymbol{k}_i f(\boldsymbol{p} + h z_i \boldsymbol{k}_i) \right) \end{equation}

これをシェーダープログラムにすると以下のようになります. しかし,これでは map() の呼び出し回数が6回となり,わざわざ精度を犠牲にしてまで前方差分を採用した意味がなくなってしまいますね.

/*!
 * @brief Calculate normal of the objects using forward differences with loop.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal02LoopNG(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float3 s = float3(1.0, -1.0, 0.0);  // used only for generating k.
    static const float3 k[6] = {s.xzz, s.yzz, s.zxz, s.zyz, s.zzx, s.zzy};
    static const float z[6] = {1.0, 0.0, 1.0, 0.0, 1.0, 0.0};

    float3 normal = float3(0.0, 0.0, 0.0);

    UNITY_LOOP
    for (int i = 0; i < 6; i++) {
        normal += k[i] * map(p + h * z[i] * k[i]);
    }

    return normalize(normal);
}

前方差分による法線計算(f(p)無視)

レイマーチングにおいて, $f(\boldsymbol{p}) \approx 0$ となるように $\boldsymbol{p}$ を求めていました. そこで大胆にも式\eqref{ForwardDifference}において,$f(\boldsymbol{p}) = 0$ とおき,法線を計算するという考えがあるようです.

\begin{equation} \boldsymbol{n}(\boldsymbol{p}) = normalize \left( \begin{pmatrix} f \left(\boldsymbol{p} + \begin{pmatrix} h & 0 & 0 \end{pmatrix}^T \right) \\ f \left(\boldsymbol{p} + \begin{pmatrix} 0 & h & 0 \end{pmatrix}^T \right) \\ f \left(\boldsymbol{p} + \begin{pmatrix} 0 & 0 & h \end{pmatrix}^T \right) \end{pmatrix} \right) \end{equation}

実際に実装してみるとわかるのですが,かなり精度が悪く,描画結果が残念なものになりました. レイマーチングの打ち切り条件をかなり厳しくしないと良い描画結果が得られないのではないでしょうか?

前方差分(f(p)無視)

map(p) の減算が無いため,コードはかなりシンプルになります.

/*!
 * @brief Calculate normal of the objects ignoring f(p).
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal03(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float2 d = float2(h, 0.0);

    return normalize(
        float3(
            map(p + d.xyy),
            map(p + d.yxy),
            map(p + d.yyx)));
}

前方差分(f(p)無視)(ループ)

前方差分(ループ)と同様です.

/*!
 * @brief Calculate normal of the objects ignoring f(p)
 * and using forward differences with loop.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal03Loop(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float3 s = float3(1.0, -1.0, 0.0);  // used only for generating k.
    static const float3 k[3] = {s.xzz, s.zxz, s.zzx};

    float3 normal = float3(0.0, 0.0, 0.0);

    UNITY_LOOP
    for (int i = 0; i < 3; i++) {
        normal += k[i] * map(p + h * k[i]);
    }

    return normalize(normal);
}

前方差分(f(p)無視)(LUT無しループ)

前方差分(LUT無しループ)と同様です.

/*!
 * @brief Calculate normal of the objects ignoring f(p)
 * and using forward differences with loop without Look Up Table.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal03LoopNoLut(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value

    float3 normal = float3(0.0, 0.0, 0.0);

    UNITY_LOOP
    for (int i = 0; i < 3; i++) {
        const float3 k = float3(int3((i + 3) >> 1, i, i >> 1) & 1);
        normal += k * map(p + h * k);
    }

    return normalize(normal);
}

Tetrahedron Technique による法線計算

Tetrahedron TechniqueとはInigo Quilez氏の記事で紹介されている正規化法線ベクトルの計算手法の1つです. 割と有名な手法かもしれませんね.

まず,以下のように4つのベクトルを定めます.

\begin{equation} \begin{cases} \boldsymbol{k}''_0 & = & \begin{pmatrix}1 & -1 & -1\end{pmatrix}^T \\ \boldsymbol{k}''_1 & = & \begin{pmatrix}-1 & -1 & 1\end{pmatrix}^T \\ \boldsymbol{k}''_2 & = & \begin{pmatrix}-1 & 1 & -1\end{pmatrix}^T \\ \boldsymbol{k}''_3 & = & \begin{pmatrix}1 & 1 & 1\end{pmatrix}^T \end{cases} \label{SelectorVector03} \end{equation}

すると,正規化法線ベクトルは以下のように計算できます.

\begin{equation} \boldsymbol{n}(\boldsymbol{p}) = normalize \left( \sum_i \boldsymbol{k}''_i f(\boldsymbol{p} + h \boldsymbol{k}''_i) \right) \label{TetrahedronTechnique} \end{equation}

導出過程については,Inigo Quilez氏の記事を参照してください. $\sum_i \boldsymbol{k}''_i f(\boldsymbol{p}) = \boldsymbol{0}$であることと,方向微分の公式を用いてうまく式変形しているので,感銘を受けるものがあります.

式\eqref{CentralDifferenceSum}と式\eqref{TetrahedronTechnique}が同じ形であること,また,式\eqref{SelectorVector01}の$\boldsymbol{k}_i$について

\begin{equation} \sum_i \boldsymbol{k}_i = \boldsymbol{0} \end{equation}

そして,式\eqref{SelectorVector03}の$\boldsymbol{k}''_i$についても

\begin{equation} \sum_i \boldsymbol{k}''_i = \boldsymbol{0} \end{equation}

となることから,式\eqref{TetrahedronTechnique}のように正規化法線ベクトルが計算できることは自然なことであるように直感的に感じることができます.

Tetrahedron Technique

数式の通りにコードを書くと以下のようになります. ライブコーディング等でゼロから記述する際に,提案した手法の中では最も楽に記述できるコードかつ美しいのではないだろうかと個人的には思います.

/*!
 * @brief Calculate normal of the objects using tetrahedron techniue.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal04(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float2 s = float2(1.0, -1.0);
    static const float2 hs = h * s;

    return normalize(
        s.xyy * map(p + hs.xyy)
            + s.yxy * map(p + hs.yxy)
            + s.yyx * map(p + hs.yyx)
            + map(p + hs.xxx).xxx);
}

数式通りなら,map(p + hs.xxx).xxx の部分は s.xxx * map(p + hs.xxx) ですが,s.xxx は全要素 1.0 のベクトルなので,この2つは同一の結果になるわけです. まぁ,ベクトルに対するスカラーの加算は,スカラー側が全要素同じ値のベクトルとして扱われるため,単に map(p + hs.xxx) と書いてもよかったのですが,数学的にはベクトルとスカラーの乗算はあってもベクトルとスカラーの加算はなく,直感的ではないということで前述の表現を用いることにしました.

Tetrahedron Technique(ループ)

式\eqref{TetrahedronTechnique}が総和の形なので,それをそのまま for を用いたプログラムにしてみます.

/*!
 * @brief Calculate normal of the objects using tetrahedron techniue with loop.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal04Loop(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value
    static const float2 s = float2(1.0, -1.0);  // used only for generating k.
    static const float3 k[4] = {s.xyy, s.yxy, s.yyx, s.xxx};

    float3 normal = float3(0.0, 0.0, 0.0);

    UNITY_LOOP
    for (int i = 0; i < 4; i++) {
        normal += k[i] * map(p + h * k[i]);
    }

    return normalize(normal);
}

Tetrahedron Technique(LUT無しループ)

これも $i$ から $\boldsymbol{k}''_i$ を生成することができ,ルックアップテーブルが不要となります.

/*!
 * @brief Calculate normal of the objects using tetrahedron techniue with loop without Look Up Table.
 * @param [in] p  Position of the tip of the ray.
 * @return Normal of the objects.
 */
float3 getNormal04LoopNoLut(float3 p)
{
    static const float h = 0.0001;  // replace by an appropriate value

    float3 normal = float3(0.0, 0.0, 0.0);

    UNITY_LOOP
    for (int i = 0; i < 4; i++) {
        const float3 k = float3(int3((i + 3) >> 1, i, i >> 1) & 1) * 2.0 - 1.0;
        normal += k * map(p + h * k);
    }

    return normalize(normal);
}

コード中の int3 は要素が 01 のベクトルです. これに2を掛け,1を減ずることで, -11 のベクトルになります.

各手法の長所と短所

各手法の長所と短所をまとめると以下の通りとなります.

手法 SDF評価回数 ループ利用時SDF呼び出し箇所数 誤差
中心差分 6 1
前方差分 4 2
前方差分(f(p)無視) 3 1
Tetrahedron Technique 4 1 おそらく前方差分より小

総合的に見て,一番バランスが良いのがTetrahedron Techniqueだと思います.

まとめ

本記事では下記3つの手法それぞれの正規化法線ベクトルの計算手法を示しました.

  • 中心差分
  • 前方差分
  • Tetrahedron Technique

またそれぞれの手法についてループを用いた実装も示しました. ループを用いた実装にする利点としては以下の通りです.

  • 生成コードサイズが小さくなる
    • 生成コードの可読性が向上する
  • UNITY_UNROLL[unroll]) を指定することで容易にループ展開可能

おまけ

本記事ではshaderlab(Cg/HLSL)での実装例を示しましたが,ShadertoyにGLSLでの実装例も置いてあります. GLSLの実装コードが欲しい場合は見てください.

ただし,ルックアップテーブルを作成する際の配列の宣言と同時に初期化を行っている部分と,ルックアップテーブルを使用しない場合の係数ベクトル作成における $i$ のビット演算はGLES 3.0以降でなければ利用できないため注意が必要です.

配列については宣言と同時に初期化しなければ利用可能なので,例えば

const vec3 k[4] = vec3[](s.xyy, s.yxy, s.yyx, s.xxx);

vec3 k[4];
k[0] = s.xyy;
k[1] = s.yxy;
k[2] = s.yyx;
k[3] = s.xxx;

とすれば,古いGLSLでもコンパイルが通るコードとなります.

また,Inigo Quilez氏の記事の最後では,GLSLでループ命令を生成するためのHackが紹介されています. GLSLでは [loop] のようなコンパイラへの指示がないため,あえてループ命令を生成しようとした場合,uniform変数に依存した定数ではないゼロを作る等の工夫が必要になるようです.

記事中のコードを本記事に合わせた関数名・変数名に変更すると以下のようになります. 謎に $0.5773$ を掛けている部分がありますが, $\dfrac{1}{\sqrt{3}} \approx 0.5773$ なので, k の正規化を図っているのでしょう.

vec3 getNormal(vec3 p)
{
    const float h = 0.0001;  // replace by an appropriate value
#define ZERO (min(iFrame, 0))  // non-constant zero

    vec3 normal = vec3(0.0);
    for (int i = ZERO; i < 4; i++) {
        vec3 k = 0.5773 * (2.0 * vec3(((i + 3) >> 1) & 1, (i >> 1) & 1, i & 1) - 1.0);
        normal += k * map(pos + h * k);
    }

    return normalize(normal);
}

このようなHackが必要になるのは,特定のプラットフォーム(特にWebGL)におけるシェーダーコンパイラmap() が長大である場合,利用可能な命令数を超過し,クラッシュすることがあるためのようです.

参考文献

Unityのシェーダーにおいて,定数の整数乗を行う場合の繰り返し二乗法はループ展開されるか?

TL;DR

繰り返し二乗法の関数を定義し,指数に定数を指定した場合はループ展開される. しかし,pow(x, 5.0) のように指数が比較的小さい整数定数を指定した場合でも繰り返し二乗法をループ展開したものと同一のコード生成がされるため,自前で繰り返し二乗法の関数を設ける必要はないかもしれない.

前置き

Unityのシェーダーの標準ライブラリのコードを眺めていて,以下のようなものを見つけた.

  • UnityStandardBRDF.cginc
inline half Pow4 (half x)
{
    return x*x*x*x;
}

inline float2 Pow4 (float2 x)
{
    return x*x*x*x;
}

inline half3 Pow4 (half3 x)
{
    return x*x*x*x;
}

inline half4 Pow4 (half4 x)
{
    return x*x*x*x;
}

// Pow5 uses the same amount of instructions as generic pow(), but has 2 advantages:
// 1) better instruction pipelining
// 2) no need to worry about NaNs
inline half Pow5 (half x)
{
    return x*x * x*x * x;
}

inline half2 Pow5 (half2 x)
{
    return x*x * x*x * x;
}

inline half3 Pow5 (half3 x)
{
    return x*x * x*x * x;
}

inline half4 Pow5 (half4 x)
{
    return x*x * x*x * x;
}

pow() 関数による累乗の計算は整数ではない指数による累乗にも対応している. しかし,実装としては log2()exp2() を利用したものとなるため,Pow5() のコメントにあるように,命令数は同じでも(乗算3回か log, mul, exp の3命令)計算負荷は単純な乗算の方が軽いはずである.

float3 pow(float3 x, float3 y)

    return exp2(log2(x) * y);
}

4乗と5乗で同じように処理を記述しているのはコード量が膨れ上がると思った. また,2乗や3乗のバリエーションも欲しい.

指数が非負整数の累乗は繰り返し二乗法で効率的に計算可能であることはよく知られている. 以下のように繰り返し二乗法の関数を定義し,累乗部分に定数を指定することで,良い感じにループ展開されるかどうかを調べた. (すなわち,Pow4()Pow5() の出力アセンブリと一致,または同等のものになるかどうか調べた)

inline float pown(float x, int n)
{
    float v = 1.0;
    UNITY_UNROLL
    for (; n > 0; n >>= 1) {
        v *= (n & 1) == 0 ? 1.0 : x;
        x *= x;
    }
    return v;
}

inline float2 pown(float2 x, int n)
{
    static const float2 ones = float2(1.0, 1.0);

    float2 v = ones;
    UNITY_UNROLL
    for (; n > 0; n >>= 1) {
        v *= (n & 1) == 0 ? ones : x;
        x *= x;
    }
    return v;
}

inline float3 pown(float3 x, int n)
{
    static const float3 ones = float3(1.0, 1.0, 1.0);

    float3 v = ones;
    UNITY_UNROLL
    for (; n > 0; n >>= 1) {
        v *= (n & 1) == 0 ? ones : x;
        x *= x;
    }
    return v;
}

inline float4 pown(float4 x, int n)
{
    static const float4 ones = float4(1.0, 1.0, 1.0, 1.0);

    float4 v = ones;
    UNITY_UNROLL
    for (; n > 0; n >>= 1) {
        v *= (n & 1) == 0 ? ones : x;
        x *= x;
    }
    return v;
}

繰り返し二乗法

繰り返し二乗法の再帰的な定義は下記の通り(簡単のため $n$ は非負整数とする).

\begin{equation} x^{n} = \begin{cases} x (x^{\frac{n - 1}{2}})^2 & \text{where} ~ n \equiv 1 \pmod 2 \\ (x^{\frac{n}{2}})^2 & \text{where} ~ n \equiv 0 \pmod 2 \end{cases} \end{equation}

乗算命令の回数は$O(\log n)$であるが,正確には下記の通り(ループ展開した場合).

\begin{equation} C(n) = \lfloor \log_2 n \rfloor + popcnt(n) - 1 \label{numberOfMulExpBySquaring} \end{equation}

$popcnt$ は立っているビット数を数える関数であり,あえて数式で記述するならば下記のような定義となる.

\begin{equation} popcnt(n) = \begin{cases} 0 & \text{where} ~ n = 0 \\ popcnt \left(\dfrac{n}{2} \right) & \text{where} ~ n > 0 ~ \text{and} ~ n \equiv 0 \pmod 2 \\ 1 + popcnt \left( \dfrac{n - 1}{2} \right) & \text{where} ~ n > 0 ~ \text{and} ~ n \equiv 1 \pmod 2 \end{cases} \end{equation}

確認用シェーダー

指数は5,ベクトルの次元は3で確認すれば十分だろう. multi_compile 用のキーワードを設け,それぞれ下記のコードになるようにした.

キーワード コード詳細
NAIVE pow() 組み込み関数を利用
NAIVE_MUL 自前の乗算5回を行う関数を利用
ITERATIVE_SQUARE 自前の繰り返し二乗法を行う関数を利用
  • IntPower.shader
Shader "koturn/IntPower"
{
    Properties
    {
        [KeywordEnum(NAIVE, NAIVE_MUL, ITERATIVE_SQUARE)]
        _ExpMethod("Exp method", Int) = 2  // Default: ITERATIVE_SQUARE
    }

    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0
            #pragma multi_compile_local_fragment _EXPMETHOD_NAIVE _EXPMETHOD_NAIVE_MUL _EXPMETHOD_ITERATIVE_SQUARE

            #include "UnityCG.cginc"

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

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


            inline float3 pow5(float3 x)
            {
                return x * x * x * x * x;
            }

            inline float3 pown(float3 x, int n)
            {
                static const float3 ones = float3(1.0, 1.0, 1.0);

                float3 v = ones;
                UNITY_UNROLL
                for (; n > 0; n >>= 1) {
                    v *= (n & 1) == 0 ? ones : x;
                    x *= x;
                }
                return v;
            }

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

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
#if defined(_EXPMETHOD_NAIVE)
                // return float4(pow(i.color.rgb, 5.0), 1.0);
                // return float4(exp2(log2(i.color.rgb) * 5.0)), 1.0);
                return float4(exp2(i.color.rgb * log2(5.0))), 1.0);
#elif defined(_EXPMETHOD_NAIVE_MUL)
                return float4(pow5(i.color.rgb), 1.0);
#else
                return float4(pown(i.color.rgb, 5), 1.0);
#endif
            }
            ENDCG
        }
    }
}

確認用シェーダーの生成コード

各キーワードに対し,それぞれ下記のコードが生成されていた. すなわち,どれも全く同じ繰り返し二乗法をループ展開したコードとなっていた. となると,繰り返し二乗法の関数を自前で定義する必要はないかもしれない?

  • _EXPMETHOD_NAIVE
//////////////////////////////////////////////////////
Global Keywords: <none>
Local Keywords: _EXPMETHOD_NAIVE
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 3 math, 1 temp registers
Shader Disassembly:
//
// Generated by Microsoft (R) D3D Shader Disassembler
//
//
// Input signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_POSITION              0   xyzw        0      POS   float
// TEXCOORD                 0   xy          1     NONE   float
// TEXCOORD                 1   xyzw        2     NONE   float   xyz
//
//
// Output signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_Target                0   xyzw        0   TARGET   float   xyzw
//
      ps_4_0
      dcl_input_ps linear v2.xyz
      dcl_output o0.xyzw
      dcl_temps 1
   0: mul r0.xyz, v2.xyzx, v2.xyzx
   1: mul r0.xyz, r0.xyzx, r0.xyzx
   2: mul o0.xyz, r0.xyzx, v2.xyzx
   3: mov o0.w, l(1.000000)
   4: ret
// Approximately 0 instruction slots used
  • _EXPMETHOD_NAIVE_MUL
//////////////////////////////////////////////////////
Global Keywords: <none>
Local Keywords: _EXPMETHOD_NAIVE_MUL
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 3 math, 1 temp registers
Shader Disassembly:
//
// Generated by Microsoft (R) D3D Shader Disassembler
//
//
// Input signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_POSITION              0   xyzw        0      POS   float
// TEXCOORD                 0   xy          1     NONE   float
// TEXCOORD                 1   xyzw        2     NONE   float   xyz
//
//
// Output signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_Target                0   xyzw        0   TARGET   float   xyzw
//
      ps_4_0
      dcl_input_ps linear v2.xyz
      dcl_output o0.xyzw
      dcl_temps 1
   0: mul r0.xyz, v2.xyzx, v2.xyzx
   1: mul r0.xyz, r0.xyzx, r0.xyzx
   2: mul o0.xyz, r0.xyzx, v2.xyzx
   3: mov o0.w, l(1.000000)
   4: ret
// Approximately 0 instruction slots used
  • _EXPMETHOD_ITERATIVE_SQUARE
//////////////////////////////////////////////////////
Global Keywords: <none>
Local Keywords: _EXPMETHOD_ITERATIVE_SQUARE
-- Vertex shader for "d3d11":
// No shader variant for this keyword set. The closest match will be used instead.

-- Hardware tier variant: Tier 1
-- Fragment shader for "d3d11":
// Stats: 3 math, 1 temp registers
Shader Disassembly:
//
// Generated by Microsoft (R) D3D Shader Disassembler
//
//
// Input signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_POSITION              0   xyzw        0      POS   float
// TEXCOORD                 0   xy          1     NONE   float
// TEXCOORD                 1   xyzw        2     NONE   float   xyz
//
//
// Output signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_Target                0   xyzw        0   TARGET   float   xyzw
//
      ps_4_0
      dcl_input_ps linear v2.xyz
      dcl_output o0.xyzw
      dcl_temps 1
   0: mul r0.xyz, v2.xyzx, v2.xyzx
   1: mul r0.xyz, r0.xyzx, r0.xyzx
   2: mul o0.xyz, r0.xyzx, v2.xyzx
   3: mov o0.w, l(1.000000)
   4: ret
// Approximately 0 instruction slots used

pow()関数はどこまでを繰り返し二乗法のコードとするか

繰り返し二乗法の乗算回数は式 \eqref{numberOfMulExpBySquaring} の通りであるため,かなり多い乗算回数よりも log, mul, exp の3命令を用いた方が好ましいと考えられる. シェーダーコンパイラが整数定数を指定した pow() 関数から log, mul, exp の3命令を生成する閾値を調べてみることにした.

結果は下記の表の通り. この結果から指数 $n$ ではなく,乗算命令数 $C(n)$ が閾値となっており,7回以下なら繰り返し二乗法,8回以上なら log, mul, exp の3命令を生成することがわかる.

$$n$$ $$C(n)$$ 複数の乗算命令?
1 0 movのみ
2 1
3 2
4 2
5 3
6 3
7 4
8 3
9 4
10 4
11 5
12 4
13 5
14 5
15 6
16 4
17 5
18 5
19 6
20 5
21 6
22 6
23 7
24 5
25 6
26 6
27 7
28 6
29 7
30 7
31 8
32 5

まとめ

繰り返し二乗法の関数を用意し,指数に定数を指定することで,単純な乗算の命令が並ぶことが確認できた. これにより,2乗,3乗,4乗,...の関数を個別に用意せずとも,繰り返し二乗法の関数だけ用意するだけでよいことになる.

しかし,Direct3D11の出力アセンブラを確認する限り,組み込み関数 pow() の指数に定数を指定することでも繰り返し二乗法のコード生成をすることがわかった. しかも,乗算命令数によっては log, mul, exp の3命令にするようだ. このことから,わざわざ自前で繰り返し二乗法の関数を用意する必要はないかもしれない.

参考文献