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.

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 using multiple Pass instances to a Frame object.

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;
  • Binding sizes are aligned to 16 bytes for layout correctness; this is handled automatically.
  • Large uniform blobs are uploaded via an internal buffer pool.

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 supported; the correct view dimension is inferred.
  • Integer textures map to Sint/Uint sample types; float textures use filterable float when possible.

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 is respected when creating the binding layout.
  • User must ensure the adapter supports the chosen format/access mode.

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 are bound with read-only storage access; read_write allows writes when supported.
  • Buffer byte spans are computed from WGSL shapes; arrays/structs honor stride and alignment.
  • CPU-side updates use the same set(“path”, value) and get_bytes(“path”) APIs as uniforms, with array indexing supported (e.g., buf.items[2].v).
  • Large buffers are uploaded via a dedicated storage buffer pool.

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, FragmentColor rewrites push constants into classic uniform buffers placed in a newly allocated bind group. In this case:

  • A bind group slot will be used by this fallback group (allocated as max existing group + 1).
  • There is no check for the max bind groups supported.
  • If you use push constants and many bind groups, very high group indices can exceed device limits.
  • Each push-constant root becomes one uniform buffer binding in the fallback group.
  • Currently, the fallback is applied for render pipelines; compute pipeline fallback may be added later.
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 instance from the given WGSL source string, file path, or URL.

GLSL is also supported if you enable the glsl feature. Shadertoy-flavored GLSL is supported if the shadertoy feature is enabled.

If the optional features are enabled, the constructor will try to automatically detect the shader type and parse it accordingly.

If an exception occurs during parsing, the error message will indicate the location of the error.

If the initial source validation passes, the shader is guaranteed to work on the GPU. All uniforms are initialized to their default zero values.

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(())
}

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(())
}