# ShaderPad llms-full This file aggregates the raw markdown content that powers the ShaderPad docs site. Prefer [llms.txt](https://misery.co/shaderpad/llms.txt) first for the smaller curated index. For local example source mirrors, use [examples/source](https://misery.co/shaderpad/examples/source/). --- ## Overview - Markdown: https://misery.co/shaderpad/index.md - Canonical HTML: https://misery.co/shaderpad/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/page.md ShaderPad handles the repetitive work required to render fragment shaders in the browser. It’s performant, flexible, and comes “batteries included” for most needs. ShaderPad has optional plugins — from PNG export to face/pose tracking — for more specific requirements. Simple, performant, and portable, ShaderPad lets you focus on writing GLSL. - [Installation](https://misery.co/shaderpad/docs/getting-started/installation/) - Install ShaderPad, start from a template, or set up an AI-assisted workflow. - [Quickstart](https://misery.co/shaderpad/docs/getting-started/quickstart/) - Get started by rendering your first simple example shaders. - [Examples](https://misery.co/shaderpad/docs/getting-started/examples/) - Browse runnable examples and source code for common ShaderPad patterns. - [API reference](https://misery.co/shaderpad/docs/api/shaderpad/) - Jump straight to options, methods, events, and utilities. ## Meet ShaderPad ShaderPad is a minimal fragment shader library for the web. It handles WebGL2 scaffolding, uniform and texture synchronization, resizing, history buffers, and other runtime plumbing. It’s performant by default and lets you focus on the creative parts of graphics programming. With a small footprint (6.1 kB gzipped), effects load quickly and run well on any device. And if you want face filters, pose-driven visuals, hand tracking, or object segmentation, MediaPipe-powered plugins give you one of the fastest ways to start building. ![Face mesh created with ShaderPad’s face plugin](https://misery.co/shaderpad/wink.png) ## What can I build with ShaderPad? - Create fullscreen interactive shaders in under 10 lines of JS code. - Add post-processing effects to existing `canvas`, `img` and `video` elements. - Efficiently create multi-pass graphics pipelines with minimal overhead. - Make your own face filters or pose detection apps like [Strange Camera](https://strange.cam). - Vibe code your first shader using the [AI entry points](https://misery.co/shaderpad/docs/getting-started/installation#using-ai/). ## Comparisons To Other Libraries [ThreeJS](https://threejs.org) is an incredible framework, but it’s nearly 30x the size of ShaderPad. If you want to use your GPU without a full 3D library, ShaderPad is a great choice. Hosted shader playgrounds like [ShaderToy](https://www.shadertoy.com) are perfect for sketches, but they keep your work locked into that platform. ShaderPad aims to provide a similar speed of iteration while giving you something you can drop into any project. ## Inspiration - [ShaderToy](https://www.shadertoy.com): The original shader playground. Still one of the coolest places on the Internet. - [ThreeJS](https://threejs.org): The most popular 3D library on the web by a landslide, for good reason. - [TWGL](https://twgljs.org/): A performant and unopinionated WebGL library for the browser. - [ShaderBooth](https://shaderbooth.com/): A fun, immediate, and inspiring way to learn and experiment with shaders. > **WebGL2 is required:** ShaderPad targets WebGL 2.0, which is [widely available across all major browsers.](https://caniuse.com/webgl2) ## Interested? Choose a path ### Start here - [Installation](https://misery.co/shaderpad/docs/getting-started/installation/) - [Quickstart](https://misery.co/shaderpad/docs/getting-started/quickstart/) - [Learning shaders](https://misery.co/shaderpad/docs/getting-started/learning-shaders/) - [Built-in inputs](https://misery.co/shaderpad/docs/core-concepts/built-in-inputs/) ### Camera effects and filters - [Webcam input](https://misery.co/shaderpad/docs/guides/webcam-input/) - [Textures](https://misery.co/shaderpad/docs/core-concepts/textures/) - [History](https://misery.co/shaderpad/docs/core-concepts/history/) - [Face plugin](https://misery.co/shaderpad/docs/plugins/face/) ### Advanced / multi-pass effects - [Chaining shaders](https://misery.co/shaderpad/docs/guides/chaining-shaders/) - [Format and precision](https://misery.co/shaderpad/docs/core-concepts/format-and-precision/) - [Performance](https://misery.co/shaderpad/docs/guides/performance/) - [API reference](https://misery.co/shaderpad/docs/api/shaderpad/) --- ## AI agent guide - Markdown: https://misery.co/shaderpad/docs/getting-started/ai-agent-guide.md - Canonical HTML: https://misery.co/shaderpad/docs/getting-started/ai-agent-guide/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/getting-started/ai-agent-guide/page.md > **Not for humans:** This page is for AI coding agents, not human-first onboarding. If you are using Codex, Cursor, Claude, ChatGPT, or another coding assistant, point the agent here; if you are reading the docs yourself, start with [Quickstart](https://misery.co/shaderpad/docs/getting-started/quickstart/) instead. Treat this page as the synthesis layer: use it for routing, defaults, and pattern selection, then open narrower API or plugin pages for exact option names. This page is intentionally table-heavy and recipe-heavy: pick the smallest pattern that fits, then open only the linked deep-dive pages you actually need. The machine-readable entry point is [`/llms.txt`](https://misery.co/shaderpad/llms.txt); if you want a structured page catalog, use [`/llms-index.json`](https://misery.co/shaderpad/llms-index.json); if you want the full docs corpus in one fetch, use [`/llms-full.txt`](https://misery.co/shaderpad/llms-full.txt); if you want local raw example source, use [`/examples/source`](https://misery.co/shaderpad/examples/source/). ## Decision Order 1. Start with one fragment shader and one `ShaderPad` instance. 2. For browser fullscreen work, default to `createFullscreenCanvas()` plus `autosize()`. 3. Add only the built-ins, uniforms, textures, history, or plugins the prompt actually needs. 4. Keep chained passes on one WebGL context whenever possible. 5. Before adding more passes, reduce resolution, history depth, texture bandwidth, or detector work. ## Source Of Truth Use the smallest source that answers the question precisely. | Need | Best source | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Fast routing, small ingest, top-level rules | [`/llms.txt`](https://misery.co/shaderpad/llms.txt) | | Structured page catalog for tools or retrieval pipelines | [`/llms-index.json`](https://misery.co/shaderpad/llms-index.json) | | Bulk ingest of the whole docs corpus | [`/llms-full.txt`](https://misery.co/shaderpad/llms-full.txt) | | Exact page source in raw markdown | the matching `.md` mirror, such as [`/docs/getting-started/ai-agent-guide.md`](https://misery.co/shaderpad/docs/getting-started/ai-agent-guide.md) | | Concrete code patterns and public example source | [`/examples/source`](https://misery.co/shaderpad/examples/source/) and the linked `/examples/source/*.ts` mirrors | | Exact option names, helper names, uniform names, and behavior | the specific API or plugin page | | Broader narrative and longer example walkthroughs | [`README.md`](https://misery.co/shaderpad/README.md) | If two sources overlap, prefer the narrower one for exact facts. For example, use the plugin page for helper function names and overloads, and use this page for routing, defaults, and recipe selection. ## Smallest Safe Browser Template Use this when the prompt says “fullscreen shader”, “browser demo”, or “animate a fragment shader”. ```javascript import ShaderPad from 'shaderpad'; import { createFullscreenCanvas } from 'shaderpad/util'; import autosize from 'shaderpad/plugins/autosize'; const fragmentShaderSrc = `#version 300 es precision highp float; in vec2 v_uv; uniform float u_time; out vec4 outColor; void main() { outColor = vec4(v_uv, 0.5 + 0.5 * sin(u_time), 1.0); }`; const shader = new ShaderPad(fragmentShaderSrc, { canvas: createFullscreenCanvas(), plugins: [autosize()], }); shader.play(); ``` Add `helpers()` only when you actually need helper GLSL like `fitCover()` or `historyZ()`. ## Hard Rules | Rule | Why | | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | Normal fragment shaders must include `#version 300 es`, `precision highp float;`, `in vec2 v_uv;`, and `out vec4 outColor;`. | These are the normal WebGL2 ShaderPad defaults. | | Declare only the built-in uniforms you actually use. | Avoid duplicate or stale declarations. | | Call `initializeUniform()` before `updateUniforms()`. | Updates do not create uniforms. | | Call `initializeTexture()` before `updateTextures()`. | Updates do not create textures. | | For live DOM textures like webcam or video, call `updateTextures()` every frame. | The GPU copy does not refresh itself. | | Use `play()` for normal animation, `step()` for manual advancement, and `draw()` only for render-only passes. | `draw()` does not advance time, frame count, or history. | | If you use `helpers()`, do not manually declare `u_resolution`. | The plugin injects it for you. | | MediaPipe plugins only read the texture named by `textureName`. | If the texture name does not match, the detector sees no source. | | Prefer one WebGL context for chained passes. | Cross-context chaining forces CPU readback and re-upload. | | For expensive intermediates, reduce resolution before adding more math. | This is usually the highest-value performance win. | | For masks, IDs, position maps, and other data textures, use the smallest format and `NEAREST` filtering that works. | This reduces bandwidth and preserves discrete values. | ## Use This API | Need | Use | Notes | | ------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------- | | One-time JS value in GLSL | `initializeUniform(name, type, value)` | Do this before the first update. | | Per-frame JS value in GLSL | `updateUniforms({...})` inside `play()` | Good for animation, UI controls, or sensor input. | | Static image, canvas, video, typed array, or another `ShaderPad` as input | `initializeTexture(name, source, options?)` | Another `ShaderPad` is the normal multi-pass path. | | Refresh a live texture | `updateTextures({...})` | Call every frame for webcam or video. | | Output history / feedback | `history` on the `ShaderPad` constructor | Sample with `u_history` plus `historyZ()` if using `helpers()`. | | History on one input texture | `initializeTexture(..., { history: N })` | Good for delayed webcam or video effects. | | Advance exactly one frame | `step(options?)` | Use this for manual pass ordering. | | Render current state without advancing time, frame, or history | `draw(options?)` | Good for pure display passes. | | Skip writing the current frame into history | `skipHistoryWrite: true` | Works for texture history, output history, and plugin history. | | Reset frame/time counters | `resetFrame()` | Useful when replay cadence changes. | | Reset frame/time counters and clear history | `reset()` | Useful after size or topology changes. | | Update only part of a typed-array texture | `updateTextures({ name: { data, width, height, x, y, isPartial: true } })` | Avoid full reuploads when a small region changes. | ## Pick The Smallest Pattern | Goal | Default move | Performance-first note | Good public starting point | | -------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | Fullscreen browser shader | `createFullscreenCanvas()` plus `autosize()` | Start with one visible canvas and one pass. | [`basic.ts`](https://misery.co/shaderpad/examples/source/basic.ts) | | Webcam or video compositing | One live texture, update it every frame | Use `fitCover()` instead of stretching when aspect ratios differ. | [`webcam.ts`](https://misery.co/shaderpad/examples/source/webcam.ts) | | Shader output feedback or trails | Enable `history`, add `helpers()`, sample with `historyZ()` | Keep history depth as small as the max delay really needs. | [`history.ts`](https://misery.co/shaderpad/examples/source/history.ts) | | Webcam time delays or RGB echoes | Put `{ history: N }` on the webcam texture | Compute `N` from the largest frame offset and stop there. | [`webcam-trails.ts`](https://misery.co/shaderpad/examples/source/webcam-trails.ts), [`webcam-channel-trails.ts`](https://misery.co/shaderpad/examples/source/webcam-channel-trails.ts) | | Face, mouth, eye, or eyebrow region effects | Use `face()` plus `faceAt()`, `inFace()`, or the specific region helpers | Use a region helper first, then only do expensive math inside the hit region. | [`face.ts`](https://misery.co/shaderpad/examples/source/face.ts), [`camo.ts`](https://misery.co/shaderpad/examples/source/camo.ts) | | Body region effects | Use `pose()` plus `poseAt()` or `inPose()` and `poseLandmark()` | `poseAt()` tells you which pose owns a pixel without manual mask decoding. | [`pose.ts`](https://misery.co/shaderpad/examples/source/pose.ts), [`camo.ts`](https://misery.co/shaderpad/examples/source/camo.ts) | | Generic segmentation recolor or cutout | Use `segmenter()` plus `segmentAt()` | Request `outputConfidenceMasks` only if the shader uses category or confidence. | [`segmenter.ts`](https://misery.co/shaderpad/examples/source/segmenter.ts) | | Gesture trails from landmarks | Put `history` on the plugin options, then use helper overloads with `framesAgo` | Before increasing history depth, try decimating writes with `skipHistoryWrite`. | [`finger-pens.ts`](https://misery.co/shaderpad/examples/source/finger-pens.ts) | | Two-pass compositing | Pass the first `ShaderPad` into the second as a texture | Keep the same source object and matching plugin config so detector work can be shared. | [`mediapipe-chaining.ts`](https://misery.co/shaderpad/examples/source/mediapipe-chaining.ts) | | Low-bandwidth preprocessing | Use an offscreen pass with a smaller canvas and a smaller format | `R8` plus `NEAREST` is often enough for masks or grayscale. | [`single-channel-textures.ts`](https://misery.co/shaderpad/examples/source/single-channel-textures.ts) | | Heavy blur, glow, simulation, or sorting pipelines | Build a composite object that owns several internal `ShaderPad` passes | Keep one context, shrink intermediates, cap iterations, and show only the final pass. | See the recipes below. | ## Vision Plugin Quick Map All four MediaPipe plugins require `npm install @mediapipe/tasks-vision`, and all four require the configured `textureName` to match the actual initialized live texture name. | Plugin | Use when | Fast region helper | Point helper | History support | | ------------- | ----------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------- | | `face()` | face masks, eyes, mouth, eyebrows, facial centers | `inFace()`, `faceAt()`, `eyeAt()`, `mouthAt()` | `faceLandmark()` | Set `options.history` if you need older face masks or landmarks. | | `pose()` | body ownership, body masks, torso/limb anchors | `inPose()`, `poseAt()` | `poseLandmark()` | Helper overloads accept `framesAgo` when `history` is enabled. | | `segmenter()` | foreground/background or category-based segmentation | `segmentAt()` | none | `segmentAt(..., framesAgo)` works when `history` is enabled. | | `hands()` | fingertips, handedness, hand centers, gesture anchors | none | `handLandmark()`, `isLeftHand()`, `isRightHand()` | Very powerful for trails, but keep history depth and write frequency under control. | ## Format And Filter Defaults | Texture role | Good default | | ----------------------------------------------------------- | ------------------------------------------- | | Ordinary color compositing | `RGBA8` with `LINEAR` | | Single-channel masks or grayscale | `R8` with `NEAREST` | | Float accumulation, halation, or trails | `RGBA16F` with `HALF_FLOAT` | | Scalar float data | `R32F` with `NEAREST` | | Integer IDs, coordinates, acceptance maps, or position maps | `R32UI`, `RG16I`, or `RG32I` with `NEAREST` | Use `RGBA32F` only when half-float or single-channel formats are not enough. ## Recipes These recipes are distilled from the public examples plus larger real-world ShaderPad pipelines. They focus on structure, not on app scaffolding or effect-specific math. ### Recolor Part Of A Segmenter Result Use this for “change hair color”, “tint only the foreground”, or “highlight segmented regions”. ```javascript import ShaderPad from 'shaderpad'; import segmenter from 'shaderpad/plugins/segmenter'; const shader = new ShaderPad(fragmentShaderSrc, { canvas, plugins: [ segmenter({ textureName: 'u_webcam', options: { outputConfidenceMasks: true, }, }), ], }); shader.initializeTexture('u_webcam', video); shader.play(() => { shader.updateTextures({ u_webcam: video }); }); ``` ```glsl uniform sampler2D u_webcam; uniform int u_numCategories; void main() { vec3 color = texture(u_webcam, v_uv).rgb; vec2 seg = segmentAt(v_uv); float confidence = seg.x; int category = int(floor(seg.y * float(u_numCategories - 1) + 0.5)); float isForeground = float(category != 0) * confidence; color = mix(color, vec3(0.0, 1.0, 0.4), isForeground * 0.35); outColor = vec4(color, 1.0); } ``` Use a custom `modelPath` when you need a domain-specific segmentation model. If you only need a flat mask and not category/confidence blending, avoid `outputConfidenceMasks` to keep the output smaller and simpler. ### Distort Or Recolor Relative To A Tracked Face Or Body Center Use this for “camouflage”, “magnify the face”, “pixelate only the head”, or “shift pixels around a body center”. ```glsl uniform sampler2D u_webcam; void main() { vec3 color = texture(u_webcam, v_uv).rgb; vec2 faceHit = faceAt(v_uv); if (faceHit.x > 0.0) { int i = int(faceHit.y); vec2 center = vec2(faceLandmark(i, FACE_LANDMARK_FACE_CENTER)); vec2 delta = v_uv - center; float lenDelta = max(length(delta), 1e-5); vec2 dir = delta / lenDelta; vec2 px = 1.0 / vec2(textureSize(u_webcam, 0)); vec2 sampleUV = v_uv + dir * 20.0 * px; color = texture(u_webcam, sampleUV).rgb; } outColor = vec4(color, 1.0); } ``` The structure matters more than the exact numbers: - Use `faceAt()` or `poseAt()` once to determine ownership. - Use `faceLandmark()` or `poseLandmark()` for the anchor point. - Convert pixel offsets to UV with `1.0 / textureSize(...)`. - Do not loop over every landmark per pixel unless the effect truly needs it. ### Delay Or Echo Live Input With Texture History Use this for RGB channel offsets, webcam echoes, row/column delays, or tiled time slices. ```javascript import ShaderPad from 'shaderpad'; import helpers from 'shaderpad/plugins/helpers'; const MAX_DELAY = 30; const shader = new ShaderPad(fragmentShaderSrc, { canvas, plugins: [helpers()], }); shader.initializeTexture('u_webcam', video, { history: MAX_DELAY }); shader.play(() => { shader.updateTextures({ u_webcam: video }); }); ``` ```glsl uniform highp sampler2DArray u_webcam; uniform int u_webcamFrameOffset; void main() { vec2 uv = fitCover(vec2(1.0 - v_uv.x, v_uv.y), vec2(textureSize(u_webcam, 0))); vec3 now = texture(u_webcam, vec3(uv, historyZ(u_webcam, u_webcamFrameOffset, 0))).rgb; vec3 past = texture(u_webcam, vec3(uv, historyZ(u_webcam, u_webcamFrameOffset, 12))).rgb; outColor = vec4(now.r, past.g, past.b, 1.0); } ``` Compute the history depth from the largest frame offset you actually sample. If the maximum delay is `12`, do not allocate `120`. ### Use Plugin History, But Write To It Sparingly Use this for landmark trails, gesture drawing, and any effect that samples older MediaPipe landmarks or masks. ```javascript import ShaderPad from 'shaderpad'; import helpers from 'shaderpad/plugins/helpers'; import hands from 'shaderpad/plugins/hands'; const HISTORY = 120; const shader = new ShaderPad(fragmentShaderSrc, { canvas, plugins: [ helpers(), hands({ textureName: 'u_webcam', options: { maxHands: 1, history: HISTORY }, }), ], }); shader.initializeTexture('u_webcam', video); shader.play((_time, frame) => { const options = { skipHistoryWrite: frame % 4 !== 0 }; shader.updateTextures({ u_webcam: video }, options); return options; }); ``` ```glsl vec2 current = vec2(handLandmark(0, INDEX_FINGER_TIP)); vec2 older = vec2(handLandmark(0, INDEX_FINGER_TIP, 8)); ``` If the effect still looks good with every fourth or fifth frame stored, do that before increasing history depth. ### Share One Detector Across Multiple Passes Use this when multiple passes need the same face, pose, hands, or segmenter data. ```javascript const passA = new ShaderPad(fragmentA, { canvas: sharedCanvas, plugins: [face({ textureName: 'u_webcam', options: { maxFaces: 3 } })], }); const passB = new ShaderPad(fragmentB, { canvas: sharedCanvas, plugins: [face({ textureName: 'u_webcam', options: { maxFaces: 3 } })], }); passA.initializeTexture('u_webcam', video); passB.initializeTexture('u_webcam', video); passB.initializeTexture('u_passA', passA); passB.play(() => { passA.updateTextures({ u_webcam: video }); passB.updateTextures({ u_webcam: video }); passA.step(); passB.updateTextures({ u_passA: passA }); }); ``` Detector batching works best when these all match across passes: - plugin type - `textureName` - detector setup options - underlying source object, such as the same `HTMLVideoElement` ### Run An Expensive Blur Or Composite Pipeline Use this for masked blur, bloom, glow, or any effect where most of the cost is in intermediate convolution passes. ```javascript const blurDown = new ShaderPad(blurDownFrag, { canvas: { width: 512, height: 512 }, plugins: [helpers()], }); const blurUp = new ShaderPad(blurUpFrag, { canvas: { width: 512, height: 512 }, plugins: [helpers()], }); const main = new ShaderPad(compositeFrag, { canvas, plugins: [helpers(), autosize(), segmenter({ textureName: 'u_webcam' })], }); blurDown.initializeTexture('u_input', video); blurUp.initializeTexture('u_input', blurDown); main.initializeTexture('u_blurred', blurUp); main.initializeTexture('u_webcam', video); main.play(() => { main.updateTextures({ u_webcam: video }); blurDown.updateTextures({ u_input: video }); blurDown.step(); blurUp.updateTextures({ u_input: blurDown }); blurUp.step(); main.updateTextures({ u_blurred: blurUp }); }); ``` Performance rules for this pattern: - Keep intermediates smaller than the final output. - Keep all passes on one context if you can; if different pass sizes make separate offscreen passes simpler, make those intermediates much smaller. - Only the final composite needs to be visible. - If resize matters, resize the intermediate canvases too. - Put history only on the pass or plugin that really needs it. ### Run An Iterative Data Pipeline Use this for field simulations, iterative refinement, or GPU data-map pipelines such as sorting, diffusion, or acceptance maps. ```javascript const chainOpts = { canvas: sharedCanvas, plugins: [helpers()], minFilter: 'NEAREST', magFilter: 'NEAREST', }; const score = new ShaderPad(scoreFrag, { ...chainOpts, internalFormat: 'RG16I', format: 'RG_INTEGER', type: 'SHORT', }); const state = new ShaderPad(stateFrag, { ...chainOpts, history: 1, internalFormat: 'R32UI', format: 'RED_INTEGER', type: 'UNSIGNED_INT', }); const output = new ShaderPad(outputFrag, { canvas, plugins: [helpers(), autosize()], }); score.initializeTexture('u_state', state); state.initializeTexture('u_score', score); output.initializeTexture('u_state', state); output.play(() => { for (let i = 0; i < iterationsPerFrame; i++) { score.updateTextures({ u_state: state }); score.step(); state.updateTextures({ u_score: score }); state.step(); } output.updateTextures({ u_state: state }); }); ``` Use this structure when the effect is fundamentally “update a data structure, then visualize it”. - Use integer or single-channel formats for non-color data. - Use `NEAREST` for data maps. - Keep a fixed iteration budget per frame. - If resolution, pixel size, or topology changes invalidate the state, call `resetFrame()` or `reset()`. - When possible, run the solver at a smaller resolution than the final composite. ### Turn Plugin Results Into A CPU-Drawn Overlay Texture Use this when the plugin gives you landmarks, but the cheapest or simplest overlay is easier to rasterize in JS than in GLSL. ```javascript const overlay = document.createElement('canvas'); overlay.width = overlay.height = 1024; const ctx = overlay.getContext('2d'); const shader = new ShaderPad(fragmentShaderSrc, { canvas, plugins: [face({ textureName: 'u_webcam', options: { maxFaces: 3 } })], }); shader.initializeTexture('u_overlay', overlay); shader.on('face:result', result => { drawFaceMeshToCanvas(ctx, result.faceLandmarks); shader.updateTextures({ u_overlay: overlay }); }); ``` This is often better than building a second detector or encoding a lot of 2D line logic in the fragment shader. The detector already ran; reuse the result event. ## Common Failure Modes | Symptom | Usually this means | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Shader compiles badly | Missing `#version 300 es`, missing `out vec4 outColor;`, or duplicate `u_resolution` because `helpers()` already injected it. | | Effect renders black | `play()` was never called, a live texture was never updated, or the shader is sampling the wrong texture name. | | History-dependent effect stays frozen | The pass is using `draw()` instead of `play()` or `step()`, or history writes are being skipped. | | Vision plugin appears inactive | The `@mediapipe/tasks-vision` package is missing, `textureName` does not match the real texture, or the live texture is not being updated. | | Multi-pass pipeline is unexpectedly slow | Passes are on different WebGL contexts, intermediates are full resolution, or data textures use bigger formats than necessary. | | Landmark or mask history is too expensive | History depth is too large, or frames are being written more often than the effect needs. | ## Recommended Reading Order When you need more than this page, pull context in this order: 1. [Quickstart](https://misery.co/shaderpad/docs/getting-started/quickstart/) 2. [ShaderPad API](https://misery.co/shaderpad/docs/api/shaderpad/) 3. [Methods](https://misery.co/shaderpad/docs/api/methods/) 4. [Built-in inputs](https://misery.co/shaderpad/docs/core-concepts/built-in-inputs/) 5. [History](https://misery.co/shaderpad/docs/core-concepts/history/), if the effect depends on previous frames 6. [Performance](https://misery.co/shaderpad/docs/guides/performance/), if the task is multi-pass, high-resolution, or MediaPipe-heavy 7. [Chaining shaders](https://misery.co/shaderpad/docs/guides/chaining-shaders/), if the effect is multi-pass 8. The specific plugin page, but only if you are actually using that plugin --- ## Installation - Markdown: https://misery.co/shaderpad/docs/getting-started/installation.md - Canonical HTML: https://misery.co/shaderpad/docs/getting-started/installation/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/getting-started/installation/page.md The core `ShaderPad` bundle is currently 6.1 kB gzipped. ## Start From A Template If you want to create a new project, use one of the provided starter templates: ```bash npm create shaderpad@latest ``` The CLI will prompt you to choose a starter app interactively. You can also start exparimenting with the templates in your browser: - [Open the basic starter (TS) in StackBlitz](https://stackblitz.com/fork/github/miseryco/shaderpad/tree/main/packages/create-shaderpad/template-basic-ts?title=ShaderPad%20Basic%20TypeScript) - [Open the basic starter (JS) in StackBlitz](https://stackblitz.com/fork/github/miseryco/shaderpad/tree/main/packages/create-shaderpad/template-basic-js?title=ShaderPad%20Basic%20JavaScript) - [Open the face filter starter (TS) in StackBlitz](https://stackblitz.com/fork/github/miseryco/shaderpad/tree/main/packages/create-shaderpad/template-face-ts?title=ShaderPad%20Face%20TypeScript) - [Open the face filter starter (JS) in StackBlitz](https://stackblitz.com/fork/github/miseryco/shaderpad/tree/main/packages/create-shaderpad/template-face-js?title=ShaderPad%20Face%20JavaScript) ## Install Into An Existing Project To add ShaderPad to a project you already have: ```bash npm install shaderpad ``` > **Peer dependency requirements:** If you wish to use the tracking plugins ([face](https://misery.co/shaderpad/docs/plugins/face/), [pose](https://misery.co/shaderpad/docs/plugins/pose/), [hands](https://misery.co/shaderpad/docs/plugins/hands/), or [segmenter](https://misery.co/shaderpad/docs/plugins/segmenter/)), you’ll also need to install MediaPipe as a peer dependency: ```bash npm install @mediapipe/tasks-vision ``` ## Using AI If you are working with Claude, Cursor, Codex, or another coding assistant, the best documentation entry point for agents is the [AI agent guide](https://misery.co/shaderpad/docs/getting-started/ai-agent-guide/). That page is written for AI assistants, not humans, and it links to smaller machine-readable sources and example mirrors. If your tool accepts extra context URLs, these are the most useful: - [`/llms.txt`](https://misery.co/shaderpad/llms.txt) for the small discovery layer - [`/llms-index.json`](https://misery.co/shaderpad/llms-index.json) for a structured page catalog - [`/llms-full.txt`](https://misery.co/shaderpad/llms-full.txt) for a one-fetch docs corpus - [`/examples/source`](https://misery.co/shaderpad/examples/source/) for raw example source mirrors The smoothest workflow is usually: 1. Start a local project with `npm create shaderpad@latest`. 2. Point your agent to the [AI agent guide](https://misery.co/shaderpad/docs/getting-started/ai-agent-guide/). 3. Tell it which starter you chose and what you want to build. 4. Ask for the smallest working version first, then iterate. Good prompts to start with: - “Read the [ShaderPad AI agent guide](https://misery.co/shaderpad/docs/getting-started/ai-agent-guide/) and turn the basic starter into an animated shader that looks like a holographic orb.” - “Using the basic ShaderPad starter, build an animated pattern using Cairo tiling.” - “Using the face filter starter, make a face-tracked pixelation effect. Keep the first version as small and readable as possible.” If you are working locally with an editor agent, it also helps to tell it to start with one fragment shader, one `ShaderPad` instance, `createFullscreenCanvas()` plus `autosize()`, and `shader.play()` for animation. Those defaults line up with the rest of the docs and tend to produce the cleanest first pass. --- ## Learning shaders - Markdown: https://misery.co/shaderpad/docs/getting-started/learning-shaders.md - Canonical HTML: https://misery.co/shaderpad/docs/getting-started/learning-shaders/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/getting-started/learning-shaders/page.md ShaderPad can help you build shader-driven projects quickly, but it does not replace the broader process of learning how shaders work. If you are new to this space, the fastest path is usually a mix of play, fundamentals, and following people whose work excites you. If you are already confident programming in WebGL2, you can probably skip this page. ## Click Around ShaderToy If you have not already, go to [ShaderToy](https://www.shadertoy.com/results?query=&sort=hot) and click around for 15 minutes. You are almost guaranteed to find a few inspiring projects, and you will come back with a much better feel for what shaders can do. Don’t worry yet if you don’t understand every line. At this stage, it is more useful to build taste and curiosity than to force all the technical details at once. ## Start With Fundamentals When you are ready to get technical, read [The Book of Shaders](https://thebookofshaders.com). It is one of the best introductions to the mental models behind fragment shaders, shaping functions, color, noise, and procedural composition. ## Get Inspired Find graphics programmers and shader artists online and pay attention to how they explain their work, what kinds of effects they build, and what tools they use. Some examples worth exploring: - [Inigo Quilez](https://iquilezles.org/articles/distfunctions/) - [Gary “Shane” Warne](https://www.shadertoy.com/user/Shane) - [FrostKiwi](https://blog.frost.kiwi/dual-kawase/) - [Liam Egan](https://codepen.io/shubniggurath) - [kishimisu](https://www.instagram.com/kishimisu/) - [The Art of Code](https://www.youtube.com/@TheArtofCodeIsCool) These are only starting points. Find more people whose work speaks to you, and build your own list over time. Having a strong personal reference library is one of the best ways to keep improving. ## When To Use ShaderPad Use ShaderPad when you want to move from inspiration into a real project quickly. The wider shader community can help sharpen your visual taste, technical intuition, and sense of what is possible. --- ## Quickstart - Markdown: https://misery.co/shaderpad/docs/getting-started/quickstart.md - Canonical HTML: https://misery.co/shaderpad/docs/getting-started/quickstart/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/getting-started/quickstart/page.md If you want the fastest way to start a new ShaderPad project, run `npm create shaderpad@latest` locally or open the [basic starter in StackBlitz](https://stackblitz.com/fork/github/miseryco/shaderpad/tree/main/packages/create-shaderpad/template-basic-ts?title=ShaderPad%20Basic%20TypeScript). For more setup options, including how to work with AI tooling, see [Installation](https://misery.co/shaderpad/docs/getting-started/installation/). --- Let’s start with a simple animated ShaderPad that tracks your cursor: ```javascript import ShaderPad from 'shaderpad'; const fragmentShaderSrc = `#version 300 es precision highp float; in vec2 v_uv; uniform float u_time; uniform vec2 u_cursor; uniform vec2 u_resolution; out vec4 outColor; void main() { vec2 uv = (v_uv - u_cursor) * 2.0; uv.x *= u_resolution.x / u_resolution.y; float glow = 0.25 / max(length(uv), 0.001); vec3 color = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0.0, 2.0, 4.0)); outColor = vec4(color * glow, 1.0); }`; const canvas = document.createElement('canvas'); const shader = new ShaderPad(fragmentShaderSrc, { canvas }); shader.play(); ``` _Interactive preview omitted in llms-full._ ## Required GLSL Declarations In your fragment shader, you must declare `in vec2 v_uv` (normalized shader coordinates) and `out vec4 outColor` (the output color). You can also declare any of the [built-in uniforms](https://misery.co/shaderpad/docs/core-concepts/built-in-inputs/) like `u_time`, `u_cursor`, or `u_resolution`, as shown in the shader above. The complete built-in uniform list is documented in the [Uniforms API reference](https://misery.co/shaderpad/docs/api/uniforms/). ## Make It Fullscreen To make a fullscreen ShaderPad, use the `createFullscreenCanvas` utility along with the `autosize` plugin. ```javascript import ShaderPad from 'shaderpad'; import { createFullscreenCanvas } from 'shaderpad/util'; import autosize from 'shaderpad/plugins/autosize'; const fragmentShaderSrc = `#version 300 es precision highp float; in vec2 v_uv; out vec4 outColor; void main() { outColor = vec4(v_uv, 0.0, 1.0); }`; const shader = new ShaderPad(fragmentShaderSrc, { canvas: createFullscreenCanvas(), plugins: [autosize()], }); shader.play(); ``` ## Add Dynamic Data You can synchronize dynamic data from JavaScript to your shader through custom uniforms and textures, and update them with a callback passed to `play()`. ```javascript import ShaderPad from 'shaderpad'; import { createFullscreenCanvas } from 'shaderpad/util'; import autosize from 'shaderpad/plugins/autosize'; const fragmentShaderSrc = `#version 300 es precision highp float; in vec2 v_uv; uniform sampler2D u_video; uniform float u_playhead; out vec4 outColor; void main() { vec4 color = texture(u_video, v_uv); color.rgb = mix(color.rgb, 1.0 - color.rgb, u_playhead); outColor = color; }`; const video = document.querySelector('video'); video.play(); // To support autoplay, videos require a `muted` attribute const shader = new ShaderPad(fragmentShaderSrc, { canvas: createFullscreenCanvas(), plugins: [autosize()], }); shader.initializeUniform('u_playhead', 'float', 0.0); shader.initializeTexture('u_video', video); shader.play(() => { shader.updateUniforms({ u_playhead: video.currentTime / video.duration || 0 }); shader.updateTextures({ u_video: video }); }); ``` These examples show some very basic applications of ShaderPad to get you started quickly. Continue reading for a closer look at how it works. --- ## Built-in inputs - Markdown: https://misery.co/shaderpad/docs/core-concepts/built-in-inputs.md - Canonical HTML: https://misery.co/shaderpad/docs/core-concepts/built-in-inputs/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/core-concepts/built-in-inputs/page.md ShaderPad programs come with a few built-in inputs for convenience. To use them, add the corresponding declarations at the top of your fragment shader. If your program does not use a particular uniform, ShaderPad will automatically free up the resources used to manage it. ## `v_uv` ```glsl in vec2 v_uv; ``` - Range: `0.0` to `1.0` - Origin: bottom-left in shader space - Use it for normalized addressing and fullscreen effects ## Built-in Uniforms | Name | Type | Meaning | | -------------- | ------- | --------------------------------------------------------------------- | | `u_time` | `float` | Elapsed time in seconds. | | `u_frame` | `int` | Frame counter. | | `u_resolution` | `vec2` | Drawing buffer size in pixels. | | `u_cursor` | `vec2` | Normalized cursor position from bottom-left (0.0) to top-right (1.0). | | `u_click` | `vec3` | Normalized position of last click plus boolean pressed state. | More complete documentation is available in the [Uniforms API reference](https://misery.co/shaderpad/docs/api/uniforms/). > **Note about u_resolution:** If you use the [helpers plugin](https://misery.co/shaderpad/docs/plugins/helpers/), it injects the `u_resolution` declaration for you. Do not declare `u_resolution` manually in that case. ## Example This example uses `u_time`, `u_cursor`, `u_click`, and `u_resolution` together with one custom uniform: ```javascript import ShaderPad from 'shaderpad'; const fragmentShaderSrc = `#version 300 es precision highp float; // Built-in variables. in vec2 v_uv; uniform float u_time; uniform vec2 u_resolution; uniform vec2 u_cursor; uniform vec3 u_click; out vec4 outColor; // Custom uniform. uniform vec3 u_cursorColor; void main() { vec2 uv = v_uv * u_resolution; vec2 dotGrid = mod(uv, 50.0) - 25.0; float dotDist = length(dotGrid); float dot = step(dotDist, 5.0); vec2 clickPos = u_click.xy; float isClicked = u_click.z; float cursorDist = distance(uv, u_cursor * u_resolution); float clickDist = distance(uv, clickPos * u_resolution); float cursorRadius = 25.0 + sin(u_time * 5.0) * 5.0 + isClicked * 15.0; float cursor = step(cursorDist, cursorRadius); float click = step(clickDist, 15.0); vec3 color = mix(vec3(0.0, 0.0, 1.0), vec3(1.0), dot); color = mix(color, u_cursorColor, cursor); color = mix(color, vec3(1.0), click); outColor = vec4(color, 1.0); }`; const canvas = document.createElement('canvas'); document.body.append(canvas); const shader = new ShaderPad(fragmentShaderSrc, { canvas }); const getColor = time => [time, time + (Math.PI * 2) / 3, time + (Math.PI * 4) / 3].map(x => 0.5 + 0.5 * Math.sin(x)); shader.initializeUniform('u_cursorColor', 'float', getColor(0)); shader.play(time => { shader.updateUniforms({ u_cursorColor: getColor(time) }); }); ``` _Interactive preview omitted in llms-full._ --- ## Canvas and input - Markdown: https://misery.co/shaderpad/docs/core-concepts/canvas-and-input.md - Canonical HTML: https://misery.co/shaderpad/docs/core-concepts/canvas-and-input/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/core-concepts/canvas-and-input/page.md ShaderPad can render into an existing canvas, or create an offscreen canvas for headless use. ## Canvas Options ```javascript const shader = new ShaderPad(fragmentShaderSrc, { canvas }); ``` Valid values for `canvas`: - `HTMLCanvasElement` - `OffscreenCanvas` - `{ width, height }` creates a headless `OffscreenCanvas` - `null` creates a 1 × 1 `OffscreenCanvas` that you can resize later ## Fullscreen Setup Use the utility helper if you want a simple fullscreen canvas. For responsive resolution, combine it with the autosize plugin: ```javascript import ShaderPad from 'shaderpad'; import { createFullscreenCanvas } from 'shaderpad/util'; import autosize from 'shaderpad/plugins/autosize'; const canvas = createFullscreenCanvas(); const shader = new ShaderPad(fragmentShaderSrc, { canvas, plugins: [autosize()] }); ``` ## Cursor Tracking `cursorTarget` controls how `u_cursor` and `u_click` are normalized. ```javascript const shader = new ShaderPad(fragmentShaderSrc, { canvas, cursorTarget: window, }); ``` Use `window` when the shader responds to viewport-wide input rather than canvas-local input. You can pass any DOM element to track input within that specific container. ## Resolution Changes > **Resolution vs. Rendered Size:** An HTML canvas has both a rendered size controlled by CSS, and a drawing-buffer resolution controlled by its `width` and `height` attributes. That separation can be useful, so ShaderPad does not automatically adjust its resolution to match the rendered size. If you want to ensure resolution always matches the rendered size, use the [autosize plugin](https://misery.co/shaderpad/docs/plugins/autosize/). If your canvas has a dynamic size, you will want to use the [autosize plugin](https://misery.co/shaderpad/docs/plugins/autosize/) to sync the canvas `width` and `height` to its rendered size. For instance, `createFullscreenCanvas()` creates a canvas that fills the viewport, but without `autosize`, it will not automatically adjust resolution when the viewport size changes. ShaderPad automatically updates its internal textures and render targets to match the canvas's resolution, so updating the `width` and `height` attributes will also update the GL drawing buffer size, history textures, and other internal state. If you want to run a callback after resolution changes, use `shader.on('updateResolution', callback)`. If you use the `autosize` plugin, it emits an `autosize:resize` event when the rendered size changes. --- ## Format and precision - Markdown: https://misery.co/shaderpad/docs/core-concepts/format-and-precision.md - Canonical HTML: https://misery.co/shaderpad/docs/core-concepts/format-and-precision/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/core-concepts/format-and-precision/page.md ShaderPad exposes its underlying WebGL2 texture format controls for advanced use cases. This is useful when you want compact buffers, integer pipelines, or high-precision feedback. ## Option Defaults Constructor options configure ShaderPad's internal render target and output history. Below are the available options, along with their default values: - `internalFormat`: `RGBA8` - `format`: `RGBA` - `type`: `UNSIGNED_BYTE` - `minFilter`: `LINEAR` - `magFilter`: `LINEAR` - `wrapS`: `CLAMP_TO_EDGE` - `wrapT`: `CLAMP_TO_EDGE` Texture options on `initializeTexture()` use the same fields, plus `preserveY: false`. ## Constructor Options When used in the constructor, these options define how ShaderPad stores: - The displayed canvas - The history texture when `history` is enabled - Chained passes when another `ShaderPad` samples this instance ```javascript const shader = new ShaderPad(fragmentShaderSrc, { canvas, history: 24, internalFormat: 'RGBA32F', type: 'FLOAT', minFilter: 'NEAREST', magFilter: 'NEAREST', }); ``` > **Canvas Color Precision:** Due to browser restrictions, a rendered canvas will not display color precision beyond its native 8-bit. Reduced-channel formats such as `R8` or `RG8` can visibly change the result by dropping color channels. ## Texture Options Texture format is configured separately from the ShaderPad instance settings, and takes similar options. ```javascript shader.initializeTexture( 'u_mask', { data: new Uint8Array(width * height), width, height }, { internalFormat: 'R8', format: 'RED', type: 'UNSIGNED_BYTE', minFilter: 'NEAREST', magFilter: 'NEAREST', }, ); ``` This is useful for: - Input streams with a different format than the defaults - Masks or grayscale buffers - Passing or storing integer IDs, category indices, or precise data as a texture If history is enabled for a texture, it will inherit the texture’s format settings. ## GLSL Sampler Types In your GLSL code, the sampler type must match the texture’s format family: - Use `sampler2D` for normalized color formats such as `R8` or `RGBA8`, and float formats such as `R32F` or `RGBA32F` - Use `usampler2D` for unsigned integer formats such as `R8UI` or `RGBA16UI` - Use `isampler2D` for signed integer formats such as `R32I` or `RGBA8I` The same rules apply to history textures, which are stored as `sampler2DArray`, `usampler2DArray`, or `isampler2DArray`. > **Sampler Type Mismatch:** If the sampler family does not match the texture's format family, the shader will either fail to compile/link or return incorrect values. ## Defaults And Inference If you provide `type` but not `internalFormat`, ShaderPad infers a matching storage format. For example: - `FLOAT` defaults to `RGBA32F` - `UNSIGNED_BYTE` defaults to `RGBA8` If you omit `format`, ShaderPad derives one from `internalFormat`. The defaults are practical, but it’s a good idea to set all three options explicitly. ## Chained ShaderPads Preserve Format And Precision If you initialize a texture from another `ShaderPad` without overriding its texture options, the destination texture inherits the source format settings. That means high-precision and integer data are transferred correctly by default, even when the two `ShaderPad` instances use different WebGL contexts. > **Chain Performance:** Cross-context chains preserve the data format, but they do not share a GPU texture. They are correct, but slower. --- ## History - Markdown: https://misery.co/shaderpad/docs/core-concepts/history.md - Canonical HTML: https://misery.co/shaderpad/docs/core-concepts/history/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/core-concepts/history/page.md History is one of ShaderPad’s more unique features, and can assist with snapshots, feedback effects, and creative coding. It lets you sample earlier frames without building the ring buffer yourself. ## Shader History Enable history of the shader’s output in the constructor: ```javascript const shader = new ShaderPad(fragmentShaderSrc, { canvas, history: 10, }); ``` In GLSL, that gives you access to prior rendered frames, which you can sample like this: ```glsl uniform highp sampler2DArray u_history; uniform int u_historyFrameOffset; ``` ## Texture History You can also maintain history for an individual texture: ```javascript shader.initializeTexture('u_webcam', videoElement, { history: 30 }); ``` That creates a history texture plus a matching frame-offset uniform such as `u_webcamFrameOffset`. ## Plugin History Some plugins can also keep history for the textures they generate. For example, MediaPipe-backed plugins such as `face`, `hands`, `pose`, and `segmenter` accept a `history` option on their internal texture config. ```javascript import face from 'shaderpad/plugins/face'; const shader = new ShaderPad(fragmentShaderSrc, { plugins: [face({ textureName: 'u_webcam', options: { history: 20 } })], }); ``` Plugin history behaves like texture history, including respecting `skipHistoryWrite` on the watched texture. ## Sampling Previous Frames The `helpers` plugin provides `historyZ()` for sampling previous frames: ```javascript import ShaderPad from 'shaderpad'; import helpers from 'shaderpad/plugins/helpers'; const FRAME_DELAY = 10; const fragmentShaderSrc = `#version 300 es precision highp float; in vec2 v_uv; uniform sampler2DArray u_history; uniform int u_historyFrameOffset; out vec4 outColor; void main() { // Get the output color from FRAME_DELAY frames ago. float z = historyZ(u_history, u_historyFrameOffset, ${FRAME_DELAY}); vec4 previous = texture(u_history, vec3(v_uv, z)); outColor = previous; }`; const canvas = document.createElement('canvas'); const shader = new ShaderPad(fragmentShaderSrc, { canvas, history: FRAME_DELAY, plugins: [helpers()], }); ``` Use `historyZ(..., 1)` to sample the previous stored frame. Larger values move further back in time. For texture and plugin history, `historyZ(..., 0)` is also valid and refers to the current value. ## Skipping History Writes Each frame is written to the history buffer by default. You can prevent a step from updating history with: ```javascript { skipHistoryWrite: true; } ``` This option is either returned from the `play()` callback or passed to `step()` as an argument: ```javascript // Only write every 10th frame to history. shader.play((time, frame) => { return { skipHistoryWrite: !!(frame % 10) }; }); ``` ```javascript // Skip writing this frame to history. shader.step({ skipHistoryWrite: true }); ``` ## Writing To Specific Texture History Slots `updateTextures()` also exposes `historyWriteIndex` for history textures: ```javascript shader.updateTextures({ u_webcam: videoElement }, { historyWriteIndex: 3 }); // Write to slot 3. ``` This writes into the specified slot and updates the texture's `*FrameOffset` uniform to that slot. ## History Precision History buffers match the [precision and format options](https://misery.co/shaderpad/docs/core-concepts/format-and-precision/) of their corresponding texture. This applies to framebuffer history (configured in the `ShaderPad` constructor) and texture history (configured in `initializeTexture()`). --- > **draw does not advance history:** `draw()` renders the current state only. Use `step()` when a pass should count as a new frame in history. --- ## Shader lifecycle - Markdown: https://misery.co/shaderpad/docs/core-concepts/shader-lifecycle.md - Canonical HTML: https://misery.co/shaderpad/docs/core-concepts/shader-lifecycle/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/core-concepts/shader-lifecycle/page.md ShaderPad has the following render lifecycle: 1. Construct a shader using `new ShaderPad()` 1. Initialize custom uniforms or textures using `initializeUniform()` and `initializeTexture()` 1. Render with `play()`, `step()`, or `draw()` 1. Clean up with `destroy()` You can go over the details of each method in the [Methods](https://misery.co/shaderpad/docs/api/methods/) API reference. Below is a quick overview, including when you may want to use each method. ## Constructor ```javascript const shader = new ShaderPad(fragmentShaderSrc, { canvas }); ``` ## Rendering Methods > **Quick Reference:** - Use `play()` for animation loops - Use `step()` for manual time/frame advancement - Use `draw()` when time, frame, and history should remain unchanged ### `play(onBeforeStep?)` `play()` starts the animation loop. `u_time` and `u_frame` uniforms are updated automatically, and history is kept up to date. ```javascript shader.play((time, frame) => { shader.updateUniforms({ u_speed: Math.sin(time) }); }); ``` Use it when: - You want to animate the shader over time - You don’t need manual control over timing - You’re rendering a single shader or a straightforward rendering pipeline ### `step(options?)` `step()` advances exactly one frame, and renders without triggering an ongoing animation loop. `u_time` and `u_frame` uniforms are updated automatically, and history is kept up to date. ```javascript shader.step({ skipHistoryWrite: true }); ``` Use it when: - You want deterministic manual control over the animation frame - You are building a chained pipeline, and/or another loop owns timing - Inputs change infrequently, or you want fine control over frame rate ### `draw(options?)` `draw()` renders without updating `u_time`, `u_frame`, or history. ```javascript shader.draw({ skipClear: true }); ``` Use it when: - You want to redraw the last frame without updating time, frame, or history - You want a lightweight “no-frills” render pass - The output should not count as a new animation step ## Pause, Reset, Destroy - `pause()` stops the animation loop started by `play()` - `resetFrame()` resets the clock and frame counter - `reset()` resets the clock and frame counter, and also clears history buffers - `destroy()` stops everything and releases WebGL resources and event listeners --- ## Textures - Markdown: https://misery.co/shaderpad/docs/core-concepts/textures.md - Canonical HTML: https://misery.co/shaderpad/docs/core-concepts/textures/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/core-concepts/textures/page.md ShaderPad can ingest a variety of texture sources, including images, videos, canvases, typed arrays, and chained ShaderPad outputs. It smoothly and performantly handles these with the same API, so you can spend less time on plumbing. ## Supported Texture Sources `initializeTexture()` accepts: - `HTMLImageElement` - `HTMLVideoElement` - `HTMLCanvasElement` - `OffscreenCanvas` - `ImageBitmap` - `WebGLTexture` - `ShaderPad` - `{ data, width, height }` typed-array textures ## Initialize A Texture ```javascript shader.initializeTexture('u_webcam', videoElement); shader.initializeTexture('u_image', imageElement); ``` Custom typed-array textures are also supported: ```javascript shader.initializeTexture( 'u_data', { data: new Float32Array(width * height * 4), width, height }, { internalFormat: 'RGBA32F', type: 'FLOAT', minFilter: 'NEAREST', magFilter: 'NEAREST', }, ); ``` ## Updating Live Textures Live sources such as videos should usually be updated each frame: ```javascript shader.play(() => { shader.updateTextures({ u_webcam: videoElement }); }); ``` ## Partial Texture Updates For typed-array textures, you can update a sub-region for efficiency: ```javascript shader.updateTextures({ u_data: { data, width, height, x, y, isPartial: true }, }); ``` ## Orientation Rules - DOM-backed sources are flipped vertically by default to match WebGL expectations - Set `preserveY: true` to keep DOM source orientation unchanged - Typed-array sources are expected to already be in WebGL’s bottom-up orientation ## Texture History Any texture can maintain its own history: ```javascript shader.initializeTexture('u_webcam', videoElement, { history: 30 }); ``` That creates: - A `sampler2DArray` texture in GLSL - A matching frame-offset uniform such as `u_webcamFrameOffset` With the `helpers` plugin, you can sample earlier frames like this: ```glsl float z = historyZ(u_webcam, u_webcamFrameOffset, 1); vec4 color = texture(u_webcam, vec3(v_uv, z)); ``` Here, `historyZ(..., 1)` means the previous stored texture sample. `historyZ(..., 0)` is also valid for texture history and refers to the current value. ## ShaderPad As A Texture Source You can feed one ShaderPad instance into another: ```javascript passB.initializeTexture('u_firstPass', passA); ``` This is the basis for chaining and multi-pass workflows, which are covered in detail [here](https://misery.co/shaderpad/docs/guides/chaining-shaders/). --- ## Uniforms - Markdown: https://misery.co/shaderpad/docs/core-concepts/uniforms.md - Canonical HTML: https://misery.co/shaderpad/docs/core-concepts/uniforms/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/core-concepts/uniforms/page.md Uniforms are variables you can pass from JavaScript to your GLSL code. ## Initialize A Uniform Uniforms are initialized one at a time with `initializeUniform()`. ```javascript shader.initializeUniform('u_speed', 'float', 1.5); // float shader.initializeUniform('u_color', 'float', [1, 0.5, 0]); // vec3 ``` Parameters: - `name`: `string` - `type`: `'float' | 'int' | 'uint'` - `value`: `number | number[] | (number | number[])[]` - `options?`: `{ arrayLength?: number, allowMissing?: boolean }` `allowMissing` is mainly for ShaderPad internals and plugins. When `false` or omitted, ShaderPad throws if the compiled shader does not expose the requested uniform. When `true`, missing uniforms are treated as intentional no-ops. ## Update Uniforms Updates are batched with key-value pairs: ```javascript shader.updateUniforms({ u_speed: 2.0, u_color: [1.0, 0.2, 0.1], }); ``` ## Uniform Arrays By default, array values represent vectors. You can declare uniform arrays by passing `arrayLength` to `initializeUniform()`. For example, to initialize a `float[24]` array: ```javascript shader.initializeUniform( 'u_heights', 'float', [ // 24 values... ], { arrayLength: 24 }, ); // float[24] ``` You can initialize an array of vectors like so: ```javascript shader.initializeUniform( 'u_points', 'float', [ [0.2, 0.3], [0.7, 0.8], ], { arrayLength: 2 }, ); ``` Partial updates are supported with `startIndex`: ```javascript shader.updateUniforms( { u_points: [[0.4, 0.5]], }, { startIndex: 1 }, ); ``` `updateUniforms()` also accepts `allowMissing?: boolean`, with the same semantics as `initializeUniform()`. --- ## Chaining shaders - Markdown: https://misery.co/shaderpad/docs/guides/chaining-shaders.md - Canonical HTML: https://misery.co/shaderpad/docs/guides/chaining-shaders/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/guides/chaining-shaders/page.md ShaderPad has a flexible & performant internal texture pipeline. Chaining shaders is where that becomes especially useful. ## What “Chaining” Means ShaderPad can accept many types of texture sources, including other ShaderPad instances. “Chaining” is when you feed the output of one ShaderPad into another for further processing. ```javascript const passA = new ShaderPad(fragmentA, { canvas: new OffscreenCanvas(256, 256) }); // If possible, share the same canvas for better performance const passB = new ShaderPad(fragmentB, { canvas: passA.canvas }); passB.initializeTexture('u_firstPass', passA); // Use passA’s output as an input to passB ``` ## Why Chain Shaders? Often, everything you need can be done in a single shader pass. But some effects require more than one pass. Blur is a common example: one pass might create a smaller, already-blurred version of the image, and a later pass spreads that blur back across the final image. To try to do this in a single pass would be very inefficient. Multi-pass rendering can also help structure your code. For instance, one pass might increase contrast, another might apply a color correction, and a third might add dithering. Keeping these stages separate can make the pipeline easier to understand and iterate on. When a single pass can produce the same result, it will usually be more efficient. But performance is not the only consideration; maintainability and readability matter too. ## Share WebGL Contexts When Possible For chained passes, the best default is to keep the whole pipeline on one canvas. This allows each ShaderPad instance to use the same WebGL context, which allows the entire pipeline to stay on the GPU. ```javascript import { createFullscreenCanvas } from 'shaderpad/util'; const sharedCanvas = createFullscreenCanvas(); const passA = new ShaderPad(fragmentA, { canvas: sharedCanvas, }); const passB = new ShaderPad(fragmentB, { canvas: sharedCanvas, }); passB.initializeTexture('u_firstPass', passA); ``` If two passes live on different WebGL contexts, ShaderPad has to read pixels back to the CPU and upload them back to the GPU for the next pass. That is far more expensive than just reusing a canvas. ## Synchronizing Renders For animated multi-pass work, the simplest pattern is to use a single `play()` call from the final shader pass, and orchestrate the rest from within that callback. For example: ```javascript const passA = new ShaderPad(fragmentA, { canvas: sharedCanvas }); const passB = new ShaderPad(fragmentB, { canvas: sharedCanvas }); passA.initializeTexture('u_webcam', video); passB.initializeTexture('u_webcam', video); passB.initializeTexture('u_firstPass', passA); passB.play(() => { passA.updateTextures({ u_webcam: video }); passB.updateTextures({ u_webcam: video }); passA.step(); passB.updateTextures({ u_firstPass: passA }); }); ``` This gives the whole chain one clock, one render loop, and a clear sequence. It avoids subtle bugs where two passes animate at different rates or sample stale intermediate textures. --- ## Performance - Markdown: https://misery.co/shaderpad/docs/guides/performance.md - Canonical HTML: https://misery.co/shaderpad/docs/guides/performance/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/guides/performance/page.md ShaderPad does a lot of work under the hood to make your graphics pipeline performant. If you want to fine-tune performance even further, the most valuable changes typically involve reducing texture bandwidth and avoiding unnecessary transfers between the CPU and GPU. ## Use The Smallest Format That Works The default `RGBA8` pipeline is fine for many effects, but it is not always the cheapest choice. Prefer: - `R8` or `R32F` for single-channel masks - `RGBA8` for normal color work - `RGBA32F` only when the effect truly needs float precision Smaller formats reduce memory traffic in both ordinary rendering and history buffers. Channel count matters as much as numeric precision. For example, `RGBA8` and `R32F` both use 32 bits per pixel. If an effect only needs one scalar value, dropping from `RGBA` to `R` can have a significant impact. ## Keep Chained Passes On The Same WebGL Context This is one of the biggest ShaderPad-specific wins. When chained passes share a canvas or WebGL context, ShaderPad can keep textures on the GPU. If they live on different contexts, ShaderPad has to read pixels back to the CPU and upload them again. For the full details, see [Share one canvas or WebGL context](https://misery.co/shaderpad/docs/guides/chaining-shaders#share-web-gl-contexts-when-possible/). ## Use Lower Resolution For Intermediate Passes Not every pass needs full output resolution. Downsample when a pass is only used for: - Blur or bloom inputs - Masks and segmentation - Coarse simulation fields - Preprocessing before a full-resolution composite ```javascript const lowResPass = new ShaderPad(fragmentShaderSrc, { canvas: { width: 512, height: 512 }, }); ``` If your passes already share one WebGL context, resizing that shared canvas between intermediate steps can be much cheaper than splitting the work across two separate contexts. It keeps the data on the GPU instead of forcing readbacks and reuploads. This won’t work for effects that rely on history, since those buffers need a stable size. ## Reduce History Size History increases texture bandwidth, so use it mindfully. - Disable history entirely when the effect or texture does not sample prior frames - Use `skipHistoryWrite: true` on updates or steps that should not become a new history frame For instance, let's say you want to store one sample per second for the previous 10 seconds. Instead of storing every rendered frame, you can do something like this: ```javascript const shader = new ShaderPad(fragmentShaderSrc, { canvas, history: 10, }); let lastStoredSecond = -1; shader.play(time => { const currentSecond = Math.floor(time); if (currentSecond === lastStoredSecond) { return { skipHistoryWrite: true }; } lastStoredSecond = currentSecond; }); ``` ## Prefer Partial Updates For Data Textures If a typed-array texture changes in a small region, update only that region instead of reallocating or reuploading the whole texture. ```javascript shader.updateTextures({ u_data: { data: patch, width: patchWidth, height: patchHeight, x: 0, y: 0, isPartial: true, }, }); ``` ## Prefer Partial Updates For Uniform Arrays If only part of a uniform array changes, update that slice instead of resending the entire array. ```javascript shader.updateUniforms( { u_points: [[mouseX, mouseY]], }, { startIndex: activePointIndex }, ); ``` This works well for UI control points, palettes, or other small, incrementally changing arrays. If the dataset is large and changes often, a data texture is usually a better fit. ## Choose Filtering Deliberately `NEAREST` is usually the more performant choice, so start there unless you specifically need interpolation. - Use `NEAREST` for data textures such as masks, landmarks, IDs, integer buffers, and history lookups that should stay discrete - Use `LINEAR` for imagery, smooth scaling, blur inputs, or anything that should blend between texels If a texture encodes values rather than pixels, `NEAREST` is usually the right answer. ## Batched MediaPipe Detection ShaderPad's MediaPipe plugins are designed to avoid redundant model work in chained renders. If you attach a MediaPipe plugin to multiple `ShaderPad` instances, ShaderPad shares a detector as long as these match: - The plugin type (`face`, `pose`, `hands`, or `segmenter`) - The `textureName` - The plugin options that affect detector setup - The underlying media source object That means you can run a chained pipeline like this without paying for multiple pose detections per frame: ```javascript const camera = document.querySelector('video'); const sharedCanvas = new OffscreenCanvas(1, 1); const preprocess = new ShaderPad(preprocessFrag, { canvas: sharedCanvas, plugins: [pose({ textureName: 'u_video', options: { maxPoses: 1 } })], }); const composite = new ShaderPad(compositeFrag, { canvas: sharedCanvas, plugins: [pose({ textureName: 'u_video', options: { maxPoses: 1 } })], textures: { u_scene: preprocess }, }); preprocess.updateTextures({ u_video: camera }); composite.updateTextures({ u_video: camera }); ``` On the first pass, the shared detector runs and caches the result for that source frame. On later passes in the same render chain, the plugin sees the same source and video timestamp, skips a new MediaPipe call, and just publishes the cached landmark or mask textures to each subscriber. To keep batching effective: - Keep `textureName` identical across passes - Keep plugin options aligned across passes - Avoid creating duplicate plugins with tiny option differences unless you really need separate detectors Batching can be useful when several passes need the same tracking data, such as a mask-generation pass plus a later stylization or composite pass. ## Be Deliberate With Plugins MediaPipe plugins do real work outside the fragment shader. Enable them only when required, and configure the smallest useful outputs: - Lower history depths when tracking history is short-lived - Avoid extra model outputs unless the shader uses them - Update the source texture once and reuse it across passes ## Profile The Whole Workflow With ShaderPad, the slow part is often not the fragment shader itself. It can be: - Updating a large texture every step - Keeping unnecessary history buffers alive - Transferring chained data across different WebGL contexts - Storing more channels or precision than the effect needs The most significant performance optimizations are often structural. --- ## Saving images - Markdown: https://misery.co/shaderpad/docs/guides/saving-images.md - Canonical HTML: https://misery.co/shaderpad/docs/guides/saving-images/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/guides/saving-images/page.md The `save` plugin adds a `save()` method that exports the current frame as a PNG file. On mobile, a share dialog is shown by default. ```typescript import ShaderPad from 'shaderpad'; import autosize from 'shaderpad/plugins/autosize'; import save, { WithSave } from 'shaderpad/plugins/save'; const canvas = document.createElement('canvas'); const shader = new ShaderPad(fragmentShaderSrc, { canvas: canvas, plugins: [save(), autosize()], }) as WithSave; const saveButton = document.createElement('button'); saveButton.textContent = 'Save'; saveButton.onclick = () => { shader.save('Soft Spirals', 'Saved with ShaderPad'); }; document.body.append(canvas); document.body.append(saveButton); shader.play(); ``` {% saving-images-preview /%} --- ## Webcam input - Markdown: https://misery.co/shaderpad/docs/guides/webcam-input.md - Canonical HTML: https://misery.co/shaderpad/docs/guides/webcam-input/ - Canonical source: https://github.com/miseryco/shaderpad/blob/main/docs/src/app/docs/guides/webcam-input/page.md ShaderPad makes it easy to ingest video textures, including live webcam streams. ## Basic Flow 1. Get a `MediaStream` 2. Attach it to a `