🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

OpenGL 4 bloom / glow map

Started by
13 comments, last by taby 1 year, 7 months ago

At the request of warp9, I added in a glow map / bloom filter. Here's the relevant code from ortho_reflectance.fs.glsl:

   int count = 0;

   vec4 glowmap_blurred_colour = texture(glowmap_tex, ftexcoord);
   count++;
   
    for( float d=0.0; d<pi_times_2; d+= pi_times_2/directions)
    {
		for(float i=1.0/quality; i<=1.0; i+=1.0/quality)
        {
            vec4 temp_colour = texture( glowmap_tex, ftexcoord + vec2(cos(d),sin(d))*radius*i);

            if(true)//temp_colour.xyz != vec3(0, 0, 0))
            {
        	    glowmap_blurred_colour += temp_colour;
                count++;
            }
        }
    }
    
    // Output to screen
   glowmap_blurred_colour /= count;

   vec4 final_colour = mix(texture(regular_tex, ftexcoord), upside_down_colour, texture(reflectance_tex, ftexcoord)*upside_down_white_mask);

    // Bloom mixer
    final_colour += glowmap_blurred_colour*2; // mul by 2 to increase the glow

    frag_colour = final_colour;

Here is a screenshot, where only the eyes are glowing:

Advertisement

You should look into using a separable blur filter. The way you are doing the blur is very expensive: O(N^2) per pixel instead of O(N), and it looks a simple disc filter (bad quality). This presentation has some slides at the end describing how to do bloom better, though I was not that happy with the square-ish results of using their blur kernels. A 2-pass Gaussian blur looks a lot better with bright objects (e.g. sun). Example, with 100px kernel:

The sun is a very bright emissive sphere (10^8), and planet has grazing reflection. Both show bloom of different size.

To do bloom right you also need a multi-scale blur, i.e. you need to blur with kernels of multiple sizes, then add them together, then mix this result with the input image. This way, bright objects of any size will “excite” the blur, not just those that are bigger than your blur kernel. The most efficient way to do that is to downsample the image multiple times, then upsample back to full resolution (blurring along the way), adding together lower frequency blurs with the higher frequencies. The weights for higher/lower frequency blurs can be adjusted to achieve a desired look.

For instance, you might have intermediate images of 1/2, 1/4, 1/8, 1/16, 1/32 in size. Blur the 1/32 with 2 passes, then add the result to the 1/16 image. Then blur the 1/16 result with 2 passes, then add to the 1/8 image. Do this until you get to 1/1 size. You also want to pre-normalize the input image so that the sum of all blurs conserves energy, and doesn't overflow. Most of this is described in the presentation I linked above.

You also need to do bloom on an HDR image (e.g. float16 or float32) for it to work correctly. Many tutorials say to threshold the image before blurring, but that is obsolete information. Just do the multi-scale blur, then mix() or add to the input image.

Thank you for the details. Yes it’s quick and dirty, but it does the job for now, sort of. Let’s see what the artist does with it. Yes it does look bad when renderer in 8K.

Now that we have glowing eyes and stuff, it begs the question: do we want to do tracers?

I looked into the accumulation buffer, but it seems to be deprecated? Is this true?

If not, any suggestions on how to implement tracers? I am thinking of keeping a copy of the glow map between render passes, then blend that in.

taby said:
implement tracers

I guess you mean to do a temporal blur effect that smooths changes over time, so that lights leave trails in the image? Like this:

This can be implemented by having 2 “history” textures that are persistent across frames. You read from one, and write to the other, then switch on the next frame. On each frame, you want to do a linear combination mix() of the current history texture with the input image. The interpolation factor controls how fast or slow the response time is. This is also known as Exponential Smoothing.

That looks purrfect!

Oh yeah… it’s important that the travers fade over time.

So I've got it working using a single remembered texture. The big test will be to see what it looks like for fluid motion.

taby said:
using a single remembered texture

You may be relying on undefined behavior here (i.e. it may not work on another GPU). It's not legal to read from a texture while it is bound to a framebuffer object that is being rendered into, hence why I use 2 textures. See: OpenGL Framebuffer Object. The only way this could work reliably is if you use alpha blending to do the image combination, but that doesn't allow you to have asymmetric attack/release times for the smoothing filter, like you would need with an auto-exposure algorithm.

Could you please guide me as to how to use the two remembered textures?

Here is my code so far. It is nothing too fancy.

void draw_scene(GLuint fbo_handle)
{
	glUseProgram(point_shader.get_program());

	GLuint upside_down_white_mask_tex = 0;
	GLuint upside_down_tex = 0;
	GLuint reflectance_tex = 0;
	GLuint regular_tex = 0;
	GLuint glowmap_tex = 0;
	GLuint d_tex = 0;

	glGenTextures(1, &upside_down_white_mask_tex);
	glBindTexture(GL_TEXTURE_2D, upside_down_white_mask_tex);
	glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, win_x, win_y);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glGenTextures(1, &upside_down_tex);
	glBindTexture(GL_TEXTURE_2D, upside_down_tex);
	glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, win_x, win_y);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glGenTextures(1, &reflectance_tex);
	glBindTexture(GL_TEXTURE_2D, reflectance_tex);
	glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, win_x, win_y);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glGenTextures(1, &regular_tex);
	glBindTexture(GL_TEXTURE_2D, regular_tex);
	glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, win_x, win_y);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glGenTextures(1, &d_tex);
	glBindTexture(GL_TEXTURE_2D, d_tex);
	glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT32F, win_x, win_y);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	glGenTextures(1, &glowmap_tex);
	glBindTexture(GL_TEXTURE_2D, glowmap_tex);
	glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, win_x, win_y);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	if (last_frame_glowmap_tex == 0)
	{
		glGenTextures(1, &last_frame_glowmap_tex);
		glBindTexture(GL_TEXTURE_2D, last_frame_glowmap_tex);
		glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, win_x, win_y);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	}

	if (last_frame_glowmap_tex2 == 0)
	{
		glGenTextures(1, &last_frame_glowmap_tex2);
		glBindTexture(GL_TEXTURE_2D, last_frame_glowmap_tex2);
		glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, win_x, win_y);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	}


	glReadBuffer(GL_COLOR_ATTACHMENT0);


	// Upside down white mask
	draw_stuff(offscreen_fbo, true, false, true, false);
	glBindTexture(GL_TEXTURE_2D, upside_down_white_mask_tex);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, win_x, win_y);

	//// Upside down
	draw_stuff(offscreen_fbo, true, false, false, false);
	glBindTexture(GL_TEXTURE_2D, upside_down_tex);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, win_x, win_y);

	// Reflectance map
	draw_stuff(offscreen_fbo, false, true, false, false);
	glBindTexture(GL_TEXTURE_2D, reflectance_tex);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, win_x, win_y);

	// Regular map
	draw_stuff(offscreen_fbo, false, false, false, false);
	glBindTexture(GL_TEXTURE_2D, regular_tex);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, win_x, win_y);


	// Glow map
	draw_stuff(offscreen_fbo, false, false, false, true);
	glBindTexture(GL_TEXTURE_2D, glowmap_tex);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, win_x, win_y);


	glReadBuffer(GL_DEPTH_ATTACHMENT);
	glBindTexture(GL_TEXTURE_2D, d_tex);
	glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, win_x, win_y);

	// use the shader program
	glUseProgram(tex_reflectance.get_program());

	glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "img_width"), win_x);
	glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "img_height"), win_y);

	if (screenshot_mode)
		glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "cam_factor"), cam_factor);
	else
		glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "cam_factor"), 1);




	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, upside_down_white_mask_tex);
	glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "upside_down_white_mask_tex"), 0);

	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, upside_down_tex);
	glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "upside_down_tex"), 1);

	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, reflectance_tex);
	glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "reflectance_tex"), 2);

	glActiveTexture(GL_TEXTURE3);
	glBindTexture(GL_TEXTURE_2D, regular_tex);
	glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "regular_tex"), 3);

	glActiveTexture(GL_TEXTURE4);
	glBindTexture(GL_TEXTURE_2D, glowmap_tex);
	glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "glowmap_tex"), 4);

	glActiveTexture(GL_TEXTURE5);
	glBindTexture(GL_TEXTURE_2D, last_frame_glowmap_tex);
	glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "last_frame_glowmap_tex"), 5);

	glActiveTexture(GL_TEXTURE6);
	glBindTexture(GL_TEXTURE_2D, last_frame_glowmap_tex2);
	glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "last_frame_glowmap_tex2"), 6);

	glActiveTexture(GL_TEXTURE7);
	glBindTexture(GL_TEXTURE_2D, d_tex);
	glUniform1i(glGetUniformLocation(tex_reflectance.get_program(), "depth_tex"), 7);





	// vao and vbo handle
	GLuint vao, vbo, ibo;

	// https://raw.githubusercontent.com/progschj/OpenGL-Examples/master/03texture.cpp

	// generate and bind the vao
	glGenVertexArrays(1, &vao);
	glBindVertexArray(vao);

	// generate and bind the vertex buffer object
	glGenBuffers(1, &vbo);
	glBindBuffer(GL_ARRAY_BUFFER, vbo);

	// http://www.songho.ca/opengl/gl_transform.html


	// data for a fullscreen quad (this time with texture coords)
	static const GLfloat vertexData[] = {
		//  X     Y     Z           U     V     
		   1.0f, 1.0f, 0.0f,       1.0f, 1.0f, // vertex 0
		  -1.0f, 1.0f, 0.0f,       0.0f, 1.0f, // vertex 1
		   1.0f,-1.0f, 0.0f,       1.0f, 0.0f, // vertex 2
		  -1.0f,-1.0f, 0.0f,       0.0f, 0.0f, // vertex 3
	}; // 4 vertices with 5 components (floats) each

	// fill with data
	glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 4 * 5, vertexData, GL_DYNAMIC_DRAW);


	// set up generic attrib pointers
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), NULL);

	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (char*)0 + 3 * sizeof(GLfloat));


	// generate and bind the index buffer object
	glGenBuffers(1, &ibo);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);

	static const GLuint indexData[] = {
		0,1,2, // first triangle
		2,1,3, // second triangle
	};

	// fill with data
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * 2 * 3, indexData, GL_DYNAMIC_DRAW);

	// "unbind" vao
	glBindVertexArray(0);


	// bind the vao
	glBindVertexArray(vao);

	// draw
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

	glDeleteVertexArrays(1, &vao);
	glDeleteBuffers(1, &vbo);
	glDeleteBuffers(1, &ibo);

	use_buffers(fbo_handle, d_tex, offscreen_colour_tex);

	static int x = 0;
	
	if (x % 2 == 0)
	{
		glCopyImageSubData(glowmap_tex, GL_TEXTURE_2D, 0, 0, 0, 0,
			last_frame_glowmap_tex, GL_TEXTURE_2D, 0, 0, 0, 0,
			win_x, win_y, 1);
	}
	else
	{
		glCopyImageSubData(glowmap_tex, GL_TEXTURE_2D, 0, 0, 0, 0,
			last_frame_glowmap_tex2, GL_TEXTURE_2D, 0, 0, 0, 0,
			win_x, win_y, 1);
	}

	x++;

	glDeleteTextures(1, &upside_down_white_mask_tex);
	glDeleteTextures(1, &upside_down_tex);
	glDeleteTextures(1, &reflectance_tex);
	glDeleteTextures(1, &regular_tex);
	glDeleteTextures(1, &glowmap_tex);
	glDeleteTextures(1, &d_tex);

	//const string s = "Paused. Press esc to continue!";
	//const float font_scale = win_x / 1024.0f;

	//size_t sentence_width = get_sentence_width(font_scale, mimgs, s);
	//size_t window_width = win_x;
	//size_t window_height = win_y;

	//ortho_text.use_program();
	//print_sentence(font_scale, mimgs, ortho_text.get_program(), win_x, win_y, window_width / 2 - sentence_width / 2, window_height / 3, s);

}

This topic is closed to new replies.

Advertisement