-
Notifications
You must be signed in to change notification settings - Fork 1
Description
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 | RegistryTslBundle3. 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 | PlaygroundTslSession4. 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: existingWebGLRenderer+ShaderMaterial+ GL log scrapingTslPreviewRuntime:WebGPURenderer+NodeMaterialfromcreateMaterial()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/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 legacyerrors
9. 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 errors
10. CLI
shaderbase addcopies.tsfiles for TSL shaders (instead of.glsl)shaderbase searchshows language in results
11. Skills (shaderbase-skills/)
tsl-fundamentals: TSL node API, composition patterns, common nodestsl-patterns: Advanced TSL recipes (noise, SDF, procedural)- Update
shaderbase-manifestskill 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
postprocessingpipeline (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 |