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 +113 -64
- package/bin/sprite-machine.mjs +2 -10
- package/package.json +11 -4
- package/src/atlas.js +136 -142
- package/src/carve.js +18 -51
- package/src/colorize.js +14 -29
- package/src/constants.js +7 -12
- package/src/diag.js +13 -20
- package/src/faces.js +9 -24
- package/src/gltf.js +22 -41
- package/src/index.js +8 -11
- package/src/ingest.js +18 -35
- package/src/layers.js +55 -0
- package/src/model.js +55 -59
- package/src/node.js +23 -17
- package/src/pipeline.js +124 -9
- package/src/png-chunks.js +26 -46
- package/src/png-encode.js +12 -22
- package/src/regions.js +31 -48
- package/src/skin.js +37 -66
- package/src/t-junction.js +21 -39
- package/src/three.js +60 -0
- package/src/views.js +47 -86
- package/src/wedge-mesh.js +120 -169
- package/src/weld.js +56 -0
- package/src/mesh-util.js +0 -65
package/README.md
CHANGED
|
@@ -1,58 +1,83 @@
|
|
|
1
1
|
# sprite-machine
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
or a
|
|
6
|
-
|
|
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
|
|
12
|
-
|
|
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
|
|
14
|
+
npm install sprite-machine
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
|
|
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
|
-
//
|
|
28
|
-
const model = buildModel(sheet, { transforms });
|
|
29
|
-
// → {
|
|
30
|
-
//
|
|
31
|
-
//
|
|
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
|
|
36
|
+
// a PNG behind a NEAREST sampler. unlit: true adds KHR_materials_unlit.
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
Both are synchronous and pure.
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
//
|
|
49
|
-
const
|
|
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
|
-
`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
90
|
-
`voxelsPerMeter`
|
|
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
|
|
95
|
-
arrays. No auto-crop
|
|
96
|
-
2. **Reconcile dims
|
|
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
|
|
99
|
-
silhouette
|
|
100
|
-
4. **Surface
|
|
101
|
-
5. **Colour
|
|
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
|
|
104
|
-
average
|
|
105
|
-
6. **Mesh
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
T-junction repair keeps
|
|
110
|
-
chart per multi-colour region
|
|
111
|
-
deterministically onto a power-of-two
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
package/bin/sprite-machine.mjs
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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.
|
|
4
|
-
"description": "Turn a
|
|
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": "
|
|
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
|
}
|