5.4 KiB
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
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.
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)
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.
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.
sf build assets/environment.yaml --backend procedural --output build
The result is one .sfa per asset, palettes.sfp, and a compact index.json.