A tiny WebGPU runtime for writing Shadertoy-style fullscreen shaders in pure WGSL.
@motion-core/motion-gpu ships a framework-agnostic core plus Svelte 5 and React adapters for building fullscreen shader pipelines using WebGPU and WGSL.
It provides a minimal runtime loop, scheduler, and render graph designed specifically for fragment-driven GPU programs.
Unlike general-purpose 3D engines, Motion GPU focuses on a very narrow problem: running fullscreen fragment shaders and multi-pass GPU pipelines.
Motion GPU is designed for applications where the entire scene is driven by fullscreen shaders.
Typical use cases include:
If your application is primarily a fullscreen fragment shader pipeline, using a full 3D engine can add unnecessary complexity and bundle size.
Three.js is a powerful general-purpose 3D engine. Motion GPU focuses on a much narrower problem: running fullscreen WGSL shader pipelines.
| Feature | Three.js | Motion GPU |
|---|---|---|
| Scope | Full 3D engine | Fullscreen shader runtime |
| Shader language | TSL / generated WGSL | Native WGSL |
| Bundle size | large | tiny (3.5-5x smaller) |
| Rendering model | Scene graph | GPU pipeline |
| Shader pipeline | materials | explicit passes |
| Multi-pass | possible but indirect | first-class |
| Shader debugging | generated shaders | direct WGSL |
Motion GPU is not a replacement for Three.js.
Instead, it is designed for cases where a full 3D engine would be unnecessary overhead.
Motion GPU follows a simple three-step flow:
defineMaterial(...).<FragCanvas />.useFrame(...), useMotionGPU(), and useTexture(...).Fullscreen WebGPU renderer for WGSL fragment shaders
Strict material contract and validation (fn frag(uv: vec2f) -> vec4f)
Runtime uniform and texture updates without rebuilding the pipeline
Frame scheduler with task ordering, stages, invalidation modes, diagnostics and profiling
Render graph with built-in post-process passes:
ShaderPassBlitPassCopyPassGPU compute passes:
ComputePass — single-dispatch GPU compute workloadsPingPongComputePass — iterative multi-step simulations with texture A/B alternationNamed render targets for multi-pass pipelines
Structured error normalization with built-in overlay UI and custom renderer support
Advanced runtime API for namespaced shared user context and scheduler presets
@motion-core/motion-gpu/svelte exposes the runtime API for Svelte:
FragCanvasdefineMaterialuseMotionGPUuseFrameuseTextureShaderPassBlitPassCopyPassComputePassPingPongComputePassAlso exports runtime/core types:
@motion-core/motion-gpu/svelte/advanced re-exports everything above, plus:
useMotionGPUUserContextsetMotionGPUUserContextapplySchedulerPresetcaptureSchedulerDebugSnapshot@motion-core/motion-gpu/react exposes the runtime API for React:
FragCanvasdefineMaterialuseMotionGPUuseFrameuseTextureShaderPassBlitPassCopyPassComputePassPingPongComputePassAlso exports runtime/core types:
@motion-core/motion-gpu/react/advanced re-exports everything above, plus:
useMotionGPUUserContextuseSetMotionGPUUserContextsetMotionGPUUserContextapplySchedulerPresetcaptureSchedulerDebugSnapshot@motion-core/motion-gpu (and explicit alias @motion-core/motion-gpu/core) exposes adapter-building primitives:
defineMaterialresolveMaterialcreateCurrentWritablecreateFrameRegistrycreateMotionGPURuntimeLooploadTexturesFromUrlstoMotionGPUErrorReportShaderPassBlitPassCopyPassComputePassPingPongComputePass@motion-core/motion-gpu/advanced (and explicit alias @motion-core/motion-gpu/core/advanced) re-exports core plus:
applySchedulerPresetcaptureSchedulerDebugSnapshot/svelte, /svelte/advanced)/react, /react/advanced)https:// or localhost)npm i @motion-core/motion-gpu
MotionGPU documentation is also available for AI tools via Context7.
<!-- App.svelte -->
<script lang="ts">
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu/svelte';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
return vec4f(uv.x, uv.y, 0.25, 1.0);
}
`
});
</script>
<div style="width: 100vw; height: 100vh;">
<FragCanvas {material} />
</div>
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu/react';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
return vec4f(uv.x, uv.y, 0.25, 1.0);
}
`
});
export function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<FragCanvas material={material} />
</div>
);
}
useFrame<!-- App.svelte -->
<script lang="ts">
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu/svelte';
import Runtime from './Runtime.svelte';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let wave = 0.5 + 0.5 * sin(motiongpuUniforms.uTime + uv.x * 8.0);
return vec4f(vec3f(wave), 1.0);
}
`,
uniforms: {
uTime: 0
}
});
</script>
<FragCanvas {material}>
<Runtime />
</FragCanvas>
<!-- Runtime.svelte -->
<script lang="ts">
import { useFrame } from '@motion-core/motion-gpu/svelte';
useFrame((state) => {
state.setUniform('uTime', state.time);
});
</script>
import { useFrame } from '@motion-core/motion-gpu/react';
export function Runtime() {
useFrame((state) => {
state.setUniform('uTime', state.time);
});
return null;
}
<!-- App.svelte -->
<script lang="ts">
import { FragCanvas, defineMaterial, ComputePass } from '@motion-core/motion-gpu/svelte';
import Runtime from './Runtime.svelte';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let idx = u32(uv.x * 255.0);
let particle = particles[idx];
return vec4f(particle.rgb, 1.0);
}
`,
storageBuffers: {
particles: { size: 4096, type: 'array<vec4f>', access: 'read-write' }
}
});
const simulate = new ComputePass({
compute: `
@compute @workgroup_size(64)
fn compute(@builtin(global_invocation_id) id: vec3u) {
let i = id.x;
let t = motiongpuFrame.time;
particles[i] = vec4f(sin(t + f32(i)), cos(t + f32(i)), 0.0, 1.0);
}
`,
dispatch: [16]
});
</script>
<FragCanvas {material} passes={[simulate]}>
<Runtime />
</FragCanvas>
import { FragCanvas, defineMaterial, ComputePass } from '@motion-core/motion-gpu/react';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let idx = u32(uv.x * 255.0);
let particle = particles[idx];
return vec4f(particle.rgb, 1.0);
}
`,
storageBuffers: {
particles: { size: 4096, type: 'array<vec4f>', access: 'read-write' }
}
});
const simulate = new ComputePass({
compute: `
@compute @workgroup_size(64)
fn compute(@builtin(global_invocation_id) id: vec3u) {
let i = id.x;
let t = motiongpuFrame.time;
particles[i] = vec4f(sin(t + f32(i)), cos(t + f32(i)), 0.0, 1.0);
}
`,
dispatch: [16]
});
export function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<FragCanvas material={material} passes={[simulate]} />
</div>
);
}
defineMaterial(...) validates and freezes:
definesincludesA deterministic material signature is generated from resolved shader/layout metadata.
Inside useFrame(...) callbacks you update per-frame values:
state.setUniform(name, value)state.setTexture(name, value)state.writeStorageBuffer(name, data, { offset? })state.readStorageBuffer(name) — returns Promise<ArrayBuffer>state.invalidate(token?)state.advance()FragCanvas resolves material state, schedules tasks, and decides whether to render based on:
renderMode (always, on-demand, manual)autoRenderThese are enforced by runtime validation.
fn frag(uv: vec2f) -> vec4f
ShaderPass fragment entrypoint must be:fn shade(inputColor: vec4f, uv: vec2f) -> vec4f
useFrame() and useMotionGPU() must be called inside <FragCanvas> subtree.
You can only set uniforms/textures that were declared in defineMaterial(...).
Uniform/texture/include/define names must match WGSL-safe identifiers:
[A-Za-z_][A-Za-z0-9_]*
needsSwap: true is valid only for input: 'source' and output: 'target'.
Render passes cannot read from input: 'canvas'.
maxDelta and profiling window must be finite and greater than 0.
ComputePass shader must contain @compute @workgroup_size(...) and a fn compute(...) entrypoint with a @builtin(global_invocation_id) parameter.
PingPongComputePass iterations must be >= 1. The target must reference a texture declared with storage: true and explicit width/height.
Compute passes do not participate in render pass slot routing (no input/output/needsSwap).
Storage buffer size must be > 0 and a multiple of 4. All storage buffers must be declared in defineMaterial({ storageBuffers }).
outputColorSpace changesRun from packages/motion-gpu:
bun run build
bun run check
bun run test
bun run test:e2e
bun run lint
bun run format
bun run perf:core
bun run perf:core:check
bun run perf:core:baseline
bun run perf:runtime
bun run perf:runtime:check
bun run perf:runtime:baseline
This project is licensed under the MIT License.
See the LICENSE file for details.