koturnの日記

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

C# でファイルがロックされているかどうかを調べる

この記事では C# でファイルがロックされているかどうか調べる方法を紹介する. なお,本記事の言語バージョンは C# 11以降を前提としている.

FileStreamインスタンス生成時のIOExceptionによって調べる

通常の C# の範疇では FileStream でファイルを開こうとしたときに IOException が発生するかどうかによって判別するしかない. 下記コード中の

  • IsReadLocked(): 読み込みロックされているかどうか
  • IsWriteLocked(): 書き込みロックされているかどうか

を調べるメソッドになっている.

using System.IO;
#if !NETFRAMEWORK && !WINDOWS
using System.Runtime.InteropServices;
#endif  // !NETFRAMEWORK && !WINDOWS


namespace Koturn
{
    /// <summary>
    /// Provides utility methods to check whether a file is locked or not.
    /// </summary>
    public static class FileLockCheckUtils
    {
#if NETFRAMEWORK || WINDOWS
        /// <summary>
        /// A flag whether current running platform is Windows or not.
        /// </summary>
        private const bool _isWindows = true;
#else
        /// <summary>
        /// A flag whether current running platform is Windows or not.
        /// </summary>
        private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
#endif  // NETFRAMEWORK || WINDOWS

        /// <summary>
        /// Determine whether specified file is read-locked or not.
        /// </summary>
        /// <param name="filePath">File path to determine.</param>
        /// <returns>true if specified file is read-locked, otherwise false.</returns>
        public static bool IsReadLocked(string filePath)
        {
            return IsLockedCore(filePath, FileAccess.Read);
        }


        /// <summary>
        /// Determine whether specified file is write-locked or not.
        /// </summary>
        /// <param name="filePath">File path to determine.</param>
        /// <returns>true if specified file is write-locked, otherwise false.</returns>
        public static bool IsWriteLocked(string filePath)
        {
            return IsLockedCore(filePath, FileAccess.Write);
        }

        /// <summary>
        /// Determine whether specified file is read/write-locked or not.
        /// </summary>
        /// <param name="filePath">File path to determine.</param>
        /// <returns>true if specified file is write-locked, otherwise false.</returns>
        /// <remarks>
        /// <seealso href="https://learn.microsoft.com/en-us/dotnet/standard/io/handling-io-errors"/>
        /// </remarks>
        private static bool IsLockedCore(string filePath, FileAccess access)
        {
            const int ErrorSharingViolation = 0x00000020;
#if NETCOREAPP1_0_OR_GREATER
            // .NET: Buffer size must be greater than or equal to 0.
            const int BufferSize = 0;
#else
            // .NET Framework or .NET Standard: Buffer size must be greater than 0; 0 is not allowed.
            const int BufferSize = 1;
#endif  // NETCOREAPP1_0_OR_GREATER
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException(filePath);
            }

            FileStream? fs = null;
            try
            {
                if ((access & FileAccess.Write) != 0)
                {
                    // Try to open for write.
                    fs = new FileStream(filePath, FileMode.Open, FileAccess.Write, FileShare.Read, BufferSize);
                }
                else
                {
                    // Try to open for read.
                    fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Write, BufferSize);
                }
            }
            catch (IOException ex)
            {
                // Assume that VRChat process owns the log file.
                if (_isWindows && (ex.HResult & 0x0000ffff) != ErrorSharingViolation)
                {
                    throw;
                }
                return true;
            }
            finally
            {
                fs?.Dispose();
            }

            return false;
        }
    }
}

重要なのが FileStreamFileShare を指定している第四引数である. ここで 指定されなかった属性 は共有を許さないものとし,その読み込みまたは書き込み属性のロックを取得しにいく.

しかし,先んじて他アプリケーション,あるいは自身の処理で対象ファイルがその属性の共有を許可しなかった場合に IOException が発生する.

一口に IOException といっても,様々な理由で発生する可能性がある. そこで HResult プロパティの値を確認すれば,発生理由を調べることが可能である. MSDNに記載されているように,下位4バイトが32となっていれば共有違反が原因だとわかる.

ただし,Windows以外の環境だとHResultの値については上記の通りになっていないと考えられる. 事前にWindowsか否かを判定しておき,Windows以外であれば IOException 発生時は共有違反として扱うものとする(これでよいかどうか未調査). とはいえ,.NET FrameworkWindowsターゲットのバイナリである場合は,実行時にOSの判定を行う必要はないため,コンパイル時に NETFRAMEWORK, WINDOWS マクロの定義有無によってWindowsか否かを判定している.

余談であるが, FileStream のコンストラクタで指定できるバッファサイズは .NET Framework か .NET かによって許容される値が下記のように異なっている.

  • .NET Framework: 1以上でなければならない(0は例外が発生する)
  • .NET: 0以上であればOK

そのため,.NET であればバッファサイズは0を,そうでなければ(.NET Standardも)1を指定するようにした. これは,バッファサイズ0であれば内部的に Array.Empty<byte>() が使用されることを期待しているためである.

Win32 APIを用いて調べる

ファイルがロックされているかどうかを調べるためだけに例外を発生させるのは大袈裟感が否めない. 例外送出はとてもコストの高い処理であるため,可能であれば単純な判定で済ませたいものである.

.NET Frameworkにおける FileStream の実装を追えばわかるように,最後には CreateFile() 関数に行き着く. すなわち,Win32 APICreateFile() を用いて判定することで例外を発生させることなくファイルがロックされているかどうかがわかる.

Windowsであれば CreateFile() を用いてロック判定を行うように,最初に提示したコードを書き直すと下記のようになる.

#if NET7_0_OR_GREATER
#    define SUPPORT_LIBRARY_IMPORT
#endif  // NET7_0_OR_GREATER

using System.IO;
using System.Runtime.InteropServices;
#if NETFRAMEWORK || WINDOWS
using System;
using Microsoft.Win32.SafeHandles;
#if !NET7_0_OR_GREATER
using System.Security;
#endif  // !NET7_0_OR_GREATER
#endif  // NETFRAMEWORK || WINDOWS


namespace Koturn
{
    /// <summary>
    /// Provides utility methods to check whether a file is locked or not.
    /// </summary>
#if SUPPORT_LIBRARY_IMPORT && WINDOWS
    public static partial class FileLockCheckUtils
#else
    public static class FileLockCheckUtils
#endif  // SUPPORT_LIBRARY_IMPORT && WINDOWS
    {
#if !NETFRAMEWORK && !WINDOWS
        /// <summary>
        /// A flag whether current running platform is Windows or not.
        /// </summary>
        private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
#endif  // !NETFRAMEWORK && !WINDOWS

        /// <summary>
        /// Determine whether specified file is read-locked or not.
        /// </summary>
        /// <param name="filePath">File path to determine.</param>
        /// <returns>true if specified file is read-locked, otherwise false.</returns>
        public static bool IsReadLocked(string filePath)
        {
            return IsLockedCore(filePath, FileAccess.Read);
        }


        /// <summary>
        /// Determine whether specified file is write-locked or not.
        /// </summary>
        /// <param name="filePath">File path to determine.</param>
        /// <returns>true if specified file is write-locked, otherwise false.</returns>
        public static bool IsWriteLocked(string filePath)
        {
            return IsLockedCore(filePath, FileAccess.Write);
        }

        /// <summary>
        /// Determine whether specified file is read/write-locked or not.
        /// </summary>
        /// <param name="filePath">File path to determine.</param>
        /// <returns>true if specified file is write-locked, otherwise false.</returns>
        /// <remarks>
        /// <seealso href="https://learn.microsoft.com/en-us/dotnet/standard/io/handling-io-errors"/>
        /// </remarks>
        private static bool IsLockedCore(string filePath, FileAccess access)
        {
            const int ErrorSharingViolation = 0x00000020;
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException(filePath);
            }

#if NETFRAMEWORK || WINDOWS
            // Try to open for write.
            const nint invalidHandleValue = -1;

            using (var hFile = (access & FileAccess.Write) != 0
                ? SafeNativeMethods.CreateFile(filePath, GenericAccessRights.Write, FileShare.Read, IntPtr.Zero, FileMode.Open)
                : SafeNativeMethods.CreateFile(filePath, GenericAccessRights.Read, FileShare.Write, IntPtr.Zero, FileMode.Open))
            {
                return hFile.DangerousGetHandle() == invalidHandleValue && Marshal.GetLastWin32Error() == ErrorSharingViolation;
            }
#else
#if NETCOREAPP1_0_OR_GREATER
            // .NET: Buffer size must be greater than or equal to 0.
            const int BufferSize = 0;
#else
            // .NET Framework or .NET Standard: Buffer size must be greater than 0; 0 is not allowed.
            const int BufferSize = 1;
#endif  // NETCOREAPP1_0_OR_GREATER

            FileStream? fs = null;
            try
            {
                if ((access & FileAccess.Write) != 0)
                {
                    // Try to open for write.
                    fs = new FileStream(filePath, FileMode.Open, FileAccess.Write, FileShare.Read, BufferSize);
                }
                else
                {
                    // Try to open for read.
                    fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Write, BufferSize);
                }
            }
            catch (IOException ex)
            {
                // Assume that VRChat process owns the log file.
                if (_isWindows && (ex.HResult & 0x0000ffff) != ErrorSharingViolation)
                {
                    throw;
                }
                return true;
            }
            finally
            {
                fs?.Dispose();
            }

            return false;
#endif
        }

#if NETFRAMEWORK || WINDOWS
        [Flags]
        internal enum GenericAccessRights : uint
        {
            All = 0x10000000,
            Execute = 0x20000000,
            Write = 0x40000000,
            Read = 0x80000000,
        }

        [Flags]
        internal enum FileFlagAndAttributes : uint
        {
            AttrReadonly = 0x00000001,
            AttrHidden = 0x00000002,
            AttrSystem = 0x00000004,
            AttrArchive = 0x00000020,
            AttrNormal = 0x00000080,
            AttrTemporary = 0x00000100,
            AttrOffline = 0x00001000,
            AttrEncrypted = 0x00004000,
            SecurityAnonymous = 0x00000000,
            SecurityIdentification = 0x00010000,
            SecurityImpersonation = 0x00020000,
            SecurityDelegation = 0x00030000,
            SecurityContextTracking = 0x00040000,
            SecurityEffectiveOnly = 0x00080000,
            FlagOpenNoRecall = 0x00100000,
            FlagOpenReparsePoint = 0x00200000,
            FlagSessionAware = 0x00800000,
            FlagPosixSemantics = 0x01000000,
            FlagBackupSemantics = 0x02000000,
            FlagDeleteOnClose = 0x04000000,
            FlagSequentialScan = 0x08000000,
            FlagRandomAccess = 0x10000000,
            FlagNoBuffering = 0x20000000,
            FlagOverlapped = 0x40000000,
            FlagWriteThrough = 0x80000000
        }

        /// <summary>
        /// Provides native methods.
        /// </summary>
#if SUPPORT_LIBRARY_IMPORT
        private static partial class SafeNativeMethods
#else
        [SuppressUnmanagedCodeSecurity]
        private static class SafeNativeMethods
#endif  // SUPPORT_LIBRARY_IMPORT
        {
#if SUPPORT_LIBRARY_IMPORT
            [LibraryImport("kernel32.dll", EntryPoint = nameof(CreateFile) + "W", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
            public static partial SafeFileHandle CreateFile(
                string fileName,
                GenericAccessRights desiredAccess,
                FileShare shareMode,
                IntPtr pSecurityAttributes,
                FileMode creationDisposition,
                FileFlagAndAttributes flagsAndAttributes = FileFlagAndAttributes.AttrNormal,
                IntPtr hTemplateFile = default);
#else
            [DllImport("kernel32.dll", EntryPoint = nameof(CreateFile) + "W", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern SafeFileHandle CreateFile(
                string fileName,
                GenericAccessRights desiredAccess,
                FileShare shareMode,
                IntPtr pSecurityAttributes,
                FileMode creationDisposition,
                FileFlagAndAttributes flagsAndAttributes = FileFlagAndAttributes.AttrNormal,
                IntPtr hTemplateFile = default);
#endif  // SUPPORT_LIBRARY_IMPORT
        }
#endif  // NETFRAMEWORK || WINDOWS
    }
}

Previmでダークモード対応する

背景

最近のWebサイトではOSのダークモード設定をそのまま反映しているものもある. 僕は普段からOSの設定をダークモードにしているので,previmでダークモード対応を行うことを考えた.

デモ

以下のようになった. ダークモードとライトモードの切り替えも示している.

Previmダークモードのデモ
Previmダークモードのデモ

実装方針

実装方針としては下記のようにした.

  • 本体はいじらずカスタムCSSとアドオンのみで対応する
  • ダークモードとライトモードを切り替え可能にする
    • このため,メディアクエリを用いたCSSだけでの対応は不可
    • 方法としてはプレビュー上にボタン追加やクエリパラメータではなく,開発者モードでの関数呼び出しでよい
      • applyLightTheme() でライトモードに, applyDarkThene() でダークモードに

実装

カスタムCSS

ライトモード,ダークモード用の色はCSS変数を用いて管理する. ライトモードの色はprevim付属のデフォルトのCSSに従う. ダークモードの色はGitHubの配色を参考に,デフォルトのCSSを置き換えることにした.

コードブロックの配色に関して,ライトモードはprevim付属のhighlight.jsのCSSではなく, ダークモードともども最新のhighlightjs/highlight.jsの下記CSSを用いることにした

モード CSS
ライトモード github.css
ダークモード github-dark.css

previmの仕組み上,付属のhighlight.jsのCSSはアドオンとして追加されるため,カスタムCSSより後に読み込まれる. そのため,CSSの優先順位を利用し,カスタムCSSの配色を優先させる.

/* Color for initial loading */
:root {
  --color-fg-normal: #333;
  --color-bg-normal: white;
  transition: 0.5s;
}
@media (prefers-color-scheme: dark) {
  :root {
    --color-fg-normal: #e6edf3;
    --color-bg-normal: #0d1117;
  }
}

/* Color for light mode */
:root.theme-light {
  --color-fg-normal: #333;
  --color-bg-normal: white;
  --color-bg-second: #f8f8f8;
  --color-fg-head: black;
  --color-border-code: #eaeaea;
  --color-blockquote: #777777;
  /* Colors from highlightjs/highlight.js/src/styles/github.css */
  --color-fg-hljs: #24292e;
  --color-bg-hljs: #f8f8f8;
  --color-fg-hljs-keyword: #d73a49;
  --color-fg-hljs-title: #6f42c1;
  --color-fg-hljs-variable: #005cc5;
  --color-fg-hljs-string: #032f62;
  --color-fg-hljs-symbol: #e36209;
  --color-fg-hljs-comment: #6a737d;
  --color-fg-hljs-name: #22863a;
  --color-fg-hljs-subst: #24292e;
  --color-fg-hljs-section: #005cc5;
  --color-fg-hljs-bullet: #735c0f;
  --color-fg-hljs-emphasis: #24292e;
  --color-fg-hljs-strong: #24292e;
  --color-fg-hljs-addition: #22863a;
  --color-bg-hljs-addition: #f0fff4;
  --color-fg-hljs-deletion: #b31d28;
  --color-bg-hljs-deletion: #ffeef0;
}

/* Color for dark mode */
:root.theme-dark {
  --color-fg-normal: #e6edf3;
  --color-bg-normal: #0d1117;
  --color-bg-second: #161b22;
  --color-fg-head: #e6edf3;
  --color-border-code: #111111;
  --color-blockquote: #e6edf3;
  /* Colors from highlightjs/highlight.js/src/styles/github-dark.css */
  --color-fg-hljs: #c9d1d9;
  --color-bg-hljs: #161b22;
  --color-fg-hljs-keyword: #ff7b72;
  --color-fg-hljs-title: #d2a8ff;
  --color-fg-hljs-variable: #79c0ff;
  --color-fg-hljs-string: #a5d6ff;
  --color-fg-hljs-symbol: #ffa657;
  --color-fg-hljs-comment: #8b949e;
  --color-fg-hljs-name: #7ee787;
  --color-fg-hljs-subst: #c9d1d9;
  --color-fg-hljs-section: #1f6feb;
  --color-fg-hljs-bullet: #f2cc60;
  --color-fg-hljs-emphasis: #c9d1d9;
  --color-fg-hljs-strong: #c9d1d9;
  --color-fg-hljs-addition: #aff5b4;
  --color-bg-hljs-addition: #033a16;
  --color-fg-hljs-deletion: #ffdcd7;
  --color-bg-hljs-deletion: #67060c;
}

html {
  background: var(--color-bg-normal);
}

body {
  color: var(--color-fg-normal);
}

h1 {
  color: var(--color-fg-head);
}

h2 {
  color: var(--color-fg-head);
}

pre,
.highlight pre {
  background-color: var(--color-bg-second);
}

code {
  border-color: var(--color-border-code);
  background-color: var(--color-bg-second);
}

blockquote {
  color: var(--color-blockquote);
}

table tr {
  background-color: var(--color-bg-normal);
}
table tr:nth-child(2n) {
  background-color: var(--color-bg-second);
}

/*
 * Overwrite higilight.css
 */
code.hljs {
  color: var(--color-fg-hljs);
  background: var(--color-bg-hljs);
}

span.hljs-doctag,
span.hljs-keyword,
span.hljs-meta .hljs-keyword,
span.hljs-template-tag,
span.hljs-template-variable,
span.hljs-type,
span.hljs-variable.language_ {
  /* prettylights-syntax-keyword */
  color: var(--color-fg-hljs-keyword);
}

span.hljs-title,
span.hljs-title.class_,
span.hljs-title.class_.inherited__,
span.hljs-title.function_ {
  /* prettylights-syntax-entity */
  color: var(--color-fg-hljs-title);
}

span.hljs-attr,
span.hljs-attribute,
span.hljs-literal,
span.hljs-meta,
span.hljs-number,
span.hljs-operator,
span.hljs-variable,
span.hljs-selector-attr,
span.hljs-selector-class,
span.hljs-selector-id {
  /* prettylights-syntax-constant */
  color: var(--color-fg-hljs-variable);
}

span.hljs-regexp,
span.hljs-string,
span.hljs-meta .hljs-string {
  /* prettylights-syntax-string */
  color: var(--color-fg-hljs-string);
}

span.hljs-built_in,
span.hljs-symbol {
  /* prettylights-syntax-variable */
  color: var(--color-fg-hljs-symbol);
}

span.hljs-comment,
span.hljs-code,
span.hljs-formula {
  /* prettylights-syntax-comment */
  color: var(--color-fg-hljs-comment);
}

span.hljs-name,
span.hljs-quote,
span.hljs-selector-tag,
span.hljs-selector-pseudo {
  /* prettylights-syntax-entity-tag */
  color: var(--color-fg-hljs-name);
}

span.hljs-subst {
  /* prettylights-syntax-storage-modifier-import */
  color: var(--color-fg-hljs-subst);
}

span.hljs-section {
  /* prettylights-syntax-markup-heading */
  color: var(--color-fg-hljs-section);
}

span.hljs-bullet {
  /* prettylights-syntax-markup-list */
  color: var(--color-fg-hljs-bullet);
}

span.hljs-emphasis {
  /* prettylights-syntax-markup-italic */
  color: var(--color-fg-hljs-emphasis);
}

span.hljs-strong {
  /* prettylights-syntax-markup-bold */
  color: var(--color-fg-hljs-strong);
}

span.hljs-addition {
  /* prettylights-syntax-markup-inserted */
  color: var(--color-fg-hljs-addition);
  background-color: var(--color-bg-hljs-addition);
}

span.hljs-deletion {
  /* prettylights-syntax-markup-deleted */
  color: var(--color-fg-hljs-deletion);
  background-color: var(--color-bg-hljs-deletion);
}

上記のCSS~/.vim/previm/custom.css として保存する.

アドオン設定

Previmの設定は下記のようにしておく. js のコードは url や path が無くても差し込めるとのこと.

アドオンの 'code' に記載したコードはプレビュー更新時に毎回実行されるものである. 今回実行したい処理はワンタイム実行のみでよく,本来は外部ファイルとすべきではあるが,めんどくさくてサボっている.

また,プレビュー初回読み込みまではモードなしとなるため,CSS側でデフォルトの配色設定を行っている. (特にダークモードについてはメディアクエリで前景色と背景色のCSS変数のみ設定している)

let g:previm_custom_css_path = '~/.vim/previm/custom.css'
let g:previm_extra_libraries = [
      \ {
      \   'name': 'theme',
      \   'files': [
      \     {
      \       'type': 'js',
      \       'code': [
      \         '(function(global, doc) {',
      \         "  'use strict';",
      \         '  function applyTheme(themeName) {',
      \         '    doc.documentElement.className = themeName;',
      \         '  }',
      \         "  if (doc.documentElement.className === '') {",
      \         "    var prefersColorSchemeDark = typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches;",
      \         "    applyTheme(prefersColorSchemeDark ? 'theme-dark' : 'theme-light');",
      \         '  }',
      \         '  global.applyLightTheme = function() {',
      \         "    applyTheme('theme-light');",
      \         '  };',
      \         '  global.applyDarkTheme = function() {',
      \         "    applyTheme('theme-dark');",
      \         '  };',
      \         '})(window, document);'
      \       ]
      \     }
      \   ]
      \ }
      \]

参考

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のカスタムシェーダー(あるいはlilToon拡張)の作成にあたってのTipsを紹介する. ただし,公式ドキュメントに記載されている情報は記載せず,筆者がlilToonのカスタムシェーダーを作成する中で得た知見を記載する. 基本的なカスタムシェーダーの情報は下記公式ドキュメントを参照すること.

ドキュメント中にある下記のファイルがテンプレートであり,公式ドキュメント同様,この中の数ファイルを編集する形で述べていく.

なお,本記事で紹介する事項を盛り込んだテンプレートジェネレータを作成している.

※lilToon本体に更新があれば,本記事中のリンクも最新のソースへのリンクへと更新している(現在 2.3.4 へのリンク).

そもそもlilToonのカスタムシェーダーとは

lilToonのカスタムシェーダーとは,lilToon本体のシェーダージェネレーターを利用して,lilToon本体を変更することなく,非破壊的に処理を追加・既存処理を変更して作成されたlilToon本体とは別の派生シェーダーを指す.

実態としては,ScriptedImporter を用い,拡張子 .lilcontainer のテンプレートファイルからShaderLabソースアセットを動的に生成している. 本体側ではプリプロセッサマクロによる処理の差し込み可能な箇所があるため,カスタムシェーダー側にてそれらのマクロを定義することにより,処理の追加・既存処理の変更が実現できる.

なお,lilToon本体は ScriptedImporter を用いているわけではなく,拡張子 .lilinternal のテンプレートファイル から /Packages/jp.lilxyzw.liltoon/Shader (/Assets/liltoon/Shader) 配下に,直接Unityプロジェクトのレンダーパイプラインに応じたShaderLabソースを生成するようになっている.

カスタムシェーダーの利点としては下記のようなものがあると思う.

  • 自然とBRP, URP, HDRPなどの各種レンダーパイプラインに対応したシェーダーとなる
  • 本体側の処理はそのままにできるため,本体とライティング等の差がなく,見た目の親和性を保てる
  • 特殊な処理を記述していない限り,本体側をバージョンアップしても動作する
    • 反対に本体のバージョンを 1.4.1 あたりに戻したとしても動作する
  • シェーダー作成者は追加したい処理の一部を記述するだけでよく,簡単に作成できる
    • ある主のフレームワークというべき機構に則ってシェーダー処理およびインスペクタを書く形になるので,特有の難しさはあるが...

目次

custom.hlsl, custom_insert.hlsl について

知っておくべき文法

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

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

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

カスタムシェーダーの全体図

カスタムシェーダーは下記のような順で各ファイルのインクルードが行われる.

上記からわかる通り, BEFORE_xx, OVERRIDE_xx 系のマクロは必ずしも custom.hlsl で定義する必要はなく, custom_insert.hlsl で定義してもよい. 特に LIL_LITE などのマクロの定義有無で定義を分けたい場合( #if を用いて定義を分けたい場合)は,むしろ custom_insert.hlsl で定義しなくてはならない.

各レンダーパイプライン,パスでどのようなシェーダーとなるかは Assets/lilToon/CustomShaderResources 以下のファイルを参照. 例えば,BRP用のデフォルトのシェーダーは Assets/lilToon/CustomShaderResources/BRP/Default.lilblockを参照するとよい.

また,具体的に生成されるシェーダーは .lilcontainerファイル 選択時に表示される「Export Shader」ボタンから保存することができる. 保存したシェーダーの内容を眺めると処理順を掴みやすいと思われる.

lilToonカスタムシェーダのExport

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.LogErrorFormat("Failed to open Library/ShaderCache.db [{0}]", result);
        return;
    }

    try
    {
        result = NativeMethods.Execute(dbHandle, "DELETE FROM shadererrors");
        if (result != 0)
        {
            Debug.LogErrorFormat("SQL failed [{0}]", result);
            return;
        }
    }
    finally
    {
        result = NativeMethods.Close(dbHandle);
        if (result != 0)
        {
            Debug.LogErrorFormat("Failed to close database [{0}]", 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="pDb">SQLite db handle.</param>
    /// <returns>Result code.</returns>
    /// <remarks>
    /// <seealso href="https://www.sqlite.org/c3ref/open.html"/>
    /// </remarks>
    [DllImport(LibraryName, EntryPoint = "sqlite3_open16", CallingConvention = CallConv, CharSet = CharSet.Unicode)]
    public static extern int Open([In] string filePath, out IntPtr pDb);

    /// <summary>
    /// Close database.
    /// </summary>
    /// <param name="pDb">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 pDb);

    /// <summary>
    /// Execute specified SQL.
    /// </summary>
    /// <param name="pDb">SQLite db handle.</param>
    /// <param name="sql">SQL to be evaluated.</param>
    /// <param name="pCallback">Callback function.</param>
    /// <param name="pCallbackArg">1st argument to callback.</param>
    /// <param name="pErrMsg">Error message 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 pDb, [In] string sql, IntPtr pCallback = default(IntPtr), IntPtr pCallbackArg = default(IntPtr), IntPtr pErrMsg = default(IntPtr));
}

ただ単に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の両対応のため

Unrecognized sampler 'sampler_maintex' ...というコンパイルエラーが発生する

例えば, OVERRIDE_MAIN マクロを

#define OVERRIDE_MAIN \
    fd.col = float4(1.0, 1.0, 0.5, 1.0);

のように定義したときに,下記のシェーダーコンパイルエラーが発生する.

Fragment program 'frag': Unrecognized sampler 'sampler_maintex' - does not match any texture and is not a recognized inline name (should contain filter and wrap modes).

lilToonでは _MainTex のテクスチャサンプラ sampler_MainTex を使い回すようになっている. そのため, _MainTex のサンプリングを無くすと,HLSLの最適化処理により sampler_MainTex は削除されてしまい,前述のコンパイルエラーが発生する.

対策1: サンプラを使用していることを明示する

下記のように コンパイル時定数ではない 0を掛けることにより,最適化によるサンプリング処理の除去を阻害することができる. LIL_TIME_Time.y ,すなわちワールドに入室してからの経過秒数のマクロであるが,uniform変数である以上,コンパイラは min(0.0, LIL_TIME) が0になることをコンパイル時に判定できないことを利用している.

#define OVERRIDE_MAIN \
    LIL_GET_MAIN_TEX \
    fd.col = min(0.0, LIL_TIME) * fd.col + float4(1.0, 1.0, 0.5, 1.0);

下記のように単純に0.0を掛けると,コンパイル時最適化によりサンプリング部分のコードも除去される. それによって _MainTex の参照無しとなり, _MainTex のテクスチャサンプラも削除され,コンパイルエラーとなってしまう.

#define OVERRIDE_MAIN \
    LIL_GET_MAIN_TEX \
    fd.col = 0.0 * fd.col + float4(1.0, 1.0, 0.5, 1.0);

対策2: インラインサンプラに置換するマクロを定義する

テクスチャサンプラはテクスチャに付随するものの他に,特殊な命名規則で宣言することで,テクスチャとは独立して用意することができる.

lilToon本体では2種類のインラインサンプラが宣言されているので,マクロ定義によって sampler_MainTex をこれらのサンプラに置き換える.

ただし,アウトライン用のパスでは同様の置換マクロ定義が行われているので,アウトライン以外のパスでの置換を試みる必要がある. 具体的には custom_insert.hlsl で下記の置換定義を行う.

#if !defined(LIL_OUTLINE)
#    define sampler_MainTex lil_sampler_linear_clamp
#endif

テクスチャフィルタリングモードやテクスチャラップモードを別のものにしたい場合は自前でサンプラを定義すること.

uniform変数を宣言する

公式ドキュメントに記載の通り,uniform変数の宣言は LIL_CUSTOM_PROPERTIESLIL_CUSTOM_TEXTURES で行う. このとき,知っておくべき文法で述べたように,マクロの行継続 \ が抜けるとそれ以降の変数はマクロとして展開されないので注意.

誤例1

_CustomVertexWaveSpeed はマクロ定義外となる.

#define LIL_CUSTOM_PROPERTIES \
    float4  _CustomVertexWaveScale; \
    float4  _CustomVertexWaveStrength;
    float   _CustomVertexWaveSpeed;

正しくは _CustomVertexWaveStrength の行末に行継続が必要である.

#define LIL_CUSTOM_PROPERTIES \
    float4  _CustomVertexWaveScale; \
    float4  _CustomVertexWaveStrength; \
    float   _CustomVertexWaveSpeed;

誤例2

空行を入れようとした例. _CustomVertexWaveStrength_CustomVertexWaveSpeed がマクロ定義外となる.

#define LIL_CUSTOM_PROPERTIES \
    float4  _CustomVertexWaveScale; \

    float4  _CustomVertexWaveStrength; \
    float   _CustomVertexWaveSpeed;

正しくは空行にも行継続を入れなければならない.

#define LIL_CUSTOM_PROPERTIES \
    float4  _CustomVertexWaveScale; \
\
    float4  _CustomVertexWaveStrength; \
    float   _CustomVertexWaveSpeed;

uniform変数がマクロ定義外となることの影響

LIL_CUSTOM_PROPERTIES マクロの展開箇所は UnityPerMaterial という名前のConstant Bufferの定義内 である. これは URP や HDPR 等で利用できる SRP Batcher のためのものである(BRPではSRP Batcherには関係のない機能).

そのため LIL_CUSTOM_PROPERTIES の定義外となったとしても, lil_custom.hlsl 内でuniform変数が定義されるようになるため,コンパイルエラーになるわけではない(なので気がつきにくい). しかし,

  • SRP Batcher に対応しておいた方がお行儀が良いこと
  • 1つのConstant Bufferにまとめておいた方がおそらくデータ転送効率がよい

ことから, LIL_CUSTOM_PROPERTIES マクロにunifrom変数定義をまとめておくべきだと思われる. (Constant Bufferの指定なしのuniform変数は $Global というConstant Bufferに格納される)

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

メンバの追加は 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 \
    output.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となるため,## を用いて字句結合を行う. 頂点シェーダー内での出力構造体変数は output を指定,フラグメントシェーダー内での入力構造体変数は 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

lilToon 1.4.1で直った

ジオメトリシェーダーを利用する

公式のジオメトリシェーダーを用いた作例に習い,下記21ファイルに

lilSubShaderInsertPost "lilCustomShaderInsertPost.lilblock"

という記述を加える.

  1. ltsmulti.lilcontainer
  2. ltsmulti_fur.lilcontainer
  3. ltsmulti_gem.lilcontainer
  4. ltsmulti_o.lilcontainer
  5. ltsmulti_ref.lilcontainer
  6. ltspass_cutout.lilcontainer
  7. ltspass_lite_cutout.lilcontainer
  8. ltspass_lite_opaque.lilcontainer
  9. ltspass_lite_transparent.lilcontainer
  10. ltspass_opaque.lilcontainer
  11. ltspass_tess_cutout.lilcontainer
  12. ltspass_tess_opaque.lilcontainer
  13. ltspass_tess_transparent.lilcontainer
  14. ltspass_transparent.lilcontainer
  15. lts_fakeshadow.lilcontainer
  16. lts_fur.lilcontainer
  17. lts_fur_cutout.lilcontainer
  18. lts_fur_two.lilcontainer
  19. lts_gem.lilcontainer
  20. lts_ref.lilcontainer
  21. lts_ref_blur.lilcontainer

lilSubShaderInsertPost という記述があれば,指定ファイルの内容置換されるので,下記2ファイルを用意する.

  • lilCustomShaderInsertPost.lilblock
#include "custom_insert_post.hlsl"
  • custom_insert_post.hlsl
// 望む処理を記載する.

そして, lilCustomShaderDatas.lilblock に下記の記述を追加する.

Replace "\r\n" "\n"
Replace "\r" "\n"
Replace "// Build Option\n            #pragma vertex vert\n            #pragma fragment frag\n" "// Build Option\n            #pragma vertex vert\n            #pragma geometry geomCustom\n            #pragma fragment frag\n            #pragma require geometry\n"
Replace "            #pragma vertex vertTess\n            #pragma fragment frag\n            #pragma hull hull\n            #pragma domain domain\n            #pragma require tesshw tessellation\n" "            #pragma vertex vertTess\n            #pragma fragment frag\n            #pragma hull hull\n            #pragma domain domain\n            #pragma geometry geomCustom\n            #pragma require tesshw tessellation geometry\n"

上記の置換定義により

            #pragma vert
            #pragma frag

という行が,

            #pragma vert
            #pragma geomCustom
            #pragma frag

となり,

            #pragma vertex vertTess
            #pragma fragment frag
            #pragma hull hull
            #pragma domain domain
            #pragma require tesshw tessellation

という行が,

            #pragma vertex vertTess
            #pragma fragment frag
            #pragma hull hull
            #pragma domain domain
            #pragma geomCustom
            #pragma require tesshw tessellation

という行に置換される.

公式の置換定義との差異について

※公式の作例だと下記のようになっている.

Replace "            #pragma vertex vert\r\n            #pragma fragment frag\r\n" "            #pragma vertex vertCustom\r\n            #pragma geometry geomCustom\r\n            #pragma fragment frag\r\n            #pragma require geometry\r\n"
Replace "            #pragma vertex vertTess\r\n            #pragma fragment frag\r\n            #pragma hull hull\r\n            #pragma domain domain\r\n            #pragma require tesshw tessellation\r\n" "            #pragma vertex vertTess\r\n            #pragma fragment frag\r\n            #pragma hull hull\r\n            #pragma domain domainCustom\r\n            #pragma geometry geomCustom\r\n            #pragma require tesshw tessellation geometry\r\n"

.lilcontainerファイルからshaderlabソースの構築にはStringBufferが用いられている. 1行のテキストの追加にはAppendLineメソッドを利用しているため,構築の途中段階ではOSデフォルトの改行コード(WindowsであればCR+LF(\r\n)であり,LinuxであればLF(\n))のシェーダーコード文字列が生成される. そして,置換の適用は改行コードをCR+LFに正規化する前であるため,lilToon 2.3.2 以前において,上記の置換定義はWindowsでしかうまく動作しなかった. しかし, lilToon 2.3.3 で置換定義の改行コードが \r\n, \r, \n のいずれであってもOSのデフォルトの改行コードとして解釈するように修正 したため,Windows以外の環境においてもうまく動作するようになった.

lilToon 2.3.2 以前でこの問題を回避するためには,カスタムシェーダー側の置換定義で

Replace "\r\n" "\n"
Replace "\r" "\n"

を一番初めに記述し,改行コードを \n に正規化しておく(改行コードがCRである環境は今日ではほぼ存在しないが念のため). そして,他の置換定義における改行コードを \n とし,WindowsでもLinuxでも適切に置換が行われるようにした方がよい. このような記述をしたとしても, lilToon 2.3.3 以降での置換動作に問題はない.

また,

により, LightMode = "Never" のパスでも置換が行われてしまうようになり,コンパイルエラーが発生するようになった. ダミーのパスであるため実害はないと思われるが,気持ち悪い. コンパイルエラーの原因である LightMode = "Never" において置換が行われないようにするために前行の // Build Option というコメントを含めて置換するのがよいと思われる.

GrabPassを利用する

GrabPassを利用するには3種類の方法がある. 最初に各方法を比較した表を置いておく.

手法 Replaceタグ InsertPassPre GrabPass入りのlilblockを作成
手軽さ
パフォーマンス ×
柔軟性 × ×

Replaceタグ

最も単純な対応方法であるが,推奨しない方法である.

lilCustomShaderDatas.lilblock のReplaceタグを用いることでGrabPassの挿入を行うことが可能である. どのシェーダーでもForwardBaseが先頭にあり,かつ // Forward というコメントが記載されている. 先頭にGrabPassを挿入するなら下記の定義で可能である.

Replace "\r\n" "\n"
Replace "\r" "\n"
Replace "        // Forward\n" "        GrabPass {}\n\n        // Forward\n"

ただし,置換処理は極めて単純なものであり,下記のようにバックスラッシュでのエスケープを用いて置換定義内にダブルクオートそのものを含めるようなことはできない. すなわち,取り込み先のテクスチャ名の指定はできないため,複数回使用する場合のパフォーマンスは悪い.

Replace "        // Forward\n" "        GrabPass { \"_MyGrabTexture\" }\n\n        // Forward\n"

また,lilToon本体側で(未使用ではあるが) _GrabTexture の宣言がされているため,カスタムシェーダー側での宣言が不要である.

InsertPassPre

lilCustomShaderDatas.lilblock のInsertPassPreタグを用いることで任意の文字列をどのパスよりも先立ってGrabPassを挿入することが可能である. 下記の合計5つのGem系シェーダーとRefraction系シェーダーには既に GrabPass {"_lilBackgroundTexture"} という形でGrabPassが定義されている.

  1. DefaultGem.lilblock
  2. DefaultMultiGem.lilblock
  3. DefaultMultiRefraction.lilblock
  4. DefaultRefraction.lilblock
  5. DefaultRefractionBlur.lilblock

なので,これら5つのシェーダーではGrabPassを定義しないようにしつつ,それ以外のシェーダーでは定義するようにする.

公式サイトに記載されているように,

  • 指定ファイルの末尾がBRPであればBRP
  • 引数を2つ指定する形式では第一引数が対象とするシェーダーバリエーションの部分文字列
  • PrePassは後に記述されているものが優先

であるので,これを踏まえて対応を行う. まず, lilCustomShaderDatas.lilblock では下記を記述する.

InsertPassPre "PrePass.lilblock"
InsertPassPre "Gem" "PrePassNone.lilblock"
InsertPassPre "Refraction" "PrePassNone.lilblock"

GrabPassはBRPでしか使用できないため,URPやHDRPに影響を及ぼさないように,下記の PrePassBRP.lilblock を作成する.

// GrabPass
GrabPass {"_lilBackgroundTexture"}

PrePassNoneBRP.lilblock は空ファイルで作成する.

これで全てのシェーダーバリエーションで _lilBackgroundTexture にレンダリング結果が入る.

lilToonの本体では

  • _lilBackgroundTexture
  • _lilBackgroundTexture_TexelSize

2つが定義されているため,カスタムシェーダー側での宣言は不要である.

また,

  • LIL_GET_BG_TEX

というサンプリング用のマクロも定義されているため,活用するとよい. このマクロはURPやHDRP用にも定義されており,使用することでBRP以外のレンダーパイプラインに対応したことになる.

GrabPass入りのlilblockを作成

各シェーダーの大部分のテンプレートは lilSubShaderXXX で指定されているものである. このテンプレートは本体側に同梱されているが,自作のものを指定することが可能である. 本体側の処理を見てわかるように,指定されているものがDefaultという文字列を含み,かつlilblockという拡張子を含んでいなければ,カスタムシェーダーのShaderディレクトリをベースデイレクトリとして,指定されたファイルを展開するようになっている.

下記17個のlilblockファイルをlilToon本体の CustomShaderResources/BRP ディレクトリからカスタムシェーダーの lilCustomShaderDatas.lilblock ファイルがあるディレクトリに,UsePass系列を除いてコピーし,編集を行う. 直下に全てコピーすると散らかるので,本体に習って BRP という名前のサブディレクトリを作成し,その中にまとめるのがオススメである.

ただし,「GrabPass {"_lilBackgroundTexture"} 有無」欄に○が付いた5個のlilblockについては, GrabPass {"_lilBackgroundTexture"} というGrabPassが先頭に記述されている. 他12個でも同名のテクスチャで宣言するのであれば作成は不要である.

No. lilblockファイル名 GrabPass {"_lilBackgroundTexture"} 有無
1 Default.lilblock
2 DefaultFakeShadow.lilblock
3 DefaultFur.lilblock
4 DefaultFurTwoPass.lilblock
5 DefaultGem.lilblock
6 DefaultLite.lilblock
7 DefaultLiteTwoSide.lilblock
8 DefaultMulti.lilblock
9 DefaultMultiFur.lilblock
10 DefaultMultiGem.lilblock
11 DefaultMultiOutline.lilblock
12 DefaultMultiRefraction.lilblock
13 DefaultRefraction.lilblock
14 DefaultRefractionBlur.lilblock
15 DefaultTessellation.lilblock
16 DefaultTessellationTwoSide.lilblock
17 DefaultTwoSide.lilblock

次に前述のlilblockファイルを読み込むようにlilcontainerファイルを編集する. lilSubShaderBRPタグの引数を下記表に記載されているものに変更すること. 「Gem or Refraction」に○が付いているものは,デフォルトで GrabPass {"_lilBackgroundTexture"} が記述されているlilblockが指定されているものである.

No. lilcontainerファイル名 lilSubShaderBRP変更前 lilSubShaderBRP変更後 Gem or Refraction
1 lts_fakeshadow.lilcontainer DefaultFakeShadow BRP/CustomFakeShadow.lilblock
2 lts_fur.lilcontainer DefaultFur BRP/CustomFur.lilblock
3 lts_fur_cutout.lilcontainer DefaultFur BRP/CustomFur.lilblock
4 lts_fur_two.lilcontainer DefaultFurTwoPass BRP/CustomFurTwoPass.lilblock
5 lts_gem.lilcontainer DefaultGem BRP/CustomGem.lilblock
6 lts_ref.lilcontainer DefaultRefraction BRP/CustomRefraction.lilblock
7 lts_ref_blur.lilcontainer DefaultRefractionBlur BRP/CustomRefractionBlur.lilblock
8 ltsmulti.lilcontainer DefaultMulti BRP/CustomMulti.lilblock
9 ltsmulti_fur.lilcontainer DefaultMultiFur BRP/CustomMultiFur.ilblock
10 ltsmulti_gem.lilcontainer DefaultMultiGem BRP/CustomMultiGem.lilblock
11 ltsmulti_o.lilcontainer DefaultMultiOutline BRP/CustomMultiOutline.lilblock
12 ltsmulti_ref.lilcontainer DefaultMultiRefraction BRP/CustomMultiRefraction.lilblock
13 ltspass_cutout.lilcontainer Default BRP/Custom.lilblock
14 ltspass_lite_cutout.lilcontainer DefaultLite BRP/CustomLite.lilblock
15 ltspass_lite_opaque.lilcontainer DefaultLite BRP/CustomLite.lilblock
16 ltspass_lite_transparent.lilcontainer DefaultLiteTwoSide BRP/CustomLiteTwoSide.lilblock
17 ltspass_opaque.lilcontainer Default BRP/Custom.lilblock
18 ltspass_tess_cutout.lilcontainer DefaultTessellation BRP/CustomTessellation.lilblock
19 ltspass_tess_opaque.lilcontainer DefaultTessellation BRP/CustomTessellation.lilblock
20 ltspass_tess_transparent.lilcontainer DefaultTessellationTwoSide BRP/CustomTessellationTwoSide.lilblock
21 ltspass_transparent.lilcontainer DefaultTwoSide BRP/CustomTwoSide.lilblock

エミッションの実装

lilFargData には emissionColor というメンバがあり,発光の色と記載されている. 本体バージョン1.3.6以前では,各処理工程で fd.emissionColor にエミッション色を足し込んでおくことで,最後に fd.colfd.emissionColor が加算される仕組みとなっていた.

しかし,このメンバは本体バージョン1.3.7以降では本体側の処理では使用されないメンバとなった. ( fd.emissionColor に値が足し込まれてはいるが,使用されない)

1.3.7以降で単純な加算合成のエミッションを実現するためには, fd.color にエミッション色を足し込む実装にしなければならない.

fd.col.rgb += yourEmissionColor.rgb;

本体と同様に,4種類:

  • Normal(通常)
  • Add(加算)
  • Screen(スクリーン)
  • Multiply(乗算)

のエミッションを実現する場合は,本体側の実装と同様に lilBlendColor() を用いるとよい.

float yourEmissionBlend = _YourEmissionBlend * yourEmissionColor.a;
#if LIL_RENDER == 2 && !defined(LIL_REFRACTION)
    yourEmissionBlend *= fd.col.a;
#endif
fd.col.rgb = lilBlendColor(fd.col.rgb, yourEmissionColor.rgb, yourEmissionBlend, _YourEmissionBlendMode);

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

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

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

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


namespace lilToon
{
    /// <summary>
    /// Startup method provider.
    /// </summary>
    internal sealed class Startup : AssetPostprocessor
    {
        /// <summary>
        /// Buffer size of streams.
        /// </summary>
        private const int DefaultBufferSize = 1024;
        /// <summary>
        /// GUID of shader directory.
        /// </summary>
        // TODO: Replace to GUID of your "Shader" directory.
        private const string GuidShaderDir = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";  // TODO

#if UNITY_2021_2_OR_NEWER
        /// <summary>
        /// This is called after importing of any number of assets is complete.
        /// </summary>
        /// <param name="importedAssets">Array of paths to imported assets.</param>
        /// <param name="deletedAssets">Array of paths to deleted assets.</param>
        /// <param name="movedAssets">Array of paths to moved assets.</param>
        /// <param name="movedFromAssetPaths">Array of original paths for moved assets.</param>
        /// <param name="didDomainReload">Boolean set to true if there has been a domain reload.</param>
        /// <remarks>
        /// <seealso href="https://docs.unity3d.com/2022.3/Documentation/ScriptReference/AssetPostprocessor.OnPostprocessAllAssets.html"/>
        /// </remarks>
        private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths, bool didDomainReload)
#else
        /// <summary>
        /// This is called after importing of any number of assets is complete.
        /// </summary>
        /// <param name="importedAssets">Array of paths to imported assets.</param>
        /// <param name="deletedAssets">Array of paths to deleted assets.</param>
        /// <param name="movedAssets">Array of paths to moved assets.</param>
        /// <param name="movedFromAssetPaths">Array of original paths for moved assets.</param>
        /// <remarks>
        /// <seealso href="https://docs.unity3d.com/2019.4/Documentation/ScriptReference/AssetPostprocessor.OnPostprocessAllAssets.html"/>
        /// </remarks>
        private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
#endif  // UNITY_2021_2_OR_NEWER
        {
            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);
            if (dstDirPath.Length == 0)
            {
                Debug.LogWarning("Cannot find file or directory corresponding to GUID: " + GuidShaderDir);
                return;
            }
            if (!Directory.Exists(dstDirPath))
            {
                Debug.LogWarningFormat("Directory not found: {0} ({1})", dstDirPath, GuidShaderDir);
                return;
            }
            UpdateVersionDefFile(Path.Combine(dstDirPath, "lil_current_version.hlsl"));
        }

        /// <summary>
        /// Update definition file of version value of lilToon, lil_current_version_value.hlsl.
        /// </summary>
        /// <param name="filePath">Destination file path.</param>
        /// <param name="bufferSize">Buffer size for temporary buffer and <see cref="FileStream"/>,
        /// and initial capacity of <see cref="MemoryStream"/>.</param>
        public static void UpdateVersionDefFile(string filePath, int bufferSize = DefaultBufferSize)
        {
            using (var ms = new MemoryStream(bufferSize))
            {
                WriteVersionFileBytes(ms);
                var buffer = ms.GetBuffer();
                var length = (int)ms.Length;

                if (CompareFileBytes(filePath, buffer, 0, length, bufferSize))
                {
                    return;
                }

                using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize, FileOptions.SequentialScan))
                {
                    fs.Write(buffer, 0, length);
                }

                Debug.LogFormat("Update {0}", filePath);
            }
        }

        /// <summary>
        /// Write version file content to <see cref="s"/>.
        /// </summary>
        /// <param name="s">Destination stream.</param>
        /// <param name="bufferSize">Buffer size for <see cref="StreamWriter"/>.</param>
        private static void WriteVersionFileBytes(Stream s, int bufferSize = DefaultBufferSize)
        {
            using (var writer = new StreamWriter(s, Encoding.ASCII, bufferSize, true)
            {
                NewLine = "\n"
            })
            {
                writer.WriteLine("#ifndef LIL_CURRENT_VERSION_INCLUDED");
                writer.WriteLine("#define LIL_CURRENT_VERSION_INCLUDED");
                writer.WriteLine();
                writer.WriteLine("#define LIL_CURRENT_VERSION_VALUE {0}", lilConstants.currentVersionValue);

                var match = new Regex(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)").Match(lilConstants.currentVersionName);
                if (match.Success)
                {
                    var groups = match.Groups;
                    writer.WriteLine("#define LIL_CURRENT_VERSION_MAJOR {0}", groups[1].Value);
                    writer.WriteLine("#define LIL_CURRENT_VERSION_MINOR {0}", groups[2].Value);
                    writer.WriteLine("#define LIL_CURRENT_VERSION_PATCH {0}", groups[3].Value);
                }

                writer.WriteLine();
                writer.WriteLine("#endif  // LIL_CURRENT_VERSION_INCLUDED");
            }
        }

        /// <summary>
        /// Compare file content with specified byte sequence.
        /// </summary>
        /// <param name="filePath">Target file path.</param>
        /// <param name="contentData">File content data to compare.</param>
        /// <param name="offset">Offset of <paramref name="contentData"/>,</param>
        /// <param name="count">Length of <paramref name="contentData"/>.</param>
        /// <param name="bufferSize">Buffer size for temporary buffer and <see cref="FileStream"/>.</param>
        /// <returns>True if file content is same to <see cref="contentData"/>, otherwise false.</returns>
        private static bool CompareFileBytes(string filePath, byte[] contentData, int offset, int count, int bufferSize = DefaultBufferSize)
        {
            if (!File.Exists(filePath))
            {
                return false;
            }
            if (new FileInfo(filePath).Length != count)
            {
                return false;
            }

            var minBufferSize = Math.Min(count, bufferSize);
            using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, minBufferSize, FileOptions.SequentialScan))
            {
                var buffer = new byte[minBufferSize];
                int nRead;
                while ((nRead = fs.Read(buffer, 0, buffer.Length)) > 0)
                {
                    if (!CompareMemory(buffer, 0, contentData, offset, nRead))
                    {
                        return false;
                    }
                    offset += nRead;
                }
            }

            return true;
        }

        /// <summary>
        /// Compare two byte data.
        /// </summary>
        /// <param name="data1">First byte data array.</param>
        /// <param name="offset1">Offset of first byte data array.</param>
        /// <param name="data2">Second byte data array.</param>
        /// <param name="offset2">Offset of second byte data array.</param>
        /// <param name="count">Number of bytes comparing <paramref name="data1"/> and <paramref name="data2"/>.</param>
        /// <returns>True if two byte data is same, otherwise false.</returns>
        private static bool CompareMemory(byte[] data1, int offset1, byte[] data2, int offset2, int count)
        {
            for (int i = 0; i < count; i++)
            {
                if (data1[offset1 + i] != data2[offset2 + i])
                {
                    return false;
                }
            }
            return true;
        }
    }
}

上記のコードで生成されるのは lil_current_version.hlsl というファイルで,下記のようなたった数行のファイルである. ファイルサイズが十分に小さいので,NTFSなWindowsにおいてMFTに格納されるため,ディスクサイズとしてもやさしい.

#ifndef LIL_CURRENT_VERSION_INCLUDED
#define LIL_CURRENT_VERSION_INCLUDED

#define LIL_CURRENT_VERSION_VALUE 45
#define LIL_CURRENT_VERSION_MAJOR 2
#define LIL_CURRENT_VERSION_MINOR 3
#define LIL_CURRENT_VERSION_PATCH 4

#endif  // LIL_CURRENT_VERSION_INCLUDED

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

#include "lil_current_version.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

バージョン文字列(currentVersionName)とバージョン値(currentVersionValue, 上記の例だと45)の対応関係は以下の表の通り. これは Assets/lilToon/Editor/lilConstants.cs の変更履歴を見ればわかる. ただし,バージョン文字列lilToon 2.1.1 より package.json のバージョンを参照するようになっている.

lilToonのバージョン currentVersionName currentVersionValue 備考
1.3.0 "1.3.0" 26
1.3.1 "1.3.1" 27
1.3.2 "1.3.2" 28
1.3.3 "1.3.3" 29
1.3.4 "1.3.4" 30
1.3.5 "1.3.5" 31
1.3.6 "1.3.6" 32
1.3.7 "1.3.7" 33
1.4.0 "1.4.0" 34
1.4.1 "1.4.1" 35
1.5.0 "1.5.0" 36
1.5.1 "1.5.1" 37
1.6.0 "1.6.0" 38
1.6.1 "1.6.1" 39
1.7.0 "1.7.0" 40
1.7.1 "1.7.1" 41
1.7.2 "1.7.2" 42
1.7.3 "1.7.3" 43
1.8.0 "1.8.0" 44
1.8.1 "1.8.0" 44 currentVersionName, currentVersionValue ともに据置き
1.8.2 "1.8.2" 44 currentVersionValue は据置きにする方針とした?
1.8.3 "1.8.3" 44
1.8.4 "1.8.4" 44
1.8.5 "1.8.5" 44
1.9.0 "1.9.0" 45
1.10.0 "1.10.0" 45
1.10.1 "1.10.1" 45
1.10.2 "1.10.2" 45
1.10.3 "1.10.3" 45
2.0.0 "2.0.0" 45
2.1.0 "2.1.0" 45
2.1.1 "2.1.1" 45
2.1.2 "2.1.2" 45
2.1.3 "2.1.3" 45
2.1.4 "2.1.4" 45
2.1.5 "2.1.5" 45
2.1.6 "2.1.6" 45
2.1.7 "2.1.7" 45
2.1.8 "2.1.8" 45
2.1.9 "2.1.9" 45
2.1.10 "2.1.10" 45
2.2.0 "2.2.0" 45
2.2.1 "2.2.1" 45
2.3.0 "2.3.0" 45
2.3.1 "2.3.1" 45
2.3.2 "2.3.2" 45
2.3.3 "2.3.3" 45
2.3.4 "2.3.4" 45

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

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

  1. ltsmulti.lilcontainer
  2. ltsmulti_fur.lilcontainer
  3. ltsmulti_gem.lilcontainer
  4. ltsmulti_o.lilcontainer
  5. 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ファイルの代わりに下記1ファイルにのみキーワードのpragmaを記述する.

  1. 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);

    var material = (Material)materialEditor.target;
    foreach (var keyword 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);

    var material = (Material)materialEditor.target;
    foreach (var keyword 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.
#if !defined(LIL_FEATURE_AUDIOLINK) && !defined(AUDIOLINK_CGINC_INCLUDED)
TEXTURE2D_FLOAT(_AudioTexture);
float4 _AudioTexture_TexelSize;
#endif  // !defined(LIL_FEATURE_AUDIOLINK) && !defined(AUDIOLINK_CGINC_INCLUDED)

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

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

下記9つの変数はVRChat側から提供されているuniform変数であり,どのワールドでも使用することができる. (最初は3つだけだったので,今後さらにuniform変数が追加される可能性もある)

  • _VRChatCameraMode
  • _VRChatCameraMask
  • _VRChatMirrorMode
  • _VRChatFaceMirrorMode
  • _VRChatMirrorCameraPos
  • _VRChatScreenCameraPos
  • _VRChatScreenCameraRot
  • _VRChatPhotoCameraPos
  • _VRChatPhotoCameraRot
  • _VRChatTimeUTCUnixSeconds
  • _VRChatTimeNetworkMs
  • _VRChatTimeEncoded1
  • _VRChatTimeEncoded2

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

#define LIL_CUSTOM_PROPERTIES \
    float _VRChatCameraMode; \
    uint _VRChatCameraMask; \
    float _VRChatMirrorMode; \
    float _VRChatFaceMirrorMode; \
    float3 _VRChatMirrorCameraPos; \
    float3 _VRChatScreenCameraPos; \
    float4 _VRChatScreenCameraRot; \
    float3 _VRChatPhotoCameraPos; \
    float4 _VRChatPhotoCameraRot; \
    uint _VRChatTimeUTCUnixSeconds; \
    uint _VRChatTimeNetworkMs; \
    uint _VRChatTimeEncoded1; \
    uint _VRChatTimeEncoded2;

なお,VRChat用の変数を用いるということは,VRChat用のシェーダーである,すなわちBRP用シェーダーであることを意味するため,どのConstant Bufferに含まれるかはあまり意識しなくてよい. そのため, LIL_CUSTOM_PROPERTIES マクロ外でuniform変数を宣言しても問題ない.

今後,もし本体側で宣言された場合,

  • featureマクロが定義されるなら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 マクロの呼び出しを入れておくことで,コンパイラの最適化処理(※)により,頂点シェーダーの先頭あたりに上記のコードを記述したのと同一のコードが生成される. すなわち,より早い段階でearly returnを行うことができる. LIL_V2F_OUT_BASE.positionCSfloat4 であるが, 0.0 / 0.0 は全要素 0.0 / 0.0float4 に暗黙的に変換されるのを利用している. 後続の処理は不要なので,returnしておく.

※下記のコードにおいて,(1) ~ (2), (2) ~ (3)間に x を参照する処理がなければ,(2)の代入処理自体は削除される.これと似た最適化処理を期待.

float x = 0.0;  // (1)

/* ----- 何かの処理 ----- */

x = 1.0;  // (2)

/* ----- 何かの処理 ----- */

x = 0.0;  // (3)

参考: LIL_INITIALIZE_STRUCT なし版のDXD11アセンブリコード(クリックで開く閉じる).

Keywords: DIRECTIONAL
-- Hardware tier variant: Tier 1
-- Vertex shader for "d3d11":
// Stats: 207 math, 13 temp registers, 27 branches

// -- 略 --

   0: if_nz cb0[168].w
   1:   mov o0.xyzw, l(0,0,0,0)
   2:   mov o1.xyzw, l(0,0,0,0)
   3:   mov o2.xyzw, l(0,0,0,0)
   4:   mov o3.xyzw, l(0,0,0,0)
   5:   mov o4.xyzw, l(0,0,0,0)
   6:   mov o6.xyzw, l(0,0,0,0)
   7:   mov o5.xyz, l(0,0,0,0)
   8:   mov o7.xyz, l(0,0,0,0)
   9:   mov o8.xyz, l(0,0,0,0)
  10:   mov o9.xyz, l(0,0,0,0)
  11:   ret
  12: endif
  13: ine r0.x, cb0[183].y, l(0)
  14: ine r0.y, cb0[184].z, l(0)
  15: and r0.x, r0.y, r0.x
  16: if_nz r0.x
  17:   ieq r0.xy, cb0[167].zzzz, l(1, 2, 0, 0)
  18:   add r1.xyz, v0.xyzx, -cb0[99].xyzx
  19:   dp3 r0.z, r1.xyzx, r1.xyzx
  20:   sqrt r0.z, r0.z
  21:   mad r0.z, r0.z, cb0[98].x, cb0[98].y
  22:   movc r0.x, r0.x, r0.z, cb0[98].y
  23:   sincos r1.x, r2.x, cb0[98].z
  24:   add r0.zw, v1.xxxy, l(0.000000, 0.000000, -0.500000, -0.500000)
  25:   mul r0.w, r1.x, r0.w
  26:   mad r0.z, r0.z, r2.x, -r0.w
  27:   add r0.z, r0.z, l(0.500000)
  28:   mad r0.z, r0.z, cb0[98].x, cb0[98].y
  29:   movc r0.x, r0.y, r0.z, r0.x
  30:   if_nz cb0[184].w
  31:     mul r0.z, cb0[101].x, -cb1[0].y
  32:     mul r0.z, r0.z, cb0[101].y
  33:     mul r0.z, r0.z, l(0.016667)
  34:     frc r0.z, r0.z
  35:     add r0.z, r0.z, cb0[101].z
  36:     add r1.x, r0.z, r0.x
  37:     mov r1.y, cb0[98].w
  38:     sample_l r1.xyzw, r1.xyxx, t0.xyzw, s0, l(0.000000)
  39:   else
  40:     resinfo r2.xyzw, l(0), t1.xyzw
  41:     ftoi r0.z, r2.x
  42:     ilt r0.z, l(16), r0.z
  43:     if_nz r0.z
  44:       mul r0.y, cb0[98].w, l(0.062500)
  45:       sample_l r1.xyzw, r0.xyxx, t1.xyzw, s1, l(0.000000)
  46:       mov_sat r1.x, r1.x
  47:     else
  48:       mad r0.x, cb1[0].y, cb0[95].z, -r0.x
  49:       frc r0.x, r0.x
  50:       add_sat r0.x, r0.x, cb0[95].w
  51:       mul r0.x, r0.x, cb0[95].y
  52:       mad_sat r1.x, -r0.x, cb0[95].x, cb0[95].x
  53:     endif
  54:   endif
  55:   mad r0.xyz, v9.xyzx, cb0[100].wwww, cb0[100].xyzx
  56:   mad r0.xyz, r0.xyzx, r1.xxxx, v0.xyzx
  57: else
  58:   mov r0.xyz, v0.xyzx
  59: endif
  60: mul r1.xyz, r0.yyyy, cb3[1].xyzx
  61: mad r1.xyz, cb3[0].xyzx, r0.xxxx, r1.xyzx
  62: mad r1.xyz, cb3[2].xyzx, r0.zzzz, r1.xyzx
  63: add r1.xyz, r1.xyzx, cb3[3].xyzx
  64: dp3 r2.x, v9.xyzx, cb3[4].xyzx
  65: dp3 r2.y, v9.xyzx, cb3[5].xyzx
  66: dp3 r2.z, v9.xyzx, cb3[6].xyzx
  67: dp3 r1.w, r2.xyzx, r2.xyzx
  68: rsq r1.w, r1.w
  69: mul r2.xyz, r1.wwww, r2.xyzx
  70: mul r3.xyz, v10.yyyy, cb3[1].xyzx
  71: mad r3.xyz, cb3[0].xyzx, v10.xxxx, r3.xyzx
  72: mad r3.xyz, cb3[2].xyzx, v10.zzzz, r3.xyzx
  73: dp3 r1.w, r3.xyzx, r3.xyzx
  74: rsq r1.w, r1.w
  75: mul r3.xyz, r1.wwww, r3.xyzx
  76: ne r1.w, cb0[0].x, l(0.000000)
  77: if_nz r1.w
  78:   mov o0.xyzw, l(0xffc00000,0xffc00000,0xffc00000,0xffc00000)
  79:   mov o1.xy, v1.xyxx
  80:   mov o1.zw, v2.xxxy
  81:   mov o2.xy, v3.xyxx
  82:   mov o2.zw, v4.xxxy
  83:   mov o3.xyz, r0.xyzx
  84:   mov o3.w, l(0)
  85:   mov o4.xyz, r1.xyzx
  86:   mov o4.w, l(0)
  87:   mov o6.w, v10.w
  88:   mov o6.xyz, r3.xyzx
  89:   mov o5.xyz, r2.xyzx
  90:   mov o7.xyz, l(0,0,0,0)
  91:   mov o8.xyz, l(0,0,0,0)
  92:   mov o9.xyz, l(0,0,0,0)
  93:   ret
  94: else
  95:   mul r5.xyzw, r1.yyyy, cb4[18].xyzw
  96:   mad r5.xyzw, cb4[17].xyzw, r1.xxxx, r5.xyzw
  97:   mad r5.xyzw, cb4[19].xyzw, r1.zzzz, r5.xyzw
  98:   add r4.xyzw, r5.zxyw, cb4[20].zxyw
  99: endif

参考: LIL_INITIALIZE_STRUCT あり版のDXD11アセンブリコード(クリックで開く閉じる).

Keywords: DIRECTIONAL
-- Hardware tier variant: Tier 1
-- Vertex shader for "d3d11":
// Stats: 207 math, 12 temp registers, 27 branches

// -- 略 --

   0: if_nz cb0[168].w
   1:   mov o0.xyzw, l(0,0,0,0)
   2:   mov o1.xyzw, l(0,0,0,0)
   3:   mov o2.xyzw, l(0,0,0,0)
   4:   mov o3.xyzw, l(0,0,0,0)
   5:   mov o4.xyzw, l(0,0,0,0)
   6:   mov o6.xyzw, l(0,0,0,0)
   7:   mov o5.xyz, l(0,0,0,0)
   8:   mov o7.xyz, l(0,0,0,0)
   9:   mov o8.xyz, l(0,0,0,0)
  10:   mov o9.xyz, l(0,0,0,0)
  11:   ret
  12: endif
  13: ine r0.x, cb0[183].y, l(0)
  14: ine r0.y, cb0[184].z, l(0)
  15: and r0.x, r0.y, r0.x
  16: if_nz r0.x
  17:   ieq r0.xy, cb0[167].zzzz, l(1, 2, 0, 0)
  18:   add r1.xyz, v0.xyzx, -cb0[99].xyzx
  19:   dp3 r0.z, r1.xyzx, r1.xyzx
  20:   sqrt r0.z, r0.z
  21:   mad r0.z, r0.z, cb0[98].x, cb0[98].y
  22:   movc r0.x, r0.x, r0.z, cb0[98].y
  23:   sincos r1.x, r2.x, cb0[98].z
  24:   add r0.zw, v1.xxxy, l(0.000000, 0.000000, -0.500000, -0.500000)
  25:   mul r0.w, r1.x, r0.w
  26:   mad r0.z, r0.z, r2.x, -r0.w
  27:   add r0.z, r0.z, l(0.500000)
  28:   mad r0.z, r0.z, cb0[98].x, cb0[98].y
  29:   movc r0.x, r0.y, r0.z, r0.x
  30:   if_nz cb0[184].w
  31:     mul r0.z, cb0[101].x, -cb1[0].y
  32:     mul r0.z, r0.z, cb0[101].y
  33:     mul r0.z, r0.z, l(0.016667)
  34:     frc r0.z, r0.z
  35:     add r0.z, r0.z, cb0[101].z
  36:     add r1.x, r0.z, r0.x
  37:     mov r1.y, cb0[98].w
  38:     sample_l r1.xyzw, r1.xyxx, t0.xyzw, s0, l(0.000000)
  39:   else
  40:     resinfo r2.xyzw, l(0), t1.xyzw
  41:     ftoi r0.z, r2.x
  42:     ilt r0.z, l(16), r0.z
  43:     if_nz r0.z
  44:       mul r0.y, cb0[98].w, l(0.062500)
  45:       sample_l r1.xyzw, r0.xyxx, t1.xyzw, s1, l(0.000000)
  46:       mov_sat r1.x, r1.x
  47:     else
  48:       mad r0.x, cb1[0].y, cb0[95].z, -r0.x
  49:       frc r0.x, r0.x
  50:       add_sat r0.x, r0.x, cb0[95].w
  51:       mul r0.x, r0.x, cb0[95].y
  52:       mad_sat r1.x, -r0.x, cb0[95].x, cb0[95].x
  53:     endif
  54:   endif
  55:   mad r0.xyz, v9.xyzx, cb0[100].wwww, cb0[100].xyzx
  56:   mad r0.xyz, r0.xyzx, r1.xxxx, v0.xyzx
  57: else
  58:   mov r0.xyz, v0.xyzx
  59: endif
  60: ne r0.w, cb0[0].x, l(0.000000)
  61: if_nz r0.w
  62:   mov o0.xyzw, l(0xffc00000,0xffc00000,0xffc00000,0xffc00000)
  63:   mov o1.xyzw, l(0,0,0,0)
  64:   mov o2.xyzw, l(0,0,0,0)
  65:   mov o3.xyzw, l(0,0,0,0)
  66:   mov o4.xyzw, l(0,0,0,0)
  67:   mov o6.xyzw, l(0,0,0,0)
  68:   mov o5.xyz, l(0,0,0,0)
  69:   mov o7.xyz, l(0,0,0,0)
  70:   mov o8.xyz, l(0,0,0,0)
  71:   mov o9.xyz, l(0,0,0,0)
  72:   ret
  73: else
  74:   mul r8.xyz, r0.yyyy, cb3[1].xyzx
  75:   mad r8.xyz, cb3[0].xyzx, r0.xxxx, r8.xyzx
  76:   mad r8.xyz, cb3[2].xyzx, r0.zzzz, r8.xyzx
  77:   add r5.xyz, r8.xyzx, cb3[3].xyzx
  78:   mul r8.xyzw, r5.yyyy, cb4[18].xyzw
  79:   mad r8.xyzw, cb4[17].xyzw, r5.xxxx, r8.xyzw
  80:   mad r8.xyzw, cb4[19].xyzw, r5.zzzz, r8.xyzw
  81:   add r7.xyzw, r8.zxyw, cb4[20].zxyw
  82:   dp3 r8.x, v9.xyzx, cb3[4].xyzx
  83:   dp3 r8.y, v9.xyzx, cb3[5].xyzx
  84:   dp3 r8.z, v9.xyzx, cb3[6].xyzx
  85:   dp3 r0.w, r8.xyzx, r8.xyzx
  86:   rsq r0.w, r0.w
  87:   mul r8.xyz, r0.wwww, r8.xyzx
  88:   mul r9.xyz, v10.yyyy, cb3[1].xyzx
  89:   mad r9.xyz, cb3[0].xyzx, v10.xxxx, r9.xyzx
  90:   mad r9.xyz, cb3[2].xyzx, v10.zzzz, r9.xyzx
  91:   dp3 r0.w, r9.xyzx, r9.xyzx
  92:   rsq r0.w, r0.w
  93:   mul r9.xyz, r0.wwww, r9.xyzx
  94:   mov r10.xy, v1.xyxx
  95:   mov r10.zw, v2.xxxy
  96:   mov r11.xy, v3.xyxx
  97:   mov r11.zw, v4.xxxy
  98:   mov r9.w, v10.w
  99:   mov o1.xyzw, r10.xyzw
 100:   mov o2.xyzw, r11.xyzw
 101:   mov o6.xyzw, r9.xyzw
 102:   mov o3.xyz, r0.xyzx
 103:   mov o5.xyz, r8.xyzx
 104: endif

NaNの警告回避

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

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

シェーダー名の制約

  • シェーダー名に "Multi" が含まれる場合はmulti版のシェーダー
  • シェーダー名に "Lite" が含まれる場合はlite版のシェーダー

と判定する処理が本体にいくつかあるため,

  • lilCustomShaderDatas.lilblock
  • カスタムシェーダーのインスペクタ

に記述するシェーダー名にMultiやLiteを含めると,意図しないエラーが発生することがあるので用いてはならない. 例えば "koturn/lilToonMultiRimLight" という名前にすると,Lite版のシェーダーに切り替えた際にMulti版の処理を通り,特定プロパティが存在しないことにより処理エラーが発生する.

ただし,v2.3.0 で改善したので,恣意的に誤検知される名前にしない限りはエラーは発生しなくなった.

マテリアルエディタ

キーワードについて

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

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

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列目以降が各言語に応じた文言である. lilToon本体や lilToonGeometryFX では,キー名にsプレフィックスが用いられている.

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   側面を生成 측면 생성   生成侧面    生成側面

TSVとは言っても,CSVにおけるRFC4180のようなデータ自体に改行やセパレータが含まれることを考慮したパースの仕方ではなく,単純に1行読み取ってタブ文字で分割している処理となっていることに注意. また,1行目のヘッダ行は単純に読み飛ばしされているため,ヘッダ順序を変えたからといって言語の記述順が変わるわけではない. 先頭フィールドから順に,下記のように解釈される.

  1. キー名
  2. 英語名
  3. 日本語名
  4. 韓国語
  5. 中国語(簡体字)
  6. 中国語(繁体字)

lilToon 2.1.8 以前は設定している言語に対応するフィールド項目が存在しない場合,IndexOutOfRangeExceptionが発生し,インスペクタが表示できない問題があったが,lilToon 2.1.9で修正した.

ファイル名は何でもよい(GUIDで参照するため). 作例に習うなら lang_custom.txt . ただし,拡張子 .tsv にしておくと,GitHubでプレビュー表示したときに表形式で表示される利点がある.

GUIDは lang_custom.txt.meta を参照すること.

C# 側では LoadCustomLanguage() メソッドでファイルを読み込み, GetLoc() メソッドでキーを指定してローカライズされた文言を取得する. もし,定義されていないキーであった場合.GetLoc()本体側の言語ファイルのローカライズ結果を返す. 本体側の言語ファイルでも定義されていない場合は,英語フォールバック結果を返そうとするが,基本的にそこで引っ掛かることはなく,結局キー名がそのまま返る.

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

    LoadCustomLanguage("a5875813c34e16a49ae1c8e1a846ea75");  // このGUIDは lilToonGeometryFX の言語ファイルのもの

    // ...
}

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

    var label = GetLoc("sCustomGeometryAnimation");

    // ...
}

.poファイル

lilToon本体では2.0.0からTSV形式の言語ファイルが廃止され,.poファイルによる多言語対応となった.

現状 L10nクラスはinternalであるが,そのうちカスタムシェーダー側で利用可能なように仕組みが整うかもしれない.

言語TSVファイルの読み込み回数を減らす

テンプレートに習って言語TSVファイルの読み込みを行うと, OnGUI() が呼び出される度に言語ファイルの読み込みが行われる. そこまで気になるほどの動作の重さを感じることはないが,無駄なファイルIOは避けたいものである.

カスタムシェーダーの言語ファイルの読み込みが必要なのは下記3場面である.

  • カスタムシェーダーの言語ファイルが読み込まれていないとき
  • 言語を切り替えたとき
  • 別のカスタムシェーダーのインスペクタから当該カスタムシェーダーのインスペクタに表示を切り替えたとき

これを実現するためには,

  • OnGUI() をオーバーライドし,言語ファイルの読み込みが必要であるかどうかを判定
  • LoadCustomProperties() にて前述の判定結果を基に言語ファイルを読み込み

という手段を取るとよい. 具体的には下記のようなコードにするとよい.

    /// <summary>
    /// <see cref="ShaderGUI"/> for the custom shader variations of lilToon.
    /// </summary>
    public sealed class LilToonPluralRimLightInspector : lilToonInspector
    {
        /// <summary>
        /// Name of this custom shader.
        /// </summary>
        public const string ShaderName = "koturn/lilToonPluralRimLight";

        /// <summary>
        /// A flag whether to fold custom properties or not.
        /// </summary>
        private static bool isShowCustomProperties;
        /// <summary>
        /// A language name when the language file was last loaded.
        /// </summary>
        private static string prevLanguageName;

        // ---------- 略 ----------

        /// <summary>
        /// Draw property items.
        /// </summary>
        /// <param name="materialEditor">The <see cref="MaterialEditor"/> that are calling this <see cref="OnGUI(MaterialEditor, MaterialProperty[])"/> (the 'owner').</param>
        /// <param name="props">Material properties of the current selected shader.</param>
        public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)
        {
            _shouldLoadLanguage = lts == null || lts.name != ShaderName + "/lilToon" || prevLanguageName != lilLanguageManager.langSet.languageName;

            base.OnGUI(materialEditor, props);
        }

        /// <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("/[Optional] ");

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

            if (_shouldLoadLanguage)
            {
                LoadCustomLanguage(AssetGuid.LangCustom);
                prevLanguageName = lilLanguageManager.langSet.languageName;
            }

            // ---------- 略 ----------
        }
    }

項目の検索に対応する

lilToon本体に備わっている機能として,インスペクタの下記検索窓に項目名やプロパティ名を入力することで,表示項目を絞り込むことができる.

lilToonインスペクタの検索窓

しかし,単純にカスタムシェーダーのインスペクタを作成した場合は,どのようなキーワードを用いても,カスタムシェーダーで追加したプロパティが表示されてしまう. 検索キーワードに応じて描画しないようにするためには, lilEditorGUI.ChecPropertyToDraw() を用いて判定を行う必要がある.

if (lilEditorGUI.CheckPropertyToDraw(_fooProp))
{
    m_MaterialEditor.ShaderProperty(_fooProp, "Label for toggle property");  // MaterialProperty _fooProp;
}

項目の多言語を考慮するなら下記のとおり.

var label = GetLoc("sCustomGeometryAnimation");
if (lilEditorGUI.CheckPropertyToDraw(_fooProp) || lilEditorGUI.CheckPropertyToDraw(label))
{
    m_MaterialEditor.ShaderProperty(_fooProp, "Label for toggle property");  // MaterialProperty _fooProp;
}

ただし,これは displayName が言語ファイルのキー名と同一になっていない場合のみ. lilCustomShaderProperties.lilblock のプロパティ定義でキー名が含まれているなら,最初の1つの判定のみで済む.

        _FooProp ("sFooProp", Int) = 0

また,このように定義している場合は,いちいち GetLoc()lilEditorGUI.CheckPropertyToDraw() の呼び出しを行う必要はなく, lilEditorGUI.LocalizedProperty() の呼び出しだけで済む.

lilEditorGUI.LocalizedProperty(m_MaterialEditor, _fooProp);

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]);
        }
    }
}

ただし,上記コードは

  • Undo (Ctrl-Z) が考慮されていない
  • C# のコードとしてイマイチ
  • inspector.ConvertMaterialToCustomShader の処理が大袈裟である(lilToon本体とカスタムシェーダーの全てのバリエーションについて Shader.Find() を呼び出す)
  • Material Variantを除外していない

ので,下記のようにするのがオススメである.

/// <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 material in Selection.GetFiltered<Material>(SelectionMode.Assets))
    {
#if UNITY_2022_1_OR_NEWER
        if (material.parent != null)
        {
            Debug.LogWarningFormat("Ignore {0} because it is Material Variant", AssetDatabase.GetAssetPath(material));
            continue;
        }
#endif  // UNITY_2022_1_OR_NEWER

        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] lilToonOutlineOnlyCutout": return shaderName + "/[Optional] OutlineOnly/Cutout";
        case "_lil/[Optional] lilToonOutlineOnlyTransparent": 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] lilToonFurOnlyTransparent": 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/[Optional] 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 material in Selection.GetFiltered<Material>(SelectionMode.Assets))
    {
#if UNITY_2022_1_OR_NEWER
        if (material.parent != null)
        {
            Debug.LogWarningFormat("Ignore {0} because it is Material Variant", AssetDatabase.GetAssetPath(material));
            continue;
        }
#endif  // UNITY_2022_1_OR_NEWER

        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] lilToonOutlineOnlyCutout";
        case shaderName + "/[Optional] OutlineOnly/Transparent": return "_lil/[Optional] lilToonOutlineOnlyTransparent";
        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] lilToonFurOnlyTransparent";
        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/[Optional] 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のラベルとして使用できる.

オリジナルシェーダーとカスタムシェーダーの対応表

オリジナルシェーダーの名前とカスタムシェーダー名の対応表は下記の通り(横長なので折り畳みにした). *LIL_SHADER_NAME*lilCustomShaderDatas.lilblock で指定するシェーダー名である.

(カスタムシェーダーは .lilcontainer を編集すれば自由にシェーダー名は変更可能であるが,テンプレートにならった場合の例を記載した)

シェーダー名対応表(クリックで開く閉じる).

No. lilShaderManager メンバ名 ファイル名 シェーダー名 シェーダー名(カスタム)
1 lts lts.shader lilToon *LIL_SHADER_NAME*/lilToon
2 ltsc lts_cutout.shader Hidden/lilToonCutout Hidden/*LIL_SHADER_NAME*/Cutout
3 ltst lts_trans.shader Hidden/lilToonTransparent Hidden/*LIL_SHADER_NAME*/Transparent
4 ltsot lts_onetrans.shader Hidden/lilToonOnePassTransparent Hidden/*LIL_SHADER_NAME*/OnePassTransparent
5 ltstt lts_twotrans.shader Hidden/lilToonTwoPassTransparent Hidden/*LIL_SHADER_NAME*/TwoPassTransparent
6 ltso lts_o.shader Hidden/lilToonOutline Hidden/*LIL_SHADER_NAME*/OpaqueOutline
7 ltsco lts_cutout_o.shader Hidden/lilToonCutoutOutline Hidden/*LIL_SHADER_NAME*/CutoutOutline
8 ltsto lts_trans_o.shader Hidden/lilToonTransparentOutline Hidden/*LIL_SHADER_NAME*/TransparentOutline
9 ltsoto lts_onetrans_o.shader Hidden/lilToonOnePassTransparentOutline Hidden/*LIL_SHADER_NAME*/OnePassTransparentOutline
10 ltstto lts_twotrans_o.shader Hidden/lilToonTwoPassTransparentOutline Hidden/*LIL_SHADER_NAME*/TwoPassTransparentOutline
11 ltsoo lts_oo.shader _lil/[Optional] lilToonOutlineOnly *LIL_SHADER_NAME*/[Optional] OutlineOnly/Opaque
12 ltscoo lts_cutout_oo.shader _lil/[Optional] lilToonOutlineOnlyCutout *LIL_SHADER_NAME*/[Optional] OutlineOnly/Cutout
13 ltstoo lts_trans_oo.shader _lil/[Optional] lilToonOutlineOnlyTransparent *LIL_SHADER_NAME*/[Optional] OutlineOnly/Transparent
14 ltstess lts_tess.shader Hidden/lilToonTessellation Hidden/*LIL_SHADER_NAME*/Tessellation/Opaque
15 ltstessc lts_tess_cutout.shader Hidden/lilToonTessellationCutout Hidden/*LIL_SHADER_NAME*/Tessellation/Cutout
16 ltstesst lts_tess_trans.shader Hidden/lilToonTessellationTransparent Hidden/*LIL_SHADER_NAME*/Tessellation/Transparent
17 ltstessot lts_tess_onetrans.shader Hidden/lilToonTessellationOnePassTransparent Hidden/*LIL_SHADER_NAME*/Tessellation/OnePassTransparent
18 ltstesstt lts_tess_twotrans.shader Hidden/lilToonTessellationTwoPassTransparent Hidden/*LIL_SHADER_NAME*/Tessellation/TwoPassTransparent
19 ltstesso lts_tess_o.shader Hidden/lilToonTessellationOutline Hidden/*LIL_SHADER_NAME*/Tessellation/OpaqueOutline
20 ltstessco lts_tess_cutout_o.shader Hidden/lilToonTessellationCutoutOutline Hidden/*LIL_SHADER_NAME*/Tessellation/CutoutOutline
21 ltstessto lts_tess_trans_o.shader Hidden/lilToonTessellationTransparentOutline Hidden/*LIL_SHADER_NAME*/Tessellation/TransparentOutline
22 ltstessoto lts_tess_onetrans_o.shader Hidden/lilToonTessellationOnePassTransparentOutline Hidden/*LIL_SHADER_NAME*/Tessellation/OnePassTransparentOutline
23 ltstesstto lts_tess_twotrans_o.shader Hidden/lilToonTessellationTwoPassTransparentOutline Hidden/*LIL_SHADER_NAME*/Tessellation/TwoPassTransparentOutline
24 ltsl ltsl.shader Hidden/lilToonLite *LIL_SHADER_NAME*/lilToonLite
25 ltslc ltsl_cutout.shader Hidden/lilToonLiteCutout Hidden/*LIL_SHADER_NAME*/Lite/Cutout
26 ltslt ltsl_trans.shader Hidden/lilToonLiteTransparent Hidden/*LIL_SHADER_NAME*/Lite/Transparent
27 ltslot ltsl_onetrans.shader Hidden/lilToonLiteOnePassTransparent Hidden/*LIL_SHADER_NAME*/Lite/OnePassTransparent
28 ltsltt ltsl_twotrans.shader Hidden/lilToonLiteTwoPassTransparent Hidden/*LIL_SHADER_NAME*/Lite/TwoPassTransparent
29 ltslo ltsl_o.shader Hidden/lilToonLiteOutline Hidden/*LIL_SHADER_NAME*/Lite/OpaqueOutline
30 ltslco ltsl_cutout_o.shader Hidden/lilToonLiteCutoutOutline Hidden/*LIL_SHADER_NAME*/Lite/CutoutOutline
31 ltslto ltsl_trans_o.shader Hidden/lilToonLiteTransparentOutline Hidden/*LIL_SHADER_NAME*/Lite/TransparentOutline
32 ltsloto ltsl_onetrans_o.shader Hidden/lilToonLiteOnePassTransparentOutline Hidden/*LIL_SHADER_NAME*/Lite/OnePassTransparentOutline
33 ltsltto ltsl_twotrans_o.shader Hidden/lilToonLiteTwoPassTransparentOutline Hidden/*LIL_SHADER_NAME*/Lite/TwoPassTransparentOutline
34 ltsref lts_ref.shader Hidden/lilToonRefraction Hidden/*LIL_SHADER_NAME*/Refraction
35 ltsrefb lts_ref_blur.shader Hidden/lilToonRefractionBlur Hidden/*LIL_SHADER_NAME*/RefractionBlur
36 ltsfur lts_fur.shader Hidden/lilToonFur Hidden/*LIL_SHADER_NAME*/Fur
37 ltsfurc lts_fur_cutout.shader Hidden/lilToonFurCutout Hidden/*LIL_SHADER_NAME*/FurCutout
38 ltsfurtwo lts_fur_two.shader Hidden/lilToonFurTwoPass Hidden/*LIL_SHADER_NAME*/FurTwoPass
39 ltsfuro lts_furonly.shader _lil/[Optional] lilToonFurOnlyTransparent *LIL_SHADER_NAME*/[Optional] FurOnly/Transparent
40 ltsfuroc lts_furonly_cutout.shader _lil/[Optional] lilToonFurOnlyCutout *LIL_SHADER_NAME*/[Optional] FurOnly/Cutout
41 ltsfurotwo lts_furonly_two.shader _lil/[Optional] lilToonFurOnlyTwoPass *LIL_SHADER_NAME*/[Optional] FurOnly/TwoPass
42 ltsgem lts_gem.shader Hidden/lilToonGem Hidden/*LIL_SHADER_NAME*/Gem
43 ltsfs lts_fakeshadow.shader _lil/[Optional] lilToonFakeShadow *LIL_SHADER_NAME*/[Optional] FakeShadow
44 ltsover lts_overlay.shader _lil/[Optional] lilToonOverlay *LIL_SHADER_NAME*/[Optional] Overlay
45 ltsoover lts_overlay_one.shader _lil/[Optional] lilToonOverlayOnePass *LIL_SHADER_NAME*/[Optional] OverlayOnePass
46 ltslover ltsl_overlay.shader _lil/[Optional] lilToonLiteOverlay *LIL_SHADER_NAME*/[Optional] LiteOverlay
47 ltsloover ltsl_overlay_one.shader _lil/[Optional] lilToonLiteOverlayOnePass *LIL_SHADER_NAME*/[Optional] LiteOverlayOnePass
48 ltsm ltsmulti.shader _lil/lilToonMulti *LIL_SHADER_NAME*/lilToonMulti
49 ltsmo ltsmulti_o.shader Hidden/lilToonMultiOutline Hidden/*LIL_SHADER_NAME*/MultiOutline
50 ltsmref ltsmulti_ref.shader Hidden/lilToonMultiRefraction Hidden/*LIL_SHADER_NAME*/MultiRefraction
51 ltsmfur ltsmulti_fur.shader Hidden/lilToonMultiFur Hidden/*LIL_SHADER_NAME*/MultiFur
52 ltsmgem ltsmulti_gem.shader Hidden/lilToonMultiGem Hidden/*LIL_SHADER_NAME*/MultiGem
53 ltsbaker ltspass_baker.shader Hidden/ltsother_baker
54 ltspo ltspass_opaque.shader Hidden/ltspass_opaque Hidden/*LIL_SHADER_NAME*/ltspass_opaque
55 ltspc ltspass_cutout.shader Hidden/ltspass_cutout Hidden/*LIL_SHADER_NAME*/ltspass_cutout
56 ltspt ltspass_transparent.shader Hidden/ltspass_transparent Hidden/*LIL_SHADER_NAME*/ltspass_transparent
57 ltsptesso ltspass_tess_opaque.shader Hidden/ltspass_tess_opaque Hidden/*LIL_SHADER_NAME*/ltspass_tess_opaque
58 ltsptessc ltspass_tess_cutout.shader Hidden/ltspass_tess_cutout Hidden/*LIL_SHADER_NAME*/ltspass_tess_cutout
59 ltsptesst ltspass_tess_transparent.shader Hidden/ltspass_tess_transparent Hidden/*LIL_SHADER_NAME*/ltspass_tess_transparent
60 ltspass_bakeramp.shader Hidden/ltsother_bakeramp
61 ltspass_dummy.shader Hidden/ltspass_dummy
62 ltspass_lite_cutout.shader Hidden/ltspass_lite_cutout Hidden/*LIL_SHADER_NAME*/ltspass_lite_cutout
63 ltspass_lite_opaque.shader Hidden/ltspass_lite_opaque Hidden/*LIL_SHADER_NAME*/ltspass_lite_opaque
64 ltspass_lite_transparent.shader Hidden/ltspass_lite_transparent Hidden/*LIL_SHADER_NAME*/ltspass_lite_transparent
65 ltspass_proponly.shader Hidden/ltspass_proponly

その他

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

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

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

  • シェーダーファイル向け
$ ctags -f shaderlab.lilToon.tags --languages=c --langmap=c:+.shader,c:+.hlsl -R ~/lilToon/Assets/lilToon/Shader/
  • C# 用
$ 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

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

記事内容+自分好みに修正したインスペクタテンプレート. 全部入りのテンプレートのunitypackageは ここから ダウンロードできる. また,GitHubリポジトリとしても置いてある

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

using System;
using System.IO;
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;


namespace lilToon
{
    /// <summary>
    /// <see cref="ShaderGUI"/> for the custom shader variations of lilToon.
    /// </summary>
    public class TemplateAllInspector : lilToonInspector
    {
        /// <summary>
        /// GUID of shader directory.
        /// </summary>
        // TODO: Replace to GUID of your "Shader" directory.
        public const string GuidShaderDir = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
        /// <summary>
        /// Name of this custom shader.
        /// </summary>
        private const string ShaderName = "TemplateAll";

        // Custom properties
        //private MaterialProperty customVariable;

        /// <summary>
        /// A flag whether to fold custom properties or not.
        /// </summary>
        private static bool isShowCustomProperties;
        /// <summary>
        /// A language name when the language file was last loaded.
        /// </summary>
        private static string prevLanguageName;


        /// <summary>
        /// Draw property items.
        /// </summary>
        /// <param name="materialEditor">The <see cref="MaterialEditor"/> that are calling this <see cref="OnGUI(MaterialEditor, MaterialProperty[])"/> (the 'owner').</param>
        /// <param name="props">Material properties of the current selected shader.</param>
        public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] props)
        {
            _shouldLoadLanguage = lts == null || lts.name != ShaderName + "/lilToon" || prevLanguageName != lilLanguageManager.langSet.languageName;

            base.OnGUI(materialEditor, props);
        }

        /// <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("/[Optional] ");

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

            if (_shouldLoadLanguage)
            {
                //LoadCustomLanguage("");
                prevLanguageName = lilLanguageManager.langSet.languageName;
            }

            //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

            var titleLoc = GetLoc("sCustomShaderTitle");
            isShowCustomProperties = Foldout(titleLoc, titleLoc, isShowCustomProperties);
            if (!isShowCustomProperties)
            {
                return;
            }

            using (new EditorGUILayout.VerticalScope(boxOuter))
            {
                EditorGUILayout.LabelField(GetLoc("sCustomPropertyCategory"), 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)]
#pragma warning disable IDE0052 // Remove unread private members
        private static void ConvertMaterialToCustomShaderMenu()
#pragma warning restore IDE0052 // Remove unread private members
        {
            foreach (var material in Selection.GetFiltered<Material>(SelectionMode.Assets))
            {
#if UNITY_2022_1_OR_NEWER
                if (material.parent != null)
                {
                    Debug.LogWarningFormat("Ignore {0} because it is Material Variant", AssetDatabase.GetAssetPath(material));
                    continue;
                }
#endif  // UNITY_2022_1_OR_NEWER

                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, ShaderName + "/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)]
#pragma warning disable IDE0051 // Remove unused private members
        private static bool ValidateConvertMaterialToCustomShaderMenu()
#pragma warning restore IDE0051 // Remove unused private members
        {
            foreach (var material in Selection.GetFiltered<Material>(SelectionMode.Assets))
            {
#if UNITY_2022_1_OR_NEWER
                if (material.parent != null)
                {
                    Debug.LogWarningFormat("Ignore {0} because it is Material Variant", AssetDatabase.GetAssetPath(material));
                    continue;
                }
#endif  // UNITY_2022_1_OR_NEWER

                if (GetCorrespondingCustomShaderName(material.shader.name) != null)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Try to replace the shader of the material to original lilToon shader.
        /// </summary>
        [MenuItem("Assets/" + ShaderName + "/Convert material to original shader", false, 1101)]
#pragma warning disable IDE0051 // Remove unused private members
        private static void ConvertMaterialToOriginalShaderMenu()
#pragma warning restore IDE0051 // Remove unused private members
        {
            foreach (var material in Selection.GetFiltered<Material>(SelectionMode.Assets))
            {
#if UNITY_2022_1_OR_NEWER
                if (material.parent != null)
                {
                    Debug.LogWarningFormat("Ignore {0} because it is Material Variant", AssetDatabase.GetAssetPath(material));
                    continue;
                }
#endif  // UNITY_2022_1_OR_NEWER

                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, ShaderName + "/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)]
#pragma warning disable IDE0051 // Remove unused private members
        private static bool ValidateConvertMaterialToOriginalShader()
#pragma warning restore IDE0051 // Remove unused private members
        {
            foreach (var material in Selection.GetFiltered<Material>(SelectionMode.Assets))
            {
#if UNITY_2022_1_OR_NEWER
                if (material.parent != null)
                {
                    Debug.LogWarningFormat("Ignore {0} because it is Material Variant", AssetDatabase.GetAssetPath(material));
                    continue;
                }
#endif  // UNITY_2022_1_OR_NEWER

                if (GetCorrespondingOriginalShaderName(material.shader.name) != null)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Callback method for menu item which refreshes shader cache and reimport.
        /// </summary>
        [MenuItem("Assets/" + ShaderName + "/Refresh shader cache", false, 2000)]
#pragma warning disable IDE0052 // Remove unread private members
        private static void RefreshShaderCacheMenu()
#pragma warning restore IDE0052 // Remove unread private members
        {
            var result = NativeMethods.Open("Library/ShaderCache.db", out var pDb);
            if (result != 0)
            {
                Debug.LogErrorFormat("Failed to open Library/ShaderCache.db [{0}]", result);
                return;
            }

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

            var shaderDirPath = AssetDatabase.GUIDToAssetPath(GuidShaderDir);
            if (shaderDirPath.Length == 0)
            {
                Debug.LogWarning("Cannot find file or directory corresponding to GUID: " + GuidShaderDir);
                return;
            }
            if (!Directory.Exists(shaderDirPath))
            {
                Debug.LogWarningFormat("Directory not found: {0} ({1})", shaderDirPath, GuidShaderDir);
                return;
            }

            AssetDatabase.ImportAsset(shaderDirPath, 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)]
#pragma warning disable IDE0051 // Remove unused private members
        private static bool ValidateRefreshShaderCacheMenu()
#pragma warning restore IDE0051 // Remove unused private members
        {
            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] lilToonOutlineOnlyCutout": return ShaderName + "/[Optional] OutlineOnly/Cutout";
                case "_lil/[Optional] lilToonOutlineOnlyTransparent": 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] lilToonFurOnlyTransparent": 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/[Optional] 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] lilToonOutlineOnlyCutout";
                case ShaderName + "/[Optional] OutlineOnly/Transparent": return "_lil/[Optional] lilToonOutlineOnlyTransparent";
                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] lilToonFurOnlyTransparent";
                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/[Optional] 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="pDb">SQLite db handle.</param>
            /// <returns>Result code.</returns>
            /// <remarks>
            /// <seealso href="https://www.sqlite.org/c3ref/open.html"/>
            /// </remarks>
            [DllImport(LibraryName, EntryPoint = "sqlite3_open16", CallingConvention = CallConv, CharSet = CharSet.Unicode)]
            public static extern int Open([In] string filePath, out IntPtr pDb);

            /// <summary>
            /// Close database.
            /// </summary>
            /// <param name="pDb">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 pDb);

            /// <summary>
            /// Execute specified SQL.
            /// </summary>
            /// <param name="pDb">SQLite db handle.</param>
            /// <param name="sql">SQL to be evaluated.</param>
            /// <param name="pCallback">Callback function.</param>
            /// <param name="pCallbackArg">1st argument to callback.</param>
            /// <param name="pErrMsg">Error message 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 pDb, [In] string sql, IntPtr pCallback = default(IntPtr), IntPtr pCallbackArg = default(IntPtr), IntPtr pErrMsg = default(IntPtr));
        }
    }
}

作例

Templateと作例を比較すると勉強になるはず.

Booth(無料)

Booth(有料)

GitHub

カスタムシェーダーではない作例

以下はlilToonのカスタムシェーダーに分類されない作例である.

  • 【無料】ミラー、カメラで映ったり映らなかったりするlilToon【無料】
    • lilToon本体を書き換えた派生シェーダー.
      そのため,本体側の変更への追従は作者の対応を待たなければならない.
  • lilToon互換 メルトディゾルブシェーダー【無料配布】
    • カスタムシェーダーの作り方に則っているわけではなく,lilToonのインスペクタを用いるようにしたlilToonベースのシェーダー.
      そのため,lilToonの最適化機能を利用できない,描画モードを変更すると,本体のシェーダーに差し替わってしまう挙動をしてしまう.
      また,本体側のプロパティ追加に追従できず,バージョンアップした際にコンパイルエラーが発生する将来的な可能性がある.

レイマーチングにおけるマーチングループの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

に置き換えるとよい.

まとめ

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

参考文献