# Procedural backend `procedural` is deterministic: identical manifest data and seed produce byte-identical RGBA frames. Variant `n` derives a stable RNG stream from the asset seed. Unknown parameters fail the build. The backend serves two kinds of generators, selected by the manifest `generator` name. A name may live in only one of the two registries, so the choice is never ambiguous. | kind | signature | `dirs` | returns | |---|---|---|---| | single frame | `(params, rng) -> (rgba, (pivot_x, pivot_y))` | must be `1` | one image per variant | | sequence | `(spec, rng) -> RawAsset` | any `1..32` | frames, directions, animations | ## Single-frame generators ```python from spriteforge.backends.procedural import register_generator register_generator("floor_tile", floor_tile) ``` The backend repeats the generator `params.variants` times, one frame per variant, with `duration_ms` taken from `fps`, one direction and the animation `default`. ### `floor_tile` Isometric diamond. Parameters: `width` (64), `height` (width/2), `color`, `edge_color`, `noise`, `variants`, `dither`. ### `wall_tile` Isometric top plus two vertical faces. Parameters: `width` (64), `top_height` (width/2), `height` (64), `top_color`, `left_color`, `right_color`, `variants`, `dither`. ### `decal` Irregular soft ellipse. Parameters: `width` (48), `height` (24), `color`, `irregularity`, `variants`, `dither`. Colors are `#RRGGBB` or `#RRGGBBAA`. ## Sequence generators A sequence generator owns its whole asset: it receives the validated `AssetSpec` (so `dirs`, `fps`, `pivot`, `size`, `anims`, `params` and `seed` are all visible) plus one seeded `numpy.random.Generator`, and returns a `RawAsset`. This is the shape used by animated multi-direction sprites such as characters. ```python from spriteforge.backends.procedural import register_sequence_generator from spriteforge.backends.sequence import (build_sequence_asset, direction_centidegrees, duration_from_fps, resolve_pivot) def walker(spec, rng): directions = direction_centidegrees(spec.dirs) duration = duration_from_fps(spec.fps) animations = {} for name in spec.anims: animations[name] = [[draw(name, direction, frame, duration, rng) for frame in range(4)] for direction in range(len(directions))] return build_sequence_asset(animations, directions) register_sequence_generator("walker", walker) ``` ```yaml hero: backend: procedural generator: walker dirs: 8 fps: 12 anims: [idle, walk, attack] size: 48x64 params: {palette: bone} ``` Registration happens at import time, so the module that calls `register_sequence_generator` must be imported by the package — add it to `spriteforge/backends/__init__.py`. Otherwise `sf build` reports an unknown generator and lists the registered names of both kinds. The result is checked against the manifest before anything is encoded: the direction count must equal `dirs`, animation names must be unique and non-empty, every animation requested in `anims` must be present, and the frame count must equal what the animation table describes. Failures name the generator. Everything downstream is unchanged: the common postprocess thresholds alpha, crops each frame while preserving its pivot, optionally scales down to manifest `size`, builds one median-cut RGB palette, optionally applies ordered dithering, and generates estimated XY normals and local depth. Cropping means frames of one animation differ in size, so the runtime aligns them by pivot, never by corner. ### Directions and animations `direction_centidegrees(count)` returns `count` evenly spaced angles in hundredths of a degree, starting at `0` — the same table the Blender driver emits, so an asset keeps facing the same way whichever backend built it. `build_sequence_asset(animations, directions)` takes `{animation_name: [direction][frame]}` and lays the frames out the way the `.sfa` format expects: animations are stored back to back, and inside one animation the order is direction-major, that is all frames of direction 0, then direction 1, and so on. ```text index = animation.first_frame + direction * animation.frames_per_direction + frame ``` That formula lives in exactly one function, `sequence.frame_index`, used both by the layout code and by the tests. A generator must never index frames by hand: a wrong order does not fail the build, it silently ships sprites that face the wrong way. Animation order in the file follows the key order of the mapping, and the first key becomes the asset's default animation. Optional arguments mirror the format: `layer_count` with one `layer_orders` permutation per frame, and `warnings` surfaced by `sf validate`. ### Drawing helpers `spriteforge.pixel` is a backend-agnostic pixel-art toolbox over `HxWx4` uint8 arrays — the same buffer `RawFrame` expects. It has canvas and fill, bounds checked pixels, Bresenham lines, thick lines, rectangles, ellipses, capsules for limbs, silhouette outlining, colour shading and three-step ramps, a directional rim light, and depth-sorted limbs that darken the far arm and leg. Randomness is never global: every noisy helper takes the `numpy.random.Generator` handed to the generator, which is what keeps builds reproducible. ```text sf build assets/environment.yaml --backend procedural --output build ``` The result is one `.sfa` per asset, `palettes.sfp`, and a compact `index.json`.