凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity のシェーダをテンプレートファイルから生成するエディタ拡張をつくってみた

はじめに

Unity でシェーダをテンプレートファイルから生成できるアセットを作ったので、作った動機および使い方の紹介をしたいと思います。

以前 uRaymarching というアセットの一部の機能として作成したものを分離したものになります。

tips.hecomi.com

ダウンロード

github.com

外観

テンプレートファイルを用意すると、中に書かれた分岐や変数に応じて、こんな感じのシェーダ生成 UI が作成されます:

f:id:hecomi:20171020191350p:plain

UI のコンポーネントとしては、チェックボックスによるフラグの ON/OFF や、変数のプルダウンリストからの選択、コード編集ブロックなどになります。

背景・動機

Unity でシェーダの一部分を変更したいときはシェーダバリアントを利用します。

docs.unity3d.com

具体的には以下のように #ifdef の分岐の変数を #pragma multi_compile 文や #pragma shader_feature でキーワードとして指定しておき、それをスクリプト等から切り替えられる、という形です。

CGINCLUDE
...
fixed4 getColor()
{
#ifdef HOGE
    fixed4 col = fixed4(1, 0, 0, 1);
#elif FUGA
    fixed4 col = fixed4(0, 1, 0, 1);
#elif PIYO
    fixed4 col = fixed4(0, 0, 1, 1);
#else
    fixed4 col = fixed4(0, 0, 0, 1);
#endif
    return col;
}
...
ENDCG
...
Pass
{
    ...
    #pragma vertex Vert
    #pragma fragment Frag
    #pragma multi_compile HOGE FUGA PIYO ___
    ...
}
var mat = GetComponent<Renderer>().material;
mat.EnableKeyword("HOGE");

ただ、この方法では CGPROGRAM/CGINCLUDE ~ ENDCG ブロック内での分岐しか出来ません。ちょっとだけプロパティを追加したり、ごっそりパスを追加 / 削除したり、そういったブロックでの外側の分岐や変数を使いたいときもあると思います。この話は以下のロジカルビートさんの「いけにえと雪のセツナ」のエントリでも言及されています:

「いけにえと雪のセツナ」グラフィック解説(第3回・シェーダ編) – 株式会社ロジカルビート

また、コードの一部だけ変えたシェーダを量産したいケースもあります。この際、量産したい数だけシェーダをコピーして用意したり、全ての分岐を単一のシェーダの分岐として突っ込んだりする必要がありますが、これは作業量(元ファイルに変更が入ると全部修正)やリソースの無駄遣い(multi_compile の場合、分岐 x 他の分岐文のシェーダのコンパイルが走る)といった観点からも良くありません。テンプレートファイルで編集したい箇所だけ記述しておき、そこを UI からちょこちょこいじって、生成したシェーダを一括管理出来たら良いな、と思いました。

こういった背景から色々と機能を作成してみました。

使い方

使い方は、以下のような流れになります。

  1. シェーダテンプレートの用意(*/Resources/ShaderTemplates 以下に .txt ファイルを置く)
  2. プロジェクトウィンドウから「Create > Shader > uShaderTemplate > Generator」でジェネレータを生成
  3. シェーダの名前やシェーダテンプレートを生成した Generator のインスペクタから選択
  4. 変数やコードブロックを編集
  5. 「Export」ボタンを押してシェーダを生成
  6. 「Create Material」ボタンを押してマテリアルを生成(or プロジェクトビューの「Create > Material」)

ジェネレータ

ジェネレータは生成されたシェーダのパラメタを保存したりテンプレートに記述された情報から UI を生成したりといった役割を果たすアセット(ScriptableObject)です。プロジェクトウィンドウで作成することが出来ます:

f:id:hecomi:20171020191502p:plain

アセットファイルをクリックすると以下のようなインスペクタが表示され、テンプレートで設定した変数が編集できるようになっています:

f:id:hecomi:20171020191350p:plain

ビルドに含む意味は無いので、Editor ディレクトリ以下に置いておくのが良いと思います。

f:id:hecomi:20171020191138p:plain

テンプレートファイル

さて、試しにサンプルとして追加した「uShaderTemplate > Examples > Editor > Resources > ShaderTemplates > 1. VertFrag.txt」を見てみましょう:

Shader "Custom/<Name>"
{

Properties
{
@block Properties
_MainTex("Texture", 2D) = "white" {}
@endblock
}

SubShader
{

Tags { "Queue"="<Queue=Geometry|Transparent>" "RenderType"="<RenderType=Opaque|Transparent>" }
LOD <LOD=100>

CGINCLUDE

#include "UnityCG.cginc"

struct v2f
{
    float2 uv : TEXCOORD0;
@if UseFog : true
    UNITY_FOG_COORDS(1)
@endif
    float4 vertex : SV_POSITION;
};

@block VertexShader
sampler2D _MainTex;
float4 _MainTex_ST;

v2f vert(appdata_full v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}
@endblock

@block FragmentShader
fixed4 frag(v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);
    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}
@endblock

ENDCG

Pass
{
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
@if UseFog : true
    #pragma multi_compile_fog
@endif
    ENDCG
}

}

CustomEditor "uShaderTemplate.MaterialEditor"

}

コード中に @if hoge<Fuga>@block Piyo といった記述が見つかると思います。これらがテンプレートファイルで使用できる文法になります。#pragma multi_compile#pragma shader_feature と異なり、シェーダ内の任意の場所で使用することが出来ます。

任意の */Resources/ShaderTemplates ディレクトリ下に置くと自動で検出され、「Basic > Shader Template」に表示されるようになります。

テンプレートファイルで使える文法

使える文法は以下になります:

分岐

@if HogeHoge
#define HOGEHOGE
@endif

"Hoge Hoge" とラベル付けられたチェックボックスCondition セクションに表示されるようになります。チェックしたときのみ IF 文の中身が展開されます。

また、@else を追加してチェックされてないときの展開される文も指定できます:

@if HogeHoge2 : false
#define HOGEHOGE2
@else
#define HOGEHOGE3
@endif

なお、@elsif は未実装です。

変数

変数は <> で囲んだものが Variables セクションに表示されるようになります。3 つの書き方があります:

#define Hoge <Hoge> // 空のテキストフィールド
#define Fuga <Fuga=Hoge> // デフォルト値のあるテキストフィールド
#define Piyo <Piyo=Hoge|Fuga|Piyo> // プルダウンリスト

コードブロック

@block ~ @endblock で囲んだ場所はシンタックスハイライト付きのコードエディタとして新しいセクションが生成されます。直接インスペクタからコードを編集しても良いですが、生成後のシェーダでも @block ~ @endblock の記述があり、この内部を外部エディタで編集しても、保存した際に自動で反映されるようになっています(FileWatcher で監視)。

@block Moge
float Moge2() { return _Move * _Moge; }
@endblock

ボタン

f:id:hecomi:20171020193816p:plain

ボタンの各機能は以下になります:

  • Export(Ctrl+R)
    • シェーダをジェネレータの情報を元にエクスポートします。Ctrl + R でも可能です。
  • Create Material
    • 生成されたシェーダを元にマテリアルを生成します。
  • Reset to Default
    • テンプレートに書かれたデフォルト値へと全てのパラメタを変更します。
  • Update Template
    • テンプレートファイルを編集した場合、このボタンを押すと変更が反映されます。
  • Reconvert All
    • 生成されたシェーダを一括変換します。テンプレートの変更を全適用したいときに便利です。

定数

f:id:hecomi:20171024002704p:plain

変数をそれぞれのシェーダで入れる代わりに、定数を格納したアセットを作成し、それを複数のジェネレータで共有することが出来ます。

「Create > Shader > uShaderTemplate > Constants」をプロジェクトビューから選択し、定数ファイルを生成します。アセットには Name と対応する Value を入力することが出来ます。これをジェネレータの Constants フィールドにドラッグ・ドロップすることで適用することが出来ます。注意点として、定数ファイルを修正した場合、その変更を適用したい場合は「Reconvert All」で全てのシェーダを再コンバートする必要があります。

その他

コードエディタについて

本当はコードエディタ部も分離したかったのですが、シェーダの編集に特化した作りになっているので、取り敢えず今はこのままで…。

シンタックスハイライトの仕組みは以下の記事にまとめてあります。

tips.hecomi.com

Tips

  • 生成されたシェーダやジェネレータは場所を移動しても管理されます(シェーダは参照を保持しているため)
  • マテリアルやゲームオブジェクトのインスペクタからも編集できます
    • テンプレートにエラーが有ると表示されなくなるので、テンプレートをいじる時はジェネレータのインスペクタで行うのをオススメします'
  • 生成されたシェーダを利用したマテリアルの参照は「Materials」セクションから参照できます
  • ジェネレータは ScriptableObject のアセットになっています。
    • ScriptableObject はデータをシリアライズするのにとても便利で、実装が面倒な Undo / Redo も特に意識すること無く機能して便利です
    • PostProcessing Stack などもパラメタの管理は ScriptableObject を利用しています

おわりに

ずっと前に作成したものだったのですが、まとめていなかったので書きました。ご要望あれば機能追加していきます。