138 lines
5.4 KiB
Markdown
138 lines
5.4 KiB
Markdown
|
|
# 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`.
|