Shaders
Shaders are programs used to implement graphical effects that run on the GPU. SS14 uses pixel (fragment) shaders to implement per-sprite effects including stealth; and overlay effects, including space drugs, drunkenness, blindness, and singularity space bending.
Defining a Shader
Each shader has a YAML prototype stored in Resources/Prototypes/Shaders
. All shaders have fields:
type
: must beshader
id
: unique prototype ID (a string) that identifies this shaderkind
: eithercanvas
orshader
(see below)
canvas
Shaders
This is a shader with preset options, akin to Godot’s CanvasItemMaterial
. This shader has two required properties:
blend_mode
: The way the object is drawn over the scene. These are the same as the equivalent for Godot. Barring the naming being slightly different. Possible values aremix
,add
,subtract
,multiply
andpremultiplied_alpha
light_mode
: The way the object interacts with light. Again equivalent to Godot’s, with the values beingnormal
,unshaded
andlight_only
.
For example, the unshaded
shader, which is used to draw the lit portion of computer displays and indicators by masking lighting operations, is defined by:
- type: shader
id: unshaded
kind: canvas
light_mode: unshaded
A canvas shaders is really just the default source shader (see /Shaders/Internal/default-sprite.swsl
), with the lighting, blending, and stencil options applied.
source
Shaders
These are custom shaders written in the Space Wizard Shader Language (SWSL). source
shaders have one required property:
path
: The path, with respect to theResources/
directory, of the SWSL shader source file.
For example:
- type: shader
id: GreyscaleFullscreen
kind: source
path: "/Textures/Shaders/greyscale_fullscreen.swsl"
Custom shaders are typically stored in .swsl
files in Resources/Textures/Shaders
.
SWSL
Space Wizard Shader Language (SWSL) is based on the Godot Shading language. Most of the compatibility differences between Godot and SWSL are handled automatically, except for the following differences:
-
You MUST ensure that all numeric types (e.g.
float
,vec3
) have ahighp
orlowp
precision qualifier. See this article on why. -
Avoid using any variable names that are reserved words in common shader specifications. Your computer may ignore them but they will break on other machines. Check the keywords section (3.8) here
Since this is a 2D game, only the fragment shader is used, i.e. the shader consists, at minimum, of:
void fragment() {
COLOR = vec4(r, g, b, a);
}
Available Variables (Fragment Shaders)
Name | Type | Description |
---|---|---|
FRAGCOORD | highp vec4 | The coordinates within the fragment. |
COLOR | lowp vec4 | The resulting pixel color. (Think of it as the return value of the fragment shader.) |
lightMap | sampler2D | The lighting map of the current fragment (applied automatically by base-default.frag ) |
modulate | highp vec4 | The draw color (applied automatically by base-default.frag ) |
SCREEN_PIXEL_SIZE | highp vec2 | The size of one pixel, in local units. |
TIME | highp float | The number of seconds since game startup. |
Stencil Test Parameters
Shader support stencil operations. This is an advanced rendering feature that can be useful for some things if you know what you’re doing. The feature closely mimics the stencil parameters as exposed by OpenGL (and as far as I can tell, Vulkan too). See The OpenGL wiki if you need a reference.
Stencil parameters are defined in a separate stencil
object. As an example:
- type: shader
id: stencilDraw
kind: canvas
stencil:
ref: 1
op: Keep
func: NotEqual
Indeed, stencil parameters are independent from the shader kind
.
The options are:
ref
: The reference to compare / write as passed to the second parameter ofglStencilFunc
.- Default is 0.
op
: The operation to apply to the stencil buffer if the stencil test passes. You can only set the op on pass (glStencilOp
’s third parameter).- Options are
Keep
,Zero
,Replace
,IncrementClamp
,IncrementWrap
,DecrementClamp
,DecrementWrap
,Invert
- Default is
Keep
.
- Options are
func
: The comparison function to use to see if the test passes. (glStencilFunc
’s first parameter).- Options are
Always
,Never
,Less
,LessOrEqual
,Greater
,GreaterOrEqual
,NotEqual
,Equal
. - Default is
Always
.
- Options are
readMask
: Mask to use when reading from the stencil buffer (glStencilFunc
’s third parameter).- Default is all 1’s
writeMask
: Mask to use when writing to the stencil buffer (glStencilMask
’s parameter).- Default is all 1’s
Overlays
An overlay applies a shader to an entire screen or viewport (as opposed to individual sprites). Drug effects, blindness, and singularity space bending are examples of overlays (written in C#) that load shader prototypes (defined in YAML) with may load custom shader effects (written in SWSL).
Overlays extend Overlay
, for example:
public sealed class BlindOverlay : Overlay
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
// Set this to true to get a ScreenTexture. Otherwise, it is null.
public override bool RequestScreenTexture => true;
// This needs to be set to the appropriate overlay layer.
public override OverlaySpace Space => OverlaySpace.WorldSpace;
// Store references to the shaders.
private readonly ShaderInstance _greyscaleShader;
private readonly ShaderInstance _circleMaskShader;
public BlindOverlay()
{
IoCManager.InjectDependencies(this);
// Load shaders from prototypes
_greyscaleShader = _prototypeManager.Index<ShaderPrototype>("GreyscaleFullscreen").InstanceUnique();
_circleMaskShader = _prototypeManager.Index<ShaderPrototype>("CircleMask").InstanceUnique();
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
// If this returns true, this shader will be drawn. If this
// method is not overriden, it defaults to returning true,
// i.e. the shader is always active for everyone all the time.
}
protected override void Draw(in OverlayDrawArgs args)
{
// If your shader needs inputs (the pixels currently on the screen),
// you must check that it is not null and pass it into your shader.
if (ScreenTexture == null)
return;
_greyscaleShader?.SetParameter("SCREEN_TEXTURE", ScreenTexture);
var handle = args.WorldHandle;
var viewport = args.WorldBounds;
// draw the greyscale shader
handle.UseShader(_greyscaleShader);
handle.DrawRect(viewport, Color.White);
// draw the circle mask shader
handle.UseShader(_circleMaskShader);
handle.DrawRect(viewport, Color.White);
// stop using this shader
handle.UseShader(null);
}
}
The shader needs an area to draw in. Here, it’s a white rectangle equal to the WorldBounds. Note that WorldAABB is not adjusted for rotation and is likely to break many shaders.
Finally, in order for overlays to actually be drawn, they need to be added to the overlay manager:
[Dependency] private readonly IOverlayManager _overlayMan = default!;
_overlayMan.AddOverlay(your_overlay_here);
_overlayMan.RemoveOverlay(your_overlay_here);
If this overlay is supposed to be always active, add it to PostInit()
in Content.Client/Entry/EntryPoint.cs
.
Testing and Debugging
You can use the /rldshader
command to reload the .swsl
shaders without restarting the game. This means you can often use colour outputs to interactively debug shaders.
Unlike programs written in C#, shaders are compiled by your graphics driver at run time, which means that even simple syntax errors in shaders will only show up when you start the client.
Therefore, it is important that you test your shader by running the client. Shader syntax errors will show up as exceptions (you’ll need to do some looking to find the error message) while loading shader prototypes.
For certain classes of errors, the client will dump an error.glsl
file with the post-processed shader that failed to load. Usually the shader compilation output log will also be shown and identify the problem, but you can also feed the .glsl
file to a tool like
External Tools
- renderdoc - An excellent tool for debugging rendering and shaders.