Custom shaders in a layer
A CustomDrawable passed to
render([...]) renders in a fixed phase — "underlay" (before every layer) or
"overlay" (after every layer). That is the right tool for a full-view
background or a post-scene effect, but it cannot land between two primitives
of a single layer.
layer.pushCustom(drawable) does. It pushes a custom-shader drawable into a
layer at that exact submission point, and the drawable renders at that depth
relative to the layer’s other shapes, glyphs, and sprites — no placement flag,
no separate render list.
layer.pushCustom(plantOutline); // custom WGSL quad (outline shader)layer.pushSprite(plant); // plant sprite, drawn over its outlinelayer.pushSprite(mound); // mound, drawn over the outline's baseThe outline is a raw pipeline you own, yet it slots into the sprite stack by push order — the outline under its plant, the mound over the outline’s base — with no manual z bookkeeping.
The one contract: write ctx.depth
Section titled “The one contract: write ctx.depth”Everything a layer draws shares one unified push-order depth domain: the renderer walks the layer’s shape/glyph/sprite/custom pushes in submission order, assigns each a depth, and depth-tests them together. Your drawable joins that domain by writing the depth the renderer hands it:
import { createCustomDrawable } from "insomni";
const outline = createCustomDrawable("ui", (pass, ctx) => { // ctx.depth is this drawable's reconciled place in the layer's push order. device.queue.writeBuffer(depthUniform, 0, new Float32Array([ctx.depth])); pass.setPipeline(pipeline); pass.setBindGroup(0, ctx.cameraBindGroup); // if your shader uses the camera pass.setBindGroup(1, myBindGroup); pass.draw(4);});
layer.pushCustom(outline);// In your vertex shader — the whole author-side cost:out.position = vec4f(xy, u.depth, 1.0); // u.depth ← ctx.depthThen pick a depth mode that matches your fragment, exactly like the built-in kinds:
- Opaque output →
depthWriteEnabled: true,depthCompare: "less". It occludes anything pushed before it and is occluded by anything opaque pushed after. - Transparent output →
depthWriteEnabled: false,depthCompare: "less". It blends over what was pushed before and is covered by opaque content pushed after — the standard painter behavior sprites already use.
ctx also carries device, encoder, the applied scissor, the layer’s
space, and the frame’s colorView / depthView / viewport. Do not
begin or end a render pass — you are handed an active one.
Where it draws
Section titled “Where it draws”The pushed drawable rides alongside the layer’s sprites: in the single interleaved painter pass (no OIT), or in the post-resolve sprite pass (OIT). It is drawn once per frame, never twice.
OIT ceiling. With OIT enabled, a pushed drawable composites post-resolve, so it interleaves by depth with the layer’s opaque sprites and opaque shapes but sits over the already-resolved transparent scene — the same limitation sprites and top-level overlay drawables have. If you need to interleave into dense transparent geometry, keep that geometry in the same layer as ordinary primitives.
When to use which
Section titled “When to use which”| You want… | Reach for |
|---|---|
| A custom quad at a point in a layer’s stack | layer.pushCustom(drawable) (this page) |
| A full-view background / post-scene effect | top-level CustomDrawable with placement |
| A textured quad with a WGSL fragment, no pipeline | createTextureEffect |
See also
Section titled “See also”- Custom shaders —
createTextureEffectand the two shader shapes. - CustomDrawable — the raw render-pass hook, and its top-level
placementphases. - Layers & groups — how a layer batches shapes, glyphs, and sprites.