Welcome to FragmentColor!
FragmentColor is a cross-platform GPU programming library for Rust, JavaScript, Python, Swift, and Kotlin.
It directly targets each platform’s native graphics API: Vulkan, Metal, DirectX, OpenGL, WebGL, or WebGPU.
See Platform Support for details.
The API encourages a simple shader composition workflow. You can use WGSL or GLSL shaders as the source of truth for visual consistency across platforms, while avoiding the boilerplate needed to make modern GPU rendering work.
Check the Documentation and the API Reference for more information.
Follow the project on GitHub to stay tuned on the latest updates.
Example
Section titled “Example”pip install fragmentcolor rendercanvas glfw
from fragmentcolor import Renderer, Shader, Pass, Frame from rendercanvas.auto import RenderCanvas, loop
# Initializes a renderer and a target compatible with the given canvas canvas = RenderCanvas(size=(800, 600)) renderer = Renderer() target = renderer.create_target(canvas)
# You can pass the shader as a source string, file path, or URL: circle = Shader("./path/to/circle.wgsl") triangle = Shader("https://fragmentcolor.org/shaders/triangle.wgsl") shader = Shader("""28 collapsed lines
struct VertexOutput { @builtin(position) coords: vec4<f32>, }
struct MyStruct { my_field: vec3<f32>, }
@group(0) @binding(0) var<uniform> my_struct: MyStruct;
@group(0) @binding(1) var<uniform> my_vec2: vec2<f32>;
@vertex fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> VertexOutput { const vertices = array( vec2( -1., -1.), vec2( 3., -1.), vec2( -1., 3.) ); return VertexOutput(vec4<f32>(vertices[in_vertex_index], 0.0, 1.0)); }
@fragment fn fs_main() -> @location(0) vec4<f32> { return vec4<f32>(my_struct.my_field, 1.0); } """)
# The library binds and updates the uniforms automatically shader.set("my_struct.my_field", [0.1, 0.8, 0.9]) shader.set("my_vec2", [1.0, 1.0])
# One shader is all you need to render renderer.render(shader, target)
# But you can also combine multiple shaders in a render Pass rpass = Pass("single pass") rpass.add_shader(circle) rpass.add_shader(triangle) rpass.add_shader(shader) renderer.render(rpass, target)
# Finally, you can combine multiple passes in a Frame frame = Frame() frame.add_pass(rpass) frame.add_pass(Pass("GUI pass")) renderer.render(frame, target)
# To animate, simply update the uniforms in a loop @canvas.request_draw def animate(): circle.set("position", [0.0, 0.0]) renderer.render(frame, target)
loop.run()
npm install fragmentcolor
import init, { Renderer, Shader, Pass, Frame } from "fragmentcolor";
async function start() {// Initializes the WASM moduleawait init();
// Initializes a renderer and a target compatible with the given canvasconst canvas = document.getElementById("my-canvas");const renderer = new Renderer();const target = await renderer.createTarget(canvas);
// You can pass the shader as a source string, file path, or URL:const circle = new Shader("./path/to/circle.wgsl");const triangle = new Shader("https://fragmentcolor.org/shaders/triangle.wgsl");const shader = new Shader(`28 collapsed lines
struct VertexOutput { @builtin(position) coords: vec4<f32>, }
struct MyStruct { my_field: vec3<f32>, }
@group(0) @binding(0) var<uniform> my_struct: MyStruct;
@group(0) @binding(1) var<uniform> my_vec2: vec2<f32>;
@vertex fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> VertexOutput { const vertices = array( vec2( -1., -1.), vec2( 3., -1.), vec2( -1., 3.) ); return VertexOutput(vec4<f32>(vertices[in_vertex_index], 0.0, 1.0)); }
@fragment fn fs_main() -> @location(0) vec4<f32> { return vec4<f32>(my_struct.my_field, 1.0); }`);
// The library binds and updates the uniforms automaticallyshader.set("my_struct.my_field", [0.1, 0.8, 0.9]);shader.set("my_vec2", [1.0, 1.0]);
// One shader is all you need to renderrenderer.render(circle, target);
// But you can also combine multiple shaders in a render Passconst rpass = new Pass("single pass");rpass.addShader(circle);rpass.addShader(triangle);rpass.addShader(shader);renderer.render(rpass, target);
// Finally, you can combine multiple passes in a Frameconst frame = new Frame();frame.addPass(rpass);frame.addPass(new Pass("GUI pass"));renderer.render(frame, target);
// To animate, simply update the uniforms in a loopfunction animate() { circle.set("position", [0.0, 0.0]); renderer.render(frame, target); requestAnimationFrame(animate);}animate();}
start();
use fragmentcolor::{Renderer, Shader, Pass, Frame};use pollster::block_on;
fn main() -> Result<(), Box<dyn std::error::Error>> { block_on(async { // Initializes a renderer and a target (offscreen example here) let renderer = Renderer::new(); let target = renderer.create_texture_target([800, 600]).await?;
// You can pass the shader as a source string, file path, or URL: let circle = Shader::new("./path/to/circle.wgsl")?; let triangle = Shader::new("https://fragmentcolor.org/shaders/triangle.wgsl")?; let wgsl = r#"28 collapsed lines
struct VertexOutput { @builtin(position) coords: vec4<f32>,}
struct MyStruct { my_field: vec3<f32>,}
@group(0) @binding(0)var<uniform> my_struct: MyStruct;
@group(0) @binding(1)var<uniform> my_vec2: vec2<f32>;
@vertexfn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> VertexOutput { const vertices = array( vec2( -1., -1.), vec2( 3., -1.), vec2( -1., 3.) ); return VertexOutput(vec4<f32>(vertices[in_vertex_index], 0.0, 1.0));}
@fragmentfn fs_main() -> @location(0) vec4<f32> { return vec4<f32>(my_struct.my_field, 1.0);}"#; let mut shader = Shader::new(wgsl)?;
// The library binds and updates the uniforms automatically shader.set("my_struct.my_field", [0.1f32, 0.8, 0.9])?; shader.set("my_vec2", [1.0f32, 1.0])?;
// One shader is all you need to render renderer.render(&shader, &target)?;
// But you can also combine multiple shaders in a render Pass let mut rpass = Pass::new("single pass"); rpass.add_shader(&circle); rpass.add_shader(&triangle); rpass.add_shader(&shader); renderer.render(&rpass, &target)?;
// Finally, you can combine multiple passes in a Frame let mut frame = Frame::new(); frame.add_pass(rpass); frame.add_pass(Pass::new("GUI pass")); renderer.render(&frame, &target)?;
// To animate, simply update the uniforms in a loop for i in 0..60 { circle.set("position", [0.0f32, i as f32])?; renderer.render(&frame, &target)?; }
Ok(()) })}
import FragmentColorimport SwiftUI
struct ShaderView: View {
@State private var mousePos = CGPoint(x: 0, y: 0) let renderer: FragmentColor.Renderer let target: FragmentColor.Target let circle: Shader
init() { let canvas = MetalCanvasView() // Your Metal-backed view
renderer = FragmentColor.Renderer() target = renderer.createTarget(canvas)
circle = Shader("circle.wgsl")! circle.setUniform("circle.radius", 200.0) circle.setUniform("circle.color", SIMD4<Float>(1.0, 0.0, 0.0, 0.8)) circle.setUniform("circle.border", 20.0) }
var body: some View { CanvasRepresentation() .gesture(DragGesture(minimumDistance: 0) .onChanged { value in mousePos = value.location } ) .onAppear(perform: animate) }
func animate() { circle.setUniform("circle.position", SIMD2<Float>( Float(mousePos.x), Float(mousePos.y) )) renderer.render(circle, to: target)
DispatchQueue.main.asyncAfter(deadline: .now() + 1/60) { animate() } }
}
struct CanvasRepresentation: NSViewRepresentable {
func makeNSView(context: Context) -> some NSView { let view = MetalCanvasView() view.wantsLayer = true return view }
func updateNSView(_ nsView: NSViewType, context: Context) {}
}
import androidx.compose.foundation.Canvasimport androidx.compose.foundation.gestures.detectDragGesturesimport androidx.compose.runtime.*import androidx.compose.ui.Modifierimport fragmentcolor.FragmentColorimport fragmentcolor.Shader
@Composablefun ShaderCanvas() { var mousePos by remember { mutableStateOf(Offset.Zero) } val renderer = remember { FragmentColor.Renderer() } val target = remember { renderer.createTarget(canvas) } val circle = remember { Shader("circle.wgsl") }.apply { setUniform("circle.radius", 200.0f) setUniform("circle.color", floatArrayOf(1.0f, 0.0f, 0.0f, 0.8f)) setUniform("circle.border", 20.0f) }
LaunchedEffect(Unit) { while (true) { withFrameNanos { circle.setUniform("circle.position", floatArrayOf( mousePos.x.toFloat(), mousePos.y.toFloat() )) renderer.render(circle, target) } } }
Canvas( modifier = Modifier.detectDragGestures { change, _ -> mousePos = change.position } ) { FragmentColor.init(this, renderer, target) }
}