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 @vertex stage
  • fs_main — the @fragment stage

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

TypeAffectsGlobal Default KeyPer-Type Override
entityMobs and creaturesdefault-shaderYes — via shader: in mob config
blockBlock chunk renderingdefault-block-shaderNo
static-objectInteractables and treesdefault-static-object-shaderYes — via shader: in interactable/tree config
particleAll particlesdefault-particle-shaderNo
weatherWeather layer renderingdefault-weather-shaderYes — via client.shader in weather config
biome-backdropBiome background layersdefault-biome-backdrop-shaderYes — via shader: in biome config
iconStatus icons (health, oxygen)default-icon-shaderNo
itemInventory and hotbar itemsdefault-item-shaderNo
break-particleBlock and static-object break particlesdefault-break-particle-shaderNo

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_main may 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 declares frame (FrameUniforms), lights, persistence, sky_light (group 0) and sector (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 edit fs_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.

LocationAttributeTypeDescription
0positionvec2<f32>Unit quad vertex position
1tex_coordsvec2<f32>Unit quad UV
2pos_pxvec2<f32>Screen-space top-left of the entity (pixels)
3frame_sizevec2<f32>Animation frame size (pixels)
4entity_posvec2<f32>World-space center position
5tex_indexi32Layer in the entity texture array (frame included)
6flip_xi321 = flip horizontally
7sky_lighti32Sky light level (0-15)
8rotationvec4<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.

LocationAttributeTypeDescription
0positionvec2<f32>Unit quad vertex position
1tex_coordsvec2<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:

LocationAttributeType
0positionvec2<f32>
1tex_coordsvec2<f32>
2pos_pxvec2<f32>
3frame_sizevec2<f32>
4entity_posvec2<f32>
5tex_indexi32
6flip_xi32
7sky_lighti32

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.

LocationAttributeTypeDescription
0positionvec2<f32>Unit quad vertex position
1tex_coordsvec2<f32>Unit quad UV
2posvec2<f32>World-space particle position
3velvec2<f32>Particle velocity
4lifef32Remaining lifetime in seconds (<= 0 = dead)
5particle_type_idu32Particle 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:

LocationAttributeType
0positionvec2<f32>
1tex_coordsvec2<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.

LocationAttributeTypeDescription
0positionvec2<f32>Unit quad vertex position
1tex_coordsvec2<f32>Unit quad UV
2posvec2<f32>Screen-space anchor of this icon instance
3icon_type_idu32Icon type (row in the atlas offsets texture)
4frame_indexu32CPU-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.

LocationAttributeTypeDescription
0positionvec2<f32>Unit quad vertex position
1tex_coordsvec2<f32>Unit quad UV
2posvec2<f32>Screen-space anchor of this item instance
3item_idu32Item 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).

LocationAttributeTypeDescription
0positionvec2<f32>Unit quad vertex position
1tex_coordsvec2<f32>Unit quad UV
2pos_pxvec2<f32>Screen-space top-left (pixels)
3frame_sizevec2<f32>Width/height (pixels)
4colorvec4<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);
}