This section describes bloom effects. Bloom is used to reproduce the effects that occur in real cameras when taking pictures in a bright environment. The bloom effect simulates fringes of light that extend from the borders of bright areas, which creates the illusion of a bright light overwhelming the camera.

The following screenshot shows bloom at the entrance of the cave in the Ice Cave demo:

The bloom effect occurs on real lenses because they cannot focus perfectly. When the light passes through the circular aperture of the camera, some light is diffracted, creating a bright disk around the image. This effect is not typically noticeable under most conditions, but it is visible under intense lighting.

Implementing bloom

Bloom effects are typically implemented as a post-processing effect. Generating effects this way can use a large amount of computing power, so if possible, avoid them in mobile games. This is especially true if your game, like the Ice Cave demo, uses many complex effects.

One alternative approach uses a simple plane. This is where you place the plane between the camera and the light source. The plane must be oriented with the normal pointing to the area where the camera will be situated.

To create a bloom effect this way, place a plane where you want the bloom effect to occur. Modulate the intensity of the effect using a factor that is based on the alignment of the view vector, with the plane normal, and the light source. This method is implemented in the Ice Cave demo, as shown in the following image:

Another way to avoid post-processing is to bake your bloom. If we have a map, for example a glossiness map, we can use that to determine where bloom must occur. A map can work for the static areas of your scene.

Note: If we do resort to post-processing, there are more efficient ways to do the bloom, so we prefer the Dual Filtering technique, for more information see Post-processed bloom. 

Planar bloom: creating a bloom modulation factor script

The alignment factor can be calculated using a script. This factor alters the bloom effect depending on the angle that the camera is viewing the effect from.

For example, the following script calculates the alignment factor for a plane that it is attached to:

// Light-plane normal-camera alignment
Vector3 planeToCamVec = Camera.main.transform.position - gameObject.transform.position;
Vector3 sunLightToPlainVec = origPlainPos - sunLight.transform.position;
float sunLightPlainCameraAlignment = Vector3.Dot(plainToCamVec, sunLightToPlainVec);
sunLightPlainCameraAlignment = Mathf.Clamp (sunLightPlainCameraAlignment, 0.0f, 1.0f);

The alignment factor sunLightPlaneCameraAlignment is passed to the shader to modulate the intensity of the rendered color.

Planar bloom; the vertex shader

The vertex shader receives a sunLightPlainCameraAlignment factor and uses it to modulate the intensity of the rendered bloom color.

The vertex shader applies the MVP matrix, which passes the texture coordinates to the fragment shader, and outputs the vertex coordinates. This is shown in the following code

vertexOutput vert(vertexInput input)
	vertexOutput output;
	output.tex = input.texcoord;
	output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
	return output;

Planar bloom: the fragment shader

The fragment shader fetches the texture color and increments it in using a CurrBloomColor tint color, which is passed as a uniform variable. The alignment factor modulates the color before it is used.

The following code shows how this process is performed:

half4 frag(vertexOutput input) : COLOR
	half4 textureColor = tex2D(_MainTex, input.tex.xy);
	textureColor += textureColor * _CurrBloomColor;
	return textureColor * _AligmentFactor;

The bloom plane is rendered in the transparent queue after all opaque geometry. In the shader, use the following queue tag to set the rendering order:

Tags { "Queue" = "Transparent" + 1}

The bloom plane is applied using additive one-to-one blending, to combine the fragment color with the corresponding pixel which is already stored in the frame buffer. The shader also disables writing to the depth buffer using the instruction, ZWrite Off, so that existing objects are not occluded.

The following image shows the texture that the Ice Cave demo uses for its bloom effect, and the result that it creates:

Planar bloom: correcting the bloom effect

Using a plane to produce a bloom effect is simple and suitable for mobile devices. However, it does not produce the expected results when an opaque object is placed between the light source producing the bloom and the camera. You can produce the expected results by using an occlusion map.

The incorrect appearance of the bloom effect behind an opaque object occurs because the effect is rendered in the transparent queue. Therefore, the blending occurs on top of any objects that are in front of the bloom plane.

The following image shows an example of the incorrect bloom effect that is produced without any fixes:

To prevent an incorrect bloom effect from occurring, you can alter the script which calculates the alignment factor so that it processes an extra occlusion map. This occlusion map describes the areas where the camera normally applies the bloom effect incorrectly. These conflicting areas are assigned an occlusion map value of zero. This factor combines with the alignment factor to set the intensity of factor in these places to zero. This change sets the bloom to zero so that it no longer renders over objects which must occlude it.

The following code implements this adjustment:

IntensityFactor = alignmentFactor * occlusionMapFactor

The Ice Cave demo camera can move freely in three dimensions. Therefore, three gray scale maps are used, each one covering a different height. Black means zero, so the occlusion map factor multiplied by the alignment factor makes the final intensity zero, occluding the bloom. White means one, so the intensity is equal to the alignment factor and the bloom is not occluded.

The following image shows the occlusionMapFactor at ground level:

Note: The black radiating areas in the middle of the map are zones behind the opaque plinths that occlude the bloom effect from the entrance of the cave.

There are no occluding objects at a height Hmax above ground level. Therefore, the bloom occlusion map above ground level is white. The following image shows the occlusionMapFactor above ground level:

Below the height Hmin, and below ground level the bloom effect is completely occluded. Therefore, the bloom occlusion map below ground level is completely black. The white contour is added so that the map is visible. The following image shows the occlusionMapFactor below ground level:

Planar bloom: interpolation between occlusion maps

In the Ice Cave demo, the script calculates the XZ projection of the camera position on the 2D cave map every frame. The script also normalizes the result between zero and one.

If the camera height is below Hmin or above Hmax, the normalized coordinates are used to fetch the color value from a single map. If the camera height is between Hmin and Hmax, the color is retrieved from both maps and interpolated.

This interpolation creates a smooth transition between the effects at different heights.

The Ice Cave demo uses the GetPixelBilinear() function to retrieve the colors from the map or maps.

This function returns a filter color value that uses the following code:

float groundOcclusionFactor = groundOcclusionMap.GetPixelBilinear(camPosXZNormalized.x, camPosXZNormalized.y)).r;

Using the bloom occlusion maps prevents the blending of the bloom on top of occluding opaque object. The following image shows a sequence of images with the resulting effect. When the camera is entering and leaving, the occluding black is behind the opaque plinth, preventing the incorrect bloom:

Baking bloom

For static terrain, it is possible to use a baked bloom map to efficiently produce the bloom effect.

In the game Spellsouls by Nordeus, a glossiness map from the terrain was a good starting point to produce a baked bloom map.

The following image shows the initial glossiness texture:

From the initial glossiness texture, we can generate the bloom map by creating an R8 scaled luminance map, and then blurring. This texture can then be stored in the alpha channel of the glossiness map itself. This means that you have one less texture access.

The following image shows a luminance map, stored in alpha channel:

In the vertex shader, we compute an alignment factor between the reflected light vector and the camera vector. This is shown in the following code:

// Vertex shader
float lightObjCameraAlignment = dot(objToCam, reftLightDir);
half alignmentFactor = clamp(lightObjCameraAlignment, 0.0, 1.0);

Then in the fragment shader we make a pixel brighter based on the following:

  • The data we sample from the bloom map
  • The alignment factor that we previously computed
  • A BloomStrength factor that we can tune

How to make a pixel brighter, based on the preceding list, is shown in the following code:

// Fragment shader
half bloom = rawGlossMap.a;
finalColor += finalColor * bloom * i.alignmentFactor * _BloomStrength;

The following image shows a Spellsouls terrain, with bloom applied:

This technique might produce a less compelling bloom. But the cost is massively reduced, so the trade-off is often worth it.

However, this technique does not work well for dynamic objects because the bloom cannot bleed off the object well. Therefore, game designer Nordeus, used the planar bloom, that is described earlier in this section, for the characters in Spellsouls.

Post-processed bloom

It is important to choose the most efficient bloom technique possible, even if you have enough rendering overheard to be able to handle a post-processing pass.

Before Nordeus in Spellsouls used the Baked and Planar bloom for their terrain and characters respectively, there were experiments on getting the most efficient post-processed bloom.

On mobile, the standard Unity post-processing bloom is too slow, but can also be too general. For example, you cannot choose to only have certain materials blooming. You must have either all materials blooming, or none.

For a more efficient post-process technique, you can use Multiple Render Target (MRT) to render only those objects that you want to bloom into a separate texture. For your blooming objects, render where there is specular greater than 1 into the separate texture. This texture will be the input to a custom bloom pipeline.

When developing for mobile, we must save the power and heat that is caused by excess memory bandwidth. Therefore, we use an R8 frame buffer, storing just the scaled luminance of the pixel.

With the values greater than one being captured into the texture, an HDR-like effect is achieved without an HDR frame buffer. In the MRT step, apply a scaling factor to represent values greater than 1 with fixed point.

The following image shows MRT R8 bloom texture:

The blur step of bloom is the most expensive part, so this is where it is most important to use the correct technique. Our recommendation is called dual filtering, which features optimized downscaling or upscaling filters. Dual Filtering achieves a stronger effect, like a larger Gaussian radius, at a much lower cost, to be specific it has 14 times performance improvement at 1080p. An example of dual filtering is shown in the following image:

Dual filtering uses bilinear filters that are in the GPU hardware to allow fewer samples to be done. By sampling between pixels, we get a weighted average of those pixel values in one sample.

On the down sample, four samples are required to smudge in some of all the neighboring pixels, along with one sample of the center pixel itself. On the up sample, we reconstruct information from the down sample pass. The up-sample pattern was chosen to get a smooth circular shape to the blur. Different shapes can be chosen for different artistic looks, and different distances to make the blur bigger or smaller.

Note: For further reading on the filter, see Marius Bjorges Dual Filtering presentation notes.

After the blur is calculated, that the bloom can be added to the rest of the scene, producing the final look. This is shown in the following image:

The following graph shows that efficient post-processing comes with significant cost however, adding in a texture to bake in the bloom costs virtually nothing:

Previous Next