# lookbookKit
The Lookbook: a scripted test scene with one repeatable 26-second camera path whose stops each test one way a look fails, plus the code that plays it over any style stack, saves the same frames for side-by-side comparison and measures stability with real numbers.
Version 1.0.12. Depends on styleKit 1.2.3 and starterBlocks 1.0.12 (the tests, the combination test and the reference looks use its blocks).
## Install and use
Install **lookbookKit** from ZeroMind (search "lookbookKit" or "style lookbook test scene"); it brings styleKit and starterBlocks along (the Lookbook's tests, combination test and reference looks use starterBlocks' blocks). Installs with several dependencies can be slow (#21420), so give an install time to finish and check `/zero/source` for the package folder. Every call below loads the `Lookbook` scene first when it is not loaded (`Lookbook.ensureScene()`), so you can start from any scene:
```luau
local Lookbook = require("~lookbookKit.Lookbook")
local StyleKit = require("~styleKit.StyleKit")
StyleKit.load({ { block = "posterize" }, { block = "depthLine" } })
local shots = Lookbook.run({ label = "posterInk" }) -- the same frames, as PNGs
local numbers = Lookbook.measure({ dt = 1 / 15 }) -- stability numbers, styled and unstyled
print(numbers.stylized.all.warpedDiff, numbers.stylized.markStuck, numbers.passMs.high.total)
local cmp = Lookbook.compare({ { block = "depthLine" } }, { { block = "sketchLine" } }) -- two stacks side by side
print(cmp.sideBySide[1].file) -- stack A on the left, B on the right
Lookbook.combos("depthLine") -- the combination test
```
From the shell: `zero lookbook measure`, `zero lookbook run myLook`, `zero lookbook combos depthLine`.
## The stops
One camera path, 26 seconds, the same every time (everything that moves is driven by the Lookbook's clock, so the same moment gives the same frame):
| # | Stop | What it tests |
|---|---|---|
| 1 | Textured wall | A fine texture under a pan, a dolly, an orbit and a 10x zoom: marks that slide, swim or keep their screen size. |
| 2 | Flat grey wall under a slow light change | Banding and steps sweeping across a flat surface as light changes slowly. |
| 3 | Thin geometry | Wires and fence slats thinner than a line: shimmer and broken lines. |
| 4 | Floor grazing to the horizon | False lines and mark noise on a surface seen edge-on. |
| 5 | Fast skinned character | Lines and marks on a fast, deforming body. |
| 6 | Contact and intersection | Double outlines and halos where objects touch and cut through each other. |
| 7 | Wind-blown foliage | Busy edges and flicker on swaying leaf cards. |
| 8 | Transparency | Glass, water and smoke over lined objects. |
| 9 | Rotating sun, moving spot, zero lights | Light-dependent blocks under moving light, and a moment with every light off, where the frame must stay black. |
| 10 | Depth planes with fog and sky | Planes from 5 to 160 m in fog: line fading, mark size with distance, sky treatment. |
| 11 | Mixed animation rates | Objects stepping at 6, 12, 24 and 60 per second beside smooth motion and the held clock. |
| 12 | Excluded object, world label and UI | An object the stack must leave alone, a label in the world, and screen UI that must stay readable. |
`LookbookPath.segments()` names the moments the measurements read: `pan`, `dolly`, `orbit`, `zoom`, `slowLight`, `sunRotate`, `zeroLights`, `excluded`. Packages can add stops after the core path with `LookbookPath.extend(stop)` (lookbookShowcaseKit adds one built from published kits).
## The numbers
Everything is measured on the frame the player saw, after antialiasing, the upscale and the stack, before the UI, by the `lookbookProbe` render feature on the GPU, and read back:
- **warpedDiff**: the mean difference (0..255) between each frame and the one before it, warped along the motion vectors, with newly revealed pixels left out (`revealedShare` says how many). A stable look scores close to the unstyled frame; boil and grain raise it on purpose.
- **marks / marksWarped / marksSameSpot / markStuck**: the stack's own marks are the frame the player saw less the frame before the stack. `marks` is how strong their fine detail is; `marksWarped` and `marksSameSpot` correlate it with the previous frame along motion and on the same screen spot. During the pan, **markStuck** = sameSpot less warped: clearly negative when the marks stay on things (plain paper reads about -0.67), clearly positive when they are stuck to the screen (soft grain reads about +0.68). Absent when a stack leaves no marks of its own.
- **detailWarped / detailSameSpot**: how well fine detail (a pixel less the mean of its four neighbours) correlates with the previous frame along motion, and on the same screen spot. During the pan, **screenStuck** = same spot minus warped: positive means marks stay on the screen instead of on things.
- **zoomMarks**: the typical size of fine marks (pixels between detail sign changes) at the start and the end of the 10x zoom. Marks that stay on things grow with the zoom (`growth` well above 1); marks stuck to the screen keep their size (`growth` near 1).
- **passMs**: GPU milliseconds per style pass (`post.sk.<entry>.<block>`) and their total, measured over the pan at each quality tier asked for (high and low by default).
- **baseline**: the same numbers for the unstyled frame, measured in the same run.
- **missed**: frames the probe could not read back (0 in every run so far); a run with misses is not comparable.
How a run steps: the probe itself places the camera and the clock one `dt` step per drawn frame, and measures every frame against the frame before it, so the camera moves on every frame as a player's does and two runs place the same poses and measure the same frames, whatever the frame rate. Two runs of the same stack give identical numbers.
`Lookbook.compare(a, b)` photographs the same moments for both stacks (`<folder>/a`, `<folder>/b`), writes one side-by-side picture per moment (`<folder>/side_<T>_<stop>.png`, A left, B right) and measures both (`numbers.a`, `numbers.b`; pass `numbers = false` to skip).
`Lookbook.combos(blockId)` puts the block with one partner from every other slot (partners at strength 0.6), plays the pan, then swaps the block for a sibling of its slot and plays it again. A round fails when an entry did not run, the engine logged errors, or no frame was measured.
## Cost per look, and measuring on the web
`Lookbook.cost()` (shell: `zero lookbook cost 1920 1080`) times every style pass of a look over the pan, high and low quality, with the 3D view drawing at the size you ask for: in the editor the window is grown until the view itself is that size, so the numbers are for a real 1080p picture. The cost table for this release is in styleKit's README under "What a look costs".
**On the web**, one command does the whole report. Open this world in a browser engine, and in its shell run:
```
zero lookbook report
```
It first measures the frame rate in the scene you are in (`Lookbook.frameRate()`: no look, the Cookbook look on high, on low, taken in turns so a machine that speeds up or slows down meanwhile affects all three alike), then loads the Lookbook scene, measures the Cookbook stack over the full path (stability numbers beside the unstyled frame), prices every reference look at 1920 x 1080, writes `/zero/source/tmp/lookbook/report_<platform>.md` and `.json`, and puts the previous scene back. `zero lookbook report true` adds every block alone. A desktop run of the same command writes `report_native.md`, so the two sit side by side. The numbers do not depend on the frame rate (one measured step per drawn frame), so a slower browser gives the same stability numbers and only the milliseconds differ.
## Parts
- `Lookbook.scene`: the stops, the player setup, a synced empty StyleStack, the `LookbookCamera` the runner routes to the viewport, the excluded object `LookbookExcluded`, the world label and the screen panel.
- `LookbookPath`: the path (`LENGTH`, `STOPS`, `pose(T)`, `segments()`, `extend`).
- `LookbookClock`: the Lookbook's time; `LookbookAnimator`: the component that moves everything from it.
- `Lookbook`: `run`, `compare`, `measure`, `combos`, `frameAt`, `frameRate`, `cost`, `report`, `keyTimes`, `last`, `ensureScene`, `restoreScene`.
- `lookbookProbe` render feature and the `lbStore` / `lbMeasure` compute shaders.
- `lookbook` toolbox (`measure`, `run`, `combos`, `cost`, `report`) and the `lookbook` test suite.
## Known limits
- **The shower-door score (`markStuck`) swings with a few pixels of view size.** Measured 2026-11-01 around 1080p, the same paper (paperBlocks' kraft) scored -0.58 at 1918 x 1080, +0.33 at 1917 x 1077 and +0.50 at 1920 x 1080 in one session, and starterBlocks' `plainPaper` and watercolorBlocks' `wcColdPress` also scored positive at 1920 x 1080 though their papers are laid on world position. The pan's mark strength (`pan.marks`) jumps with it (about 5 against 13), while warped difference, mark growth and GPU times stay put. Until this is understood, compare a look's `markStuck` against another look measured at the same view size in the same session, never against a published number, and check sliding by eye with a slow pan. `result.size` says which size a run used.
- The runner takes the viewport (`camera.setEditorOverride`) while it plays and gives it back after; do not drive the editor camera meanwhile.
- A call made from another scene loads the Lookbook scene and leaves it loaded; `ensureScene` returns the scene it replaced and `restoreScene` puts it back (the test suite does both).
- The numbers are taken one step per drawn frame at the `dt` you pass, so they do not depend on the frame rate; compare stacks measured with the same `dt`. Two runs of the same stack give the same numbers to every printed digit.
- The smoke in the transparency stop is moving cards, since the engine's particle effects cannot be driven by the Lookbook's clock frame by frame.
- Offscreen captures do not draw the stack (styleKit runs after post); the Lookbook therefore always reads presented frames.
## Version history
**1.0.12 (2026-11-02)**: needs styleKit 1.2.3, which keeps the scene's sun on a block after its shader compiles (a fresh install could draw sun-lit blocks without their sun); no block changed.
**1.0.11 (2026-11-01)**: `Lookbook.measure` states the 3D view size its motion numbers were measured at (`result.size`), and refuses a run whose view changed size halfway (an editor panel opening or the window resized), since every number depends on that size; built on styleKit 1.2.2, whose sun no longer carries over from the run before.
**1.0.10 (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.0.9 (2026-11-01)**: documentation only: the install note says how installing works today (install from ZeroMind search; installs with several dependencies can be slow).
**1.0.8 (2026-11-01)**: the report and `Lookbook.cost` also price the ink looks (brush and wash, comic) and line and wash wherever every block they name is installed (inkBlocks, watercolorBlocks), and the report adds the brush-and-wash ink look to its frame-rate table, so `zero lookbook report` in a web build gives the ink numbers too. Worlds without those kits get the same rows as before.
**1.0.7 (2026-11-01)**: a measurement also waits until no shader has compiled and no GPU pipeline has been built for a whole second after its stack loads, so the first run after a scene load no longer measures frames drawn before the stack's shaders were ready.
**1.0.6 (2026-11-01)**: every measurement waits until the stack it loads has finished compiling and the picture has settled before it starts, so the first run after a stack change no longer measures half-styled frames and two runs give the same numbers.
**1.0.5 (2026-10-31)**: store listing rewritten for creators; built on styleKit 1.0.8 and starterBlocks 1.0.6.
**1.0.4 (2026-10-31)**: depends on starterBlocks as well as styleKit, as it always used both; `Lookbook.frameRate()` measures the frame rate in the scene you are in (no look, high, low, in turns), and `zero lookbook report` starts with it, so a web run states its frame rate.
**1.0.3 (2026-10-31)**: the probe steps and measures one frame per drawn frame, so two runs give identical numbers; `markStuck` tells marks on things from marks on the screen; `compare` writes side-by-side pictures and both stacks' numbers; `run` and `compare` save frames again; every call loads the Lookbook scene itself; `cost` and `report` for 1080p and web numbers; the Lookbook panel no longer dims every frame; new tests for repeatability, stuck marks and combined stacks.
**1.0.2 (2026-10-31)**: runs drive the stack's clock with the Lookbook's time, so a stack with boil or grain gives the same numbers run after run; the probe measures nearly every frame instead of about half; GPU timings are read only once the profiler's window holds the tier alone.
**1.0.1 (2026-10-31)**: `frameAt`, the test suite, frames after a cut left out of the numbers, a run camera of its own, GPU timings counted only for the stack being measured.
**1.0.0 (2026-10-31)**: first release.