@motion-core/motion-gpu
Version:
Framework-agnostic WebGPU runtime for fullscreen WGSL shaders with explicit Svelte, React, and Vue adapter entrypoints.
424 lines (308 loc) • 10.7 kB
Markdown
<div align="center">
[](https://opensource.org/licenses/MIT)
[](https://svelte.dev)
[](https://react.dev)
[](https://vuejs.org)
[](https://gpuweb.github.io/gpuweb/)
[](https://www.typescriptlang.org)
[](https://www.npmjs.com/package/@motion-core/motion-gpu)
</div>
**Motion GPU** is a minimalist WebGPU framework for writing Shadertoy-style fullscreen shaders in pure WGSL. It provides a framework-agnostic core with Svelte 5, React, and Vue adapters for building fragment-driven GPU programs and multi-pass rendering pipelines. The framework includes a minimal runtime loop, scheduler, and render graph tailored specifically for fullscreen shader execution, focusing on a narrow GPU workflow rather than general-purpose 3D rendering.
# When to Use Motion GPU
Motion GPU is designed for applications where the entire scene is driven by fullscreen shaders.
Typical use cases include:
- Shadertoy-style GPU experiments
- Generative art
- Procedural textures
- Multi-pass post-processing pipelines
- GPU simulations
- Shader editors and live-coding tools
- Interactive visual experiments
If your application is primarily a fullscreen fragment shader pipeline, using a full 3D engine can add unnecessary complexity and bundle size.
# Why Not Use Three.js?
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 framework |
| Shader language | TSL / generated WGSL | Native WGSL |
| Bundle size | 186 kB (gzip) | 25.4 kB (gzip) |
| 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.
**Note:** Bundle size figures are based on measurements from https://bundlejs.com/
# Core Workflow
Motion GPU follows a simple three-step flow:
1. Define an immutable material with `defineMaterial(...)`.
2. Render it with `<FragCanvas />`.
3. Drive runtime updates with `useFrame(...)`, `useMotionGPU()`, and `useTexture(...)`.
# What This Package Includes
- 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:
- `ShaderPass`
- `BlitPass`
- `CopyPass`
- GPU compute passes:
- `ComputePass` — single-dispatch GPU compute workloads
- `PingPongComputePass` — iterative multi-step simulations with texture A/B alternation
- Named 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
# Requirements
- Svelte 5 is required only for the Svelte adapter entrypoints (`/svelte`, `/svelte/advanced`)
- React 18+ is required only for the React adapter entrypoints (`/react`, `/react/advanced`)
- Vue 3.5+ is required only for the Vue adapter entrypoints (`/vue`, `/vue/advanced`)
- A browser/runtime with WebGPU support
- Secure context (`https://` or `localhost`)
# Installation
```bash
npm i @motion-core/motion-gpu
```
# AI Documentation
MotionGPU documentation is also available for AI tools via [Context7](https://context7.com/motion-core/motion-gpu).
# Quick Start
## 1. Create a material and render it
```svelte
<!-- 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>
```
### React equivalent
```tsx
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>
);
}
```
## 2. Add animated uniforms via `useFrame`
```svelte
<!-- 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>
```
```svelte
<!-- Runtime.svelte -->
<script lang="ts">
import { useFrame } from '@motion-core/motion-gpu/svelte';
useFrame((state) => {
state.setUniform('uTime', state.time);
});
</script>
```
```tsx
import { useFrame } from '@motion-core/motion-gpu/react';
export function Runtime() {
useFrame((state) => {
state.setUniform('uTime', state.time);
});
return null;
}
```
## 3. Add a GPU compute pass
```svelte
<!-- 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>
```
```tsx
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>
);
}
```
# Core Runtime Model
## Material Phase (compile-time contract)
`defineMaterial(...)` validates and freezes:
- WGSL fragment source
- Uniform declarations
- Texture declarations
- Compile-time `defines`
- Shader `includes`
- Storage buffer declarations
A deterministic material signature is generated from resolved shader/layout metadata.
## Frame Phase (runtime updates)
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()`
## Renderer Phase
`FragCanvas` resolves material state, schedules tasks, and decides whether to render based on:
- `renderMode` (`always`, `on-demand`, `manual`)
- invalidation / advance state
- `autoRender`
# Hard Contracts and Validation Rules
These are enforced by runtime validation.
1. Material entrypoint must be:
```
fn frag(uv: vec2f) -> vec4f
```
2. `ShaderPass` fragment entrypoint must be:
```
fn shade(inputColor: vec4f, uv: vec2f) -> vec4f
```
3. `useFrame()` and `useMotionGPU()` must be called inside `<FragCanvas>` subtree.
4. You can only set uniforms/textures that were declared in `defineMaterial(...)`.
5. Uniform/texture/include/define names must match WGSL-safe identifiers:
```
[A-Za-z_][A-Za-z0-9_]*
```
6. `needsSwap: true` is valid only for `input: 'source'` and `output: 'target'`.
7. Render passes cannot read from `input: 'canvas'`.
8. `maxDelta` and profiling window must be finite and greater than `0`.
9. `ComputePass` shader must contain `@compute @workgroup_size(...)` and a `fn compute(...)` entrypoint with a `@builtin(global_invocation_id)` parameter.
10. `PingPongComputePass` `iterations` must be `>= 1`. The `target` must reference a texture declared with `storage: true` and explicit `width`/`height`.
11. Compute passes do not participate in render pass slot routing (no `input`/`output`/`needsSwap`).
12. Storage buffer `size` must be `> 0` and a multiple of 4. All storage buffers must be declared in `defineMaterial({ storageBuffers })`.
# Pipeline Rebuild Rules
## Rebuilds renderer
- Material signature changes (shader/layout/bindings)
- `outputColorSpace` changes
## Does not rebuild renderer
- Runtime uniform value changes
- Runtime texture source changes
- Clear color changes
- Canvas resize (resources are resized/reallocated as needed)
# Development
Run from `packages/motion-gpu`:
```bash
pnpm run build
pnpm run check
pnpm run test
pnpm run test:e2e
pnpm run lint
pnpm run format
```
## Performance
```bash
pnpm run perf:core
pnpm run perf:core:check
pnpm run perf:core:baseline
pnpm run perf:runtime
pnpm run perf:runtime:check
pnpm run perf:runtime:baseline
```
# License
This project is licensed under the MIT License.
See the `LICENSE` file for details.