Post screen shaders

This is a really interesting topic, because it deals with a very powerful feature. Once you know what a post screen shader is you'll agree with me. So here's the explanation. You already should know what a standard shader effect is. If not, please read the chapter that deals with them. If you do not know how to write shaders you can not write post screen shaders and if you do not know what they are you can not even use them. Normal shader effects are great, because they can be used for many very decent effects. However, their possibilities are limited, because they can be applied only to single materials of models. This is a disadvantage post screen shaders do not have. They do not get applied to particular materials or models, but to the whole screen. After Ultimate 3D has rendered the complete scene, it applies the post screen shaders to the rendered image. This makes them powerful.

A simple example for this is that you want everything to be black and white for some reason. You could realize this by creating black and white copies of all textures and by changing all material colors to gray scale. You could also use a pixel shader effect for absolutely everything that's visible. Neither is desirable because it requires that you change every single object in your scene. With a post screen shader this can be realized much easier. You simply write one (very simple) post screen shader that implements the black and white effect. Then you apply it and you are done without even touching any scene objects. The post screen shader just takes the rendered image, takes the color out of every single pixel, and outputs it to the screen.

This is an example in which the post screen shader saves you a lot of work, but it does not do something that would not be possible without it. So here's another one that couldn't be done without post screen shaders: a simple blur effect. Imagine the protagonist of your game has been poisoned. Now you need some cool effect to show the player that he is not fine. A blur effect that applies to the whole screen is just perfect for this purpose. With usual shader effects this couldn't be implemented, but with post screen shaders it's no problem.

The range of possible effects is wide. Bloom, blur and different motion blur effects are common uses of post screen shaders. Additionally, post screen shaders can be used for numerous warp effects. Various exotic effects can also be realized. For example you can invert the colors, change the gamma, double the seen image (for drunk protagonists ;) ), darken or brighten up particular areas of the screen, highlight the contours for comic style graphics or make post screen shaders that make everything look as in an old black and white movie that has been watched thousands of times. Another thing they can be used for are depth of field effects, but that's a very advanced and computing time intensive technique.


The way post screen shaders work

This is a short chapter and the processes that are being described here are done automatically by Ultimate 3D. Nevertheless, it's important to know about these things, because otherwise you will not be able to understand why post screen shaders need to look the way they are meant to look. When you start using a post screen shader Ultimate 3D will not render the scene directly to the screen anymore. Instead it will render everything to a texture. When that's done, this texture contains everything you'd usually see on the screen. Then it renders a quad, which covers the whole screen and uses this texture. And this quad uses your post screen shader. This way you can define how the texture, which contains the rendered image of the scene, will get rendered to the screen.

It's also possible to use multiple post screen shaders. They will be applied one after the other. If you use two post screen shaders, Ultimate 3D will render the complete scene to a texture. Then it will render a quad that uses this scene texture to a second texture using the first post screen shader you defined. In the next step it renders this second texture to the screen using the second post screen shader you defined. If there were a third post screen shader the second one would have rendered to the first texture again and this first texture would get rendered to the screen using the third post screen shader. This means that there will be never more than two textures for all post screen shaders, so in terms of memory usage post screen shaders are very economical.

Another thing you should note about chained post screen shaders is that the order they get applied in does matter. Here's a simple example of this. Let's say you have a scene in which everything is blue for some reason (unrealistic, but it's a good example right now). The red channel and the green channel are always zero. And let's say that you've got two post screen shaders. The first one makes everything gray scale by taking the arithmetic mean of all three channels, and the second one ignores the green channel and the blue channel and replicates the values of the red channel to all three channels. If you apply the red-replication post screen shader before the gray scale post screen shader, the red-replication post screen shader will output a black picture, because the red channel is not used in the scene and the gray scale shader will not change this black picture. If you use the red-replication post screen shader after the gray scale post screen shader the gray scale shader will output a darker version of the blue channel to all three channels and the red-replication shader will have some positive data to output. So the results differ, depending on the order of the effects. For this reason you can set up a priority for every post screen shader.


The use of post screen shaders

It may seem a bit strange, but post screen shaders get represented by Game Maker objects just like models, primitives and terrains. The reason for this is that it makes their use very comfortable. You simply have one object for every post screen shader effect and whenever you need one of them you simply create an instance of the corresponding object. The rest is done by the object automatically. Also it happens very often that the same post screen shader needs to be used multiple times to reach an effect that's strong enough. In this case you can simply create multiple instances of the post screen shader object. And if a Game Maker room always uses a particular post screen shader you just place an instance of the post screen shader object in this room and you're done. No elaborate coding.

So the only thing you need to know now is how to create these post screen shader objects. It's quite easy. The complicated part is writing the shaders, but if that's too difficult for you, you can always use shaders made by others. The creation of valid post screen shaders will be dealt with below. The creation of post screen shader objects is similar to the creation of all other Ultimate 3D objects. In the first step you need to set up a couple of variables. Then you call a function to create the post screen shader. After that you can call some other functions to set up other options for the post screen shader. Finally you need to put a call to the Destroy() script in the destroy event. Step() is not needed. Here's a list of the variables that are available for post screen shaders. There are only four:

vertex_shader
The vertex shader that belongs to this post screen shader. This variable has to be set to a string, e.g. "effects/BlurPSS.vsh".

pixel_shader
The pixel shader that belongs to this post screen shader. This variable has to be set to a string, e.g. "effects/BlurPSS.psh". Note that the creation of the post screen shader effect will fail, without showing an error in case the required pixel shader version is not supported. For this reason you have to call GetSupportedPSVersion() before creating the post screen shader to make sure that the required pixel shader version is supported.

camera_index (optional)
Every post screen shader applies to the rendered image of a particular camera object. If this is not the default camera, you have to set this variable to the index of the particular camera, which is the value of the number variable of that camera (ranging from 1 to 31, as 0 refers to the default camera), not the id of the object.

priority (optional)
As explained above, the order post screen shaders get applied in does matter. For this reason you can use this variable to assign priorities to post screen shader objects. This can be any floating point value. The higher the value of this variable is, the earlier the post screen shader gets applied in each frame.


Once you are done with setting up all these variables you can use the function CreatePostScreenShader() to create the post screen shader. Now you may be wondering why post screen shaders do not use the Ultimate 3D effect file format (*.ufx). The reason is that it would have made things unnecessarily complicated. Since post screen shaders do not get applied to 3D objects but to the whole screen, there is not much precomputed constant information that could be set up for them. There's no information about materials, the transformation of scene objects does not matter and the light sources are already applied. Usually all you need is some color information or some set of factors to change the strength of the effect. These can be set up either through the def instruction of the particular shader, or through the following two functions:

This function sets up a particular vertex shader constant register for the post screen shader object it's called by. It is able to overwrite constant registers that have been set up through the def instruction of vertex shaders.

SetPSSVSConstant(
VSConstantIndex,
Value1, Value2, Value3, Value4
)

VSConstantIndex
The index of the vertex shader constant register you want to modify. This can be an arbitrary value in the range from 0 to 95.

Value1, Value2, Value3, Value4
The values you want to set up for this constant register. Value1 gets copied to the x-component, Value2 gets copied to the y-component, Value3 gets copied to the z-component and Value4 gets copied to the w-component.


This function sets up a particular pixel shader constant register for the post screen shader object it's called by. It is not able to overwrite constant registers that have been set up through the def instruction of pixel shaders.

SetPSSPSConstant(
PSConstantIndex,
Value1, Value2, Value3, Value4
)

PSConstantIndex
The index of the pixel shader constant register you want to modify. This can be an arbitrary value in the range from 0 to 7.

Value1, Value2, Value3, Value4
The values you want to set up for this constant register.


But the shader constants aren't everything you can set up for post screen shaders. They can also have one texture set up for every texture stage. Only the texture of the first texture stage (stage 0) can not be changed, because this stage always uses the scene texture. The textures of all other stages can be set up through the following function.

This function sets up the given texture as a texture for the given texture stage of the post screen shader object it's called by:

SetPSSTexture(
TextureStage,
TextureIndex
)

TextureStage
The index of the stage you want to set up another texture for. Since the first texture stage always uses the scene texture and can not be modified, this has to be a value in the range from 1 to 7.

TextureIndex
The index of the texture you want to set up for this stage, which is the return value of the particular texture loading function.


When writing post screen shaders it happens quite often that you can take advantage of particular texture filters to make your effects look better. For example if you use linear filtering for the scene texture this will already make it a bit blurry, which can be desired or not. For this reason you can set up an individual texture filter for every single texture stage for post screen shaders. By default, the first stage uses nearest point filtering and all other stages use the texture filter that has been set up through SetFilter(...), which is linear filtering by default. If you want to set up another filter you can use the following function to do this:

This function sets up a different texture filter for a particular texture stage of the post screen shader object that calls this function:

SetPSSFilter(
TextureStage,
FilterType
)

TextureStage
The texture stage you want to set up a new filter for. This can be an arbitrary value in the range from 0 to 7.

FilterType
The filter you want to set up for this stage. To get a description of the meaning of the single values you can pass for this parameter, please refer to the description of the function
SetFilter(...).


Writing post screen shaders

Post screen shaders are written in the same assembler language as the usual shader effects. However, they are usually quite different. The vertex shaders, especially, can differ very strongly. The first reason is that no matrix calculations need to be done, because the vertex position register is already in projection space. The second reason is that the quads, that are being used to apply the post screen shaders, are made up of no more than four vertices. So every vertex shader of a post screen shader does not compute more than four vertices, which is about nothing in comparison to the hundreds and thousands of vertices that need to be computed for shaders that are used for 3D models. For this reason you do not need to care about optimization in post screen vertex shaders. If ten additional lines of vertex shader code can save you one line of pixel shader code, or if they make the use of the post screen shader effect a little bit more comfortable, you should use them.

The most important thing you need to know to be able to write valid post screen shaders, are the available vertex data registers. These are v0, v7 and v8. Here's a description of their contents:

v0 contains the vertex position in projection space. Since it's already in projection space you can simply use a mov instruction to pass it to the oPos output register. No matrix is needed.

v7 contains the texture coordinates that need to be used for the scene texture.

v8 contains texture coordinates in the range from 0 to 1, which can be used for all other textures.

The reason that v7 and v8 are different is that textures must always have dimensions made up of powers of two, whereas render targets can have an arbitrary size. So if you have a viewport with a size of 800*600, the scene texture will have the dimensions 1024*1024, so the texture coordinates of v7 will be in the range from 0 to 800/1024 for the x-coordinate, and in the range from 0 to 600/1024 for the y-coordinate.

After all this theory here's a little example post screen shader. It uses the texture coordinate distortion map that's supposed to be set up for stage 1 to distort the image on the screen. This little example post screen shader is already very powerful. It can be used for cool warp effects.

Here's the vertex shader code:

vs.1.1

mov oPos, v0
mov oT0, v7
mov oT1, v8

Couldn't be easier, could it :D ? Here's the pixel shader code:

ps.1.4

texcrd r0.rgb, t0
texld r1, t1

mad r1.rgb, r1, c0, r0

phase

texld r0, r1

The pixel shader does the following. First it gets the first texture coordinate for this pixel and the data from the distortion map. Then it multiplies the data from the distortion map by a factor and adds the pixel's first texture coordinate. Finally it uses this new texture coordinate to look up a texel in the scene texture. This will be the output of this shader. To use this shader you need to set up the distortion map for stage 1 and a distortion strength factor for the first pixel shader constant. That's all.



© Christoph Peters. Some rights reserved.

Creative Commons License XHTML 1.0 Transitional