Skip to content

Shader Editor

The Shader Editor authors hook-based shaders - custom WGSL that plugs into Awaken's standard PBR shader at four well-defined points, plus a table of parameters those hooks can read. It is where you make a flag wave, a surface glow, or geometry cut out.

These are the project's Shaders (hook-based), listed under the Shaders tab and distinct from the legacy PBR Material presets you snapshot in the Asset Browser. Under the hood a Shader is stored as a Material render object, so you still assign one to an object through its MeshMaterial component. See Materials for the shader model behind it.

A Shader can instead be authored as a node graph rather than by hand-writing WGSL. This page describes the WGSL-hook surface and the node-graph workflow; the Shader Graph page is the full node reference.

The layout

A sidebar lists your shaders; the main area edits the open one.

  • The sidebar is the shared ListWorkspace - the same list panel the Code Panel and Particle Effects tabs use. It collapses to an icon strip (◑ per material, name on hover) to reclaim width. Double-click (or right-click ▸ Rename) to rename; right-click ▸ Delete to remove (with a confirm - objects using it fall back to the default shader). + New Shader (WGSL hooks) and + New Shader Graph (a node graph) each create one and drop you into naming it.
  • Open a shader from here, from the + New Shader / + New Shader Graph buttons in the Asset Browser, by double-clicking a shader asset, or via Edit in its Inspector view.
  • The bar above the hooks shows the open material's name (read-only - rename lives on the sidebar row), a Compile button, and the compile status.

📸 Screenshot - save as img/editor-shader-editor.png

The Shader Editor: the shader list on the left, the four WGSL hook editors (with one filled in and its "on" badge), the ✓ compiles status in the bar, and the param table below showing a couple of params and the floats budget meter.

The four WGSL hook slots

Each hook is a WGSL snippet spliced into the standard mesh shader at a specific point. Leave one blank to skip it - the marker simply vanishes. Reference a parameter in a hook as params.<name>.

The panel lists the hooks in execution order - top to bottom here is top to bottom in the composed shader - with a divider at the vertex → fragment boundary.

HookStageYou modifyIn scope
Vertex Positionvertex, after the model transformworld - a vec4<f32> in world spacepos / normal / uv (read-only object-space vertex inputs), m (the instance record), params.*, globals.*
Discardfragment, first statement of fsnothing - it is an early exit (if (…) { discard; })in.* varyings (in.uv, in.worldPos, …), params.*, globals.*
Surface Normalfragment, right after N is derivedN - a vec3<f32> world-space, normalized shading normalin.*, params.*, globals.*
Surface Colorfragment, after the albedo sample, before lightingalbedo (vec3<f32> linear rgb) and outAlpha (f32)everything above plus texel (the sampled albedo texture)

Two things routinely trip authors up:

  • pos is not writable. The vertex hook runs after the model matrix, so what you displace is world, in world space. Displacing pos would be an object-space edit the template has already consumed.
  • texel is not in scope in Discard. The discard hook is the very first line of the fragment stage, before the albedo texture is sampled. You do not need it for texture cutout anyway: the template already discards texel.a < 0.5 for every material, hook or not.

A hook with content shows an on badge; a hook with compile errors shows a red error count next to it, so a problem is visible even when that editor is scrolled off screen.

The hook editors

Each slot is a full Monaco editor with WGSL syntax highlighting (Awaken's own forge-wgsl language - keywords, types, attributes, builtin functions, and the hook-provided names - params, globals, camera, in, albedo, N, outAlpha, texel - coloured distinctly from things you must declare yourself). Completions offer the WGSL builtin functions, types and keywords, and typing globals. offers exactly the two fields that struct has - time and anim.

The editors auto-size to their content: no inner scrollbars, and a mouse wheel over one scrolls the panel rather than being swallowed. The panel scrolls once, so you read all four hooks as one shader.

Compiling and errors

Composition is not validation. composeShader is a pure string splice that always succeeds, so a badge based on it would say "OK" for WGSL that red-screens the viewport. The editor therefore performs a real compile:

  1. composeShaderMapped splices the hooks into MESH_MATERIAL_WGSL and records, for each hook, which composed line range its body landed on.
  2. The composed WGSL is handed to createShaderModule on a dedicated validation GPUDevice - deliberately not the viewport's, so opening a project straight onto this tab still validates (no viewport has mounted yet) and a deliberately-broken probe can never interfere with the renderer's error scopes or flood the Console.
  3. getCompilationInfo() returns the driver's own diagnostics, in composed-shader line numbers.
  4. Each message is routed back through the span map to the hook that owns it, at the line inside that hook, and shown as an inline squiggle with the compiler's text on hover.
  • Validation runs immediately when you open or switch material, and ~400 ms after your last keystroke while editing, so half-typed code isn't squiggled.
  • The Compile button re-checks on demand and logs the result - and every error's location - to the Console, the same way a script compile does.
  • The status reads ✓ compiles (plus a warning count if any), or ✗ N errors with the first error's location in its tooltip. If no GPU device could be acquired at all it says not compiled rather than claiming the shader is fine.
  • Messages that land outside every hook - in the template, or in the generated Globals / MaterialParams structs - have no editor to squiggle. They get their own list above the hooks, quoted with the composed line number. These usually mean a param name collides with something, or a hook assigned the wrong type.

A material that fails to compile is not fatal at render time either: the material cache falls back to the default pipeline, so a broken hook shows plain PBR rather than crashing the frame.

The parameter table

Parameters expose editable values to the hooks. Each row has:

  • Name - how you reference it (params.<name>).
  • Type - float, vec2, vec3, vec4, color, bool, or texture.
  • Default - the starting value (comma-separated for vectors; a checkbox for bool; texture params show (bound separately) and take no slot floats).
  • Per-object - when ticked, each object using the material can override this parameter's value individually.

Use + Add Param to add a row and to remove one. Changing a row's type resets its default to a fresh value of that type.

Per-object params

Ticking Per-object makes a param authorable per entity: select an object with that material and the Inspector's MeshMaterial component grows a per-object overrides block listing exactly the perObject params - a checkbox for bool, a swatch + hex for color, scrubbable number boxes for float and vectors. Shared (un-ticked) params are deliberately not listed: they are one value for every object using the material, so a box there would silently do nothing.

The override map is sparse. A key exists only where that object diverges from the asset default, which is why the ↺ reset button deletes the key rather than writing the default back - the object then keeps tracking the material, so retuning the default in this panel moves every un-overridden object with it. Overrides are a declared component field, so they are saved with the scene and ride the export into the shipped game.

This is what lets one material serve many objects: author one Water, and give each pond its own tint or wave height.

The 16-float budget

Every numeric parameter is packed into a fixed 16-float per-object slot - shared params always use their default, per-object params take the override when the object has one. A meter beside the Params heading shows how many floats your parameters use (float = 1, vec2 = 2, vec3/color = 3, vec4 = 4, bool = 1; texture params take no slot floats). Vectors align to 4-float boundaries, so the packed total can round up. Go over and a warning appears - extra parameters beyond the budget won't render (the renderer zeroes the slot rather than freezing). Keep the parameter set lean.

What a new material starts as

+ New Shader does not give you four blank boxes. It seeds a material that already composes cleanly:

  • one param - tint, a color, default white, marked per-object (so the Inspector override control has something to show immediately, and assigning the fresh material changes nothing until you mean it to);
  • a hook in every slot doing a deliberately redundant self-assignment - world = world;, N = N;, albedo = albedo * params.tint;, outAlpha = outAlpha; - each with a comment naming the variable and its type.

The point is discoverability: the one genuinely non-obvious thing about this system is what the variable at each splice point is called, and a blank box can't tell you. discard can't self-assign, so it gets a commented example instead. Delete a line and that hook goes inert.

Node graph shaders

A shader does not have to be hand-written WGSL. + New Shader Graph authors the same asset as a visual node graph: wire value, texture, and math nodes into a Surface output node, and the graph compiles down to the same hooks and parameters a written shader produces. It is the same asset, edited here rather than in a separate tab - so the live mirroring, the preview, and the parameter table all work the same way. For a graph shader the Params table edits the graph's own properties (the source the Property nodes read), not a hand-authored param list.

The section below covers the canvas workflow. For the full list of node types, the Surface output ports, subgraphs, and how a graph compiles, see the Shader Graph reference.

The Node Graph canvas fills the editor body, with the live preview and the parameter table in a right rail. Drag a node to move it; drag from a port to wire it. The header above the canvas shows the node count and three controls:

  • Tidy - auto-arranges every node left to right by how far it feeds from the output (sources on the left, Surface on the right), discarding any manual positions. It commits as one undo step, so a botched layout is one ⌘Z away.
  • Fit - frames the whole graph in the view.
  • A zoom slider with a percentage readout.

The Tidy and Fit buttons sit at the left of the header's control group, before the zoom slider.

Working in the graph:

  • Add a node from the searchable palette (value, texture-sample, math, UV, and interpolator nodes, plus any subgraph you have made). A node drops at the view centre.
  • Wire a node by dragging from an output port to an input port; delete a wire by clicking it, or a node by selecting it and pressing Delete (or its ✕). The Surface node is permanent and cannot be deleted.
  • Multi-select with shift-drag to move, copy/paste (⌘C / ⌘V) or duplicate a group of nodes.
  • Group → Subgraph collapses the selected nodes into one reusable subgraph node you can drop into other graphs.
  • Some nodes edit inline: a Constant's number, a Property's or Split channel's dropdown.

The preview in the rail is context-aware: select a node and it renders that node's output on its own (unlit), so you can inspect an intermediate value; with no node selected it shows the final surface. A small badge names which you are seeing. If the selected node's output can't stand alone as a colour (an unwired subgraph output, for instance), the preview falls back to the final surface and says so.

A collapsible Generated WGSL panel in the rail shows the hook bodies the graph compiles to (syntax-highlighted, read-only), for learning what a node chain becomes or debugging a compile error.

📸 Screenshot - save as img/editor-shader-graph.png

A node-graph material open on the Node Graph canvas, with a few nodes wired into the Surface output and the Tidy / Fit controls in the header.

Live mirroring - no Save button

There is no explicit save. Every edit writes the whole material straight back into the asset store, which bumps the asset revision so the viewport re-registers the material and any object using it re-renders immediately. Renaming keeps the material's stable id, so assignments never break.

See also

  • Shader Graph - the visual node editor and its full node reference
  • Materials - the shader model and the standard PBR base
  • Asset Browser - creating and assigning shaders
  • Inspector - the MeshMaterial component and its per-object overrides
  • PBR & lighting - the lighting your hooks feed into
  • Code Panel - the same sidebar, Compile button, and Monaco editor for scripts

Awaken — browser-native WebGPU game engine.