[TOC]
All shaders in Banshee are written in BSL (Banshee Shading Language). The core of the language is based on HLSL (High Level Shading Language), with various extensions to make development easier. In this manual we will not cover HLSL syntax, nor talk about shaders in general, and will instead focus on the functionality specific to BSL. If you are not familiar with the concept of a shader, or HLSL syntax, it is suggested you learn about them before continuing.
A simple BSL program that renders a mesh all in white looks like this:
technique MyShader
{
code
{
struct VStoFS
{
float4 position : SV_Position;
};
cbuffer PerObject
{
float4x4 gMatWorldViewProj;
}
struct VertexInput
{
float3 position : POSITION;
};
VStoFS vsmain(VertexInput input)
{
VStoFS output;
output.position = mul(gMatWorldViewProj, input.position);
return output;
}
float4 fsmain(in VStoFS input) : SV_Target0
{
return float4(1.0f, 1.0f, 1.0f 1.0f);
}
};
};
As you can see, aside from the technique and code blocks, the shader code looks identical to HLSL. Each BSL shader must contain at least one technique. Inside the technique you can use a variety of options, but the minimum required is the code block, which allows you to specify programmable shader code. Using just this syntax you get the full power of HLSL, as if you were using it directly.
There are a few restrictions compared to normal HLSL that you must be aware of:
Let's now move onto more advanced functionality specific to BSL.
Aside from the code block, a technique can also specify four blocks that allow it to control non-programmable parts of the pipeline:
An example shader using a variety of these blocks is shown:
technique MyShader
{
// No depth reads of writes
depth
{
read = false;
write = false;
};
// Enable blending on the first render target
blend
{
target
{
enabled = true;
color = { srcA, srcIA, add };
writemask = RGB;
};
};
// Disable culling
raster
{
cull = none;
};
// Only render if stencil test passes
stencil
{
enabled = true;
front = { keep, keep, keep, lte };
reference = 1;
};
code
{
// ... some shader code
};
};
Let's cover all available options for each block type.
| Name | Valid values | Reference |
|---|---|---|
| fill | wire, solid (See @ref bs::PolygonMode "PolygonMode") | @ref bs::RASTERIZER_STATE_DESC::polygonMode "RASTERIZER_STATE_DESC::polygonMode" |
| cull | cw, ccw, none (See @ref bs::CullingMode "CullingMode") | @ref bs::RASTERIZER_STATE_DESC::cullMode "RASTERIZER_STATE_DESC::cullMode" |
| scissor | true, false | @ref bs::RASTERIZER_STATE_DESC::scissorEnable "RASTERIZER_STATE_DESC::scissorEnable" |
| multisample | true, false | @ref bs::RASTERIZER_STATE_DESC::multisampleEnable "RASTERIZER_STATE_DESC::multisampleEnable" |
| lineaa | true, false | @ref bs::RASTERIZER_STATE_DESC::antialiasedLineEnable "RASTERIZER_STATE_DESC::antialiasedLineEnable" |
| Name | Valid values | Reference |
|---|---|---|
| read | true, false | @ref bs::DEPTH_STENCIL_STATE_DESC::depthReadEnable "DEPTH_STENCIL_STATE_DESC::depthReadEnable" |
| write | true, false | @ref bs::DEPTH_STENCIL_STATE_DESC::depthWriteEnable "DEPTH_STENCIL_STATE_DESC::depthWriteEnable" |
| compare | never, always, lt, lte, eq, neq, gte, gt (See @ref bs::CompareFunction "CompareFunction") | @ref bs::DEPTH_STENCIL_STATE_DESC::depthComparisonFunc "DEPTH_STENCIL_STATE_DESC::depthComparisonFunc" |
| bias | float | @ref bs::RASTERIZER_STATE_DESC::depthBias "RASTERIZER_STATE_DESC::depthBias" |
| scaledBias | float | @ref bs::RASTERIZER_STATE_DESC::slopeScaledDepthBias "RASTERIZER_STATE_DESC::slopeScaledDepthBias" |
| clip | true, false | @ref bs::RASTERIZER_STATE_DESC::depthClipEnable "RASTERIZER_STATE_DESC::depthClipEnable" |
| Name | Valid values | Reference |
|---|---|---|
| reference | integer | Reference value to use for stencil compare operations. |
| enabled | true, false | @ref bs::DEPTH_STENCIL_STATE_DESC::stencilEnable "DEPTH_STENCIL_STATE_DESC::stencilEnable" |
| readmask | integer in [0, 255] range | @ref bs::DEPTH_STENCIL_STATE_DESC::stencilReadMask "DEPTH_STENCIL_STATE_DESC::stencilReadMask" |
| writemask | integer in [0, 255] range | @ref bs::DEPTH_STENCIL_STATE_DESC::stencilWriteMask "DEPTH_STENCIL_STATE_DESC::stencilWriteMask" |
| front | StencilOp block | Stencil operations and compare function for front facing geometry |
| back | StencilOp block | Stencil operations and compare function for back facing geometry |
front and back options are blocks themselves, and they accept the following options: Name | Valid values | Reference ---------------------|---------------------------- |----------------------- fail | keep, zero, replace, inc, dec, incwrap, decwrap, inverse (See @ref bs::StencilOperation "StencilOperation") | @ref bs::DEPTH_STENCIL_STATE_DESC::frontStencilFailOp "DEPTH_STENCIL_STATE_DESC::frontStencilFailOp" & @ref bs::DEPTH_STENCIL_STATE_DESC::backStencilFailOp "DEPTH_STENCIL_STATE_DESC::backStencilFailOp" zfail | keep, zero, replace, inc, dec, incwrap, decwrap, inverse (See @ref bs::StencilOperation "StencilOperation") | @ref bs::DEPTH_STENCIL_STATE_DESC::frontStencilZFailOp "DEPTH_STENCIL_STATE_DESC::frontStencilZFailOp" & @ref bs::DEPTH_STENCIL_STATE_DESC::backStencilZFailOp "DEPTH_STENCIL_STATE_DESC::backStencilZFailOp" pass | keep, zero, replace, inc, dec, incwrap, decwrap, inverse (See @ref bs::StencilOperation "StencilOperation") | @ref bs::DEPTH_STENCIL_STATE_DESC::frontStencilPassOp "DEPTH_STENCIL_STATE_DESC::frontStencilPassOp" & @ref bs::DEPTH_STENCIL_STATE_DESC::backStencilPassOp "DEPTH_STENCIL_STATE_DESC::backStencilPassOp" compare | never, always, lt, lte, eq, neq, gte, gt (See @ref bs::CompareFunction "CompareFunction") | @ref bs::DEPTH_STENCIL_STATE_DESC::frontStencilComparisonFunc "DEPTH_STENCIL_STATE_DESC::frontStencilComparisonFunc" & @ref bs::DEPTH_STENCIL_STATE_DESC::backStencilComparisonFunc "DEPTH_STENCIL_STATE_DESC::backStencilComparisonFunc"
An example of a stencil block:
technique MyShader
{
// Only render if stencil test passes
stencil
{
enabled = true;
front = {
fail = keep; // On stencil test fail don't do anything
zfail = keep; // On depth test fail don't do anything
pass = inc; // On stencil test pass, increment stencil buffer by one
compare = always; // Always pass the stencil test
};
};
};
StencilOp block can also be declared succintly on a single line. Same code as above, written differently:
technique MyShader
{
// Only render if stencil test passes
stencil
{
enabled = true;
front = { keep, keep, inc, always }; // Note the order is always: fail op., zfail op., pass op., compare function
};
};
| Name | Valid values | Reference |
|---|---|---|
| dither | true, false | @ref bs::BLEND_STATE_DESC::alphaToCoverageEnable "BLEND_STATE_DESC::alphaToCoverageEnable" |
| independant | true, false | @ref bs::BLEND_STATE_DESC::independantBlendEnable "BLEND_STATE_DESC::independantBlendEnable" |
| target | Target block | Blend operations for a specific render target. Multiple Target blocks can exist under a single blend block. |
Target block accepts the following options: Name | Valid values | Reference ---------------------|---------------------------- |----------------------- index | positive integer | Index of the render target these options are applied to. If not specified the index is derived from the order in which Target blocks are defined. enabled | true, false | @ref bs::RENDER_TARGET_BLEND_STATE_DESC::blendEnable "RENDER_TARGET_BLEND_STATE_DESC::blendEnable" writemask | R, G, B, A or any combination (e.g. RG, RBA, RGBA). "empty" for zero mask. | @ref bs::RENDER_TARGET_BLEND_STATE_DESC::renderTargetWriteMask "RENDER_TARGET_BLEND_STATE_DESC::renderTargetWriteMask" color | BlendOp block | Represents the blend operation to execute on the color channels. alpha | BlendOp block | Represents the blend operation to execute on the alpha channel. Only relevant if independant blend is enabled.
BlendOp block accepts the following options: Name | Valid values | Reference ---------------------|---------------------------- |----------------------- source | one, zero, dstRGB, srcRGB, dstIRGB, srcIRGB, dstA, srcA, dstIA, srcIA (See @ref bs::BlendFactor "BlendFactor") | @ref bs::RENDER_TARGET_BLEND_STATE_DESC::srcBlend "RENDER_TARGET_BLEND_STATE_DESC::srcBlend", @ref bs::RENDER_TARGET_BLEND_STATE_DESC::srcBlendAlpha "RENDER_TARGET_BLEND_STATE_DESC::srcBlendAlpha" dest | one, zero, dstRGB, srcRGB, dstIRGB, srcIRGB, dstA, srcA, dstIA, srcIA (See @ref bs::BlendFactor "BlendFactor") | @ref bs::RENDER_TARGET_BLEND_STATE_DESC::dstBlend "RENDER_TARGET_BLEND_STATE_DESC::dstBlend", @ref bs::RENDER_TARGET_BLEND_STATE_DESC::dstBlendAlpha "RENDER_TARGET_BLEND_STATE_DESC::dstBlendAlpha" op | add, sub, rsub, min, max (See @ref bs::BlendOperation "BlendOperation") | @ref bs::RENDER_TARGET_BLEND_STATE_DESC::blendOp "RENDER_TARGET_BLEND_STATE_DESC::blendOp", @ref bs::RENDER_TARGET_BLEND_STATE_DESC::blendOpAlpha "RENDER_TARGET_BLEND_STATE_DESC::blendOpAlpha"
An example of a blend block:
technique MyShader
{
blend
{
target
{
enabled = true;
color = {
source = srcA;
dest = srcIA;
op = add;
};
writemask = RGB;
};
};
};
BlendOp block can also be defined succintly on a single line. Same code as above, written differently:
technique MyShader
{
blend
{
target
{
enabled = true;
color = { srcA, srcIA, add }; // Note the order is always: source, destination, operation
writemask = RGB;
};
};
};
When writing complex shaders is it is often useful to break them up into components. This is where the concept of a mixin comes in. Any shader code or programmable states defined in a mixin can be included in any technique. Syntax within a mixin block is identical to syntax in a technique block, meaning you can define code and non-programmable state blocks as shown above.
// Provides common functionality that might be useful for many different shaders
mixin MyMixin
{
code
{
Texture2D gGBufferAlbedoTex;
Texture2D gDepthBufferTex;
struct SurfaceData
{
float4 albedo;
float depth;
};
SurfaceData getGBufferData(uint2 pixelPos)
{
SurfaceData output;
output.albedo = gGBufferAlbedoTex.Load(int3(pixelPos, 0));
output.depth = gDepthBufferTex.Load(int3(pixelPos, 0)).r;
return output;
}
};
};
When a technique wishes to use a mixin, simply add it to the technique using the same mixin keyword, followed by its name.
technique MyShader
{
mixin MyMixin;
code
{
// ...
float4 fsmain(in VStoFS input) : SV_Target0
{
uint2 pixelPos = ...;
// Technique can now call methods from the mixin
SurfaceData surfaceData = getGBufferData(pixelPos);
return surfaceData.albedo;
}
};
};
Included mixins will append their shader code and states to the technique they are included in. If mixin and technique define the same states, the value of the states present on the technique will be used. If multiple included mixins use the same state then the state from the last included mixin will be used. Shader code is included in the order in which mixins are defined, followed by shader code from the technique itself.
Often you will want to define mixins in separate files. BSL files are normally stored with the ".bsl" extension, but when writing include files you should use the ".bslinc" extension instead, in order to prevent the system trying to compile the shader code on its own.
In order to include other files in BSL, use the #include command. The paths are relative to the working folder, or if you are working in Banshee 3D editor, the paths are relative to your project folder. You can use the special variables $ENGINE$ and $EDITOR$ to access paths to the builtin engine and editor shaders.
// Include the code for accessing Banshee's GBuffer
#include "$ENGINE$/GBufferInput.bslinc"
technique MyShader
{
mixin GBufferInput;
// ...
};
Mixins can override each other if another mixin is defined with the same name. The last defined mixin is considered the override and will be used in the technique. Techniques can also reference mixins that haven't been declared yet.
// This mixin overrides the MyMixin behaviour we defined above
mixin MyMixin
{
code
{
struct SurfaceData
{
float4 albedo;
float depth;
};
SurfaceData getGBufferData(uint2 pixelPos)
{
SurfaceData output;
output.albedo = float4(1, 0, 0, 1); // Always return red color
output.depth = 0.0f;
return output;
}
};
};
Passes can be used when a technique needs to perform multiple complex operations in a sequence. Each pass can be thought of as its own fully functional shader. By default techniques have one pass, which doesn't have to be explicitly defined, as was the case in all examples above. To explicitly define a pass, use the pass block and define the relevant code/state blocks within it, same as it was shown for techniques above. Passes will be executed sequentially one after another in the order they are defined.
technique MyShader
{
// First pass
pass
{
code
{
// Some shader code
};
};
// Second pass
pass
{
// Enable blending so data written by this pass gets blended with data from the previous pass
blend
{
target
{
enabled = true;
color = {
source = srcA;
dest = srcIA;
op = add;
};
writemask = RGB;
};
};
code
{
// Some shader code
};
};
};
All constants (uniforms) of primitive types can be assigned default values. These values will be used if the user doesn't assign the values explicitly. The relevant syntax is:
For vectors/matrices: "type name = { v0, v1, ... };", where the number of values is the total number of elements in a vector/matrix
technique MyShader
{
code
{
cbuffer SomeBuffer
{
bool val1 = false;
float val2 = 5.0f;
float3 val3 = { 0.0f, 1.0f, 2.0f };
float2x2 val4 = { 0.0f, 1.0f, 2.0f, 3.0f };
};
// ... remaining shader code
};
};
Textures can also be assigned default values, limited to a specific subset. Valid set of values is:
normal - Texture representing a normal map with normals facing in the Y (up) direction
technique MyShader
{
code
{
Texture2D albedoMap = black;
Texture2D normalMap = normal;
// ... remaining shader code
};
};
Finally, sampler states may also be assigned default values. The values are specified in a block immediately following the sampler state definition.
technique MyShader
{
code
{
SamplerState MySampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};
// ... remaining shader code
};
};
Valid values within the sampler state block are: Name | Valid values | Reference ---------------------|--------------------------------------------|------------------------ AddressU | WRAP, MIRROR, CLAMP, BORDER, MIRROR_ONCE | @ref bs::SAMPLER_STATE_DESC::addressMode "SAMPLER_STATE_DESC::addressMode" AddressV | WRAP, MIRROR, CLAMP, BORDER, MIRROR_ONCE | @ref bs::SAMPLER_STATE_DESC::addressMode "SAMPLER_STATE_DESC::addressMode" AddressW | WRAP, MIRROR, CLAMP, BORDER, MIRROR_ONCE | @ref bs::SAMPLER_STATE_DESC::addressMode "SAMPLER_STATE_DESC::addressMode" BorderColor | float4 | @ref bs::SAMPLER_STATE_DESC::borderColor "SAMPLER_STATE_DESC::borderColor" Filter | See table below | @ref bs::SAMPLER_STATE_DESC::minFilter "SAMPLER_STATE_DESC::minFilter", @ref bs::SAMPLER_STATE_DESC::magFilter "SAMPLER_STATE_DESC::magFilter", @ref bs::SAMPLER_STATE_DESC::mipFilter "SAMPLER_STATE_DESC::mipFilter" MaxAnisotropy | uint | @ref bs::SAMPLER_STATE_DESC::maxAniso "SAMPLER_STATE_DESC::maxAniso" MaxLOD | float | @ref bs::SAMPLER_STATE_DESC::mipMax "SAMPLER_STATE_DESC::mipMax" MinLOD | float | @ref bs::SAMPLER_STATE_DESC::mipMin "SAMPLER_STATE_DESC::mipMin" MipLODBias | float | @ref bs::SAMPLER_STATE_DESC::mipmapBias "SAMPLER_STATE_DESC::mipmapBias" ComparisonFunc | NEVER, LESS, EQUAL, LESS_EQUAL, GREATER, NOT_EQUAL, GREATER_EQUAL, ALWAYS | @ref bs::SAMPLER_STATE_DESC::comparisonFunc "SAMPLER_STATE_DESC::comparisonFunc"
Filter valid values:
Note: Sampler state default values use the same syntax as HLSL Effects framework.
BSL provides a couple of extension attributes that can be applied to constants (uniforms) or constant (uniform) blocks. Attributes are specified using the standard HSLS [] attribute syntax.
technique MyShader
{
code
{
[someAttribute]
Texture2D someMap;
[someAttributeWithParameters(p0, p1)]
Texture2D someMap2;
};
};
Supported attribute types are: Name | Parameters | Usable on | Description ---------------------|------------------------ |------------------------|------------------ internal | none | constants and cbuffers | Forces the constant (or all the constants in a buffer if applied to cbuffer) to be hidden from the materials public interface (editor UI or Material API). This is useful for constants that are set by the engine itself and shouldn't be touched by normal users. Additionaly internal cbuffers must be explicitly created and assigned by the low level rendering API, as they will not be created automatically. color | none | float3 or float4 constants | Marks the floating point vector as a color. This ensures the constant is displayed as a color in the editor UI (with access to a color picker), and is represented by the Color structure in Material API. layout | See table below | RW texture or buffer constant | Used primarily as compatibility with OpenGL and Vulkan code, which require read-write objects (e.g. RWTexture) to have an explicit layout provided in shader. This is only required when READING from a read-write object AND when you will be using either OpenGL or Vulkan render backend.
layout valid values:
Where:
Example of attributes in action:
technique MyShader
{
code
{
// Hide from public interface
[internal]
Texture2D someMap;
cbuffer SomeBuffer
{
float3 position;
// Interpret as color, instead of a 3D vector
[color]
float3 tint;
};
// Texture contains 4-channel 16-bit floating point data, and we plan on reading from it
[layout(rgba16f))]
RWTexture2D someMap2;
};
};
BSL supports a few global options that control all techniques and mixins in a shader file. These options are specified in a options block, which must be defined at the top most level along with technique or mixin blocks.
options
{
separable = true;
sort = backtofront;
transparent = true;
priority = 100;
};
technique MyShader
{
// Technique definition
};
Valid options are: Name | Valid values | Default value | Description ---------------------|---------------------------- |------------------------|------------------ separable | true, false | false | When true, tells the renderer that passes within the shader don't need to be renderered one straight after another. This allows the system to perform rendering more optimally, but can be unfeasible for most materials which will depend on exact rendering order. Only relevant if a technique has multiple passes. sort | none, backtofront, fronttoback | fronttoback | Determines how does the renderer sort objects with this material before rendering. Most objects should be sorted front to back in order to avoid overdraw. Transparent (see below) objects will always be sorted back to front and this option is ignored. When no sorting is active the system will try to group objects based on the material alone, reducing material switching and potentially reducing CPU overhead, at the cost of overdraw. transparent | true, false | false | Notifies the renderer that this object is see-through. This will force the renderer to the use back to front sorting mode, and likely employ a different rendering method. Attempting to render transparent geometry without this option set to true will likely result in graphical artifacts. priority | integer | 0 | Allows you to force objects with this shader to render before others. Objects with higher priority will be rendered before those with lower priority. If sorting is enabled, objects will be sorted within their priority groups (i.e. priority takes precedence over sort mode).