Unity3D Bloom Animation Script

bloomAnim.gif

Unity’s post-processing stack is very powerful, and very simple. But modifying it from code can be tricky. It took me a while to get it working initially, which is why I wanted to provide an example so others can get it working more quickly.

I’ve uploaded the code this tutorial is based on to pastebin, view it here!

Notes: The code I’m presenting is taken directly from our current game, Valley of Shadow (working title). As such, some brief modifications would need to be made to include it in your own game. Also, this example focuses just on Bloom, but the general idea applies to the rest of the post processing options as well.

This code was written for Unity 2018. Future versions of Unity may not work the same way.


Set Up Post Processing

Post-processing is not on by default, you’ll need to set it up first. To do so is outside the scope of this tutorial, mostly because Unity already provides a pretty easy explanation of doing it. Click here to see their documentation on the subject.

At any rate, once you have post processing set up, your Camera object should look something like this:

Camera.JPG

Duplicate Your Profile

The very first thing you want to do is duplicate your Post Processing Profile. Why? Well, the profile is just an asset. As such, if you make any changes to the profile during runtime, they will save directly to the asset itself. This will result in any changes carrying over to subsequent playthroughs, which you probably don’t want.

Here’s how I do that in my general player script:

public PostProcessingProfile PostProcessing { get; private set; }

private void DuplicatePostProcessing()
{
  var behaviour = Camera.main.gameObject.GetComponent<PostProcessingBehaviour>();
  var newProfile = Instantiate(behaviour.profile, transform);
  PostProcessing = newProfile;
  behaviour.profile = PostProcessing;
}

In Awake(), you can simply call DuplicatePostProcessing() and that will handle the duplication.

The reason PostProcessing is a public attribute is so other scripts can access it, which is the whole point of this tutorial!

Now that the profile is duplicated, we can start actually modifying it.


Set Up Public Variables

The first thing we want to do in our script is set up the variables that we want designers to be able to play with through the editor. For each post processing option, this will change. For Bloom, we are just going to modify the Intensity, Threshold, and Radius. We also want the designer to state how long this animation should occur over. Thus, we have:

public float Duration;
public Vector2 IntensityRamp;
public Vector2 ThresholdRamp;
public Vector2 RadiusRamp;

Why are the “ramp” values Vector2’s instead of floats? Because this script animates each variable from one value to another over time. The X value of the Vector2 represents the start value of each variable, and the Y value represents the end value. Thus, if we want Intensity to go from 0 to 15, IntensityRamp would be (0, 15).


Animate Via Coroutine

I’m a sucker for coroutines, so we are going to perform our animation through one. We will expose a non-coroutine method that will fire it off, though. This way, from anywhere else in code we can simply call “Activate” and the bloom script will handle the asynchronous functionality.

private bool _enabled;
private float _intensity, _threshold, _radius;
private bool _isRunning = false;

public void Activate()
{
  StartCoroutine(Activate_Async());
}

private IEnumerator Activate_Async()
{
  _isRunning = true;

  BloomModel bloom = GameState.Player.PostProcessing.bloom;
  BloomModel.Settings settings = bloom.settings;

  // Save these values before modifying them, so we can revert them when we are done.
  _intensity = settings.bloom.intensity;
  _threshold = settings.bloom.threshold;
  _radius = settings.bloom.radius;
  _enabled = bloom.enabled;

  bloom.enabled = true;
  float timer = 0;
  while(timer < Duration)
  {
    timer += Time.deltaTime;
    settings.bloom.intensity = Mathf.Lerp(IntensityRamp.x, IntensityRamp.y, timer / Duration);
    settings.bloom.threshold = Mathf.Lerp(ThresholdRamp.x, ThresholdRamp.y, timer / Duration);
    settings.bloom.radius = Mathf.Lerp(RadiusRamp.x, RadiusRamp.y, timer / Duration);
    bloom.settings = settings;
    yield return null;
  }

  ResetValues();

  _isRunning = false;
}

Pretty straightforward here. There are 2 areas where it might be easy to get tripped up.

  1. In our game, GameState is where we store all the player info. That’s why we can call GameState.Player.PostProcessing, which returns the public PostProcessing attribute that we defined earlier in this tutorial. This will have to change for your game.

  2. Make sure you set bloom.settings = settings at the end of the while loop. Otherwise you are just modifying values but not setting them. This has caused me issues in the past because I always seem to forget doing it.


Reset The Values

Depending on the type of effect you are trying to create, you might not want to keep the modified bloom values around after the script has finished executing. If you do want to keep them, ignore this section and remove “ResetValues();” from the previous tutorial section.

At any rate, this is how you reset the values:

private void ResetValues()
{
    BloomModel bloom = GameState.Player.PostProcessing.bloom;
    BloomModel.Settings settings = bloom.settings;

    bloom.enabled = _enabled;
    settings.bloom.intensity = _intensity;
    settings.bloom.threshold = _threshold;
    settings.bloom.radius = _radius;

    bloom.settings = settings;
}

Again, straightforward. Just don’t forget to set bloom.settings = settings.

Also, in the event that this script is destroyed before it finishes animating, you’ll want to reset the values then as well. This is the purpose to the “_isRunning” variable above.

private void OnDestroy()
{
  if(_isRunning)
    ResetValues();
}

Conclusion

So there you have it, if everything went properly you should have a script which, when activated, animates the bloom in real time. This script is fairly easy to translate to other effects as well, such as Vignette or Depth of Field.

I’m sure there are a million ways to improve this script, so if you have thoughts on doing so please let me know!