Intro to LE Post Processing Effects
Post effects are shader programs written in OpenGL Shading Language (GLSL). We use post effects to influence the entire look and feel of a scene. We refer to these effects as "post" effects because they are applied right after the scene has been rendered. They produce a fish eye effect, color an entire scene in grayscale, etc. In each frame of a game, a post effect iterates through every screen pixel and manipulates its color value.
You can find post effect programs in LE project Shaders/PostEffects folder. You apply them either via Root of the scene in the editor, or via Camera:AddPostEffect() function in code. Here is a screenshot of the fish eye effect I've recently made:
You can get the effect via the workshop. Hopefully, after reading this post, you'll have a sufficient knowledge to understand it and make your own cool effects. So, when you write a post effect shader, the primary - though not a final - goal is to get a color value of the current scene pixel. This is the easy part. Then comes the final goal: to do something interesting with the fetched color value.
GETTING THE PIXEL COLOR VALUE
Let's start with the simplest version of the post effect - the one that does nothing Our idea is to output pixel color values ready to be displayed on the screen, right after the current scene frame has been processed. We'll fetch frame's pixels' color values and simply forward them further down the rendering pipeline. If everything goes well, we will not notice any difference. If we mess up something, we'll either get the unpredicted results or, which is worse, we won't see anything. Particularly if our shader fails to compile because of syntax or other errors.
A post shader script consist of a vertex shader and a fragment shader.
A vertex part of the shader:
#version 400 uniform mat4 projectionmatrix; uniform mat4 drawmatrix; uniform vec2 position[4]; in vec3 vertex_position; void main(void) { gl_Position = projectionmatrix * (drawmatrix * vec4(position[gl_VertexID], 0.0, 1.0)); }
A fragment part of the shader:
#version 400 uniform sampler2D texture1; uniform bool isbackbuffer; uniform vec2 buffersize; out vec4 fragData0; void main(void) { vec2 coords = vec2(gl_FragCoord) / buffersize.xy; if (isbackbuffer) coords.y = 1.0 - coords.y; fragData0 = texture(texture1, coords); }
Since we won't use the vertex shader, you can ignore it for this tutorial. Let us instead concentrate on the fragment shader. First of all, what is a fragment? A fragment is a numerical value that defines a color of an individual screen pixel. There can be mutliple fragments per pixel. So, if you are using a 800 x 600 resolution, there will be at least 800 x 600 fragments. To keep it simple, we will think in terms of one fragment per pixel.
Let's say that our current screen resoultion is 800x600 and we are using one fragment per pixel. That means that we will be dealing with 800x600 fragments. Now, in every frame of your game, a fragment shader processes each of this fragments. The goal of the fragment shader is to output the final value of the current fragment, which in turn defines the final color of the screen pixel. The out vec4 fragData0 defines the variable which wil store this value. The out is a variable modifier that indicates a fragment shader output value. The vec4 defines a variable type. In GLSL, a color is represented as a 4-dimensional vector with r, g, b and a components.
uniform is another variable modifier. It denotes a sort of a constant value of the fragment shader. As you recall, a fragment shader script applies in turn to each fragment in each frame. The uniform modifier says that this value stays the same (uniform) for each fragment. Other uniforms used here:
-
buffersize is a 2-dimensional uniform vector which stores the size of the screen in pixels. In our case, buffersize.x = 800 and buffersize.y = 600.
-
sampler2D texture1 is another uniform variable. It stores the actual color values of screen pixels. We can read this values and use them to manipulate final color values, as we shall soon see. sampler2D is a special GLSL type that stores texture data.
-
isbackbuffer is a boolean you can safely ignore for all common purposes.
The hearth of the fragment shader is its main function. This is where the final fragment color value - a 4-dimensional vector with r, g, b and a components - gets calculated:
void main(void) { vec2 coords = vec2(gl_FragCoord) / buffersize.xy; if (isbackbuffer) coords.y = 1.0 - coords.y; fragData0 = texture(texture1, coords); }
gl_FragCoord is a built-in input vec4 that contains the screen (to be more precise, the game window) coordinates of the current fragment. In our case its x range is [0-799] and its y range is [0-599] since we are using 800 x 600 window resolution. On the other hand, texture1 coordinates are given in a so-called normalized space. Their range is [0-1]. In order to map fragment coordinates to texture coordinates, we simply divide gl_FragCoord.xy with buffersize.xy. For example, if our current fragment coordiantes are x=100 and y=80 our coords.x = 100/800 (0.125) and our coords.y = 80/600 (0.133).
vec2() is a GLSL function that extracts first two components, x and y, from a given vector. Please recall that gl_FragCoord is a vec4. buffersize.xy is an expression to get first two componets from an existing vector. Since buffersize is a vec2 we could have simply used buffersize instead of buffersize.xy.
The third line is where we finally set the output color value of the fragment. We use a built-in GLSL texture() function and normalized ([0-1] range) fragment coordinates stored in coords. The texture() is a so-called look up function. You give it a texture that you want to query for a color value and coordinates of a texel (a texture element) you want to get a color value for. (A texture element is analoguous to a fragment: there can be more than one texel per pixel. For the sake of simplicity, we'll just think of a texel as a pixel.) Basically, you just ask a texture to return its color value at certain xy coordinates. In our case, we want a texel with coordinates 0.125 and 0.133. Please recall that texture coordinates are given in normalized space: 0,0 is a bottom left texel and 1,1 is top right texel.
If everything went well - particularly, if shader compiled -, you won't notice any difference. That is because we simply read every single frame buffer pixel and outputed its value back to be displayed on the screen.
PROCESSING THE PIXEL COLOR VALUE
Let us now do something more adventurous: let us remove a red color from the final image that will be displayed on the screen.
void main(void) { vec2 coords = vec2(gl_FragCoord) / buffersize.xy; if (isbackbuffer) coords.y = 1.0 - coords.y; fragData0 = texture(texture1, coords); fragData0.r = 0; }
As you recall, fragData0 is a vec4. In its first component we store a red value of the pixel. Therefore, fragData0.r = 0 simply means that we are turning to zero the red value for every pixel on the screen. Here is what we've got:
Let's now try something even more ambitious: to make a horizontal red value gradient:
void main(void) { vec2 coords = vec2(gl_FragCoord) / buffersize; if (isbackbuffer) coords.y = 1.0 - coords.y; fragData0 = texture(texture1, coords); fragData0.r = coords.x; }
As we are moving further to the left, the screen gets more and more red. That's the effect of the fragData0.r = coords.x; line. Recall that fragData and coords components have [0-1] range. We are, basically, making our red channel dependent on the coords.x. The further we get to the right, the latter gets bigger and bigger and, consequently, our red channel gets more and more red value.
I hope this explains basics and wets your appetite. Go ahead and try to modify the green and blue components of the fragData0. You can also use math functions such as mod, sin, cos, etc., to process fragment color values. To get a current time, add uniform float currenttime to the fragment shader. You can then use it in the main function to make time based animated effects.
If you have any comments and corrections, feel free to respond. If you make a cool effect, publish it in the workshop and leave us a note/picture here. Thank you!
- 17
17 Comments
Recommended Comments