Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

213 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Shader Studio

Your self-hosted workspace for building, tuning, and collecting WebGL shaders.

Browse a shader library, edit GLSL with live diagnostics, generate controls from a schema, save presets, and move everything between installations as portable JSON.

CI status Angular 22 Node.js 22 or newer pnpm 10


Shader Studio showing the Warp Tunnel shader and its generated controls — sliders, a checkbox, a select, and color pickers

The shader you select becomes the application canvas, so you edit the thing you are looking at. A broken draft never blanks the preview: Shader Studio keeps the last valid version running and places compiler diagnostics in the editor.

Highlights

  • Live GLSL workflow — Monaco editing, driver-backed diagnostics, and a safe compile pipeline that preserves the last working render.
  • Schema-generated controls — describe numbers, booleans, colors, and selects once; Shader Studio builds the control panel and uniforms for you.
  • A library that stays yours — shaders and presets live as readable files on disk, with atomic writes and no external database.
  • Portable by design — export one shader or the complete collection to a versioned JSON bundle and import it elsewhere.
  • Wallpaper Engine HTML — export the current shader as one self-contained, live WebGL document and drag it directly into Wallpaper Engine.
  • Interactive previews — pointer velocity, click ripples, pause, screenshots, a configurable post-processing chain (Bloom, Vignette), render scaling, and texture inputs are built in.
  • Web and desktop — run the SSR web app on your own machine or package the Electron desktop app for Windows.
  • MCP server — let Claude Code, Codex, or Cursor drive a running Shader Studio tab: list shaders, edit GLSL, tune uniforms, and capture screenshots.

Contents

Quick start

Requirements

  • Node.js ^22.22.3 || ^24.15.0 || >=26
  • pnpm 10

Clone the repository and start the development server:

git clone https://github.com/antelm-dev/shader-studio.git
cd shader-studio
pnpm install
pnpm dev

Open http://localhost:4200. The development server runs the real Express API and SSR application; the API is not mocked.

Self-hosting

Docker

The fastest way to run Shader Studio. Compose brings up PostgreSQL and the app together; the app waits for the database to be healthy before it starts.

git clone https://github.com/antelm-dev/shader-studio.git
cd shader-studio
cp .env.example .env         # then set POSTGRES_PASSWORD to a long random value
docker compose up -d --build

Shader Studio is then available at http://localhost:4000, with the five example shaders already seeded.

Keep .env local: it is ignored by both Git and the Docker build context. Only .env.example, which contains no usable credential, belongs in version control. If .env was already tracked, adding it to .gitignore is not enough: remove it from the index and rotate any password that may have been shared.

Your shaders live in PostgreSQL, persisted in the named postgres-data volume; everything else in the image is disposable. The database is not published on a host port — only the app container reaches it, over Compose's internal network — and the connection string never leaves the server (the browser only ever talks to the REST API).

Reaching the app under any name other than localhost — a machine name on your LAN, a domain behind a reverse proxy — means telling SSR about it in .env, otherwise the request is rejected:

SHADER_ALLOWED_HOSTS=localhost,127.0.0.1,[::1],shaders.example.com

.env also sets SHADER_PORT (host port, default 4000) and SHADER_SEED. To upgrade, docker compose up -d --build; the volume and its data are left alone.

From source

Build the production application and run its Node server against PostgreSQL:

pnpm build
DATABASE_URL=postgres://user:pass@localhost:5432/shader_studio pnpm serve:ssr

Shader Studio is then available at http://localhost:4000. When DATABASE_URL is set the server uses PostgreSQL; when it is not set the server falls back to a local SQLite file under SHADER_DATA_DIR (./data), which is convenient for pnpm dev:server but not intended for production. When exposing the app beyond localhost, set NG_ALLOWED_HOSTS to the hostnames that are allowed to reach SSR.

Important

Shader Studio has no authentication or multi-user isolation. Deploy it only on a trusted private network, behind an authenticated reverse proxy, or through a private access layer.

Configuration

The server reads these directly; under Compose they are derived from .env (see .env.example).

Variable Default Purpose
DATABASE_URL PostgreSQL connection string; when set, selects PostgreSQL, otherwise SQLite
PORT 4000 Port for the SSR server
SHADER_DATA_DIR ./data SQLite database directory (used only when DATABASE_URL is unset)
SHADER_EXAMPLES_DIR ./examples Source directory for the bundled examples that seed an empty store
SHADER_SEED Set to 0 to disable seeding an empty store
NG_ALLOWED_HOSTS localhost,127.0.0.1,[::1] Comma-separated hosts SSR may render for; set this when deploying
DATABASE_POOL_MAX 10 Maximum PostgreSQL pool connections

Compose-only variables (.env): POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD (build DATABASE_URL), plus SHADER_PORT (host port) and SHADER_ALLOWED_HOSTS (feeds NG_ALLOWED_HOSTS).

Data & backups

PostgreSQL (Web / Docker) is the primary store. Back it up and restore it with the standard tools, e.g. against the bundled Compose service:

# Backup
docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backup.sql
# Restore (into an empty database)
docker compose exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"' < backup.sql

SQLite (Desktop / dev) lives at one file. On the desktop app it is <userData>/library/shader-studio.sqlite (%APPDATA%/Shader Studio/library on Windows, ~/Library/Application Support/Shader Studio/library on macOS, ~/.config/Shader Studio/library on Linux). From-source dev without DATABASE_URL uses <SHADER_DATA_DIR>/shader-studio.sqlite. Back it up by copying the file (and its -wal/-shm siblings) while the app is closed.

Importing an old file library. The legacy per-shader folder format is never deleted. The desktop app imports <userData>/library/shaders automatically on first launch after upgrading (once, verified, files kept). For Docker, mount the old library and run the one-shot importer explicitly — it never modifies the source:

# with the old library mounted at /legacy-data (see docker-compose.yml)
docker compose --profile migrate run --rm migrate --source=/legacy-data
# add --mode=overwrite to replace shaders whose id already exists (default: rename)

Disabling examples. Set SHADER_SEED=0 to start with an empty library. Seeding is idempotent and versioned in the database, so it never overwrites your shaders and a deleted example does not reappear.

Diagnostics. The app fails fast and loudly if the database is unreachable or its schema cannot be brought to the expected version — check the container logs (docker compose logs shader-studio / docker compose logs postgres). The health of the API is GET /api/shaders (also the container HEALTHCHECK). Internal database errors are never leaked to the client: the REST/IPC layer only ever returns { error: { code, message, details? } }, never SQL, the connection string, or a stack trace.

Desktop app

The Electron target currently packages for Windows:

pnpm dev:desktop  # Angular dev server + Electron with main-process reload
pnpm pack:win     # unpacked app in release/win-unpacked
pnpm dist:win     # NSIS installer and portable executable in release/

The desktop target uses electron-ipc-module and electron-run. It stores its library in a SQLite database under Electron's per-user application-data directory (<userData>/library/shader-studio.sqlite) and does not start the Express server. The web and SSR targets use PostgreSQL behind the same REST API.

Development

The application uses Angular 22 (zoneless SSR), Angular Material, Express, three.js, lil-gui, and Monaco.

Script What it does
pnpm dev Dev server with HMR, SSR and the API
pnpm build Production build into dist/
pnpm serve:ssr Run the built SSR server
pnpm test Unit tests (Vitest), incl. the MCP server
pnpm lint Oxlint
pnpm format Oxfmt
pnpm typecheck tsc -b, strict
pnpm dev:mcp Run the MCP server from source

Using Shader Studio

Keyboard shortcuts

Key Action
Space Pause / resume time
H Show / hide the controls
S Save the frame as a PNG
Ctrl+S Save the shader
Shift+Alt+F Format the GLSL in the editor

The editor

Right-click the editor's toolbar for its menu.

  • Format GLSL re-indents by block depth and does nothing else. No rewrapping, no spacing opinions: a shader is dense numeric code whose columns are usually aligned on purpose, and a formatter that argues with that is one people turn off.
  • Copy full GLSL copies the fragment as a file that stands on its own — the source plus the declarations the engine would otherwise have supplied: the precision qualifier, the built-in uniforms, and one uniform per control. Anything the source already declares is left alone, so the result never redeclares its way into a compile error somewhere else.

Typing offers snippets for the things you would otherwise be looking up: main, uv, ripple (the u_clickData loop), channel, palette, hash21, noise, fbm, rot2 and uniform.

Post-processing

The Effects Rack, above the parameter controls in the Controls tab, is the configurable chain applied after the shader's own Image pass (never to Buffer A-D): Bloom and Vignette today, at most one instance of each. A master switch bypasses the whole chain without touching any effect's own settings; each effect has its own enable toggle, a reset to defaults, and remove/add. Reordering (drag, or the move-up/move-down buttons) changes the order the effects are actually applied in — order is part of the chain, not just the rack's display. Every change to the rack is a draft edit like any parameter: it marks the shader unsaved and is undone by discarding the draft.

Presets

A preset captures the live parameter values under a name. Ticking Also capture the render settings when saving stores the shader's post-processing chain alongside them, and applying that preset brings it back — which, since the chain belongs to the shader rather than to the knobs, leaves the document with unsaved changes. Presets saved without it never touch the chain. The ones that carry it are marked with an icon on their chip.

On desktop, More actions → Open output window opens a clean, independently resizable render surface. It follows the active draft, live parameters, pause state, render scale, and texture assignments, making it suitable for a second monitor or projector while the main window remains the control workspace.

Move the pointer over the background to push the shader around; click to drop a ripple. Both are fed to the shader as uniforms (see below) — what a shader does with them is up to it.


Architecture

Five concerns, kept apart on purpose. Nothing below the line knows about Angular.

apps/
  web/
    src/                 Angular browser, SSR, and desktop entry points
      app/               workspace state, rendering, editor, and UI
  server/
    src/                 Express SSR host and REST API
      api/               route parsing and HTTP error mapping
      create-library.ts  picks PostgreSQL (DATABASE_URL) or SQLite, then seeds
      cli.ts             `migrate-files` — one-shot legacy import
  desktop/
    main/src/            Electron lifecycle, windows, updates, and IPC handlers
    preload/src/         sandboxed context bridge
    package.json         desktop build, development, packaging, and type checks

libs/
  shared/                model, validation, GLSL, capture, and MCP contracts
  backend/               Node-only storage and i18n shared by server and desktop
    src/library/         ShaderLibrary — engine-agnostic shader domain logic
    src/persistence/     ShaderRepository + SQLite / Postgres / legacy adapters
    src/storage/         the legacy file store, now a read-only import source
  desktop-api/           generated, typed IPC bridge contract

tools/
  mcp/                   standalone `@shader-studio/mcp` server
  workspace/             checks, generators, smoke tests, and repo automation

Storage

Shaders, projects, presets, textures and thumbnails live in SQL: SQLite in the Electron app (via Node's built-in node:sqlite, so there is no native module to rebuild), PostgreSQL on the Web/Docker server (via Drizzle ORM's node-postgres adapter). Both sit behind one ShaderRepository contract, and all the domain logic — validation, id generation, deriving fragment/vertex from the project, presets, import/export, seeding — lives once in ShaderLibrary, above the contract. The web app knows none of this: it still talks only to ShaderApi (HttpShaderApi over REST, DesktopShaderApi over IPC), so SQLite stays in the Electron main process and the PostgreSQL connection string never reaches the browser.

Every mutation of a shader and its dependents runs in a transaction. The schema is versioned by ordered, deterministic migrations that run before any API/IPC is exposed and fail loudly rather than auto-generate. Concurrent writes are caught with a per-shader revision: a save may send the revision it read as expectedRevision and get a 409 conflict instead of silently clobbering a newer write (absent, it stays last-writer-wins). The old per-shader file library is no longer the primary store but is kept as a read-only import source — imported once on the desktop, or on demand via migrate-files under Docker, and never deleted.

The typed PostgreSQL model is in libs/backend/src/persistence/postgres/schema.ts. Add future server relations (users, sessions, memberships, invitations) there, and add the corresponding ordered migration in postgres/migrations.ts. Migrations deliberately continue to use the existing storage_metadata.schema_version ledger: deployed databases already have that history, so introducing the ORM does not create a second baseline or attempt to recreate live tables.

Each directory above is a pnpm workspace package with its own dependency manifest and runtime-specific scripts/configuration. The root package only orchestrates workspace commands and retains the desktop application metadata consumed by Electron Builder.

The store keeps three layers of state deliberately distinct:

  • record — the shader as the server last gave it to us.
  • draft — the editor buffers. The difference from record is what "unsaved changes" means, and what Save sends.
  • params — the live uniform values. Turning a knob is not an unsaved edit to the source; it is a value you can capture as a preset.

SSR

Express serves the API and renders the app in the same process. During SSR the app calls its own /api over a same-origin request (the absolute origin comes from the incoming request; see app.config.server.ts).

The rendered state is handed to the browser through Angular's TransferState, so the first client render is identical to the server's markup and hydration does not throw the page away. The server deliberately opens the first shader rather than the last one you had open — it cannot read your localStorage, and rendering a different shader than the client would then hydrate is exactly the mismatch the snapshot exists to prevent. The client switches to your remembered shader once it takes over.

three.js, lil-gui and Monaco are all dynamically imported: none of them exist on the server (lil-gui injects a stylesheet at import time and would throw), and keeping them out of the initial bundle lets the shell paint first.

Compiling without breaking the preview

A candidate shader is compiled against an offscreen 1×1 render target before it is allowed anywhere near the screen. Only if the driver accepts it does the live material get swapped. If it does not, the previous shader keeps rendering and the driver's log comes back as diagnostics.

Line numbers in a driver's log count from the top of the source three.js assembled, which is not the source you typed — three prepends a prelude. The engine finds your source inside the full source and subtracts the offset, so a diagnostic lands on the line you are actually looking at.


Shader format

Note

The live store is now SQL (see Storage). The per-shader directory layout below is the legacy/import format: it is what the old file store wrote, what the desktop app and migrate-files read to import into the database, and the shape the examples ship in. The .shader.json bundle format further down is unchanged and remains the interchange format for import/export.

One directory per shader. The directory name is the id — it is the primary key, which is why it is validated before it is ever joined onto a path.

data/
  .seeded                    marker; stops examples coming back after you delete them
  shaders/
    poured-paint/
      meta.json              name, description, control schema, render settings
      project.json           the multi-pass project: passes, buffers, files,
                              channel wiring — the source of truth for a shader
      fragment.glsl          the Image pass's source — a mirror of project.json
      vertex.glsl            the project's vertex shader — also a mirror
      presets.json           { "presets": [ ... ] }

A shader is a project — an Image pass, an optional Common pass of shared code, up to four buffers, and any number of plain source files a pass can #include. project.json is that whole document; fragment.glsl/vertex.glsl are kept in sync with it (the Image pass's source, and the project's vertex shader) so that anything reading the old two-file shape still works. A shader that predates project.json — or one whose copy of it is unreadable — reads as a single-pass project built from its fragment.glsl/vertex.glsl, wired up exactly as the old single-pass engine bound its four iChannels.

examples/shaders/ uses exactly this layout, and is copied into data/ the first time you run an empty store. data/ is gitignored; examples/ is not.

Writes are atomic (temp file + rename), and mutations of a given shader are serialized, so a half-written meta.json is never observable — project.json included.

Ids are lowercase letters, digits and inner hyphens — no dots, no separators. That rules out .., hidden files, and Windows' reserved device names. Renaming a shader changes its display name only; the id, and therefore the path, is stable.

meta.json

{
  "name": "Hex Pulse",
  "description": "A hexagonal lattice that answers back.",
  "author": "Shader Studio",
  "createdAt": "2026-07-12T00:00:00.000Z",
  "updatedAt": "2026-07-12T00:00:00.000Z",
  "controls": [/* see below */],
  "render": {
    "postProcessing": {
      "enabled": true,
      "effects": [
        {
          "type": "bloom",
          "enabled": true,
          "settings": { "strength": 0.55, "radius": 0.55, "threshold": 0.65 }
        },
        {
          "type": "vignette",
          "enabled": false,
          "settings": { "intensity": 0.4, "softness": 0.5, "roundness": 1 }
        }
      ]
    }
  }
}

postProcessing.effects is ordered — that order is what the renderer applies the chain in, not just how the rack displays it. Older render shapes, including a bare { "bloom": {...} } (pre-chain) record, still import; see Import and export.

presets.json

{
  "presets": [
    {
      "id": "solar-storm",
      "name": "Solar Storm",
      "createdAt": "2026-07-12T00:00:00.000Z",
      "values": { "timeScale": 1.4, "colorLine": "#ff7a3d" },
      "render": {
        "postProcessing": {
          "enabled": true,
          "effects": [
            {
              "type": "bloom",
              "enabled": true,
              "settings": { "strength": 1.2, "radius": 0.4, "threshold": 0.7 }
            }
          ]
        }
      }
    }
  ]
}

render is optional and usually absent: a preset without it restores the values and leaves the shader's own post-processing chain alone. values are projected onto the current control schema when applied — anything the schema no longer declares is dropped, and a number outside a narrowed range is clamped rather than discarded, so a preset outlives the edits made to the shader underneath it.


Control schema

A shader declares its parameters; the GUI is generated from that declaration. No shader ever writes GUI code. Add a control in the Config tab and its knob appears immediately, bound to a uniform, without a reload.

The rule: a control keyed warpIntensity feeds uniform float u_warpIntensity. The uniform is always the key prefixed with u_.

type GLSL uniform Widget Required fields
number float u_<key> slider default, min, max, (step)
boolean bool u_<key> checkbox default
color vec3 u_<key> color picker default as #rrggbb
select float u_<key> dropdown default, options (label → number)

label (GUI text) and folder (grouping) are optional on all of them.

[
  {
    "key": "timeScale",
    "type": "number",
    "label": "Time Scale",
    "folder": "Motion",
    "default": 0.5,
    "min": 0,
    "max": 2
  },
  { "key": "mirror", "type": "boolean", "label": "Mirror", "folder": "Flight", "default": false },
  {
    "key": "colorLine",
    "type": "color",
    "label": "Lattice",
    "folder": "Palette",
    "default": "#54e0ff"
  },
  {
    "key": "paletteMode",
    "type": "select",
    "label": "Palette Mode",
    "folder": "Palette",
    "default": 1,
    "options": { "Classic": 0, "Neon": 1, "Ember": 2 }
  }
]

Colors are passed to the shader as display-space sRGB vec3 (three.js's colour management is off), which is what you almost certainly want when you pick #54e0ff and expect to see #54e0ff.

Built-in uniforms

Provided to every shader whether it declares them or not. Declare the ones you use.

uniform vec2  iResolution;             // drawing-buffer size, pixels
uniform float iTime;                   // seconds; pausable, and it does not
                                       // fast-forward when you resume
uniform vec4  iMouse;                  // xy: pointer in pixels, z: 1 while pressed
uniform vec2  iMouseVel;               // pointer velocity, pixels/second
uniform vec3  u_clickData[__MAX_WAVES__]; // per click: xy pixels, z = birth time
                                          // (z <= 0 means the slot is unused)

__MAX_WAVES__ is substituted with the ripple-slot count (24) before compiling, so use it for the array size and any loop bound. examples/shaders/hex-pulse is the worked example.

The vertex shader is an ordinary three.js ShaderMaterial vertex shader, and gets position, uv, projectionMatrix and modelViewMatrix. The default passes vUv through, which is all a full-screen shader needs.

Editing the schema

Changing the controls re-projects every preset onto the new schema: a value for a control you deleted is dropped, and a value now out of range is clamped rather than discarded — a preset saved before you narrowed a slider is still worth keeping. A schema that does not parse is reported in the diagnostics strip, blocks saving, and leaves the working GUI alone.


Import and export

One documented format, used for both a single shader and a whole collection. Everything needed to reproduce a shader elsewhere is in it: the whole project (passes, buffers, files, channel wiring), schema, render settings and presets.

GET /api/shaders/:id/export

{
  "format": "shader-studio/v3",
  "kind": "shader",
  "exportedAt": "2026-07-12T12:00:00.000Z",
  "shader": {
    "id": "hex-pulse",
    "name": "Hex Pulse",
    "description": "...",
    "author": "Shader Studio",
    "controls": [/* the schema */],
    "render": { "postProcessing": { "enabled": true, "effects": [/* see meta.json above */] } },
    "fragment": "precision highp float; ...",
    "vertex": "varying vec2 vUv; ...",
    "project": {
      "version": 1,
      "vertex": "varying vec2 vUv; ...",
      "passes": [/* Image, an optional Common, up to four buffers */],
      "files": [/* plain #include-able source files */]
    },
    "presets": [
      {
        "id": "circuit",
        "name": "Circuit",
        "createdAt": "...",
        "values": { "timeScale": 0.5, "colorPulse": "#5ef2ff" }
      }
    ]
  }
}

fragment/vertex stay in the payload as mirrors of project (the Image pass's source and the project's vertex shader), for anything that still reads the old two-string shape.

GET /api/export returns the same thing with "kind": "collection" and a "shaders": [ ... ] array of those payloads. Import accepts either kind.

shader-studio/v1 and v2 bundles still import. v1 predates project entirely; on import, a project is synthesized from fragment/vertex the same way a shader that predates project.json on disk does — one Image pass, an empty Common pass, and the four iChannels bound exactly as the old single-pass engine bound them. v2 has project but predates the post-processing chain: its bare { "bloom": {...} } render is migrated into a single-effect chain the same way a render missing entirely is filled in.

Import modes. rename (the default) never destroys anything: a shader whose id already exists is given a fresh, suffixed one. overwrite replaces the shader holding that id, which is what makes an export → import round trip idempotent. The UI asks before it overwrites.

Bundles are validated on the way in, and a bundle with a broken id but a usable name is recovered rather than rejected — hand-edited files are expected.

Wallpaper Engine HTML

Import & export → Export Wallpaper Engine HTML… downloads one standalone .html file. The desktop app creates a dedicated project folder containing index.html; after a web download, put the HTML in its own folder yourself. Then drag index.html onto Wallpaper Engine's Create Wallpaper target. This matters because Wallpaper Engine copies the selected file's whole directory. The document contains its WebGL runtime, composed Image and buffer passes, current parameter values, and base64-embedded channel textures; it has no CDN or network dependency.

Time, resolution, pointer velocity, click ripples, texture sampling, fixed/scaled buffer resolutions, and multipass feedback are supported. The post-processing chain (Bloom, Vignette) is not currently reproduced by the standalone runtime, and the export reports one generic compatibility warning whenever the source shader has any post-processing effect enabled.

Shader controls are exported with stable Wallpaper Engine property keys. Their current values work immediately. To expose a control in Wallpaper Engine's user interface, add a matching property under Edit → Change Project settings: Slider for number/select, Checkbox for boolean, or Color for color. The generated HTML contains the key mapping in window.__SHADER_STUDIO_WALLPAPER__.controls.


API

All errors are { "error": { "code", "message", "details"? } }. 400 invalid · 404 not found · 409 conflict.

Method Route Purpose
GET /api/shaders List (summaries)
POST /api/shaders Create from the template
GET /api/shaders/:id Read one, in full
PUT /api/shaders/:id Partial update (name, source, controls, render)
DELETE /api/shaders/:id Delete
POST /api/shaders/:id/duplicate Copy, presets included
GET /api/shaders/:id/presets List presets
POST /api/shaders/:id/presets Save; reusing a name overwrites
DELETE /api/shaders/:id/presets/:presetId Delete a preset
GET /api/shaders/:id/export Export one
GET /api/export Export everything
POST /api/import Import a bundle

Preset values are sanitized against the shader's schema on save, so a preset can never carry a value for a control that does not exist.


MCP server

tools/mcp publishes @shader-studio/mcp on npm: an MCP server that lets Claude Code, Codex, Cursor, and other MCP clients drive a locally-open Shader Studio tab — list shaders, edit GLSL live, tune uniforms, apply presets, and capture screenshots — through the same authenticated localhost WebSocket bridge the app already exposes. It speaks MCP over stdio to the client and makes no outbound network calls of its own.

npx -y @shader-studio/mcp
# or, wired into Claude Code:
claude mcp add shader-studio -- npx -y @shader-studio/mcp

Pairing the browser tab, the security model, environment variables, and per-client config (Codex, Cursor, Windows) are documented in tools/mcp/README.md.


Included shaders

Shader Shows
Poured Paint 43 controls. Domain-warped fbm, layers quantized into pooled bands with hard contour lips, wet specular relief, OKLab palette ramp, click ripples with chromatic dispersion, and momentum-tunable pointer smearing. Bloom on.
Aurora Veil Curtains draped by warping the x axis with slow noise, over a twinkling star field.
Hex Pulse u_clickData: click and a wavefront crosses the lattice. Hover lights the cells under the cursor.
Warp Tunnel A tunnel from 1/r — no raymarching. Demonstrates select and boolean controls.

Poured Paint and its five presets are carried over from the project this app grew out of, and are the reference for what the format can express.

All shaders distributed in examples/shaders are original Shader Studio examples and are licensed under Apache-2.0 with the rest of the project.


Tests

pnpm test

~600 tests, focused where a bug would actually cost you something:

  • backend/library/conformance.ts — one behavioural suite the storage engines must both pass, run against SQLite (a temp file) and, when SHADER_TEST_DATABASE_URL is set, PostgreSQL. It is where "the logic is not duplicated between engines" is enforced: init/migrations, CRUD, sorted listing, cascade delete, duplicate, presets, textures on all four channels, thumbnails, import/export (v1 and v2, rename/overwrite), multipass projects, idempotent seeding, transaction rollback, revision conflicts, legacy migration, corrupt JSON and missing assets, and persistence across a restart.

  • server/api/router.spec.ts — the REST layer over a real SQLite-backed library: status codes, the { error: { code, message } } envelope, a 409 on a stale expectedRevision, and texture upload/serve/clear.

  • libs/shared/src/validate/validate.spec.ts — ids (every traversal and reserved-name case), the control schema, preset sanitization and clamping, and bundle round-trips — including a shader-studio/v1 bundle (no project field) synthesizing one via migrateLegacyProject, and a malformed project being sanitized rather than failing the import.

  • backend/storage/shader-storage.spec.ts — the legacy file store (now the read-only import source) against a real temp directory, not a mocked fs, because the whole point of that layer is what it does to the filesystem and a mock would let a traversal bug through. Covers CRUD, path traversal, atomic update, preset lifecycle, import modes, and seeding, plus a project describe block: project.json round-trips through a fresh read and a fresh ShaderStorage, a shader with none reads as a legacy-migrated project, duplicate and export/import carry buffers/Common/ files/wiring, and a legacy fragment/vertex-only patch reconciles onto the current project instead of replacing it.

  • core/shader-store.spec.ts — the client-side store: selection, dirty tracking, save/discard paths, and preset application.

  • core/project-workspace.spec.ts — a shader record becoming a project (legacy and otherwise), unsaved-changes detection, and the pre-upgrade localStorage → server migration: a matching baseline pushes and clears, a stale one reconciles (record wins for the Image pass, buffers survive) before pushing, and a failed push leaves the local copy for the next load to retry.

  • ui/document-status.spec.ts — the saved/dirty/saving indicator, including the debounce timing.

  • core/draft-recovery.spec.ts and core/panel-prefs.spec.ts — what survives a reload: unsaved drafts, and panel sizes.

  • rendering/glsl-diagnostics.spec.ts — both driver log dialects, and the prelude offset that makes a line number point at the right line.

  • rendering/glsl-export.spec.ts — the generated uniform prelude, and above all what it declines to generate: a declaration the source already carries would be a redeclaration error wherever the copy is pasted.

  • editor/glsl-format.spec.ts — indentation depth, braces that are only part of a comment, and idempotence.

  • rendering/shadertoy-import.spec.ts — the Shadertoy source rewrite.


Known limitations

  • One active shader. The browser list displays saved thumbnails, but it does not continuously render every shader in the library. The selected shader and the optional output window are the live render surfaces.
  • No auth, no multi-user. The web API uses PostgreSQL or SQLite and assumes a single trusted user. It is a studio, not a public service; expose it only behind an authenticated proxy or private access layer.
  • Bloom and Vignette are the only built-in post effects, at most one instance of each. Tone mapping, color grading, chromatic aberration, FXAA and LUTs are not implemented. A preset can optionally capture the complete render settings, including the chain.
  • Monaco's stylesheet is global (~88 kB gzipped), not lazy: the CSS its ESM modules import lands in a chunk nothing links, so the editor comes out structurally unstyled if you rely on it. The editor's code is still lazy.
  • Shader compilation uses a real draw call to a 1×1 target to force a compile. It is the only reliable way to make three.js compile eagerly, but it does mean a recompile costs one hidden frame.
  • GLSL diagnostics come from the driver, so their exact wording varies by browser and GPU.

License

Copyright 2026 Adel Terki.

Shader Studio is licensed under the Apache License 2.0. Third-party software included by the project remains under its respective license; see THIRD_PARTY_NOTICES.md.

About

A live playground for crafting, tuning, and performing GLSL shaders.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages