# Gallery (scene)
All seventeen paperBlocks papers side by side under ONE style stack, each on its own labelled stand. It is the package cover.
- **The stands.** A 6, 6, 5 grid of plinths, each carrying the same still life: a red ball, a blue cube, a green column, a yellow house with a grey gable roof and a pale plaque leaning back toward the sun. A white card in front of each names the paper and its block id (`paperKraft`). A fairly low sun from the left front rakes across the plinth tops and the plaques, so the tooth, weave, fibres and ruling catch the light, and a strong sky fill keeps the toned and dark papers lit enough to show their put-on lights.
- **One stack, every paper.** The first paper entry, `p_hotPress` (`paperHotPress`), is unmasked and covers the floor and everything around the stands. Every later paper entry (`p_coldPress`, `p_rough`, `p_board`, `p_vellum`, `p_rice`, `p_newsprint`, `p_notebook`, `p_sketchbook`, `p_parchment`, `p_canvas`, `p_kraft`, `p_tonedGrey`, `p_tonedTan`, `p_black`, `p_blackboard`, `p_greenChalkboard`) lays its sheet over it inside a world sphere around its own stand (radius 2 m, soft 0.1): styleKit's layered grounds. The washes and lines above feel each region's own paper. Try one in a single change: `StyleKit.swap("p_kraft", "paperTonedTan")`.
- **The live look, fitted to your world every time the scene loads.** With watercolorBlocks and inkBlocks installed (`StyleKit.block("wcWash")` and `StyleKit.block("inkPenLine")` both found), a watercolor wash with an ink pen line in the `earthy` pigments: `wash` (`wcWash`), `bleed` (`wcBleed`), `shadow` (`wcShadowPool`, density 0.35), `grain` (`wcGranulation`), `dry` (`wcDryBrush`, coverage 0.25), `edge` (`wcEdgeDarken`), `line` (`inkPenLine`, width 1.6) and `boil` (`inkBoil`, 8 drawings a second): 25 entries. With paperBlocks alone, styleKit's own pencil instead: `depth` (`depthLine`) under `line` (`sketchLine`), no pigment set: 19 entries. The scene file is saved with the watercolor and ink look (it was saved in a world that has them), so its entrypoint fits it on every load, in edit and in play and on every peer (`PaperGalleryLook.fit()`): where any block the saved stack names is not installed, the look entries are replaced with the look this world can draw, every paper entry keeps its mask and settings, and the title card's second line says which look it is ("One stack: a pencil line over every paper (add watercolorBlocks and inkBlocks for a wash)" with paperBlocks alone). A stack whose blocks are all installed is left exactly as it is, your own edits included. So the very first open in a paperBlocks-only world already shows pencil lines over every paper. The line's distance thinning and fade are pushed past the gallery, since its view stands far back.
Labels and the title are left out of the stack by name (`exclude = { "GalleryLabels" }`), so they stay as made after the scene is saved and loaded again, each on a white card of its own (`gallery/materials/galleryCard`, unlit). The GalleryView camera stands raised and far back on a 26 degree lens with every stand and label in the frame; it draws `all !debug !EditorUI`, so editor gizmos never reach the styled frame, and sits below the player camera (priority -10), so it never takes a joining player's view. Give it the viewport in edit with `camera.setEditorOverride(entity.find("GalleryView").id)`. Players get the normal setup and spawn in front of the gallery.
Built by `paperBlocks.PaperGallery` (stands, labels, look and paper entries), using only the engine's built-in meshes, this package's gallery materials and styleKit, so it builds in any world with paperBlocks installed. Load it with `zero scene load paperBlocks.Gallery`.
**Cost:** seventeen layered papers make 70 passes with the watercolor and ink look; a masked paper does no work outside its own sphere, so the gallery measured 14.7 ms of GPU time on high at 2097 x 1409 (33 ms before masked papers learned to skip the rest of the screen). It is a showcase, not a stack to ship in a game: a game picks one or two papers (a single paper is 0.52 to 0.74 ms high and 0.15 to 0.20 ms low at 1920 x 1080 on a fresh install, see the package README).
# PaperGallery (module)
Builds a gallery of paper swatches, one per paperBlocks paper, under one style stack. Each swatch is a plinth carrying the same still life (a red ball, a blue cube, a green column, a yellow house with a grey gable roof and a pale plaque leaning back toward the sun), with a white-card label naming the paper and its block id. The first paper (hot press) covers the floor and everything around the stands; every other paper lays its sheet over it inside a world sphere around its own stand (styleKit's layered grounds), so the same washes and lines run over every paper side by side.
- `PaperGallery.build({ columns, pitchX, pitchZ, boards, describe })` places every stand and label (and, given `boards`, a reference picture board right of each stand) and answers the stack's `entries`, the look it chose, the pigment set and the names to leave out of the stack (`exclude`).
- `PaperGallery.look()` (the same as `PaperGalleryLook.look()`) is the live look: a watercolor wash with an ink pen line when watercolorBlocks and inkBlocks are installed, styleKit's own pencil (`sketchLine` over `depthLine`) otherwise.
- `PaperGallery.setting`, `title`, `players` and `view` add the lights and floor, a title card, the normal player setup and a screenshot camera.
Uses only engine built-in meshes, this package's gallery materials and styleKit, so it builds in any world with paperBlocks installed. Used by paperBlocks' Gallery scene and the Style Cookbook's Paper Swatches recipe: `require("~paperBlocks.PaperGallery")`.
# PaperGalleryLook (module)
The live look the paperBlocks Gallery lays over every paper, and the check the Gallery runs each time it loads.
- `PaperGalleryLook.look()` answers the look entries, their kind and pigment set: a watercolor wash with an ink pen line (`wcWash`, `wcBleed`, `wcShadowPool`, `wcGranulation`, `wcDryBrush`, `wcEdgeDarken`, `inkPenLine`, and `inkBoil` when present) when watercolorBlocks and inkBlocks are installed, and styleKit's own pencil (`sketchLine` over `depthLine`) otherwise.
- `PaperGalleryLook.fit()` runs from the Gallery's entrypoint on every load, in edit and in play, on every peer. A scene file is saved once, in whichever world saved it, so the Gallery as published names the watercolor and ink blocks. Where any block it names is not installed, `fit` replaces the look entries with the look this world can draw, keeps every paper entry with its mask and settings, and rewrites the title card to say which look it is. A stack whose blocks are all installed is left exactly as it is, your own edits included.
It does no computed asset lookups, so a scene's runtime may require it.
# PaperMaterial (module)
Put an object on the same paper as the picture, inside its own materials. `PaperMaterial.paint(entityId)` gives every Model at or under the entity a material on paperBlocks' `paperSurface` shader, derived from the one it wears (its base colour and texture are kept; the original collection is never rewritten), with the settings of the paper the scene's style stack is on, tuned values included. `PaperMaterial.paint(entityId, "paperCanvas", { emboss = 0.8 })` names a paper and overrides settings; `sheet = 0` lays only the tooth's relief and tone (for when the full-frame paper still covers the object). The tooth sits on the object's own position, so it stays on it as it moves, and a `PaperSurface` component records the originals and keeps the tooth the full-frame paper's size on every screen. `PaperMaterial.unpaint(entityId, { removeFiles = true })` puts every original back and deletes the materials it wrote. `PaperMaterial.list()` names every painted entity. The derived materials are world assets and the swap is a synced change, so every player sees the same paper on the object. See the guide "How to choose a paper for my art style", section "A paper on an object".
# PaperSurface (component)
Marks an entity that `PaperMaterial.paint` put on paper: it remembers the material every Model under it wore before (`originals`), the paperSurface materials it wears now (`materials`) and the paper and settings they were written with (`settings`). These fields are synced and saved with the scene, so every player sees the same paper on the object, and `PaperMaterial.unpaint` puts every original back. About once a second on each machine it also sets the materials' `pixelsPerRef` to that machine's screen height over 1080, so the tooth keeps the full-frame paper's size in the picture on any screen; that is a runtime setting and is never written into the material files. Add it through `PaperMaterial.paint`, not by hand.
# paper (paperBlocks shader)
The main pass every paperBlocks paper shares: lays the picture on the sheet the paper's ground field published. Darks go into the sheet as transparent media and settle into the tooth's valleys; on toned and dark sheets (`lights` above 0) the picture's lights are put on in an opaque light that catches the tooth's peaks first; the tooth is lit by the scene's sun (`sk_sun`), strongest under raking light; `sheen` adds a soft shine where light reaches. Every pixel returns through styleKit's `sk_finish`, so an unlit frame stays black on every paper. Post-process shaders have no preview image.
# Black paper (paperBlocks, Ground)
Black paper for white ink: a near-black sheet with a soft tooth, the picture drawn in white. A near-black sheet with a soft tooth and faint lighter fibres.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperBlack") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Watercolor's transparent washes do not show on black; the sheet puts the picture's lights on in white ink with a little of their colour, its darks stay black paper.
- **Ink:** Ink blocks reading the paper field draw in white; pair with inkBlocks' `inkWhiteOnDark` set for the whole white-ink look.
- **Lights:** Everything is put on in white; black with every light off.
## Its defaults
`tint` #1b1a19, `toothHeight` 0.35, `toothSharpness` 0.4, `toothSize` 4, `fibre` 0.15, `fibreLength` 6, `fibreTone` 0.25, `mottle` 0.05, `thirst` 0.5, `emboss` 0.25, `sheen` 0, `lights` 0.75, `lightColour` #f4f3ee, `lightTint` 0.15, `lightsFrom` 0.1. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Blackboard (paperBlocks, Ground)
Dark slate blackboard with chalk dust and ghosts of old wiping, dead matte: the picture reads as light chalk marks on dark. Dark slate with a fine sharp grain, dusty smears of old wiping and chalk specks; no sheen at all.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperBlackboard") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Transparent watercolor cannot show on slate, so the board turns the painted picture into chalk: its lights become chalk marks (keeping a third of their colour, like coloured chalk) that catch the grain, its darks are bare board. Washes read as soft chalk tone; granulation and dry brush read as chalk breaking on the grain.
- **Ink:** Ink blocks that read the paper field draw in light chalk instead of dark ink, so lines show; a block that does not read it would draw dark lines that vanish into the board.
- **Lights:** Everything you see is put on. The board shows only where the scene has light, so with every light off it is black, not slate.
## Its defaults
`tint` #2a2e2d, `toothHeight` 0.4, `toothSharpness` 0.75, `toothSize` 3, `fibre` 0, `fibreLength` 4, `fibreTone` 0, `mottle` 0.25, `thirst` 0.05, `emboss` 0.15, `sheen` 0, `lights` 0.6, `lightColour` #ecebe3, `lightTint` 0.35, `lightsFrom` 0.15, `dust` 0.6. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Illustration board (paperBlocks, Ground)
Hard, smooth, even illustration board: a bright flat ground for clean ink and controlled washes. The smoothest sheet: almost no tooth, perfectly even tone, a hard surface.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperBoard") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Washes lie flat and even with crisp edges; almost no granulation or dry-brush break.
- **Ink:** Cleanest lines of all papers: no breaks, no feathering.
- **Lights:** Lights are bare board.
## Its defaults
`tint` #f7f6f1, `toothHeight` 0.08, `toothSharpness` 0.5, `toothSize` 3, `fibre` 0, `fibreLength` 4, `fibreTone` 0, `mottle` 0.02, `thirst` 0.25, `emboss` 0.06, `sheen` 0.03, `lights` 0. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Canvas (paperBlocks, Ground)
Primed canvas: a plain weave of threads you can see through the paint and the ink. A plain over-and-under weave of slubby threads on an off-white primed ground; the weave replaces the felt tooth.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperCanvas") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Paint settles in the gaps between threads and thins on their crowns, so the weave shows through every wash; granulation and dry brush follow the threads.
- **Ink:** Lines break on the thread crowns in a regular rhythm.
- **Lights:** Lights are the bare primed canvas. The weave catches raking light strongly (`emboss` 0.6).
## Its defaults
`tint` #eee7d6, `toothHeight` 0.85, `toothSharpness` 0.3, `toothSize` 7, `fibre` 0.1, `fibreLength` 4, `fibreTone` 0, `weave` 0.9, `mottle` 0.06, `thirst` 0.35, `emboss` 0.6, `sheen` 0.05, `lights` 0. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Cold press (paperBlocks, Ground)
Cold-press watercolor paper: a rough felt tooth on a warm white sheet, the standard paper watercolor and ink blocks were tuned on. Rough felt tooth with a few cotton fibres on a warm white sheet. The tooth is tall enough that paint visibly settles into its valleys and a raking sun picks out every bump.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperColdPress") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Granulation settles into the valleys, dry brush skips over the peaks and leaves flecks of bare paper, washes bleed and rim exactly as the watercolor blocks were tuned (thirst 0.5 is their neutral).
- **Ink:** A pen line catches on the bumps and breaks a little where the tooth rises; a brush line drags a slightly ragged edge.
- **Lights:** Lights are bare paper: nothing is put on.
## Its defaults
`tint` #f6f1e5, `toothHeight` 0.75, `toothSharpness` 0.35, `toothSize` 6, `fibre` 0.2, `fibreLength` 6, `fibreTone` 0, `mottle` 0.12, `thirst` 0.5, `emboss` 0.45, `sheen` 0, `lights` 0. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# paperField (paperBlocks shader)
The ground field every paperBlocks paper publishes: rgb the sheet's colour with its tone (tooth shading, fibres, mottling, foxing, ruled lines, chalk dust and wiping ghosts), a the tooth height that granulation, dry brush and ink lines read. A later paper in the stack lays its field over the one before by its mask, so parts of a world can sit on different papers. Post-process shaders have no preview image.
# Green chalkboard (paperBlocks, Ground)
A green school chalkboard beside the black slate: dusty, matte, the picture drawn in chalk. A painted green board, lighter than slate, with heavier dust and wiping.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperGreenChalkboard") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** As on the blackboard: the picture becomes chalk, its darks the green board.
- **Ink:** Ink blocks reading the paper field draw in chalk.
- **Lights:** Everything is put on in chalk; black with every light off.
## Its defaults
`tint` #2e4a3b, `toothHeight` 0.4, `toothSharpness` 0.7, `toothSize` 3, `fibre` 0, `fibreLength` 4, `fibreTone` 0, `mottle` 0.22, `thirst` 0.05, `emboss` 0.15, `sheen` 0, `lights` 0.6, `lightColour` #eef0e6, `lightTint` 0.3, `lightsFrom` 0.15, `dust` 0.7. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Hot press (paperBlocks, Ground)
Hot-press paper: smooth, almost no tooth, a crisp white sheet that keeps wash edges hard and ink lines exact. Smooth and hard-sized: barely any tooth, a cool crisp white and a faint sheen.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperHotPress") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Granulation has almost nothing to settle into, so it reads as a soft even speckle; dry brush drags streaks rather than flecks; the bleed barely runs and dried rims are darker and harder (the sheet is sized, so pigment gathers at the edge).
- **Ink:** Lines stay exact and crisp, with no feathering and almost no breaks.
- **Lights:** Lights are bare paper.
## Its defaults
`tint` #f9f8f3, `toothHeight` 0.12, `toothSharpness` 0.6, `toothSize` 3, `fibre` 0.05, `fibreLength` 4, `fibreTone` 0, `mottle` 0.03, `thirst` 0.15, `emboss` 0.12, `sheen` 0.08, `lights` 0. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Kraft (paperBlocks, Ground)
Brown, fibrous kraft paper: a mid-tone ground where darks are laid in and the picture's lights are put on in white gouache. A warm brown sheet full of darker fibres. The sheet itself stands for the picture's middle value.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperKraft") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Transparent washes only darken the brown: shadows and colour sink in as darker brown. Everything lighter than the sheet is put on in white gouache that catches the tooth first, so lit planes, sky and highlights come back as opaque lights.
- **Ink:** Ink lines stay dark and read clearly on the brown.
- **Lights:** This is the paper where lights must be put on: `lights` 0.85 puts most of them on in `lightColour`; turn it down for a darker, moodier sheet, up for brighter lights. A scene lit only dimly reads mostly as bare brown.
## Its defaults
`tint` #ad875b, `toothHeight` 0.45, `toothSharpness` 0.4, `toothSize` 4, `fibre` 0.6, `fibreLength` 9, `fibreTone` -0.45, `mottle` 0.2, `thirst` 0.6, `emboss` 0.3, `sheen` 0, `lights` 0.85, `lightColour` #f7f3ea, `lightTint` 0.25, `lightsFrom` 0.45. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Newsprint (paperBlocks, Ground)
Cheap grey-cream newsprint, a little yellowed, with a fine flat grain: thirsty, so ink goes slightly fuzzy. A grey-cream, slightly yellowed sheet with a fine, flat grain and short dark fibre flecks.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperNewsprint") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Washes go dull (the sheet is grey) and spread; granulation is a fine even grain.
- **Ink:** Lines feather slightly and look printed on cheap stock; good for comic and zine looks.
- **Lights:** Lights are the bare grey-cream sheet: whites never reach white.
## Its defaults
`tint` #e2dccb, `toothHeight` 0.25, `toothSharpness` 0.7, `toothSize` 2.5, `fibre` 0.15, `fibreLength` 3, `fibreTone` -0.4, `mottle` 0.08, `thirst` 0.8, `emboss` 0.1, `sheen` 0, `lights` 0. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Ruled notebook (paperBlocks, Ground)
A ruled notebook page: smooth bright paper with pale blue lines, a little thirsty, for doodles and school-book looks. Smooth bright paper with pale blue ruled lines laid on every surface, about 30 reference pixels apart.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperNotebook") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Washes spread a little (cheap paper), the ruling shows through them.
- **Ink:** Lines feather slightly, like a ballpoint on cheap paper.
- **Lights:** Lights are the bare page. The lines keep their spacing in the picture and fade to a flat tint where they would be too thin to draw.
## Its defaults
`tint` #fbfaf5, `toothHeight` 0.15, `toothSharpness` 0.5, `toothSize` 3, `fibre` 0.05, `fibreLength` 4, `fibreTone` 0, `mottle` 0.03, `rule` 0.55, `ruleColour` #9db4d4, `ruleSpacing` 30, `thirst` 0.7, `emboss` 0.08, `sheen` 0, `lights` 0. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Parchment (paperBlocks, Ground)
Warm parchment: blotchy, uneven tone with a soft sheen where light rakes it, for old maps and documents. A warm, blotchy skin with strong uneven tone (warmer where darker), little tooth and a soft sheen.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperParchment") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Washes sit on top and take on the blotches; the sheet is fairly sized, so bleeds are short and rims show.
- **Ink:** Lines are crisp, with a slight spread in the thirstier blotches.
- **Lights:** The brightest lights are put on faintly (`lights` 0.15) so highlights still read on the warm sheet. Its sheen is a soft shine where the sun's reflection or a grazing view meets the surface; it adds light only where the scene has light.
## Its defaults
`tint` #e9d5aa, `toothHeight` 0.25, `toothSharpness` 0.2, `toothSize` 7, `fibre` 0.15, `fibreLength` 8, `fibreTone` 0, `mottle` 0.75, `foxing` 0.05, `thirst` 0.3, `emboss` 0.25, `sheen` 0.45, `lights` 0.15, `lightColour` #fbf3df, `lightTint` 0.2, `lightsFrom` 0.6. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Rice paper (paperBlocks, Ground)
Thin, soft rice paper with long visible fibres: thirsty, so washes spread and feather and ink lines bleed into the fibres. A thin, soft sheet with long fibres running through it and a cloudy formation; very little tooth.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperRice") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** The wettest paper: the bleed runs much further, edges feather along the fibres, dried rims all but vanish; granulation is faint, there is little tooth to hold it.
- **Ink:** Lines spread wider and feather at their edges, more where fibres run; ink wash painting looks its most natural here.
- **Lights:** Lights are bare paper.
## Its defaults
`tint` #f1ecdf, `toothHeight` 0.3, `toothSharpness` 0.15, `toothSize` 5, `fibre` 0.85, `fibreLength` 18, `fibreTone` 0.35, `mottle` 0.18, `thirst` 0.95, `emboss` 0.2, `sheen` 0, `lights` 0. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Rough (paperBlocks, Ground)
Rough watercolor paper: the deepest, largest tooth, for loose washes full of granulation and broken dry-brush strokes. A heavy, deep tooth half again larger than cold press. Every wash sits in pits and every stroke breaks on the hills.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperRough") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Granulation is at its strongest, dry brush leaves the most bare paper; washes still bleed a little more than on cold press.
- **Ink:** Lines break often on the peaks; a dry brush line comes out scratchy. Use a heavier width if a clean outline matters.
- **Lights:** Lights are bare paper.
## Its defaults
`tint` #f5f0e4, `toothHeight` 1, `toothSharpness` 0.45, `toothSize` 9, `fibre` 0.15, `fibreLength` 5, `fibreTone` 0, `mottle` 0.1, `thirst` 0.55, `emboss` 0.6, `sheen` 0, `lights` 0. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Aged sketchbook (paperBlocks, Ground)
An aged, foxed sketchbook page: yellowed, mottled, with brown age spots showing through the drawing. A yellowed cartridge page with uneven tone and brown foxing spots.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperSketchbook") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Washes are warmed by the yellowed sheet and the foxing shows through them.
- **Ink:** Lines behave as on cold press, slightly softer.
- **Lights:** Lights are the yellowed sheet.
## Its defaults
`tint` #ecdcbb, `toothHeight` 0.45, `toothSharpness` 0.3, `toothSize` 5, `fibre` 0.25, `fibreLength` 6, `fibreTone` -0.15, `mottle` 0.35, `foxing` 0.55, `thirst` 0.55, `emboss` 0.3, `sheen` 0, `lights` 0. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# paperTakes (paperBlocks shader)
The paper field every paperBlocks paper publishes for blocks that read it (styleKit `skPaper`): thirst (how far a wash spreads and a line feathers), sheen, fibre and whether the sheet is dark (so ink turns light). Made only when a block in the stack reads `paper`. Post-process shaders have no preview image.
# Toned grey (paperBlocks, Ground)
Toned grey drawing paper: a mid-grey ground, darks laid in, lights put on in white chalk and gouache. A neutral mid-grey drawing sheet with a medium tooth.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperTonedGrey") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Washes darken the grey; everything lighter than the sheet is put on in white that catches the tooth.
- **Ink:** Ink lines stay dark.
- **Lights:** Lights are put on (`lights` 0.8): the classic three-value drawing on toned paper.
## Its defaults
`tint` #9c9b96, `toothHeight` 0.5, `toothSharpness` 0.4, `toothSize` 4, `fibre` 0.2, `fibreLength` 6, `fibreTone` -0.2, `mottle` 0.08, `thirst` 0.45, `emboss` 0.3, `sheen` 0, `lights` 0.8, `lightColour` #f8f8f4, `lightTint` 0.3, `lightsFrom` 0.4. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Toned tan (paperBlocks, Ground)
Toned tan drawing paper: a warm mid-tone ground with lights put on in white. A warm tan sheet, lighter and smoother than kraft.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperTonedTan") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Washes are warmed and darkened by the tan; lights are put on in warm white.
- **Ink:** Ink lines stay dark; sepia ink sits naturally on it.
- **Lights:** Lights are put on (`lights` 0.75).
## Its defaults
`tint` #c8ab83, `toothHeight` 0.5, `toothSharpness` 0.4, `toothSize` 4, `fibre` 0.25, `fibreLength` 6, `fibreTone` -0.2, `mottle` 0.1, `thirst` 0.5, `emboss` 0.3, `sheen` 0, `lights` 0.75, `lightColour` #fbf6ec, `lightTint` 0.3, `lightsFrom` 0.4. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# Vellum (paperBlocks, Ground)
Smooth vellum: a cool cream sheet with a cloudy formation and a satin sheen, very hard-sized. Glassy smooth, with a soft cloudy formation and a satin sheen.
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.swap("paper", "paperVellum") -- replaces the stack's paper in one change
```
## What it does to each kind of block
- **Watercolor:** Washes do not soak in: no bleed, hard dark rims, almost no granulation.
- **Ink:** The crispest lines after board, with a slight sheen around them in raking light.
- **Lights:** Lights are the bare sheet.
## Its defaults
`tint` #f3f0e7, `toothHeight` 0.05, `toothSharpness` 0.3, `toothSize` 6, `fibre` 0.08, `fibreLength` 10, `fibreTone` 0.2, `mottle` 0.15, `thirst` 0.08, `emboss` 0.05, `sheen` 0.25, `lights` 0. Every other shared setting is at the shared default (no weave, ruling, foxing or dust).
## How it moves and what it covers
- **Motion:** the tooth, fibres, weave, blotches and spots are laid on the surfaces they cover (world position along each surface's main axis), so they stay on still things when the camera pans, dollies or zooms; each keeps about the same size in the picture by growing into the next octave rather than sliding, and anything finer than about two drawn pixels fades to its mean, so the distance never shimmers. On a moving prop or a skinned character the sheet slides across it (a full-frame pass has no per-object position).
- **Sky and fog:** the sky gets the same sheet, laid on the view direction, so it stays put as you turn; fog is part of the picture and is laid on the paper like any other colour.
- **Transparent things and particles** have no depth of their own, so the sheet under them is the sheet of the surface behind.
- **Foliage** takes the sheet like any surface; very fine leaves read through its tooth.
- **World text and UI:** the screen UI is drawn after the stack and stays readable; world labels take the paper unless they are left out (`StyleKit.options({ exclude = { "LabelName" } })`, ideally on a card of their own).
- **Black stays black:** every pixel returns through styleKit's shared exit and the sheet only multiplies or is lit by the scene's own light, so with every light off the frame is black, never paper coloured.
- **Low quality:** the same picture at half resolution; parts too fine for the smaller image are laid as their average instead of aliasing.
## Swapping
Every paperBlocks paper shares the same settings, so `StyleKit.swap("paper", "<other paper>")` changes the sheet in one change and swapping back to the paper you had (`inkPaper`, `wcColdPress`, `plainPaper` or another paperBlocks paper) puts back exactly the settings it had. Keep your tuning across papers with `StyleKit.swap("paper", "<other paper>", { keepSettings = true })`.
# paperCore (shader module)
The one paper model every paperBlocks paper is built from. `pb_sample(uv)` returns the sheet at a pixel: its height (felt tooth and grit, long fibres, canvas weave), its tone (tooth shading, fibre tone, blotchy mottling, foxing spots) and how much fibre is there. `pb_ground_colour` adds ruled lines and, on chalkboards, dust and the ghosts of old wiping. `pb_published_height` gives the height the shared ground carries (centred on 0.5, as deep as the tooth is tall, so granulation and dry brush feel exactly how much tooth there is). `pb_field_finish` lets a later Ground block lay its field over the one before by its mask.
Everything sits on surfaces (world position along each surface's main axis, the view direction on the sky), keeps about the same size in the picture by blending two octaves, and fades any part finer than about two drawn pixels to its mean, so the tooth stays on things under a pan, dolly or zoom and never shimmers in the distance or on the low tier. Pure math over styleKit's skCommon, skPatterns and skAnchored; it reads the paper settings every paperBlocks shader declares (tint, toothHeight, toothSharpness, toothSize, fibre, fibreLength, fibreTone, weave, mottle, foxing, rule, ruleColour, ruleSpacing, thirst, emboss, sheen, lights, lightColour, lightTint, dust).
# paperSurface (paperBlocks surface shader)
A paperBlocks paper on an object's own material, for a hero object, a character or a chosen set that should sit on the same paper as the rest of the picture. It takes the same named settings every paperBlocks paper has (tint, toothHeight, toothSharpness, toothSize, fibre, fibreLength, fibreTone, weave, mottle, foxing, rule, ruleColour, ruleSpacing, emboss, sheen, lights, lightColour, lightTint, lightsFrom), so a paper's defaults copy across as they are, plus the object's own `base_color`, `base_color_texture` and `metallic`, `sheet` (1 lays the whole paper on the object, 0 only the tooth's relief and tone) and `pixelsPerRef` (screen height / 1080, kept current by the `PaperSurface` component that `PaperMaterial.paint` adds).
The tooth, fibres, weave, blotches, spots and rules are laid on the object's own position, so they stay on it when it moves or animates as a whole, and they keep about the full-frame paper's size in the picture (two octaves blended by how many metres one pixel covers), fading any part finer than about two drawn pixels. The tooth bends the surface normal, so every light in the scene rakes across it through the engine's own PBR lighting; `sheen` lowers the roughness so the sheet shines where light reaches it. The picture is laid on the sheet by the full-frame paper pass's own rule (darks into the sheet and its valleys, lights put on on toned and dark papers), and nothing is ever brighter than the light that reached it, so an unlit object stays black.
Apply it with `PaperMaterial.paint(entityId, "paperColdPress")` rather than by hand: that writes derived materials that keep each original's colour and texture, copies the paper's defaults, keeps `pixelsPerRef` current, and `PaperMaterial.unpaint` puts the originals back. Skinned characters: the position is the bind-pose position, so the tooth stays on the body as it animates. Post-process shaders aside, a surface shader preview shows it on a sphere.
# paperTooth (shader module)
The pure math every paperBlocks paper is made of: `pb_tooth` (felt tooth and grit, by sharpness), `pb_fibres` (long fibres), `pb_weave` (a plain canvas weave), `pb_rules` (ruled lines), `pb_foxing` (age spots) and `pb_keep` (how much of a part to keep at a given drawn size). It reads no screen inputs, so the full-frame papers (through `paperCore`) and the `paperSurface` material lay exactly the same tooth. Include it after styleKit's `skPatterns`. Every `px` argument is drawn pixels per tooth unit, and any part finer than about two drawn pixels fades to its mean, so the tooth never shimmers.
# paperBlocks test suite
Checks every paper against styleKit's block rules and the paper rules: each is a Ground block that lays its ground over the one before by its mask and publishes the paper field, all seventeen share exactly the same named settings with defaults inside their ranges, a paper swaps in and out in one change and swapping back puts back exactly the settings it replaced, `keepSettings` carries tuning across papers, the paper field is made only when a block reads it, and two papers layer by mask; a paper put on an object with `PaperMaterial.paint` keeps the object's colour, changes the picture, and comes off with `unpaint` leaving the frame exactly as before and no files behind. On the presented frame, over a small lit set of its own: putting a paper on changes the picture and taking it off puts the frame back exactly (every paper, twice); the same stack reads clearly differently on hot press, cold press and canvas; and with every light off the frame stays black on every paper, the dark ones included (measured at the Lookbook's every-light-off moment, so it needs lookbookKit's modules, which an install of paperBlocks brings). Checks that need watercolorBlocks or inkBlocks (the same stack with a wash and an ink line, the paper field being read) run where those packages are installed and stand down as skipped, saying why, where they are not.
# paperWorld
Put an existing world, scene or installed kit on paper with paperBlocks and styleKit, and leave it undoable.
## What it does
1. **Survey.** Reads the scene's stack exactly as it is (every entry with its id, block, strength, on, mask and settings, and the stack options), finds its Ground entry if it has one, and lists the lights (is there a sun, is it low), the screen UI, the world labels, the characters and the colours that carry gameplay meaning. The recorded stack is the undo.
2. **Choose.** Picks one of the seventeen papers from the reference picture (when one is given) or the asker's words, with the reason, what the paper needs from the scene's light, and the settings to start from. On a toned or dark sheet it says honestly what happens: a white paper's lights become put-on gouache, chalk or white ink, and transparent washes on a blackboard or black paper become chalk or white ink.
3. **Apply.** In one call: `StyleKit.swap(<the Ground entry>, <paper>)` when the stack has a Ground block (strength and mask stay, and the entry remembers the old paper's settings), or `StyleKit.add(<paper>, { id = "paper" })` when it has none.
4. **Tune.** At most two rounds of `StyleKit.set` on the paper's shared settings (tooth, thirst, lights, sheen, mottle, dust) against the reference and the scene's light, on presented frames.
5. **Check.** Screen UI and world labels readable over the palest and darkest parts of the sheet, characters and gameplay colours still read, black stays black with the lights off, the low tier, and the Lookbook's numbers for the stack before and after when lookbookKit is installed (it says so when it is not).
6. **Report.** The paper chosen and why, the stack before and after, every setting changed, the checks, the numbers, and the undo.
**Undo.** Nothing outside the stack is changed. When the paper was swapped in, `StyleKit.swap(<entry>, <old paper>)` puts the old paper back with exactly its settings (styleKit 1.2.0 swap memory), and the frame returns as it was. When it was added, `StyleKit.remove("paper")` takes it off. The report also carries the full `StyleKit.load(...)` call of the stack from before the run, which restores everything regardless of what happened since.
## What it needs
paperBlocks installed (it brings styleKit 1.2.0). watercolorBlocks and inkBlocks are optional: their bleed, rims and ink lines read the paper's thirst and dark ground when they are in the stack. The Lookbook check uses lookbookKit when the world has it.
## Starting it
zero workflow start --name paperWorld --args '{"subject":"main","paper":"brown kraft with white gouache highlights"}'
`subject` names the world, scene or kit (default: the loaded scene); `paper` is the paper in the asker's words or a block id (default: the run proposes one); `reference` is an optional path to a reference picture.
# styleBlock (asset type)
A styleBlock is one stackable piece of an art style. It sits in one of ten slots (Surface, Light, Fill, Tone, Line, Colour, Ground, Time, Raster, Medium), it states what it reads and what it writes, and the styleKit stack runs it over the whole frame after antialiasing. Any block swaps for any other block of its slot with no other change.
## Folder shape
A block is a plain folder holding the block asset and its shader side by side:
```
blocks/posterize/
posterize.styleBlock/ the block: block.yaml, README.md, .metadata (all required)
posterize.shader/ the post-process shader the stack runs
posterizeField.shader/ optional: the shader that writes a shared field (Ground or Time blocks)
posterize.shaderModule/ optional: the piece a material includes for a chosen set of objects
```
Paths in `block.yaml` are relative to the plain folder. The shader sits beside the `.styleBlock` rather than inside it, because a shader nested inside another asset folder is not listed by the engine and could not be found without a computed lookup that would block publishing.
The quickest way to start one is `StyleKit.newBlock("myTint", { slot = "colour" })` (written to `/zero/source/myBlocks/myTint`, or the `folder` you pass), which writes the whole folder, block and shader, and registers it at once. `asset.create("styleBlock", name, { slot = "line" })` writes only the `.styleBlock` files; write the `<name>.shader` beside it yourself.
## block.yaml
```yaml
name: "Posterize"
slot: colour # surface light fill tone line colour ground time raster medium
version: "1.0.0"
summary: "Cuts the picture into a few flat tone steps."
reads: [frame] # frame depth normal albedo motion ground wobble clock mask soft pigments rendered paper sun
writes: [frame] # frame, and field:ground, field:paper (Ground) or field:wobble (Time) when it has a field
stacking: layer # optional: layer (default) or chain
rank: 10 # optional: StyleKit.add places it before the first block of its slot with a higher rank
shader: posterize.shader # relative to the plain folder
field: { name: ground, shader: plainPaperField.shader } # optional, Ground and Time only
include: posterize.shaderModule # optional
settings: # every non-sk_ property of the shader, with what it means
levels: "How many steps each channel keeps."
defaults: # optional: this block's own defaults over the shader's (many blocks, one shader)
levels: 4
quality: # optional per-tier overrides of settings
low: { samples: 4 }
```
A Ground block's `field` may also name a paper-field shader and say that it layers (styleKit 1.2.0):
```yaml
field: { name: ground, shader: paperField.shader, paper: paperTakes.shader, layers: true }
writes: [frame, field:ground, field:paper]
```
- `paper`: a shader that publishes the paper field (`sk_paper`: r thirst -1 hard-sized to +1 thirsty, 0 the standard sheet; g sheen; b fibre; a dark ground 0 to 1) for blocks that read `paper`. It is run only when a live block reads it. A Ground block without one leaves the paper field neutral (every value 0). Add `field:paper` to `writes`.
- `layers: true`: when this block is not the first Ground block in the stack, it lays its ground (and its paper field) over the one before by its strength and mask, instead of being ignored as a later field. Its field shaders receive the field before as `sk_ground` / `sk_paper` and mix over it (`sk_view.y` is 1 when they are layered).
`defaults` lets several blocks share one shader (paperBlocks' seventeen papers run on one): each key must be a setting the shader declares, it overrides the shader's default for this block on every tier (`quality` overrides come after it, an entry's own settings last), and `StyleKit.settingsOf(id)` reports it as the block's default.
The shader's `properties.yaml` starts with the styleKit shared header (the `sk_` properties) and then declares the block's own settings with their defaults and ranges. `StyleKit.checkBlock(id)` reports anything missing.
## Stacking and rank
- `stacking: layer` (the default): every block in a slot reads the slot's input, and later ones are laid over the earlier ones by strength and mask.
- `stacking: chain`: the block reads what the blocks before it in its slot made, so modifier blocks build on each other (a wash, then its granulation, then its darkened edges).
- `rank: <number>`: `StyleKit.add` places the block before the first block of its slot with a higher rank, so a chain lands in its intended order whatever order its blocks are added in.
## Extra inputs
Five inputs come after the shared header, each declared straight after it and bound by the stack only when `reads` lists the matching word:
- `sk_soft` (`reads: soft`): a half-resolution, depth-aware blur of what the block reads, never smearing a nearer thing over a farther one. Read it through the `skSoft` shader module (`sk_soft_at`, `sk_soft_wide`, `sk_soft_depth`, `sk_soft_in_front`). Its width is the stack option `softRadius`.
- `sk_pigments` (`reads: pigments`): the scene's pigment set as a 16 x 1 palette. Read it through the `skPigmentSet` shader module (`skg_has_set`, `skg_pigment`, `skg_shadow`, `skg_light`, `skg_air`, `skg_remix`).
- `sk_rendered` (`reads: rendered`): the frame as it entered the stack, before any block changed it. Read it through the `skRendered` shader module (`sk_rendered_at`). Its use is a light estimate against `sk_albedo` (texture cancels out) or an original colour to restore.
- `sk_paper` (`reads: paper`, a texture, default black): the paper field the Ground blocks publish. Read it through the `skPaper` shader module (`sk_thirst`, `sk_sheen`, `sk_fibre`, `sk_dark_ground`). Where no Ground block publishes it every value is 0, so the block draws as it did without it. A Ground block's own paper-field shader declares it to read the field laid before it, without listing `paper` under reads.
- `sk_sun` (`reads: sun`, a float4): the direction toward the scene's first directional light, `w` 1 when it gives light. Read about once a second. Raking light and sheen use it.
`StyleKit.checkBlock` refuses a block that declares one of them without the word under `reads`, or lists the word without declaring the texture.
When an asset of this type registers on any machine (world load, install, a collaborator's new block arriving), the type's `onRegister` hands it to the styleKit library, so `StyleKit.blocks()` lists it and a stack can use it at once.
# styleKit
Build your own art style out of stackable look blocks: a look is a stack of small blocks (a wash, an ink line, a paper, a grain) that runs over the whole frame after antialiasing, lives in the scene and looks the same for every player.
Version 1.2.2.
## Install and use
Install **styleKit** from ZeroMind (search "styleKit" or "stackable art style blocks"). Installs with several dependencies can be slow (#21420), so give an install time to finish and check `/zero/source` for the package folder. Then, from the console or any script, stack one of each of the kit's four example blocks:
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.add("posterize") -- Colour: a few flat tone steps
StyleKit.add("duotone", { strength = 0.5 }) -- Colour: two inks, laid half over the posterize
StyleKit.add("depthLine") -- Line: a clean ink outline from depth
StyleKit.add("sketchLine", { strength = 0.6 }) -- Line: soft pencil strokes laid over the outline
print(#StyleKit.describe().entries) -- 4
StyleKit.clear() -- the frame is exactly as rendered again
```
No material is edited. The first `add` makes a synced `StyleStack` entity in the scene, so every player draws the same stack; put a `StyleStack` component in your scene's build to keep a look with the scene.
## The blocks in this package
| Block | Slot | What it does |
|---|---|---|
| `posterize` | Colour | Cuts the picture into a few flat tone steps, like a screen print. |
| `duotone` | Colour | Maps the picture onto a shadow ink and a light ink. |
| `depthLine` | Line | A clean, one-sided ink outline from depth plus creases from normals. |
| `sketchLine` | Line | Soft, broken pencil strokes over shape and colour edges, redrawn on the held clock. |
| `sharedView` | Medium | A diagnostic for block authors: shows one shared input in place of the picture. |
Every block lists its slot and its tags (the words a person would search for: `watercolor`, `pencil`, `halftone`, `pixel art`, `chalk`), so you can pick blocks by tag with `StyleKit.blocks({ tag = "pencil" })` or `zero styleKit list --tag pencil`. A block's tags come from its `block.yaml` `tags:` list when it has one, otherwise from its `.metadata` tags.
More blocks come in their own packages and plug straight in: `starterBlocks` has a block for every slot (card surface, light bands, flat wash, surface dots, plain paper, dark board, line boil, chunky pixels, soft grain, vignette), `watercolorBlocks` has ten chained watercolor Fill blocks and a cold-press paper, with pigment sets, whole looks and a hero material, `inkBlocks` has five ink lines, four ink fills, ink colours and whole looks, and `paperBlocks` has seventeen papers (cold press, rough, hot press, rice, newsprint, kraft, parchment, canvas, board, blackboard, green chalkboard, black paper, aged sketchbook, toned grey and tan, ruled notebook, vellum) that publish the paper field and layer by region. `StyleKit.blocks()` lists every block installed. A package can also ship pigment sets (`pigmentSet` assets: `StyleKit.pigmentSets()`) and whole looks (`styleLook` assets: `StyleKit.looks()`) next to its blocks.
## How a stack works
A stack is an ordered list of entries `{ id, block, on, strength, mask, settings }` on a `StyleStack` component. Blocks sit in ten slots that always run in this order:
1. **Surface**: what things are made of.
2. **Light**: how surfaces take light.
3. **Fill**: how areas of colour are laid.
4. **Tone**: marks that carry shade.
5. **Line**: edges.
6. **Colour**: palette and grade.
7. **Ground**: what it is drawn on. The first Ground block publishes the shared ground (and the paper field, when it has one); a later Ground block whose field says `layers: true` lays its ground over the one before by its strength and mask.
8. **Time**: how motion is held. The first Time block publishes the shared wobble.
9. **Raster**: how the image is sampled.
10. **Medium**: what it is seen through.
Any slot can be empty. Any block swaps for any other block of its slot with no other change (`StyleKit.swap(entry, otherBlock)`); strength and mask stay. Inside a slot the list order decides: the first block reads the slot's input, and every later block in the same slot reads the same input and is laid over it by its strength and mask (`stacking: layer`, the default). So two Line blocks with weights 1 and 0.4 blend 60 / 40, and two Fill blocks with complementary masks split the frame. A block whose `block.yaml` says `stacking: chain` reads what the blocks before it in its slot made instead, so modifier blocks build on each other (a wash, then its granulation, then its darkened edges). A block's `rank: <number>` decides where `StyleKit.add` places it: before the first block of its slot with a higher rank, so a chain lands in its painting order whatever order its blocks are added in.
### Keeping a stack
A stack made with these calls is a live edit of the scene, like moving a prop. Save the scene (`layers.active:save()` or the editor's save) and the stack is in its `scene.json`: it comes back on every load and after a restart, the same for every player. `StyleKit.clear()` takes every block off the frame; to get the saved stack back, revert the scene (`tools.use("scene", "revert")`). `scene.reload` keeps live edits by saving them first, so a clear followed by a reload is a saved clear. Keep one StyleStack per scene.
### Shared by every block in a stack
- **One clock.** `sk_time()` and the held clock `sk_held()` / `sk_step()`, stepped at the stack's `holdRate` (8 per second by default). Every wobble and boil runs on the held clock.
- **One set of coordinates that stay on things.** `sk_surface_coord(uv, perMetre)` lays marks on surfaces from world position along each surface's main axis, and on the sky from the view direction, so a pan or a dolly does not drag them.
- **Surface data copied once per frame**: view distance, world position, normal and base colour (`sk_dist`, `sk_world`, `sk_normal`, `sk_albedo`).
- **One ground** (`sk_ground_at`): paper colour and grain height, published by the first Ground block (plain white paper with flat grain when there is none).
- **Layered grounds** (since 1.2.0): a later Ground block whose `block.yaml` field says `layers: true` lays its ground, and its paper field, over the one before by its strength and mask, so regions of a world sit on different papers: `StyleKit.add("paperParchment", { id = "mapRoom", mask = { kind = "sphere", center = { 30, 2, 10 }, radius = 6 } })`. A Ground block without `layers: true` publishes only when it is the first.
- **One paper field** (`sk_paper`, read through `skPaper`, since 1.2.0): how the sheet under each pixel takes paint and ink, for a block that lists `paper` under `reads`: `sk_thirst(uv)` (-1 hard-sized, 0 the standard sheet blocks are tuned on, +1 thirsty), `sk_sheen(uv)`, `sk_fibre(uv)` and `sk_dark_ground(uv)` (1 where the sheet is dark and the picture is made of light marks). A Ground block publishes it with a `field.paper` shader; where none does every value is 0, so a block reading it draws exactly as before. It is only made when some live block reads it. paperBlocks' papers publish it; watercolorBlocks' bleed and edge darkening and every inkBlocks block read it.
- **The scene's sun** (`sk_sun`, a float4, since 1.2.0): for a block that declares it and lists `sun` under `reads`, the direction toward the scene's first directional light, `w` 1 when it gives light (0, and a default direction, when there is none). Read about once a second through the lighting toolbox, so a moving sun reaches blocks within a second. Raking light on paper tooth and sheen use it.
- **One wobble** (`sk_wobble_uv`): an offset published by the first Time block (still when there is none). A wash and an ink line that both read it tremble together.
- **One pattern library** (`skPatterns`): noise, fractal noise, cells, dots, hatching, ordered threshold, triplanar noise for materials (`skp_triplanar_fbm`) and brush strokes (`skp_stroke`).
- **One soft field** (`sk_soft`, read through `skSoft`): a half-resolution, depth-aware blur of what a block reads, for a block that lists `soft` under `reads`. Each tap is tested against the centre surface's plane rebuilt from world positions, so a nearer thing is never smeared over a farther one. The stack makes it for the first block that reads it and makes it again whenever a later reader reads a different image; its width is the stack option `softRadius` (reference pixels, 10 by default).
- **One pigment set** (`sk_pigments`, read through `skPigmentSet`): the scene's palette of paints as a 16 x 1 texture, for a block that lists `pigments` under `reads`. `StyleKit.setPigments(id)` changes it for every block and every player at once; with none picked blocks keep the scene's own colours.
- **One rendered frame** (`sk_rendered`, read through `skRendered`): the frame as it entered the stack, for a block that lists `rendered` under `reads`. Light estimates read it against the base colour, so surface texture cancels instead of printing back as shade. With `sk_geo_normal` (a normal rebuilt from world positions, every tier) it lets a block judge light and shape on the low tier and the forward path too.
- **One way to mix paint** (`skPigment`): pigment mixing in transmittance and optical density (log space), two pigments in one wash, glazes that multiply, dilution toward the paper. Pure math, so a full-frame block and a material mix paint the same way.
- **Noise that stays on things and keeps its size** (`skAnchored`): triplanar noise on world position that stays on still things (`sk_anchored_fbm`, `sk_anchored_cells`), and noise that also keeps its size in the picture at any distance by blending two octaves by distance (`sk_scaled_fbm`, `sk_scaled_level`, `sk_surface_plane`, `sk_ref_metres`), for paper tooth, flecks and strokes. `sk_tooth(uv)` reads the shared paper's valleys (0 on a peak, 1 in a valley).
- **Chain stacking** inside a slot (`stacking: chain` and `rank` in `block.yaml`): later blocks build on what the ones before them made.
- **Many blocks, one shader** (`defaults:` in `block.yaml`, since 1.2.0): a block's own defaults over its shader's, so a family of blocks (paperBlocks' seventeen papers) shares one shader and each block is its own starting point. `StyleKit.settingsOf(id)` reports the block's defaults.
- **Swap memory** (since 1.2.0): each stack entry remembers the settings of every block it has held, so `StyleKit.swap("paper", "paperKraft")` then `StyleKit.swap("paper", "inkPaper")` puts back exactly the settings `inkPaper` had, and the frame returns exactly. The memory is kept on the entry in the synced stack, so it is shared and saved with the scene; `load` and `applyLook` write new entries without it.
- **One exit** (`sk_finish`): strength, mask, layer mode, and black stays black: a block never makes light where the frame had none.
### Masks
`StyleKit.setMask(entry, mask)` limits a block:
- `{ kind = "depth", near = 0, far = 12, soft = 0.2 }`: by view distance in metres. The soft edges lie outside the band and scale with their own distance: the block fades in over `soft` x `near` metres before `near` and out over `soft` x `far` metres after `far`, so nothing nearer than `near` x (1 - `soft`) is touched, whatever `far` is. The sky lies at the viewing camera's far distance (its `Camera.far`, 8000 m by default): a band whose `far` reaches that distance takes the sky in, a shorter one leaves it out.
- `{ kind = "rect", rect = { x0, y0, x1, y1 } }`: a screen region in 0..1 (top left is 0, 0).
- `{ kind = "sphere", center = { x, y, z }, radius = 5 }`: a region of the world.
- `{ kind = "group", group = 1 }`: the objects in mask group 1, 2 or 3 (`StyleKit.options({ groups = { { id, id }, { id } } })`).
- `invert = true` flips any of them.
`StyleKit.options({ exclude = { "SignBoard" } })` leaves objects (and their children) exactly as rendered: a prop, a world label. Name them: an entry is an entity name or an id, and a scene should store names, because an id written into the stack does not survive the scene being saved and loaded again (the entity comes back under a new id). Entities that arrive later are picked up within half a second. Put labels on a card of their own (a plain white quad behind the words), so the area left out around them is card rather than the raw scene.
### Quality
The stack follows the device's quality tier (`renderer.qualityTier()`), and the tiers really change the cost:
- `high` and `medium`: every block pass and every shared input at full resolution. `medium` also applies each block's `quality.medium` settings where it states them.
- `low`: the shared inputs (surface data, ground, wobble) and the block chain itself run at half resolution, a quarter of the pixels for every pass, and blocks take their `quality.low` settings (fewer samples, fewer strokes). The copy of base colour and normals is made on `low` too, at that half resolution (since 1.1.4), so a block judges light and lays marks by the same rules on every tier and a look stays the same look; only the forward render path, which keeps no g-buffer, goes without it. The result is scaled up once at the end, before the UI; excluded objects are put back from the full-size frame, so they stay crisp. Measured on the Lookbook's textured wall at 2097 by 1409 with a wash, a sketch line, paper, boil and dots: 1.64 ms of block passes at full resolution, 0.30 ms on `low` (before the half-size copy, which adds one small compute pass).
On `low`, marks are drawn at half resolution and so are softer: the honest trade for running on a small device. `StyleKit.setQuality("low")` forces a tier on this machine; `StyleKit.setQuality(nil)` follows the device again. The Lookbook measures the cost per pass at each tier.
### Whole frame or inside a material
Every block works on the whole frame with no material edits. Where it means something, a block also ships a shader module a material includes for a chosen set of objects (`include:` in its `block.yaml`), for example starterBlocks' Stepped light, whose `bandedToon` shader bands a material by the same rule. Use a group mask to keep the full-frame pass off those objects.
## API
| Call | What it does |
|---|---|
| `StyleKit.blocks(filter?)` | Every block installed: `{ id, name, slot, tags, summary, reads, writes, settings, package, path }`. Filter by `slot`, `package`, `tag` (one tag or a list, all must match) or `text` (which also searches tags). In the shell: `zero styleKit list`, `zero styleKit list line`, `zero styleKit list --tag pencil`. |
| `StyleKit.block(id)` / `settingsOf(id)` | One block / its settings with docs, defaults (the block's own `defaults:` where it states them) and ranges. |
| `StyleKit.add(id, opts?)` | Add a block (`strength`, `mask`, `settings`, `id`, `index`). Returns `ok, entryId`. |
| `StyleKit.remove(entry)` | Remove an entry (by entry id, block id or position). |
| `StyleKit.swap(entry, id, opts?)` | Swap an entry's block for another of its slot; strength and mask stay. The entry remembers the settings of every block it held, so a block swapped back in gets exactly the settings it had (a block it never held starts from its defaults). `{ keepSettings = true }` carries the current settings across instead; `{ anySlot = true }` allows a swap across slots. |
| `StyleKit.move(entry, index)` | Reorder (orders blocks inside a slot). |
| `StyleKit.setStrength(entry, 0..1)` / `setOn(entry, bool)` | Weight / switch an entry. |
| `StyleKit.setMask(entry, mask?)` | Limit an entry by depth, region or group. |
| `StyleKit.set(entry, { name = value })` | Change settings. Colours take `"#rrggbb"`, `{ r, g, b }` or `{ r, g, b, a }`. |
| `StyleKit.load(entries, opts?)` / `clear()` | Replace / empty the stack. `opts`: `holdRate`, `markScale`, `pigments`, `softRadius`, `look`. |
| `StyleKit.options({ holdRate, markScale, exclude, groups, active, pigments, softRadius })` | Stack-wide options. `pigments` is a pigment set id (`""` for none); `softRadius` is the soft field's width in reference pixels (2 to 40, default 10). |
| `StyleKit.pigmentSets(filter?)` / `pigmentSet(id)` | Every pigment set installed: `{ id, name, summary, tags, pigments = { { name, colour, granulation } }, shadow, light, air, package, path }`, filtered by `package`, `tag` or `text` / one set by id. |
| `StyleKit.setPigments(id)` | Pick the pigment set every block that reads pigments mixes from, for the whole stack and every player. `""` picks none, so blocks keep the scene's own colours. |
| `StyleKit.looks(filter?)` / `look(id)` | Every whole look installed (`styleLook` assets): `{ id, name, summary, tags, entries, pigments, holdRate, markScale, package, path }` / one look by id. |
| `StyleKit.applyLook(id, opts?)` | Apply a whole look in one change: its blocks replace the stack and its pigment set, hold rate, mark scale and soft radius are set with them. `{ keep = { "line" } }` keeps the entries of the slots named from the current stack. A look naming a block that is not installed is refused and nothing changes. |
| `StyleKit.describe()` | What is in the stack, in run order, its `pigments`, `look` and `softRadius`, and what the runtime drew last frame. |
| `StyleKit.checkBlock(id)` | Problems with a block against the block rules (empty when it passes). |
| `StyleKit.newBlock(name, { slot, folder })` | Write a working block of your own to start from. |
| `StyleKit.quality()` / `setQuality(tier?)` | The tier the stack draws at / force one here. |
| `StyleKit.setClock(fn?)` | Drive the stack's one clock (boil, grain, wobble) from your own time in seconds, for a cutscene, a recording or the Lookbook; `nil` returns to the shared clock (wall-clock UTC, the same held step on every player's machine). |
Writes go through the stack's owner, so in play any player can change the shared stack and every player sees the change. The `styleKit` toolbox offers the same from the shell (`zero styleKit list`, `zero styleKit stack`, `zero styleKit add posterize`).
## Writing a block
A block is a plain folder holding `<name>.styleBlock/` (block.yaml, README.md, .metadata) and its `<name>.shader/` (a post-process shader whose `properties.yaml` starts with the styleKit shared header). `newBlock` writes one that works and registers it at once:
```luau
local StyleKit = require("~styleKit.StyleKit")
local path = StyleKit.newBlock("warmTint", { slot = "medium", title = "Warm tint" })
print(path) -- /zero/source/myBlocks/warmTint/warmTint.styleBlock
StyleKit.add("warmTint", { strength = 0.6 }) -- a warm filter over the whole look
StyleKit.set("warmTint1", { tint = "#ffcf8a" }) -- its setting, like any block's
```
Then edit `/zero/source/myBlocks/warmTint/warmTint.shader/shader.wgsl` to make it do what you want; the stack picks up the change as you save. To share blocks, move their folders into the `blocks/` folder of a package of your own (a `<name>.package` folder with its `package.yaml`): a package that holds blocks adds them to every world that installs it, with no code. The guide "Writing your own style block" walks the whole shape every block follows.
## Rules every block follows
- It states its slot, what it reads and what it writes, and takes over nothing it does not own (a Ground block alone writes the ground, a Time block alone the wobble).
- Every setting is named, documented in `block.yaml` and has a good default in its shader.
- Marks stay on things unless the medium really is a screen (pixels, grain, a vignette), and they run after antialiasing and upscaling.
- Wobble runs on the held clock.
- Its README says what it gives up in motion and how it treats opaque things, transparent things, particles, sky, fog, skinned characters, foliage and world text.
- With every light off the frame stays black (`sk_finish` holds every block to it).
- The UI is drawn after the stack, so it stays readable.
- No full-screen flashing above 3 Hz and no fine high-contrast stripes filling the view.
`StyleKit.checkBlock` and the `styleKit` test suite check the rules that can be read from files.
## Guides and workflow
This package carries its guides (search them with the `guides` tool or ZeroMind): combining blocks into your own art style, how to use styleKit, writing your own style block, and a worked example from an empty world to a small stylized game. The `restyleWorld` workflow gives an existing world, scene or kit a new look non-destructively, and the `styleKit` agent skill tells an agent what it can do with blocks.
## What a look costs
GPU milliseconds per frame of the style passes, 3D view drawing at 1893 x 1072 (98% of a 1920 x 1080 frame's pixels), measured with the Lookbook (`zero lookbook cost 1920 1080 true`) over its pan on a desktop GPU. The rest of the frame (scene, shadows, antialiasing) is not counted. Low quality runs the whole chain at half resolution.
| Look | High | Low |
|---|---|---|
| README example: posterize, duotone 0.5, depthLine, sketchLine 0.6 | 0.60 | 0.11 |
| Cookbook: flatWash, depthLine, plainPaper, lineBoil, softGrain 0.6 | 1.03 | 0.21 |
| Comic: lightBands, surfaceDots, depthLine, posterize | 0.41 | 0.10 |
| Chalk: depthLine, darkBoard | 0.43 | 0.10 |
| Retro: posterize, chunkyPixels, softGrain, vignette | 0.22 | 0.07 |
Each block alone, high / low: flatWash 0.45 / 0.07, sketchLine 0.32 / 0.06, darkBoard 0.24 / 0.07, plainPaper 0.23 / 0.07, depthLine 0.19 / 0.04, cardSurface 0.11 / 0.03, lightBands 0.09 / 0.03, surfaceDots 0.09 / 0.03, softGrain 0.09 / 0.03, lineBoil 0.06 / 0.02, chunkyPixels 0.06 / 0.02, posterize 0.05 / 0.02, duotone 0.04 / 0.02, vignette 0.04 / 0.02. A look costs a little less than its blocks added up, because the shared inputs (depth, surface coordinates, ground) are made once per frame for the whole stack.
**Frame rate and the web.** On the web the Cookbook look was measured at about 29 to 30 fps in the Cookbook scene (one sample 25), against about 43 fps with no look, and the low tier did not change it: the look cost about 10 ms a frame there while its passes took about 1 ms of GPU time. That measurement is of styleKit 1.0.6. The cost was not the shading but the work around it, which a browser pays several times over: 15 screen-sized images made up front whatever the look, 11 passes, and engine calls every frame for every effect. 1.0.7 cuts it, measured on desktop: the Cookbook look now makes 7 images on high and 5 on low (was 15), runs 9 passes on high and 8 on low (was 11), and spends 0.19 ms of Luau a frame on high and 0.12 ms on low (was 0.40 ms on both), so low now saves passes and Luau, not only pixels. **Whether 1.0.7 holds 30 fps on the web is not measured yet:** an agent cannot start a browser engine (#20946), and in the desktop editor the whole-frame rate swings more from the editor's own load than the look costs, so no desktop fps is quoted here. `zero lookbook report` now starts with the frame rate in the scene you are in (no look, the look on high, on low, measured in turns), so one run in the web build gives the answer. The web limits it may still meet are open engine issues: #20750 (web frame rate), #17441 (cost of scene-colour features) and #17478 (post tiers on mobile).
## How it runs
The `styleStack` render feature starts on a machine as soon as the scene's stack holds a live block (one that is on, has strength above 0 and is installed), on every player's machine alike. Each frame it copies the base colour and normal from the deferred g-buffer at `afterLighting`, then at `afterScenePost` (after antialiasing and the render-scale upscale, before the UI) it rebuilds world position and distance from depth, copies the frame, publishes the ground and wobble, runs each slot, puts excluded objects back and hands the result to the screen. A frame of the Cookbook look runs 9 passes: the copy of base colour and normals (only when a live block reads them, and never on the low tier), world position, the two shared fields, one pass per block, and the hand-off to the screen; the blocks read the scene directly, so there is no copy of the frame first. Only five screen-sized images are made up front (the chain's two, world position and the two fields); the rest are made the first time a look uses them (group masks, excluded objects, the base colour and normal copies). The Luau work per frame is a few calls into the engine: uniforms, settings and mask groups are written only when the stack, the tier or the camera's frame inputs change, and the clock only when the held step moves. When the stack holds no live block (after `StyleKit.clear()`, or with every entry off) the kit lets everything of its own go within two frames: every block effect and its five internal effects are removed from the screen effect list, its screen targets, buffer and mask materials are freed, and the render feature itself ends. `StyleKit.holdsAnything()` then reads false, and the first block added brings it all back.
## Known limits
- **Offscreen captures and render-to-texture cameras do not draw the stack.** The stack runs at `afterScenePost`, which only the presented frame reaches, so `capture` with `source = "main"`, `"entity"` or `"position"` shows the unstyled scene; use `source = "presented"` or `renderer.presentedFrames`. Per-camera post: engine issues #66 and #17427.
- **Depth in a feature's fragment pass reads 0 after post.** `zero_scene_depth` is not bound to a render feature's fragment pass at `afterScenePost`, so the kit copies depth with a compute pass once per frame and blocks read that copy (#17427).
- **G-buffer channels are refused after post**, so base colour and normal are copied at `afterLighting`. On the forward path there is no base colour: Surface and Light blocks fall back to working from brightness alone.
- **Marks on moving objects.** Surface coordinates come from world position, so marks stay put on still things under any camera move, but slide across a moving skinned character or a moving prop. Object-space coordinates need per-object data a full-frame pass does not have, so marks that stay on a moving object can only be drawn inside its material: watercolorBlocks' `watercolorSurface` shader shows how, carrying the object's own position from `vertex_clip` (`local_position`) to the fragment in the vertex colour (applied with `WatercolorHero.paint`).
- **The soft field costs two half-resolution passes each time a reader reads a new image.** In a chain, a later block that reads `soft` reads a different picture from the first one, so the field is made again for it; keep soft readers few, or next to each other on the same input, on small devices.
- **Post effects get one pass each and no history** (#17427): a block cannot read the previous frame or its own last output, so nothing in a block can build up or settle over time.
- **Effect uniforms reset on recompile** without notice; the runtime renews its writes twice a second, so after a shader edit a setting can take up to half a second to come back.
- **One held clock for every player, as close as their clocks are set.** Since 1.1.3 the held clock runs on wall-clock UTC (caught to the frame from `os.time()` ticking over), so every machine shows the same boil step at the same moment; machines whose clocks are set by the network agree to a few hundredths of a second, so two screens differ only in that sliver around each step. A machine whose clock is set wrong boils out of step with the others. `StyleKit.setClock` drives it from a time of your choosing instead, so a cutscene or the Lookbook repeats exactly.
- **Transparent things and particles have no depth**, so lines and surface marks treat them as the opaque surface behind them; each block's README says how.
- **Stage order** (#17426, #17439): blocks rely on `afterScenePost` running after the TAA resolve and the upscale, as the engine documents today.
- **One stack per scene.** With two StyleStack entities in a scene the kit draws the last one it found; keep one, and change it rather than adding a second.
- **After a scene switch** the engine can free the stack's screen targets; the runtime notices within about two seconds and rebuilds them, so the look can be missing for that moment.
- Post-process shaders have no preview image, so the shader previews in this package are absent.
## Version history
**1.2.2 (2026-11-01)**: the scene's sun (`reads: sun`, `sk_sun`) is read every 4 frames instead of about once a second, at once whenever the stack changes, and every frame while someone drives the clock (`StyleKit.setClock`, as the Lookbook does). A block that rakes light from the sun (paperBlocks' papers) no longer steps once a second under a moving sun, and no longer draws its first second with the sun of the scene or run before it, which had made Lookbook runs depend on what ran before them. `StyleKit.VERSION` says 1.2.2 (it had stayed at 1.2.0).
**1.2.1 (2026-11-01)**: documentation only: install notes describe how installing works (install from ZeroMind search; installs with several dependencies can be slow) without naming internal ticket numbers.
**1.2.0 (2026-11-01)**: the paper field, a second shared ground input (`reads: paper`, `sk_paper`, read through `skPaper`: thirst, sheen, fibre, dark ground), published by a Ground block's `field.paper` shader, neutral where none does and made only when a live block reads it; layered grounds (`field.layers: true`: a later Ground block lays its ground and paper field over the one before by its strength and mask, so regions of a world sit on different papers); the scene's sun for blocks that declare `sk_sun` (`reads: sun`); `defaults:` in `block.yaml` (per-block defaults over the shader's, reported by `settingsOf`, so many blocks share one shader); swap memory (each entry remembers the settings of every block it held, so swapping back returns the frame exactly; `keepSettings` carries settings across instead). `checkBlock` knows `field:paper` (Ground only), `sk_paper` and `sk_sun`, and a default for a setting the shader lacks.
**1.1.0 (2026-10-31)**: blocks can build on each other inside a slot (`stacking: chain` in `block.yaml`; `layer` stays the default) and take a place in it (`rank`); two extra inputs a block may declare after the shared header, the soft field (`sk_soft`, a half-resolution, depth-aware blur, width `softRadius`, read through `skSoft`) and the scene's pigment set (`sk_pigments`, read through `skPigmentSet`); pigment mixing in optical density (`skPigment`), noise that stays on things and keeps its size in the picture (`skAnchored`), `skp_triplanar_fbm`, `skp_stroke` and `sk_tooth`; two new asset types, `pigmentSet` and `styleLook`, with `pigmentSets`, `pigmentSet`, `setPigments`, `looks`, `look` and `applyLook`; `options` and `load` take `pigments` and `softRadius`, `describe()` returns `pigments`, `look` and `softRadius`, and the StyleStack carries them as synced fields.
**1.1.5 (2026-11-01)**: documentation only: Known limits and the using-styleKit guide said the held clock was each machine's own and out of step between players; since 1.1.3 it runs on wall-clock UTC and every machine shows the same boil step. Both now say so.
**1.1.4 (2026-11-01)**: the `low` tier makes the copy of base colour and normals too, at its half resolution, so a block judges light and lays marks by the same rules on every tier (a look on `low` had stopped being the same look: light read from brightness turned dark materials grey and put mid-tone marks on lit walls). `sk_mark_normal(uv)` gives marks a surface facing on every path, rebuilt from neighbouring world positions where no normal was copied, and `sk_surface_coord`, `sk_surface_plane`, the triplanar noise and `sk_ref_metres` use it, so a wall is never projected as a floor (which smeared marks down their columns into vertical stripes).
**1.1.3 (2026-11-01)**: the stack's held clock runs on wall-clock UTC (caught to the frame from `os.time()` ticking over), so every player's machine holds the same boil step at the same moment instead of each running its own; the step number wraps every 65536 steps at the same moment everywhere, so shaders keep every digit. `StyleKit.setClock(fn)` still drives it for a cutscene or the Lookbook.
**1.1.2 (2026-11-01)**: `exclude` and group lists take entity names as well as ids (an id stored in a scene does not survive a save and load, so a scene's left-out board or labels were being painted over), and entries that find nothing yet are looked for again twice a second; a stack whose component was removed and added again is let go instead of refusing every change.
**1.1.1 (2026-10-31)**: a third extra input, the frame as rendered (`reads: rendered`, `sk_rendered`, `skRendered`), for light estimates that do not print surface texture back; `sk_geo_normal` (a normal from world positions on every tier); and `sk_albedo` and `sk_normal` answer "unknown" when no g-buffer copy was made (low tier, forward path) instead of reading a stale frame.
**1.0.8 (2026-10-31)**: store listing rewritten for creators; built on the current engine library (@builtin 0405ea4).
**1.0.7 (2026-10-31)**: a depth mask's near limit holds whatever `far` is (each soft edge now scales with its own distance, not the whole band) and the sky lies at the camera's far distance; blocks list their tags and `blocks({ tag })` and `zero styleKit list --tag` pick by them; a lighter frame for the web: images made only when a look uses them (7 on high, 5 on low, was 15), no copy of the frame before the chain, no base colour copy on low or when no block reads it, and uniforms, settings and the clock written only when they change (Luau per frame 0.19 ms high, 0.12 ms low, was 0.40).
**1.0.6 (2026-10-31)**: clearing the stack leaves nothing of the kit behind (every effect, target, buffer and the render feature are released within two frames, and `StyleKit.holdsAnything()` says so); the renderer starts only for a live block, on every machine; a per-look GPU cost table at 1080p; the wrong "black floor of 17" limit is gone (it was the Lookbook panel); the shared black guard now caps a dark pixel at its own brightness and its own brightest channel, so no block, alone or stacked, lightens an unlit frame (worst of all fourteen: +0.17 of 255).
**1.0.5 (2026-10-31)**: `StyleKit.setClock` drives the stack's clock from a time of your choosing, so boil, grain and wobble repeat exactly in a cutscene, a recording or the Lookbook.
**1.0.4 (2026-10-31)**: `newBlock` registers the block before it returns, so the README example stacks it on the next line; a block whose shader cannot be registered skips itself instead of stopping the whole stack, and the shader index is read afresh when blocks change; the author example writes to `/zero/source/myBlocks`; how to keep, clear and revert a saved stack.
**1.0.3 (2026-10-31)**: screen targets freed by a scene switch are noticed and rebuilt; Known limits on one stack per scene and the dark viewport floor.
**1.0.2 (2026-10-31)**: the `low` quality tier runs the block chain at half resolution (about five times cheaper in the Lookbook), excluded objects are restored at full size on every tier, the black guard closes fully on unlit pixels, the block type README describes the side-by-side folder layout.
**1.0.1 (2026-10-31)**: masks by object group and exclusion drawn per group, durable state across hot reloads, the toolbox, the test suite, `newBlock`, guides, workflow and agent skill.
**1.0.0 (2026-10-31)**: first release. Block type, StyleStack component, runtime, masks (depth, rect, sphere, group), exclusion, shared clock, coordinates, ground, wobble and patterns, quality tiers, four example blocks and a diagnostic block, toolbox, tests, guides, workflow and agent skill.
# StyleStack (component)
The scene's look: an ordered list of style blocks with each one's strength, mask and settings, plus the stack-wide held clock rate, mark scale, excluded objects and mask groups. It lives on a synced entity in the scene, so it is saved with the scene and every player draws the same stack. Change it with the `StyleKit` module (`add`, `remove`, `swap`, `move`, `setStrength`, `setMask`, `set`, `clear`, `load`, `options`); the runtime redraws on the next frame.
# StyleKit (module)
The styleKit library: the block registry (`blocks`, `block`, `settingsOf`, `checkBlock`), the stack operations every player shares (`add`, `remove`, `swap`, `move`, `setStrength`, `setMask`, `set`, `setOn`, `clear`, `load`, `options`), `describe` for what is in the stack, and quality control (`quality`, `setQuality`). Reach it with `require("~styleKit.StyleKit")` from any script.
# skPatterns (shader module)
The one pattern library every styleKit block draws from: hashes, value noise and fractal noise in 2D and 3D, cell (Worley) distance, a round dot screen sized by tone, parallel hatching and a 4x4 ordered threshold. Pure math with no bindings, so a full-frame block and a surface material that includes a block's piece share the same marks. Include it with `#include "~styleKit.shaders.skPatterns"` (the package name reaches it wherever styleKit was installed).
# skCommon (shader module)
The prelude every styleKit block includes. It reads the shared inputs the stack fills each frame (the `sk_` properties of the styleKit shared header): one clock (`sk_time`, `sk_held`, `sk_step`), surface data copied once per frame (`sk_world`, `sk_dist`, `sk_normal`, `sk_albedo`), coordinates that stay on things (`sk_surface_coord`), the shared ground (`sk_ground_at`) and wobble (`sk_wobble_uv`), resolution-independent mark sizes (`sk_ref_uv`), the block's mask (`sk_weight`) and the one exit every block returns through (`sk_finish`: strength, mask, layer mode, and black stays black). Include it with `#include "~styleKit.shaders.skCommon"` after declaring the shared header.
# skAnchored (shader module)
Stable noise that stays on things, for styleKit blocks. Include after `skCommon` and `skPatterns`.
- `sk_anchored_fbm(uv, perMetre, octaves)`: fractal noise laid on the world position of the surface under the pixel from three sides (no seam where a surface turns; the sky by view direction). Sized in metres: a wash's grading, wet patches.
- `sk_anchored_cells(uv, perMetre)`: one random value per patch (blooms, pools, dry patches).
- `sk_scaled_fbm(uv, refPx, octaves)`: noise that stays on the surface and keeps marks about `refPx` reference pixels across at any distance, by blending two octaves by distance (marks grow into the next octave rather than sliding). Paper tooth, grain, flecks and strokes that should keep their size in the picture.
- `sk_scaled_level(uv, refPx)`, `sk_surface_plane(uv)`, `sk_ref_metres(uv)`: the parts, for drawing your own pattern (cells, strokes) the same way.
For things that move, use `skp_triplanar_fbm` on the object's own position inside a material (`local_position`).
# StyleRuntime (module)
The per-machine half of styleKit, called by the styleStack render feature each frame: it keeps one post-process effect per stack entry, fills the shared inputs (clock, quality, surface data, ground, wobble, object mask) and enqueues the passes in slot order after antialiasing. Scripts use `StyleKit`, not this module.
The full-frame pass of the Depth line style block: one-sided silhouettes from inverse distance and creases from normals.
# Depth line (styleBlock, Line)
Draws a clean ink outline where depth jumps and where surfaces fold, the same weight everywhere, for comic, cartoon and technical drawing looks.
Settings: `width` (1.6 reference px), `ink` (near black), `opacity` (0.9), `silhouette` (1), `crease` (0.6), `threshold` (0.02), `creaseAngle` (0.35), `fadeStart` (40 m), `fadeEnd` (120 m), `grainBreak` (0.2), `wobble` (1). Low quality samples two directions instead of four and draws no creases.
How it reads: silhouettes come from the inverse of the shared view distance, which is linear across any flat surface on screen, so a floor grazing to the horizon draws no false lines; the line lands on the near side of an edge only, so two touching objects never get a doubled outline. Creases come from the shared normals. The ink is broken by the shared ground's grain (`grainBreak`) and follows the shared wobble, so it boils with a wash on one clock and the paper shows through it.
## What it gives up in motion
Lines are found again every frame from the opaque depth, so they do not carry history: a part thinner than a line (wires, fence slats, far railings) can shimmer as it crosses pixels, and a crease drawn from normals pops in and out as a curved surface turns past `creaseAngle`. Line weight is constant on screen, so it does not shrink with distance until `fadeStart`.
## How it treats content
- Opaque things: outlined and creased.
- Transparent things (glass, water): not in the depth, so they get no outline; objects behind glass keep theirs, drawn over the glass colour.
- Particles and smoke: no outline; lines of objects behind smoke stay visible through it.
- Sky: never lined; objects get an outline against it.
- Fog: lines are drawn over fog at full ink until `fadeStart`; lower `fadeEnd` to sink far lines into fog.
- Skinned characters: outlined from their depth each frame, so a fast character keeps a clean line.
- Foliage: alpha-cut leaves are in the depth and get busy outlines; raise `threshold` or mask foliage out with a group mask.
- World text: text drawn as geometry gets outlined like any object, which can make small letters heavy; exclude labels.
- Black stays black: in a frame with no light the line is not drawn.
The full-frame pass of the Duotone style block: maps brightness onto a shadow ink and a light ink.
# Duotone (styleBlock, Colour)
Maps the picture onto two inks, a shadow colour and a light colour, like a two colour print or a tinted photograph.
Settings: `shadowInk` (deep blue), `lightInk` (warm cream), `contrast` (1.1), `balance` (0), `keepColour` (0.1). Colours are display colours as picked, written as `"#1c2245"` or `{ r, g, b }`.
## What it gives up in motion
Nothing extra: it maps each pixel on its own, so it adds no marks that could slide or flicker. What it gives up is hue: colour-coded gameplay (a red enemy, a green pickup) reads only by brightness, so raise `keepColour` or exclude those objects.
## How it treats content
- Opaque things: mapped by brightness onto the two inks.
- Transparent things (glass, water): mapped as they appear, with what is behind them.
- Particles and smoke: mapped; bright additive sparks become the light ink.
- Sky: mapped; a blue sky turns to the middle of the two inks.
- Fog: mapped; fog pulls distant things toward one ink, which reads as depth.
- Skinned characters: mapped like any surface.
- Foliage: mapped; leaf colour is lost, leaf shape stays.
- World text: mapped; keep labels light on dark or dark on light so they keep their contrast, or exclude them.
- Black stays black: a pixel with no light stays black even though the shadow ink is not black.
The full-frame pass of the Posterize style block: steps brightness (or each channel) into a few flat levels.
# Posterize (styleBlock, Colour)
Cuts the picture into a few flat tone steps, like a screen print or a poster printed with a small set of inks.
Settings: `levels` (5), `byBrightness` (1), `softness` (0.12), `saturation` (1.1). Low quality draws hard steps (softness 0).
## What it gives up in motion
Smooth gradients become hard steps. When a light changes slowly, a step edge sweeps across a surface instead of the surface fading, and noisy content (fine textures, film grain) flickers between two steps. Keep `softness` above 0.05 where light moves. It holds no marks of its own, so nothing slides or sticks to the screen.
## How it treats content
- Opaque things: stepped by brightness, hue kept.
- Transparent things (glass, water): stepped as they appear, with what is behind them.
- Particles and smoke: stepped like everything else; soft smoke turns into a few bands.
- Sky: stepped; a gradient sky becomes bands.
- Fog: stepped; distance fog becomes a few flat depth bands, which often reads well.
- Skinned characters: stepped like any surface, no extra cost.
- Foliage: stepped; moving leaves can flicker between steps.
- World text: stepped with the rest; light text on dark keeps its contrast. Exclude a label with `StyleKit.options({ exclude = { id } })` to leave it untouched.
- Black stays black: a pixel with no light stays black.
The pass of the Shared inputs view diagnostic block.
# Shared inputs view (styleBlock, Medium)
A diagnostic block for block authors: shows one of the shared inputs every block reads (distance, normals, base colour, ground, wobble, object mask or surface coordinates) in place of the picture.
Settings: `show` (0 view distance, 1 normal, 2 base colour, 3 ground with its grain on the right half, 4 wobble, 5 object mask, 6 surface coordinates), `range` (60 m). Use it with a rect mask to see an input beside the picture.
## What it gives up in motion
It replaces the picture, so it is not a look. Watching `show = 6` while the camera pans is the quickest way to see that surface coordinates stay on things.
## How it treats content
- Opaque things: shows their shared data.
- Transparent things (glass, water): shows the opaque surface behind, since the shared data comes from the opaque depth.
- Particles and smoke: invisible in every view but the ground and wobble.
- Sky: distance 0 (black), no normal or base colour; coordinates come from the view direction.
- Fog: not part of the shared data.
- Skinned characters: shown like any opaque thing.
- Foliage: alpha-cut leaves appear, blended leaves do not.
- World text: appears where it is geometry in the depth.
- Black stays black: not applicable; this is a diagnostic and it is the one block that does not return through `sk_finish`.
The full-frame pass of the Sketch line style block: jittered, broken pencil strokes over shape and colour edges, redrawn on the held clock.
# Sketch line (styleBlock, Line)
Draws soft, broken pencil lines over edges of shape and colour, doubled and redrawn a little on every step of the held clock, for a hand-sketched or storyboard look.
Settings: `width` (1.3 reference px), `lead` (graphite grey), `darkness` (0.7), `colourEdges` (0.5), `strokes` (2), `jitter` (1.2 px), `breakup` (0.35), `breakScale` (3 per metre), `fadeEnd` (80 m), `wobble` (1). Low quality draws one stroke per edge, medium two.
The strokes multiply the picture with the pencil colour instead of covering it, so colour and paper show through them. Breaks in the strokes are drawn in surface coordinates, so they stay on the object as the camera moves; only the small redraw offset changes, and it changes on the stack's held clock.
## What it gives up in motion
The strokes are redrawn off the edge on every step of the held clock, so lines boil: that is the look, and it runs at the stack's `holdRate` (8 steps a second by default), never per frame and never faster than the clock. Colour edges are found in the picture itself, so a texture that shimmers makes its strokes shimmer too. On a fast moving skinned character the breaks are taken from world position, so they slide along the body as it moves.
## How it treats content
- Opaque things: shape strokes from depth and normals, plus colour strokes.
- Transparent things (glass, water): no shape strokes; strong colour edges seen through them are stroked.
- Particles and smoke: bright particles over a dark background draw colour strokes around them; soft smoke draws few.
- Sky: no shape strokes; strong sky gradients (a sun disc) can draw a faint colour stroke.
- Fog: fog lowers colour contrast, so far strokes fade naturally; shape strokes end at `fadeEnd`.
- Skinned characters: stroked each frame like any opaque thing; breaks slide on the moving body (see above).
- Foliage: busy; lower `colourEdges` or mask foliage out.
- World text: letters get pencil edges; exclude labels that must stay crisp.
- Black stays black: a frame with no light draws no strokes.
# pigmentSet (asset type)
A pigment set is a palette of paints that every styleKit block reading `pigments` mixes from: up to eight pigments, each with how much it granulates, plus the pigment shadows pool in, the tint lit areas thin toward and the colour far things lift toward. One change (`StyleKit.setPigments(id)`) re-colours a whole painted look for every player, because the set is named on the scene's synced StyleStack.
## Folder shape
```
earthy.pigmentSet/
pigments.yaml the palette (required)
README.md what it is for and the mood it gives (required)
.metadata { "tags": ["pigment set", "watercolor", "earthy"] } (required)
```
## pigments.yaml
```yaml
name: "Earth pigments"
summary: "Ochres, siennas and umbers with a cool grey: a warm, granulating landscape palette."
tags: [earthy, warm, landscape]
pigments: # up to eight; colour is the masstone as picked (#rrggbb)
- { name: "yellow ochre", colour: "#d1a24a", granulation: 0.35 }
- { name: "burnt sienna", colour: "#a4512b", granulation: 0.5 }
- { name: "ultramarine", colour: "#3c4fa8", granulation: 0.9 }
shadow: { colour: "#4b4470", strength: 0.6 } # what shadows pool toward
light: { colour: "#ffe3b8", strength: 0.5 } # what lit areas thin toward
air: { colour: "#ece6d6", strength: 0.4 } # what distance and fog lift toward
pull: 0.75 # how hard colours are re-mixed from the pigments, 0..1
darkest: 0.05 # the darkest value the set reaches (display, 0..1)
saturation: 1.0 # 1 keeps the pigments' own chroma, lower greys them
```
A block reads the set through the `skPigmentSet` shader module (`skg_remix`, `skg_shadow`, `skg_light`, `skg_air`). With no set picked (`StyleKit.setPigments("")`) blocks keep the scene's own colours. The id is the folder name without `.pigmentSet`; list them with `StyleKit.pigmentSets()`.
·computeshader · born here
❒asset styleKit internal: copies base colour and world normal from the deferred g-buffer at afterLighting into scene-space targets the style blocks read after antialiasing.
styleKit internal: writes one constant value over a whole target, to reset a shared field that no block in the stack publishes.
styleKit internal: writes selected objects into one channel of the stack's object mask, depth-tested against the scene.
styleKit internal: merges the per-group object masks into the stack's one object mask.