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:
calic
2026-05-28 20:51:44 +02:00
commit 247cdf0575
160 changed files with 2322 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
{
"schema": "blob-14",
"tile_size_px": 64,
"sub_cell_px": 16,
"shape_kind": "rect_list",
"_note": "Collision matches the rectilinear visual exactly. For diagonal_cut/organic/concave styles, the visible boundary deviates by up to 5 px at convex external corners \u2014 acceptable for typical gameplay collision. Modders needing pixel-perfect physics ship per-style polygon overrides.",
"slots": [
{
"slot": 0,
"name": "isolated",
"bitmask": "0b00000000",
"rects": [
[
16,
16,
32,
32
]
]
},
{
"slot": 1,
"name": "end",
"bitmask": "0b00000001",
"rects": [
[
16,
0,
32,
48
]
]
},
{
"slot": 2,
"name": "corner_open",
"bitmask": "0b00000101",
"rects": [
[
16,
0,
32,
16
],
[
16,
16,
48,
32
]
]
},
{
"slot": 3,
"name": "corner_full",
"bitmask": "0b00000111",
"rects": [
[
16,
0,
48,
48
]
]
},
{
"slot": 4,
"name": "straight",
"bitmask": "0b00010001",
"rects": [
[
16,
0,
32,
64
]
]
},
{
"slot": 5,
"name": "tee_open",
"bitmask": "0b00010101",
"rects": [
[
16,
0,
32,
16
],
[
16,
16,
48,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 6,
"name": "tee_half",
"bitmask": "0b00010111",
"rects": [
[
16,
0,
48,
48
],
[
16,
48,
32,
16
]
]
},
{
"slot": 7,
"name": "tee_full",
"bitmask": "0b00011111",
"rects": [
[
16,
0,
48,
64
]
]
},
{
"slot": 8,
"name": "cross_open",
"bitmask": "0b01010101",
"rects": [
[
16,
0,
32,
16
],
[
0,
16,
64,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 9,
"name": "cross_q1",
"bitmask": "0b01010111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 10,
"name": "cross_q2adj",
"bitmask": "0b01011111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
16,
48,
48,
16
]
]
},
{
"slot": 11,
"name": "cross_q2opp",
"bitmask": "0b01110111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
0,
48,
48,
16
]
]
},
{
"slot": 12,
"name": "cross_q3",
"bitmask": "0b01111111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
48
]
]
},
{
"slot": 13,
"name": "solid",
"bitmask": "0b11111111",
"rects": [
[
0,
0,
64,
64
]
]
}
],
"style": "concave"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

View File

@@ -0,0 +1,270 @@
{
"schema": "blob-14",
"tile_size_px": 64,
"sub_cell_px": 16,
"shape_kind": "rect_list",
"_note": "Collision matches the rectilinear visual exactly. For diagonal_cut/organic/concave styles, the visible boundary deviates by up to 5 px at convex external corners \u2014 acceptable for typical gameplay collision. Modders needing pixel-perfect physics ship per-style polygon overrides.",
"slots": [
{
"slot": 0,
"name": "isolated",
"bitmask": "0b00000000",
"rects": [
[
16,
16,
32,
32
]
]
},
{
"slot": 1,
"name": "end",
"bitmask": "0b00000001",
"rects": [
[
16,
0,
32,
48
]
]
},
{
"slot": 2,
"name": "corner_open",
"bitmask": "0b00000101",
"rects": [
[
16,
0,
32,
16
],
[
16,
16,
48,
32
]
]
},
{
"slot": 3,
"name": "corner_full",
"bitmask": "0b00000111",
"rects": [
[
16,
0,
48,
48
]
]
},
{
"slot": 4,
"name": "straight",
"bitmask": "0b00010001",
"rects": [
[
16,
0,
32,
64
]
]
},
{
"slot": 5,
"name": "tee_open",
"bitmask": "0b00010101",
"rects": [
[
16,
0,
32,
16
],
[
16,
16,
48,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 6,
"name": "tee_half",
"bitmask": "0b00010111",
"rects": [
[
16,
0,
48,
48
],
[
16,
48,
32,
16
]
]
},
{
"slot": 7,
"name": "tee_full",
"bitmask": "0b00011111",
"rects": [
[
16,
0,
48,
64
]
]
},
{
"slot": 8,
"name": "cross_open",
"bitmask": "0b01010101",
"rects": [
[
16,
0,
32,
16
],
[
0,
16,
64,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 9,
"name": "cross_q1",
"bitmask": "0b01010111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 10,
"name": "cross_q2adj",
"bitmask": "0b01011111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
16,
48,
48,
16
]
]
},
{
"slot": 11,
"name": "cross_q2opp",
"bitmask": "0b01110111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
0,
48,
48,
16
]
]
},
{
"slot": 12,
"name": "cross_q3",
"bitmask": "0b01111111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
48
]
]
},
{
"slot": 13,
"name": "solid",
"bitmask": "0b11111111",
"rects": [
[
0,
0,
64,
64
]
]
}
],
"style": "diagonal_cut"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

View File

@@ -0,0 +1,270 @@
{
"schema": "blob-14",
"tile_size_px": 64,
"sub_cell_px": 16,
"shape_kind": "rect_list",
"_note": "Collision matches the rectilinear visual exactly. For diagonal_cut/organic/concave styles, the visible boundary deviates by up to 5 px at convex external corners \u2014 acceptable for typical gameplay collision. Modders needing pixel-perfect physics ship per-style polygon overrides.",
"slots": [
{
"slot": 0,
"name": "isolated",
"bitmask": "0b00000000",
"rects": [
[
16,
16,
32,
32
]
]
},
{
"slot": 1,
"name": "end",
"bitmask": "0b00000001",
"rects": [
[
16,
0,
32,
48
]
]
},
{
"slot": 2,
"name": "corner_open",
"bitmask": "0b00000101",
"rects": [
[
16,
0,
32,
16
],
[
16,
16,
48,
32
]
]
},
{
"slot": 3,
"name": "corner_full",
"bitmask": "0b00000111",
"rects": [
[
16,
0,
48,
48
]
]
},
{
"slot": 4,
"name": "straight",
"bitmask": "0b00010001",
"rects": [
[
16,
0,
32,
64
]
]
},
{
"slot": 5,
"name": "tee_open",
"bitmask": "0b00010101",
"rects": [
[
16,
0,
32,
16
],
[
16,
16,
48,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 6,
"name": "tee_half",
"bitmask": "0b00010111",
"rects": [
[
16,
0,
48,
48
],
[
16,
48,
32,
16
]
]
},
{
"slot": 7,
"name": "tee_full",
"bitmask": "0b00011111",
"rects": [
[
16,
0,
48,
64
]
]
},
{
"slot": 8,
"name": "cross_open",
"bitmask": "0b01010101",
"rects": [
[
16,
0,
32,
16
],
[
0,
16,
64,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 9,
"name": "cross_q1",
"bitmask": "0b01010111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 10,
"name": "cross_q2adj",
"bitmask": "0b01011111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
16,
48,
48,
16
]
]
},
{
"slot": 11,
"name": "cross_q2opp",
"bitmask": "0b01110111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
0,
48,
48,
16
]
]
},
{
"slot": 12,
"name": "cross_q3",
"bitmask": "0b01111111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
48
]
]
},
{
"slot": 13,
"name": "solid",
"bitmask": "0b11111111",
"rects": [
[
0,
0,
64,
64
]
]
}
],
"style": "organic"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

View File

@@ -0,0 +1,270 @@
{
"schema": "blob-14",
"tile_size_px": 64,
"sub_cell_px": 16,
"shape_kind": "rect_list",
"_note": "Collision matches the rectilinear visual exactly. For diagonal_cut/organic/concave styles, the visible boundary deviates by up to 5 px at convex external corners \u2014 acceptable for typical gameplay collision. Modders needing pixel-perfect physics ship per-style polygon overrides.",
"slots": [
{
"slot": 0,
"name": "isolated",
"bitmask": "0b00000000",
"rects": [
[
16,
16,
32,
32
]
]
},
{
"slot": 1,
"name": "end",
"bitmask": "0b00000001",
"rects": [
[
16,
0,
32,
48
]
]
},
{
"slot": 2,
"name": "corner_open",
"bitmask": "0b00000101",
"rects": [
[
16,
0,
32,
16
],
[
16,
16,
48,
32
]
]
},
{
"slot": 3,
"name": "corner_full",
"bitmask": "0b00000111",
"rects": [
[
16,
0,
48,
48
]
]
},
{
"slot": 4,
"name": "straight",
"bitmask": "0b00010001",
"rects": [
[
16,
0,
32,
64
]
]
},
{
"slot": 5,
"name": "tee_open",
"bitmask": "0b00010101",
"rects": [
[
16,
0,
32,
16
],
[
16,
16,
48,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 6,
"name": "tee_half",
"bitmask": "0b00010111",
"rects": [
[
16,
0,
48,
48
],
[
16,
48,
32,
16
]
]
},
{
"slot": 7,
"name": "tee_full",
"bitmask": "0b00011111",
"rects": [
[
16,
0,
48,
64
]
]
},
{
"slot": 8,
"name": "cross_open",
"bitmask": "0b01010101",
"rects": [
[
16,
0,
32,
16
],
[
0,
16,
64,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 9,
"name": "cross_q1",
"bitmask": "0b01010111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
16,
48,
32,
16
]
]
},
{
"slot": 10,
"name": "cross_q2adj",
"bitmask": "0b01011111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
16,
48,
48,
16
]
]
},
{
"slot": 11,
"name": "cross_q2opp",
"bitmask": "0b01110111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
32
],
[
0,
48,
48,
16
]
]
},
{
"slot": 12,
"name": "cross_q3",
"bitmask": "0b01111111",
"rects": [
[
16,
0,
48,
16
],
[
0,
16,
64,
48
]
]
},
{
"slot": 13,
"name": "solid",
"bitmask": "0b11111111",
"rects": [
[
0,
0,
64,
64
]
]
}
],
"style": "rectilinear"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Some files were not shown because too many files have changed in this diff Show More