Context
ShaderBase favors Three.js but currently only supports GLSL shaders. Three.js has native TSL (Three Shading Language) support — a JavaScript-based node material system that works with both WebGL and WebGPU renderers. TSL is the future of Three.js materials and we should support it across the stack.
Decisions
| Decision |
Choice |
Rationale |
| Storage format |
JS source code (.ts files) |
Readable, diffable, agent-friendly, shadcn-compatible |
| Execution model |
Sandboxed iframe, postMessage in/out |
TSL is JS by design — isolate execution from app origin |
| Scope |
Full stack (surface + geometry) |
No postprocessing in v1 |
| Contract |
export function createMaterial(): NodeMaterial |
Single entry point, clear interface |
| Type model |
Discriminated unions on language |
No nullable-field soup — invalid states unrepresentable |
| Versioning |
Schema 0.1.0 → 0.2.0, registry format 0.1.0 → 0.2.0 |
Real contract break, ship cleanly |
| MCP compat |
language defaults to "glsl", legacy fields preserved |
Avoid breaking existing agent integrations |
Architecture
1. Schema (packages/schema)
Discriminated union on language:
// Base fields shared by all shaders
const baseManifestSchema = z.object({
schemaVersion: z.literal("0.2.0"),
name: shaderNameSchema,
displayName: nonEmptyStringSchema,
// ... all shared fields
})
// GLSL-specific
const glslManifestSchema = baseManifestSchema.extend({
language: z.literal('glsl').default('glsl'),
files: fileReferencesSchema, // { vertex, fragment, includes }
})
// TSL-specific
const tslManifestSchema = baseManifestSchema.extend({
language: z.literal('tsl'),
tslEntry: relativePathSchema, // e.g. "source.ts"
})
const shaderManifestSchema = z.discriminatedUnion('language', [
glslManifestSchema,
tslManifestSchema,
])
GLSL manifest example:
{
"schemaVersion": "0.2.0",
"name": "gradient-radial",
"language": "glsl",
"files": { "vertex": "vertex.glsl", "fragment": "fragment.glsl", "includes": [] }
}
TSL manifest example:
{
"schemaVersion": "0.2.0",
"name": "tsl-gradient-wave",
"language": "tsl",
"tslEntry": "source.ts"
}
2. Registry Bundle
Discriminated union — no more assuming vertexSource + fragmentSource on every shader:
type RegistryGlslBundle = RegistryBundleBase & {
language: 'glsl'
vertexSource: string
fragmentSource: string
}
type RegistryTslBundle = RegistryBundleBase & {
language: 'tsl'
tslSource: string
}
type RegistryShaderBundle = RegistryGlslBundle | RegistryTslBundle
3. Playground Types
Discriminated session types — no invalid states:
type PlaygroundGlslSession = PlaygroundSessionBase & {
language: 'glsl'
vertexSource: string
fragmentSource: string
}
type PlaygroundTslSession = PlaygroundSessionBase & {
language: 'tsl'
tslSource: string
}
type PlaygroundSession = PlaygroundGlslSession | PlaygroundTslSession
4. Structured Errors
type PlaygroundError =
| { kind: 'glsl-compile'; message: string }
| { kind: 'glsl-link'; message: string }
| { kind: 'tsl-parse'; message: string }
| { kind: 'tsl-runtime'; message: string }
| { kind: 'tsl-material-build'; message: string }
MCP backward compat: keep compilationErrors: string[] alongside new structuredErrors: PlaygroundError[].
5. Database Migration
ALTER TABLE playground_sessions ADD COLUMN shader_language TEXT NOT NULL DEFAULT 'glsl';
ALTER TABLE playground_sessions ADD COLUMN tsl_source TEXT;
6. Preview Runtimes (Playground Canvas)
Two separate runtimes behind a shared interface:
type PreviewResult = {
errors: PlaygroundError[]
screenshotBase64: string | null
}
interface PreviewRuntime {
init(container: HTMLElement): Promise<void>
render(session: PlaygroundSession): Promise<PreviewResult>
dispose(): void
}
GlslPreviewRuntime: existing WebGLRenderer + ShaderMaterial + GL log scraping
TslPreviewRuntime: WebGPURenderer + NodeMaterial from createMaterial() contract
TSL runs in a sandboxed iframe (sandbox="allow-scripts", no allow-same-origin). Source passed via postMessage, screenshots/errors returned via postMessage. Same isolation model as CodePen/CodeSandbox.
7. Playground Editor
- Language selector toggle: GLSL / TSL
- TSL mode: single editor pane with JS/TS syntax highlighting
- GLSL mode: vertex/fragment tab split (unchanged)
8. API Routes
POST /api/playground/create accepts language (default "glsl") and language-specific source fields
POST /api/playground/:id/update accepts tslSource for TSL sessions, vertexSource/fragmentSource for GLSL
GET /api/playground/:id/state returns discriminated session
GET /api/playground/:id/errors returns structuredErrors alongside legacy errors
9. MCP Tools
create_playground gains optional language param (default "glsl") and tslSource
update_shader gains tslSource (mutually exclusive with vertex/fragment for TSL sessions)
get_shader returns discriminated bundle (GLSL or TSL)
search_shaders returns language in index entries
get_errors returns structured errors
10. CLI
shaderbase add copies .ts files for TSL shaders (instead of .glsl)
shaderbase search shows language in results
11. Skills (shaderbase-skills/)
tsl-fundamentals: TSL node API, composition patterns, common nodes
tsl-patterns: Advanced TSL recipes (noise, SDF, procedural)
- Update
shaderbase-manifest skill for TSL manifest fields
TSL Contract
Every TSL shader exports createMaterial:
import { color, mix, uv, sin, time } from 'three/tsl';
import { NodeMaterial } from 'three/webgpu';
export function createMaterial(): NodeMaterial {
const material = new NodeMaterial();
const t = sin(time.mul(2.0)).mul(0.5).add(0.5);
material.colorNode = mix(
color(0x1a1a2e),
color(0xe94560),
mix(uv().x, uv().y, t)
);
return material;
}
Implementation Order
- Schema: discriminated union, bump to
0.2.0
- Registry types + builder: discriminated bundle
- CLI + MCP: handle both bundle shapes
- Playground types + DB migration
- API route updates
- MCP handler updates
- Playground UI: language selector, editor mode
- Sandboxed TSL preview runtime
- Structured error model
- First TSL shader in the corpus
- Skills
Explicitly Out of Scope
- TSL
postprocessing pipeline (separate follow-up issue)
- Same-origin execution of user TSL source
- Phased prep release — ship the contract change directly
Version Bumps
| Contract |
From |
To |
| Manifest schema |
0.1.0 |
0.2.0 |
| Registry format |
0.1.0 |
0.2.0 |
@shaderbase/schema |
minor bump |
|
@shaderbase/cli |
minor bump if public contract changes |
|
@shaderbase/mcp |
minor bump if tool output changes |
|
Context
ShaderBase favors Three.js but currently only supports GLSL shaders. Three.js has native TSL (Three Shading Language) support — a JavaScript-based node material system that works with both WebGL and WebGPU renderers. TSL is the future of Three.js materials and we should support it across the stack.
Decisions
.tsfiles)postMessagein/outexport function createMaterial(): NodeMateriallanguage0.1.0→0.2.0, registry format0.1.0→0.2.0languagedefaults to"glsl", legacy fields preservedArchitecture
1. Schema (
packages/schema)Discriminated union on
language:GLSL manifest example:
{ "schemaVersion": "0.2.0", "name": "gradient-radial", "language": "glsl", "files": { "vertex": "vertex.glsl", "fragment": "fragment.glsl", "includes": [] } }TSL manifest example:
{ "schemaVersion": "0.2.0", "name": "tsl-gradient-wave", "language": "tsl", "tslEntry": "source.ts" }2. Registry Bundle
Discriminated union — no more assuming
vertexSource+fragmentSourceon every shader:3. Playground Types
Discriminated session types — no invalid states:
4. Structured Errors
MCP backward compat: keep
compilationErrors: string[]alongside newstructuredErrors: PlaygroundError[].5. Database Migration
6. Preview Runtimes (Playground Canvas)
Two separate runtimes behind a shared interface:
GlslPreviewRuntime: existingWebGLRenderer+ShaderMaterial+ GL log scrapingTslPreviewRuntime:WebGPURenderer+NodeMaterialfromcreateMaterial()contractTSL runs in a sandboxed iframe (
sandbox="allow-scripts", noallow-same-origin). Source passed viapostMessage, screenshots/errors returned viapostMessage. Same isolation model as CodePen/CodeSandbox.7. Playground Editor
8. API Routes
POST /api/playground/createacceptslanguage(default"glsl") and language-specific source fieldsPOST /api/playground/:id/updateacceptstslSourcefor TSL sessions,vertexSource/fragmentSourcefor GLSLGET /api/playground/:id/statereturns discriminated sessionGET /api/playground/:id/errorsreturnsstructuredErrorsalongside legacyerrors9. MCP Tools
create_playgroundgains optionallanguageparam (default"glsl") andtslSourceupdate_shadergainstslSource(mutually exclusive with vertex/fragment for TSL sessions)get_shaderreturns discriminated bundle (GLSL or TSL)search_shadersreturnslanguagein index entriesget_errorsreturns structured errors10. CLI
shaderbase addcopies.tsfiles for TSL shaders (instead of.glsl)shaderbase searchshows language in results11. Skills (
shaderbase-skills/)tsl-fundamentals: TSL node API, composition patterns, common nodestsl-patterns: Advanced TSL recipes (noise, SDF, procedural)shaderbase-manifestskill for TSL manifest fieldsTSL Contract
Every TSL shader exports
createMaterial:Implementation Order
0.2.0Explicitly Out of Scope
postprocessingpipeline (separate follow-up issue)Version Bumps
0.1.00.2.00.1.00.2.0@shaderbase/schema@shaderbase/cli@shaderbase/mcp