spriteforge/docs/procedural.md

138 lines
5.4 KiB
Markdown
Raw Permalink Normal View History

# 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`.