Tutorial: Recreating The Volumetric Light Effect
Published a year ago
Hello everyone!
After my last update, “Improving The Game’s Style,” I received a lot of interest about how I managed to create my Volumetric Light Effect. I really appreciated getting these messages, as the creation of the Volumetric Light Effect proved to be quite the undertaking. After getting such great feedback, I decided to share a short tutorial on how I created this unique effect. My hope is that giving other game creators who are interested in the Volumetric Light Effect a look into my development process will allow them to utilize the same sort of techniques throughout their own video game projects.
While a name like “Volumetric Light Effect” may seem to imply a high level of technical complexity, this visual effect doesn’t take any fancy software. Instead, I opted to utilize basic, off the shelf software packages that are readily available and inexpensive to use. To create a Volumetric effect, you only need Photoshop, Unity, and some simple C# skills. Thankfully, no shader coding is required! I prefer using basic software tools throughout my development process, as it not only saves my studio money, but helps to ensure that anyone who wants to utilized my processes in their own game development are able to do so.
Before we move forward, I want to note that while this tutorial is how to recreate the Volumetric effect, it’s not a real visual effect. This may sound confusion, so please let me explain. This sort of approach works especially well for games with Orthographic or Perspective cameras, where the distances may look flat. Additionally, my Volumetric Light Effect has proven to be less resource intensive that traditional visual effects. Since I’m developing Crumbling World as a mobile phone application, this is especially important for many reasons. Specifically, freeing up resources gives me the ability to add more exciting features and functionalities to my games, all while ensuring that they run as smoothly as possible on a user’s mobile device or smartphone.

Step 1: Creating The Sun Shafts

The first step to achieving my Volumetric Light Effect is creating different sun shafts. This part of my effect is especially important to get right, as it serves as the base layer for the rest of Volumetric Light Effect’s development moving forward. To recreate this effect, I created a simple gradient from white with transparency to total transparency. I decided to use the color white, but you can use any color you’d like, depending on the mood you want to create for your scene. For example, using a darker color can help to create a more negative or brooding mood, while a lighter color will help to create a lighter, more cheery mood surrounding your game’s levels. This choice is really up to each developer, as different games (or levels within each game) will want to accomplish the creation of a different mood for different points of the game.
If you have been following my updates, you’ll know that I’m working hard to ensure that Crumbling World leverages procedural generation as much as possible. I really like using procedural generation features in my games, as it helps to ensure that levels are random and continue to feel fresh to players, regardless of how many times one plays the game. To add another factor of randomness and diversity, I created five different gradients with different transparencies.
I plan to add different sizes and positions later on with a few lines of code (if you want to know how at this moment, feel free to jump down to my C# code sample seen below). After this, you can add more or less Gaussian Blur to your sun shafts, depending on the style of your game. Once again, this choice is largely up to each individual developer, as different moods can be accomplished easily with different levels of Gaussian Blur.
Most importantly, I want to make sure that you know that all of the images should be saved as PNG files in order to keep the transparency. This is really important, as the effect can be thrown off (or even don’t work at all) if the images created above are accidentally saved in a different image file format.

Step 2: Importing To Unity

Once your images are ready, it’s time to import them into Unity. Thankfully, this is a pretty straight forward process, as Unity makes it really easy to import files into the development environment. After importing your images, select them, and change the Texture Type to Sprite (2D and UI). We want to place these images in our scene, using 3D positioning while avoiding having to rely on UI and Canvas. This is because our images will be placed in front of the camera, and we want them to be affected by their relative distance from the camera. This helps to make our “fake effect” more realistic. In other words, this will allow the Volumetric Light Effect to look very close to, if not exactly like, how a traditional visual effect would look.

Next, I created an empty GameObject, called vlight, and added a Sprite Renderer Component while placing the gradient image in the sprite field. From there, I created an animation that only moved the image in the X-Axis.
To create a very subtle animation, move the image just a bit for 10 seconds, but in a way that will loop seamlessly. This may seem strange, but we will be using some C# code later to improve upon this effect. The C# code will combine frame by frame animations and animations created by the code seamlessly. In this situation, I opted to code the fade in and out animation because it allows for a modification to the alpha channel, with the code depending on the relative distance to the camera.
From here, create another GameObject to be used as a container. The gradient will be the children element, while vlight_01 is the parent element. This process is repeated for each image.
Once all these GameObjects are created, create one final GameObject to use as the container for each of the 5 prefabs. This GameObject will be called VolumetricLight (but don’t worry about the naming conventions, as they don’t affect the code).

Step 3: Coding The Volumetric Light Effect

Once all the prefabs are created, make a new C# file called VolumetricLightManager.
This script randomizes the lights, rotating them to face the camera. At the same time, it animates a fading effect that changes depending on the relative distance to the camera. There are five lights, and we will be parameterizing the lights displayed. This way, if different volumetric lights are utilized they will look more realistic.
public List<GameObject> vlights = new List<GameObject>(); List<GameObject> vlightsTemporary; GameObject vlight; int range; int rand; int i; int totalLights; float xscale; float xpos; float ypos = 2f; float solidDistance = 12f; float transparentDistance = 10f WaitForSeconds wfs_checkDistanceToCamera; float alpha; float dist;
First off, I declared all of my variables. For performance, I always try to chache as many variables as possible.
From here, vlights represents the list of all the gradient images. vlightsTemporary is a copy of my list of lights. I use this copy because I want to separate them from the lights I’m using in order to avoid using the same lights more than once.
solidDistance is the minimum separation between the lights and the camera. This ensures that any distance below the solid distance will start fading out.
This is just a quick overview of some of the variables seen above. Don’t worry though, as the rest of the variables will be explained later on as they are used.
private void Awake()
 { wfs_checkDistanceToCamera = new WaitForSeconds(0.1f); }
Here, I set the variable on Awake for my enumerator. This variable is used to check the distances between the lights and the cameras. Feel free to edit this piece to check the distance more or less frequently, depending on your preference.
void OnEnable () { totalLights = vlights.Count; vlightsTemporary = new List<GameObject>(vlights); Reset(); StartLights(); }
Now, I have all my light game objects set as inactive within the scene. They can be activated when needed, so instead of using the Start function we will utilize OnEnable. Additionally, I cached the sum total of the lights with the totallights variable, as I learned that this method achieves better performance than alternative approaches.
From here, I made a copy of my list of lights as explained above. The lights are coded to reset in order to ensure we start from scratch with no unintended rotations or scaling. A pooling system is utilized to increase performance, ensuring that the lights are not destroyed all the time via instantiation. I’m still able to activate them and deactivate them if needed, that’s why we are resetting them before use. Finally, I start on the lights.
void StartLights(){ range = Random.Range(3,4); for (i = 0; i &#60; range; i++){ rand = Random.Range(0, vlightsTemporary.Count); vlight = vlightsTemporary[rand]; LightOn(vlight); vlightsTemporary.Remove(vlight); } StartCoroutine(CheckDistanceToCamera()); }
The StartLights method randomly selects between 3 to 5 lights, with Random.Range excluding the last parameter when using integers. This is the first step toward ensuring randomization with the volumetric lights.
The list is used temporarily, then removes the last light choosen. This ensures that only lights that haven’t been already used are selected. From here, I turn on the light and start the coroutine that checks the distance between the light and the camera.
void LightOn(GameObject rayLight){ rayLight.transform.GetChild(0).GetComponent<SpriteRenderer>().enabled = true; // Position xpos = Random.Range(0, 5f); rayLight.transform.localPosition = new Vector3(xpos, ypos, rayLight.transform.localPosition.z); // Scale xscale = Random.Range(2f, 3f); rayLight.transform.GetChild(0).localScale = new Vector3(xscale, 2f, 2f); // Rotation rayLight.transform.GetChild(0).rotation = Quaternion.Euler(new Vector3(30f, 0, -30f)); // I rotate 30f on x to face the camera }
The LightOn method turns the lights on by enabling the component Sprite Renderer to work. To add to this effect, the lights are randomized by position on the x-axis. Since we are looking for lights to overlap each other, as when they move they cause the animation, the overlapping helps to make the effect appear more realistic to players.
From here, the scale of the x-axis is randomized, affecting the width of the underlying gradients. Again, this helps the effect appear to be more realistic.
This last edit made is the rotation. I rotate the sun shaft by 30 degrees on the x-axis to face the camera. (The camera is rotated by 30 degrees on the x-axis as well.) Here, you can use the Transform.LookRotation command, but as my camera is not going to chage (and I want some specific angles), I opted to hardcode the values. Finally, the sun shafts are rotated -30 degrees on the z-axis. This is the actual rotation a player would see in the scene, and you can play with this angle to rotate the lights as you would like.
private void Reset() { for (i = 0;i<totalLights;i++) { vlights[i].transform.GetChild(0).GetComponent<SpriteRenderer> ().enabled = false; vlights[i].transform.GetChild(0).localScale = new Vector3(2f, 2f, 2f); vlights[i].transform.localPosition = new Vector3(0, ypos, 0); vlights[i].transform.GetChild(0).rotation = Quaternion.Euler(new Vector3(0, 0, 0)); } }
The reset method resets all of the values to default values, which helps to avoid issues or weird calculations with the lights.
private IEnumerator CheckDistanceToCamera() { while (true) { yield return wfs_checkDistanceToCamera; dist = Vector3.Distance(transform.position, Camera.main.transform.position); for (i = 0; i &#60; totalLights; i++){ if (dist &#60; solidDistance){ alpha = (dist – transparentDistance) / (solidDistance – transparentDistance); } vlights[i].transform.GetChild(0).GetComponent<SpriteRenderer>().color = new Color(255f, 255f, 255f, alpha); } } }
Finally, the coroutine that checks the distance between the camera and the lights needs to be settled. Instead of using a coroutine you could use the Update method, but in an effort to optimize my code I opted not to take this approach. In fact, the code performs much better in this way than it would checking it 60 times per second when the frame rate is 60 fps.
The coroutine checks the distance. If the distance is larger than 9 and less than 12, then the lights start to fade out. 12 and 9 are values I think work the best with my camera, but these values can be changed depending on your personal preferences. The fading in and out helps in two ways:
It’s another detail that helps to make the effect more realistic, as the intensity of the light changes it becomes more believable.
The lights never get clipped out by the camera, avoiding weird rendering issues. When the camera is too close to the light and the clipping out starts, our light is already faded out.
I attach this code to my prefab, and then we are ready to go with placing our lights in the scene. Additionally, this is created as a prefab due to the fact that we can instantiate it anywhere within our scene if needed.

Step 4: Adding More Detail and Post Processing Effects

We can add more detail and realistic complexity through the utilization of a Spot Light that moves with the player. This will emphasize the effect when the player is under the volumetric lights, making it “pop” more to the player.
In addition to the Spot Light, I added some floating dust particles to make the effect appear more realistic. This also adds volume to the effect, as it was originally flat.
Finally, I used the Post Processing Stack to enhance and emphasize the Volumetric Light Effect. If you’d like to clear more, you can check out the documentation here, but as a quick overview I’m using the definitions from the Unity Manual that I think are very accurate and instructive.

From the Unity Manual: “Bloom is an effect used to reproduce an imaging artifact of real-world cameras. The effect produces fringes of light extending from the borders of bright areas in an image, contributing to the illusion of an extremely bright light overwhelming the camera.”
This makes our lights more realistic and bring, introducing a number of interesting, subtle effects.
Depth of Field:
From the Unity Manual: “Depth of Field is a common post-processing effect that simulates the focus properties of a camera. In real life, a camera can only focus sharply on an object at a specific distance; objects nearer or farther from the camera will be somewhat out of focus.”
This effect is very useful, as it adds depth to the scene, blurs our lights, and affects the particles system all at the same time.
From the Unity Manual: “In Photography, vignetting is the term used for the darkening and/or desaturating towards the edges of an image compared to the center. It is also often used for artistic effect, such as to draw focus to the center of an image.”
As mentioned above, this helps to add another level of artistic flair to the effect.

Download the Volumetric Light Package here.
That’s all for this update of Crumbling World and my ongoing game development journey. If you want to learn more about how I’m developing this exciting new game, I invite you to check back often for more updates. While I’m working extremely hard on developing a great and enjoyable game, I am also making sure to document the project’s progress along the way. New blog posts are always under development! In the meantime, I highly suggest you check out some of my previous posts to learn more about myself, my game development studio, and the upcoming release of Crumbling World.
Dani Marti
Indie Game Developer - Artist