Shader API refactor
Introduction
I was once working on a complete refactor of the shader API. The goal was to provide more flexibility for shader modding.
Basics
In this system, all Shaders are defined by name in a file named SHDRDEFS
. This would, in theory, allow for custom Shader definitions that can be used within a level. Every Shader must define at least one Source, which is (currently) GLSL code for a Shader Object. This can be either a vertex shader, or a fragment shader. Example:
Shader Default
{
BuiltIn
Source Vertex
{
void main(void)
{
gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
gl_FrontColor = gl_Color;
gl_TexCoord[0].xy = gl_MultiTexCoord0.xy;
gl_ClipVertex = gl_ModelViewMatrix * gl_Vertex;
}
}
Source Fragment
{
uniform sampler2D tex;
uniform vec4 poly_color;
void main(void)
{
gl_FragColor = texture2D(tex, gl_TexCoord[0].st) * poly_color;
}
}
}
This Shader, called Default
, is a built-in Shader (more on that later) that defines a Vertex Source and a Fragment Source. Shaders may also be Include Shaders and be included in other Shaders, reducing code duplication. Example, using gbshader:
Shader GBShader
{
IsInclude
Source Fragment
{
const int greylevels = 3;
const float dither[4*4] = {
0.0625, 0.5625, 0.1875, 0.6875,
0.8125, 0.3215, 0.9375, 0.4375,
0.2500, 0.7500, 0.1250, 0.6250,
1.0000, 0.5000, 0.8750, 0.3750
};
const vec4 black = {0, 0.1, 0, 1};
const vec4 white = {0.6, 1, 0.4, 1};
const int dscale = 2;
void DoGBShader(void)
{
float grey = gl_FragColor.r * 0.2126 + gl_FragColor.g * 0.7152 + gl_FragColor.b * 0.0722;
int index = int(mod(gl_FragCoord.x/dscale, 4.0)) + int(mod(gl_FragCoord.y/dscale, 4.0)) * 4;
grey *= greylevels;
grey += dither[index];
index = int(grey);
gl_FragColor.rgb = black.rgb + (white.rgb - black.rgb) * index / greylevels;
}
}
}
Shader Sky
{
Include GBShader
Source Fragment
{
uniform sampler2D tex;
uniform vec4 poly_color;
void main(void)
{
gl_FragColor = texture2D(tex, gl_TexCoord[0].st) * gl_Color * poly_color;
DoGBShader(); // Modifies gl_FragColor
}
}
}
This replaces the WaterRipple
Shader, which is a built-in, so the vertex Source does not have to be defined. It includes GBShader
, and replaces the Fragment Source. In it, it calls the DoGBShader
function, defined in the GBShader
Include.
Keywords
In a Shader Definition Block, certain semantics are used to define how a Shader will work. Here's a list of them:
Source <stage>
This defines a Source for the Shader's Program Object. The Program Object contains compiled code for the stages of a Shader. <stage>
can be either Vertex
or Fragment
.
After the definition of the source, comes a block ({}
) with GLSL code for the specified Shader stage, or the keyword UseDefault
. Using UseDefault
will instead copy the code from the Default
shader, a built-in.
Example:
Shader ExampleScreenShader
{
Source Vertex UseDefault
Source Fragment
{
// GLSL code here...
}
}
BuiltIn
This states that the Shader's is a built-in. This is used for SRB2's own default base shaders. Built-in Shaders cannot be replaced.
Example:
Shader Flat
{
BuiltIn
IncludeSoftwareShaders
Source Vertex UseDefault
Source Fragment
{
void main(void)
{
R_DefaultSoftwareMix();
}
}
}
IsInclude
This states that the Shader is an Include. Includes are not compiled as their own shaders, and should not be used individually.
Example:
Shader WaterShaderIncludes
{
BuiltIn
IsInclude
Source Fragment
{
uniform float leveltime;
const float freq = 0.025;
const float amp = 0.025;
const float speed = 2.0;
const float pi = 3.14159;
vec4 R_GetWaterTexel(void)
{
float z = (gl_FragCoord.z / gl_FragCoord.w) / 2.0;
float a = -pi * (z * freq) + (leveltime * speed);
float sdistort = sin(a) * amp;
float cdistort = cos(a) * amp;
return texture2D(tex, vec2(gl_TexCoord[0].s - sdistort, gl_TexCoord[0].t - cdistort));
}
}
}
Note that Includes cannot contain a main
function.
Replace
This keyword will make the Shader entirely replace the Shader which has the same name. This does not work for Shaders that are not Includes.
Include
This keyword will include another Shader into the Shader. Includes can be recursive. Example:
Shader GBShader
{
...
}
Shader SoftwareShaderIncludes
{
Include GBShader
Source Fragment
{
/* The entire code here... */
void R_DefaultSoftwareMix(void)
{
gl_FragColor = R_DoSoftwareMix(texture2D(tex, gl_TexCoord[0].st));
DoGBShader();
}
}
}
Shader Sky
{
Include GBShader
Source Fragment
{
uniform sampler2D tex;
uniform vec4 poly_color;
void main(void)
{
gl_FragColor = texture2D(tex, gl_TexCoord[0].st) * gl_Color * poly_color;
DoGBShader();
}
}
}
Shader WaterRipple
{
Source Fragment
{
void main(void)
{
gl_FragColor = R_DoSoftwareMix(R_GetWaterTexel());
DoGBShader();
}
}
}
This replaces the Fragment Source SoftwareShaderIncludes
built-in. It includes the GBShader
Shader and calls its DoGBShader
function.
Note that WaterRipple
doesn't have to include GBShader
, since it is a built-in that has already included SoftwareShaderIncludes
, using the IncludeSoftwareShaders
keyword.
IncludeSoftwareShaders
This keyword will include the SoftwareShaderIncludes
Shader into the Shader. Example:
Shader CoolSpriteShader
{
IncludeSoftwareShaders
Source Vertex UseDefault
Source Fragment
{
void main(void)
{
R_DefaultSoftwareMix();
}
}
}
RemoveSoftwareShaders
This does the opposite of IncludeSoftwareShaders
.
ClearIncludes <stage>
This keyword can be used to remove Includes. <stage>
can be either Vertex
or Fragment
. If the stage is specified, it will remove any Sources of all Includes. If the stage is omitted, all of the Includes will be removed.
Example:
Shader SomeonesSpriteShader
{
ClearIncludes Fragment
Include MyInclude
Source Fragment
{
void main(void)
{
DoTheCoolStuff();
}
}
}
Challenges
- How can custom shaders be assigned to mobjs and other surfaces of a level?
- Add a mobj field that specifies the numeric shader ID to use. The ID can be found with another function. Alternatively, abstract the field in Lua by only accepting strings and letting the Lua API handle everything.
- For walls, add a sidedef field?
- A linedef effect could also work.
- What about using shaders in 2D drawing?
- There is currently no way to use binary shader code, such as SPIR-V.
Another problem is that a single SPIR-V file can multiple shader stages, which means that a Shader must have to specify which entry point to use. An example of how this could work:
Shader ExampleShader
{
IsBinaryShader SPIRV
Source Vertex
{
File "Shaders/SPIR-V/Example.spv"
EntryPoint "vertexMain"
}
Source Fragment
{
File "Shaders/SPIR-V/Example.spv"
EntryPoint "fragmentMain"
}
}
The OpenGL standard says that you cannot link SPIR-V shaders to non-SPIR-V shaders, which is why IsBinaryShader
is a keyword for the entire Shader.
Also, some GPUs may not support binary shaders. There should be some kind of fallback mechanism to use uncompiled shader code instead. One that I thought of:
Shader ExampleShader
{
Source Vertex
{
// GLSL code
}
Source Fragment
{
// GLSL code
}
BinarySource Vertex
{
Type SPIRV
File "Shaders/SPIR-V/Example.spv"
EntryPoint "vertexMain"
}
BinarySource Fragment
{
Type SPIRV
File "Shaders/SPIR-V/Example.spv"
EntryPoint "fragmentMain"
}
}
Alternatively:
Shader ExampleShader
{
Source Vertex GLSL // 'GLSL' keyword is optional, Source is assumed to be GLSL by default
{
// GLSL code
}
Source Fragment GLSL
{
// GLSL code
}
Source Vertex Binary
{
Type SPIRV
File "Shaders/SPIR-V/Example.spv"
EntryPoint "vertexMain"
}
Source Fragment Binary
{
Type SPIRV
File "Shaders/SPIR-V/Example.spv"
EntryPoint "fragmentMain"
}
}