Skip to content

Shader

The Shader object is the main building block in FragmentColor.

It takes a WGSL or GLSL shader source as input, parses it, validates it, and exposes the uniforms as keys.

Shader::new accepts a source string, a registry slug like "sdf2d/circle", an https:// URL, a local file path, or an array mixing any of those. Array parts are deduplicated by hash and concatenated in order, so you can pull pure helper functions from the public registry at https://fragmentcolor.org/shaders/ into your own shader without copy-pasting. Override the registry base with Shader::set_registry.

To draw your shader, you must use your Shader instance as input to a Renderer.

You can compose Shader instances into a Pass object to create more complex rendering pipelines.

You can also create renderings with multiple Render Passes by passing an array of Pass instances to Renderer::render.

Classic uniforms are declared with var<uniform> and can be nested structs/arrays. FragmentColor exposes every root and nested field as addressable keys using dot and index notation:

  • Set a field: shader.set("u.color", [r, g, b, a])
  • Index arrays: shader.set("u.arr[1]", value)
struct MyUniform {
color: vec4<f32>,
arr: array<vec4<f32>, 2>
};
@group(0) @binding(0) var<uniform> u: MyUniform;

FragmentColor handles std140-style 16-byte alignment for you, and large uniform blobs are pooled internally — there is nothing to configure.

Sampled textures and samplers are supported via texture_* and sampler declarations. You can bind a Texture object created by the Renderer directly to a texture uniform (e.g., shader.set("tex", &texture)); samplers are provided automatically:

  • If a texture is bound in the same group, the sampler defaults to that texture’s sampler.
  • Otherwise, a reasonable default sampler is used.
@group(0) @binding(0) var tex: texture_2d<f32>;
@group(0) @binding(1) var samp: sampler;

2D, 3D, cube, and array variants are all supported; the correct view dimension is inferred from the WGSL declaration. Integer textures map to Sint / Uint sample types; float textures use filterable float when the device allows it.

Writeable/readable image surfaces are supported via storage textures (texture_storage_*). Access flags are preserved from WGSL and mapped to the device:

  • read -> read-only storage access
  • write -> write-only storage access
  • read_write -> read+write (when supported)
@group(0) @binding(0) var img: texture_storage_2d<rgba8unorm, write>;

The declared storage format flows through to the binding layout untouched. You’re responsible for picking a format and access mode the adapter supports.

Structured buffers are supported via var<storage, read> or var<storage, read_write> and can contain nested structs/arrays. FragmentColor preserves and applies the WGSL access flags when creating binding layouts and setting visibility.

struct Buf { a: vec4<f32> };
@group(0) @binding(0) var<storage, read> ssbo: Buf;

Read-only buffers bind with read-only storage access; read_write allows writes where the device supports it. CPU-side updates use the same shader.set("path", value) API as uniforms, with array indexing (e.g. buf.items[2].v). Buffer byte spans are computed from the WGSL shape — arrays and structs honor stride and alignment automatically — and large buffers are pooled internally.

Push constants are supported with var<push_constants> in all platforms.

They will fallback to regular uniform buffers when:

  • push_constants are not natively supported (ex. on Web),
  • multiple push-constant roots are declared, or
  • the total push-constant size exceeds the device limit.

In fallback mode, push constants are rewritten as classic uniform buffers in a newly allocated bind group (one binding per push-constant root). The fallback group goes into the next free slot (max_existing_group + 1); FragmentColor does not check the device’s max_bind_groups limit, so a shader that already uses many bind groups and push constants can exceed it. Render pipelines fall back today; compute pipeline fallback may follow.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Shader, Renderer};
let shader = Shader::new(r#"
@vertex
fn vs_main(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0)
);
return vec4<f32>(pos[index], 0.0, 1.0);
}
@group(0) @binding(0)
var<uniform> resolution: vec2<f32>;
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0); // Red
}
"#)?;
// Set the "resolution" uniform
shader.set("resolution", [800.0, 600.0])?;
let res: [f32; 2] = shader.get("resolution")?;
let renderer = Renderer::new();
let target = renderer.create_texture_target([16, 16]).await?;
renderer.render(&shader, &target)?;
5 collapsed lines
assert_eq!(res, [800.0, 600.0]);
assert!(shader.list_uniforms().len() >= 1);
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> { pollster::block_on(run()) }

Creates a new Shader. The input is a single string, or an array of strings, of any of these shapes:

  • a raw WGSL source string
  • a registry slug like "sdf2d/circle", which pulls a helper function from the public registry at https://fragmentcolor.org/shaders/
  • an https:// URL pointing at a .wgsl file
  • a local file path ending in .wgsl, .glsl, .frag, or .vert

When you pass an array, parts are deduplicated by source hash and concatenated in order before validation, so you can compose pure helper functions from the registry with your own source.

If validation fails, the error message points at the line in the resulting WGSL. If validation passes, the shader is guaranteed to work on the GPU. All uniforms are initialized to zero.

GLSL is supported only as a single part (a .vert, .frag, or .glsl file). Mixing GLSL with other parts is rejected.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Shader;
let shader = Shader::new(r#"
@vertex
fn vs_main(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0)
);
return vec4<f32>(pos[index], 0.0, 1.0);
}
@group(0) @binding(0)
var<uniform> resolution: vec2<f32>;
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0); // Red
}
"#)?;
3 collapsed lines
assert!(shader.list_keys().len() >= 1);
Ok(())
}
use fragmentcolor::Shader;
let main = r#"
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
let p = array<vec2<f32>,3>(vec2f(-1.,-1.), vec2f(3.,-1.), vec2f(-1.,3.));
return vec4<f32>(p[i], 0.0, 1.0);
}
@fragment fn fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
let d = circle(pos.xy - vec2<f32>(400.0, 300.0), 100.0);
let n = simplex2(pos.xy * 0.01);
return vec4<f32>(vec3<f32>(step(0.0, d) + n * 0.1), 1.0);
}
"#;
let shader = Shader::new([
"sdf2d/circle", // pure function: fn circle(p: vec2<f32>, r: f32) -> f32
"noise/simplex2", // pure function: fn simplex2(v: vec2<f32>) -> f32
main,
])?;
1 collapsed line
Ok::<(), Box<dyn std::error::Error>>(())

Async constructor that returns a compiled Shader from one or more parts. Each part can be:

  • a raw WGSL source string
  • a registry slug like "sdf2d/circle"
  • an https:// URL pointing at a .wgsl file
  • a local file path ending in .wgsl, .glsl, .frag, or .vert (native platforms only)

Parts are deduplicated by source hash and concatenated in order before compilation. fetch is the async path used when any part needs network or file I/O. Shader::new covers the same shapes for callers that prefer a synchronous constructor.

PlatformSpellingAsync mechanism
Web (JS)await Shader.fetch(input)Promise via wasm_bindgen async
PythonShader.fetch(input)Blocks the calling thread via pollster::block_on
Swifttry await Shader.fetch(input)async throws via uniffi async method
KotlinShaderFetch(input)suspend fun via uniffi async method

Note (Swift / Kotlin): uniffi 0.31 does not support async constructors. The underlying uniffi binding is an async instance method; the Swift Shader.fetch(_:) and Kotlin ShaderFetch(...) wrappers handle the throw-away receiver internally so callers see a clean static factory.

1 collapsed line
async fn run() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Shader;
// Full registry URL.
let shader = Shader::fetch("https://fragmentcolor.org/shaders/sdf2d/circle.wgsl").await?;
// Equivalent shorthand using the registry slug.
let shader2 = Shader::fetch("sdf2d/circle").await?;
4 collapsed lines
let _ = (shader, shader2);
Ok(())
}
fn main() { let _ = pollster::block_on(run()); }

Sets the value of the uniform identified by the given key.

If the key does not exist or the value format is incorrect, the set method throws an exception. The shader remains valid, and if the exception is caught, the shader can still be used with the renderer.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Renderer, Shader};
let r = Renderer::new();
let shader = Shader::new(r#"
@group(0) @binding(0) var<uniform> resolution: vec2<f32>;
struct VOut { @builtin(position) pos: vec4<f32> };
@vertex fn vs_main(@builtin(vertex_index) i: u32) -> VOut {
var p = array<vec2<f32>, 3>(vec2<f32>(-1.,-1.), vec2<f32>(3.,-1.), vec2<f32>(-1.,3.));
var out: VOut;
out.pos = vec4<f32>(p[i], 0., 1.);
return out;
}
@fragment fn main() -> @location(0) vec4<f32> { return vec4<f32>(1.,0.,0.,1.); }
"#)?;
// Set scalars/vectors on declared uniforms
shader.set("resolution", [800.0, 600.0])?;
2 collapsed lines
Ok(())
}

Returns the current value of the uniform identified by the given key.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Shader;
let shader = Shader::default();
shader.set("resolution", [800.0, 600.0])?;
let res: [f32; 2] = shader.get("resolution")?;
3 collapsed lines
assert_eq!(res, [800.0, 600.0]);
Ok(())
}

Returns a list of all uniform names in the Shader (excluding struct fields).

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Shader;
let shader = Shader::default();
let list = shader.list_uniforms();
3 collapsed lines
assert!(list.contains(&"resolution".to_string()));
Ok(())
}

Returns a list of all keys in the Shader, including uniform names and struct fields using the dot notation.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Shader;
let shader = Shader::default();
let keys = shader.list_keys();
3 collapsed lines
assert!(keys.contains(&"resolution".to_string()));
Ok(())
}

Build a basic WGSL shader source from a single Vertex layout.

This inspects the vertex position dimensionality (2D or 3D) and optional properties. It generates a minimal vertex shader that consumes @location(0) position and a fragment shader that returns a flat color by default. If a color: vec4<f32> property exists, it is passed through to the fragment stage and used as output.

This is intended as a fallback and for quick debugging. Canonical usage is the opposite: write your own shader and then build Meshes that match it.

use fragmentcolor::{Shader, Vertex};
let vertex = Vertex::new([0.0, 0.0, 0.0]);
let shader = Shader::from_vertex(&vertex);
1 collapsed line
let _ = shader;

Build a basic WGSL shader source from the first vertex in a Mesh.

The resulting shader automatically adds the provided Mesh to its internal list of Meshes to render, so the user doesn’t need to call Shader::add_mesh manually.

This function uses the first Vertex to infer position dimensionality and optional properties.

It generates a minimal vertex shader that consumes @location(0) position and a fragment shader that returns a flat color by default. If a color: vec4<f32> property exists, it is passed through to the fragment stage and used as output.

If the Mesh has no vertices, a default shader is returned and a warning is logged. Because the default shader does not take any vertex inputs, it is compatible with any Mesh.

use fragmentcolor::{Mesh, Shader};
let mut mesh = Mesh::new();
mesh.add_vertex([0.0, 0.0, 0.0]);
let shader = Shader::from_mesh(&mesh);
1 collapsed line
let _ = shader;

Attach a Mesh to this Shader. The Renderer will draw all meshes attached to it (one draw call per mesh, same pipeline).

This method now validates that the mesh’s vertex/instance layout is compatible with the shader’s @location inputs and returns ResultResult<(), ShaderError>.

  • On success, the mesh is attached and will be drawn when this shader is rendered.
  • On mismatch (missing attribute or type mismatch), returns an error and does not attach.

Use Shader::validate_mesh for performing a compatibility check without attaching.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Shader, Mesh};
let shader = Shader::new(r#"
@vertex fn vs_main(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {
return vec4<f32>(pos, 1.0);
}
@fragment fn fs_main() -> @location(0) vec4<f32> { return vec4<f32>(1.,0.,0.,1.); }
"#)?;
let mesh = Mesh::new();
mesh.add_vertex([0.0, 0.0, 0.0]);
// Attach mesh to this shader (errors if incompatible)
shader.add_mesh(&mesh)?;
// Renderer will draw the mesh when rendering this pass.
// Each Shader represents a RenderPipeline or ComputePipeline
// in the GPU. Adding multiple meshes to it will draw all meshes
// and all its instances in the same Pipeline.
2 collapsed lines
Ok(())
}

Remove a single Mesh previously attached to this Shader. If the Mesh is attached multiple times, removes the first match.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Shader, Mesh};
let shader = Shader::new(r#"
struct VOut { @builtin(position) pos: vec4<f32> };
@vertex
fn vs_main(@location(0) pos: vec2<f32>) -> VOut {
var out: VOut;
out.pos = vec4<f32>(pos, 0.0, 1.0);
return out;
}
@fragment
fn fs_main(_v: VOut) -> @location(0) vec4<f32> { return vec4<f32>(1.0,0.0,0.0,1.0); }
"#)?;
let mesh = Mesh::new();
mesh.add_vertex([0.0, 0.0]);
shader.add_mesh(&mesh)?;
// Detach the mesh
shader.remove_mesh(&mesh);
2 collapsed lines
Ok(())
}

Remove multiple meshes from this Shader.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Shader, Mesh};
let shader = Shader::new(r#"
struct VOut { @builtin(position) pos: vec4<f32> };
@vertex
fn vs_main(@location(0) pos: vec2<f32>) -> VOut {
var out: VOut;
out.pos = vec4<f32>(pos, 0.0, 1.0);
return out;
}
@fragment
fn fs_main(_v: VOut) -> @location(0) vec4<f32> { return vec4<f32>(1.0,0.0,0.0,1.0); }
"#)?;
let m1 = Mesh::new();
m1.add_vertex([0.0, 0.0]);
let m2 = Mesh::new();
m2.add_vertex([0.5, 0.0]);
shader.add_mesh(&m1)?;
shader.add_mesh(&m2)?;
shader.remove_meshes([&m1, &m2]);
2 collapsed lines
Ok(())
}

Remove all meshes attached to this Shader.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Shader, Mesh};
let shader = Shader::new(r#"
struct VOut { @builtin(position) pos: vec4<f32> };
@vertex
fn vs_main(@location(0) pos: vec2<f32>) -> VOut {
var out: VOut;
out.pos = vec4<f32>(pos, 0.0, 1.0);
return out;
}
@fragment
fn fs_main(_v: VOut) -> @location(0) vec4<f32> { return vec4<f32>(1.0,0.0,0.0,1.0); }
"#)?;
let mesh = Mesh::new();
mesh.add_vertex([0.0, 0.0]);
shader.add_mesh(&mesh)?;
// Clear all
shader.clear_meshes();
2 collapsed lines
Ok(())
}

Validate that a Mesh is compatible with this Shader’s vertex inputs.

  • Checks presence and type for all @location(…) inputs of the vertex entry point.
  • Matches attributes in the following order:
    1. Instance attributes by explicit @location index (if the mesh has instances)
    2. Vertex attributes by explicit @location index (position is assumed at @location(0))
    3. Fallback by name (tries instance first, then vertex)
  • Returns Ok(()) when all inputs are matched with a compatible wgpu::VertexFormat; returns an error otherwise.
  • This method is called automatically when adding a Mesh to a Shader or Pass, so you usually don’t need to call it manually.
  • If the Shader has no @location inputs (fullscreen/builtin-only), attaching a Mesh is rejected.
  • This method does not allocate GPU buffers; it inspects CPU-side vertex/instance data only.
1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::{Shader, Pass, Mesh};
let shader = Shader::new(r#"
struct VOut { @builtin(position) pos: vec4<f32> };
@vertex fn vs_main(@location(0) pos: vec3<f32>) -> VOut {
var out: VOut;
out.pos = vec4<f32>(pos, 1.0);
return out;
}
@fragment fn fs_main(_v: VOut) -> @location(0) vec4<f32> { return vec4<f32>(1.,0.,0.,1.); }
"#)?;
let pass = Pass::from_shader("p", &shader);
let mesh = Mesh::new();
mesh.add_vertices([
[-0.5, -0.5, 0.0],
[ 0.5, -0.5, 0.0],
[ 0.0, 0.5, 0.0],
]);
shader.validate_mesh(&mesh)?; // Ok
pass.add_mesh(&mesh)?;
2 collapsed lines
Ok(())
}

Returns true if this Shader is a compute shader (has a compute entry point).

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Shader;
let shader = Shader::new(r#"
@compute @workgroup_size(1)
fn cs_main() { }
"#)?;
// Call the method
let is_compute = shader.is_compute();
4 collapsed lines
let _ = is_compute;
assert!(shader.is_compute());
Ok(())
}

Override the base URL used to resolve shader slugs (e.g. "sdf2d/circle").

When a slug is passed to Shader::new, it is expanded to:

<base_url>/<slug>.wgsl

The default base URL is https://fragmentcolor.org/shaders/, which serves the public shader registry. Override it to point at your own collection (a CDN, a local dev server, or a mirror) without changing the rest of your code.

The base may end with or without a trailing slash; both are normalised at lookup time.

This setting is process-wide. It applies to every subsequent call to Shader::new(...) until overridden again.

1 collapsed line
fn main() -> Result<(), Box<dyn std::error::Error>> {
use fragmentcolor::Shader;
// Point at your own mirror of the registry
Shader::set_registry("https://cdn.example.com/shaders/");
// Now the slug "sdf2d/circle" resolves to https://cdn.example.com/shaders/sdf2d/circle.wgsl
// (Skipping the actual fetch in this doctest)
3 collapsed lines
let _ = Shader::default();
Ok(())
}