Skip to content

Custom ‐ Post Processing Shaders

Vlod edited this page Apr 10, 2024 · 3 revisions

Custom shaders or post-processing effects have the same API. You have to write a fragment shader and load it. I made them to be more or less compatible with each other. The only difference between a post-process shader and a normal shader is that the normal shader is also expected to take into account colors and texture coordinates. This is because when doing the post process, the entire screen and the entire texture will be used. So I made the redundant attributes for a post-process shader have default values, so you can use a normal shader for a post-processing effect.

This is a default post process shader, this means one that does nothing to the image:

#version 130
precision highp float;

out vec4 color;
in vec2 v_positions; //the positions are in NDC [-1, 1], for a post process shader they will just be the entire screen
in vec2 v_texture; //the positions are in texture space, for a post process shader they will just be the entire texture
in vec4 v_color; //for a post process shader the color is just 1,1,1,1


uniform sampler2D u_sampler;

void main()
{
	//both ways work the same for a post processing shader
	//color = texture2D(u_sampler, (v_positions + vec2(1.f))/2.f).rgba;
	color = texture2D(u_sampler, v_texture).rgba;
}

How does post-process work?

Right now, it is expected that the input is a texture the same size as the output frame buffer. So make sure that you set things correctly, if you, for example, use the flushPostProcess() method, make sure the renderer's dimensions match the output FBO by using flushPostProcess().

Ok so the idea is simple, once you draw your geometry in an FBO, then you use that FBO to draw that texture into another FBO or the main screen, but applying a post-processing shader. This is easy because my API has many methods to help you. You need to understand the mechanism tho. So be careful to initialize, resize, and clear your FBOs correctly, and also be careful, you can't read and write in an FBO at the same time. So if you want to apply multiple effects you need to ping-pong between 2 FBOs

Fortunately, you have the option to use a high-level API that does all of that for you. For advanced shaders, there are some lower-level methods that you can use but it is still simple to do if you understand the mechanism.

Full example

removeColors.frag

#version 330
precision highp float;

out vec4 color;
in vec2 v_texture;

uniform sampler2D u_sampler;

uniform int u_strength = 10;


void main()
{
	color = texture2D(u_sampler, v_texture).rgba;

	color.rgb *= u_strength;		
	color.rgb = floor(color.rgb);
	color.rgb /= u_strength;
}

blur.frag

#version 330
precision highp float;

out vec4 color;
in vec2 v_positions;
uniform sampler2D u_sampler;

void main()
{
	ivec2 s = textureSize(u_sampler, 0).xy;

	vec2 texelSize = vec2(1.f) / s;

	vec3 rez = vec3(0.f);

	for(int i=-3; i<=3; i++)
		for(int j=-3; j<=3; j++)
		{
			rez += texture2D(u_sampler, 
				(v_positions + vec2(1.f))/2.f + texelSize*vec2(i,j)).rgb * (1.f / 49.f);
		}

	color = vec4(rez,1);
}

Code:

#include <glad/glad.h>
#include <glfw/glfw3.h>
#include "gl2d/gl2d.h"

int main()
{
	// Initialize GLFW
	glfwInit();
	GLFWwindow *window = glfwCreateWindow(840, 640, "Window", nullptr, nullptr);
	glfwMakeContextCurrent(window);
	gladLoadGLLoader((GLADloadproc)(glfwGetProcAddress));
	
	// Initialize gl2d
	gl2d::init();


	gl2d::Renderer2D renderer;
	renderer.create();

	// Load resources example
	//gl2d::Font font(RESOURCES_PATH "roboto_black.ttf");
	gl2d::Texture texture(RESOURCES_PATH "test.jpg");
	gl2d::Texture background(RESOURCES_PATH "background.png");

	glm::ivec2 backgroundSize = background.GetSize();


	//auto default = gl2d::createPostProcessShaderFromFile(RESOURCES_PATH "defaultPostProcess.frag");
	auto blur = gl2d::createPostProcessShaderFromFile(RESOURCES_PATH "blur.frag");
	auto removeColors = gl2d::createPostProcessShaderFromFile(RESOURCES_PATH "removeColors.frag");
	gl2d::FrameBuffer fbo;
	gl2d::FrameBuffer fbo2;

	fbo.create(1, 1);
	fbo2.create(1, 1);

	// Main loop
	while (!glfwWindowShouldClose(window))
	{
		//very important, don't forget to call renderer.updateWindowMetrics, 
		//this is probably the thing that I forget most often
		int w = 0; int h = 0;
		glfwGetWindowSize(window, &w, &h);
		renderer.updateWindowMetrics(w, h);
		fbo.resize(w, h);
		fbo2.resize(w, h);



		// Handle input and update

		// Clear screen
		renderer.clearScreen({0, 0, 0, 1});
		fbo.clear();
		fbo2.clear();

		// Render objects
		renderer.renderRectangle({0, 0, backgroundSize}, background);
		
		renderer.renderRectangle({100, 250, 100, 100}, Colors_Orange, {}, 0);
		renderer.renderRectangle({100, 100, 100, 100}, texture, Colors_White, {}, 0);
		renderer.renderRectangle({400, 200, 100, 100}, texture, Colors_White, {}, 0);
		// Add more rendering here...


		//let the library handle it for you
		renderer.flushPostProcess({blur, removeColors});
		

		//you can also post process a texture and render it onto another fbo or the screen!
		//renderer.flushFBO(fbo);
		//renderer.postProcessOverATexture({blur, removeColors}, fbo.texture);


		//manually doing it version 1
		//renderer.flushFBO(fbo);
		//renderer.renderPostProcess(blur, fbo.texture, fbo2);
		//renderer.renderPostProcess(removeColors, fbo2.texture, {});

		//manually doing it version 2
		//renderer.flushFBO(fbo);
		//renderer.renderPostProcess(blur, fbo.texture, fbo2);
		//renderer.renderPostProcess(removeColors, fbo2.texture, fbo);
		//renderer.renderFrameBufferToTheEntireScreen(fbo);

		//manually doing it version 3
		//renderer.flushFBO(fbo);
		//renderer.renderPostProcess(blur, fbo.texture, fbo2);
		//renderer.renderPostProcess(removeColors, fbo2.texture, fbo);
		//renderer.renderRectangle({0,0,w,h},fbo.texture);
		//renderer.flush();

		// Swap buffers and poll events
		glfwSwapBuffers(window);
		glfwPollEvents();
	} 

	//cleanup if you want, no need for it here tho.
	return 0;
}

Note that you need to be careful with sizes. The size of the texture input should be the size of the fbo out. When using postProcessOverATexture or flushPostProcess the rendered should be configured to have the right dimensions! This is like so because the common case is rendering to the main FBO and that should have the sizes of the renderer. Note that a default FBO will draw to the screen. So you can pass {} to draw to the screen.

Result:

image

Clone this wiki locally