Shader preprocessor: variants, macros and includes
Working variants, include paths, dependency updates and diagnostics.
The preprocessor selects and expands text before the shader compiler sees it. Use it for shader variants and shared code. Use uniforms for values that must change while the same compiled shader remains in use.
A #define is not a material parameter. SetShaderParameter cannot change it; changing a macro means changing/reloading shader source and compiling the resulting variant. Do not assume a macro variant is automatically faster than a uniform branch: measure the actual shader and target device.
Working variant and include
Create Palette.reckfellshaderinc at the project root:
#ifndef RF_DOC_MODE
#define RF_DOC_MODE 0
#endif
#define RF_DOC_RGB(r, g, b) vec3((r), (g), (b))
#define RF_DOC_FACTOR 2
#undef RF_DOC_FACTOR
#define RF_DOC_FACTOR 1
vec3 rf_doc_color() {
#if RF_DOC_MODE == 0
return RF_DOC_RGB(1.0, 0.0, 0.0) * float(RF_DOC_FACTOR);
#elif RF_DOC_MODE == 1
return RF_DOC_RGB(0.0, 1.0, 0.0) * float(RF_DOC_FACTOR);
#else
return RF_DOC_RGB(0.0, 0.0, 1.0) * float(RF_DOC_FACTOR);
#endif
}RF_DOC_MODE defaults to 0 if the including source did not define it. The function-like macro parenthesizes its arguments. RF_DOC_FACTOR demonstrates the required #undef before redefining a macro; it is a source substitution, not a runtime uniform.
Save this helper as ShaderVariants.cs:
using System;
using Reckfell;
public static class ShaderVariants
{
public static ShaderMaterial Create(int mode)
{
if (mode < 0 || mode > 2)
throw new ArgumentOutOfRangeException(nameof(mode));
string code = "shader_type canvas_item;\nrender_mode unshaded;\n" +
"#define RF_DOC_MODE " + mode + "\n" +
"#include \"asset://Palette.reckfellshaderinc\"\n" +
"#include \"asset://Palette.reckfellshaderinc\"\n" +
"#if 0\nThis branch is excluded before shader compilation.\n#endif\n" +
"void fragment() { COLOR = vec4(rf_doc_color(), 1.0); }\n";
return new ShaderMaterial { Shader = new Shader { Code = code } };
}
}Assign ShaderVariants.Create(0) to the Material of a visible ColorRect or Sprite2D. Mode 0 renders red, 1 green, 2 blue. The helper deliberately includes the same resource twice: the preprocessor skips an already included resource path within one preprocessing operation, so it does not define the function twice. The inactive #if 0 text is removed before shader-language parsing.
The factory creates a separate Shader for each call. If many materials need the same variant, cache/share that Shader intentionally; material-specific uniforms can still differ. Do not rebuild Code every frame to animate a value.
Directives and boundaries
| Directive | Meaning |
|---|---|
| #define NAME text | Replaces identifier tokens after the definition; it does not replace arbitrary substrings of other names. |
| #define FN(a, b) expression | Substitutes required arguments. Parenthesize arguments and the expression; this is not a typed function call. |
| #undef NAME | Removes a user macro before redefinition. An absent user macro is allowed; built-in macros cannot be undefined. |
| #if / #elif / #else / #endif | Selects source branches using preprocessor expressions. Close every conditional. |
| #ifdef / #ifndef | Tests whether a macro exists, not whether its replacement is nonzero. |
| defined(NAME) | Tests one macro identifier inside a condition. |
| #include "path" | Loads a ShaderInclude resource and processes it in the current macro context. |
| #error message | Stops preprocessing with a diagnostic, useful for rejecting unsupported combinations. |
| #pragma disable_preprocessor | Disables further preprocessing. It does not make remaining unsupported directives valid shader code. |
Put directives on their own lines. A semicolon at the end of a macro replacement becomes part of the replacement and may break its uses; do not add one automatically. A trailing backslash continues a directive onto the next line. Token concatenation with ## is available, but prefer ordinary shader functions when token substitution is not needed.
An existence test and a value test differ: with #define FLAG 0, #ifdef FLAG is true while #if FLAG is false. With an empty flag, use #ifdef or defined(FLAG), not an arithmetic value test. A uniform cannot participate in #if: uniforms are runtime shader inputs.
CURRENT_RENDERER can be compared with RENDERER_FORWARD_PLUS, RENDERER_MOBILE or RENDERER_COMPATIBILITY. Those constants identify the selected renderer, not a complete hardware-feature test. The executed examples use Forward+/Vulkan.
Resolve includes deliberately
Use the exact .reckfellshaderinc extension for includes. In a saved .reckfellshader, a relative include is resolved against the shader file location. For shader source created in C#, an absolute asset:// path works without a base file. To use a relative path on a Shader with no ResourcePath, call SetIncludePath with a full base shader filename before assigning Code, not merely a directory. Loading a shader resource sets up that base for you.
Includes may include other includes, but avoid cycles. Nested include depth is limited to 25. Reusing one include does not create a parameterized fresh copy for every #include: repeated paths are skipped. If you need multiple generated implementations, give them explicit separate declarations or redesign the shared function.
Changing ShaderInclude.Code notifies dependent shaders; the runtime check confirms changed pixels and restoration without reassigning the material. Shader.Code/GetCode return the original source, including directives, not a dump of expanded backend code. Editing an include in memory does not automatically save its file.
Diagnose preprocessing errors
| Symptom | Check |
|---|---|
| Macro redefinition | Previous #define; remove it with #undef or use a deliberate default guard. |
| Unmatched branch | Missing #endif, misplaced #else/#elif or a conditional spanning an include boundary. |
| Include not found | File extension, actual path and the base filename for a relative path. |
| Cyclic/depth error | Include graph; repeated resource paths are not a license to create recursive dependencies. |
| Macro changed but the material looks unchanged | The wrong shared Shader/resource may have been edited, or a runtime uniform was intended. |
| #pragma did not fix compilation | Disabling preprocessing does not translate foreign shader syntax. |
Actual GPU checks cover the published variants, argument macro, undef/redefinition, excluded text, repeated/relative includes and dependency changes. Invalid-directive diagnostics, editor completion, all renderers and performance are source-reviewed or separate validation, not claimed runtime tests here. For foreign source, use GLSL conversion.