Table of Contents

All shaders in bs::f are written in BSL (bs::f 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.

Basics

A simple BSL program that renders a mesh all in white looks like this:

shader 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 shader and code blocks, the shader code looks identical to HLSL. Inside the shader block you can use a variety of options, but the minimum required is the code block, which allows you to specify programmable shader code. Inside the code block you get to use 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:

All primitive (non-object) shader constants (uniforms in GLSL lingo) must be part of a cbuffer. Primitive types are any types that are not textures, buffers or samplers.

Entry points for specific shader stages must have specific names. In the example above vsmain serves as the entry point for the vertex shader, while fsmain serves as the entry point for the fragment (pixel) shader. The full list of entry point names per type:

vsmain - Vertex shader

gsmain - Geometry shader

hsmain - Hull (Tesselation control) shader

dsmain - Domain (Tesselation evaluation) shader

fsmain - Fragment (Pixel) shader

csmain - Compute shader

Let's now move onto more advanced functionality specific to BSL.

Non-programmable states

Aside from the code block, a shader can also specify four blocks that allow it to control non-programmable parts of the pipeline:

Mixins

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 shader, or in another mixin. Syntax within a mixin block is identical to syntax in a shader 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 shader wishes to use a mixin, simply add it to the shader using the same mixin keyword, followed by its name.

shader MyShader

{

mixin MyMixin;

code

{

// ...

float4 fsmain(in VStoFS input) : SV_Target0

{

uint2 pixelPos = ...;

// We 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 shader they are included in. If mixin and shader define the same states, the value of the states present on the shader will be used. If multiple included mixins use the same state then the state from the last included mixin will be used. Code is included in the order in which mixins are defined, followed by code from the shader 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 the GBuffer

#include "$ENGINE$/GBufferInput.bslinc"

shader MyShader

{

mixin GBufferInput;

// ...

};

Mixin overrides

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 shader. Shaders can also reference mixins that haven't been declared yet. This concept of mixin overrides allows you to change functionality of complex shaders without having to be aware of any code other than the mixin you are overriding. This concept is heavily used when writing surface and lighting shaders, as described in a later manual.

// 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

Passes can be used when a shader needs to perform multiple complex operations in a sequence. Each pass can be thought of as its own fully functional shader. By default shaders 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 shaders above. Passes will be executed sequentially one after another in the order they are defined.

shader 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

};

};

};

Default values

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 scalars: "type name = value;"

For vectors/matrices: "type name = { v0, v1, ... };", where the number of values is the total number of elements in a vector/matrix

shader 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:

Note: Sampler state default values use the same syntax as HLSL Effects framework.

Attributes

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.

shader 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.

alias

Texture name

SamplerState

Allows you to provide an alternative name to a SamplerState. This is important when using render backends like OpenGL, which may not support separate sampler states. In these cases the sampler state will be 'merged' with the texture it is used on. This means all internal systems will report the name of this sampler to be the same as the name of the texture, rather than the given name. In this case you will want to explictly add an alias(TextureName) attribute so the system knows this sampler state might have an alternative name.

// Texture contains 4-channel 16-bit floating point data, and we plan on reading from it

[layout(rgba16f))]

RWTexture2D someMap2;

};

};

Global options

BSL supports a few global options that control all shaders 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 shader or mixin blocks.

options

{

separable = true;

sort = backtofront;

transparent = true;

priority = 100;

};

shader MyShader

{

// Shader 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 shader 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.

forward

true, false

false

Notifies the renderer that this object should be rendered using the forward rendering pipeline (as opposed to a deferred one).

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).

Variations

Sometimes you need a few versions of the same shader, that are mostly similar but have some minor differences between them. For example, when rendering objects you might need to support a vertex shader for static meshes, as well as those using skinned and/or morph animation.

This is where the variation block comes into play. It allows you to specify a set of permutations for which the shader will be compiled. During shader import every permutation of that shader will be parsed, enabling pre-processor #define blocks depending on the current permutation. The #defines take on the name of their variation, and one of the user provided values.

// An example shader supporting different mesh animation modes

shader VertexInput

{

// This will be be compiled using 4 different variations:

// - Static (no animation) (SKINNED = false, MORPH = false)

// - Skinned animation (SKINNED = true, MORPH = false)

// - Morph animation (SKINNED = false, MORPH = true)

// - Skinned morph animation (SKINNED = true, MORPH = true)

variations

{

SKINNED = { false, true };

MORPH = { false, true };

};

code

{

struct VertexInput

{

float3 position : POSITION;

float3 normal : NORMAL; // Note: Half-precision could be used

float4 tangent : TANGENT; // Note: Half-precision could be used

float2 uv0 : TEXCOORD0;

#if SKINNED

uint4 blendIndices : BLENDINDICES;

float4 blendWeights : BLENDWEIGHT;

#endif

#if MORPH

float3 deltaPosition : POSITION1;

float4 deltaNormal : NORMAL1;

#endif

};

// Potentially more code...

};

};

The syntax within the variation block is as follows:

IDENTIFIER = { bool/int, bool/int, ... }

Each variation block can have one or multiple entries. Each must have a unique identifier. Each entry can take on two or more values, each representing a single variation. The values must be boolean (true/false), or integers. If there are multiple variation entries, a variation for each possible combination of their values will be created.

Once a shader with variations is imported, those variations will be available on the shader in the form of Technique objects. In general variations are only required when working with the low level rendering API, therefore we discuss techniques in more details in the developer manuals.

Sub-shaders

Each BSL file can contain an optional set of sub-shaders, alongside the main shader. Sub-shaders are recognized by the renderer and are meant to allow the user to override functionality of default shaders used by the renderer. They are specified using the subshader keyword, followed by an unique identifier. Sub-shaders are only allowed to contain mixin blocks, within which the same rules as for normal mixins apply.

shader MainShader

{

// Shader definition

};

subshader SomeSubShader

{

mixin SomeMixin

{

// Mixin definition

};

};

The mixins you specify in a sub-shader will be used to override some existing mixin, allowing you to change specific rendering functionality.

Imagine for example that you wanted to implement a custom lighting model while using deferred rendering. Deferred rendering doesn't perform lighting when rendering the object, instead it does lighting in a single pass for all objects at once. The renderer will closely interact with that lighting shader, and if you wanted to change it you would need to rewrite it using the exact rules as the renderer expects. Worse yet, the renderer might support different forms of deferred rendering (e.g. standard vs. tiled), meaning you would need to write multiple shaders to fully support it. You would also need to keep your shader(s) updated whenever the renderer's internal change. For a normal user this is very difficult and requires detailed knowledge of how the renderer works.

bsf's renderer instead provides a set of "extension points". Extension points match to a specific part of the renderer pipeline that can be overriden. For example an extension point named "DeferredDirectLighting" would let you override the BRDF and direct lighting calculations during deferred rendering. Sub-shaders are how you "link" with those extension points. The name of the sub-shader corresponds to the extension point name, and the mixins in the sub-shader override specific functionality of that extension point. For example you might override the "StandardBRDF" mixin to change the BRDF model for the standard material, or "LuminanceSpot" to change how is luminance for spot lights calculated.

This way you only override small parts of the renderer shader code, without having to rewrite all the complex shader code, while all the implementation details are transparently handled by the renderer itself.

// Sub-shader overriding direct lighting used for deferred rendering

subshader DeferredDirectLighting // Name of the subshader specifies exactly what part of the rendering pipeline to override

{

// Exact name of the mixin to override. Each subshader extension point can provide one or multiple mixins that can be overriden