sprite-machine 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,58 +1,64 @@
1
1
  # sprite-machine
2
2
 
3
- Turn a **3×2 sheet of pixel-art face sprites** (left / front / top over
4
- right / back / bottom) into a real, low-poly, textured **three.js mesh**
5
- or a **glTF 2.0 binary** any engine's importer reads. The chunky look is
6
- carved into the geometry, not faked by a shader: 1 pixel = 1 voxel, with
7
- 45° wedges smoothing every same-colour staircase and the colour riding a
8
- nearest-sampled skin texture.
3
+ Turns a 3×2 sheet of pixel-art face sprites (left / front / top over right /
4
+ back / bottom) into a low-poly, textured three.js mesh or a glTF 2.0 binary.
5
+ One pixel is one voxel. 45° wedges smooth every same-colour staircase, and
6
+ the colour comes from a nearest-sampled skin texture.
9
7
 
10
8
  This is the engine behind [Sprite Machine](https://aportilla.github.io/sprite-machine/),
11
- the System 7 desktop app that draws these sheets. Keep the sheet as the
12
- source of truth and derive the model wherever you need it — at a build
13
- step, at a server's startup, or in the browser, straight into a scene.
9
+ the desktop app that draws these sheets. It runs at a build step, at a
10
+ server's startup, or in the browser.
14
11
 
15
12
  ```bash
16
13
  npm install sprite-machine three
17
14
  ```
18
15
 
19
- `three` is a peer dependency: the mesh is a `THREE.Mesh` and the mesher
20
- uses three's geometry utilities. Node 20.19+ / 22.12+.
16
+ `three` is a peer dependency: the mesh is a `THREE.Mesh`, and the mesher uses
17
+ three's geometry utilities. Requires Node 20.19+ or 22.12+.
21
18
 
22
19
  ## The API
23
20
 
24
21
  ```js
25
22
  import { buildModel, modelToGlb } from 'sprite-machine';
26
23
 
27
- // pixels in: {width, height, data} an ImageData, or the same shape
28
- const model = buildModel(sheet, { transforms });
24
+ // sheet: {width, height, data}, an ImageData or the same shape
25
+ const model = buildModel(sheet, { transforms, layers });
29
26
  // → { mesh, dims, triangles, warnings, unitsPerVoxel: 1 }
30
- // a THREE.Mesh at ONE UNIT PER VOXEL, its skin the material's map;
31
- // `transforms` is the document's per-view reorientation (optional)
27
+ // mesh is a THREE.Mesh at one unit per voxel, its skin the material's map.
28
+ // transforms is the per-view reorientation (optional). layers is the
29
+ // sheet's block count (optional, 1 by default; see Layers).
32
30
 
33
31
  const glb = modelToGlb(model, { name: 'car', voxelsPerMeter: 10 });
34
32
  // → Uint8Array: one node, one mesh, one primitive, the skin embedded as
35
- // a PNG behind a NEAREST sampler; `unlit: true` for KHR_materials_unlit
33
+ // a PNG behind a NEAREST sampler. unlit: true adds KHR_materials_unlit.
36
34
  ```
37
35
 
38
- Both are synchronous and pure. A three.js page uses `model.mesh` directly
39
- and never writes a file; a Node process writes the glb. To run the build
40
- off a main thread, wrap it in a worker or a child process.
36
+ Both functions are synchronous and pure. `buildModel` throws on an invalid
37
+ sheet or a sheet with no painted view in any layer. A three.js page can use
38
+ `model.mesh` directly, and a Node process writes the glb. To build off the main
39
+ thread, call them from a worker or a child process.
41
40
 
42
41
  ### In Node: a document PNG in
43
42
 
44
43
  ```js
45
44
  import { readSheet, sheetToGlb } from 'sprite-machine/node';
46
45
 
47
- const { image, name, transforms } = readSheet(bytes); // pngjs decodes; the
48
- // Title and sprite-machine:transforms chunks are read as the app reads them
49
- const glb = sheetToGlb(bytes, { voxelsPerMeter: 10 }); // the three calls in one
46
+ // pngjs decodes the pixels. The Title, sprite-machine:transforms and
47
+ // sprite-machine:layers chunks are read as the app reads them.
48
+ const { image, name, transforms, layers } = readSheet(bytes);
49
+ const glb = sheetToGlb(bytes, { voxelsPerMeter: 10 }); // readSheet, buildModel, modelToGlb
50
50
  ```
51
51
 
52
- `sprite-machine/node` is the one entry with a decoder; the root entry takes
53
- pixels and depends on nothing but three, so a browser bundle never sees
54
- `pngjs`. In a browser, decode with `createImageBitmap` and a canvas and
55
- hand `buildModel` the `ImageData`.
52
+ `readSheet` also returns `chunks`, every text chunk verbatim. `layers` is the
53
+ layer names, or null. `sheetToGlb` builds as many layers as the chunk names
54
+ when that count divides the sheet's height into whole blocks, and one layer
55
+ otherwise. It takes an optional `name`, which overrides the Title chunk. With
56
+ neither, the name is `'sprite'`.
57
+
58
+ `sprite-machine/node` is the only entry with a PNG decoder. The root entry
59
+ takes pixels and depends only on three, so a browser bundle never includes
60
+ `pngjs`. In a browser, decode with `createImageBitmap` and a canvas, and pass
61
+ the `ImageData` to `buildModel`.
56
62
 
57
63
  ### The CLI
58
64
 
@@ -60,59 +66,83 @@ hand `buildModel` the `ImageData`.
60
66
  npx sprite-machine build sprites/*.png --out models/ [--voxels-per-meter 10] [--unlit]
61
67
  ```
62
68
 
63
- One `<name>.glb` per sheet the Title chunk's name, else the file's — and a
64
- line per file. The first failure exits non-zero.
69
+ Writes one `<name>.glb` per sheet, named from the Title chunk or else the
70
+ file name, and prints a line per file. The first failure exits non-zero.
65
71
 
66
72
  ## The sheet
67
73
 
68
- One PNG, six tiles in a fixed layout, empty tiles allowed (a face with no
69
- view of its own is mirror-filled from its opposite):
74
+ One PNG with six tiles in a fixed layout. Empty tiles are allowed: a face
75
+ with no view of its own is mirror-filled from its opposite.
70
76
 
71
77
  ```
72
78
  LEFT FRONT TOP
73
79
  RIGHT BACK BOTTOM
74
80
  ```
75
81
 
76
- The tile size derives from the image (a 120×80 sheet is 40×40 tiles). Use
77
- **square tiles**: a tile is a literal slice of the voxel lattice, so a
78
- pixel's position inside its tile is its position in the object, and the
79
- faces must be **registered** — a FRONT pixel is solid only where the SIDE
80
- covers its row and the TOP covers its column. Sprites are hard pixel art:
81
- every texel fully opaque or fully transparent.
82
-
83
- World: `+x` right, `+y` up, `+z` toward the front. Draw FRONT and BACK
84
- head-on and upright; RIGHT and LEFT as the sides with the front pointing
85
- right and left; TOP and BOTTOM as plan views with the front at the top edge.
86
- Per-tile `rot` / `flipX` / `flipY` transforms exist for sheets that don't
82
+ The tile size derives from the image (a 120×80 sheet has 40×40 tiles). Use
83
+ square tiles. A tile is a slice of the voxel lattice, so a pixel's position
84
+ in its tile is its position in the object. The faces must be **registered**:
85
+ a FRONT pixel is solid only where the SIDE covers its row and the TOP covers
86
+ its column. Every texel must be fully opaque or fully transparent.
87
+
88
+ World axes: `+x` right, `+y` up, `+z` toward the front. Draw FRONT and BACK
89
+ head-on and upright, RIGHT and LEFT as side views with the front pointing
90
+ right and left, and TOP and BOTTOM as plan views with the front at the top
91
+ edge. Per-tile `rot` / `flipX` / `flipY` transforms handle sheets that don't
87
92
  follow the convention.
88
93
 
89
- The model's origin is the **lattice floor's centre**, Y up, winding CCW.
90
- `voxelsPerMeter` is the reader's scale: at 10, a 40-voxel car is 4 m long.
94
+ The model's origin is the centre of the lattice floor, Y up, with CCW
95
+ winding. `voxelsPerMeter` sets the glb's scale: at 10, a 40-voxel car is
96
+ 4 m long.
97
+
98
+ ### Layers
99
+
100
+ A sheet can stack several blocks of the six tiles, one under another at one
101
+ tile size: `3t × 2tN` for `N` layers, the first on top. Each layer carves its
102
+ own hull and the model is their union, so it can hold concave shapes a single
103
+ hull fills in, such as a body on separate wheels. Where two layers hold the
104
+ same voxel, the later layer colours its faces. A layer with views on one plane
105
+ only is one voxel deep, on the face of the lattice those views look at: a
106
+ front-only layer lies on the front plane.
107
+
108
+ ```js
109
+ const model = buildModel(sheet, { layers: 2 });
110
+ ```
111
+
112
+ Without `layers`, a sheet is one block. A document PNG names its blocks in a
113
+ `sprite-machine:layers` text chunk, `{"layers":[{"name":"Body"},{"name":"Wheels"}]}`,
114
+ whose length is the layer count. The parts are exported too: `sliceLayers`,
115
+ `buildLayeredVoxels`, `unionVoxels`, `LAYERS_CHUNK`, `layersChunk`,
116
+ `parseLayersChunk`, `layerCount` and `LAYER_MAX` (8, the app's cap, which the
117
+ chunk parser also holds to).
91
118
 
92
119
  ## The technique: multi-view visual-hull voxelization
93
120
 
94
- 1. **Ingest** each tile at native size into occupancy and packed-RGB
95
- arrays. No auto-crop: registration is the whole point.
96
- 2. **Reconcile dims** one integer resolution per axis from the tile size
121
+ 1. **Ingest**: each tile at native size into occupancy and packed-RGB
122
+ arrays. No auto-crop, so the views stay registered.
123
+ 2. **Reconcile dims**: one integer resolution per axis from the tile size
97
124
  (`front → W×H`, `side → D×H`, `top → W×D`), views placed at identity.
98
- 3. **Carve** a voxel is solid iff it is inside every provided view's
99
- silhouette: a boolean AND of extruded masks.
100
- 4. **Surface** keep the voxels with an exposed face, six-bit masks.
101
- 5. **Colour** each exposed face takes the colour of the view that sees it
125
+ 3. **Carve**: a voxel is solid iff it is inside every provided view's
126
+ silhouette (a boolean AND of extruded masks).
127
+ 4. **Surface**: keep the voxels with an exposed face, as six-bit masks.
128
+ 5. **Colour**: each exposed face takes the colour of the view that sees it
102
129
  first along its axis (depth-aware first hit), snapped to the sprite's
103
- palette; faces no view sees fall through mirrored opposite neighbour
104
- average dominant body colour.
105
- 6. **Mesh** exposed faces merge on occupancy alone into coplanar regions
106
- (holes included), triangulated by earcut; 45° **wedges** fill every
107
- concave unit-step notch whose two faces share a material, one quad per
108
- slope block, the gable caps folded into the walls; a lattice-exact
109
- T-junction repair keeps it watertight. The colour is the **skin**: a
110
- chart per multi-colour region, a swatch per colour, packed
111
- deterministically onto a power-of-two sheet and sampled nearest.
112
-
113
- The wedge gate is strict and local: paint a riser and its tread the same
114
- colour and the corner ramps; paint them differently and it stays a crisp
115
- step. That is the author's control over every slope.
130
+ palette. Faces no view sees fall back to the mirrored opposite, then the
131
+ neighbour average, then the dominant body colour.
132
+ 6. **Mesh**: exposed faces merge on occupancy alone into coplanar regions (holes
133
+ included), triangulated by earcut. 45° wedges fill every concave
134
+ unit-step notch whose two faces share a material, one quad per slope
135
+ block, with the gable caps folded into the walls. A lattice-exact
136
+ T-junction repair keeps the mesh watertight. The colour is a skin: a
137
+ chart per multi-colour region and a swatch per colour, packed
138
+ deterministically onto a power-of-two texture and sampled nearest.
139
+
140
+ With layers, steps 1 to 5 run once per layer. The union ORs the solids in the
141
+ largest lattice, extracts its surface again, and gives each exposed face the
142
+ colour the last layer holding its voxel gave it. Step 6 meshes the union.
143
+
144
+ The wedge gate is local. A riser and its tread painted the same colour get a
145
+ ramp at the corner. Painted differently, they keep the step.
116
146
 
117
147
  ## License
118
148
 
@@ -1,14 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // ---------------------------------------------------------------------------
3
- // The CLI: sprite sheets in, glb files out.
4
- //
5
- // sprite-machine build <sheet.png>... --out <dir> [--voxels-per-meter N] [--unlit]
6
- //
7
- // One <name>.glb per sheet under --out — the Title chunk's name, else the
8
- // file's — and a line per file naming it, its triangles and its bytes. The
9
- // first failure exits non-zero. A thin shell over `sprite-machine/node`:
10
- // nothing here builds anything.
11
- // ---------------------------------------------------------------------------
2
+ // CLI over sprite-machine/node: writes one glb per sprite sheet (see USAGE)
3
+ // and prints each file's path, name and size. Exits 1 on the first failure.
12
4
 
13
5
  import { parseArgs } from 'node:util';
14
6
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sprite-machine",
3
- "version": "0.1.0",
4
- "description": "Turn a 3×2 sheet of pixel-art face sprites into a low-poly, textured three.js mesh or a glTF binary — the engine behind Sprite Machine.",
3
+ "version": "0.1.1",
4
+ "description": "Turn a sheet of pixel-art face sprites into a low-poly, textured three.js mesh or a glTF binary — the engine behind Sprite Machine.",
5
5
  "license": "MIT",
6
6
  "author": "Adam Portilla",
7
7
  "type": "module",