--- name: scaffold-shader description: Scaffold a new simple WGSL shader for the OGPU engine, wired for a RenderPipeline + Mesh. Use whenever the user wants to create a new shader, start a shader from scratch, add a .wgsl file for a mesh, get a shader boilerplate/template/starting point, or "make me a basic shader" / "scaffold a shader". Produces a minimal vs/fs WGSL file following the engine's reflection conventions, optionally with the JS pipeline wiring. --- # Scaffold a simple WGSL shader Generate a minimal, correct WGSL file (plus optional JS wiring) for a mesh rendered through `RenderPipeline` + `Mesh`. The base shader is deliberately bare — vs/fs with the standard transform uniforms and a fragment that outputs the normal mapped to 0..1. No custom uniforms, no textures: those are modifications you layer on top after generating, per the task. The reference shape is `examples/hellowebgpu/cube.wgsl` (same conventions). ## Conventions that are NOT optional (reflection-enforced) These come from `webgpu-utils` reflection in `RenderPipeline`/`Mesh` — get one wrong and the pipeline silently shades garbage or fails construction: - Uniform block MUST be `@group(0) @binding(0) var uniforms : Uniforms`. Lowercase `uniforms`, struct typed (commonly `Uniforms`). `RenderPipeline` reads `this.defs.uniforms.uniforms` by that exact name. - Vertex entry point MUST be `vs`, fragment MUST be `fs`. Hardcoded. - `Mesh.draw` writes standard uniforms BY NAME into the struct — only the fields that exist are written. Available: `projectionMatrix`, `viewMatrix`, `modelViewMatrix`, `modelMatrix`, `objectMatrix` (inverse model), `normalMatrix` (mat3x3f), `cameraPosition` (vec3f), `cameraQuaternion` (vec4f), `resolution` (vec2f), `time` (f32). Declare only the subset the shader uses, spelled exactly. Custom uniforms (e.g. `uScale`, `uAlpha`) go after them and are set via `mesh.uniforms.set({...})` — the `Mesh` owns the uniform values, not the pipeline. - Vertex attribute `@location` is assigned by declaration ORDER in the geometry's `data` object, not by name. Standard primitives (`@core/primitives` `Box`/`Sphere`/…) emit `position` (loc 0), `normal` (loc 1), `uv` (loc 2). The `Vertex` struct must match that order. - Texture uniforms are named `t` (e.g. `tMap`). Samplers keep descriptive names. Only add these if the shader actually samples a texture — a declared binding with no bind-group entry throws at pipeline creation. ## Workflow 1. Ask/confirm only what changes the output (skip if obvious from the request): - Shader name / target file path (default: alongside the consuming JS, e.g. `examples//.wgsl`, or `./.wgsl` next to the caller). - Does it need the JS pipeline wiring too, or just the `.wgsl`? Default: just the `.wgsl` unless they're clearly starting a new example. 2. Generate the base `.wgsl` by RUNNING the scaffold script — don't hand-write the boilerplate. The script emits a guaranteed-correct vs/fs skeleton with the bindings/struct/attribute order already right, so you spend your effort on the task-specific shading, not on re-deriving reflection rules: ```bash python .claude/skills/scaffold-shader/scripts/scaffold.py \ --out examples//.wgsl \ # --std projectionMatrix,modelViewMatrix,normalMatrix,time # override std fields # --force # overwrite an existing file ``` It refuses to clobber an existing file unless `--force`. Run `scaffold.py -h` for the full flag list. 3. Layer the requested modifications ON TOP of the generated file with Edit — the script output is a bare starting point (normal→0..1 fragment), not the finished shader. This is where the user's actual intent lands: custom uniforms, textures, custom fragment math, extra varyings, displacement in the vertex stage, sampling logic, etc. Keep the reflection conventions intact while you edit: - custom uniforms go AFTER the standard fields in `Uniforms`, set via `mesh.uniforms.set({...})`; - textures are `t` + a sampler at the next free `@binding`, and need a matching bind-group entry (see `references/js-wiring.md`). 4. Validate the WGSL without a browser — `node scripts/validate-shaders.mjs ` runs `naga`. The freshly-generated base always passes; run it again after your edits to catch syntax/type errors before the user loads the page. (If `naga` is absent the script exits 2 — `brew install naga-cli`, not `naga`.) 5. If JS wiring requested, write it from `references/js-wiring.md` (no-texture variant is the short path; texture variant adds the sampler/texture bind-group entries via `pipeline.defs.samplers.*` / `pipeline.defs.textures.*`). 6. Tell the user how to render it: `new RenderPipeline(gpu, { code, vertexBuffers: geometry.bufferLayouts })` → `new Mesh(gpu, { pipeline, geometry, bindGroups })` → `mesh.setParent(scene)`. The `Mesh` REQUIRES a `bindGroups` factory (pipeline serves layouts only — see `references/js-wiring.md`). Custom uniforms set with `mesh.uniforms.set({...})` (Mesh.draw uploads each frame). 7. Hot reload works automatically — every `*.wgsl?raw` import is an HMR boundary, so editing the new file reloads the pipeline live (`ShaderReload`). ## References - `scripts/scaffold.py` — the generator. Emits the bare vs/fs base (normal→0..1); flags: `--out`, `--std`, `--force`. - `references/shader-template.wgsl` — annotated reference of the shape the script emits (read it to understand the output, not to hand-copy it). - `references/js-wiring.md` — pipeline + mesh wiring, no-texture and texture variants.