Material emission fading with easing functions

I created a simple class that supports fading specific material property values in and out. My purpose was to change these dynamically:

  • the emission color of the standard shader
  • the emission float value of a specific shader.

I thought I’d share in case anyone else has use for it, feel free to extend:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Rowlan
{
    /// <summary>
    /// Fade a material property value in and out with specified easing functions.
    /// Supported property types:
    /// + Float
    /// + Color
    ///
    /// Please note that there's also the option to Lerp between materials in Unity:
    /// https://docs.unity3d.com/ScriptReference/Material.Lerp.html
    ///
    /// </summary>
    public class MaterialFader : MonoBehaviour
    {
        #region Public Variables
        [Header("GameObject")]

        public GameObject sceneGameObject;

        [Header("Material")]

        public MaterialPropertyType propertyType = MaterialPropertyType.Float;

        [Tooltip("The property name in the shader, e. g. _EmissionColor")]
        public string propertyNameID;

        [Header( "Fade")]

        public float minimumValue = 0f;
        public float maximumValue = 1f;

        [Tooltip("The fade duration in seconds")]
        public float duration = 1f;

        [Tooltip("The easing meachinsm")]
        public Ease ease = Ease.Linear;

        [Header("Input")]

        [Tooltip("The input key that triggers the fading")]
        public KeyCode toggleKeyCode = KeyCode.None;

        #endregion Public Variables

        #region Internal Variables
        private FadeDirection fadeDirection = FadeDirection.In;

        private Coroutine currentFadeCoroutine = null;

        private readonly List<FadeMaterial> fadeMaterials = new List<FadeMaterial>();

        #endregion Internal Variables

        #region Initialization
        void Start()
        {
            RegisterFadeMaterials();
        }

        private void RegisterFadeMaterials()
        {
            Renderer[] rendererChildren = sceneGameObject.GetComponentsInChildren<Renderer>();

            foreach (Renderer renderer in rendererChildren)
            {
                FadeMaterial fadeMaterial = null;

                switch (propertyType)
                {
                    case MaterialPropertyType.Float:
                        fadeMaterial = new FloatFadeMaterial(renderer, propertyNameID);
                        break;
                    case MaterialPropertyType.Color:
                        fadeMaterial = new ColorFadeMaterial(renderer, propertyNameID);
                        break;
                }

                if (fadeMaterial.IsValid())
                {
                    fadeMaterials.Add(fadeMaterial);
                }
            }
        }
        #endregion Initialization

        #region Fade Trigger
        void Update()
        {
            if (Input.anyKeyDown)
            {
                if (Input.GetKeyDown(toggleKeyCode))
                {
                    if (currentFadeCoroutine != null)
                    {
                        StopCoroutine(currentFadeCoroutine);
                    }

                    // start fading
                    if (fadeDirection == FadeDirection.In)
                    {
                        currentFadeCoroutine = StartCoroutine(Fade(minimumValue, maximumValue));
                    }
                    else if (fadeDirection == FadeDirection.Out)
                    {
                        currentFadeCoroutine = StartCoroutine(Fade(maximumValue, minimumValue));
                    }

                    // toggle fade direction
                    fadeDirection = fadeDirection == FadeDirection.In ? FadeDirection.Out : FadeDirection.In;

                }
            }
        }
        #endregion Fade Trigger

        #region Fade Logic
        IEnumerator Fade( float beginValue, float endValue)
        {
            float timeElapsed = 0;

            while (timeElapsed < duration)
            {
                float value = ease.Lerp(beginValue, endValue, timeElapsed / duration);

                timeElapsed += Time.deltaTime;

                UpdateMaterials(value);

                yield return null;
            }

            // set end value explicitly, otherwise it might not be the specified one because of the time increments
            UpdateMaterials(endValue);
        }

        void UpdateMaterials(float value)
        {
            foreach(FadeMaterial fadeMaterial in fadeMaterials) {
                fadeMaterial.UpdateMaterials(value);
            }
        }

        #endregion Fade Logic

        #region FadeMaterial
        public abstract class FadeMaterial {

            protected string propertyNameID;
            protected Renderer renderer;
            protected List<Material> materials;

            public FadeMaterial(Renderer renderer, string propertyNameID)
            {
                this.renderer = renderer;
                this.propertyNameID = propertyNameID;

                RegisterMaterials();
            }

            private void RegisterMaterials() {

                materials = new List<Material>();

                foreach (Material material in renderer.materials)
                {
                    if (material.HasProperty(propertyNameID))
                    {
                        materials.Add(material);
                    }
                }
            }

            public bool IsValid()
            {
                return materials.Count > 0;
            }

            public abstract void UpdateMaterials(float value);
        }

        public class FloatFadeMaterial: FadeMaterial
        {
            public FloatFadeMaterial(Renderer renderer, string propertyNameID) : base(renderer, propertyNameID)
            {
            }

            public override void UpdateMaterials(float value)
            {
                for (int i = 0; i < materials.Count; i++)
                {

                    Material material = materials[i];
                    material.SetFloat(propertyNameID, value);
                }

                renderer.UpdateGIMaterials();
            }
        }

        public class ColorFadeMaterial: FadeMaterial
        {
            private readonly List<Color> baseColors = new List<Color>();

            public ColorFadeMaterial(Renderer renderer, string propertyNameID) : base(renderer, propertyNameID)
            {
                RegisterBaseColors();
            }

            private void RegisterBaseColors() {

                foreach (Material material in materials)
                {
                    baseColors.Add(material.GetColor(propertyNameID));
                }
            }

            public override void UpdateMaterials(float value)
            {
                for (int i = 0; i < materials.Count; i++)
                {

                    Material material = materials[i];

                    Color baseColor = baseColors[i];
                    Color color = baseColor * value; // note about gamma: for gamma you could use baseColor * Mathf.LinearToGammaSpace( value)

                    material.SetColor(propertyNameID, color);
                }

                // update the material; this is an important step, otherwise the material changes wouldn't show up
                renderer.UpdateGIMaterials();
            }
        }
        #endregion FadeMaterial
    }

    #region Enums
    public enum FadeDirection
    {
        In,
        Out
    }

    public enum MaterialPropertyType
    {
        Float,
        Color
    }

    public enum Ease
    {
        Linear,
        EaseInQuad,
        EaseOutQuad
    }

    public static class EaseExtensions
    {
        public static float Lerp(this Ease ease, float start, float end, float value)
        {
            switch (ease)
            {
                case Ease.Linear: return Mathf.Lerp(start, end, value);
                case Ease.EaseInQuad: end -= start; return end * value * value + start;
                case Ease.EaseOutQuad: end -= start; return -end * value * (value - 2) + start;
                default: throw new ArgumentOutOfRangeException("Unsupported parameter " + ease);
            }
        }

    }
    #endregion Enums
}

Example Setup:

In my case I was toying around with the new Malbers Realistic Wolf. The eyes use the Standard material, property _EmissionColor as color. The fur is a special shader with a property _EmissionPower as float.

Looks like this then:

6921998--812090--magic wolf.gif

Note: Unity also has a material lerp mechanism, in case you have use for it.

1 Like

You probably want to look into using MaterialPropertyBlocks instead of modifying the material directly. Especially as accessing .materials creates a new material, and Unity does not automatically garbage collect materials created this way meaning the above code will create memory leaks if used on objects that are instantiated and destroyed while the game is running.

1 Like

Basic example code for how to use material property blocks:

private MaterialPropertyBlock matBlock;

void Start()
{
  // just need to create this once and reuse it
  matBlock = new MaterialPropertyBlock();
}

void Update()
{
  // get current properly block values, if any are being used, from the renderer
  // useful if multiple scripts might be modifying properties
  renderer.GetPropertyBlock(matBlock);

  // assign color property on material property block
  matBlock.SetColor("_MyColor", myColor);

  // apply back onto renderer
  renderer.SetPropertyBlock(matBlock);
}

That will apply to all materials on the renderer component. Alternatively you can apply custom values to individual material indices if you also put the material index as a second argument for the get / set functions.

Using material property blocks has the nice side benefit of being way faster than modifying materials directly, both on the script side and rendering.

2 Likes

Thank you very much for the expertise! I updated the code according to your suggestion. It was also very helpful with the material indexes. If you have a GameObject with 8 materials and you only need to set the emission on e. g. the HeadLight material of a car, the filtering definitely has to happen.

A question if I may: renderer.UpdateGIMaterials() isn’t necessary anymore now from what I noticed. I see it works now without that invocation. Or should I still apply it?

In case anyone wants the code, I put it on github:

https://github.com/Roland09/Fader

I refactored it heavily to make the fading more general purpose. Now you can e. g. also combine the material fading with other logic, e. g. Enviro’s time. Here’s Material and Enviro time combined:

https://www.youtube.com/watch?v=A6mdaOySVQM

1 Like

Wasn’t necessary before either. That’s only useful if you were using the now deprecated Enlighten dynamic GI, and if the renderer in question was static.

2 Likes