A new crate, horizon-shader-api, reads and checks Horizon's own WGSL shader packs (docs/api/shaders.md), so the game, the launcher and the registry judge a pack the same way. This first part is the format: the manifest (id, name, version, the api line, shared files, the sky, material and post stages, options typed for WGSL override constants, a GPU time budget) and the package (a folder or a .hshader zip, built reproducibly so its SHA-256 depends only on its files). The stages' WGSL is checked in the next parts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.3 KiB
Shader API
Horizon has no shader packs today: the earlier implementation was removed on 2026-10-24. Two things will replace it, designed separately: a Horizon shader API, described here, and a clean-room loader for the existing pack format (its own spec; it is not part of this API). Both sit on the same renderer hooks, agreed between the two.
Overview: what a shader author will be able to do
Write WGSL against documented stages of Horizon's renderer, with typed inputs, options the player can change, a reload as soon as a file is saved and an overlay that shows what each pass costs and draws. The renderer keeps everything that makes it fast (culling, batching, LOD, the mesher); a pack changes how surfaces and light look, not how the world is fed to the GPU.
The design
- A pack is a folder or a zip with
horizon-shaders.toml(id, name, version, theapiline, the stages it uses, its options) and WGSL files. No preprocessor and no other shading language: WGSL is what wgpu validates and compiles for Vulkan, Metal and DirectX 12. - Stages, not pipelines. A stage is a function with a fixed signature that the renderer calls inside its own pass,
for example a terrain material (
fn material(input: TerrainInput) -> Surface), the lighting of the visible surfaces, the sky, an optional shadow pass, and a chain of full-screen post passes ending in the frame. A pack that leaves a stage out gets vanilla's look for it. - Typed inputs. Each stage receives structs (the frame: time, camera, sun and moon, weather, fog; the surface: position, normal, block and its light) whose WGSL is generated from the Rust types the renderer uploads. The generated reference lists every field with its unit and range, and a test fails when it drifts.
- Options are WGSL
overrideconstants. The manifest declares each option (on/off, a number in a range, a choice) with its default; the game shows them on the pack's settings screen and sets them when it builds the pipeline. No text substitution, and every option combination is validated the same way. - Hot reload. The game watches the pack's folder. A saved file is validated with naga and the stage rebuilt; on an error the last good version keeps drawing and the error, with its file and line, is shown in the overlay.
- Debug overlay. GPU time per stage and pass (timestamp queries), the targets each pass writes (show any of them full-screen), the pack's options and its compile errors.
- Budgets. A pack declares the targets and passes it needs; the renderer refuses more than its limits, and the benchmark (benchmark-report.md) records the pack and its cost, so "fast" stays measured.
- Off by default. Without a pack the renderer is vanilla's look, and no pack code runs.
The pack format (schema 1)
A pack is a folder, or the same files zipped as a .hshader, with horizon-shaders.toml at its root
(crate horizon-shader-api; the sample with every key is
crates/horizon-shader-api/tests/samples/schema-1/):
schema = 1 # the manifest's format; a reader refuses schemas newer than it knows
id = "soft_light" # [a-z0-9_]{1,64}: the settings key and the registry id
name = "Soft Light"
version = "1.0.0"
api = "0.1" # the shader API line the WGSL is written for
description = "..." # optional, like authors and license
shared = ["common.wgsl"] # optional: WGSL put before every stage's file, in order
[sky] # each stage is optional, but a pack has at least one
file = "sky.wgsl"
[material]
file = "material.wgsl"
[[post]] # full-screen passes, in the order they run (8 at most)
file = "post/grade.wgsl"
name = "Colour grade" # optional: the GPU time overlay's label
[[options]] # 64 at most; `id` is the WGSL `override` it sets
id = "saturation"
name = "Saturation"
type = "float" # bool, float (min, max, step), int (min, max), choice (values)
default = 1.1
min = 0.0
max = 2.0
step = 0.05
[budget]
gpu_ms = 1.5 # optional: the GPU time the pack aims to add (0 to 16 ms)
An option's override has the type of its kind: bool, f32, i32, or u32 for a choice (the index of the
value). The pack holds only the manifest, the .wgsl files it names (each used, UTF-8, 256 KiB at most),
README.md, LICENSE (.md, .txt) and icon.png, under plain paths, 8 MiB in all. A .hshader is built
the same way every time (files in path order, fixed dates), so a pack's SHA-256 depends only on its files.
Guide, reference and examples
None yet. When the first stage lands: a guide here, a generated reference (shaders-reference.md) and an example pack
in the repository that the renderer's tests load.
Stability
Not started. The first stages will be experimental (opt-in), then preview once a reference, an example and a test pack exist (stability.md).
Not there yet
The stages and everything that runs a pack (the format above is read and checked). The order: the renderer hooks shared with the format loader, then the material and post stages with options and hot reload, then the overlay, lighting, sky and shadow stages.
Changelog
- 2026-10-25: the pack format, schema 1:
horizon-shaders.tomland the.hshaderpackage. - 2026-10-24: the earlier shader pack code was removed; this API is designed from scratch.