LoginSignup
24
9

More than 1 year has passed since last update.

[Unity]正攻法でUIのマテリアルをいじる

Last updated at Posted at 2021-12-11

本記事はQualiArts Advent Calendar 2021 12日目の記事となります。

シェーダーのプロパティをC#で制御する

UIの彩度を変更するシェーダーを作りました。
シェーダーの _Saturationプロパティを変更すると彩度が変わるシンプルなものです。

彩度のシェーダーコード
Shader "UI/UISaturation"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Saturation("Saturation", Range (0, 2)) = 1
    }
    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" }

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

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

            sampler2D _MainTex;
            float _Saturation;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                float3 grayXfer = float3(0.3, 0.59, 0.11);
                float grayf = dot(grayXfer, col);
                float3 gray = float3(grayf, grayf, grayf);
                return float4(lerp(gray, col, _Saturation), 1.0);
            }
            ENDCG
        }
    }
}

このプロパティをC#スクリプトから制御する方法についてまとめます。
画面収録 2021-12-07 17.32.26.gif

失敗例

Updateの中でGraphic(Imageの親)のマテリアルに SetFloatしてみました。

    [RequireComponent(typeof(Graphic))]
    [ExecuteAlways]
    public class UISaturation2 : MonoBehaviour
    {
        [SerializeField, Range(0, 2)]
        private float _saturation = 1;

        private Graphic _graphic;
        public Graphic graphic => _graphic ? _graphic : _graphic = GetComponent<Graphic>();
        public readonly int saturationPropertyId = Shader.PropertyToID("_Saturation");

        private void Update()
        {
            graphic.material.SetFloat(saturationPropertyId, _saturation);
        }
    }

C# 側から彩度をコントロール出来てるように見えます。
画面収録 2021-12-07 17.50.24-lossy.gif

問題点

Updateの中でSetFloatする方法には問題があります。
graphic.materialは共通のものなので、マテリアルを共有している別のコンポーネントの彩度まで変わってしまいました。
画面収録 2021-12-07 17.52.30-lossy.gif

これを回避するためには、値を変更するタイミングでマテリアルを複製して差し替える必要があります。
しかしEditorでも動かすことを前提にすると、どのタイミングでマテリアルを複製するのか、さらに複製したマテリアルをいつ元に戻すのかの制御が難しくなってしまいます。

正攻法

Unityが用意しているMaskコンポーネントでは、 IMaterialModifierを使ってマテリアルの値を変更してました。
https://docs.unity3d.com/ja/2018.4/ScriptReference/UI.IMaterialModifier.html

Maskを参考にして彩度を変更するスクリプトを正攻法で実装するとこうなります。

    [RequireComponent(typeof(Graphic))]
    [ExecuteAlways]
    [DisallowMultipleComponent]
    [AddComponentMenu("UI/Effects/Saturation", 15)]
    public class UISaturation : UIBehaviour, IMaterialModifier
    {
        [SerializeField]
        private float _saturation = 1;

        [NonSerialized]
        private Graphic _graphic;
        public Graphic graphic => _graphic ? _graphic : _graphic = GetComponent<Graphic>();

        /// <summary>
        /// 彩度変更用のマテリアル
        /// </summary>
        [NonSerialized]
        private Material _saturationMaterial;

        public readonly int saturationPropertyId = Shader.PropertyToID("_Saturation");

        protected override void OnEnable()
        {
            base.OnEnable();
            if(graphic == null) return;
            _graphic.SetMaterialDirty();
        }

        protected override void OnDisable()
        {
            base.OnDisable();
            if (_saturationMaterial != null) DestroyImmediate(_saturationMaterial);
            _saturationMaterial = null;

            if (graphic != null) _graphic.SetMaterialDirty();

        }

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();
            if (!IsActive() || graphic == null) return;
            graphic.SetMaterialDirty();
        }
#endif

        protected override void OnDidApplyAnimationProperties()
        {
            base.OnDidApplyAnimationProperties();
            if (!IsActive() || graphic == null) return;
            graphic.SetMaterialDirty();
        }

        public Material GetModifiedMaterial(Material baseMaterial)
        {
            // 彩度変更に対応していないマテリアルを弾く
            if (IsActive() == false || _graphic == null || !baseMaterial.HasProperty(saturationPropertyId))
                return baseMaterial;

            // マテリアル複製
            if (_saturationMaterial == null)
            {
                _saturationMaterial = new Material(baseMaterial);
                _saturationMaterial.hideFlags = HideFlags.HideAndDontSave;
            }

            // これまでのプロパティを引き継ぐ
            _saturationMaterial.CopyPropertiesFromMaterial(baseMaterial);

            _saturationMaterial.SetFloat(saturationPropertyId, _saturation);

            return _saturationMaterial;
        }
    }

ポイントは IMaterialModifier を実装する GetModifiedMaterial メソッドで、このメソッドはGraphicのマテリアルにDirtyフラグが設定されたタイミングで呼ばれます。
GetModifiedMaterialは以下のように IMaterialModifierを実装した全てのコンポーネントが順番に呼び出されます。

GetModifiedMaterialの仕組み
Graphicに設定されたマテリアル
↓
AコンポーネントのGetModifiedMaterial
↓
BコンポーネントのGetModifiedMaterial
↓
CコンポーネントのGetModifiedMaterial
↓
レンダリングされるマテリアル

よってbaseMaterialには前のコンポーネントで変更されたマテリアルの情報が入ります。

GetModifiedMaterialの内容

GetModifiedMaterial 内の処理を切り出して説明します

            // 彩度変更に対応していないマテリアルを弾く
            if (IsActive() == false || _graphic == null || !baseMaterial.HasProperty(saturationPropertyId))
                return baseMaterial;

ここでは以下の状態の時にマテリアルを変更せずに、処理を終わらせます

  1. 自身のオブジェクトが非アクティブの時
  2. Graphicコンポーネントが見つからない時
    3.. シェーダー が彩度変更に対応していない時
            // マテリアル複製
            if (_saturationMaterial == null)
            {
                _saturationMaterial = new Material(baseMaterial);
                _saturationMaterial.hideFlags = HideFlags.HideAndDontSave;
            }

ここでマテリアルを複製します。これにより個別に彩度を調整できるようになります。
このマテリアルは動的に生成され、保存する必要はないため、HideFlagにHideFlags.HideAndDontSave を指定して、シーンに対して変更通知が飛ばないようにしています。

            // これまでのプロパティを引き継ぐ
            _saturationMaterial.CopyPropertiesFromMaterial(baseMaterial);

GetModifiedMaterialはコンポーネント毎に順番に呼び出されます。そのため前のコンポーネントで変更されたプロパティをここで引継ぎます。

            _saturationMaterial.SetFloat(saturationPropertyId, _saturation);

最後に彩度のパラメーターを設定します。

値変更時にマテリアルを更新する

GetModifiedMaterialを設定しただけではマテリアルは更新されません。GetModifiedMaterialはGraphicのマテリアルにDirtyフラグが設定されたタイミングで呼ばれるため更新したいタイミングでDirtyフラグを設定します

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();
            if (!IsActive() || graphic == null) return;
            graphic.SetMaterialDirty();
        }
#endif

        protected override void OnDidApplyAnimationProperties()
        {
            base.OnDidApplyAnimationProperties();
            if (!IsActive() || graphic == null) return;
            graphic.SetMaterialDirty();
        }

OnValidateOnDidApplyAnimationProperties メソッドをオーバーライドして、Editorから値を変更した時と、アニメーションカーブで値を変更した時にGraphicのマテリアルにDirtyフラグを設定します。

結果

これで、コンポーネント毎に彩度を変えられるようになりました。
また GetModifiedMaterialを使った正攻法でマテリアルを変更する事で、Unityが用意しているMaskコンポーネントと競合することなくマテリアルの値を変更できます。
画面収録 2021-12-07 17.55.53.gif

参考

24
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
9