Lesson 5 (Part 5): Controlling Color with MeshRenderer

White boxes falling from the sky is a beautiful site, but it would look even better if the debris appeared to change color. In this section we are going to look at how to access the “color” property of an object programmatically using the Mesh Renderer component.

Every 3D mesh object in our scene has a Mesh Renderer component that includes properties about how this will be visualized. Included in this is the Materials channel that holds the material to be applied to this mesh. (Actually, materials… meshes can have multiple materials applied to the same object)

To change the color of a material (the “Albedo” color, in our standard unity material), we will use the Material.SetColor( ) function. This method takes two parameters, a string containing the name of the channel we wish to set, and a Color value.

The Color type is similar in structure to a Vector3, but instead of (x, y, z) values, it holds (r, g, b, a) values, each one a float corresponding to the Red, Green, Blue and Alpha (transparency) channels. (If you’ve never heard of rgb values to define colors, check out the Wikipedia entry on the RGB color model.)

Also like the Vector3 format, the Color type has some preset values, like Color.red or Color.black. You can read more about these and the methods of the Color type in the Unity script API.

In this example script, I only have one function that will actually change the main color. Here use SetColor( ) to alter the “_Color” channel. This is the only place that this particular function happens. All other functions are simply calling this script.

    private void SetColor(Color colorVal)
    {
        // overrides the material color value with the new "colorVal"
        renderer.material.SetColor("_Color", colorVal);
    }

NOTE: It is a little sloppy that I created a function called SetColor( ) and used it to call a function called SetColor( ). At first glance this may appear to be a recursive function and thus cause an error. But these are two different methods. One would live inside of our ColorDemo script, while the other that we call is specifically for the Material class. Context matters!

If this method were attached to every cube, this we could simply call SetColor( ) on each of them. To change every cube in something like our Enemy object, we can perform an operation similar to the one we did with our Rigidbody[ ] array when we applied the explosion to all child objects of a referenced object. We could even use this individuality to randomize the timing and coloration of this, say if we wanted to vary the debris chunks.

    private void SetAllChildColors(Color colorValue)
    {
        Renderer[] cubelist = GetComponentsInChildren<Renderer>();

        foreach (Renderer cube in cubelist)
        {
            cube.material.SetColor("_Color", colorValue);
        }

    }

There is also a shortcut that could be used for this process. If you have a series of objects you know will repeatedly change, you can pre-populate a Renderer[ ] array, and then pass the entire array to run a SetColor( ) function, rather than having to perform the GetComponentsInChildren( ) function every time.

Instead of calling SetColor over and over for each object, we call it once and pass the array to it, like so:

private void SetColor(Renderer[] renderlist, Color colorValue)
{
    foreach(Renderer renderer in renderlist)
    {
        renderer.material.SetColor("_Color", colorValue);
    }
}

Transitioning to Color

Another process that you may want to do is to alter the color gradually, rather than using an all-at-once approach. This will require us to change the color value of the object over time, so we will use a coroutine to manage this.

public IEnumerator FadeAllChildren(Color colorValue, Color startValue)
{
    // Color oldColor = renderer.material.GetColor("_Color");
    Color oldColor = startValue;
    Color lerpedColor;
    float timer = 0.0f;

    while (timer < fadeTime)
    {
        // how far along are we?
        float timeElapsed = timer / fadeTime;

        // get the linear interpolation color
        lerpedColor = Color.Lerp(oldColor, colorValue, timeElapsed);

        // set the color
        // renderer.material.SetColor("_Color", lerpedColor);
        SetAllChildColors(lerpedColor);

        yield return new WaitForEndOfFrame();

        // increment the timer
        timer += Time.deltaTime;

    }

    // make sure we set the final color
    SetAllChildColors(colorValue);



}

We set our coroutine parameters to include the color we want to get to, and how long it should take us to get there. We then define the old color (what color is it at the beginning) so that we have a value to interpolate from. Finally, we set our own timer object at 0.0.

We start a “while” loop set with a yield that will return a wait until the next frame, at which point it will run again. This will ensure a smooth transition. Within the loop we calculate how far along we are (by taking the time elapsed so far, then dividing it by the overall time, for a normalized value of completion. We use this timeElapsed for the Color.Lerp( ) command, a linear interpolation between these two values of the Color structure.

We run our SetColor( ) command to convert all of the cubes in our array to this interpolated color value, then finally we increment the time using Time.deltaTime.

After the timer has elapsed, we exit the while loop. There is a chance that we did not get all the way to the final color, depending on the framerate, so we run SetColor( ) one more time, this time with the final value, just to be sure.

Now we have a nice color transition, but the debris is still lying around (at least until it disappears at the end of the 10 second timer). Let’s clean this up by adding a fade-out so that it gently dissolves away.

It turns out that we have already written the code that we will need for this. Alpha is one of the channels in our Color value, and so we can transition to a color with an alpha of 0 to make it disappear.

BUT…

Our material can ONLY use an alpha value if the Rendering Mode is set to Fade or Transparent. Transparent sounds better, but even a fully transparent object is still partly visible. This is a shader meant for materials like glass, that still retain some visibility. “Fade” is the setting we will want to use, as it renders the alpha evenly across the object until it disappears.

In order to time this transition, we set up another coroutine to space our transition calls out. We call the transition to black, wait 2 seconds, then call the transition to clear.

And that’s it!


ColorDemo.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ColorDemo : MonoBehaviour
{
    public float fadeTime = 0.0f;

    private Renderer renderer;

    // Start is called before the first frame update
    void Start()
    {
        renderer = GetComponent<Renderer>();

        
    }

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(Vector3.up, (15.0f * Time.deltaTime));
    }

    public void SetEnemyToRed()
    {
        SetAllChildColors(Color.red);
    }

    public void FadeOutEnemy()
    {
        // get the first cube with a renderer and find the color
        Renderer firstCube = GetComponentInChildren<Renderer>();
        Color startValue = firstCube.material.GetColor("_Color");

        // copy the start value, but turn off the alpha channel
        Color endValue = startValue;
        endValue.a = 0;

        StartCoroutine(FadeAllChildren(endValue, startValue));

    }

    private void SetAllChildColors(Color colorValue)
    {
        Renderer[] cubelist = GetComponentsInChildren<Renderer>();

        foreach (Renderer cube in cubelist)
        {
            cube.material.SetColor("_Color", colorValue);
        }

    }

    public IEnumerator FadeAllChildren(Color colorValue, Color startValue)
    {
        // Color oldColor = renderer.material.GetColor("_Color");
        Color oldColor = startValue;
        Color lerpedColor;
        float timer = 0.0f;

        while (timer < fadeTime)
        {
            // how far along are we?
            float timeElapsed = timer / fadeTime;

            // get the linear interpolation color
            lerpedColor = Color.Lerp(oldColor, colorValue, timeElapsed);

            // set the color
            // renderer.material.SetColor("_Color", lerpedColor);
            SetAllChildColors(lerpedColor);

            yield return new WaitForEndOfFrame();

            // increment the timer
            timer += Time.deltaTime;

        }

        // make sure we set the final color
        SetAllChildColors(colorValue);



    }



    public void SetColor(string colorName)
    {
        Color colorVal = new Color();

        switch (colorName)
        {
            case "red":
                // set the color to red
                colorVal = new Color(1, 0, 0, 1);
                break;
            case "white":
                colorVal = new Color(1, 1, 1, 1);
                break;
            case "blue":
                colorVal.r = 0;
                colorVal.b = 1;
                colorVal.g = 0;
                colorVal.a = 1;
                break;
            case "green":
                colorVal = Color.green;
                break;
            case "yellow":
                colorVal = Color.yellow;
                break;
            case "transparent":
                colorVal.a = 0.5f;
                // colorVal = Color.clear;
                break;
            default:
                colorVal = Color.cyan;
                break;

        }

        renderer.material.SetColor("_Color", colorVal);



    }

    public void FadeTo(string colorName)
    {
        Color colorVal = new Color();

        switch (colorName)
        {
            case "red":
                // set the color to red
                colorVal = new Color(1, 0, 0, 1);
                break;
            case "white":
                colorVal = new Color(1, 1, 1, 1);
                break;
            case "blue":
                colorVal.r = 0;
                colorVal.b = 1;
                colorVal.g = 0;
                colorVal.a = 1;
                break;
            case "green":
                colorVal = Color.green;
                break;
            case "yellow":
                colorVal = Color.yellow;
                break;
            case "transparent":
                colorVal.a = 0.5f;
                // colorVal = Color.clear;
                break;
            default:
                colorVal = Color.cyan;
                break;

        }

        StartCoroutine(TransitionColor(colorVal));

    }


    public IEnumerator TransitionColor(Color newColor)
    {
        Color oldColor = renderer.material.GetColor("_Color");
        Color lerpedColor;
        float timer = 0.0f;

        while (timer < fadeTime)
        {
            // how far along are we?
            float timeElapsed = timer / fadeTime;

            // get the linear interpolation color
            lerpedColor = Color.Lerp(oldColor, newColor, timeElapsed);

            // set the color
            renderer.material.SetColor("_Color", lerpedColor);

            yield return new WaitForEndOfFrame();

            // increment the timer
            timer += Time.deltaTime;

        }

        // make sure we set the final color
        renderer.material.SetColor("_Color", newColor);


    }

}