【Unity】【シェーダ】使用するTEXCOORDをマテリアルから指定できるようにする

使用するTEXCOORDをマテリアルから指定できるようにする方法です。

Unity2018.3.1

作るもの

例えば今Scaleという変数をTEXCOORDに入れて渡してシェーダでこの値を使うことを考えます。
ScaleはTEXCOORD0かTEXCOORD1のxyzwのいずれかに入れて渡されるとします。
こんな時には「何番目のTEXCOORDのどのSwizzle(x/y/z/w)を使うか」をマテリアルから指定できると便利です。

f:id:halya_11:20190120002250p:plain

特にParticle SystemのCustom Dataなどを扱うときに便利です。

シェーダ(ShaderGUIを使う場合)

シェーダはこんな感じに書いておきます。

Shader "Example"
{
    Properties {
        // プロパティを定義
        [HideInInspector]_ScaleCoordIndex ("__scalecoordindex", int)            = 3
        [HideInInspector]_ScaleCoordSwizzle ("__scalecoordswizzle", int)        = 3
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            
           #pragma vertex vert
           #pragma fragment frag

           #include "UnityCG.cginc"
            
            // マクロ定義。cgincファイルに移してもOK
           #define SELECTABLE_COORD_DEFINE(name) uniform int name##CoordIndex; uniform int name##CoordSwizzle;
           #define SELECTABLE_COORD_VERTEX_INPUT(index1, index2) half4 selectableCoord1: TEXCOORD##index1; half4 selectableCoord2: TEXCOORD##index2;
           #define SELECTABLE_COORD_VERTEX_INITIALIZATION(appdata, v2f) half4x4 selectableCoords = { appdata.selectableCoord1, appdata.selectableCoord2, half4(0.0, 0.0, 0.0, 0.0), half4(0.0, 0.0, 0.0, 0.0)};
           #define SELECTABLE_COORD_VERTEX_GET(name) selectableCoords[name##CoordIndex][name##CoordSwizzle]

            // 変数を定義
            SELECTABLE_COORD_DEFINE(_Scale)

            struct appdata
            {
                float4 vertex       : POSITION;
                // 使いたいTEXCOORDのインデックスを入力
                SELECTABLE_COORD_VERTEX_INPUT(1, 2)
            };

            struct v2f
            {
                float4 vertex       : SV_POSITION;
            };
            
            v2f vert (appdata v)
            {
                v2f o           = (v2f)0;

                // 頂点シェーダでSelectableCoordを使うための初期化
                SELECTABLE_COORD_VERTEX_INITIALIZATION(v, o)

                // SelectableCoordによる値の取得
                half4 scale     = SELECTABLE_COORD_VERTEX_GET(_Scale);
                o.vertex        = UnityObjectToClipPos(v.vertex * scale);

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return 1;
            }

            ENDCG
        }
    }

    CustomEditor "ExampleShaderGUI"
}

説明はコメントの通りです。
マクロの部分は汎用的なものなのでcgincファイルに切り出しておくと便利です。

ShaderGUI(ShaderGUIを使う場合)

インスペクタは下記のようにして作ります。

using UnityEngine;
using UnityEditor;

public class ExampleShaderGUI : ShaderGUI 
{
    // TEXCOORDのインデックス
    public enum Coord
    {
        Unused = 3,
        Coord1 = 0,
        Coord2 = 1,
    }

    // TEXCOORDのSwizzle
    public enum Swizzle
    {
        X = 0,
        Y = 1,
        Z = 2,
        W = 3,
    }

    public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
    {
        DrawSelectableCoord("_Scale", properties);
    }

    private void DrawSelectableCoord(string prefix, MaterialProperty[] properties)
    {
        var coordIndexName = prefix + "CoordIndex";
        var coordSwizzleName = prefix + "CoordSwizzle";
        var coordIndexProp = FindProperty(coordIndexName, properties);
        var coordSwizzleProp = FindProperty(coordIndexName, properties);

        // TEXCOORDのインデックスをEnumで入力
        var coordIndex = (Coord)coordIndexProp.floatValue;
        using (var scope = new EditorGUI.ChangeCheckScope()) {
            coordIndex = (Coord)EditorGUILayout.EnumPopup(ObjectNames.NicifyVariableName(coordIndexName), coordIndex);
            if (scope.changed) {
                coordIndexProp.floatValue = (float)coordIndex;
            }
        }
        // TEXCOORDのSwizzleをEnumで入力
        var coordSwizzle = (Swizzle)coordIndexProp.floatValue;
        using (var scope = new EditorGUI.ChangeCheckScope()) {
            coordSwizzle = (Swizzle)EditorGUILayout.EnumPopup(ObjectNames.NicifyVariableName(coordSwizzleName), coordSwizzle);
            if (scope.changed) {
                coordSwizzleProp.floatValue = (float)coordSwizzle;
            }
        }
    }
}

これは単純にシェーダのプロパティを列挙型で入力できるようにしているだけです。

結果

ここまでの結果はこんな感じになります。

f:id:halya_11:20181112140633p:plain

アトリビュート(MaterialPropertyDrawer)を使って書き直したほうがいい気がしてきた

ここまで書いたところで、ShaderGUIで書くよりもMaterialPropertyDrawerを使ったほうが汎用性が高い気がしてきました。

ので書きました。

MaterialPropertyDrawer(アトリビュートを使う場合)

まずMaterialPropertyDrawerはこんな感じに書きます。

using UnityEngine;
using UnityEditor;

public class SelectableCoordDrawer : MaterialPropertyDrawer
{
    // TEXCOORDのインデックス
    public enum Coord
    {
        Unused = 3,
        Coord1 = 0,
        Coord2 = 1,
    }

    // TEXCOORDのSwizzle
    public enum Swizzle
    {
        X = 0,
        Y = 1,
        Z = 2,
        W = 3,
    }

    private string _propertyName;

    public SelectableCoordDrawer(string propertyName)
    {
        _propertyName = propertyName;
    }
    
    public override void OnGUI(Rect position, MaterialProperty prop, GUIContent label, MaterialEditor editor)
    {
        var changed = false;
        position = EditorGUI.PrefixLabel(position, new GUIContent(ObjectNames.NicifyVariableName(_propertyName + "Coord")));
        
        // 描画領域が狭すぎるので少しずらす
        position.width += 30;
        position.x -= 30;
        
        // Coordを描画
        var coordPosition = position;
        coordPosition.xMax -= 30;
        var coordIndex = (Coord)prop.vectorValue.x;
        using (var scope = new EditorGUI.ChangeCheckScope()) {
            coordIndex = (Coord)EditorGUI.EnumPopup(coordPosition, coordIndex);
            if (scope.changed) {
                changed = true;
            }
        }
        // Swizzleを描画
        var swizzlePosition = position;
        swizzlePosition.x += coordPosition.width + 4;
        swizzlePosition.width -= coordPosition.width + 4;
        var coordSwizzle = (Swizzle)prop.vectorValue.y;
        using (var scope = new EditorGUI.ChangeCheckScope()) {
            coordSwizzle = (Swizzle)EditorGUI.EnumPopup(swizzlePosition, coordSwizzle);
            if (scope.changed) {
                changed = true;
            }
        }
        if (changed) {
            prop.vectorValue = new Vector4((int)coordIndex, (int)coordSwizzle, prop.vectorValue.z, prop.vectorValue.w);
        }
    }
}

シェーダ(アトリビュートを使う場合)

シェーダはこんな感じです。
Vector型のプロパティにSelectableCoordアトリビュートを付けるだけで使えます。
アトリビュートにしたのでCustomEditorの指定は削除します。

Shader "Example"
{
    Properties {
        // プロパティを定義
        [SelectableCoord(_Scale)]
        _ScaleCoord ("__scalecoord", Vector) = (0, 0, 0, 0)
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            
           #pragma vertex vert
           #pragma fragment frag

           #include "UnityCG.cginc"

            // マクロ定義。cgincファイルに移してもOK
           #define SELECTABLE_COORD_DEFINE(name) uniform int2 name##Coord;
           #define SELECTABLE_COORD_VERTEX_INPUT(index1, index2) half4 selectableCoord1: TEXCOORD##index1; half4 selectableCoord2: TEXCOORD##index2;
           #define SELECTABLE_COORD_VERTEX_INITIALIZATION(appdata, v2f) half4x4 selectableCoords = { appdata.selectableCoord1, appdata.selectableCoord2, half4(0.0, 0.0, 0.0, 0.0), half4(0.0, 0.0, 0.0, 0.0)};
           #define SELECTABLE_COORD_VERTEX_GET(name) selectableCoords[name##Coord.x][name##Coord.y]

            // 変数を定義
            SELECTABLE_COORD_DEFINE(_Scale)

            struct appdata
            {
                float4 vertex : POSITION;
                // 使いたいTEXCOORDのインデックスを入力
                SELECTABLE_COORD_VERTEX_INPUT(1, 2)
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;

                // 頂点シェーダでSelectableCoordを使うための初期化
                SELECTABLE_COORD_VERTEX_INITIALIZATION(v, o)

                // SelectableCoordによる値の取得
                half4 scale = SELECTABLE_COORD_VERTEX_GET(_Scale);
                o.vertex = UnityObjectToClipPos(v.vertex * scale);

                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                return 1;
            }

            ENDCG
        }
    }
}

結果

結果はこんな感じになります。1行にしてみました。

f:id:halya_11:20190120002250p:plain

関連

light11.hatenadiary.com