Shader Configuration
Replace built-in rendering shaders with custom WGSL, applied globally or per-type.
Mods can replace the built-in rendering shaders with custom WGSL (the WebGPU Shading Language). Shaders are declared in a single config file per mod and can be applied globally (affecting all objects of a category) or per-type (targeting a specific mob, static object, weather type, or biome). Status icons (health, oxygen), inventory items, and block/static-object break particles can also be overridden globally.
Each override is one .wgsl file containing two entry points:
vs_main— the@vertexstagefs_main— the@fragmentstage
File Location
mods/<your-mod>/shaders.yaml
WGSL source files are placed anywhere under the mod folder and referenced by path relative to the mod root:
mods/<your-mod>/shaders/my_shader.wgsl
Minimal Example
shaders:
- name: my_entity_shader
source: shaders/my_entity_shader.wgsl
This registers a shader named YourMod:my_entity_shader. Because no type: is given, it defaults to entity.
Full File Reference
# Global default overrides (all optional)
default-shader: my_entity_shader # applies to all entities/mobs
default-block-shader: my_block_shader # applies to all block chunks
default-static-object-shader: my_static_shader # applies to all static objects
default-particle-shader: my_particle_shader # applies to all particles
default-weather-shader: my_weather_shader # applies to all weather types
default-biome-backdrop-shader: my_biome_shader # applies to all biome backdrops
default-icon-shader: my_icon_shader # applies to all status icons (health, oxygen)
default-item-shader: my_item_shader # applies to all inventory and hotbar items
default-break-particle-shader: my_break_shader # applies to block and static-object break particles
shaders:
- name: my_entity_shader # Required. Name used to reference this shader.
source: shaders/entity.wgsl # Required. Path to the WGSL file, relative to mod root.
type: entity # Optional. One of: entity, block, static-object, particle,
# weather, biome-backdrop, icon, item, break-particle.
# Defaults to entity if omitted.
- name: my_block_shader
source: shaders/block.wgsl
type: block
- name: my_break_shader
source: shaders/break.wgsl
type: break-particle
Shader Types
| Type | Affects | Global Default Key | Per-Type Override |
|---|---|---|---|
entity | Mobs and creatures | default-shader | Yes — via shader: in mob config |
block | Block chunk rendering | default-block-shader | No |
static-object | Interactables and trees | default-static-object-shader | Yes — via shader: in interactable/tree config |
particle | All particles | default-particle-shader | No |
weather | Weather layer rendering | default-weather-shader | Yes — via client.shader in weather config |
biome-backdrop | Biome background layers | default-biome-backdrop-shader | Yes — via shader: in biome config |
icon | Status icons (health, oxygen) | default-icon-shader | No |
item | Inventory and hotbar items | default-item-shader | No |
break-particle | Block and static-object break particles | default-break-particle-shader | No |
Block, particle, icon, item, and break-particle shaders can only be set globally. Per-type overrides for these categories are not supported.
Shader Names and Namespacing
If you write a shader name without a colon (e.g., MY_SHADER), it is automatically prefixed with your mod name at load time: YourMod:my_shader.
You can reference a shader from another mod by using its fully-qualified name: OtherMod:their_shader.
Per-Type Shader Assignments
Mobs
Add shader: to a mob entry in your mod’s mob config:
- name: my_creature
shader: my_entity_shader
# ... rest of mob config
The value can be a short name (auto-prefixed with your mod name) or a fully-qualified name with a colon.
Static Objects (Interactables and Trees)
Add shader: to the relevant entry in your interactable or tree config:
- name: my_chest
shader: my_static_shader
# ... rest of interactable config
Weather Types
Add shader: to the client: block of the relevant entry in your mod’s weather config:
weathers:
- name: blizzard
timing:
# ... min-ms / max-ms / transition-out-ms
client:
shader: my_weather_shader
# ... layers, etc.
Biomes
Add shader: to the relevant entry in your mod’s biome config:
biomes:
- name: crystal_cavern
shader: my_biome_shader
# ... rest of biome config
Shader Contracts
Every shader type has a fixed contract: the set of vertex attribute @locations the engine feeds vs_main, and the set of @group/@binding resources the engine provides. Your shader is checked against its contract at load time:
vs_mainmay only consume@locations from the allowed set (you don’t have to consume all of them).- Any
@group(G) @binding(B)your entry points actually use must exist in the engine-provided set. Declared-but-unused bindings are ignored. - Built-in inputs (
@builtin(vertex_index),@builtin(instance_index), …) are always allowed.
A shader that violates its contract, fails to parse, or fails validation is rejected: a warning is written to logs/client.log and the engine’s built-in shader is used instead. No game crash occurs.
In every contract below, @location(0) position / @location(1) tex_coords are the shared unit quad (vec2<f32> each, positions in [0,1]²); the remaining locations are per-instance attributes.
World Domains and the Lighting Prelude
The entity, block, and static-object domains are lit world passes. Before compiling, the engine prepends assets/shaders/lighting_common.wgsl to your source, so:
- Do not redeclare
@group(0)or@group(1)resources — the prelude already declaresframe(FrameUniforms),lights,persistence,sky_light(group 0) andsector(SectorUniforms, group 1). You just use them. - The prelude’s lighting helper functions (
dynamic_light_color,sky_light_factor,ray_march_dampening,composite_lighting, …) are available to call. - Study the engine originals in
assets/shaders/(entity_instanced.wgsl,block.wgsl,static_object.wgsl) — the easiest way to write an override is to copy one and editfs_main.
Non-world domains (particle, weather, biome-backdrop, icon, item, break-particle) get no prelude — your file must declare the bindings it uses, matching the tables below.
Entity (entity)
Pattern: assets/shaders/entity_instanced.wgsl. Lighting prelude prepended.
| Location | Attribute | Type | Description |
|---|---|---|---|
| 0 | position | vec2<f32> | Unit quad vertex position |
| 1 | tex_coords | vec2<f32> | Unit quad UV |
| 2 | pos_px | vec2<f32> | Screen-space top-left of the entity (pixels) |
| 3 | frame_size | vec2<f32> | Animation frame size (pixels) |
| 4 | entity_pos | vec2<f32> | World-space center position |
| 5 | tex_index | i32 | Layer in the entity texture array (frame included) |
| 6 | flip_x | i32 | 1 = flip horizontally |
| 7 | sky_light | i32 | Sky light level (0-15) |
| 8 | rotation | vec4<f32> | 2×2 rotation matrix, column-major |
Bindings (beyond the prelude’s groups 0/1): @group(2) @binding(0) texture_2d_array<f32> (sprites), @group(2) @binding(1) texture_2d_array<f32> (light masks), @group(2) @binding(2) sampler.
Block (block)
Pattern: assets/shaders/block.wgsl. Lighting prelude prepended.
| Location | Attribute | Type | Description |
|---|---|---|---|
| 0 | position | vec2<f32> | Unit quad vertex position |
| 1 | tex_coords | vec2<f32> | UV into the pre-baked chunk texture |
Bindings (beyond groups 0/1): @group(2) @binding(0) texture_2d<f32> (chunk texture), @group(2) @binding(1) sampler.
Static Object (static-object)
Pattern: assets/shaders/static_object.wgsl. Lighting prelude prepended. Same as entity minus rotation:
| Location | Attribute | Type |
|---|---|---|
| 0 | position | vec2<f32> |
| 1 | tex_coords | vec2<f32> |
| 2 | pos_px | vec2<f32> |
| 3 | frame_size | vec2<f32> |
| 4 | entity_pos | vec2<f32> |
| 5 | tex_index | i32 |
| 6 | flip_x | i32 |
| 7 | sky_light | i32 |
Bindings (beyond groups 0/1): @group(2) @binding(0)/(1) texture arrays, @group(2) @binding(2) sampler, @group(2) @binding(3) var<uniform> ObjectDrawUniforms (per-draw rotation + padded array frame size).
Particle (particle)
Pattern: assets/shaders/particle.wgsl.
| Location | Attribute | Type | Description |
|---|---|---|---|
| 0 | position | vec2<f32> | Unit quad vertex position |
| 1 | tex_coords | vec2<f32> | Unit quad UV |
| 2 | pos | vec2<f32> | World-space particle position |
| 3 | vel | vec2<f32> | Particle velocity |
| 4 | life | f32 | Remaining lifetime in seconds (<= 0 = dead) |
| 5 | particle_type_id | u32 | Particle type (row in the atlas offsets texture) |
Bindings: @group(0) @binding(0) var<uniform> ParticleUniforms, @group(0) @binding(1) texture_2d<f32> (atlas offsets, read with textureLoad), @group(0) @binding(2) texture_2d<f32> (particle atlas), @group(0) @binding(3) sampler.
Dead-slot cull warning (required): the engine draws the entire GPU particle ring buffer every frame; dead slots have life <= 0.0. Your vs_main must eject dead slots before rasterization by emitting a clip-space position outside the view volume (e.g. vec4<f32>(2.0, 2.0, 2.0, 1.0)) when life <= 0.0. An fs_main discard alone is not enough — it runs after rasterization, and the resulting invisible overdraw has been measured to cost ~10 ms/frame. Copy the cull from assets/shaders/particle.wgsl.
Weather (weather) and Biome Backdrop (biome-backdrop)
Pattern: assets/shaders/backdrop.wgsl. Both domains share one contract:
| Location | Attribute | Type |
|---|---|---|
| 0 | position | vec2<f32> |
| 1 | tex_coords | vec2<f32> |
Bindings: @group(0) @binding(0) var<uniform> QuadUniforms (position/size/frame/crop/opacity/skylight tint — see backdrop.wgsl), @group(0) @binding(1) texture_2d<f32>, @group(0) @binding(2) sampler.
Icon (icon)
Pattern: assets/shaders/icon.wgsl.
| Location | Attribute | Type | Description |
|---|---|---|---|
| 0 | position | vec2<f32> | Unit quad vertex position |
| 1 | tex_coords | vec2<f32> | Unit quad UV |
| 2 | pos | vec2<f32> | Screen-space anchor of this icon instance |
| 3 | icon_type_id | u32 | Icon type (row in the atlas offsets texture) |
| 4 | frame_index | u32 | CPU-computed animation frame |
Bindings: @group(0) @binding(0) var<uniform> AtlasUniforms, @group(0) @binding(1) offsets texture (textureLoad), @group(0) @binding(2) atlas texture, @group(0) @binding(3) sampler.
Item (item)
Pattern: assets/shaders/item.wgsl.
| Location | Attribute | Type | Description |
|---|---|---|---|
| 0 | position | vec2<f32> | Unit quad vertex position |
| 1 | tex_coords | vec2<f32> | Unit quad UV |
| 2 | pos | vec2<f32> | Screen-space anchor of this item instance |
| 3 | item_id | u32 | Item atlas slot index |
Bindings: same atlas group as icon (AtlasUniforms + offsets + atlas + sampler at @group(0) @binding(0..3)).
Break Particle (break-particle)
Pattern: assets/shaders/rectangle.wgsl (break particles are instanced colored squares).
| Location | Attribute | Type | Description |
|---|---|---|---|
| 0 | position | vec2<f32> | Unit quad vertex position |
| 1 | tex_coords | vec2<f32> | Unit quad UV |
| 2 | pos_px | vec2<f32> | Screen-space top-left (pixels) |
| 3 | frame_size | vec2<f32> | Width/height (pixels) |
| 4 | color | vec4<f32> | RGBA sampled from the broken block/object texture (normalized to [0,1]) |
Bindings: @group(0) @binding(0) var<uniform> RectangleUniforms (screen_size: vec2<f32>).
Errors
If a shader fails to parse, fails WGSL validation, or violates its contract, the engine logs a warning to logs/client.log and silently falls back to the built-in shader for that category. Mod shaders can never crash the game at load time.
Complete Example
A mod that tints all break particles red:
shaders.yaml:
default-break-particle-shader: red_break
shaders:
- name: red_break
source: shaders/red_break.wgsl
type: break-particle
shaders/red_break.wgsl:
struct RectangleUniforms {
screen_size: vec2<f32>,
_pad: vec2<f32>,
}
@group(0) @binding(0) var<uniform> rect: RectangleUniforms;
struct VsOut {
@builtin(position) clip_pos: vec4<f32>,
@location(0) color: vec4<f32>,
}
@vertex
fn vs_main(
@location(0) position: vec2<f32>,
@location(2) pos_px: vec2<f32>,
@location(3) frame_size: vec2<f32>,
@location(4) color: vec4<f32>,
) -> VsOut {
var out: VsOut;
let px = pos_px + position * frame_size;
let ndc = px / rect.screen_size * 2.0 - 1.0;
out.clip_pos = vec4<f32>(ndc.x, -ndc.y, 0.0, 1.0);
out.color = color;
return out;
}
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
// Keep the particle's alpha, force the hue red.
return vec4<f32>(1.0, 0.1, 0.1, in.color.a);
}