B Tutorial Shader Shaders 101

Users who are viewing this thread

Shaders 101
Helping your mod stand apart
For Warband, VC and NW.
WIP

I am not an expert on shaders, I only recently learned hlsl and am in no way qualified to teach anyone anything about this. That being said, I am still going to try. If you are a technical artist and can pitch in to help round out this tutorial, I encourage you to pitch in.
If you would like a tutorial from an actual expert and programmer, and not some idiot, click here.

First: You must download The Warband Shader Kit from Taleworlds. This will include everything you need to make your own shader.
Second: Follow Swyters Compiler Tweaks. This will speed things up and make for a nicer, more fluid experience.
Third: Find an editor you like, I use Notepad++ with the language set to C. I've also had good experiences in VSCode.

A shader is actually made of three parts, a Vertex Shader, Pixel Shader and Technique. We will also declare the struct for the Vertex Shader Output ourselves, mostly for completionists' sake, but it is generally not necessary. However with more advanced shaders that need to pass along unique information it will be.

Scroll to the bottom of the file and define your VS struct
C-like:
struct VS_OUTPUT_FIRST_SHADER // Defining the structure for the vertex shader as well as any variables being passed through to the Pixel Shader
{
    float4 Pos           : POSITION;    // Vertex Position, Local to Mesh
    float2 Tex0             : TEXCOORD0;    // Texture Coordinates form Mesh
    float4 Color         : COLOR0;        // Vertex Coloring
 
    float  Fog           : FOG;            // Pixel Shader Fog
};

C-like:
// Read as: OUTPUT_STRUCTURE vs_shader_name(values inputted from the engine)
VS_OUTPUT_FIRST_SHADER vs_my_first_shader(float4 vPosition : POSITION, float4 vColor : COLOR, float2 tc : TEXCOORD0)
{
    VS_OUTPUT_FIRST_SHADER Out;                           // Out is just naming the Output Varaibles
 
    float4 vWorldPos = mul(matWorld, vPosition);    // vPosition in World Space, Used for Fog.
 
    Out.Tex0 = tc;                                    // Texture Coords passed straight through unmodified
 
    Out.Pos = mul(matWorldViewProj, vPosition);        // vPosition in Projected World View (World Space in Camera)
 
 
    // Native Fog Calculation //
    float3 P = mul(matWorldView, vPosition);         // vPosition in view space
    float d = length(P);                            // Distance from Camera
    Out.Fog = get_fog_amount_new(d, vWorldPos.z);    // Pass to a function to determine fog amount using depth and altitude

    Out.Color = vColor;                                // Vertex Color passed straight through unmodified
    return Out;                                        // Output the VS
}

You might think, hey, why isn't there an PS_INPUT, and you could make one, but the rendering pipeline assumes the VS_OUTPUT is 1:1 with PS_INPUT so it's redundant and completely unused in any Native shader.

C-like:
PS_OUTPUT ps_my_first_shader(VS_OUTPUT_FIRST_SHADER In)
{
    PS_OUTPUT Output;        // Name outputted variables Output.yaddayadda

    // Register Texture Maps and Uniforms
    float4 diffuse_color = tex2D(DiffuseTextureSamplerNoWrap, In.Tex0);    //Read the color for the pixel located at the Texture Coordinates

    Output.RGBColor = diffuse_color;    // Output the PS Structure

    return Output;
}


C-like:
technique my_first_shader    // Name the shader technique
{
    pass P0    // In the first pass (only one available in our engine :cry:)
    {
        VertexShader = compile vs_2_0 vs_my_first_shader();    // Our Vertex Shader, compiled using vs_2_0 with no constants
        PixelShader = compile ps_2_0 ps_my_first_shader(); // Our Pixel Shader, compiled using ps_2_0 with no constants
 
    }
}

Congratulations, you Authored your first Shader!

This only applies to the base Warband engine, Continue to "So you want to write a shader for WSE2?" for information on how to register the shader in WSE2.
Registering in OpenBrf

I find it easiest to simply copy and paste a native shader that also uses the same maps your shader is using, changing the shader and technique names to the target shader.

Then, change the shader on your target material. Boom. In game.

ImagesDescriptions
Mx-jS.png
Shader Tab:
  • Shader Name to reference in Materials
Data Tab:
  • Technique: Technique name in MB.fx
  • Requires: Prerequisite Flag data (See Below)
  • Fallback: Shader the engine should use if this one isn't usable
  • Flags: Shader Flag data (See Below)
  • Texture Access
    • map:
      • 0, diffuse
      • 1, diffuse2
      • 2, specular
      • 3, normal map
      • 4, environment texture
    • colorOp: ()
    • alphaOp: ()
    • flags: (No idea)
0oHdg.png
Prerequisite Flags:
  • pixel shader (pixel shaders are active in options, card supports PS1.1)
  • mid quality (shader_quality is > 0)
  • hi quality (shader_quality is > 1, card supports PS2.0a/b)
4CIpM.png
Shader Flags
  • specular enable (enables specular light)
  • static lighting (mesh will use Vertex Painting to simulate lighting (static, on scene creation))
  • uses env. map (shader uses Enviroment Map)
  • preshaded (uses preshaded technique)
  • uses instancing (shader receives instance data as inpit (TEXCOORD1...4))
  • biased (used for shadowmap bias)
  • uses pixel shader (this shader uses pixel shader)
  • uses HLSL (if not set FFP will be used)
  • uses normal map (shader receives binormal and tangent as input (BINORMAL, TANGENT))
  • uses skinning (shader receives skinning data (BLENDWEIGHTS, BLENDINDICIES))
Speculative Data, doesn't line up with my actual data, but they are the maps

So, you've come to a point where you would like to use this shader in more circumstances than one. Say you want to support more or less the same shader but give your artists an option to use it with a diffuse2 map that is lerped between based on vertex color.

Inside the inputs of your pixel shader add this:
C-like:
PS_OUTPUT ps_my_first_shader(VS_OUTPUT_FIRST_SHADER In, uniform const bool use_diffuse2)
//This lets the pixel shader know that it is to expect an input of a boolean that we're naming as use_diffuse2 as its first input. This input cannot be changed, hence the const and uniform parameters
{

    PS_OUTPUT Output;        // Name outputted variables Output.yaddayadda

    // Register Texture Maps and Uniforms

    float4 diffuse_color = tex2D(DiffuseTextureSamplerNoWrap, In.Tex0);    //Read the color for the pixel located at the Texture Coordinates
 
    if(use_diffuse2){    // Read as if(this is true), continue into these curlies
        float4 diffuse2_color = tex2D(Diffuse2Sampler, In.Tex0); // Read and store the diffuse2 texture
        diffuse_color = lerp(diffuse_color, diffuse2_color, saturate(distance(In.Color.rgb, float3(1.0, 1.0, 1.0)))); // This is ridiculous, but the less white the vertex color is, the more diffuse2 will show
    }

    Output.RGBColor = diffuse_color;    // Output the PS Structure

    return Output;

}

This will allow us to branch the shader inside the compile instructions inside the technique declaration, so that we don't have to write a whole extra shader just for that one function.

C-like:
technique my_first_shader    // Name the shader technique
{
    pass P0    // In the first pass (only one available in our engine :cry:)
    {
        VertexShader = compile vs_2_0 vs_my_first_shader();    // Our Vertex Shader, compiled using vs_2_0 with no constants
        PixelShader = compile ps_2_0 ps_my_first_shader(false); // Our Pixel Shader, compiled using ps_2_0 with no constants
    }

}
technique my_first_shader_d2lerp    // Name the NEW shader technique
{
    pass P0    // In the first pass (only one available in our engine :cry:)
    {
        VertexShader = compile vs_2_0 vs_my_first_shader();    // Our Vertex Shader, compiled using vs_2_0 with no constants
        PixelShader = compile ps_2_0 ps_my_first_shader(true); // Our Pixel Shader, compiled using ps_2_0 with no constants
    }
}

You can do other things like declaring the default value by setting it inside the shader input, that way you don't have to pass a value into the shader's input. uniform const bool use_diffuse2 = false will make it where use_diffuse2 will always be false unless you pass a different value when calling the technique.

If you're using the same bits of code across multiple shaders and you want either make it easy to iterate across all the usages, or keep everything visually consistent, you'll want to use functions. Functions are essentially little programs inside your shader code, and they're super easy to define and use.


C-like:
// Say we need an easy way to average a color's RGB value many different times
// We can write this at the top of the shader file, with the other function declarations
  float AverageValue(in float3 Value)
  {
    return ((Value.x + Value.y + Value.z) / 3);
  }

// Then inside the shader you'd do something like
float averageVal = AverageValue(In.Color.rgb); // Produces the Average of the PS input color.

Well, you're in luck! It's the exact same!
</end of section>

Well, not exactly the same. Instead of registering your shader inside the BRF manually you need to use the resource_shader.py module_shader.py inside the Barebones WSE2 Module System file to register and compile the shader brf in a format that the WSE2 engine can understand.

Thankfully, K700 has made this task very easy for us. Here's a the header I wrote for BBWSE2's module_shader.py file
Python:
# ###################################################################################################################
#    This file allows the modder to register their shader in the WSE2 Shader BRF format.
#    Each shader record contains the following fields:
#    1) Shader Name: String, Used for referencing the shader inside materials. "shader_name" in template.
#    2) Shader Technique Name: String,  The name of the shader inside the mb.fx file. "technique" in template
#    3) Alternative Name: String, If the referenced shader uses the flag "shf_uses_skinning" this will refer to technique of
#            the unskinned variant of the shader, if the shader does not, this will be the skinned variant.
#            "skinned_or_nonskinned_variant_of_technique" in the template
#    4) Flags: Int, shf_*** flags. Check header_shaders.py for available flags. flags in template
#    5) Resource Flags: Int, shrf_*** flags. Check header_shaders.py for available flags. resource_flags in template
#    6) Fallback Shader Technique Name: Name of a technique the shader can default back to, in case the current shader
#            cannot be used (typically used to support older, simpler shaders). "fallback_shader" in template
#
#    ("shader_name", "technique", "alternative_technique", flags, resource_flags, ["fallback_shader"]),
# ###################################################################################################################
If your shader is unskinned and doesn't require a skinned variant, you can leave the alternative_technique string blank. Additionally any shader with shf_uses_skinning will see the first technique as the skinned variant, and the alternative_technique as the unskinned variant.

For my_first_shader, the entry would look like:
("my_shader", "my_first_shader", "", shf_static_lighting|shf_uses_diffuse_map, 0, ["simple_shader"]),

A brief rundown of each available flag for WSE2 Shaders:
Python:
# Shader Resource Flags
shrf_lo_quality   = 0x1000                # Requires Shader Quality to be set to: Low or Above
shrf_mid_quality  = 0x2000                # Requires Shader Quality to be set to: Medium or Above
shrf_hi_quality   = 0x4000                # Requires Shader Quality to be set to: High

# Shader Flags
shf_specular_enable      = 0x20            # Shader has specular lighting enabled
shf_static_lighting      = 0x80            # Lighting is precalculated during scene loading via vertex colors
shf_has_reflections      = 0x100        # Shader receives the reflection map (rendered per frame for water reflections, this rendering pass is skipped if nothing in scene has this flag)
shf_preshaded            = 0x1000        # Target Mesh is preshaded via Vertex Color (Doesn't receive lighting info?)
shf_uses_instancing      = 0x2000        # Shader's Mesh uses an instance array to save on resources (reusing the same render pass multiple times)
shf_biased               = 0x8000        # Shader uses bias (mipmap bias? I dunno)
shf_always_fail          = 0x10000        # ?????
shf_special              = 0x20000        # ?????
shf_uses_dot3            = 0x40000        # Shader uses per-pixel lighting technique (? Speculation)
shf_uses_diffuse_map     = 0x100000        # Shader receives diffuse texture
shf_uses_diffuse_2_map   = 0x200000        # Shader receives diffuse_2 texture
shf_uses_normal_map      = 0x400000        # Shader receives normal texture
shf_uses_specular_map    = 0x800000        # Shader receives specular texture
shf_uses_environment_map = 0x1000000    # Shader receives envmap texture
shf_uses_hlsl            = 0x20000000    # Shader is written in hlsl
shf_uses_skinning        = 0x80000000    # Shader receives


After registering the shader entry, run the compiler, and it will add it to the target output brf!
 
Last edited:
HLSL effects for Pixel Shader (Mount&Blade Warband)


WHY? Just for the lolz and for starters (like myself).
If you know the basics, there is really no need to read this.


Let us start with basic HLSL syntax and an effect that does nothing:
C-like:
float4 main(float2 uv : TEXCOORD) : COLOR


{


float4 Color;


Color = tex2D( input , uv.xy);


return Color;


}

The color gets read and returned to the shader.


Now, to adapt some Pixel Shader effects in M&B, the easiest way is within the postfx.fx.
It's a good way to start/learn and you don't need to compile. Just press CTRL + F ingame.

Take a look at the FinalScenePassPS.
C-like:
float4 FinalScenePassPS(uniform const bool use_dof, uniform const int use_hdr, uniform const bool use_auto_exp, float2 texCoord: TEXCOORD0) : COLOR {


… //yada yada


    return color;

}
At the end of it, we see return Color;

Add above that: color = tex2D( postFX_sampler0 , texCoord.xy);

This does the same like the example above - nothing. The color gets read and returned to the shader. Keep in mind that you have already passed the last gamma correction. Depending on what you want do, you may want to set it before color.rgb = pow(color.rgb, output_gamma_inv);

4tsVX.jpg

(no need to click on the images - they are low quality)


We can get float2 as coordinate, means the coordinates can be split between x + y. Let us multiply the constant, in our case the color:

color = tex2D( postFX_sampler0 , texCoord.xy)*2;

Resulting in a brighter screen.
k2zGS.jpg


We can also work with variables, for example:

color = tex2D( postFX_sampler0 , texCoord.xy)*texCoord.y;
SYBDQ.jpg


Often it makes more sense to split the colors and make adjustments.
The color is a float4, meaning it is actually 4 floats stored together (RGBA), so let us change one of the values…

color = tex2D( postFX_sampler0 , texCoord.xy);
color.b = color.b*2;

wLkQE.jpg


Or do some math operations with sine and cosine.

color = tex2D( postFX_sampler0 , texCoord.xy );
color.r = color.r*sin(texCoord.x*100)*2;
color.g = color.g*cos(texCoord.x*150)*2;
color.b = color.b*sin(texCoord.x*50)*2;

uljNF.jpg



We can also work with the coordinates.

Making it smaller / divide:
texCoord.xy = texCoord.xy / 0.5;
color = tex2D( postFX_sampler0 , texCoord.xy);


Or stretch it / multiply:
texCoord.xy = texCoord.xy * 0.5;
color = tex2D( postFX_sampler0 , texCoord.xy);

vsA8I.jpg

ZRKfI.jpg


Other sine effect:

texCoord.y = texCoord.y + (sin(texCoord.y*100)*0.03);
color = tex2D( postFX_sampler0 , texCoord.xy);

rls81.jpg


Drunk effect:

color = tex2D( postFX_sampler0 , texCoord.xy-0.003)/1.7f;
color += tex2D( postFX_sampler0 , texCoord.xy+0.003)/1.7f;


There are different assignment operators:

int i1 = 1
i1 = 2; // i1 = 2
i1 += 2; // i1 = 1 + 2 = 3
i1 -= 2; // i1 = 1 - 2 = 3


Z_kfp.jpg


Color shift:

color = tex2D( postFX_sampler0 , texCoord.xy);
color.r -= tex2D( postFX_sampler0 , texCoord.xy+(1/100)).r;
color.g += tex2D( postFX_sampler0 , texCoord.xy+(1/200)).g;
color.b -= tex2D( postFX_sampler0 , texCoord.xy+(1/300)).b;


6pv-V.jpg



if else cases:

color.rgb = (color.r+color.g+color.b)/3.0f;
if (color.r<0.2 || color.r>0.9) color.r = 0; else color.r = 1.0f;
if (color.g<0.2 || color.g>0.9) color.g = 0; else color.g = 1.0f;
if (color.b<0.2 || color.b>0.9) color.b = 0; else color.b = 1.0f;


As you can see, if the color value is smaller than 0.2 or greater than 0.9 it will be set to 0 otherwise to 1. If you really want some eye ****** kill the second if line.

dstn: If you wanna do a one line if else, you can do it like this.
color.r > 0.5 ? 1.0f : 0.0f; //Read as if red channel is greater than half, set to one, else set to 0.

cIlXn.jpg

k34dO.jpg

_W-ln.jpg


Greyscaling is a popular way to change the ambient color.

float greyscale = dot(color.rgb, float3(0.30, 0.59, 0.11));
color.rgb = (lerp(greyscale, color.rgb, 0.70));


greyscale floats all colors 3times with the dot method -> (0.9,1.77,0.33). The RGB color gets a lerp with the above defined greyscale, itself and 0.7.
ME6R7.jpg



As you can see the most effects may have no real use in the FinalScenePass, but it could help you, if you make your first shader or want to change a native one.
--------------------------------------------------------------
For example:
Open the mbsrc.fx and change the color output of the Flora shader. Here with color.g x 1.5 and post FX tone mapping.
Make a copy of the flora shader, define seasons, init them in your MS and you have a simple flora season shader.

o5Eed.jpg




Or change the water color to a toxic lake. Here with:
const float3 g_cDownWaterColor = float3(1.0f/255.0f, 40.0f/255.0f, 6.0f/255.0f);
const float3 g_cUpWaterColor = float3(1.0f/255.0f, 50.0f/255.0f, 10.0f/255.0f);


ItlaQ.jpg


Or a blue without fog_fresnel
S3g0t.jpg

With fog_fresnel + saturated skybox shader
NI8_E.jpg




Only the sky is the limit (well, M&B is an old game and a lot of things are hardcoded).

Thanks to our SupaNinjaMan dstn aka. “Sir” Sionfel! You’re great.

If anyone is interested at all, I may continue.
 
Last edited:
I know nothing about shader language but found a way to disable/decrease bloom effect of max HDR settings.
1) Open file postFX.fx in the "Mount&Blade Warband" folder (where mb_warband.exe located)
2) Replace the line
float intensity = dot(color.rgb, float3(.5f, .5f, .5f));
with
float intensity = dot(color.rgb, float3(.1f, .1f, .1f));
You can change intensity of bloom effect by changing those values.
 
Back
Top Bottom