Initial commit — procedural blob-schema reference asset lib v0.1.0
Companion implementation to the design discussion at
sporel-meta/docs/design/2026-05-28-autotile-blob-styles-design.md §9.
Ships the S-V2E2-RM-Blob 14-slot schema in four canonical style
profiles, generated procedurally so the asset lib can be rebuilt
from scratch on any platform with Python 3 + Pillow:
Styles (14 base slots each, sub-cell-grid layout):
rectilinear — crisp 90-degree corners (dungeon walls, retro)
diagonal_cut — 45-degree chamfered convex corners (iso-feel)
organic — rounded quarter-arc convex corners (water/moss)
concave — concave-bite convex corners (frost/web)
Material variants (single-material atlases, one tinted set each):
blob_rect_stone — dark blue-gray (#282C38)
blob_rect_grass — mid green (#4A7838)
blob_rect_wood — light brown (#8B6B3F)
Multi-material testbench atlas:
blob_testbench — 3 materials x 14 slots = 42 tiles in one
atlas, for the vagrant-skeleton showcase
Per-style collision.json (axis-aligned rect-list, row+column merged
to minimum form: slot 13 collapses to a single 64x64 rect, slot 4
to a single 32x64 vertical bar, cross variants max 3 rects).
Phase-1: same rectilinear collision shared across all 4 styles,
max 5 px deviation at corners; per-style polygons deferred to
atlas-baker E2.
Reference sheets at docs/reference_sheet_<style>.png show all 14
slots per style in a 7x2 grid with slot index + bitmask + 3x3
neighbour mini-diagram. Color showcase at docs/color_showcase.png
demonstrates that one atlas serves N visual materials via runtime
tint (5 sample colours).
Atlas packer (scripts/atlas_pack.py) is a pure-Python substitute
for sporel-tool-atlas-baker. It produces the Sporel-conformant
4-file atlas set (tiles.atlas.json + tiles.atlas.lock.json +
tiles.diffuse.atlas.png + 1x1 placeholder tiles.height.atlas.png)
with 2 px edge-replicated padding around every tile to prevent
bilinear sampler bleed (sister-tile colours leaking at UV
boundaries — the artefact that caused the green-flicker bug
caught during 0.5.0c integration with vagrant).
Generation pipeline (scripts/bake.sh):
generate.py -> 56 base sprites + 4 collision.json
reference_sheet.py -> 4 reference_sheet PNGs
color_showcase.py -> color_showcase.png
bake_testbench.py -> 42-tile mixed-material atlas
bake_singles.py -> 3 x 14-tile single-material atlases
All output deterministic from the seed scripts (no random state).
.gitignore excludes the baked atlases (assets/atlases/) — those
are reproducible from sources via the bake scripts.
License: CC0 (procedural output, no third-party authorship claim).
Safe for any public-facing build.
Used in production by sporel-module-vagrant-skeleton via the
asset_aliases blob_testbench + blob_rect_stone — see the matching
commit there for the consumer-side integration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
260
README.md
Normal file
260
README.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# lib-asset.prototype-blob-geom
|
||||
|
||||
Procedurally-generated **reference assets** for the Sporel blob autotile
|
||||
scheme (`S-V2E2-RM-Blob`, 14-slot reduction). All sprites are simple
|
||||
geometric primitives, drawn programmatically by the bundled Python
|
||||
scripts. Intended as a **starting template** for human artists who will
|
||||
produce real blob-material sprite sheets (stone, wood, water, ...) that
|
||||
slot into the same 14-slot schema.
|
||||
|
||||
**Version:** 0.1.0
|
||||
**Lib-ID:** `lib-asset.prototype-blob-geom`
|
||||
**Role:** asset (pure data, no Lua code)
|
||||
**License:** CC0 (procedural output, no authorship claim)
|
||||
**Requires:** (none)
|
||||
|
||||
## What's in here
|
||||
|
||||
```
|
||||
prototype-blob-geom/
|
||||
├── manifest.lib
|
||||
├── LICENSE ← CC0
|
||||
├── README.md ← this file
|
||||
├── .gitignore
|
||||
├── assets/
|
||||
│ └── _sources/
|
||||
│ ├── blob_rectilinear/ ← 14 raw 64x64 PNGs + collision.json
|
||||
│ ├── blob_diagonal_cut/ ← (same slot count, different visual style)
|
||||
│ ├── blob_organic/
|
||||
│ └── blob_concave/
|
||||
├── scripts/
|
||||
│ ├── generate.py ← procedural writer (sprites + collision)
|
||||
│ ├── reference_sheet.py ← per-style overview
|
||||
│ ├── color_showcase.py ← runtime-tint demo
|
||||
│ └── bake.sh ← regen everything + atlas-baker for all 4 styles
|
||||
└── docs/
|
||||
├── reference_sheet_rectilinear.png
|
||||
├── reference_sheet_diagonal_cut.png
|
||||
├── reference_sheet_organic.png
|
||||
├── reference_sheet_concave.png
|
||||
└── color_showcase.png
|
||||
```
|
||||
|
||||
## The 14-slot schema
|
||||
|
||||
Each tile reads its 8 neighbour cells. The neighbour-bitmask is clockwise
|
||||
from north, LSB-first:
|
||||
|
||||
| bit | direction |
|
||||
|----:|:---------:|
|
||||
| 0 | N |
|
||||
| 1 | NE |
|
||||
| 2 | E |
|
||||
| 3 | SE |
|
||||
| 4 | S |
|
||||
| 5 | SW |
|
||||
| 6 | W |
|
||||
| 7 | NW |
|
||||
|
||||
A diagonal bit only counts if **both** adjacent cardinals are also set
|
||||
(blob-gating). After gating and reducing the 47 valid patterns under 90°
|
||||
rotation + mirror symmetry, you land on **14 canonical slots**:
|
||||
|
||||
| Slot | Name | Canonical bitmask | Pattern (cardinals + diagonals) |
|
||||
|-----:|--------------|------------------:|----------------------------------------|
|
||||
| 0 | isolated | `0b00000000` | no neighbours |
|
||||
| 1 | end | `0b00000001` | 1 cardinal (N) |
|
||||
| 2 | corner_open | `0b00000101` | 2 adjacent cardinals (N+E), no diag |
|
||||
| 3 | corner_full | `0b00000111` | 2 adjacent cardinals + diagonal (NE) |
|
||||
| 4 | straight | `0b00010001` | 2 opposite cardinals (N+S) |
|
||||
| 5 | tee_open | `0b00010101` | 3 cardinals (N+E+S), no diags |
|
||||
| 6 | tee_half | `0b00010111` | 3 cardinals + 1 diag (NE) |
|
||||
| 7 | tee_full | `0b00011111` | 3 cardinals + 2 diags (NE+SE) |
|
||||
| 8 | cross_open | `0b01010101` | 4 cardinals, 0 diags |
|
||||
| 9 | cross_q1 | `0b01010111` | 4 cardinals + 1 diag |
|
||||
| 10 | cross_q2adj | `0b01011111` | 4 cardinals + 2 adjacent diags |
|
||||
| 11 | cross_q2opp | `0b01110111` | 4 cardinals + 2 opposite diags (NE+SW) |
|
||||
| 12 | cross_q3 | `0b01111111` | 4 cardinals + 3 diags |
|
||||
| 13 | solid | `0b11111111` | fully surrounded |
|
||||
|
||||
At runtime the engine (or atlas-baker, future M.2+) maps every one of
|
||||
the 47 raw blob patterns to one of these 14 slots by applying the
|
||||
canonical rotation + mirror needed to bring the raw pattern to its
|
||||
canonical form. Artists only ever draw the 14 base sprites.
|
||||
|
||||
## Styles
|
||||
|
||||
All 4 styles share the **same filled-sub-cell mask per slot** (every
|
||||
64×64 tile is conceptually a 4×4 grid of 16×16 sub-cells; the
|
||||
filled-sub-cell logic is identical across styles). They differ only in
|
||||
how **external convex corners** of the material region are rendered:
|
||||
|
||||
- The mini neighbour-diagram in the bottom-right of each cell shows
|
||||
which of the 8 surrounding cells share material with the centre cell.
|
||||
- Slot 13 (`solid`) is identical across all styles because there are no
|
||||
external convex corners when material continues seamlessly into all 8
|
||||
neighbours.
|
||||
- All other slots have at least one external boundary corner, and the
|
||||
styles diverge there.
|
||||
|
||||
### Rectilinear
|
||||
|
||||
Sharp 90° corners everywhere. Crisp, blocky, pixel-perfect look. Suits
|
||||
**dungeon walls, stone floors, retro pixel art**. Easiest to author
|
||||
custom art for — every sub-cell is just a filled rectangle.
|
||||
|
||||

|
||||
|
||||
### Diagonal-cut
|
||||
|
||||
External convex corners chamfered with a 45° straight line. Gives an
|
||||
**architectural, octagonal feel** — slot 0 reads as an octagon, slot 1
|
||||
as a "tab", slot 13 stays flat (no boundary). Good for **isometric-feel
|
||||
walls, polished stone, art-deco trim**.
|
||||
|
||||

|
||||
|
||||
### Organic
|
||||
|
||||
External convex corners replaced with smooth quarter-arc curves
|
||||
(rounded-rectangle treatment). Gives a **bauchy, flowing, blob-like
|
||||
feel**. Suits **water, moss, lava, slime, vegetation** — anything that
|
||||
should look like it has surface tension.
|
||||
|
||||

|
||||
|
||||
### Concave
|
||||
|
||||
External convex corners bitten with an inward quarter-disk. The material
|
||||
boundary at corners has **concave indentations**, giving a **filigree,
|
||||
crystalline, spider-web feel**. Suits **ice frost, cracks, mycelium,
|
||||
arcane growths** — anything that should look like it's reaching out
|
||||
fragilely.
|
||||
|
||||

|
||||
|
||||
## Collision
|
||||
|
||||
Each style's source directory ships a `collision.json` describing the
|
||||
per-slot collision footprint. Geometry is expressed as a list of
|
||||
axis-aligned rectangles (row-wise + column-wise merged to the minimum
|
||||
useful count). All four styles ship **the same rectilinear collision**;
|
||||
for diagonal-cut / organic / concave the visible boundary deviates by
|
||||
at most 5 px at convex external corners, which is acceptable for
|
||||
typical gameplay collision. Pixel-perfect overrides are a future
|
||||
refinement modders can ship per material.
|
||||
|
||||
Format (one entry per slot):
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "blob-14",
|
||||
"tile_size_px": 64,
|
||||
"shape_kind": "rect_list",
|
||||
"slots": [
|
||||
{ "slot": 4, "name": "straight", "bitmask": "0b00010001",
|
||||
"rects": [[16, 0, 32, 64]] },
|
||||
{ "slot": 8, "name": "cross_open", "bitmask": "0b01010101",
|
||||
"rects": [[16, 0, 32, 16], [0, 16, 64, 32], [16, 48, 32, 16]] },
|
||||
{ "slot": 13, "name": "solid", "bitmask": "0b11111111",
|
||||
"rects": [[0, 0, 64, 64]] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Worst case (cross variants) is 3 rectangles; slot 13 collapses to a
|
||||
single 64x64 rect; slot 4 to a single 32x64 vertical bar. The numbers
|
||||
are tile-local pixels; the consuming engine translates by the tile's
|
||||
world position. Friendly to Box2D / Chipmunk / AABB-tree physics
|
||||
backends. Per the design paper §6 the atlas-baker (planned E2) will
|
||||
fold these into a sibling `tiles.collision.json` next to the diffuse
|
||||
atlas.
|
||||
|
||||
## Color variations
|
||||
|
||||
The atlases ship in a single neutral stone-gray. Materials don't fork
|
||||
the atlas — they **declare a colour tint at the material level**, and
|
||||
the engine multiplies the atlas's per-pixel alpha against the tint at
|
||||
render time. One geometric atlas, N visual materials:
|
||||
|
||||

|
||||
|
||||
This is the production architecture: defining a new material is a
|
||||
manifest-level declaration of `{schema, style, tint, collision_policy}`,
|
||||
not an asset fork. The bake-pipeline could in principle pre-tint
|
||||
atlases for shader-free renderers, but for the M.2+ atlas format the
|
||||
tint is applied at draw call time.
|
||||
|
||||
Tints used in the showcase above:
|
||||
|
||||
| Material | Tint |
|
||||
|----------|-------------|
|
||||
| stone | `#282C38` (default) |
|
||||
| grass | `#4A7838` |
|
||||
| earth | `#5C3D24` |
|
||||
| wood | `#8B6B3F` |
|
||||
| ice | `#7BA8C8` |
|
||||
|
||||
Modders define their own palette in their module manifest.
|
||||
|
||||
## How to use this lib
|
||||
|
||||
**As a real consumer:** add to module manifest's `deps` and
|
||||
`asset_aliases`, choosing which style's atlas you want:
|
||||
|
||||
```json
|
||||
"deps": [
|
||||
{"id":"lib-asset.prototype-blob-geom","version":"0.1.0"}
|
||||
],
|
||||
"asset_aliases": {
|
||||
"blob_demo_v1": "lib-asset.prototype-blob-geom"
|
||||
}
|
||||
```
|
||||
|
||||
Maps reference slots by name (`slot_05_tee_open`) or the runtime selects
|
||||
them by computed bitmask.
|
||||
|
||||
**As a template for new art:** copy the directory, rename to
|
||||
`lib-asset.<your-material-name>`, then either:
|
||||
|
||||
1. Hand-author 14 PNGs in `_sources/<your-pack>/`, one per slot, using
|
||||
one of the reference sheets above as your style guide; OR
|
||||
2. Modify `scripts/generate.py` if you want to keep the procedural
|
||||
approach for a different geometric primitive (e.g. a chunkier
|
||||
blocky style or a more aggressive organic curve).
|
||||
|
||||
The atlas-baker (`scripts/bake.sh`) packs your 14 PNGs into the runtime
|
||||
atlas format — same pipeline as `prototype-subterrain` and every other
|
||||
Sporel asset lib.
|
||||
|
||||
## Regenerating
|
||||
|
||||
```bash
|
||||
python scripts/generate.py # 14*4 = 56 base sprites + 4 collision.json
|
||||
python scripts/reference_sheet.py # 4 reference sheets
|
||||
python scripts/color_showcase.py # color showcase image
|
||||
bash scripts/bake.sh # all of the above + atlas-bake all 4 styles
|
||||
```
|
||||
|
||||
Dependencies: Python 3.10+ with Pillow; Node.js (for atlas-baker).
|
||||
|
||||
## References
|
||||
|
||||
- Design paper: [`sporel-meta/docs/design/2026-05-28-autotile-blob-styles-design.md`](../../../sporel-meta/docs/design/2026-05-28-autotile-blob-styles-design.md)
|
||||
- Boris the Brave, ["Classification of Tilesets"](https://www.boristhebrave.com/2021/11/14/classification-of-tilesets/) — origin of the `S-V2E2-RM-Blob` notation
|
||||
- cr31, ["Blob Tileset"](https://www.cr31.co.uk/stagecast/wang/blob.html) — canonical 47-blob reference
|
||||
- ADR-0005 (Reserved namespace prefixes — `lib-asset.*` is not reserved, this is a community lib)
|
||||
|
||||
## Notes
|
||||
|
||||
- **License: CC0.** This lib is safe for any context including
|
||||
public-facing builds, unlike the `toBeDeleted` prototype-asset libs
|
||||
(subterrain, fa-starter) that carry third-party licensing.
|
||||
- The output sprites are intentionally simple. Real game-grade art for
|
||||
the same schema should add: bevels, shadow under-tiles, texture noise,
|
||||
edge anti-aliasing, and per-style-profile collision polygons (see
|
||||
the design paper §6).
|
||||
- The four styles ship as **parallel atlases**, not as a single
|
||||
combined atlas. A consumer module picks one style per material; mixing
|
||||
styles for the *same* material within one map would create visual
|
||||
incoherence.
|
||||
Reference in New Issue
Block a user