Contents

M9 Tier 2 assets

Tier 2 assets are images, textures, music and the like that no deterministic builder makes. ARCHITECTURE §12 gives their pipeline: generate, post-process, QA, freeze. The generators need provider keys, and the candidates are re-surveyed when one is chosen. The rest of the pipeline does not need them, and an asset the person brings in goes through it exactly as a generated one will. This design builds that pipeline first, with imported art as its first source and painted backdrops as its first use.

Read with ARCHITECTURE §1 ("Specs, not pixels"), §12, ADR-0002 rule 7 ("Tier 2 outputs are frozen") and ADR-0004 (painted backgrounds).

The asset store

  • Frozen assets live at assets/<sha256>.<ext>, with a provenance sidecar at assets/<sha256>.json: source (imported or generated), generator, model, prompt, seed, date, license, original file name, and the steps applied.
  • Documents reference them by AssetRef {path, hash}. Validation checks the file exists, its hash matches, and its provenance has a license.
  • Change sets carry JSON and text only. As with recorded voice takes, the bytes are stored first. They are inert until a document names them, and the proposal is the document that names them.

The pipeline (core/src/assets/)

  1. Source: an imported file (import_asset, reelwright asset import), a sky Reelwright paints itself (paint_sky), or, later, an adapter: ImageAdapter.generate(prompt, size, seed?), which returns bytes and provenance. Without a configured provider, the tool says a key is needed.
  2. Normalize: decode, convert to sRGB PNG, and limit the size (backdrops: 4096 px on the long side).
  3. Snap to the palette: when the style's color.rules.snapToPalette is on, pull each pixel toward the nearest palette color (the set's palette and the style's rules), by a strength that keeps painted texture. This is the field reserved for M9.
  4. QA:
    • palette compliance: the share of pixels within ΔE (OKLab) of the palette;
    • an aspect and size check for its use (a backdrop card's aspect);
    • for character art later: silhouette readability and consistency with the approved sheet. A failure reports where the asset falls short.
  5. Freeze: store the result by its hash with full provenance, and the source by its own hash next to it.

First use: painted backdrops

A set backdrop takes image: AssetRef as an alternative to a procedural generator (SetInstance 1.2; validation requires exactly one). The image is an equirectangular panorama, 2:1: the middle column looks straight upstage (-Z), east is to the right, and the middle row is the horizon. The engine draws it on the backdrop's dome at its distance, as it draws the starfield, sampled by world direction, with the style's background saturation and value scales; parallax still comes from the camera, and motion drifts it as before. The import_asset tool proposes the set change that names the asset, with a preview of the set's wide view. An existing backdrop keeps its id, distance and drift and drops its generator, seed and params; a new id adds one at 400 m.

(implementation) A painted sky. paint_sky paints the panorama from a description, at 2048×1024, CC0, with no provider: the sky's color straight up and at the horizon, far land below the horizon, two ranges of far hills along it (the near one in the given color, the far one toward the horizon's), suns (a disc with a glow), moons and planets (lit as a ball from the sun, in two steps, the dark side in shade; bands, tipped by tiltDeg; a ring nearly edge on, its far half behind the planet and its near half before it), flat- bottomed clouds low in the sky, and stars (none by day). Azimuth 0 is straight upstage, positive to the right. It is flat, stepped color, as the cel style draws, so the snap moves little, and it goes through the same pipeline and proposal as an import. Seeded: the same description paints the same bytes. The thirtieth watch-it's showrunner, wanting a picnic under a ringed planet with no image provider, painted the sky in Python.

Snapping holds a backdrop to the set's palette and the style's shared palette, unscaled: the engine scales the backdrop by the background rules when it draws it, as it scales every set color, so the two stay in step.

A render job carries what its sets name: the core copies each frozen asset beside the job's job.json (<job dir>/assets/<sha256>.png), and the engine resolves AssetRef paths against the job's directory, or against assetRoot when a job written elsewhere names one (a Movie Maker capture's child job). The engine checks the bytes against the pinned hash and leaves out, with a warning, an image that is missing or changed.

Footage: clips from Cutwright

Cutwright, the footage editor, hands Reelwright edited footage to play on a screen in a set (its bridge, requests R5 and R6): a TV in a living room, the bridge's viewscreen.

  • The hand-over. Cutwright's clip export writes a folder: clip.json (CutwrightClip 1.0, schemas/v1/cutwright-clip.schema.json), the frames (JPEG by default, PNG for proofs) and audio.wav. Each file is named with its SHA-256. conform is one_to_one: frame k of the clip is frame k of a program at programRate. The clip's provenance names the Cutwright project and edit, the words it was cut between, and its sources by Cutwright's quick hash.

  • Bringing it in. reelwright asset import-clip <dir> --show <show> --license <spdx>, or the import_clip tool, checks the folder before anything is stored (core/src/assets/clip.ts):

    • clip.json against the schema;
    • every frame and the sound against its hash;
    • each frame is a PNG or JPEG of the clip's size;
    • the sound is a WAV of the rate, channels and length it says.

    Anything else is refused with what is wrong (clip.hash_mismatch, clip.bad_frame, clip.bad_audio, clip.invalid, clip.missing_file). The files are frozen as they came, with no normalizing and no snapping, since footage is what it is. Each sidecar names Cutwright and its version as the generator, the person's license, and the clip's own record (clip: its id, the file, the frame, the clip.json's hash, its provenance and license). The ClipInstance that names them (clips/<slug>.clip.json, clip.<slug>) is proposed: frames and sound as AssetRefs, rate the program rate, and source the clip as Cutwright wrote it.

  • Playing it. A screen cue (EpisodeIR 1.49) names an element and a clip, timed as any cue. It lasts the clip's length at the rate compiled at, or durationSec.

    • The Timeline (1.26) carries the frames that play in Scene.screens.
    • On frame n the element's screen face shows frames[n − startFrame], decided by the frame number alone: an RwGraphicScreen with a frame source. On an element with a graphic it replaces the graphic while it plays; on one without, a screen is made on its face (a prim.box, a prim.panel, or a kit piece's screen box).
    • The picture is letterboxed on black and glows.
    • Its sound is an AudioPlan clip from the element, as an SFX from it is.
    • The frames travel beside job.json as backdrops do, linked rather than copied.
    • A clip cut for another rate still plays one frame a frame, so the compiler warns (screen.rate) with the rate to compile at. --fps (reelwright render --fps 29.97) plays it as it was cut.
  • Checking it. The engine's RenderResult (1.1) says what each screen of the frame's shot showed: the clip frame, and the four corners of its picture in output pixels (top-left first, clockwise). Corners are left out when the screen is behind the camera. Before anything is encoded, the core checks every record against the Timeline (screenProblems in core/src/render/verify.ts):

    • the clip frame must be n − start;
    • the frame server must report every frame a screen of the shot plays on.

Watch it

Paint, or take any CC0 starfield image, and import it as the bridge porthole's backdrop. It snaps to the Marigold palette, passes QA with a palette-compliance score, is frozen under assets/, and the bridge's wide view shows it through the porthole. Restyling re-snaps nothing (the frozen asset is canon), and the drift report stays clean.