Jump to content
  • entries
    3
  • comments
    29
  • views
    8,088

Intro to LE Post Processing Effects


Lunarovich

7,802 views

 Share

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:

 

D24CABH.jpg

 

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 smile.png 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.

 

73GDqCa.png

 

 

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:

 

Af3m1sh.png

 

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;
}

 

zyF4dYv.jpg

 

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!

  • Upvote 17
 Share

17 Comments


Recommended Comments

You could say so. It is a tutorial on one kind of shaders - post processing effects. They are easier to grasp than model shaders, since you don't need to know linear algebra. Although, it wouldn't hurt :)

Link to comment

Thank you!

 

@tjheldna The idea is to explain LE specifics concernig effects and give a basic tutorial on post processing itself. So, there are two goals. In the next tutorial, I'll explain how to use math functions and how to make time-based animated effects. Basically, I'll adapt this tutorial for LE.

 

And the rest is up to you :)

 

"If you give a man a fish, you feed him for a day. But if you give him a fishing rod, you feed him for a lifetime". Well, this could be a good motto for this tutorial :)

  • Upvote 3
Link to comment

Great work.

 

I've been asking for more info on shaders for ages. After seeing the amazing things Shadmar has been doing with them and this nice tutorial I'll be keen to give it a try.

 

Looking forward to the next instalment.

 

Thank you.

  • Upvote 1
Link to comment

Thanks macklebee I'll look into it. I think I want to achieve this http://steamcommunity.com/sharedfiles/filedetails/?id=287356325 How did you do it ? Is it possible with Lua or only in C++ with buffers ?

 

My technique would be to render the selected objects alone in another world and render them to a texture to get a mask, and then to send this mask to the posteffect for processing.

I know how to do it with buffers, but I have no idea how to do it with Lua and posteffects without buffers.

Link to comment

Yes, it can be done with just lua because that's all i have. biggrin.png When I get a chance sometime this week (probably the weekend), I will shoot you a PM with my messy un-optimized code showing how I did exactly what you are describing. And btw the buffer class is exposed to lua. http://www.leadwerks.com/werkspace/topic/10171-buffer-commands-exposed-to-lua-with-documentation/#entry75328

Refer to any of the post-process shaders in the workshop with lua scripts (or the screen distortion shader I showed above) and you will see references to the buffer class.

Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...