sprite-machine 0.1.0 → 0.2.0

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,83 @@
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
3
+ Turns a 3×2 sheet of pixel-art face sprites (left / front / top over right /
4
+ back / bottom) into a low-poly, textured model: indexed triangle buffers and
5
+ a skin bitmap, or a glTF 2.0 binary. One pixel is one voxel. 45° wedges
6
+ smooth every same-colour staircase, and the colour comes from a
8
7
  nearest-sampled skin texture.
9
8
 
10
9
  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.
10
+ the desktop app that draws these sheets. It runs at a build step, at a
11
+ server's startup, or in the browser.
14
12
 
15
13
  ```bash
16
- npm install sprite-machine three
14
+ npm install sprite-machine
17
15
  ```
18
16
 
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+.
17
+ The root entry depends on `earcut` alone. 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 });
29
- // → { 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)
24
+ // sheet: {width, height, data}, an ImageData or the same shape
25
+ const model = buildModel(sheet, { transforms, layers });
26
+ // → { geometry, skin, color, triangles, dims, warnings, unitsPerVoxel: 1 }
27
+ // geometry is { position, normal, uv, index, bounds }: Float32 xyz per
28
+ // vertex at one unit per voxel, centered on X and Z, unit normals, uv
29
+ // pairs in [0, 1], a Uint32 CCW triangle index and the bounds. skin is
30
+ // the texture, { width, height, data }, sRGB RGBA with row 0 at v = 0.
31
+ // transforms is the per-view reorientation (optional). layers is the
32
+ // sheet's block count (optional, 1 by default; see Layers).
32
33
 
33
34
  const glb = modelToGlb(model, { name: 'car', voxelsPerMeter: 10 });
34
35
  // → Uint8Array: one node, one mesh, one primitive, the skin embedded as
35
- // a PNG behind a NEAREST sampler; `unlit: true` for KHR_materials_unlit
36
+ // a PNG behind a NEAREST sampler. unlit: true adds KHR_materials_unlit.
36
37
  ```
37
38
 
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.
39
+ Both functions are synchronous and pure. `buildModel` throws on an invalid
40
+ sheet or a sheet with no painted view in any layer. To build off the main
41
+ thread, call them from a worker or a child process.
42
+
43
+ ### In three.js
44
+
45
+ Two routes. `sprite-machine/three` turns the model into a `THREE.Mesh`, with
46
+ `three` installed beside the engine:
47
+
48
+ ```js
49
+ import { toMesh, toGeometry, skinTexture } from 'sprite-machine/three';
50
+
51
+ const mesh = toMesh(model); // a flat-shaded MeshStandardMaterial over the skin
52
+ const geo = toGeometry(model.geometry); // a BufferGeometry with its bounds
53
+ const map = skinTexture(model.skin); // a DataTexture, nearest, sRGB, flipY false
54
+ ```
55
+
56
+ Or write the glb and load it with `GLTFLoader`, which reads the sampler, the
57
+ sRGB texture, the unlit extension and the node name. Every other engine loads
58
+ the glb. `three` is an optional peer (0.152 or later), and the root entry
59
+ never imports it.
41
60
 
42
61
  ### In Node: a document PNG in
43
62
 
44
63
  ```js
45
64
  import { readSheet, sheetToGlb } from 'sprite-machine/node';
46
65
 
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
66
+ // pngjs decodes the pixels. The Title, sprite-machine:transforms and
67
+ // sprite-machine:layers chunks are read as the app reads them.
68
+ const { image, name, transforms, layers } = readSheet(bytes);
69
+ const glb = sheetToGlb(bytes, { voxelsPerMeter: 10 }); // readSheet, buildModel, modelToGlb
50
70
  ```
51
71
 
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`.
72
+ `readSheet` also returns `chunks`, every text chunk verbatim. `layers` is the
73
+ layer names, or null. `sheetToGlb` builds as many layers as the chunk names
74
+ when that count divides the sheet's height into whole blocks, and one layer
75
+ otherwise. It takes an optional `name`, which overrides the Title chunk. With
76
+ neither, the name is `'sprite'`.
77
+
78
+ `sprite-machine/node` is the only entry with a PNG decoder. The root entry
79
+ takes pixels, so a browser bundle never includes `pngjs`. In a browser, decode with `createImageBitmap` and a canvas, and pass
80
+ the `ImageData` to `buildModel`.
56
81
 
57
82
  ### The CLI
58
83
 
@@ -60,59 +85,83 @@ hand `buildModel` the `ImageData`.
60
85
  npx sprite-machine build sprites/*.png --out models/ [--voxels-per-meter 10] [--unlit]
61
86
  ```
62
87
 
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.
88
+ Writes one `<name>.glb` per sheet, named from the Title chunk or else the
89
+ file name, and prints a line per file. The first failure exits non-zero.
65
90
 
66
91
  ## The sheet
67
92
 
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):
93
+ One PNG with six tiles in a fixed layout. Empty tiles are allowed: a face
94
+ with no view of its own is mirror-filled from its opposite.
70
95
 
71
96
  ```
72
97
  LEFT FRONT TOP
73
98
  RIGHT BACK BOTTOM
74
99
  ```
75
100
 
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
101
+ The tile size derives from the image (a 120×80 sheet has 40×40 tiles). Use
102
+ square tiles. A tile is a slice of the voxel lattice, so a pixel's position
103
+ in its tile is its position in the object. The faces must be **registered**:
104
+ a FRONT pixel is solid only where the SIDE covers its row and the TOP covers
105
+ its column. Every texel must be fully opaque or fully transparent.
106
+
107
+ World axes: `+x` right, `+y` up, `+z` toward the front. Draw FRONT and BACK
108
+ head-on and upright, RIGHT and LEFT as side views with the front pointing
109
+ right and left, and TOP and BOTTOM as plan views with the front at the top
110
+ edge. Per-tile `rot` / `flipX` / `flipY` transforms handle sheets that don't
87
111
  follow the convention.
88
112
 
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.
113
+ The model's origin is the centre of the lattice floor, Y up, with CCW
114
+ winding. `voxelsPerMeter` sets the glb's scale: at 10, a 40-voxel car is
115
+ 4 m long.
116
+
117
+ ### Layers
118
+
119
+ A sheet can stack several blocks of the six tiles, one under another at one
120
+ tile size: `3t × 2tN` for `N` layers, the first on top. Each layer carves its
121
+ own hull and the model is their union, so it can hold concave shapes a single
122
+ hull fills in, such as a body on separate wheels. Where two layers hold the
123
+ same voxel, the later layer colours its faces. A layer with views on one plane
124
+ only is one voxel deep, on the face of the lattice those views look at: a
125
+ front-only layer lies on the front plane.
126
+
127
+ ```js
128
+ const model = buildModel(sheet, { layers: 2 });
129
+ ```
130
+
131
+ Without `layers`, a sheet is one block. A document PNG names its blocks in a
132
+ `sprite-machine:layers` text chunk, `{"layers":[{"name":"Body"},{"name":"Wheels"}]}`,
133
+ whose length is the layer count. The parts are exported too: `sliceLayers`,
134
+ `buildLayeredVoxels`, `unionVoxels`, `LAYERS_CHUNK`, `layersChunk`,
135
+ `parseLayersChunk`, `layerCount` and `LAYER_MAX` (8, the app's cap, which the
136
+ chunk parser also holds to).
91
137
 
92
138
  ## The technique: multi-view visual-hull voxelization
93
139
 
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
140
+ 1. **Ingest**: each tile at native size into occupancy and packed-RGB
141
+ arrays. No auto-crop, so the views stay registered.
142
+ 2. **Reconcile dims**: one integer resolution per axis from the tile size
97
143
  (`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
144
+ 3. **Carve**: a voxel is solid iff it is inside every provided view's
145
+ silhouette (a boolean AND of extruded masks).
146
+ 4. **Surface**: keep the voxels with an exposed face, as six-bit masks.
147
+ 5. **Colour**: each exposed face takes the colour of the view that sees it
102
148
  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.
149
+ palette. Faces no view sees fall back to the mirrored opposite, then the
150
+ neighbour average, then the dominant body colour.
151
+ 6. **Mesh**: exposed faces merge on occupancy alone into coplanar regions (holes
152
+ included), triangulated by earcut. 45° wedges fill every concave
153
+ unit-step notch whose two faces share a material, one quad per slope
154
+ block, with the gable caps folded into the walls. A lattice-exact
155
+ T-junction repair keeps the mesh watertight. The colour is a skin: a
156
+ chart per multi-colour region and a swatch per colour, packed
157
+ deterministically onto a power-of-two texture and sampled nearest.
158
+
159
+ With layers, steps 1 to 5 run once per layer. The union ORs the solids in the
160
+ largest lattice, extracts its surface again, and gives each exposed face the
161
+ colour the last layer holding its voxel gave it. Step 6 meshes the union.
162
+
163
+ The wedge gate is local. A riser and its tread painted the same colour get a
164
+ ramp at the corner. Painted differently, they keep the step.
116
165
 
117
166
  ## License
118
167
 
@@ -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.2.0",
4
+ "description": "Turn a sheet of pixel-art face sprites into a low-poly, textured glTF binary — the engine behind Sprite Machine.",
5
5
  "license": "MIT",
6
6
  "author": "Adam Portilla",
7
7
  "type": "module",
@@ -28,7 +28,8 @@
28
28
  "sideEffects": false,
29
29
  "exports": {
30
30
  ".": "./src/index.js",
31
- "./node": "./src/node.js"
31
+ "./node": "./src/node.js",
32
+ "./three": "./src/three.js"
32
33
  },
33
34
  "bin": {
34
35
  "sprite-machine": "./bin/sprite-machine.mjs"
@@ -44,9 +45,15 @@
44
45
  "typecheck": "tsc -p tsconfig.json"
45
46
  },
46
47
  "peerDependencies": {
47
- "three": "^0.185.0"
48
+ "three": ">=0.152.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "three": {
52
+ "optional": true
53
+ }
48
54
  },
49
55
  "dependencies": {
56
+ "earcut": "3.0.2",
50
57
  "pngjs": "^7.0.0"
51
58
  }
52
59
  }