# paperBlocks
Put your game on paper: seventeen papers a stylized look is drawn on, from cold-press watercolor paper and thirsty rice paper to brown kraft, parchment, canvas, a dusty blackboard and black paper for white ink. Each paper is a styleKit Ground block, so it swaps for any other paper in one change, stays on the surfaces it covers while the camera moves, and lets the blocks above it feel it: washes granulate into its tooth and run further on a thirsty sheet, ink lines break on rough grain and feather on soft fibres, and on toned and dark sheets the picture's lights are put on in white gouache, chalk or white ink. Parts of one world can sit on different papers (a village on cold press, a map room on parchment), and every player sees the same paper.
Version 1.1.4. Built on styleKit 1.2.3 (the paper field, layered grounds, the scene's sun, per-block defaults and swap memory are styleKit 1.2.0 features; 1.2.3 keeps the sun on every paper after its shader compiles).
## Install and use
Install **paperBlocks** from ZeroMind (search "paperBlocks" or "paper texture for a watercolor game"). It brings what its own files use, and nothing needs installing by hand: **styleKit** (the look stack its shaders build on), and **lookbookKit** with **starterBlocks** (the package's test suite measures at the Lookbook's every-light-off moment, and the Lookbook uses starterBlocks' blocks). It does not bring watercolorBlocks or inkBlocks: those are separate installs, and the papers work without them. Checked on 2026-11-01 by installing paperBlocks from ZeroMind into a brand new private world: 617 files in about 3 s, and the example below ran there line by line exactly as written (the frame changed on load, kraft read clearly differently, swapping back brought cold press back with its deeper tooth, and `clear()` returned the frame to exactly its unstyled brightness). Installs with several dependencies can be slow (#21420): give the install time to finish and check `/zero/source` for the `paperBlocks.package` folder.
Then, in a fresh world with only paperBlocks installed, from the console or any script:
```luau
local StyleKit = require("~styleKit.StyleKit")
StyleKit.load({
{ id = "line", block = "depthLine" }, -- styleKit's own clean ink outline
{ id = "paper", block = "paperColdPress" }, -- the picture laid on cold-press paper
})
StyleKit.set("paper", { toothHeight = 1 }) -- a deeper tooth: one change
StyleKit.swap("paper", "paperKraft") -- the same picture on brown kraft, lights in white gouache
StyleKit.swap("paper", "paperColdPress") -- back on cold press, with the deeper tooth it had
StyleKit.clear() -- the frame exactly as rendered again
```
Swapping back puts back exactly the settings the paper had (styleKit 1.2.0 remembers them on the entry), so the frame returns as it was. The stack lives on the scene's synced StyleStack, so every player sees the same paper, and it is saved with the scene.
To see what is installed: `StyleKit.blocks({ package = "paperBlocks" })`, or in the shell `zero styleKit list ground` and `zero styleKit list --tag paper`.
## The papers
Every paper is the same paper model with its own defaults, so a setting means the same thing on every paper and a paper you tuned on one block carries its meaning to another. "Lights" says what the paper needs from the picture's light: on a white sheet the lights are bare paper; on a toned or dark sheet they are put on in an opaque light, which only shows where the scene really is lit.
| Paper | Block | Character | Best for | What lights need |
|---|---|---|---|---|
| Cold press | `paperColdPress` | Warm white, a rough felt tooth, the standard thirst the watercolor and ink blocks were tuned on. | Watercolor, line and wash, picture books: the default sheet. | Nothing special: lights are bare paper. |
| Rough | `paperRough` | The deepest, largest tooth, warm white, a little thirstier. | Loose washes full of granulation, broken dry brush, scratchy lines. | A low sun across surfaces shows the tooth most. |
| Hot press | `paperHotPress` | Smooth, almost no tooth, crisp white, hard-sized. | Crisp washes with hard edges and dark dried rims, exact ink lines, botanical and technical illustration. | Nothing special. |
| Rice paper | `paperRice` | Thin and soft with long visible fibres, very thirsty. | Ink wash painting, soft washes that feather, lines that bleed along the fibres. | Nothing special. |
| Newsprint | `paperNewsprint` | Grey-cream, a little yellowed, a fine flat grain and darker fibres, thirsty. | Zines, cheap comics, fuzzy print-like ink. | The sheet's grey-cream is the lightest light. |
| Kraft | `paperKraft` | Brown, fibrous, a mid-tone ground. | Toned sketches, crafts and cardboard, gouache highlights. | A clear lit side: lights are put on in white gouache only where the picture is brighter than about halfway from the brown to white. A flat, dim scene stays brown. |
| Parchment | `paperParchment` | Warm cream, strongly blotchy, a few age spots, a soft sheen, fairly hard-sized. | Old maps, documents, scrolls, a map room. | The sheen shows only where light reaches a surface, most under raking light. |
| Canvas | `paperCanvas` | Primed canvas: a plain weave of threads under everything. | Painterly looks; a weave showing through paint and line. | Raking light shows the weave. |
| Illustration board | `paperBoard` | Hard, smooth, bright and even. | Clean comic ink, controlled washes, graphic looks. | Nothing special. |
| Blackboard | `paperBlackboard` | Dark slate with sharp grit, chalk dust and the ghosts of old wiping, very hard-sized, dead matte. | Chalk drawings, classrooms, puzzle rooms, night sketches. | The picture is made of chalk: only lit things are drawn. A dark scene is bare board. |
| Green chalkboard | `paperGreenChalkboard` | A green school board, dustier than the slate. | The same as the blackboard, friendlier and more schoolroom. | As the blackboard. |
| Black paper | `paperBlack` | Near-black with a soft tooth. | White ink drawings, silhouettes, night. | The picture is drawn in white ink: only lit things are drawn. |
| Aged sketchbook | `paperSketchbook` | Yellowed, mottled, foxed with brown age spots that show through the paint. | Travel journals, old sketchbooks, memories. | Nothing special: lights are the yellowed sheet. |
| Toned grey | `paperTonedGrey` | A mid-grey drawing paper. | Toned drawings: darks laid in, highlights in white chalk and gouache. | Lights are put on in white from about 40% of the way from the grey to white; the values between stay bare grey, the middle tone. |
| Toned tan | `paperTonedTan` | A warm tan drawing paper. | Warm toned drawings with white highlights. | As toned grey. |
| Ruled notebook | `paperNotebook` | Smooth bright paper with pale blue rules, a little thirsty. | Doodles, school-book and diary looks. | Nothing special; the rules show through every wash. |
| Vellum | `paperVellum` | Cool cream, a cloudy formation, a satin sheen, very hard-sized. | Drafting and technical looks, washes that pool on top with hard edges. | The sheen needs light on the surface. |
How the papers change watercolor and ink blocks, paper by paper, is in the guide "How paper changes watercolor and ink"; how to pick and tune one is in "How to choose a paper for my art style".
## The shared settings
Every paper takes the same settings (each paper starts from its own defaults; `StyleKit.settingsOf("paperKraft")` lists them with that paper's defaults and the ranges). Change them with `StyleKit.set(entry, { name = value })`; colours take `"#rrggbb"`.
| Setting | Range | What it does |
|---|---|---|
| `tint` | colour | The sheet's colour. On a white paper the bright areas become this colour; on a toned or dark sheet it is the ground the darks sink into and the lights are put on over. |
| `toothHeight` | 0 to 1 | How tall the tooth is: how much it shades the sheet, how deep paint settles into it, and how strongly granulation, dry brush and ink lines feel it. |
| `toothSharpness` | 0 to 1 | 0 soft rounded felt bumps, 1 sharp grit. Sharp grit makes put-on lights catch the peaks first (chalk on slate). |
| `toothSize` | 1 to 40 | How large one tooth looks, in reference pixels (1080p). It keeps about this size at any distance. |
| `fibre`, `fibreLength`, `fibreTone` | 0 to 1, 1 to 30, -1 to 1 | How many long fibres show, how long they are (in teeth), and whether they read lighter (above 0) or darker (below 0) than the sheet. |
| `weave` | 0 to 1 | How much canvas weave replaces the tooth. |
| `mottle` | 0 to 1 | How blotchy and uneven the sheet's tone is (warmer where darker). |
| `foxing` | 0 to 1 | How many brown age spots the sheet carries. |
| `rule`, `ruleColour`, `ruleSpacing` | 0 to 1, colour, 8 to 200 | Ruled lines: how strongly they show, their colour, and their spacing in reference pixels. |
| `thirst` | 0 to 1 | How thirsty the sheet is: 0 hard-sized (crisp wash edges, dark dried rims, exact lines), 0.5 the standard sheet the watercolor and ink blocks were tuned on (nothing changes), 1 thirsty (washes spread and feather, lines bleed into the fibres). Felt only by blocks that read the paper field. |
| `emboss` | 0 to 1 | How strongly the tooth catches light, strongest where the scene's sun rakes low across a surface. |
| `sheen` | 0 to 1 | How much the sheet shines where light reaches it. Sheen is light the scene sends: none where there is none. |
| `lights` | 0 to 1 | How much of the picture's light can be put on in an opaque light (gouache, chalk, white ink) instead of being left as bare sheet. 0 for white papers. Any amount above about 0.25 makes the sheet a toned ground (the darks sink into it) and the value is then the most light put on: the boards use 0.6, so a chalk drawing stays marks over slate even where the scene is brightest. |
| `lightColour`, `lightTint` | colour, 0 to 1 | The colour of the put-on lights, and how much of the picture's own colour they keep. |
| `lightsFrom` | 0 to 1 | Where lights start being put on, as a share of the way from the sheet's value to white: 0 everything lighter than the sheet, toward 1 only the brightest lights. |
| `dust` | 0 to 1 | Chalk dust and the ghosts of old wiping on a board. |
## How a paper draws
- **Darks go into the sheet** as transparent media: multiplied onto the sheet's colour and settling into the valleys of its tooth, thinner on its peaks.
- **Lights are put on** on toned and dark sheets (`lights` above 0), from `lightsFrom` of the way between the sheet's value and white; on sharp grain they catch the tooth's peaks first, on soft grain they lie more evenly.
- **The tooth is lit by the scene's sun** (the first directional light, read every few frames and written again whenever the paper's shader compiles), so a low raking sun shows it most, plus a little relief from the upper left as a sheet reads on a table.
- **Everything sits on surfaces** and keeps its size in the picture: the tooth, fibres, blotches, spots and rules are laid on world position, so they stay on things under a pan, a dolly or a zoom, and any part finer than about two drawn pixels fades to its average, so nothing shimmers in the distance or on the low tier. They slide across moving characters, as every full-frame mark does.
- **Black stays black.** Every pixel goes through styleKit's shared exit, so with every light off the frame is black on every paper, white paper and chalkboard alike.
Each paper publishes the stack's shared ground (the sheet's colour and tooth height, which granulation, dry brush and ink lines read) and the **paper field** (thirst, sheen, fibre and whether the sheet is dark), which the watercolor and ink blocks read. The paper field is made only when a block in the stack reads it, so a paper under styleKit's own blocks costs nothing for it.
## Regions on different papers
A later paper in the stack lays its ground and paper field over the one before by its strength and mask, so regions of one world sit on different papers:
```luau
StyleKit.add("paperColdPress", { id = "paper" }) -- the whole world on cold press
StyleKit.add("paperParchment", { id = "mapRoom",
mask = { kind = "sphere", center = { 40, 2, -12 }, radius = 6 } }) -- the map room on parchment
```
Every mask kind works (depth, rect, sphere, group; `invert = true` flips it). The older papers (`plainPaper`, `darkBoard`, `inkPaper`, `wcColdPress`) do not layer: one of them can be the first paper under a paperBlocks region, but not a region laid over another paper.
## A paper on an object
The papers above lay the sheet over the whole frame from world position, so a prop that moves, or a character that walks, slides under the tooth. To put an object on the same paper with the tooth on its own surface, give it the paper as a material:
```luau
local PaperMaterial = require("~paperBlocks.PaperMaterial")
PaperMaterial.paint(heroId) -- the paper the stack is on, with its tuned settings
StyleKit.options({ groups = { { "Hero" } } }) -- name the object (or its parent) as mask group 1
StyleKit.setMask("paper", { kind = "group", group = 1, invert = true }) -- the full-frame paper leaves it to its own material
```
`paint` 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), and it takes every setting of the paper the stack's first paperBlocks Ground entry uses, your tuning included. `PaperMaterial.paint(heroId, "paperCanvas", { emboss = 0.8 })` names another paper and overrides any setting. What the material does:
- **The tooth stays on the object.** Tooth, fibres, weave, blotches, foxing and rules are laid on the object's own position, so they ride with it as it moves (on a skinned character, the bind pose, so it stays on the body as it animates). They keep about the full-frame paper's size in the picture: the object's `PaperSurface` component keeps each machine's screen height in the material, so it matches on every screen.
- **Every light rakes it.** The tooth bends the surface normal and the engine's own lighting does the rest, so the sun, point lights and shadows show the tooth the way raking light shows it on the frame; `sheen` makes it shine where light reaches it.
- **The same picture rules.** Darks go into the sheet and its valleys; on toned and dark papers the lights are put on in `lightColour` from `lightsFrom` up; nothing is brighter than the light that reached it, so an unlit object stays black.
- **One paper, not two.** The group mask keeps the full-frame paper off the object, so the tooth is not laid twice (once sliding, once riding). If you would rather keep the full-frame paper on it, paint with `{ sheet = 0 }`: the material then adds only the tooth's relief and tone, and the frame lays the sheet.
The blocks above the paper (washes, granulation, lines) still run over the object as they run over everything; granulation and dry brush read the frame's tooth there, so on a moving object keep those off it with a group mask too. `PaperMaterial.unpaint(heroId, { removeFiles = true })` puts every original material back and deletes the materials it wrote; `PaperMaterial.list()` names every painted entity. The materials are world assets (in `/zero/source/paperMaterials` by default) and the swap is a synced change, so every player sees the paper on the object.
Limits: see-through materials (glass, water) are left as they are. A material whose own shader lays its texture in its own way (a pixel-art atlas, world-space tiling) keeps its colour and texture but the texture is then laid on the mesh's own UVs, so check it after painting. The material reads no style stack: if you retune the stack's paper, paint the object again to match.
## Mixing with watercolorBlocks and inkBlocks
The papers are Ground blocks, which run after Fill and Line, so washes and lines are laid first and the paper takes them. With watercolorBlocks or inkBlocks installed beside paperBlocks:
```luau
StyleKit.applyLook("watercolor") -- watercolorBlocks' whole look, on its own cold press
StyleKit.swap("paper", "paperRice") -- on rice paper: washes run further and rims soften
StyleKit.swap("paper", "paperHotPress") -- on hot press: crisp washes, darker narrower rims
StyleKit.applyLook("brushAndWash") -- inkBlocks' ink wash painting
StyleKit.swap("paper", "paperRice") -- the brush line feathers into the fibres
StyleKit.swap("paper", "paperBlackboard") -- the ink is drawn light, the picture in chalk
```
`wcBleed` and `wcEdgeDarken` (watercolorBlocks 1.1.0) and every inkBlocks line and fill (inkBlocks 1.0.4) read the paper field. Granulation, dry brush and every ink line's `paperBreak` read the tooth through the shared ground, so they feel how much tooth each paper really has (almost none on hot press and board). On cold press with its standard thirst every block draws exactly as it did on the paper it was tuned on.
## Relation to the other papers
| Paper | Package | What it is | Next to paperBlocks |
|---|---|---|---|
| `plainPaper` | starterBlocks | Plain warm white paper with a fine grain. | Swaps with any paperBlocks paper in one change. Publishes no paper field, so the blocks that read it draw as on the standard sheet. |
| `darkBoard` | starterBlocks | A simple chalk board with a ceiling on chalk coverage. | `paperBlackboard` is the fuller board: slate grain, dust, wiping ghosts, layering, and a paper field that tells ink blocks to draw light. |
| `inkPaper` | inkBlocks | A copy of `plainPaper`, shipped so inkBlocks' looks need nothing else. | Swaps in one change; no paper field. |
| `wcColdPress` | watercolorBlocks | watercolorBlocks' rough cold-press paper. | Swaps in one change; no paper field. `paperColdPress` is the same kind of sheet in the shared paper model. |
None of the older papers publishes the paper field, so the frame under them stays exactly as before.
## Quality
The papers follow the stack's quality tier. On `low` the chain runs at half resolution: the paper keeps its tooth at the same size in the picture, and any part too fine to draw at half resolution fades to its average instead of shimmering. `StyleKit.setQuality("low")` forces the low tier on this machine; `StyleKit.setQuality(nil)` follows the device again.
A paper costs one pass for the picture and one for its ground; the paper field adds one more pass, made only when a block in the stack reads it. Each extra region on another paper adds its own picture pass, ground pass and (when read) paper field pass.
## Multiplayer
The paper is an entry on the scene's synced StyleStack, so every player sees the same paper, settings and regions, and any player's change reaches everyone through the stack's owner. Swap memory is kept on the entry, so it is shared too. The papers keep no state of their own. Only the quality tier is per machine.
## What is in the package
| Asset | What it is |
|---|---|
| `blocks/` | The seventeen papers (`<name>.styleBlock`) and the three shaders they all share: `paper.shader` (the picture on the sheet), `paperField.shader` (the shared ground) and `paperTakes.shader` (the paper field). |
| `shaders/paperTooth.shaderModule` | The pure tooth math (felt tooth and grit, fibres, weave, rules, foxing) shared by the full-frame papers and the paper material. |
| `shaders/paperCore.shaderModule` | The full-frame paper model built on it: tooth, fibres, weave, blotches, foxing, rules, chalk dust laid on surfaces. |
| `shaders/paperSurface.shader` | The paper as a material for an object: the same tooth on the object's own position, lit by the scene's lights. |
| `PaperMaterial`, `PaperSurface` | Put an object on paper (`paint`, `unpaint`, `list`) and the component that records its original materials and keeps the tooth's size right on each screen. |
| `Gallery.scene`, `PaperGallery`, `PaperGalleryLook` | Every paper side by side under one stack, each around its own stand as a layered region; the look over them is fitted to what your world has installed every time the scene loads. |
| `guides/` | How to choose a paper for my art style, how paper changes watercolor and ink, and a worked example level on paper. |
| `workflows/paperWorld.workflow` | Put an existing world on paper, check it, and leave it undoable. |
| `tests/` | The package's test suite. |
## Known limits
- **Installs with several dependencies can be slow (#21420).** paperBlocks brings styleKit, lookbookKit and starterBlocks; the check above took about 3 s, but give a busy engine time to finish before using it.
- **The web build was not checked from inside** (#20946: an agent cannot start a web engine). `zero lookbook report` is the one command to run there.
- **A second machine was not tried** (#14448: a second engine on the same account joins as the same player). The stack's fields are replicated, the papers keep no state, and the held clock is wall-clock UTC.
- **Frame rate on the web and in the web gallery** is lower than on desktop (#20750); start slower machines on the low tier with `StyleKit.setQuality("low")`.
- **The full-frame sheet slides across moving characters.** It is laid on world position. Put the character on paper inside its own material instead (`PaperMaterial.paint`, "A paper on an object" above), and keep the full-frame paper off it with a group mask.
- **Glass, water and particles have no depth**, so the sheet under them is the sheet of the surface behind.
- **Offscreen captures and render-to-texture cameras do not draw the stack** (styleKit's Known limits).
- **Older papers do not layer.** `plainPaper`, `darkBoard`, `inkPaper` and `wcColdPress` can only be the first paper of a stack with regions.
## Removing it
`StyleKit.swap("paper", "<the paper you had>")` returns the old paper with exactly its settings; `StyleKit.remove("paper")` takes the paper off; `StyleKit.clear()` takes the whole look off. paperBlocks edits no material and nothing outside the stack, so nothing else needs putting back. Uninstalling the package leaves a stack that names its papers drawing without them until they are swapped out.
## Version history
**1.1.4 (2026-11-02)**: needs styleKit 1.2.3, and the Lookbook table is new. A reviewer could not reproduce the 1.1.3 table on a fresh install. The cause was styleKit 1.2.2 losing the scene's sun on a paper whenever its shader compiled, so each run drew the tooth with or without sun relief depending on timing (see "Why the earlier table did not reproduce"). styleKit 1.2.3 fixes it, and every paper now measures the same on both runs of a fresh install: all seventeen papers measured twice with exactly the call the README names. No block changed.
**1.1.3 (2026-11-01)**: documentation only: the Gallery's README quotes a single paper's cost as a fresh install measures it (0.52 to 0.74 ms high, 0.15 to 0.20 ms low at 1920 x 1080).
**1.1.2 (2026-11-01)**: documentation only. The Lookbook numbers are now what a fresh install measures: every paper twice in a brand new world with only paperBlocks installed, right after the engine started, at 1920 x 1080. Where a paper also showed a second repeatable outcome (kraft on a fresh install; several papers in a long session) the table lists it, since which outcome a run lands on depends on what ran before it in the engine. The 1.1.1 table came from a long session and held some of those second outcomes (cold press 3.13 / +0.18 / 1.86 where a fresh install gives 3.64 / -0.05 / 1.57). No block changed.
**1.1.1 (2026-11-01)**: the Gallery fits its look to your world every time it loads (in edit, in play, on every peer), so the first open in a paperBlocks-only world already draws styleKit's pencil line over every paper and its title card says so; before, only a rebuild did. Lookbook numbers measured again on styleKit 1.2.2 and lookbookKit 1.0.11: the earlier numbers had been taken in one long batch in which each paper's first second of path was embossed under the sun left over from the run before (styleKit read the sun about once a second), so they did not match a run on its own. The section now states the exact call and view size, and how the numbers move with the view size.
**1.1.0 (2026-11-01)**: a paper on an object: the `paperSurface` material lays any paper's tooth, fibres, weave and tone on an object's own position, lit by the scene's own lights, and `PaperMaterial.paint(entityId)` puts an object on the stack's paper (or a named one) and `unpaint` puts it back (see "A paper on an object"); the pure tooth math moved into `paperTooth`, shared by the frame and the material, with no change to how any paper draws; a test for painting and unpainting.
**1.0.1 (2026-11-01)**: the README says exactly what an install brings (styleKit, lookbookKit and starterBlocks), as a fresh-world install showed; no block changed.
**1.0.0 (2026-11-01)**: first release: seventeen papers sharing one paper model and one set of settings, the Gallery, guides, the paperWorld workflow and the test suite.
## Lookbook numbers
**What a fresh install measures.** Every paper alone, measured twice on 2026-11-02 in a brand new private world: paperBlocks installed from ZeroMind (which brought styleKit, lookbookKit 1.0.11 and starterBlocks 1.0.11) and nothing else, the stack empty before and after every run (no entries, hold rate 8, soft radius 10, no pigment set), desktop GPU, the Lookbook's 3D view at exactly **1920 x 1080** (checked before every run), one discarded warm-up run first, no missed frames. Each paper was measured with exactly this call and nothing else on the stack:
```luau
local Lookbook = require("~lookbookKit.Lookbook")
local r = Lookbook.measure({ stack = { { id = "paper", block = "paperKraft" } } })
print(r.size[1], r.size[2], r.stylized.all.warpedDiff, r.stylized.markStuck, r.stylized.screenStuck, r.stylized.zoomMarks.growth, r.passMs.high.total, r.passMs.low.total)
```
`zero viewport size` reports the 3D view as `sceneWidth` and `sceneHeight`; the window is larger than the view by the editor's panels, and the Lookbook refuses a run whose view changes size. The styleKit these numbers were taken on carries the sun fix styleKit 1.2.3 ships (below), which paperBlocks 1.1.4 needs: the published styleKit was still 1.2.2 that night, so the one-line fix was put into the fresh install's styleKit by hand. **Every paper's two runs agree** (to 0.01 where they differ at all), and the unstyled frame the same call measures was identical in all 34 runs: 2.30 (whole path), 6.57 (pan), 0.00, -0.53, 7.88. The runs are in `design/lookbook/paper_fresh_runs.txt` in the Style Cookbook world.
**Why the earlier table did not reproduce.** The Gallery's stack settings were not the cause: the Lookbook replaces the Gallery scene before it measures, the measured stack was empty with the default hold rate and soft radius before and after every run, and those defaults are the Gallery's own values anyway. The cause was a styleKit bug. styleKit 1.2.2 writes the scene's sun to a block only when the sun changes, and whenever a block's shader is compiled (the first time a paper is used in a fresh install) or recompiled, the engine puts the block's values back to their defaults, which for the sun means "no sun". So a run drew the paper with or without the sun raking its tooth depending on timing. With paperBlocks 1.1.3 and styleKit 1.2.2 freshly installed, 11 of the 17 papers gave two different results on their two runs (kraft 2.26 / +0.41 / 1.81, then 1.93 / +0.50 / 2.59; cold press 3.13 / +0.18 / 1.86 on both runs right after a warm-up of 3.64 / -0.05 / 1.57), while the unstyled frame never changed. The sunless result is the one with higher mark growth and a little less swimming; the earlier table's last column ("other repeatable outcome") was that result, and its two kraft rows were one of each. With the fix put in, the same fresh install gave every paper the same numbers twice. A mark growth of about 1.0 was not seen in any of 87 runs on this machine (the lowest is 1.57, cold press): if your runs show it, `zero lookbook report` and your GPU would help find out why.
How to read them, against the unstyled frame rather than as pass marks:
- **Warped difference** (0..255, lower is steadier; "how much the picture swims"): how much each frame differs from the one before once the camera's motion is taken out. Unstyled: 2.30 (whole path), 6.57 (pan).
- **Marks on things** (the shower-door score): negative means the stack's own marks line up with the surfaces when the camera pans, positive that they line up with the screen. Read the warning below.
- **Screen-stuck in the pan**: the whole picture's detail against the same spot on the screen (more negative is less stuck). Unstyled: -0.53.
- **Mark growth over the 10x zoom**: about 1 means marks keep their size in the picture, about 10 that they grow with the things they sit on (unstyled 7.88). Papers with almost no tooth (vellum, board, hot press, the notebook's rules) mostly follow the scene.
- **GPU ms high / low**: the style passes alone at the pan, on the high and the low quality tier.
- **Every light off**: the frame's mean brightness at the Lookbook's every-light-off moment, one frame per paper (unstyled 1.22): it must stay at the unstyled frame's black.
| Paper | Run | Warped difference, whole path | Warped difference, pan | Marks on things | Screen-stuck in the pan | Mark growth over the 10x zoom | GPU ms high | GPU ms low | Every light off (0..255) |
|---|---|---|---|---|---|---|---|---|---|
| Cold press (`paperColdPress`) | 1 | 3.63 | 7.30 | -0.05 | -0.54 | 1.57 | 0.64 | 0.17 | 1.15 |
| Cold press (`paperColdPress`) | 2 | 3.63 | 7.30 | -0.05 | -0.54 | 1.57 | 0.64 | 0.18 | |
| Rough (`paperRough`) | 1 | 4.12 | 7.96 | -0.06 | -0.52 | 1.58 | 0.64 | 0.17 | 1.17 |
| Rough (`paperRough`) | 2 | 4.11 | 7.96 | -0.06 | -0.52 | 1.58 | 0.64 | 0.18 | |
| Hot press (`paperHotPress`) | 1 | 2.35 | 6.42 | +0.45 | -0.53 | 3.86 | 0.64 | 0.17 | 1.19 |
| Hot press (`paperHotPress`) | 2 | 2.35 | 6.42 | +0.45 | -0.53 | 3.86 | 0.64 | 0.18 | |
| Rice paper (`paperRice`) | 1 | 2.87 | 6.56 | +0.15 | -0.53 | 2.02 | 0.64 | 0.18 | 1.18 |
| Rice paper (`paperRice`) | 2 | 2.87 | 6.56 | +0.15 | -0.53 | 2.02 | 0.64 | 0.17 | |
| Newsprint (`paperNewsprint`) | 1 | 2.19 | 5.71 | +0.84 | -0.53 | 3.26 | 0.64 | 0.17 | 1.06 |
| Newsprint (`paperNewsprint`) | 2 | 2.19 | 5.71 | +0.84 | -0.53 | 3.26 | 0.64 | 0.17 | |
| Kraft (`paperKraft`) | 1 | 2.26 | 4.90 | +0.41 | -0.53 | 1.81 | 0.64 | 0.18 | 1.14 |
| Kraft (`paperKraft`) | 2 | 2.26 | 4.90 | +0.41 | -0.53 | 1.81 | 0.64 | 0.18 | |
| Parchment (`paperParchment`) | 1 | 2.37 | 6.25 | +0.65 | -0.53 | 2.10 | 0.71 | 0.20 | 1.08 |
| Parchment (`paperParchment`) | 2 | 2.36 | 6.25 | +0.65 | -0.53 | 2.10 | 0.71 | 0.20 | |
| Canvas (`paperCanvas`) | 1 | 4.10 | 7.79 | +0.05 | -0.44 | 3.94 | 0.70 | 0.19 | 1.08 |
| Canvas (`paperCanvas`) | 2 | 4.09 | 7.79 | +0.05 | -0.44 | 3.94 | 0.70 | 0.19 | |
| Illustration board (`paperBoard`) | 1 | 2.28 | 6.35 | +0.78 | -0.53 | 6.07 | 0.54 | 0.15 | 1.15 |
| Illustration board (`paperBoard`) | 2 | 2.28 | 6.35 | +0.78 | -0.53 | 6.07 | 0.54 | 0.15 | |
| Blackboard (`paperBlackboard`) | 1 | 2.88 | 4.51 | +0.37 | -0.60 | 2.43 | 0.67 | 0.18 | 1.17 |
| Blackboard (`paperBlackboard`) | 2 | 2.87 | 4.51 | +0.37 | -0.60 | 2.43 | 0.67 | 0.18 | |
| Green chalkboard (`paperGreenChalkboard`) | 1 | 2.10 | 2.42 | +0.67 | -0.59 | 2.48 | 0.67 | 0.18 | 1.33 |
| Green chalkboard (`paperGreenChalkboard`) | 2 | 2.10 | 2.42 | +0.67 | -0.59 | 2.48 | 0.67 | 0.18 | |
| Black paper (`paperBlack`) | 1 | 3.45 | 8.74 | -0.56 | -0.55 | 2.09 | 0.64 | 0.17 | 1.10 |
| Black paper (`paperBlack`) | 2 | 3.45 | 8.74 | -0.56 | -0.55 | 2.09 | 0.64 | 0.18 | |
| Aged sketchbook (`paperSketchbook`) | 1 | 2.67 | 6.07 | +0.47 | -0.53 | 1.71 | 0.76 | 0.21 | 1.03 |
| Aged sketchbook (`paperSketchbook`) | 2 | 2.67 | 6.07 | +0.47 | -0.53 | 1.71 | 0.76 | 0.21 | |
| Toned grey (`paperTonedGrey`) | 1 | 2.72 | 6.62 | +0.07 | -0.53 | 1.77 | 0.64 | 0.17 | 1.20 |
| Toned grey (`paperTonedGrey`) | 2 | 2.72 | 6.62 | +0.07 | -0.53 | 1.77 | 0.64 | 0.18 | |
| Toned tan (`paperTonedTan`) | 1 | 2.86 | 7.08 | -0.10 | -0.53 | 1.77 | 0.64 | 0.17 | 1.17 |
| Toned tan (`paperTonedTan`) | 2 | 2.86 | 7.08 | -0.10 | -0.53 | 1.77 | 0.64 | 0.17 | |
| Ruled notebook (`paperNotebook`) | 1 | 2.54 | 6.39 | +0.20 | -0.52 | 4.48 | 0.66 | 0.18 | 1.19 |
| Ruled notebook (`paperNotebook`) | 2 | 2.54 | 6.39 | +0.20 | -0.52 | 4.48 | 0.66 | 0.18 | |
| Vellum (`paperVellum`) | 1 | 2.23 | 6.22 | +0.91 | -0.53 | 7.41 | 0.64 | 0.18 | 1.13 |
| Vellum (`paperVellum`) | 2 | 2.23 | 6.22 | +0.91 | -0.53 | 7.41 | 0.64 | 0.18 | |
What they say:
- **The shower-door score does not show the tooth staying on things at 1080p, for most papers.** At 1920 x 1080 most papers score positive (+0.05 to +0.91); cold press, rough, black paper and toned tan score negative. A paper's tooth is laid on world position, so this points at the Lookbook's mark comparison at this size rather than at the papers, but it is not proved. Check sliding by eye with a slow pan in your own scene. The screen-stuck column, which reads the whole picture, is -0.44 to -0.60 for every paper: nothing glues the picture to the screen.
- **Swimming.** Smooth papers add almost nothing to the unstyled path (hot press 2.35, board 2.28, vellum 2.23, newsprint 2.19, kraft 2.26 against 2.30). Deep tooth adds detail that moves with the surfaces: cold press 3.63, rough 4.12, canvas 4.10, black paper 3.45.
- **Black stays black.** With every light off every paper stays at the unstyled frame's black (1.03 to 1.33 against 1.22).
- **Cost at 1920 x 1080.** A paper alone is 0.54 to 0.76 ms on the high tier and 0.15 to 0.21 ms on the low tier, about four times cheaper; most of it is the paper's ground field (about 0.5 ms high), the picture pass about 0.14 ms. Under blocks that read the paper field (watercolorBlocks' bleed and edge darkening, every inkBlocks block) the paper field adds one more pass of about 0.5 ms high.
- **Line and wash on paper** (watercolorBlocks and inkBlocks installed; from an earlier long session on styleKit 1.2.2, so its two runs can differ by the sun bug above): 4.55 to 4.71 ms high and 0.93 to 1.00 ms low; calmer than unstyled on hot press (1.25) and 2.5 to 3.8 on cold press and canvas.
- **The Gallery and the Paper Swatches recipe** run seventeen papers at once (70 passes with the line and wash look): 14.7 ms of GPU time on high at 2097 x 1409. They are swatch books, not stacks to ship; a game picks one or two papers.
- **Web frame rate:** run `zero lookbook report` in the web build; it has not been measured from here (#20946).
Line and wash, and the unstyled baseline, from the earlier long session on styleKit 1.2.2 (`design/lookbook/paper_runs.json`; columns: warped whole path, warped pan, marks, screen-stuck, growth, GPU high, GPU low, every light off):
| Stack | Run | Warped difference, whole path | Warped difference, pan | Marks on things | Screen-stuck in the pan | Mark growth over the 10x zoom | GPU ms high | GPU ms low | Every light off (0..255) |
|---|---|---|---|---|---|---|---|---|---|
| baseline | 1 | 2.30 | 6.57 | 0.00 | -0.53 | 7.88 | - | - | 1.22 |
| baseline | 2 | 2.30 | 6.57 | 0.00 | -0.53 | 7.88 | - | - | |
| lineAndWash:paperHotPress | 1 | 1.25 | 1.43 | 0.73 | -0.61 | 3.81 | 4.55 | 0.93 | 1.04 |
| lineAndWash:paperHotPress | 2 | 1.25 | 1.43 | 0.73 | -0.61 | 3.81 | 4.56 | 0.93 | |
| lineAndWash:paperColdPress | 1 | 2.46 | 2.13 | 0.65 | -0.62 | 1.82 | 4.57 | 0.93 | 1.03 |
| lineAndWash:paperColdPress | 2 | 3.13 | 3.39 | 0.52 | -0.58 | 1.49 | 4.56 | 0.93 | |
| lineAndWash:paperCanvas | 1 | 2.97 | 3.11 | 0.62 | -0.21 | 4.84 | 4.71 | 1.00 | 1.02 |
| lineAndWash:paperCanvas | 2 | 3.79 | 4.72 | 0.49 | -0.18 | 3.27 | 4.71 | 1.00 | |
| lineAndWash:paperHotPress | bleed.wcBleed | 0.39 | 0.39 | 0.10 | 0.10 |
| lineAndWash:paperHotPress | dry.wcDryBrush | 0.27 | 0.27 | 0.07 | 0.07 |
| lineAndWash:paperHotPress | edge.wcEdgeDarken | 0.20 | 0.21 | 0.05 | 0.05 |
| lineAndWash:paperHotPress | grain.wcGranulation | 0.23 | 0.23 | 0.07 | 0.06 |
| lineAndWash:paperHotPress | line.inkPenLine | 1.19 | 1.19 | 0.15 | 0.15 |
| lineAndWash:paperHotPress | paper.paperHotPress | 0.14 | 0.14 | 0.03 | 0.03 |
| lineAndWash:paperHotPress | paper.paperHotPress.field | 0.50 | 0.50 | 0.13 | 0.13 |
| lineAndWash:paperHotPress | paper.paperHotPress.paper | 0.50 | 0.50 | 0.13 | 0.13 |
| lineAndWash:paperHotPress | wash.wcWash | 1.12 | 1.12 | 0.20 | 0.20 |
| lineAndWash:paperColdPress | bleed.wcBleed | 0.40 | 0.40 | 0.10 | 0.10 |
| lineAndWash:paperColdPress | dry.wcDryBrush | 0.27 | 0.27 | 0.07 | 0.07 |
| lineAndWash:paperColdPress | edge.wcEdgeDarken | 0.21 | 0.21 | 0.05 | 0.05 |
| lineAndWash:paperColdPress | grain.wcGranulation | 0.23 | 0.23 | 0.07 | 0.07 |
| lineAndWash:paperColdPress | line.inkPenLine | 1.19 | 1.19 | 0.15 | 0.15 |
| lineAndWash:paperColdPress | paper.paperColdPress | 0.14 | 0.14 | 0.03 | 0.03 |
| lineAndWash:paperColdPress | paper.paperColdPress.field | 0.50 | 0.50 | 0.13 | 0.13 |
| lineAndWash:paperColdPress | paper.paperColdPress.paper | 0.50 | 0.50 | 0.13 | 0.13 |
| lineAndWash:paperColdPress | wash.wcWash | 1.12 | 1.12 | 0.20 | 0.20 |
| lineAndWash:paperCanvas | bleed.wcBleed | 0.40 | 0.40 | 0.10 | 0.10 |
| lineAndWash:paperCanvas | dry.wcDryBrush | 0.28 | 0.28 | 0.08 | 0.07 |
| lineAndWash:paperCanvas | edge.wcEdgeDarken | 0.21 | 0.21 | 0.05 | 0.06 |
| lineAndWash:paperCanvas | grain.wcGranulation | 0.23 | 0.23 | 0.07 | 0.07 |
| lineAndWash:paperCanvas | line.inkPenLine | 1.20 | 1.20 | 0.16 | 0.16 |
| lineAndWash:paperCanvas | paper.paperCanvas | 0.14 | 0.14 | 0.03 | 0.04 |
| lineAndWash:paperCanvas | paper.paperCanvas.field | 0.57 | 0.57 | 0.15 | 0.15 |
| lineAndWash:paperCanvas | paper.paperCanvas.paper | 0.56 | 0.56 | 0.15 | 0.15 |
| lineAndWash:paperCanvas | wash.wcWash | 1.13 | 1.13 | 0.20 | 0.20 |
Each pass's GPU milliseconds in the long session, both runs, both tiers:
| Stack | Pass | High, run 1 | High, run 2 | Low, run 1 | Low, run 2 |
|---|---|---|---|---|---|
| paperColdPress | paper.paperColdPress | 0.13 | 0.13 | 0.04 | 0.04 |
| paperColdPress | paper.paperColdPress.field | 0.50 | 0.50 | 0.13 | 0.13 |
| paperRough | paper.paperRough | 0.13 | 0.13 | 0.04 | 0.04 |
| paperRough | paper.paperRough.field | 0.50 | 0.50 | 0.13 | 0.13 |
| paperHotPress | paper.paperHotPress | 0.13 | 0.13 | 0.04 | 0.04 |
| paperHotPress | paper.paperHotPress.field | 0.50 | 0.50 | 0.13 | 0.13 |
| paperRice | paper.paperRice | 0.13 | 0.13 | 0.04 | 0.04 |
| paperRice | paper.paperRice.field | 0.50 | 0.50 | 0.13 | 0.13 |
| paperNewsprint | paper.paperNewsprint | 0.13 | 0.13 | 0.04 | 0.04 |
| paperNewsprint | paper.paperNewsprint.field | 0.50 | 0.50 | 0.13 | 0.13 |
| paperKraft | paper.paperKraft | 0.13 | 0.14 | 0.04 | 0.04 |
| paperKraft | paper.paperKraft.field | 0.50 | 0.50 | 0.13 | 0.13 |
| paperParchment | paper.paperParchment | 0.14 | 0.14 | 0.04 | 0.04 |
| paperParchment | paper.paperParchment.field | 0.58 | 0.58 | 0.15 | 0.15 |
| paperCanvas | paper.paperCanvas | 0.14 | 0.13 | 0.04 | 0.04 |
| paperCanvas | paper.paperCanvas.field | 0.56 | 0.56 | 0.15 | 0.15 |
| paperBoard | paper.paperBoard | 0.13 | 0.13 | 0.04 | 0.04 |
| paperBoard | paper.paperBoard.field | 0.40 | 0.40 | 0.11 | 0.11 |
| paperBlackboard | paper.paperBlackboard | 0.14 | 0.14 | 0.04 | 0.04 |
| paperBlackboard | paper.paperBlackboard.field | 0.54 | 0.54 | 0.14 | 0.14 |
| paperGreenChalkboard | paper.paperGreenChalkboard | 0.14 | 0.14 | 0.04 | 0.04 |
| paperGreenChalkboard | paper.paperGreenChalkboard.field | 0.54 | 0.54 | 0.14 | 0.14 |
| paperBlack | paper.paperBlack | 0.14 | 0.13 | 0.04 | 0.04 |
| paperBlack | paper.paperBlack.field | 0.51 | 0.50 | 0.13 | 0.13 |
| paperSketchbook | paper.paperSketchbook | 0.13 | 0.13 | 0.04 | 0.04 |
| paperSketchbook | paper.paperSketchbook.field | 0.62 | 0.63 | 0.17 | 0.17 |
| paperTonedGrey | paper.paperTonedGrey | 0.14 | 0.14 | 0.04 | 0.04 |
| paperTonedGrey | paper.paperTonedGrey.field | 0.51 | 0.50 | 0.14 | 0.13 |
| paperTonedTan | paper.paperTonedTan | 0.13 | 0.14 | 0.04 | 0.04 |
| paperTonedTan | paper.paperTonedTan.field | 0.50 | 0.50 | 0.13 | 0.13 |
| paperNotebook | paper.paperNotebook | 0.13 | 0.13 | 0.04 | 0.04 |
| paperNotebook | paper.paperNotebook.field | 0.53 | 0.53 | 0.14 | 0.14 |
| paperVellum | paper.paperVellum | 0.13 | 0.13 | 0.04 | 0.04 |
| paperVellum | paper.paperVellum.field | 0.50 | 0.49 | 0.13 | 0.13 |
| lineAndWash:paperHotPress | bleed.wcBleed | 0.39 | 0.39 | 0.10 | 0.10 |
| lineAndWash:paperHotPress | dry.wcDryBrush | 0.27 | 0.27 | 0.07 | 0.07 |
| lineAndWash:paperHotPress | edge.wcEdgeDarken | 0.20 | 0.21 | 0.05 | 0.05 |
| lineAndWash:paperHotPress | grain.wcGranulation | 0.23 | 0.23 | 0.07 | 0.06 |
| lineAndWash:paperHotPress | line.inkPenLine | 1.19 | 1.19 | 0.15 | 0.15 |
| lineAndWash:paperHotPress | paper.paperHotPress | 0.14 | 0.14 | 0.03 | 0.03 |
| lineAndWash:paperHotPress | paper.paperHotPress.field | 0.50 | 0.50 | 0.13 | 0.13 |
| lineAndWash:paperHotPress | paper.paperHotPress.paper | 0.50 | 0.50 | 0.13 | 0.13 |
| lineAndWash:paperHotPress | wash.wcWash | 1.12 | 1.12 | 0.20 | 0.20 |
| lineAndWash:paperColdPress | bleed.wcBleed | 0.40 | 0.40 | 0.10 | 0.10 |
| lineAndWash:paperColdPress | dry.wcDryBrush | 0.27 | 0.27 | 0.07 | 0.07 |
| lineAndWash:paperColdPress | edge.wcEdgeDarken | 0.21 | 0.21 | 0.05 | 0.05 |
| lineAndWash:paperColdPress | grain.wcGranulation | 0.23 | 0.23 | 0.07 | 0.07 |
| lineAndWash:paperColdPress | line.inkPenLine | 1.19 | 1.19 | 0.15 | 0.15 |
| lineAndWash:paperColdPress | paper.paperColdPress | 0.14 | 0.14 | 0.03 | 0.03 |
| lineAndWash:paperColdPress | paper.paperColdPress.field | 0.50 | 0.50 | 0.13 | 0.13 |
| lineAndWash:paperColdPress | paper.paperColdPress.paper | 0.50 | 0.50 | 0.13 | 0.13 |
| lineAndWash:paperColdPress | wash.wcWash | 1.12 | 1.12 | 0.20 | 0.20 |
| lineAndWash:paperCanvas | bleed.wcBleed | 0.40 | 0.40 | 0.10 | 0.10 |
| lineAndWash:paperCanvas | dry.wcDryBrush | 0.28 | 0.28 | 0.08 | 0.07 |
| lineAndWash:paperCanvas | edge.wcEdgeDarken | 0.21 | 0.21 | 0.05 | 0.06 |
| lineAndWash:paperCanvas | grain.wcGranulation | 0.23 | 0.23 | 0.07 | 0.07 |
| lineAndWash:paperCanvas | line.inkPenLine | 1.20 | 1.20 | 0.16 | 0.16 |
| lineAndWash:paperCanvas | paper.paperCanvas | 0.14 | 0.14 | 0.03 | 0.04 |
| lineAndWash:paperCanvas | paper.paperCanvas.field | 0.57 | 0.57 | 0.15 | 0.15 |
| lineAndWash:paperCanvas | paper.paperCanvas.paper | 0.56 | 0.56 | 0.15 | 0.15 |
| lineAndWash:paperCanvas | wash.wcWash | 1.13 | 1.13 | 0.20 | 0.20 |
The fresh-install runs are in `design/lookbook/paper_fresh_runs.txt` in the Style Cookbook world.