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:
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:
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:
| Name | Meaning |
|---|---|
src | Sampled color of the primary image in the fragment body |
opacity | Combined DrawOption and ShaderOpts opacity |
screen | [2 / width, 2 / height, 1 / width, 1 / height] |
scale_factor | Current device pixel ratio |
user_globals | Sixteen vec4<f32> values populated from ShaderOpts |
tex, samp | Primary image texture and linear sampler |
extra_samp | Sampler for extra textures |
t_history, t_screen | Semantic aliases when their slots are configured |
t0 ... t3 | Generic 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:
| Group | Contents |
|---|---|
@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:
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:
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:
let source = spottedcat::model_shader_template();
let shader = spottedcat::register_model_shader(ctx, source);The source must define all three entry points:
vs_mainfor normal drawsvs_main_instancedfor instanced drawsfs_mainshared 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
| Group | Contents |
|---|---|
@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.