Shaders 101
Helping your mod stand apart
For Warband, VC and NW.
WIP
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.
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.
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
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.
Congratulations, you Authored your first Shader!
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.
Speculative Data, doesn't line up with my actual data, but they are the maps
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.
Images | Descriptions |
---|---|
| Shader Tab:
|
Prerequisite Flags:
| |
Shader Flags
|
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:
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.
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.
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
Thankfully, K700 has made this task very easy for us. Here's a the header I wrote for BBWSE2's module_shader.py file
If your shader is unskinned and doesn't require a skinned variant, you can leave the
For my_first_shader, the entry would look like:
A brief rundown of each available flag for WSE2 Shaders:
After registering the shader entry, run the compiler, and it will add it to the target output brf!
</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"]),
# ###################################################################################################################
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: