Camera Shake

>> using Unity engine 2018.3

35 minutes to complete

You will learn to write a script to simulate camera shake. You will use Perlin noise to control the frequency and magnitude of the shake effect.

Camera shake can be a powerful tool to communicate impacts or shockwaves to players. This article will describe step-by-step techniques to implement a dynamic shake effect controlled by Perlin noise, based on this GDC talk by Squirrel Eiserloh. While the goal of this article will be to implement camera shake, the same techniques can be used to apply shake motions to any kind of object or UI element.

The completed project is provided at the end of the article. Note that it also contains a large amount of comments in the C# files to aid understanding.

Prerequisites

To complete this tutorial, you will need a working knowledge of Unity engine and C#.

Download starter project .zip

These tutorials are made possible, and kept free and open source, by your support. If you enjoy them, please consider becoming my patron through Patreon.

Become a Patron!

Getting started

Download the starter project provided above, open it in the Unity editor and open the Main scene. If you expand the CameraContainer object and select the Camera, you will see the ShakeableTransform script attached to it. This script will control the camera shake effect; open it in your preferred code editor.

1. Random motion

As our camera exists in 3D space, we have available six axes we can shake it on: three translational (movement) and three angular (rotation). We will begin with some simple random movement on the X axis. Add the following to the Update method of ShakeableTransform.

transform.localPosition = new Vector3(Random.Range(-1, 1), 0, 0) * 0.3f;

We assign a random value between -0.2 and 0.2 to the X position of the camera, leaving the other two dimensions at 0.

This has actually produced a fairly effective result. We will add the Y and Z axes to see the full output. Replace the previous line of code with the following.

transform.localPosition = new Vector3(Random.Range(-1, 1), Random.Range(-1, 1), Random.Range(-1, 1)) * 0.3f;

All three axes together create a convincing camera shake. For some applications, this might be a satisfactory enough solution (angular shake can be added in the same manner). However, this method has some shortcomings: a completely new random position is generated and immediately jumped to each frame; there is no interpolation between the new position and the old.

Increasing the scaling factor from 0.2f to a larger number, like 1 or 2, makes this more apparent. This can create overly disjointed motion, and makes it impossible to control the speed of the camera shake.

Setting the scaling factor to 2 allows creates jumps between the camera's position each frame, making for a very disorienting effect.

To resolve this, we will need to calculate random values that maintain continuity—in that the previous values influence the next—to prevent large jumps. We will achieve this using Perlin noise.

2. Perlin noise motion

Our goal is to generate a series of values that appear random but maintain continuity, instead of a completely random value each frame. Perlin noise is ideal for this. Unity implements a function to generate Perlin noise from a two-dimensional input in Mathf.PerlinNoise.

Given a Vector2 as input, Mathf.PerlinNoise will return a pseudo-random value in the 0...1 range. The key here is that similar input values will result in a similar output; this is the main advantage over Random.Range, where returned values cannot be controlled by an input.

Comparsion of Random.Range (left) and Mathf.PerlinNoise (right). Note that although the Perlin noise is much smoother, it still generates a chaotic set of a values.

We will once again add shake to the X axis, but this time using Perlin noise. Replace the existing line of code with the following.

transform.localPosition = new Vector3(Mathf.PerlinNoise(0, Time.time) * 2 - 1, 0, 0) * 0.5f;

Although Mathf.PerlinNoise takes in a Vector2 as input, we keep the X value fixed at 0; scrolling through the noise on a single axis—using Time.time—is sufficient input. Note that Mathf.PerlinNoise returns a value in the 0...1 range; we transform it into the -1...1 range before using the value.

This has created smoother motion, but at a significantly slower speed. We will add a new field to control the speed of the shake.

// Add as a new field.
[SerializeField]
float frequency = 25;

…

// Modify the existing code in Update().
transform.localPosition = new Vector3(Mathf.PerlinNoise(0, Time.time * frequency) * 2 - 1, 0, 0) * 0.5f;

Larger values of frequency will result in faster shaking. Like before, we will add the other two axes in.

// Replace the previous line setting transform.localPosition.
transform.localPosition = new Vector3(Mathf.PerlinNoise(0, Time.time * frequency) * 2 - 1, 0, 0) * 0.5f;

transform.localPosition = new Vector3(
	Mathf.PerlinNoise(0, Time.time * frequency) * 2 - 1,
	Mathf.PerlinNoise(0, Time.time * frequency) * 2 - 1,
	Mathf.PerlinNoise(0, Time.time * frequency) * 2 - 1
) * 0.5f;

Since the values returned by Mathf.PerlinNoise are deterministic, running the function with the same inputs for each axis has resulted in all three axes having the same shake value. Setting the frequency to a value of 1 will make it apparent that the camera is travelling in a fixed diagonal path, rather than shaking around randomly. We will resolve this by incrementing the X value of our input.

transform.localPosition = new Vector3(
	Mathf.PerlinNoise(0, Time.time * frequency) * 2 - 1,
	Mathf.PerlinNoise(1, Time.time * frequency) * 2 - 1,
	Mathf.PerlinNoise(2, Time.time * frequency) * 2 - 1
) * 0.5f;

This has fixed the issue, but created a future potential problem: if there are multiple ShakeableTransform objects in the scene, they will all shake with the exact same motion. This can be averted by calculating a per-instance random seed to further offset the input.

// Add as a new field and method.			
private float seed;

private void Awake()
{
	seed = Random.value;
}

…

// Modify the existing code in Update().
transform.localPosition = new Vector3(
	Mathf.PerlinNoise(seed, Time.time * frequency) * 2 - 1,
	Mathf.PerlinNoise(seed + 1, Time.time * frequency) * 2 - 1,
	Mathf.PerlinNoise(seed + 2, Time.time * frequency) * 2 - 1
) * 0.5f;

Right now we are scaling the entire shake effect by a constant, 0.5. This should be stored in a field and exposed to the inspector. While we could use a float—as before—if we instead use a Vector3, we will be able to scale each axis individually.

// Add as a new field.
[SerializeField]
Vector3 maximumTranslationShake = Vector3.one * 0.5f;

…

transform.localPosition = new Vector3(
	maximumTranslationShake.x * (Mathf.PerlinNoise(seed, Time.time * frequency) * 2 - 1),
	maximumTranslationShake.y * (Mathf.PerlinNoise(seed + 1, Time.time * frequency) * 2 - 1),
	maximumTranslationShake.z * (Mathf.PerlinNoise(seed + 2, Time.time * frequency) * 2 - 1)
) * 0.5f;

Up until now, we have only implemented translational shake. Before going any further, we will add in angular shake using the same techniques.

// Add as a new field.
[SerializeField]
Vector3 maximumAngularShake = Vector3.one * 2;

…

// Add below the code assigning transform.position.
transform.localRotation = Quaternion.Euler(new Vector3(
	maximumAngularShake.x * (Mathf.PerlinNoise(seed + 3, Time.time * frequency) * 2 - 1),
	maximumAngularShake.y * (Mathf.PerlinNoise(seed + 4, Time.time * frequency) * 2 - 1),
	maximumAngularShake.z * (Mathf.PerlinNoise(seed + 5, Time.time * frequency) * 2 - 1)
));

Shaking with both movement and rotation is not always necessary. For 3D, it is often undesirable to include translational shake, as the movement can cause the camera to clip through nearby objects.

3. Shaking from trauma

Right now, the camera shakes continuously without any outside input. While this might be appropriate for an earthquake or similar scene, it's often desirable to have the camera shake from nearby impacts or effects. To accomplish this, we will control the camera's shake magnitude with a value called trauma.

Trauma will be a float value in the 0...1 range that will be multiplied into our shake strength. As well, trauma will be constantly decreasing, allowing the camera to recover from impacts when they occur.

// Add as a new field.
[SerializeField]
float recoverySpeed = 1.5f;
	
…
	
// We set trauma to 1 to trigger an impact when the scene is run,
// for debug purposes. This will later be changed to initialize trauma at 0.
private float trauma = 1;

…

// Modify the existing code in Update().
transform.localPosition = new Vector3(
	maximumTranslationShake.x * (Mathf.PerlinNoise(seed, Time.time * frequency) * 2 - 1),
	maximumTranslationShake.y * (Mathf.PerlinNoise(seed + 1, Time.time * frequency) * 2 - 1),
	maximumTranslationShake.z * (Mathf.PerlinNoise(seed + 2, Time.time * frequency) * 2 - 1)
) * trauma;

transform.localRotation = Quaternion.Euler(new Vector3(
	maximumAngularShake.x * (Mathf.PerlinNoise(seed + 3, Time.time * frequency) * 2 - 1),
	maximumAngularShake.y * (Mathf.PerlinNoise(seed + 4, Time.time * frequency) * 2 - 1),
	maximumAngularShake.z * (Mathf.PerlinNoise(seed + 5, Time.time * frequency) * 2 - 1)
) * trauma);

trauma = Mathf.Clamp01(trauma - recoverySpeed * Time.deltaTime);

Running the scene now, you will see the shake intensity fade away over time. The larger a recoverySpeed, the shorter impacts will last.

Unity will skip several frames when a scene is played in-editor due to loading. To avoid this, press Pause before pressing Play. This will allow the scene to fully load, at which time you can Unpause to run the scene.

The camera shake ends very abruptly right now; we can create a smoother falloff by raising trauma to a power.

// Add as a new field.
[SerializeField]
float traumaExponent = 2;
			
…
			
// Add to the top of Update().
float shake = Mathf.Pow(trauma, traumaExponent);

// Modify the existing code.
transform.localPosition = new Vector3(
	maximumTranslationShake.x * (Mathf.PerlinNoise(seed, Time.time * frequency) * 2 - 1),
	maximumTranslationShake.y * (Mathf.PerlinNoise(seed + 1, Time.time * frequency) * 2 - 1),
	maximumTranslationShake.z * (Mathf.PerlinNoise(seed + 2, Time.time * frequency) * 2 - 1)
) * shake;

transform.localRotation = Quaternion.Euler(new Vector3(
	maximumAngularShake.x * (Mathf.PerlinNoise(seed + 3, Time.time * frequency) * 2 - 1),
	maximumAngularShake.y * (Mathf.PerlinNoise(seed + 4, Time.time * frequency) * 2 - 1),
	maximumAngularShake.z * (Mathf.PerlinNoise(seed + 5, Time.time * frequency) * 2 - 1)
) * shake);

The impact itself is currently driven by manually initializing trauma to 1. To allow objects in the world to drive impacts, we will expose the ability to add trauma through a public method. Add the following code to the ShakeableTransform class...

// Modify trauma to initialize to 0.
private float trauma = 0;

…

// Add at the bottom of the class, just before the closing curly brace.
public void InduceStress(float stress)
{
	trauma = Mathf.Clamp01(trauma + stress);
}

...and add the code below to the Explosion script.

// Add at the end of Start().
target.InduceStress(1);

Finally, back in the scene, set the ExplosionNear game object to active. This is an explosion particle effect that triggers a short delay after the scene is run.

This is pretty effective; however, it does not take the distance between the explosion and the camera. Set both the ExplosionMiddle and ExplosionFar objects active and run the scene. All three generate the same amount of trauma—we will modify our code to instead factor distance in when applying the impact.

// Add as a new field.
[SerializeField]
float range = 45;
		
…
		
// Add just below the GetComponent line in Start().
float distance = Vector3.Distance(transform.position, target.transform.position);
float distance01 = Mathf.Clamp01(distance / range);

target.InduceStress(1 - distance01);

The distance is taken between the explosion's position and the camera's. It is then divided by the maximum radius of the impact to get a value in the 0...1 range. This value is inverted, making it so that distance01 is maximized when the explosion is directly near the camera, and minimized when it is further away.

The amount of stress applied is now modulated by the distance of the explosion from the camera. The amount of shake falls off a bit too much with the current settings; the final explosion is barely felt.

This has created a smooth falloff, where distant explosions generate less shaking, but has also resulted in significantly less trauma being applied to the camera. We will resolve this by updating some of the fields on the ShakeableTransform.

Maximum Translation Shake will be set to (1, 1, 1), Maximum Angular Shake to (15, 15, 10) and Trauma Exponent to 1—the exponential falloff isn't as needed right now. (These are the settings used in the animation at the top of this article.)

Before wrapping up, we'll add a field to control the maximum possible stress generated by the explosion, and plug distance01 into a simple equation to add some falloff, similar to what we did with trauma in ShakeableTransform.

// Add as a new field.
[SerializeField]
float maximumStress = 0.6f;

…

// Add immediately below the line declaring distance01.
float stress = (1 - Mathf.Pow(distance01, 2)) * maximumStress;

target.InduceStress(stress);

Once again, we use exponentiation to control our falloff, taking distance01 to the power of 2. Note that we perform this operation before inverting the value. This creates a convex falloff curve that favours nearby explosions causing more stress than they would with a linear curve; taking the exponentiation after inverting gives the opposite result. Alternatively, we could have used a different smoothing function entirely, like Mathf.SmoothStep, or manually create the falloff curve using the AnimationCurve class.

Comparison of trauma falloff between inverting after taking the power (1 - Mathf.Pow(distance, 2), green); inverting before taking the power (Mathf.Pow(1 - distance, 2), blue); and Mathf.Smoothstep (red).

Conclusion

Shaking effects—either generated randomly or through pseudo-random noise—can be used for more than just camera impacts. A lower frequency noise can simulate a handheld camera. UI elements can benefit from shake as well; added to dialogue text, the jittery motion can create a sense of fear or panic, while player health indicators can shake each time damage is taken.

View source GitHub repository

Leave me a message

Send me some feedback about the tutorial in the form below. I'll get back to you as soon as I can! You can alternatively message me through Twitter or Reddit.