Skip to content

Custom Shaders

Spottedcat uses WGSL for both 2D image effects and 3D model materials. Template APIs are recommended for common effects; full WGSL registration remains available when the complete pipeline stages need to be controlled.

2D image shaders

Template API

ImageShaderTemplate injects the standard structs, uniforms, texture bindings, and vertex logic. Supply only the effect body:

rust
let shader = spottedcat::register_image_shader_template(
    ctx,
    spottedcat::ImageShaderTemplate::new()
        .with_extra_textures(true)
        .with_history_at(0)
        .with_screen_at(1)
        .with_texture_alias(2, "t_noise")
        .with_fragment_body(r#"
            let history = textureSample(t_history, extra_samp, in.uv);
            let background = textureSample(t_screen, extra_samp, in.uv);
            let noise = textureSample(t_noise, extra_samp, in.local_uv).r;
            let color = mix(src.rgb, history.rgb, noise);
            return vec4<f32>(color + background.rgb * 0.1, src.a * opacity);
        "#),
);

Bind extra textures by meaning rather than slot number at draw time:

rust
let bindings = spottedcat::ImageShaderBindings::new()
    .with_history()
    .with_screen()
    .with_image("t_noise", noise_image);

screen.draw_with_shader_bindings(
    ctx,
    image,
    shader,
    draw_options,
    shader_options,
    bindings,
);

The template makes these values available:

NameMeaning
srcSampled color of the primary image in the fragment body
opacityCombined DrawOption and ShaderOpts opacity
screen[2 / width, 2 / height, 1 / width, 1 / height]
scale_factorCurrent device pixel ratio
user_globalsSixteen vec4<f32> values populated from ShaderOpts
tex, sampPrimary image texture and linear sampler
extra_sampSampler for extra textures
t_history, t_screenSemantic aliases when their slots are configured
t0 ... t3Generic aliases for extra texture slots

screen is a snapshot of the current render target before the current draw batch. history is the target contents at the end of the previous frame, making it suitable for trails, accumulation, and other temporal effects.

Full image WGSL

Use ImageShaderDesc::from_wgsl(source).with_internal_prelude(true) to provide your own vs_main and fs_main while keeping the engine definitions and bindings. With the prelude disabled, the shader must implement the full contract.

The full bind-group layout is:

GroupContents
@group(0)Primary texture at binding 0 and sampler at binding 1
@group(1)Up to four extra textures at bindings 0–3 and sampler at binding 4
@group(2)ShaderOpts user globals
@group(3)Engine globals

Group indices after the primary texture move down when extra textures are disabled. The image vertex input is:

wgsl
struct VsIn {
    @builtin(vertex_index) vertex_index: u32,
    @location(0) pos: vec2<f32>,
    @location(1) rotation: f32,
    @location(2) size: vec2<f32>,
    @location(3) uv_rect: vec4<f32>,
};

3D model shaders

Template API

ModelShaderTemplate preserves Spottedcat's pipeline layout, depth state, vertex layout, and bind groups. It exposes slots for shared declarations and the fragment body:

rust
let shader = spottedcat::register_model_shader_template(
    ctx,
    spottedcat::ModelShaderTemplate::new()
        .with_shared(
            "fn tint(c: vec3<f32>) -> vec3<f32> { return c * vec3<f32>(0.8, 0.9, 1.0); }",
        )
        .with_fragment_body(
            "return vec4<f32>(tint(src.rgb), src.a * model_globals.extra.x);",
        ),
);

Full model WGSL

Call spottedcat::model_shader_template() for a complete source matching the current engine contract, edit it, then register it:

rust
let source = spottedcat::model_shader_template();
let shader = spottedcat::register_model_shader(ctx, source);

The source must define all three entry points:

  • vs_main for normal draws
  • vs_main_instanced for instanced draws
  • fs_main shared by both pipelines

The engine owns the pipeline and bind-group layouts, depth and target formats, primitive topology, culling, and ShaderOpts payload shape.

Model bind groups

GroupContents
@group(0)ModelGlobals, SceneGlobals, and sixteen vec4<f32> user globals
@group(1)Albedo, PBR, normal, AO, and emissive textures plus sampler
@group(2)Up to 256 bone matrices
@group(3)Shadow map, irradiance and prefiltered cubemaps, BRDF LUT, and samplers

The standard vertex input uses locations 0–4 and 9 for position, UV, normal, joints, weights, and tangent. Instanced input additionally uses locations 5–8 for the four instance-matrix rows. Both vertex entry points must return matching varyings for fs_main.

Examples

Released under the MIT OR Apache-2.0 License.