OpenGL ES SDK for Android ARM Developer Center
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
Bloom

Bloom effect using OpenGL ES 3.0.

Introduction

Note
This sample uses OpenGL ES 3.0.

The source for this sample can be found in the folder of the SDK.

This tutorial assumes you know how to create a new project (if not, please start with First Android Native Application) and you have read and understood both Simple Cube and Lighting.

Overview

Bloom.png
Bloom effect: the intensity changes from very weak to very strong.

The application shows a bloom effect implementation. It draws cubes arranged in a two-dimensional 5x5 array, from which only the diagonal ones are bloomed. The intensity of the bloom effect changes from very weak, where each cube affected by the effect looks exactly as the normal ones, to very strong, when bloomed cubes make up an X shape.

The bloom effect is implemented as follows:

  1. A scene (5x5 array of cubes: cubes on diagonals are white, others are blue) is drawn to a render target.
  2. Elements that should be bloomed (the brighter ones, in this case cubes placed on diagonals) are drawn into downscaled texture object (where the rest of the scene is black).
  3. The result texture from step 2 is horizontally blurred - the outcome is stored in a texture which is then used for the vertical blur. This step can be repeated as described later (*).
  4. Both the texture in which the vertically & horizontally blurred image has been stored (result of step 3), and the texture from step 1 are blended (horizontally & vertically) and drawn into the back buffer.

(*) The blend effect is not constant during the rendering process: it changes from very weak to very strong. This is achieved by repeating step 3 a varying amount of times (depending on the required intensity of the effect) - the only difference is that for the n-th iteration the generated result of (n-th - 1) is taken as a source for the horizontal blur. To make the bloom effect more smooth, we also use continuous sampling of the textures. The results of the last two iterations from step 3 are used for the final combination pass. The colours of those two textures are mixed together with an appropriate factor value. (for more details please see the mix() function description in the OpenGL ES Shading Language documentation).

Besides the bloom effect, the application also shows:

  • matrix calculations (e.g. used for perspective view),
  • instanced drawing (each cube drawn on a screen is an instance of the same object),
  • lighting (the model is lit by a directional light),
  • rendering into a texture.

Instanced Drawing

Each cube drawn on the screen is an instance of the same object. Let's go through the whole mechanism step by step.

It is assumed that all of below functions are called for an active program object responsible for scene rendering.

We want to draw a 5x5 two-dimensional array of cubes. It is important to mention that white and blue cubes are still the instances of the same object (the difference in colours will be described later). In the code we are using define

#define NUMBER_OF_CUBES (25)

Generate a cubic shape. For more details how the cube vertices are created please refer to Simple Cube. We want our cubes to be a little bit smaller to fit in a screen. In this simple model, we could achieve that in many ways, but let's use the simplest one - scale the cube using multiplier. Thanks to that cube vertices will not be spread in a range from <-1, -1, -1> to <1, 1, 1>, but adjusted accordingly to used multiplier.

#define CUBE_SCALAR (0.8f)

Please look into tutorials/Bloom/jni/CubeModel.cpp to see how the cube triangle vertices are retrieved.

Now we need to transfer generated info into an array buffer object.

/* Generate buffer object and fill it with cube vertex coordinates data. */
GL_CHECK(glGenBuffers(1,
GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER,
GL_CHECK(glBufferData(GL_ARRAY_BUFFER,
GL_STATIC_DRAW) );

Define an array of generic vertex attribute data. Make sure that a proper buffer object (one which contains cube vertices data) is currently bound to GL_ARRAY_BUFFER. In our application we are re-binding it many times, so before calling glVertexAttribPointer() we bind it again. In one of the next steps we will describe what the sceneRenderingProgramLocationsPtr->attribCubeVertexCoordinates stands for, so don't worry about it now (it will be described in Shader Objects used for Instanced Drawing).

/* Cube coordinates are constant during rendering process. Set them now. */
GL_CHECK(glBindBuffer (GL_ARRAY_BUFFER,
GL_FLOAT,
GL_FALSE,
0,
NULL) );

When this is ready, we have to enable vertex attrib array.

We are now ready to draw multiple instanced of the same object. We use a special function for that, which is glDrawArraysInstanced()

/* Draw scene. */
GL_CHECK(glDrawArraysInstanced(GL_TRIANGLES,
0,

We wanted the cubes to be arranged in 5x5 two-dimensional array. We have to calculate location for each cube. This is done in getCubeLocations() function.

static GLfloat* getCubeLocations(GLint numberOfColumns,
GLint numberOfRows,
GLfloat cubeScalar,
GLfloat distanceBetweenCubes,
GLint* numberOfCubeLocationCoordinatesPtr)
{
ASSERT(numberOfCubeLocationCoordinatesPtr != NULL);
const float distance = distanceBetweenCubes + 2 * cubeScalar; /* A single cube spreads out from
* <-cubeScalar, -cubeScalar, -cubeScalar> to
* <cubeScalar, cubeScalar, cubeScalar>,
* with <0, 0, 0> representing the center of the cube.
* We have to enlarge the requested distance between cubes
* (2 * cubeScalar) times. */
int index = 0;
int numberOfCubeLocationCoordinates = 0;
const int numberOfPointCoordinates = 2;
GLfloat* result = NULL;
const float xStart = -( float(numberOfColumns - 1) / 2.0f * distance);
const float yStart = -( float(numberOfRows - 1) / 2.0f * distance);
numberOfCubeLocationCoordinates = numberOfPointCoordinates * numberOfColumns * numberOfRows;
result = (GLfloat*) malloc(numberOfCubeLocationCoordinates * sizeof(GLfloat) );
/* Make sure memory allocation succeeded. */
ASSERT(result != NULL);
for (int rowIndex = 0; rowIndex < numberOfRows; rowIndex++)
{
for (int columnIndex = 0; columnIndex < numberOfColumns; columnIndex++)
{
result[index++] = xStart + (rowIndex * distance);
result[index++] = yStart + (columnIndex * distance);
}
}
*numberOfCubeLocationCoordinatesPtr = numberOfCubeLocationCoordinates;
return result;
}

The result is then passed to uniform buffer object

/* Generate uniform buffer object and fill it with cube positions data. */
GL_CHECK(glGenBuffers(1,
GL_CHECK(glBindBuffer(GL_UNIFORM_BUFFER,
GL_CHECK(glBufferData(GL_UNIFORM_BUFFER,
GL_STATIC_DRAW) );

Which is then used as a source for program uniform block data. In one of the next steps we will describe what the sceneRenderingProgramLocationsPtr->uniformBlockCubeProperties stands for, so don't worry about it now (it will be described in Shader Objects used for Instanced Drawing).

/* Cube locations are constant during rendering process. Set them now. */
0 ) );
GL_CHECK(glBindBufferBase (GL_UNIFORM_BUFFER,
0,

Shader Objects used for Instanced Drawing

Let's focus on a program object now. It is assumed that you understand the mechanism of preparing and using program objects. If not, please refer to previous tutorials.

Vertex shader source:

#version 300 es
precision mediump float;
#define NUMBER_OF_CUBES (25)
const int is_diagonal_cube[NUMBER_OF_CUBES] = int[NUMBER_OF_CUBES](1, 0, 0, 0, 1,
0, 1, 0, 1, 0,
0, 0, 1, 0, 0,
0, 1, 0, 1, 0,
1, 0, 0, 0, 1);
/* UNIFORMS */
uniform mat4 mv_matrix;
uniform cube_properties
{
vec2 locations[NUMBER_OF_CUBES];
};
/* ATTRIBUTES */
in vec3 cube_vertex_coordinates;
in vec3 cube_vertex_normals;
/* OUTPUTS */
out vec3 normal;
out vec4 vertex;
flat out int is_cube_placed_on_diagonal;
void main()
{
/* Prepare translation matrix. */
mat4 cube_location_matrix = mat4(1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
locations[gl_InstanceID].x, locations[gl_InstanceID].y, 0.0, 1.0);
/* Calculate matrices. */
mat4 model_view_matrix = mv_matrix * cube_location_matrix;
mat4 model_view_projection_matrix = mvp_matrix * cube_location_matrix;
/* Set output values. */
is_cube_placed_on_diagonal = is_diagonal_cube[gl_InstanceID];
normal = vec3(model_view_matrix * vec4(cube_vertex_normals, 0.0)).xyz;
vertex = model_view_matrix * vec4(cube_vertex_coordinates, 1.0);
/* Set vertex position in NDC space. */
gl_Position = model_view_projection_matrix * vec4(cube_vertex_coordinates, 1.0);
}

As you can see, in the vertex shader we are using cube_vertex_coordinates attribute. And it's location should be used as the first argument in glVertexAttribPointer() described in one of the previous steps.

To get the attribute location it is enough to call:

locationsStoragePtr->attribCubeVertexCoordinates = GL_CHECK(glGetAttribLocation (programObjectId,
"cube_vertex_coordinates") );

There is also a uniform block used (cube_properties). It's location should be used as a second argument in glUniformBlockBinding() described in one of the previous steps.

To get the uniform block location, it is enough to call:

locationsStoragePtr->uniformBlockCubeProperties = GL_CHECK(glGetUniformBlockIndex(programObjectId,
"cube_properties") );

In the shader we have information about each cube location. According to the OpenGL ES Shading Language specification, the variable gl_InstanceID is a vertex shader input variable that holds the instance number of the current primitive in an instanced draw call. We are using this variable to refer to required element of our locations array stored in uniform block. This information is then used to prepare translation matrix which is then used for setting vertices in NDC space.

Fragment shader source:

#version 300 es
precision lowp float;
#define EPSILON (0.00001)
struct _light_properties
{
vec3 ambient;
vec3 color;
float constant_attenuation;
float linear_attenuation;
vec3 position;
float quadratic_attenauation;
float shininess;
float strength;
};
/* UNIFORMS */
uniform vec3 camera_position;
uniform _light_properties light_properties;
/* INPUTS */
in vec3 normal;
in vec4 vertex;
flat in int is_cube_placed_on_diagonal;
/* OUTPUTS */
/* Stores color data of cubes that should not be bloomed.*/
layout(location = 0) out vec4 normal_scene_color;
/* Stores color data of cubes we want bloomed. */
layout(location = 1) out vec4 bloom_element_color;
void main()
{
vec4 vertex_color = vec4(0.2, 0.4, 0.8, 1.0); /* Each cube will have the same colour. */
vec3 normalized_normals = normalize(normal);
vec3 light_direction = normalize(vec3(light_properties.position - vertex.xyz));
float attenuation = 1.0 / (light_properties.constant_attenuation + (light_properties.linear_attenuation + light_properties.quadratic_attenauation));
vec3 camera_direction = camera_position - vec3(vertex);
float diffuse = max(0.0, dot(normalized_normals, light_direction));
vec3 half_vector = normalize(light_direction + camera_direction);
float specular = 0.0;
if (abs(diffuse - 0.0) > EPSILON)
{
specular = max(0.0, dot(half_vector, normal));
specular = pow(specular, light_properties.shininess) * light_properties.strength;
}
vec3 scattered_light = light_properties.ambient * attenuation + diffuse * attenuation * light_properties.color;
vec3 reflected_light = light_properties.color * specular * attenuation;
vec3 calculated_color = min(vertex_color.xyz * scattered_light + reflected_light, vec3(1.0) );
/* Set output variables. */
vec4 color_to_be_returned = vec4(calculated_color, 1.0);
normal_scene_color = vec4(0.0);
bloom_element_color = vec4(0.0);
/* If we are dealing with a cube placed on a diagonal, use white colour.
* Otherwise, we want to output a regular cube (which means the previously
* calculated cube colour with lighting applied). */
if (is_cube_placed_on_diagonal == 1)
{
bloom_element_color = vec4(1.0, 1.0, 1.0, 1.0);
}
else
{
normal_scene_color = color_to_be_returned;
}
}

In the fragment shader there are some colours calculations implemented which stand for applying lighting effect. For more details, please read Lighting.

More details connected with the shader implementations will be described later (in Rendering Scene to Texture).

Rendering Scene to Texture

In the application we are using rendering into texture mechanism many times, but let's explain this using a single case only.

If we want our scene to be render into a texture, we should do as follows:

Generate a framebuffer and a texture objects. Do not forget about setting proper parameters for those objects. We want to have only one output from the shader object (a colour scene with blue and white cubes). We need a depth texture as well, because the scene is lit by a directional light and depth values will be used for light calculations.

/* Generate objects. */
GL_CHECK(glGenFramebuffers(1,
framebufferObjectIdPtr) );
GL_CHECK(glGenTextures (1,
originalTextureObjectIdPtr) );
GL_CHECK(glGenTextures (1,
depthToIdPtr) );
/* Bind generated framebuffer and texture objects to specific binding points. */
GL_CHECK(glBindFramebuffer(GL_FRAMEBUFFER,
*framebufferObjectIdPtr) );
GL_CHECK(glBindTexture (GL_TEXTURE_2D,
*originalTextureObjectIdPtr) );
GL_CHECK(glTexImage2D (GL_TEXTURE_2D,
0,
GL_RGBA8,
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
NULL) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_WRAP_S,
GL_CLAMP_TO_EDGE) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_WRAP_T,
GL_CLAMP_TO_EDGE) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MAG_FILTER,
GL_LINEAR) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MIN_FILTER,
GL_LINEAR) );
GL_CHECK(glBindTexture (GL_TEXTURE_2D,
*depthToIdPtr) );
GL_CHECK(glTexImage2D (GL_TEXTURE_2D,
0,
GL_DEPTH_COMPONENT32F,
0,
GL_DEPTH_COMPONENT,
GL_FLOAT,
NULL) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MIN_FILTER,
GL_NEAREST) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MAG_FILTER,
GL_NEAREST) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_WRAP_S,
GL_CLAMP_TO_EDGE) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_WRAP_T,
GL_CLAMP_TO_EDGE) );

The next step is to attach generated texture objects to the framebuffer object at specific binding points.

/* Bind colour and depth textures to framebuffer object. */
GL_CHECK(glFramebufferTexture2D(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
*originalTextureObjectIdPtr,
0) );
GL_CHECK(glFramebufferTexture2D(GL_FRAMEBUFFER,
GL_DEPTH_ATTACHMENT,
GL_TEXTURE_2D,
*depthToIdPtr,
0) );

There is only one thing remaining: the actual rendering.

Rendering into texture objects is achieved by binding the framebuffer object to to the GL_DRAW_FRAMEBUFFER framebuffer binding point (using glBindFramebuffer() function). Rendering into back buffer (the output will be visible on a screen) can be done only if default framebuffer object (ID = 0) is bound.

/* Render scene.
* The scene is rendered to two render targets:
* - 1. First texture will store color data;
* - 2. Second texture will store color data, but only for the cubes that should be
* affected by the bloom operation (remaining objects will not be rendered).
*/
{
/* Bind a framebuffer object to the GL_DRAW_FRAMEBUFFER framebuffer binding point,
* so that everything we render will end up in the FBO's attachments. */
GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER,
/* Set the viewport for the whole screen size. */
GL_CHECK(glViewport(0,
0,
/* Clear the framebuffer's content. */
GL_CHECK(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) );
/* [Instanced drawing] */
/* Draw scene. */
GL_CHECK(glDrawArraysInstanced(GL_TRIANGLES,
0,
/* [Instanced drawing] */
}

Rendering Texture on Screen

This section describes how to render a texture into the screen. In this sample we are using more advanced texture rendering, but let us describe the problem from the beginning.

The first thing you need to do is to create a program object with shaders attached, which is then linked and used (at this point we are sure you know how to do that, if not, please read previous tutorials).

What you will need as well is a texture object filled with data (you can get this one, for instance, by steps described in Rendering Scene to Texture). Let's assume that textureID refers to that texture object and textureUnit refers to the texture unit which you want to use as a binding point. This texture has to be bound to GL_TEXTURE_2D target at specific binding point.

glActiveTexture(GL_TEXTURE0 + textureUnit); /* textureUnit has to be a value from a range [0, GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1] */
glBindTexture(GL_TEXTURE_2D, textureID);

You can skip the selecting active texture unit part, if you want to use the default one.

Vertex shader source:

#version 300 es
precision mediump float;
/* GL_TRIANGLE_FAN-type quad vertex data. */
const vec4 vertex_positions[4] = vec4[4](vec4( 1.0, -1.0, 0.0, 1.0),
vec4(-1.0, -1.0, 0.0, 1.0),
vec4(-1.0, 1.0, 0.0, 1.0),
vec4( 1.0, 1.0, 0.0, 1.0) );
/* Texture UVs. */
const vec2 texture_uv[4] = vec2[4](vec2(1.0, 0.0),
vec2(0.0, 0.0),
vec2(0.0, 1.0),
vec2(1.0, 1.0) );
/* OUTPUTS */
out vec2 texture_coordinates;
void main()
{
/* Return vertex coordinates. */
gl_Position = vertex_positions[gl_VertexID];
/* Pass texture coordinated to fragment shader. */
texture_coordinates = texture_uv[gl_VertexID];
}

As you can see, we are using GL_TRIANGLE_FAN-type quad vertex data. This is important, as based on that we have to use corresponding mode for OpenGL ES draw function.

Fragment shader source:

#version 300 es
precision mediump float;
/* UNIFORMS */
uniform sampler2D sample_texture;
/* INPUTS */
in vec2 texture_coordinates;
/* OUTPUTS */
out vec4 color;
void main()
{
/* Return colour. */
color = texture(sample_texture, texture_coordinates);
}

Let's assume that programID is the program object ID you have created and linked (with shader objects attached which are described above).

/* Set active program. */
glUseProgram(programID);
/* Get texture sample uniform location. */
GLint sampleTextureUniformLocation = glGetUniformLocation(programID, "sample_texture");
/* Set uniform value. */
glUniform1i(sampleTextureUniformLocation, textureUnit);
/* Bind default framebuffer object. */
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
/* Draw texture on a screen. */
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

This is important that default framebuffer object is used (thanks to which the rendering result will be displayed on a screen). This is why we are calling glBindFramebuffer() function, but this is not obligatory as present by default. You can skip this line if you are not using any framebuffer objects.

You can skip the line with setting uniform value as well in case a GL_TEXTURE0 is used, as this is present by default. If you want to use any other binding point, then this line is essential, as it points which texture object should be rendered.

Prepare Scene That Will Be Bloomed

We have already described, how to render a scene into texture. Right now, we should have a texture containing colour scene data, which in our case is 5x5 array consist of blue and white cubes.

What we want to achieve at this point is to select all the fragments from this image which we want to be bloomed. We want to bloom only the brighter cubes. We are using program object to help us with selecting those elements.

If we use our colour scene texture as an input for this shader object, we can sample it and check the luminance of each fragment. If it is higher than a specific value, the original fragment colour will be stored, otherwise we will store the black colour value. The texture object where those results are stored can be then used as an input for blooming algorithms.

The fragment shader source

static const char getLuminanceImageFragmentShaderSource[] = "#version 300 es\n"
"precision highp float;\n"
"/* UNIFORMS */\n"
"uniform sampler2D texture_sampler;\n"
"/* INPUTS */\n"
"in vec2 texture_coordinates;\n"
"/* OUTPUTS */\n"
"out vec4 scene_color;\n"
"#define MIN_LUMINANCE (0.9)\n"
"void main()\n"
"{\n"
" vec4 sample_color = texture(texture_sampler, texture_coordinates);\n"
" float luminance = 0.2125 * sample_color.x +\n"
" 0.7154 * sample_color.y +\n"
" 0.0721 * sample_color.z;\n"
" if (luminance > MIN_LUMINANCE)\n"
" {\n"
" scene_color = sample_color;\n"
" }\n"
" else\n"
" {\n"
" scene_color = vec4(0.0);\n"
" }\n"
"}";

The vertex shader source is the same as already described in Rendering Texture on Screen

We would like the texture that will be then bloomed to be a little bit smaller (so that the bloom algorithm will be more efficient). And this is why we need to prepare the dowscaled texture storage and update viewport.

static void generateDownscaledObjects(GLuint* fboIdPtr,
GLuint* toIdPtr)
{
ASSERT(fboIdPtr != NULL);
ASSERT(toIdPtr != NULL);
/* Generate objects. */
GL_CHECK(glGenFramebuffers(1,
fboIdPtr) );
GL_CHECK(glGenTextures (1,
toIdPtr) );
/* Set texture parameters. */
GL_CHECK(glBindTexture (GL_TEXTURE_2D,
*toIdPtr) );
GL_CHECK(glTexStorage2D (GL_TEXTURE_2D,
1,
GL_RGBA8,
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_WRAP_S,
GL_CLAMP_TO_EDGE) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_WRAP_T,
GL_CLAMP_TO_EDGE) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MAG_FILTER,
GL_LINEAR) );
GL_CHECK(glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MIN_FILTER,
GL_LINEAR) );
/* Make framebuffer object active and bind texture object to it. */
GL_CHECK(glBindFramebuffer (GL_FRAMEBUFFER,
*fboIdPtr) );
GL_CHECK(glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
*toIdPtr,
0) );
/* Restore default bindings. */
GL_CHECK(glBindFramebuffer(GL_FRAMEBUFFER,
0) );
GL_CHECK(glBindTexture (GL_TEXTURE_2D,
0) );
}
/* Get the luminance image, store it in the downscaled texture. */
{
GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER,
/* Set the viewport for the whole screen size. */
GL_CHECK(glViewport(0,
0,
/* Clear the framebuffer's content. */
GL_CHECK(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) );
/* Draw texture. */
GL_CHECK(glDrawArrays(GL_TRIANGLE_FAN, 0, 4) );
}
Bloom_normal_scene.png
Colour scene shader output
Bloom_cubes_to_be_bloomed.png
Elements that should be bloomed

The downscaled texture with white cubes will be used as an input for the next operations.

Blurring

The blur effect is applied in two main steps:

  1. Horizontal Blur
  2. Vertical Blur

The fragment shader source for blur shader (horizontal blur):

static const char blurHorizontalFragmentShaderSource[] = "#version 300 es\n"
"precision mediump float;\n"
"/** Defines gaussian weights. */\n"
"const float gaussian_weights[] = float[] (0.2270270270,\n"
" 0.3162162162,\n"
" 0.0702702703);\n"
"/* UNIFORMS */\n"
"/** Radius of a blur effect to be applied. */\n"
"uniform float blur_radius;\n"
"/** Texture sampler on which the effect will be applied. */\n"
"uniform sampler2D texture_sampler;\n"
"/* INPUTS */\n"
"/** Texture coordinates. */\n"
"in vec2 texture_coordinates;\n"
"/* OUTPUTS */\n"
"/** Fragment colour that will be returned. */\n"
"out vec4 output_color;\n"
"void main()\n"
"{\n"
" vec4 total_color = vec4(0.0);\n"
" float image_resolution = float((textureSize(texture_sampler, 0)).x);\n"
" float blur_step = blur_radius / image_resolution;\n"
" /* Calculate blurred colour. */\n"
" /* Blur a texel on the right. */\n"
" total_color = texture(texture_sampler, vec2(texture_coordinates.x + 1.0 * blur_step, texture_coordinates.y)) * gaussian_weights[0] +\n"
" texture(texture_sampler, vec2(texture_coordinates.x + 2.0 * blur_step, texture_coordinates.y)) * gaussian_weights[1] +\n"
" texture(texture_sampler, vec2(texture_coordinates.x + 3.0 * blur_step, texture_coordinates.y)) * gaussian_weights[2];\n"
" /* Blur a texel on the left. */\n"
" total_color += texture(texture_sampler, vec2(texture_coordinates.x - 1.0 * blur_step, texture_coordinates.y)) * gaussian_weights[0] +\n"
" texture(texture_sampler, vec2(texture_coordinates.x - 2.0 * blur_step, texture_coordinates.y)) * gaussian_weights[1] +\n"
" texture(texture_sampler, vec2(texture_coordinates.x - 3.0 * blur_step, texture_coordinates.y)) * gaussian_weights[2];\n"
" /* Set the output colour. */\n"
" output_color = vec4(total_color.xyz, 1.0);\n"
"}";

The fragment shader source for blur shader (vertical blur):

static const char blurVerticalFragmentShaderSource[] = "#version 300 es\n"
"precision mediump float;\n"
"/** Defines gaussian weights. */\n"
"const float gaussian_weights[] = float[] (0.2270270270,\n"
" 0.3162162162,\n"
" 0.0702702703);\n"
"/* UNIFORMS */\n"
"/** Radius of a blur effect to be applied. */\n"
"uniform float blur_radius;\n"
"/** Texture sampler on which the effect will be applied. */\n"
"uniform sampler2D texture_sampler;\n"
"/* INPUTS */\n"
"/** Texture coordinates. */\n"
"in vec2 texture_coordinates;\n"
"/* OUTPUTS */\n"
"/** Fragment colour that will be returned. */\n"
"out vec4 output_color;\n"
"void main()\n"
"{\n"
" vec4 total_color = vec4(0.0);\n"
" float image_resolution = float((textureSize(texture_sampler, 0)).y);\n"
" float blur_step = blur_radius / image_resolution;\n"
" /* Calculate blurred colour. */\n"
" /* Blur a texel to the top. */\n"
" total_color = texture(texture_sampler, vec2(texture_coordinates.x, texture_coordinates.y + 1.0 * blur_step)) * gaussian_weights[0] +\n"
" texture(texture_sampler, vec2(texture_coordinates.x, texture_coordinates.y + 2.0 * blur_step)) * gaussian_weights[1] +\n"
" texture(texture_sampler, vec2(texture_coordinates.x, texture_coordinates.y + 3.0 * blur_step)) * gaussian_weights[2];\n"
" /* Blur a texel to the bottom. */\n"
" total_color += texture(texture_sampler, vec2(texture_coordinates.x, texture_coordinates.y - 1.0 * blur_step)) * gaussian_weights[0] +\n"
" texture(texture_sampler, vec2(texture_coordinates.x, texture_coordinates.y - 2.0 * blur_step)) * gaussian_weights[1] +\n"
" texture(texture_sampler, vec2(texture_coordinates.x, texture_coordinates.y - 3.0 * blur_step)) * gaussian_weights[2];\n"
" /* Set the output colour. */\n"
" output_color = vec4(total_color.xyz, 1.0);\n"
"}";

The main idea behind applying the blur effect is that for each texel its colour is spread on each side (left/bottom and right/up) with a specific weight (according to Gaussian weights stands for the normal distribution in statistics). This is done with a respect that the further from the kernel (the texel which is currently blurred) the lower impact on a fragment colour it has.

In the first pass, we are applying a horizontal blur. Once this is ready, we use the result to apply vertical blur as well. The process is shown on the schema.

Bloom_blur_schema.png
Blur schema

The blur effect is ready, however the result is rather weak, comparable to the first screen shown in Overview. To make it stronger we are applying the blur effect multiple times (the number of blurring operations is changing during rendering process). In the code presented below you can see, that the total blur effect is stored in two texture objects (the results of last and last but one iterations are stored in two different textures). The reason of this approach will be explained in Blending.

The important thing here is that the source texture of horizontal blur is changing. In the first operation we are using the downscaled texture which elements should be bloomed (the result described in Prepare Scene That Will Be Bloomed). The source texture for (n-th) operation is the blur result of (n-th - 1) operation. Thanks to that, the texture which has already been blurred, is blurred again, so the total effect is much stronger.

/* Apply the blur effect.
* The blur effect is applied in two basic steps (note that lower resolution textures are used).
* a. First, we blur the downscaled bloom texture horizontally.
* b. The result of horizontal blurring is then used for vertical blurring.
* The result texture contains image blrured in both directions.
* c. To amplify the blur effect, steps (a) and (b) are applied multiple times
* (with an exception that we now use the resulting blurred texture from the previous pass
* as an input to the horizontal blurring pass).
* d. The result of last iteration of applying the total blur effect (which is the result after the vertical blur is applied)
* is stored in a separate texture. Thanks to that, we have the last and previous blur result textures,
* both of which will be then used for continuous sampling (for the blending pass).
*
*/
/* Bind a framebuffer object to the GL_DRAW_FRAMEBUFFER framebuffer binding point,
* so that everything we render will end up in the FBO's attachments. */
GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER,
/* Set the lower viewport resolution. It corresponds to size of the texture we will be rendering to. */
GL_CHECK(glViewport(0,
0,
GL_CHECK(glEnable (GL_SCISSOR_TEST) );
/* Apply the blur effect multiple times. */
for (int blurIterationIndex = 0;
blurIterationIndex < currentNumberOfIterations;
blurIterationIndex++)
{
/* FIRST PASS - HORIZONTAL BLUR
* Take the texture showing cubes which should be bloomed and apply a horizontal blur operation.
*/
{
/* Attach the texture we want the color data to be rendered to the current draw framebuffer.*/
GL_CHECK(glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
0) );
/* In first iteration we have to take the texture which shows the cubes we want blurred.
* Later, we have to take the same texture that has already been blurred vertically. */
if (blurIterationIndex == 0)
{
}
else
{
}
/* Draw texture. */
GL_CHECK(glDrawArrays(GL_TRIANGLE_FAN, 0, 4) );
} /* FIRST PASS - HORIZONTAL BLUR */
/* SECOND PASS - VERTICAL BLUR
* Take the result of the previous pass (horizontal blur) and apply a vertical blur to this texture.
*/
{
if (blurIterationIndex == currentNumberOfIterations - 1)
{
/* In case of the last iteration, use a different framebuffer object.
* The rendering results will be written to the only color attachment of the fbo. */
GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER,
}
else
{
/* Bind a texture object we want the result data to be stored in.*/
GL_CHECK(glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
0) );
}
/* Set uniform values. */
TEXTURE_UNIT_HORIZONTAL_BLUR_TEXTURE) ); /* Indicates which texture object content should be blurred. */
/* Draw texture. */
GL_CHECK(glDrawArrays(GL_TRIANGLE_FAN, 0, 4) );
} /* SECOND PASS - VERTICAL BLUR */
} /* for (int blur_iteration_index = 0; i < numberOfIterations; blur_iteration_index++) */
GL_CHECK(glDisable(GL_SCISSOR_TEST));

Please look at the schema below to see the effect we get after this operation.

Bloom_blur_effect_schema.png
The effect of blur operation. The stronger blur effect corresponds to higher number of blur iterations.

Blending

As the result of previous steps we have:

  1. A texture object holding colour scene - the colour scene (Rendering Scene to Texture)
  2. A texture object holding bloom objects with weaker blur effect applied (the result of n-th - 1 iteration described in Blurring)
  3. A texture object holding bloom objects with stronger blur effect applied (the result of n-th iteration described in Blurring)

What we want to achieve at this step is to blend all those textures and display the result.

Our approach was the smooth changes of the blur effect strength. This is why we needed two results (n-th and n-th - 1) from blur loop. We will use the OpenGL ES Shading Language mix() function to get this effect. This approach is not obligatory. You can use the result of the last blur loop only (then the mix() operation in shader will not be needed), however it would lead to jagged result of the blur effect strength change, which does not look as nice as the smooth changes.

First of all, please look at the code presented below.

/* Apply blend effect.
* Take the original scene texture and blend it with texture that contains the total blurring effect.
*/
{
/* Bind the default framebuffer object. That indicates that the result is to be drawn to the back buffer. */
GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0) );
/* Set viewport values so that the rendering will take whole screen space. */
GL_CHECK(glViewport(0, 0, windowWidth, windowHeight) );
/* Set uniform value. */
GL_CHECK(glUniform1f(blendingProgramLocations.uniformMixFactor, mixFactor) ); /* Current mixFactor will be used for mixing two textures color values
* (texture with higher and lower blur effect value). */
/* Clear framebuffer content. */
GL_CHECK(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) );
/* Draw texture. */
GL_CHECK(glDrawArrays(GL_TRIANGLE_FAN, 0, 4) );
}

As you can see, we use default framebuffer object, thanks to which the effect will be displayed on screen rather than stored in rendering targets. The only thing which is changing is the mix factor. This will be used as a parameter for mix() function in a shader. Depends on the blur effect direction (if it increases or decreases) the mixFactor value is changing accordingly (form 0 to 1).

/* Mix factor value is calculated for a specific frame (for a specific time).
* - The number of blur passes varies from MIN_NUMBER_OF_BLUR_PASSES to MAX_NUMBER_OF_BLUR_PASSES
* and to MIN_NUMBER_OF_BLUR_PASSES again which indicates the constant animation of increasing
* and decreasing blur effect strength.
* - For each frame (time) there is a check done to verify the current number of blur passes.
* - Once we get the current number of blur passes, we have to calculate the mix factor:
* It is changing from 0 to 1 (if the blur effect is increasing) or from 1 to 0 (if the blur effect is decreasing).
* This value is set based on a time which passed from the beginning of current number of blur passes rendering in
* compare to the total time requested for changing this number.
*
* The 'rendering frame for a specific time' approach is used to avoid a situation of a different effect for slower and faster devices.
*/
/* Increase or decrease mixFactor value (depends on blurEffectDirection). */
timeIntervalIndex = (int)(time / TIME_INTERVAL);
nOfIterations = (int) timeIntervalIndex % numberOfBlurPasses;
if (nOfIterations >= (numberOfBlurPasses / 2))
{
nOfIterations = numberOfBlurPasses - (nOfIterations % numberOfBlurPasses) - 1;
blurEffectDirection = BLUR_EFFECT_DECREASE;
}
mixFactor = (time - ((int)(time / TIME_INTERVAL) * TIME_INTERVAL)) / TIME_INTERVAL;
currentNumberOfIterations = MIN_NUMBER_OF_BLUR_PASSES + nOfIterations;
if (blurEffectDirection == BLUR_EFFECT_DECREASE)
{
mixFactor = 1.0f - mixFactor;
}
if (currentNumberOfIterations != lastNumberOfIterations)
{
shouldSceneBeUpdated = true;
}
/* Store current number of iterations for future use. */
lastNumberOfIterations = currentNumberOfIterations;

In the fragment shader object (code presented below) there are three texture source inputs (accordingly to those listed above). The weaker and stronger blur textures are mixed (mix() function is applied which returns the linear blend). The total colour which will be returned is just a sum of the linear blending of blurred textures and the original scene texture.

#version 300 es
precision mediump float;
/* UNIFORMS */
uniform float mix_factor;
uniform sampler2D original_texture;
uniform sampler2D stronger_blur_texture;
uniform sampler2D weaker_blur_texture;
/* INPUTS */
in vec2 texture_coordinates;
/* OUTPUTS */
out vec4 color;
void main()
{
vec4 stronger_blur_texture_color = texture(stronger_blur_texture, texture_coordinates);
vec4 weaker_blur_texture_color = texture(weaker_blur_texture, texture_coordinates);
vec4 mixed_blur_color = mix(weaker_blur_texture_color, stronger_blur_texture_color, mix_factor);
vec4 original_color = texture(original_texture, texture_coordinates);
/* Return blended colour. */
color = original_color + mixed_blur_color;
}

Code Details

The bloom algorithm has already been described. Let's focus on the implementation details now.

  1. tutorials/Bloom/jni/Native.cpp In this file you can find the bloom algorithm implementation (you can find there most of things described above).
  2. tutorials/Bloom/jni/CubeModel.cpp In this file you can find all the functions connected with generating the cube model (generation of vertex coordinates or normals).
  3. tutorials/Bloom/jni/Matrix.cpp In this file you can find all functions connected with matrix calculations.
  4. tutorials/Bloom/jni/Shader.cpp In this file you can find implementation of functions responsible for shader operations, like shader object creation, compilation, reading and loading shader source, etc.