【Unity】【シェーダ】使っていないけどシリアライズされてしまっているシェーダプロパティを削除する

Unityでシェーダを切り替えると、切替前のシェーダのプロパティがマテリアルにシリアライズされたままになります。
つまりシェーダを切り替えまくると無駄なプロパティによりマテリアルのファイルサイズが大きくなっていきます。

この記事ではこれを解消する方法を紹介します。

問題点

問題点を把握するために、まずは下記のようなシンプルなシェーダを書きます。
テクスチャとfloat型のプロパティを一つずつ持っています。

Shader "SerializeTest"
{
    Properties
    {
        _ExampleTex ("Texture", 2D) = "white" {}
        _ExampleFloat ("Float", float) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
            
           #include "UnityCG.cginc"

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

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

            sampler2D _ExampleTex;
            float4 _ExampleTex_ST;
            float _ExampleFloat;
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _ExampleTex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_ExampleTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

次にこのシェーダをアサインしたマテリアルを作ります。
デバッグモードでインスペクタを見るとこんな感じです。

f:id:halya_11:20181002152014p:plain

テクスチャとfloat値がシリアライズされていることが確認できます。

次に、シェーダの_ExampleTexを_ExampleTex2に、_ExampleFloatを_ExampleFloat2にリネームします。

Shader "SerializeTest"
{
    Properties
    {
        // リネームした
        _ExampleTex2 ("Texture", 2D) = "white" {}
        _ExampleFloat2 ("Float", float) = 0
    }

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

そしてMaterialのインスペクタからそれぞれに値を適当に入れます。

するとリネーム前の使用していないプロパティが残ったまま、リネーム後のプロパティが追加されます。

f:id:halya_11:20181002152201p:plain

テクスチャの参照も残ったままなので、Exportする際にはDependencyと認識されてしまいます。

他シェーダのプロパティはシェーダを頻繁に切り替えるマテリアルの場合は消すべきではないですが、
大体のケースでは不要なので消してしまいたいところです。

解決方法

解決方法についてはスマートな方法がなく、シリアライズされたプロパティを上から見ていって不要なものであれば削除していきます。

#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;

public class Example {

    [MenuItem("CONTEXT/Material/Remove Unused Properties")]
    private static void RemoveUnusedMaterialProperties(MenuCommand menuCommand)
    {
        var material    = menuCommand.context as Material;
        if (material == null) {
            return;
        }

        var so          = new SerializedObject(material);
        so.Update();

        var savedProp   = so.FindProperty("m_SavedProperties");

        // Tex Envs
        var texProp     = savedProp.FindPropertyRelative("m_TexEnvs");
        for (int i = texProp.arraySize - 1; i >= 0; i--) {
            var propertyName    = texProp.GetArrayElementAtIndex(i).FindPropertyRelative("first").stringValue;
            if (!material.HasProperty(propertyName)) {
                texProp.DeleteArrayElementAtIndex(i);
            }
        }

        // Floats
        var floatProp   = savedProp.FindPropertyRelative("m_Floats");
        for (int i = floatProp.arraySize - 1; i >= 0; i--) {
            var propertyName    = floatProp.GetArrayElementAtIndex(i).FindPropertyRelative("first").stringValue;
            if (!material.HasProperty(propertyName)) {
                floatProp.DeleteArrayElementAtIndex(i);
            }
        }

        // Colors
        var colorProp   = savedProp.FindPropertyRelative("m_Colors");
        for (int i = colorProp.arraySize - 1; i >= 0; i--) {
            var propertyName    = colorProp.GetArrayElementAtIndex(i).FindPropertyRelative("first").stringValue;
            if (!material.HasProperty(propertyName)) {
                colorProp.DeleteArrayElementAtIndex(i);
            }
        }

        so.ApplyModifiedProperties();
    }
}
#endif

今回はMaterialのContextMenuから使えるようにしてみました。

f:id:halya_11:20181002155742p:plain