# The gallery: every model and texture, on pages you can read at a glance

A game makes dozens or hundreds of models (fish, weapons, toys, bosses, props), and play shows only
a few of them at a time. `vp gallery` draws **all of them**, with the game's own engine and
materials, on a few numbered pages, and checks them for you: broken or empty models, parts that
float loose, near-duplicates, budget outliers, floating models, textures that break the style.

**After making or changing any art, run `bunx vp gallery`, read every page, fix what's flagged,
and include the pages in your report. Highly recommended.**

## Contents

1. `gallery.ts`: declaring the assets
2. Running it, and the pages
3. The checks
4. Iterating: `--diff`, filters, `--scene`
5. Browsing: `vp dev ?gallery`
6. Pitfalls

## 1. `gallery.ts`: declaring the assets

Put a `gallery.ts` next to `index.ts`. Its default export gets a `Gallery` and adds every model with
a **builder**: a function that returns a fresh three.js object, made by the same code the game uses.
Builders run only for the assets a page shows. `gallery.ts` isn't part of the game's package, so
nothing it imports ships to players.

```ts
// gallery.ts
import { Group, Mesh } from 'three';
import { useGameAssets } from '@voxelparty/sdk';
import type { Gallery } from '@voxelparty/sdk/test';
import { BLOCKS, TEXTURES } from './textures';
import { FISH, RODS, fishGeometry, rodGeometry } from './models';

export default (g: Gallery) => {
  const ids = useGameAssets(TEXTURES, BLOCKS);          // as create() does
  const mesh = (geo) => new Mesh(geo, g.engine.mats.actor);

  g.group('Fish', { scale: 'fit', ground: false });      // each fills its cell
  for (const f of FISH) {
    g.add(f.name, () => mesh(fishGeometry(ids, f)), {
      tags: [f.rarity],
      variants: { shiny: () => mesh(fishGeometry(ids, f, 'shiny')), golden: () => mesh(fishGeometry(ids, f, 'gold')) },
    });
  }

  g.group('Rods', { scale: 'shared', ghost: 0.7 });      // one camera: true relative size, a ghost player
  for (const r of RODS) g.add(r.name, () => mesh(rodGeometry(ids, r)));

  g.add('Bobber', () => {                                // one that moves: a GalleryModel
    const m = mesh(bobberGeometry(ids));
    return { object: m, update: (t) => void (m.position.y = Math.sin(t * 3) * 0.05) };
  }, { motionMs: 2000 });

  g.textures('Pond textures', TEXTURES);
  g.effects('Effects', EFFECTS);                         // vfx.ts: stills, and film strips with --motion
};
```

| `g.` | |
|---|---|
| `engine` | what `create()` gets as `ctx.engine`: `mats` (the game's voxel materials: `actor`, `solid`…), `env`, `icon` |
| `group(name, opts?)` | the assets added after it belong to it (before any: "Models") |
| `add(name, build, opts?)` | a model; its number (#37) stays the same run after run |
| `textures(name, defs)` | 16×16 `TexDef`s: tiled 3×3 (seams show), with a cube for any block that uses them |
| `effect(name, effect, opts?)` | a visual effect (an `Effect` from `vfx.ts`): its busy moment on a dark plate, its film strip on the motion page |
| `effects(group, record, opts?)` | a group of effects, each one added: `g.effects('Spells', EFFECTS)` |

`build` returns an `Object3D` (a `Mesh`, a `Group`, an `Avatar`'s `root`, an `InstancedMesh`), or a
`GalleryModel` `{ object, update?(t) }` for one that moves by itself (an idle, a spin: `t` in seconds).
Animated materials (water, glow pulses) move by themselves; nothing to add.

Group options (`GalleryGroupOptions`):

- `scale`: `'fit'` (default) fills each cell with the model, for detail. `'shared'` uses one camera
  for the whole group, so relative size is true, with a ghost player and a 1-unit grid on the
  ground. Use it for things that stand side by side in play: toys, units, buildings, bosses.
- `ghost`: `false` for none; a number is the players' `Avatar` scale in your game (default 1).
- `ground` (default true): the models stand on the ground, so their base should be at y = 0. False
  for held items (their origin is the grip), projectiles, pickups, flyers and effects.
- `yaw`, `pitch`: the three-quarter view (default 30° round, 25° up; 0 looks at the model's front, +z).
- `tiny`: how tall these are on screen in play, in px, for `--tiny`.
- `pieces`: how many separate pieces each model is meant to have (default 1; more is flagged as a
  part that floats free). `'any'` for a group of effects or scattered things.

Asset options (`GalleryAssetOptions`): `tags` (shown and filterable), `variants` (other looks,
`{ name: build }`: one row each on the variants pages), `ground`, `tiny` and `pieces` (override the
group's), `motionMs` (how long its motion strip spans, default 2000), and `budget` (its own triangle
budget: flagged above it, instead of being compared with its group).

**Effects** (`GalleryEffectOptions`): `tags`, `color` (play it in a team's colour), `scale`,
`colors` (more colours, one variant each: `{ red: '#e23b3b', blue: '#3b8ee2' }`), `loop` (seconds a
looping effect runs before it's stopped, default 2.5) and `at` (the still's moment; default early
in its life, at most 0.4 s, or 1.2 s into a loop). An effect replays itself exactly to any moment,
so its pictures are the same every run. On the motion page its six frames bunch up early (1/36,
4/36, 9/36… of the way), since a burst is over in a blink and its smoke lingers. Beams go from
upper left to lower right; trails circle so they show. The model checks (ground, pivot, pieces,
triangles, look-alikes) don't apply to effects; `checkEffect`'s findings are listed instead, and
an effect with nothing alive at its still moment is flagged. `--bg dark` suits glowing effects.
See vfx.md.

A part that's meant to float (a halo, sparkles, an orbiting shield, a spell's projectile) can also
be named: an object whose `name` starts with `float` or `orbit` (`halo.name = 'orbitHalo'`) is left
out of the pieces check, with everything under it. Naming it in the model code keeps the reason
next to the part; `pieces: 3` says it from `gallery.ts` (a squadron of three planes).

Your models already have a home in the game's code: export the builders (`fishGeometry(ids, f)`)
from a `models.ts` and call them from both places. If the game puts a model together from parts
(a boss's body and wings), do it the same way in the builder.

## 2. Running it, and the pages

```sh
bunx vp gallery                      # overview (40 a page), variants, textures, the checks
bunx vp gallery --turnaround         # + front, back, left, right, top and ¾ of each (8 a page)
bunx vp gallery --all --bg dark      # every kind of page, on a dark background
```

It builds the game, opens it headless and muted (on its title card), runs `gallery.ts` inside the
game's frame, and writes PNGs to `.vp/gallery/` with `index.json` (every asset: its number, name,
size, triangles, what the checks said, and which page and cell it's on) and `report.json`. Open
the pages and read them. Every page is at most about 1,536 px on its long side, so a reading
model sees it without shrinking it and every label stays legible.

| Page | Flag | What's on it |
|---|---|---|
| `overview-N.png` | always | 40 a page (8×5), a three-quarter perspective view each. `--page N` for N a page. |
| `variants-N.png` | when there are variants | one asset a row: its base look, then each variant |
| `textures-N.png` | when there are textures | 64 a page, each tiled 3×3, nearest-neighbour, a cube for block textures |
| `turnaround-N.png` | `--turnaround` | 8 a page: front, back, left, right, top (orthographic, one scale) and ¾ |
| `silhouette-N.png` | `--silhouette` | solid black shapes: does each read by its outline alone? |
| `motion-N.png` | `--motion` | 6 frames over time of each thing that moves (animated materials, `update`) |
| `tiny-N.png` | `--tiny [px]` | each at its size on screen in play (`tiny`, else 32 px), and the same pixels blown up |
| `diff-N.png` | `--diff` | before and after, for what changed since the last run |
| `unregistered-N.png` | `--scene` | models the game drew that `gallery.ts` doesn't declare |

Every cell has the asset's **number** (#37), its name, its size in units and voxels (a voxel model's
grid is worked out from its vertices), triangles, draw calls and tags. A red outline and a **⚠**
badge mark a cell a check flagged. Numbers are kept in `.vp/gallery/numbers.json`: a new asset
gets the next free number, so "fix #37" means the same model tomorrow.

Backgrounds: `--bg light` (default), `dark`, or `night` (dark, with the light turned down): glowing
voxels only show on dark ones. `--all` draws every kind of page.

## 3. The checks

Warnings are printed (`⚠ #22 Moth: it floats 0.08 u above y = 0…`) and in `report.json`. A builder
that throws is a real failure (`✖`, and the exit code says so); the rest are warnings, and each is
worth fixing or deliberately answering.

- **Broken:** a builder that throws; no meshes; everything hidden or fully transparent; a picture
  that came out empty (inside-out faces, zero size); NaN positions; a pivot off to one side (it
  turns and scales round a point away from the model); floating above y = 0, or sunk below it,
  in a group that stands on the ground.
- **Loose parts:** "#12 Scythe: 2 separate pieces (a part floats free?): besides the biggest,
  0.4×0.4×0.1 u at (-0.45, 0.6, 0)". Each model's triangles are joined into connected pieces:
  triangles that share a corner or touch (voxels meeting face to face, edge to edge or corner to
  corner, two meshes in contact), and a piece shut inside another (an eye set in a head). More
  pieces than the model's `pieces` (default 1) is flagged, with the loose ones' size and where
  they are, and the cell's badge says "⚠ 2 pieces". A blade a voxel off its handle, wheels beside
  a body, a fin that doesn't reach the fish. Specks (under a tenth of the model's size and 2% of
  its surface) don't count; hidden objects and parts named `float…` or `orbit…` are left out; an
  `InstancedMesh`'s instances each count. Meant to be apart: `pieces: n` or `'any'`, or name the part.
- **Near-duplicates:** "#41 and #88 look 94% alike": silhouettes compared by perceptual hash and
  overlap, and colours cell by cell. Groups of look-alikes are listed together. The most useful
  check for libraries built in code: 54 weapons from one function drift into twins. Models with
  the same silhouette in other colours are listed once as recolours (a note, not a ⚠): fine for
  team colours on purpose, flat for a rarity ladder where each tier should look new.
- **Budget outliers:** triangles judged by size: the group's median triangles per unit² of
  bounding box, times this model's box, is what it's expected to have, and 4× that (and 1,000
  more) is flagged. A 6-unit log beside lily pads is fine; a 50k-triangle pebble isn't. An asset
  with a `budget` is held to that instead. Draw calls at 3× the group's median (and 4 more). Fix:
  merge the faces (`meshVolume` does), fewer voxels, or the parts into one geometry.
- **Style:** textures that aren't 16×16, framed pictures not marked `decal: true`, a block texture
  with see-through pixels that isn't `cutout`, more than 160 textures (a game's limit); a model
  that nearly vanishes against the background; one that's a speck at its size in play (`--tiny`).

`vp check` runs the gallery (overview, textures and the checks) whenever there's a `gallery.ts`,
and suggests one when a game clearly makes many models without one.

## 4. Iterating: `--diff`, filters, `--scene`

- `--diff`: compares with the last run and draws before/after pages of only the assets that
  changed (new geometry, a new material, or a picture that no longer looks the same), were added
  or were removed. Run the gallery once, change the art, run `vp gallery --diff`.
- `--group <glob>`, `--only <glob>` (a name), `--tag <tag>`: just some of them, with their usual
  numbers. `vp gallery --only "Sword*" --turnaround` to work on one family. Textures are left out
  of filtered runs unless you add `--textures`.
- `--scene`: plays the game on autopilot for 30 seconds, collects every distinct model it drew, and
  lists the ones `gallery.ts` doesn't declare ("14 models are in the game but not in your
  gallery"), with a page of them. Players' bodies, particles and anything bigger than 20 units (the
  level) are left out.

## 5. Browsing: `vp dev ?gallery`

With `bunx vp dev` running, open `http://127.0.0.1:5180/?gallery`: the whole library over the
game, in a grid with search (names, groups, `#37`) and tag filters. Click one to turn it (drag) and
zoom (wheel), with animated materials moving live; **N** switches day and night, **C** (or "Compare
with…") puts a second one beside it, **Esc** goes back. It rebuilds on save like the game, and
uses the numbers of your last `vp gallery` run.

## 6. Pitfalls

- **`import type { Gallery } from '@voxelparty/sdk/test'`**: types only. The test entry is for bun
  tests, so a value import from it fails in the game's frame.
- **"a builder returned nothing"**: return the object (`() => mesh(geo)`), not a statement block
  that forgets to.
- **Everything floats or is sunk:** your models aren't built with their base at y = 0 (the voxel
  origin: `meshVolume(v, { voxel, origin: [sx / 2, 0, sz / 2] })`), or they're not meant to stand
  (`ground: false`).
- **Shared-scale cells look empty:** thin or small things (weapons, coins) are specks at true size
  next to a player. Use `'fit'` for those, `'shared'` for things that stand side by side.
- **"2 separate pieces" on a model that's meant to be apart** (formations, orbiting bits, a
  boss's floating hands): say so with `pieces: 2` (or `'any'`) on the asset or its group, or name
  the part `float…`/`orbit…`. If it isn't meant, move the part a voxel closer: in play it floats too.
- **A builder that depends on the game running** (reads `ctx`, the world, the players) won't work
  here: builders get only `g.engine`. Pass what they need, as the game does to its model code.
- Glow only shows against dark: check glowing things with `--bg dark` or `--bg night`.
