【Unity2020】material.HasPropertyの挙動について

2024/04/16に公開

はじめに

material.HasPropertyを使用すると、マテリアルがプロパティを持っているかどうかを取得することができます。

https://docs.unity3d.com/2020.3/Documentation/ScriptReference/Material.HasProperty.html

このメソッドの振る舞いについて、直感に反するものがあったため記事としてまとめてみることにします。

環境

  • Unity2020.3.32f1
  • Built-in RP

使用したコード

シェーダー

今回は二つのシェーダーを検証に使用します。

  1. プロパティ _A を持つシェーダー Shader1_HasPropA.shader
  2. プロパティ _A を持たないシェーダー Shader2_NoPropA.shader

これらのシェーダーは、Resourcesフォルダ直下に配置し、Resources.Loadで読み込めるようにしておきます。

Shader1_HasProp.shader
Shader1_HasProp.shader

Shader "Unlit/Shader1_HasPropA"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _A ("A", Float) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}
Shader2_NoPropA.shader
Shader2_NoPropA.shader
Shader "Unlit/Shader2_NoPropA"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

C#コード

今回の検証にあたって以下のC#メソッドを使用して、material.HasPropertyの振る舞いを検証することとします。

static void CheckProperty(Material mat)
{
    var propertyName = "_A";
    int propertyID = Shader.PropertyToID(propertyName);
    bool hasProperty = mat.HasProperty(propertyID);
    Debug.Log($"material.HasProperty({propertyName}) -> {hasProperty}");
}

このメソッドを実行すると、HasPropertyの結果がConsole上へ出力されます。

検証1 : シェーダーから動的に作成

マテリアルをシェーダーから動的に作成した場合、以下のような結果となりました

  • シェーダーが_Aを持つ場合、HasPropertytrue を返す
  • シェーダーが_Aを持たない場合、HasPropertyfalse を返す
var s1 = Resources.Load<Shader>("Shader1_HasPropA");
var m1 = new Material(s1);
CheckProperty(m1); // true

var s2 = Resources.Load<Shader>("Shader2_NoPropA");
var m2 = new Material(s2);
CheckProperty(m2); // false

検証2 : マテリアルを複製した場合

マテリアルをシェーダーから複製しても、HasPropertyが返す結果に変化はありませんでした。

var s1 = Resources.Load<Shader>("Shader1_HasPropA");
var s2 = Resources.Load<Shader>("Shader2_NoPropA");

var m1 = new Material(s1);
CheckProperty(m1);// true
CheckProperty(new Material(m1));// true

var m2 = new Material(s2);
CheckProperty(m2);// false
CheckProperty(new Material(m2));// false

検証3 : material.SetFloatでプロパティを書き換えた場合

プロパティ _A を持たないシェーダーのマテリアルに対し、
SetFloatでプロパティ_Aの値を設定するとHasPropertytrueを返すようになります。

このマテリアルを複製すると、HasPropertyfalseを返すようになります。

var s2 = Resources.Load<Shader>("Shader2_NoPropA");
var m2 = new Material(s2);
CheckProperty(m2); // false

m2.SetFloat("_A", 100);
CheckProperty(m2); // true

var m2_copy = new Material(m2);
CheckProperty(m2_copy); // false

シェーダーがプロパティ_Aを持っている場合は、マテリアルを複製してもHasPropertyはtrueを返します。

var s1 = Resources.Load<Shader>("Shader1_HasPropA");
var m1 = new Material(s1);
CheckProperty(m1); // true

var m1_copy = new Material(m1);
CheckProperty(m1_copy); // true

マテリアルを複製した場合、マテリアルのプロパティ情報は
シェーダー由来の状態にリセットされる、というふるまいをするようです。

検証4 : シェーダーを動的にさしかえ

マテリアルのシェーダーを直接変更した場合、マテリアルを複製した時と同じく、
HasPropertyの結果はシェーダー由来の状態に戻るようです。

var s1 = Resources.Load<Shader>("Shader1_HasPropA");
var s2 = Resources.Load<Shader>("Shader2_NoPropA");

var m1 = new Material(s1); 
CheckProperty(m1); // true

m1.shader = s2;
CheckProperty(m1); // false

var s1 = Resources.Load<Shader>("Shader1_HasPropA");
var s2 = Resources.Load<Shader>("Shader2_NoPropA");

var m2 = new Material(s2);
CheckProperty(m2); // false

m2.shader = s1;
CheckProperty(m2); // true

マテリアルが持っているシェーダーと同じシェーダーを設定しても、プロパティの状態は元に戻ります。

var s2 = Resources.Load<Shader>("Shader2_NoPropA");

var m2 = new Material(s2);
CheckProperty(m2);// false

m2.SetFloat("_A", 100);
CheckProperty(m2); // true

m2.shader = s2;
CheckProperty(new Material(m2)); // false

余談 : Unity上で直接プロパティを追加してもHasPropertyに影響はない

UnityのInspectorをDebugモードにすると、マテリアル内部のプロパティ配列を直接いじることができます。
ここでプロパティ_Aを登録したとしても、HasPropertyfalseを返すようです。

var m2 = Resources.Load<Material>("Unlit_Shader2_NoPropA");
CheckProperty(m2);

まとめ

Unity2020.3.32f1においては、以下のような振る舞いをするようです。

  • material.HasProperty はマテリアル自身がプロパティを持っているかどうかを返す
  • シェーダーのPropertiesブロックで定義されてないプロパティに対しても、material.Set系のメソッドでプロパティの値を設定できる
  • シェーダーが指定のプロパティを持っているかどうかの厳密な判定はmaterial.HasPropertyで行うことはできない
    • material.Set系のメソッドが呼ばれていた場合、プロパティが定義されていなかったとしてもmaterial.HasPropertytrueを返す

Discussion