The HLSL11 passthrough shader is a shader I find myself needing a lot, and GameMaker does not automatically create it for us.
So here’s a working version for everyone to enjoy! Click the little clipboards at the top right of code blocks to copy it to your clipboard easily.
📜 HLSL11 Passthrough: Vertex📋//
// Simple passthrough vertex shader
//
//Input from Vertices
struct VertexShaderInput
{
float3 pos : POSITION;
// float3 norm : NORMAL;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
//Output Struct to Pixel Shader
struct VertexShaderOutput
{
float4 pos : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
//Main Program
VertexShaderOutput main(VertexShaderInput input)
{
VertexShaderOutput output;
float4 pos = float4(input.pos, 1.0f);
// Transform the vertex position into projected space.
pos = mul(gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION], pos);
output.pos = pos;
// Pass through the color
output.color = input.color;
// Pass through uv
output.uv = input.uv;
return output;
}
📜 HLSL11 Passthrough: Fragment/Pixel📋//
// Simple passthrough fragment shader
//
//Output From Vertex Shader
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
//Main Program
float4 main(PixelShaderInput input) : SV_TARGET
{
float4 vertColour = input.color;
float4 texelColour = gm_BaseTextureObject.Sample(gm_BaseTexture, input.uv);
float4 combinedColour = vertColour * texelColour;
return combinedColour;
}
The SV_POSITION
semantic is basically equivalent to gl_Position
, and the SV_TARGET
semantic is basically equivalent to gl_FragColor
BONUS EXTRA INFO!!!
Multiple Input Textures
To read from multiple textures, you need to define them a little differently to GLSL ES shaders. Instead of a sampler2D
, you need this:
Texture2D textureObject: register(t1);
SamplerState texture: register(s1);
You then sample the texture like this:
textureObject.Sample(texture, input.uv);
Both a Texture2D
and a SamplerState
are needed, as one represents a texture, the other represents any settings applied to it (like filtering, mipmaps etc). The number in the register()
function needs to be incremented for each extra texture, as it represents the sampler index (e.g, t1
+ s1
, t2
+ s2
, .., tN
+ sN
). Index 0 is reserved for gm_BaseTexture
and gm_BaseTextureObject
.
When using shader_get_sampler_index(shader, "name")
, the name you should pass is the name of the SamplerState
, not the Texture2D
– though this index will be the same as the register index, it’s good practice to get the index properly, in case the register needs to be changed in code later.
Multiple Render Targets
Unlike GLSL ES shaders GameMaker uses, HLSL shaders support multiple render targets (or MRTs for short). That means you can render to multiple surfaces simultaneously from within the same shader!
To do this, you just need to modify the fragment/pixel program of the shader. You need a struct
of outputs, with each field mapped to an SV_Target[n]
semantic, and have the main() program return an instance of that struct, like this:
📜 HLSL11 Passthrough with MRTs: Fragment/Pixel📋//
// Simple passthrough fragment shader with MRTs
//
//Output From Vertex Shader
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
//Output Struct
struct PixelShaderOutput {
float4 color : SV_Target0;
float4 example1 : SV_Target1;
float4 example2 : SV_Target2;
float4 example3 : SV_Target3;
};
//Main Program
PixelShaderOutput main(PixelShaderInput input)
{
PixelShaderOutput output;
float4 vertColour = input.color;
float4 texelColour = gm_BaseTextureObject.Sample(gm_BaseTexture, input.uv);
float4 combinedColour = vertColour * texelColour;
//Output the main thing as the proper color
output.color = combinedColour;
//Just output each other target as red, green and blue, just as an example
output.example1 = float4(1.0,0.0,0.0,1.0);
output.example1 = float4(0.0,1.0,0.0,1.0);
output.example1 = float4(0.0,0.0,1.0,1.0);
return output;
}
This can be incredibly useful for stuff like rendering normals, depth and colors all to separate surfaces in one pass, which can later be used for screenspace effects, for example.
You do need to set surfaces as render targets with surface_set_target_ext(index, surface)
and choose the MRT index desired. Remember – 0 is the default (so application_surface
or the target surface from surface_set_target
– so SV_Target1
corresponds with index 1, for example.
GameMaker Studio 2 currently supports up to 4 render targets, including the default one.
The vertex shader does not need any changes to use MRTs
#include
Another neat thing you can do with HLSL11 shaders that you can’t do with GLSL ES shaders in GameMaker is make use of the #include
preprocessor directive.
This lets you include extra code in a shader from an external file. It basically copies the entire contents of a target file and replaces the line it’s used on with those contents. This can be especially useful if you have a collection of shader functions you use a lot, but don’t want to modify EVERY shader that uses a specific function every time it needs tweaking.
So how can we use it? There are two options: you can either use an absolute path to a file, or a relative path, with one major caveat each. Paths may be enclosed in double quotes or in <>, a lot like C++. The file can have any extension, as long as the contents are plaintext.
The absolute path version is pretty much self explanatory:
#include "C:\\Path\\To\\Shader\\file.fileextension"
This method is really easy to do, but it’s not portable. If your project ends up on another computer, it will only work if the included file exists in the exact same path.
When using relative paths, the base directory is “C:\Users\<USERNAME>\AppData\Local\GameMakerStudio2\GMS2TEMP\” – combined shader files get put here for compilation, but unfortunately we can’t access any other folders, since other folders of projects in this directory have unpredictable IDs assigned to them and we can’t even access parent directories with “..” (likely due to how GameMaker maps temporary drives). So how can we use this?
What I do is create a special “library” shader, and then use some macro definitions to allow me to include it safely. The type of this shader can actually be anything, as long as final processed code included is HLSL11 code, though it makes sense to keep it as an HLSL11 shader, so we get useful syntax highlighting.
Here is what my typical example library shader looks like (it’s called hlsl_library
in the IDE):
HLSL11 Library Shader
📜 HLSL11 Library Shader (vertex): hlsl_library.vsh📋#ifdef __INCLUDE_LIBRARY__
//Prevent duplicate definition
#ifndef __LIBRARY_hlsl_library_
#define __LIBRARY_hlsl_library_
//Library functions go here
float4 make_it_red(float4 inCol) {
return inCol * float4(1.0, 0.0, 0.0, 1.0);
}
#endif
#else
// Default fallback shader goes here
// Simple passthrough vertex shader
//
//Input from Vertices
struct VertexShaderInput
{
float3 pos : POSITION;
// float3 norm : NORMAL;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
//Output Struct to Pixel Shader
struct VertexShaderOutput
{
float4 pos : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
//Main Program
VertexShaderOutput main(VertexShaderInput input)
{
VertexShaderOutput output;
float4 pos = float4(input.pos, 1.0f);
// Transform the vertex position into projected space.
pos = mul(gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION], pos);
output.pos = pos;
// Pass through the color
output.color = input.color;
// Pass through uv
output.uv = input.uv;
return output;
}
#endif
📜 HLSL11 Library Shader (fragment): hlsl_library.fsh📋//Only build the fragment program when not used as a library...
#ifndef __INCLUDE_LIBRARY__
//Output From Vertex Shader
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
//Main Program
float4 main(PixelShaderInput input) : SV_TARGET
{
float4 vertColour = input.color;
float4 texelColour = gm_BaseTextureObject.Sample(gm_BaseTexture, input.uv);
float4 combinedColour = vertColour * texelColour;
return combinedColour;
}
#endif
It adds one function when included as a library – make_it_red(float4 color)
which just returns the input color with non-red channels set to 0.This function can now be called directly, as long as it is used after the #include
directive.
The check for the definition of __INCLUDE_LIBRARY__
means that __INCLUDE_LIBRARY__
must be defined before the #include
, but this allows the shader use a fallback and still compile and run when not used as a library, which can prevent accidental crashes. In my case, I just made it act as a passthrough shader unless used as a library.
The check for and definition of __LIBRARY_hlsl_library_
prevents the library from being included more than once, in case multiple libraries that depend upon this library are used, as including the library again would crash the compiler due to function redefinition. Note that I used the name of the library in this macro – this is important since each library should have its own name and own identifying macro. You could go even further and have a definition test in place for every individual function in a library, to be even safer, but it shouldn’t usually be necessary.
If a library does include another library, it should check if __INCLUDE_LIBRARY__
is defined, and if not, #define
it. If we don’t define this macro and try to include the library, the shader will crash because the main shader code will be included, creating duplicate definitions.
A library-fallback shader can use the functions of the library, with some slight adjustments to the #ifdef logic.
The auto-generated extension for shaders is “.shader”, so the file you’d include is “<shadername>.shader”, where <shadername> is the resource name of the library shader – so in this case, our #include
lines are these:
#define __INCLUDE_LIBRARY__
#include "hlsl_library.shader"
(p.s. if you use the <> symbols instead of quotation marks, it’ll work the same, but your shader name can be syntax-highlighted!)
Remember – both the fragment and vertex programs of the library will be included, since the shader files are combined at this point.
Now any line after this include has access to any function defined in the library shader – very nice!
The shader is compiled at the same time as the game, so the #included files do not need to exist on a players computer. It appears shaders are compiled after they are all converted to combined *.shader files, so shader order does not matter when it comes to inclusion.
One minor downside to this shader library method is that you can’t have a library shared across multiple projects, without manual editing.