Shader System
All shaders in Glitter are written in Slang and compiled to SPIR-V at load time.
Every shader is described by a JSON config file under resources/testGame/shaders/.
Shader and JSON changes do not require a CMake reconfigure or C++ rebuild.
How it works
The entry point for application code is always ShaderSystem — never call
loadShaderFromJson directly.
JSON config schema
Minimal single-pass graphics shader
{
"description": "Tonemapping pass",
"source": "slang/tonemap.slang",
"type": "graphics"
}
Single-pass with explicit entry points
{
"source": "slang/tonemap.slang",
"type": "graphics",
"vertex_entry": "vertexMain",
"fragment_entry": "fragmentMain"
}
Entry point keys are optional — Slang defaults: vertexMain, fragmentMain, computeMain.
Compute shader
{
"source": "slang/ibl_spmap.slang",
"type": "compute",
"always_build": true
}
always_build: true bypasses the binary cache and forces a Slang recompile
every run. Used for IBL precompute shaders where the output is written to disk.
Family shader (multiple render passes, one .slang file)
{
"source": "slang/pbr_geometry.slang",
"vertex_entry": "vertexMain",
"passes": {
"geometry": { "fragment_entry": "gbufferFragment" },
"forward": { "fragment_entry": "forwardPbrFragment" }
}
}
A family shader has no top-level fragment_entry.
Each pass compiles an independent VkShaderModule pair sharing the same vertex stage.
Key JSON fields
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
source | string | yes | — | Path to .slang or .spv, relative to the JSON file |
type | "graphics" / "compute" | no | "graphics" | — |
vertex_entry | string | no | "vertexMain" | — |
fragment_entry | string | no | "fragmentMain" | Omit for family shaders |
compute_entry | string | no | "computeMain" | — |
passes | object | no | — | Present = family shader. Keys are pass names. |
always_build | bool | no | false | true disables binary cache |
C++ API
// Single-pass shader
auto handle = shaders.loadShader("tonemap.json");
// Family shader — must specify a pass name
auto gbuffer = shaders.loadShaderPass("pbr.json", "geometry");
auto forward = shaders.loadShaderPass("pbr.json", "forward");
Both methods return tl::expected<ShaderHandle, std::string> — always check the result.
Cache key format
ShaderSystem caches handles by canonical_uri + "#" + passName:
| Call | Cache key |
|---|---|
loadShader("tonemap.json") | shaders://tonemap.json# |
loadShaderPass("pbr.json", "geometry") | shaders://pbr.json#geometry |
Family shader contract
Calling loadShader() (no pass name) on a family shader returns a tl::make_unexpected
at debug log level — this is intentional. The scene loader calls loadShader for
every object, and the renderer resolves the correct pass lazily via loadShaderPass.
Adding a new shader
- Write the
.slangfile inresources/testGame/shaders/slang/. - Create the JSON config in
resources/testGame/shaders/. Use an existing file as template. - Load it via
shaders.loadShader("my_shader.json")orshaders.loadShaderPass(...). - Shader and JSON changes do not require a CMake reconfigure or C++ rebuild.