Welcome Guest, you are in: • Language Login

Nova by Vertice Wiki

RSS RSS

Navigation

Search the wiki
»





PoweredBy


Overview

We will see in these series of examples how it is possible to develop new materials of the shaders type with the Shader Forge provided with NOVA Studio. The Shader Forge simplifies the writing, while allowing, via graphics interfaces, the setting of our shaders (textures, lights, etc.). Indeed, if the code of the shader respects the SAS standard, the engine of NOVA Studio understands the code and interacts with its setting in a completely automatic way. SAS standard being based on simple conventions, it enables us to simplify the writing of the shaders considerably.

Before building our first shader, it is useful to go through some concepts:

We define by GPU the processor of the graphics board (Graphic Processing Unit). Its role is to produce, for a whole of points representing a world in 3D, the colour of each pixel of the rendered zone. The GPUs were especially conceived to effectively carry out calculations relating to the 3D and differ in this direction from the CPU which can carry out all types of operations but is much slower than the GPU for geometrical calculations.

The GPU is made of many components, the two more important being the vertex pipeline and the pixel pipeline because they are programmable (whereas the others can only be set up). The first is taking care of the vertices (plural of vertex) and the second of the pixels. Initially we can make the following abstraction to understand well the difference between these 2 pipelines: the vertex pipeline carries out the treatments relating to the geometry whereas the pixel pipeline carries out the treatments relative to scene rendering (pixels to be displayed on the screen).

The pipeline (i.e. the whole of the treatments carried out, in the order) of a GPU is complex. We can however schematize it to understand the way it works:

  1. The geometrical data are passed in the entry of the pipeline (the scene, in the form of points made up of 3 co-ordinates, more possibly of the co-ordinates of texture, of the normals, etc).
  2. The vertex pipeline carries out the desired geometrical transformations (transformation of the objects of the world reference in the screen reference generally)
  3. A series of operations will then allow to eliminate points to keep only those which "will be transformed into pixel" (i.e. those which will be treated by the pixel pipeline):
    • Clipping : suppression of the points which are not in the field of vision of the camera (it is useless to calculate the colour of the points which will not be visible by the camera)
    • Culling : elimination of the back faces. We will not calculate the colour of the back of the faces because they are not visible from the point of view of the user (except if set up)
    • Hatching : construction of primitives starting from the vertices (we create "smoother" primitives by interpolating some information like the colour). The hatching creates a certain number of pixels which will be sent to the pixel pipeline for treatment
  4. The pixel pipeline receives in entry the data resulting from the hatching and determines for each pixel, the colour to be posted
  5. Before carrying out the rendering, the GPU carries out the tests of alpha and depth, then carries out the operations of alpha blending and fog

Building a shader is specifying to the GPU the operations to be carried out at the level of the vertex pipeline and of the pixel pipeline (we have, moreover, the possibility to configure the culling, the alpha blending and to set up the tests of alpha, of depth). The code of a shader is directly interpreted by the GPU.

With the vertex shaders, we disconnect the vertex pipeline by default to force ours. In the same way, with the pixel shader, we disconnect the pixel pipeline by default. There is a possibility of carrying out this treatment by object. This is why we can consider the shaders as materials.

Creation of a shader with NOVA Studio

If we open the Shader Forge and if we create a new "shader material" via the New button, the NOVA Studio prompt us for the name of this new shader and then display the following screen:

Image

We have now the choice between a Material Shader or a Post-Process Shader.

Image

Post-process development process will be described in the second half of this documentation.

Material Shader

If we click on OK, the template of development by default of a shader is automatically displayed in the shader forge :


float4x4 WorldViewProjection:WORLDVIEWPROJECTION;
struct VS_INPUT
{
    float4 position:POSITION;
};

struct VS_OUTPUT
{
    float4 position:POSITION;
};

VS_OUTPUT mainVS(VS_INPUT In)
{
    VS_OUTPUT Out;

    Out.position = mul(In.position, WorldViewProjection);
	
    return Out;
}

float4 mainPS(VS_OUTPUT In) : COLOR
{
	return 1.0f;
}

technique MainTechnique
{
    pass P0
    {   
        VertexShader = compile vs_2_0 mainVS();
        PixelShader  = compile ps_2_0 mainPS();
    }
}

We find here the essential minimum structure of a shader:

  • A 4x4 matrix representing the matrix of transformation in the screen reference
  • The format of the entry points of the vertex shader described by the structure struct VS_INPUT
  • The format of the exit points of the vertex shader (and thus in entry of the pixel shader) described by the structure struct VS_OUTPUT
  • The vertex shader: mainVS
  • The pixel shader: mainPS
  • A technique: MainTechnique

If we compile this shader, NOVA locates the main technique (the first met which will be able to work with the material in progress if there are several: MainTechnique) then compile the vertex shader and the pixel shader in the specified version.

The target of a technique is thus to specify which vertex shader and which pixel shader will be used as well as the version of shaders to use:


technique PostProcess
{
    pass p0
    {
        VertexShader = compile vs_2_0 VS_Quad();
        PixelShader = compile ps_2_0 PostProcessPS();
    }
}

We will reconsider thereafter the concept of pass. We can see that for the MainTechnique technique, the vertex shader to be used is mainVS() and the pixel shader to be used is mainPS(). These 2 functions must be compiled (Compile) in release 2.0 (vs_2_0 and ps_2_0).

The graphics boards supporting the vertex shaders support at least the releases 1.0 and 1.1. Version 1.0 has nearly disappeared, release 1.1 adds some instructions to release 1.0 and corresponds to DirectX 8, the 2.0 (and more) to DirectX 9.

Variables of the shader


float4x4 WorldViewProjection:WORLDVIEWPROJECTION;

The projection matrix - calculated automatically by NOVA - will enable us to transform an object in the screen reference. To go in the screen reference, you just have to multiply the co-ordinates of the point by this matrix (we will carry out this operation in the vertex shader). NOVA recognizes this variable (WorldViewProjection) thanks to the key word WORLDVIEWPROJECTION, according to standards defined by the SAS standard .

The variables of a shader can be of various types according to what we wish to make in our shader. It can be textures, entireties, floating, matrices, etc.

The structure of the vertices


struct VS_INPUT
{
    float4 position:POSITION;
};

struct VS_OUTPUT
{
    float4 position:POSITION;
};

It is only made up of the position of the point in the space (recognized by NOVA thanks to key word SAS: POSITION).

We could however specify more parameters, like the co-ordinate(s) of texture, the normal, the diffuse colour if it proves that this information can be useful in our calculations:


struct VS_INPUT
{
    float4 position : POSITION;
    float4 normal	: NORMAL;
    float4 texCoord	: TEXCOORD0;
};

Remark : in vertex shader entry, the vertices can have a maximum structure of the following type:


struct VS_INPUT
{
    float4 position:POSITION;
    float3 normal:NORMAL;
    float2 texCoord:TEXCOORD0;
    float2 texCoord2:TEXCOORD1;
    float3 tangent:TANGENT;
    float4 diffuse:COLOR;
};

The vertex shader


VS_OUTPUT mainVS(VS_INPUT In)
{
    VS_OUTPUT Out;

    Out.position = mul(In.position, WorldViewProjection);
	
    return Out;
}

  • mainVS : name of the vertex shader
  • VS_OUTPUT : format of the exit points of this pixel shader
  • VS_INPUT : format of the input points of this pixel shader

We can interpret this vertex shader in the following way: for each point In (of VS_INPUT type) passed in argument of this vertex shader, we will turn over a Out point whose format is in conformity with that defined by VS_OUTPUT.

In the vertex shader :

  • We declare the point which we will return: Out
  • We indicate that the position of the Out point is equal to the multiplication of the co-ordinate of the point by the matrix of projection (Out.position represents the point in the screen reference whereas In.position represents the point in the reference of the object)
  • We return Out

The pixel shader


float4 mainPS(VS_OUTPUT In) : COLOR
{
    return 1.0f;
}

This pixel shader can de interpreted in the following way: for each entry point (of VS_OUTPUT type ) of the pixel shader, return white as colour of pixel for this point.

The default shder carries out the transformation of the co-ordinates and displays a white colour on the object.

Use of the SAS standard in the shaders

We will approach here the essential notion of the settings of the shader by the user thanks to the SAS standard (Standard Annotations and Semantics). SAS standard is directly interpreted by NOVA.

After having created a new shader, that we have named "Sample - Utilisation SAS", the following lines are injected at the beginning of the source file of the shader:


int informations : SasGlobal
< 
    int3 SasVersion = {1,0,0};
    string SasEffectAuthor = "Toto";
    string SasEffectCategory = "Exemples";
    string SasEffectCompany = "Vertice";
    string SasEffectDescription = "Use of globals parameters";
>;

Clicking on the Compile button makes the new material available in the Entities Browser :

Image

Selecting this material allows us to have access to its properties (Properties window ):

Image

In the sub-menu SAS, information which was entered (by respecting the SAS standard ) was recognized automatically by NOVA. Only the global parameters of our shader were specified, but this same standard can be used to allow the user to configure other parameters of the shader, as the color for example.


float4 diffuseColor : DIFFUSE;

After compiling, we remark that we have access to the diffuseColor property by viewing the parameter collection of the shader :

The shader parameters can be accessed by the Parameter property

The shader parameters can be accessed by the Parameter property


The Parameters Editor window lists all the parameters of the shader

The Parameters Editor window lists all the parameters of the shader


NOVA has directly recognized that the diffuseColor parameter was a colour (thanks to the DIFFUSE semantic), and allows us to configure it thanks to a "ColorPicker" which appears when we click on the colour to modify it:

Les paramètres peuvent être divers (lumière, texture, nombre), et nous avons de plus la possibilité de customiser l'interface proposée à l'utilisateur.

The parameters can be various(light, texture, number), and we have moreover the possibility to customize the interface offered to the user.

Example :


// Color
float4 diffuseColor : COLOR
<
    string SasUiControl = "ColorPicker";
    string SasUiDescription = "Object color";
>;

// Texture
texture colorTexture;

// Number with umeric control
float number : NUMERIC
<
    string SasUiControl = "Numeric";
    string SasUiDescription = "Number to configure";
    float SasUiMin = 0.0;
    float SasUiMax = 1.0;
    float SasUiStride = 0.1;
>;

// Number with slider
float number2 : NUMERIC
<
    string SasUiControl = "Slider";
    string SasUiDescription = "Number to configure with slider";
    float SasUiMin = 0.0;
    float SasUiMax = 1.0;
    float SasUiStride = 0.1;
>;

By accessing to the Parameters via the Properties window:

Image

The SasUiDescription parameters are used to display information bubbles when passing the mouse over the control.

Beaucoup de choses sont possibles avec la norme SAS. Pour plus d'informations ou de la documentation, se reporter au site de référence du MSDN situé à l'adresse : http://msdn.microsoft.com/en-us/library/bb173004(VS.85).aspx

There are many possibilities thanks to the SAS standard. For more information or for the documentation, please refer to the reference website for MSDN located here: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directx9_c/dx9_graphics_reference_effects_dxsas.asp

Annotations supported by the Shader Forge

The Shader Forge is able to create automatic links thanks to a certain number of keywords called annotations.

Example :


float4x4 WorldViewProjection : WORLDVIEWPROJECTION;

The Shader Forge recognize the note WORLDVIEWPROJECTION and will determine the value of the matrix of projection in the screen reference when executing. This matrix is thus available via the WorldViewProjection variable (initialized automatically by The Shader Forge).

List of the annotations supported by the Shader Forge:

  • PROJECTION
  • VIEW
  • VIEWPROJ
  • VIEWPROJECTION
  • WORLD
  • WORLDTRANSPOSE
  • WORLDT (<=> WORLDTRANSPOSE)
  • WORLDVIEW
  • WORLDVIEWINVERSE
  • WORLDI (<=> WORLDVIEWINVERSE)
  • WORLDVIEWTRANSPOSE
  • WORLDVIEWINVERSETRANSPOSE
  • WORLDVIEWIT (<=> WORLDVIEWINVERSETRANSPOSE)
  • WORLDVIEWPROJ
  • WORLDINVERSE
  • WORLDI (<=> WORLDINVERSE)
  • WORLDINVERSETRANSPOSE
  • WORLDIT (<=> WORLDINVERSETRANSPOSE)
  • VIEWINVERSE
  • VIEWI (<=> VIEWINVERSE)
  • VIEWINVERSETRANSPOSE
  • VIEWIT (<=> VIEWINVERSETRANSPOSE)
  • AMBIANT
  • TIME
  • WORLDCAMERAPOSITION

Applying a colour to an object

We will create a shader which will be used as a basis for the following examples. This shader assigns a uniform colour to the object to which it is affected.

The user must be able to specify the colour. A diffuseColor parameter is thus added in the code of new a shader:


float4x4 WorldViewProjection:WORLDVIEWPROJECTION;

float4 diffuseColor : DIFFUSE;

struct VS_INPUT
{
    float4 position:POSITION;
};

struct VS_OUTPUT
{
    float4 position:POSITION;
};

VS_OUTPUT mainVS(VS_INPUT In)
{
    VS_OUTPUT Out;

    Out.position = mul(In.position, WorldViewProjection);
	
    return Out;
}

float4 mainPS(VS_OUTPUT In) : COLOR
{
    return diffuseColor;
}

technique MainTechnique
{
    pass P0
    {   
        VertexShader = compile vs_2_0 mainVS();
        PixelShader  = compile ps_2_0 mainPS();
    }
}

Rather than turning over 1.0 as colour in the pixel shader, the pixel shader returns the configurable diffuseColor parameter.

Once this shader compiled (), the colour returned by the pixel shader is adjustable via the Material Parameters property.

Note: We can use DIFFUSE as key word. NOVA recognizes that it is a parameter of colour and automatically creates the ColorPicker in the "Parameters" window.

Applying textures

Diffuse textures

It is possible to read in one or more textures of the shaders (if required to mix the colours coming from these textures).

The entry points of the shader must have at least a value of co-ordinates of texture:


struct VS_INPUT
{
    float4 position:POSITION;
    float2 tex:TEXCOORD0;
};


The texture to use in our shader is declared :


float4 diffuseColor : DIFFUSE;

As well as a sampler to read in our texture :


sampler colorSampler = sampler_state
{
    texture = colorTexture;
};
The sampler is a tool compatible with the instruction allowing to read a colour in a texture.

The vertex shader does not do anything in particular. It calculates the position of the screen reference point and just transmits the co-ordinates of texture to the pixel shader :


VS_OUTPUT mainVS(VS_INPUT In)
{
    VS_OUTPUT Out;

    Out.position = mul(In.position, WorldViewProjection);
    Out.texCoord = In.texCoord;
	
    return Out;
}

In the pixel shader, we read the sampler via the tex2D command :


float4 mainPS(VS_OUTPUT In) : COLOR
{
    return tex2D(colorSampler, In.texCoord);
}

We then just have to specify a technique :


technique MainTechnique
{
    pass P0
    {   
        VertexShader = compile vs_2_0 mainVS();
        PixelShader  = compile ps_2_0 mainPS();
    }
}

Compile the shader (), if you did not make an error of syntax. It is now available in the Entities Browser, under Materials.

An object affected by this shader becomes totally white, which is normal because we did not specify a texture. . Go in the Properties window of the Shader and click on Parameters to specify a texture to be used.

A default texture can also be specified :


texture colorTexture : DIFFUSE
<
    string ResourceName = "ColorTexture.dds";
    string ResourceType = "2D";
>

Note: the Shader Forge looks for the textures located in the directory of the scene in which we use the shader.

Procedural textures

In the previous example, a colour is read in a texture pre-generated by a third tool. But it is also possible to generate our textures "on the fly"", i.e. directly in our shaders.

These textures known as procedural, because they are calculated dynamically with the execution according to a procedure (a function). The advantage of this technique is that we can generate the texture according to information contained in the code of the shader or according to a precise algorithm. Procedural textures must be used with good knowledge and only if the generation by an external tool is not possible.

This type of textures is usually used to generate textures of noise according to a certain algorithm, for example :


float4x4 WorldViewProjection:WORLDVIEWPROJECTION;

struct VS_INPUT
{
    float4 position:POSITION;
};

struct VS_OUTPUT
{
    float4 position : POSITION;
    float2 texCoord : TEXCOORD0;
};

VS_OUTPUT mainVS(VS_INPUT In)
{
    VS_OUTPUT Out;

    Out.position = mul(In.position, WorldViewProjection);
    Out.texCoord = In.position.xyz;
	
    return Out;
}

float4 colorToBlend : DIFFUSE = {1.0f, 1.0f, 1.0f, 1.0f};

texture NoiseTexture
<
    string type = "VOLUME";
    string function = "GenerateNoise";
>;

sampler SamplerNoise = sampler_state
{
    texture = NoiseTexture;
};

float4 GenerateNoise(float3 pos) : COLOR
{
    float4 n = (float4)0;
	
    for (int i = 1; i < 256; i+= i)
    {
    	n.r += abs(noise( 500 * i)) / i;
        n.g += abs(noise(( 1) * 500 * i)) / i;
        n.b += abs(noise(( 2) * 500 * i)) / i;
        n.a += abs(noise(( 3) * 500 * i)) / i;
    }
	
    return n;
}

float4 mainPS(VS_OUTPUT In) : COLOR
{
    colorToBlend = GenerateNoise(In.position);
    return colorToBlend;
}

technique MainTechnique
{
    pass P0
    {   
        VertexShader = compile vs_2_0 mainVS();
        PixelShader  = compile ps_2_0 mainPS();
    }
}

The lighting

We wish to be able to take into account the lighting in our shaders. To understand how the lighting works in 3D helps to understand well what occurs when we program shaders.

The calculation of lighting can be carried out in 2 different ways:

  1. At the level of the vertices (i.e. in the vertex shader)
  2. Directly at the level of the pixels (i.e. in the pixel shader)

The first method is faster because the GPU carries out an interpolation of the lighting calculation or the pixels of the face. It is however less realistic. With the second method, the calculation of lighting is carried out for all the pixels. That has a impact at the GPU level but the effect obtained is more realistic.

According to the needs for the effect, we will adopt one of these 2 techniques.

We will now see an example for each one of these methods, but it is first needed to select a model for the calculation of the light intensity. Many models were elaborated. The most running and generally used in the programming of shader is that of the diffuse reflexion. In the model of diffuse reflexion, the intensity in a point of surface depends on the angle formed between the ray of the light which touches the point and the normal on the surface. The smaller the angle formed between the ray of light and the normal in the plan is, the stronger the reflected light intensity visible by the observer is. To simplify, the following formulation will be used:

The light intensity in a point is the scalar product of the normal in this point by the vector direction of the light.

Once the light intensity calculated in a point, we just have to multiply the colour of this point by the light intensity to apply the effect of lighting.

In this example, we will only use the model of lighting of the diffuse light. Other models exist as the specular reflexion (depending of the point of view of the observer). Its description is largely available on Internet.

First part: Lighting per Vertex

We will need the world matrix (World) :


float4x4 World : WORLD;

Structure of the input points of the vertex shader. We need the normal at the point:


struct VS_INPUT
{
    float4 position : POSITION;
    float3 normal: NORMAL;
};

At the output of the vertex shader (and thus in the entry of the pixel shader), we will have the value of the intensity of the light in this point:


struct VS_OUTPUT
{
    float4 position : POSITION;
    float lightIntensity : TEXCOORD1;
};

In the vertex shader :


VS_OUTPUT mainVS(VS_INPUT In)
{
    VS_OUTPUT Out;

    Out.position = mul(In.position, WorldViewProjection);
	
    float3 N = mul(In.normal, (float3x3)World);
    N = normalize(N);
	
    float3 L = normalize(lightVector);
	
    Out.lightIntensity = dot(N, L);
	
    return Out;
}

The normal of the point is transformed in the screen reference (it "appears" in the local reference of the object) in order to be able to use it with lightVector, the vector direction of the light (expressed in the reference of the screen).


float4 mainPS(VS_OUTPUT In) : COLOR
{
    float4 color = float4(0.5, 0.5, 0.5, 1.0);	
    return color * In.lightIntensity;
}

Second part : Lighting per pixel

The structure of the inputs points of the vertex shader and the pixel shader:


float4x4 World : WORLD;
float4x4 WorldViewProjection : WORLDVIEWPROJECTION;

float3 lightVector = float3(-0.5f,2.0f,1.25f);

struct VS_INPUT
{
    float4 position : POSITION;
    float3 normal: NORMAL;
};

struct VS_OUTPUT
{
    float4 position : POSITION;
    float3 normal : TEXCOORD0;
    float light : TEXCOORD1;
};

The vertex shader compute the informations about the vertices.


VS_OUTPUT mainVS(VS_INPUT In)
{
    VS_OUTPUT Out;
	
    float3 P = mul(In.position, World);
    float3 N = mul(In.normal, World);
	
	
    Out.position = mul(In.position, WorldViewProjection);
    Out.normal = In.normal;
    Out.light = normalize(lightVector);
	
    return Out;
}

The calculation of the scalar product is carried out in the pixel shader. The intensity is not calculated at the level of the tops but for all the pixels. There will be no interpolation as with the vertex shader. The effect is much more precise :


float4 mainPS(VS_OUTPUT In) : COLOR
{
    float4 color = float4(0.5, 0.5, 0.5, 1.0);	
    float light = dot(In.normal, In.light);
    return color * light;
}

Link between the shader and the lights of a scene

It is not possible to know in advance the position, the colour or the name of the lights which will be in the scene. In this case, the Shader Forge is useful as it offers an automatic connection with the lights present at the execution.


float3 lightPos : POSITION
<
    string UIName = "Light Position";
    string Object = "PointLight";
    string Space = "World";
> = {-10.0f, 10.0f, -10.0f};

If our scene contains a light of the Point type, then LightPos contains the position of this light. If not, LightPos contains the position {- 10,10,-10}.

For the Object parameter the possible values are :
  • SpotLight
  • TargetLight
  • DirectionnalLight
  • PointLight

The POSITION semantics allows to recover the position of the corresponding light. If nothing is specified, the value of the variable (LightPos) is deduced according to the Object parameter (Direction vector if it is a DirectionnalLigth for example).

Pour récupérer la couleur d'une lumière, la sémantique LIGHTCOLOR est utilisé de la même façon qu'on a utilisé POSITION.

To get the color of the light, the LIGHTCOLOR semantics is used in the same way that POSITION was used.



Several passes in the same technique, render to texture

The techniques always consist of at least one pass, but it can contain several of them. A pass corresponds to one rendering, i.e. a treatment of the points, but is not inevitably intended to a screen rendering (rendering towards a texture for example).

Several passes can be present in the same technique. Several techniques containing each one a certain number of passes (for several versions of shaders for example) can be present in a shader.

For example while starting with the technique corresponding to the highest version (most recent) of shader and while finishing by the technique compiled by the oldest version, NOVA will apply such or such technique automatically according to the graphics board on which the scene is visualized.

In this example, the technique consists of two passes corresponding to 2 different renderings.

The first pass makes a render to texture :


pass P0 <string Script="RenderColorTarget0=target; Draw=Geometry; Clear=color; Clear=depth";>
{   
    VertexShader = compile vs_2_0 mainVS();
    PixelShader  = compile ps_2_0 mainPS();
}

Dans la définition de notre passe, des annotations sont utilisées pour spécifier un script interprétable de la façon suivante : "Effectue cette passe en la rendant sur une texture nommé target (RenderColorTarget0=target), dessine la géométrie (Draw=Geometry) sur un fond noir (Clear=color), en vidant le depth buffer (Clear=color).

In the definition of our pass, annotations are used to specify an interpretable script in the following way: "Carry out this pass while rendering it on a texture named target (RenderColorTarget0=target), draw the geometry (Draw=Geometry) on a black background (Clear=colour), by emptying the depth buffer (Clear=colour).

The code of the vertex shader and the associated pixel shader is :


VS_OUTPUT mainVS(VS_INPUT In)
{
    VS_OUTPUT Out;
	
    Out.position = mul(In.position, WorldViewProjection);
	
    return Out;
}

float4 mainPS(VS_OUTPUT In) : COLOR
{
    return 1.0f;
}

NOVA will automatically interpret the script at the time of the execution of this pass. This pass thus carries out a rendering of the object in white in a texture with black background.

The second pass carries out a rendering on the screen.


pass P1 <string Script="RenderColorTarget0=; Draw=Buffer";>
{
    VertexShader = compile vs_2_0 mainQuadVS();
    PixelShader = compile ps_1_1 mainQuadPS();
}

In the script, we specify that we will draw a quad covering all the screen (Draw=Buffer), that the rendering is not done anymore on a texture but on the screen (by not specifying any parameter for the RenderColorTarget0).

The code of the vertex shader and the associated pixel shader thus become :


VS_QUAD mainQuadVS(VS_QUAD In)
{
    VS_QUAD Out;
	
    Out.position = mul(In.position, WorldViewProjection);
    Out.texCoord = In.texCoord;
	
    return Out;
}

float4 mainQuadPS(VS_QUAD In) : COLOR
{
    float3 color = tex2D(targetSampler, In.texCoord);
    return float4(1.0f - color.r, 1.0f - color.g, 1.0f - color.b, 0);
}

With the following structure :


struct VSQUAD
{
    float4 position : POSITION;
    float2 texCoord : TEXCOORD;
};

And the texture used as target :


texture target;
sampler targetSampler = sampler_state
{
    texture = target;
};

The vertex shader does not need to transform the entry points in the screen reference . They are already transformed as NOVA makes a covering quad pass.

The pixel shader reads the colour in a texture (calculated during the preceding pass) associated the co-ordinates of texture of this point and returns the opposite of this colour for the screen rendering. In texture, the object is white and the background is black. On the screen, the background is white, the object is black.

An example: the Hatching effect

This example is a synthesis of all the previous ones. We will reproduce the effect of hatching to represent - for example - a 3D scene with the aspect of a plan drawn by hand. Information will be read in a texture, the lighting of the scene will be taken into account and of the advanced operations will be carried out with the colours.

A texture is useful for this shader: it represents the hatchings. The information contained in this texture will be superimposed to simulate a sensitivity of the hatchings to the light :

Image

To superimpose information, the co-ordinates of the textures will be modified. The original texture contains vertical lines. We will make transformations on the co-ordinates of the texture to simulate the horizontal lines.

A possible improvement of the effect consists in representing the same horizontal and vertical lines, slightly shifted, a second time.

Each hatching (horizontal, horizontal shifted, vertical, vertical shifted) reacts differently to the light. The horizontal lines are persistent whereas the vertical lines are influenced by the effect of the light.

The corresponding code to this effect is the following :


// The parameters initialized automatically by Nova:
float4x4 World : WORLD;
float4x4 View : VIEW;
float4x4 Projetion : PROJECTION;
float4x4 WorldViewProjection:WORLDVIEWPROJECTION;

// Vectors that will be used to deform the co-ordinates of the texture
float4 offset0 = {0.08f, 0.38f, 0.26f, 0.02f};
float4 offset1 = {0.12f, 0.18f, 0.25f, 0.5f};

// The colour to apply to the Hatching:
float4 HatchingColor : COLOR
<
    string SasUiControl = "ColorPicker";
    string SasUiDescripton = "Color for hatching";
>;

/ To be able to configure the direction of our light
float lightVectorX
<
    string SasUiControl = "Numeric";
>;
float lightVectorY
< 
    string SasUiControl = "Numeric";
>;

float lightVectorZ
< 
    string SasUiControl = "Numeric";
>;

// The texture that will be used for this effect:
texture HatchingTexture;

sampler samplerHatchingTexture = sampler_state
{
    texture = HatchingTexture;
};

// The structure of the entry points of the vertex shader
struct VS_INPUT
{
    float4 position:POSITION;
    float3 normal:NORMAL;
    float2 texCoord:TEXCOORD0;
};

// The structure of the exit points of the vertex shader (and consequently on entry for the pixel shader)
// We will calculate some new co-ordinates of texture in the vertex shader to apply horizontal hatching, shifted horizontal, vertical, shifted vertical
struct VS_OUTPUT
{
    float4 position:POSITION;
    float2 tex0:TEXCOORD0;
    float2 tex1:TEXCOORD1;
    float2 tex2:TEXCOORD2;
    float2 tex3:TEXCOORD3;
    float3 normal:TEXCOORD4;
    float3 light: TEXCOORD5;
};

// Vertex shader:
VS_OUTPUT mainVS(VS_INPUT In)
{
    VS_OUTPUT Out;
	
    float3 P = mul(In.position, World);
    float3 N = mul(In.normal, (float3x3)World);

    Out.position = mul(In.position, WorldViewProjection);
    Out.normal = normalize(N);
    Out.light = normalize(float3(lightVectorX, lightVectorY, lightVectorZ));
	
    // We create new co-ordinates of texture:
    Out.tex0 = In.texCoord;
    Out.tex1 = In.texCoord + offset0.xy;
    Out.tex2 = In.texCoord.yx + offset0.zw;
    Out.tex3 = In.texCoord.yx + offset1.xy;
	
    return Out;
}

// Pixel  shader for a horizontal hatching
float4 oneLevelPS(VS_OUTPUT In) : COLOR
{
    float Intensity = dot(In.normal, In.light);    
    float4 stroke = tex2D(samplerHatchingTexture , In.tex0);
    float3 color = (HatchingColor);
    color *= (Intensity <0.75 + stroke.a/4) ? stroke.rgb : 1.0;
    return float4(color.r, color.g, color.b, 1.0);
}

// Pixel  shader for two horizontal hatchings
float4 twoLevelsPS(VS_OUTPUT In) : COLOR
{
    float Intensity = dot(In.normal, In.light);    
    float4 stroke = tex2D(samplerHatchingTexture , In.tex0);
    float3 color = (HatchingColor);
    color *= (Intensity <0.75 + stroke.a/4) ? stroke.rgb : 1.0;
    stroke = tex2D(samplerHatchingTexture ,In.tex1);
    color *= (Intensity <0.50 + stroke.a/4) ? stroke.rgb : 1.0;
    return float4(color.r, color.g, color.b, 1.0);
}

// Pixel  shader for two horizontal hatchings, one vertical
float4 threeLevelsPS(VS_OUTPUT In) : COLOR
{
    float Intensity = dot(In.normal, In.light);    
    float4 stroke = tex2D(samplerHatchingTexture , In.tex0);
    float3 color = (HatchingColor);
    color *= (Intensity <0.75 + stroke.a/4) ? stroke.rgb : 1.0;
    stroke = tex2D(samplerHatchingTexture ,In.tex1);
    color *= (Intensity <0.50 + stroke.a/4) ? stroke.rgb : 1.0;
    stroke = tex2D(samplerHatchingTexture ,In.tex3);
    color *= (Intensity < stroke.a/4) ? stroke.rgb : 1.0;
    return float4(color.r, color.g, color.b, 1.0);
}

// Full hatching effect :
float4 mainPS(VS_OUTPUT In) : COLOR
{
    // Compute the light intensity
    float Intensity = dot(In.normal, In.light);    
    // Get the hatching color
    float3 color = (HatchingColor);
    // For each hatching, we have the following process:
    // If the light intensity is lower than the alpha channel of the first line + a certain level, then we do not display the line.

    // 1) First hatching
    float4 stroke = tex2D(samplerHatchingTexture , In.tex0);
    color *= (Intensity <0.75 + stroke.a/4) ? stroke.rgb : 1.0;
    // 2) Second hatching
    stroke = tex2D(samplerHatchingTexture ,In.tex1);
    color *= (Intensity <0.50 + stroke.a/4) ? stroke.rgb : 1.0;
    // 3) Third hatching
    stroke = tex2D(samplerHatchingTexture ,In.tex2);
    color *= (Intensity <0.25 + stroke.a/4) ? stroke.rgb : 1.0;
    // 4) Fourth hatching
    stroke = tex2D(samplerHatchingTexture ,In.tex3);
    color *= (Intensity < stroke.a/4) ? stroke.rgb : 1.0;
    return float4(color.r, color.g, color.b, 1.0);
}

// Technique for two horizontal and two vertical hatchings
technique twoHorizontalsTwoVerticalsLevels
{
    pass P0
    {   
        vertexShader = compile vs_1_1 mainVS();
        pixelShader  = compile ps_2_0 mainPS();
    }
}

// Technique for one horizontal hatching
technique oneHorizontalLevel
{
    pass P0
    {   
        vertexShader = compile vs_1_1 mainVS();
        pixelShader  = compile ps_2_0 oneLevelPS();
    }
}

// Technique for two horizontal hatchings
technique twoHorizontalsLevels
{
    pass P0
    {   
        vertexShader = compile vs_1_1 mainVS();
        pixelShader  = compile ps_2_0 twoLevelsPS();
    }
}

// Technique for two horizontal, two vertical hatchings and one vertical
technique twoHorizontalsOneVerticalLevels
{
    pass P0
    {   
    vertexShader = compile vs_1_1 mainVS();
    pixelShader  = compile ps_2_0 threeLevelsPS();
    }
}

We can then specify the technique to be used after having compiled this code, in the properties of the Shader:

Image

Post-process shaders

When we create a new post-process shader, the Shader Forge display the following template :


sampler2D SceneSampler;
struct VS_OUTPUT
{
    float4 Position	: POSITION;
    float2 UV		: TEXCOORD0;
};

VS_OUTPUT VS_Quad(float4 Position : POSITION, float2 TexCoord : TEXCOORD0)
{
    VS_OUTPUT OUT;

    OUT.Position = Position;
    OUT.UV = TexCoord;

    return OUT;	
}

float4 PostProcessPS(VS_OUTPUT IN):COLOR0
{
    return tex2D(SceneSampler, IN.UV);
}

technique PostProcess
{
    pass p0
    {
        VertexShader = compile vs_2_0 VS_Quad();
        PixelShader = compile ps_2_0 PostProcessPS();
    }
}

Clicking on the Compile button allows the new material to be available in the Entities Browser :

Image

A right click on the "ShaderPostProcess_Example" material makes the affectation menu appear ("Assign as Post-Process on ...").

We can see that NOVA recognized our new material is of the post-process type and we can then assign this shader to one of the cameras of the scene. In the case of the shader by default, nothing occurs.

Dans le code du shader par défaut on constate que le vertex shader n'effectue pas de transformations sur les vertices (OUT.Position = Position). La seule préoccupation des shaders de post-process se situe au niveau du pixel shader. En effet, dans le cas d'un shader post-process, NOVA envoie directement un quad recouvrant l'écran. Concrètement, un rendu complet de la scène est effectué dans une texture disponible dans le premier sampler du shader (SceneSampler dans le shader par défaut).

In the code of the shader by default, we can see that the vertex shader does not make any transformation on the vertices (OUT.Position = Position). The only concern of the shaders of post-process is at the level of the pixel shader. Indeed, in the case of a post-process shader , NOVA directly sends a quad covering the screen. Concretely, a complete rendering of the scene is carried out in a texture available in the first sampler of the shader (SceneSampler in the shader by default).

Color filtering with a post-process shader

The post-process shaders allow the development of effects influencing the final rendering of all the scene. A first simple example consists in keeping one component of the color of the pixels to create a black and white rendering :


sampler2D SceneSampler;
struct VS_OUTPUT
{
    float4 Position	: POSITION;
    float2 UV		: TEXCOORD0;
};

VS_OUTPUT VS_Quad(float4 Position : POSITION, float2 TexCoord : TEXCOORD0)
{
    VS_OUTPUT OUT;

    OUT.Position = Position;
    OUT.UV = TexCoord;

    return OUT;	
}

float4 PostProcessPS(VS_OUTPUT IN):COLOR0
{
    float4 color = tex2D(SceneSampler, IN.UV);
 
    float value = (color.r + color.g + color.b) / 3; 
    color.r = value;
    color.g = value;
    color.b = value;
	
    return color;
}

technique PostProcess
{
    pass p0
    {
        VertexShader = compile vs_2_0 VS_Quad();
        PixelShader = compile ps_2_0 PostProcessPS();
    }
}

Image

In the same way, we can keep only one of the channels by modifying the code of the Pixel Shader :


float4 PostProcessPS(VS_OUTPUT IN):COLOR0
{	
    float3 color = tex2D(SceneSampler, IN.UV);
    return float4(color.r, 0, 0, 1.0);
}

Image

This other example shows how to modify the texture coordinates using the sinus function :


float4 PostProcessPS(VS_OUTPUT IN):COLOR0
{
    IN.UV.y = IN.UV.y + (sin(IN.UV.x*200)*0.01);
    return tex2D(SceneSampler, IN.UV);
}

Image

Annotations supported by the Shader Forge

An annotation is available for the realization of post-process shaders: SCREENSIZE. This annotation allows to have access to the size of the rendering texture via a float2 (a component for the width, a component for the height) filled automatically by Nova (with the annotations WORLD, WORLDVIEW, WORLDVIEWPROJECTION, etc for the shaders of the material type).

The parameter setting of the post-process shaders runs in the same way as the shaders of the material type. There is the possibility of specifying the type of control to be used in NOVA Studio to modify the value of the parameters of the post-process shaders.

By multiplying the channels by a floating value, we will be able to visualize the effects of cut or of amplification of the channels of colors:


float4 PostProcessPS(VS_OUTPUT IN):COLOR0
{	
    float3 color = tex2D(SceneSampler, IN.UV);
    return float4(color.r * rValue, color.g * gValue, color.b * bValue, 1.0);
}

We wish to make the parameter settings of these values via the interface of NOVA Studio. As considering previously with the shaders of the material type, this is done simply by using standard SAS:


float rValue <
    string UIName = "Red canal filter";
    string UIWidget = "slider";
    float UIMin = 0.0f;
    float UIMax = 5.0f;
    float UIStep = 0.1f;
> = 1.0f;

float gValue <
    string UIName = "Green canal filter";
    string UIWidget = "slider";
    float UIMin = 0.0f;
    float UIMax = 5.0f;
    float UIStep = 0.1f;
> = 1.0f;

float bValue<
    string UIName = "Blue canal filter";
    string UIWidget = "slider";
    float UIMin = 0.0f;
    float UIMax = 5.0f;
    float UIStep = 0.1f;
> = 1.0f;

Once compiled, if we select this shader in the Properties window, we have, as in the case of the shaders of material, a Parameters line:

By clicking on Image, it is possible to specify the value of our parameters :

Image
Image

Example : edge detection with a post-process shader

The examples considered previously where carrying out simple processes. It is possible to carry out processes much more complex from the information contained in the rendering texture. Thus the detection of edges of a 3D rendering is henceforth possible.

The code used for edge detection is the following:


sampler2D SceneSampler;
float2 QuadScreenSize : SCREENSIZE;

float NPixels
<
    string UIName = "Pixels Steps";
    string UIWidget = "slider";
    float UIMin = 1.0f;
    float UIMax = 5.0f;
    float UIStep = 0.5f;
> = 1.5f;

float Threshhold
< 
    string UIWidget = "slider";
    float UIMin = 0.01f;
    float UIMax = 0.5f;
    float UIStep = 0.01f;
> = 0.2;

struct EdgeVertexOutput
{
    float4 Position            : POSITION;
    float2 UV00                : TEXCOORD0;
    float2 UV01                : TEXCOORD1;
    float2 UV02                : TEXCOORD2;
    float2 UV10                : TEXCOORD3;
    float2 UV12                : TEXCOORD4;
    float2 UV20                : TEXCOORD5;
    float2 UV21                : TEXCOORD6;
    float2 UV22                : TEXCOORD7;
};

EdgeVertexOutput edgeVS(float3 Position : POSITION, float3 TexCoord : TEXCOORD0) 
{
    EdgeVertexOutput OUT;
    OUT.Position = float4(Position, 1);
    float2 off = float2(1/(QuadScreenSize.x),1/(QuadScreenSize.y));
    float2 ctr = float2(TexCoord.xy+off); 
    float2 ox = float2(NPixels/QuadScreenSize.x,0.0);
    float2 oy = float2(0.0,NPixels/QuadScreenSize.y);
    OUT.UV00 = ctr - ox - oy;
    OUT.UV01 = ctr - oy;
    OUT.UV02 = ctr + ox - oy;
    OUT.UV10 = ctr - ox;
    OUT.UV12 = ctr + ox;
    OUT.UV20 = ctr - ox + oy;
    OUT.UV21 = ctr + oy;
    OUT.UV22 = ctr + ox + oy;
    return OUT;;
}

float getGray(float4 c)
{
    return(dot(c.rgb,((0.33333).xxx)));
}

float4 edgeDetectPS(EdgeVertexOutput IN, uniform sampler2D ColorMap, uniform float T2) : COLOR 
{
    float4 CC;
    CC = tex2D(ColorMap,IN.UV00); float g00 = getGray(CC);
    CC = tex2D(ColorMap,IN.UV01); float g01 = getGray(CC);
    CC = tex2D(ColorMap,IN.UV02); float g02 = getGray(CC);
    CC = tex2D(ColorMap,IN.UV10); float g10 = getGray(CC);
    CC = tex2D(ColorMap,IN.UV12); float g12 = getGray(CC);
    CC = tex2D(ColorMap,IN.UV20); float g20 = getGray(CC);
    CC = tex2D(ColorMap,IN.UV21); float g21 = getGray(CC);
    CC = tex2D(ColorMap,IN.UV22); float g22 = getGray(CC);
    float sx = 0;
    sx -= g00;
    sx -= g01 * 2;
    sx -= g02;
    sx += g20;
    sx += g21 * 2;
    sx += g22;
    float sy = 0;
    sy -= g00;
    sy += g02;
    sy -= g10 * 2;
    sy += g12 * 2;
    sy -= g20;
    sy += g22;
    float dist = (sx*sx+sy*sy);
    float result = 1;
    if (dist>T2) { result = 0; }
    return result.xxxx;
}

technique PostProcess
{
    pass p0
    {
        VertexShader = compile vs_2_0 edgeVS();
        PixelShader = compile ps_2_0 edgeDetectPS(SceneSampler,(Threshhold*Threshhold));
    }
}

The post-process shader renders only the edges of the geometry

The post-process shader renders only the edges of the geometry