sprite-machine 0.1.1 → 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 +31 -12
- package/package.json +11 -4
- package/src/diag.js +9 -5
- package/src/index.js +2 -2
- package/src/model.js +28 -23
- package/src/three.js +60 -0
- package/src/wedge-mesh.js +63 -34
- package/src/weld.js +56 -0
- package/src/mesh-util.js +0 -54
package/README.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# sprite-machine
|
|
2
2
|
|
|
3
3
|
Turns a 3×2 sheet of pixel-art face sprites (left / front / top over right /
|
|
4
|
-
back / bottom) into a low-poly, textured
|
|
5
|
-
One pixel is one voxel. 45° wedges
|
|
6
|
-
the colour comes from a
|
|
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
|
|
7
|
+
nearest-sampled skin texture.
|
|
7
8
|
|
|
8
9
|
This is the engine behind [Sprite Machine](https://aportilla.github.io/sprite-machine/),
|
|
9
10
|
the desktop app that draws these sheets. It runs at a build step, at a
|
|
10
11
|
server's startup, or in the browser.
|
|
11
12
|
|
|
12
13
|
```bash
|
|
13
|
-
npm install sprite-machine
|
|
14
|
+
npm install sprite-machine
|
|
14
15
|
```
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
three's geometry utilities. Requires Node 20.19+ or 22.12+.
|
|
17
|
+
The root entry depends on `earcut` alone. Requires Node 20.19+ or 22.12+.
|
|
18
18
|
|
|
19
19
|
## The API
|
|
20
20
|
|
|
@@ -23,8 +23,11 @@ import { buildModel, modelToGlb } from 'sprite-machine';
|
|
|
23
23
|
|
|
24
24
|
// sheet: {width, height, data}, an ImageData or the same shape
|
|
25
25
|
const model = buildModel(sheet, { transforms, layers });
|
|
26
|
-
// → {
|
|
27
|
-
//
|
|
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.
|
|
28
31
|
// transforms is the per-view reorientation (optional). layers is the
|
|
29
32
|
// sheet's block count (optional, 1 by default; see Layers).
|
|
30
33
|
|
|
@@ -34,10 +37,27 @@ const glb = modelToGlb(model, { name: 'car', voxelsPerMeter: 10 });
|
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
Both functions are synchronous and pure. `buildModel` throws on an invalid
|
|
37
|
-
sheet or a sheet with no painted view in any layer.
|
|
38
|
-
`model.mesh` directly, and a Node process writes the glb. To build off the main
|
|
40
|
+
sheet or a sheet with no painted view in any layer. To build off the main
|
|
39
41
|
thread, call them from a worker or a child process.
|
|
40
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.
|
|
60
|
+
|
|
41
61
|
### In Node: a document PNG in
|
|
42
62
|
|
|
43
63
|
```js
|
|
@@ -56,8 +76,7 @@ otherwise. It takes an optional `name`, which overrides the Title chunk. With
|
|
|
56
76
|
neither, the name is `'sprite'`.
|
|
57
77
|
|
|
58
78
|
`sprite-machine/node` is the only entry with a PNG decoder. The root entry
|
|
59
|
-
takes pixels
|
|
60
|
-
`pngjs`. In a browser, decode with `createImageBitmap` and a canvas, and pass
|
|
79
|
+
takes pixels, so a browser bundle never includes `pngjs`. In a browser, decode with `createImageBitmap` and a canvas, and pass
|
|
61
80
|
the `ImageData` to `buildModel`.
|
|
62
81
|
|
|
63
82
|
### The CLI
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprite-machine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Turn a sheet of pixel-art face sprites into a low-poly, textured
|
|
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
|
}
|
package/src/diag.js
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
// edges used an odd number of times are boundaries. Also returns a histogram of
|
|
4
4
|
// triangle normals by axis.
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @param {{position:ArrayLike<number>, normal:ArrayLike<number>,
|
|
8
|
+
* index?:ArrayLike<number>|null}} geometry
|
|
9
|
+
* flat triangle buffers, non-indexed when index is null
|
|
10
|
+
*/
|
|
11
|
+
export function computeDiag(geometry) {
|
|
12
|
+
const pos = geometry.position;
|
|
13
|
+
const nrm = geometry.normal;
|
|
14
|
+
const idx = geometry.index ?? null;
|
|
11
15
|
const triCount = idx ? idx.length / 3 : pos.length / 9;
|
|
12
16
|
const key = (i) => {
|
|
13
17
|
const x = Math.round(pos[i * 3] * 1e4);
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Public API of the sprite-machine package. Exports are listed by name: with
|
|
2
2
|
// export *, a name exported by two modules is silently dropped. The Node adapter
|
|
3
|
-
// is sprite-machine/node.
|
|
3
|
+
// is sprite-machine/node, the three adapter sprite-machine/three.
|
|
4
4
|
|
|
5
5
|
export {
|
|
6
6
|
packRGBA,
|
|
@@ -64,7 +64,7 @@ export { LAYERS_CHUNK, layersChunk, parseLayersChunk, layerCount } from './layer
|
|
|
64
64
|
export { traceRegions, planeKey, faceRegions } from './regions.js';
|
|
65
65
|
export { eliminateTJunctions } from './t-junction.js';
|
|
66
66
|
export { bakeSkin, uvOfLattice, swatchUV } from './skin.js';
|
|
67
|
-
export {
|
|
67
|
+
export { weldVertices } from './weld.js';
|
|
68
68
|
export { wedgeMesh } from './wedge-mesh.js';
|
|
69
69
|
export {
|
|
70
70
|
PNG_SIGNATURE,
|
package/src/model.js
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
// Headless entry: a sheet's pixels to a model, and a model to a glb.
|
|
2
2
|
//
|
|
3
3
|
// buildModel runs slice, ingest, carve, colorize, the layer union and the
|
|
4
|
-
// wedge mesher, and returns the
|
|
5
|
-
// lattice coordinate. modelToGlb writes the same glb as the app's export,
|
|
6
|
-
// the skin encoded from bytes. Both are synchronous.
|
|
4
|
+
// wedge mesher, and returns the record at one unit per voxel, so a position is
|
|
5
|
+
// a lattice coordinate. modelToGlb writes the same glb as the app's export,
|
|
6
|
+
// with the skin encoded from bytes. Both are synchronous.
|
|
7
7
|
|
|
8
8
|
import { sliceLayers, validateSheet } from './atlas.js';
|
|
9
9
|
import { buildLayeredVoxels } from './pipeline.js';
|
|
10
10
|
import { wedgeMesh } from './wedge-mesh.js';
|
|
11
11
|
import { encodePng } from './png-encode.js';
|
|
12
12
|
import { glbFromModel } from './gltf.js';
|
|
13
|
+
import { unpackRGBA } from './ingest.js';
|
|
13
14
|
import { VIEW_NAMES } from './views.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
* A built model: the
|
|
17
|
+
* A built model: the mesher's record, the lattice dims and the units per voxel
|
|
17
18
|
* (1 from buildModel).
|
|
18
|
-
* @typedef {{
|
|
19
|
-
* mesh: import('three').Mesh,
|
|
19
|
+
* @typedef {import('./wedge-mesh.js').Built & {
|
|
20
20
|
* dims: {nx:number, ny:number, nz:number},
|
|
21
21
|
* unitsPerVoxel: number,
|
|
22
|
-
* triangles: number,
|
|
23
22
|
* warnings: string[],
|
|
24
23
|
* }} Model
|
|
25
24
|
*/
|
|
@@ -34,7 +33,7 @@ import { VIEW_NAMES } from './views.js';
|
|
|
34
33
|
* `transforms`: per-view reorientation, as stored in the
|
|
35
34
|
* `sprite-machine:transforms` chunk, applied in every layer; `layers`: the
|
|
36
35
|
* sheet's block count
|
|
37
|
-
* @returns {Model} the
|
|
36
|
+
* @returns {Model} the model at one unit per voxel
|
|
38
37
|
* @throws on an invalid sheet, or one with no painted view in any layer
|
|
39
38
|
*/
|
|
40
39
|
export function buildModel(sheet, { transforms = {}, layers = 1 } = {}) {
|
|
@@ -51,20 +50,29 @@ export function buildModel(sheet, { transforms = {}, layers = 1 } = {}) {
|
|
|
51
50
|
throw new Error('buildModel: the sheet has no painted view.');
|
|
52
51
|
}
|
|
53
52
|
const { nx, ny, nz } = result.dims;
|
|
54
|
-
const mesh = wedgeMesh(result, { worldSize: Math.max(nx, ny, nz) });
|
|
55
53
|
return {
|
|
56
|
-
|
|
54
|
+
...wedgeMesh(result, { worldSize: Math.max(nx, ny, nz) }),
|
|
57
55
|
dims: result.dims,
|
|
58
56
|
unitsPerVoxel: 1,
|
|
59
|
-
triangles: Number(mesh.userData.triangles) || 0,
|
|
60
57
|
warnings: [...sliced.warnings, ...(result.warnings || [])],
|
|
61
58
|
};
|
|
62
59
|
}
|
|
63
60
|
|
|
61
|
+
/** The sRGB transfer to linear, one channel in 0..1. */
|
|
62
|
+
const srgbToLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
63
|
+
|
|
64
|
+
/** A packed sRGB color as linear RGB in 0..1. */
|
|
65
|
+
function linearRGB(packed) {
|
|
66
|
+
const { r, g, b } = unpackRGBA(packed);
|
|
67
|
+
return [r / 255, g / 255, b / 255].map(srgbToLinear);
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
/**
|
|
65
71
|
* The model as a glb, with its skin embedded behind a nearest sampler, or its
|
|
66
|
-
* flat color when
|
|
67
|
-
* @param {{
|
|
72
|
+
* flat color, made linear, when it has no skin.
|
|
73
|
+
* @param {{geometry: import('./wedge-mesh.js').Geometry,
|
|
74
|
+
* skin: import('./skin.js').Skin|null, color?: number|null,
|
|
75
|
+
* dims: {nx:number, ny:number, nz:number}, unitsPerVoxel?: number}} model
|
|
68
76
|
* @param {{name: string, voxelsPerMeter?: number, unlit?: boolean, generator?: string}} opts
|
|
69
77
|
* `voxelsPerMeter` sets the scale (at 10, a 40-voxel car is 4 m long);
|
|
70
78
|
* `unlit` adds KHR_materials_unlit; `generator` is written to the asset
|
|
@@ -74,20 +82,17 @@ export function modelToGlb(
|
|
|
74
82
|
model,
|
|
75
83
|
{ name, voxelsPerMeter = 10, unlit = false, generator }
|
|
76
84
|
) {
|
|
77
|
-
const {
|
|
85
|
+
const { geometry, skin, dims } = model;
|
|
78
86
|
const unitsPerVoxel = model.unitsPerVoxel ?? 1;
|
|
79
|
-
const geo = mesh.geometry;
|
|
80
|
-
const material = /** @type {import('three').MeshStandardMaterial} */ (mesh.material);
|
|
81
|
-
const map = material.map;
|
|
82
87
|
return glbFromModel({
|
|
83
88
|
name,
|
|
84
|
-
position:
|
|
85
|
-
normal:
|
|
86
|
-
uv:
|
|
87
|
-
index:
|
|
89
|
+
position: geometry.position,
|
|
90
|
+
normal: geometry.normal,
|
|
91
|
+
uv: geometry.uv,
|
|
92
|
+
index: geometry.index,
|
|
88
93
|
scale: 1 / (unitsPerVoxel * voxelsPerMeter),
|
|
89
|
-
image:
|
|
90
|
-
color:
|
|
94
|
+
image: skin ? { bytes: encodePng(skin) } : null,
|
|
95
|
+
color: skin || model.color == null ? null : linearRGB(model.color),
|
|
91
96
|
unlit,
|
|
92
97
|
generator,
|
|
93
98
|
extras: { 'sprite-machine': { voxelsPerMeter, dims: { ...dims } } },
|
package/src/three.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Three adapter (sprite-machine/three): the mesher's record as three objects.
|
|
2
|
+
// The only entry that imports three, an optional peer.
|
|
3
|
+
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
import { unpackRGBA } from './ingest.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The skin as a DataTexture. The class defaults match the bake: nearest
|
|
9
|
+
* filtering, no mipmaps, and flipY false so texel row 0 is v = 0. The bytes
|
|
10
|
+
* are sRGB.
|
|
11
|
+
* @param {import('./skin.js').Skin} skin
|
|
12
|
+
* @returns {THREE.DataTexture}
|
|
13
|
+
*/
|
|
14
|
+
export function skinTexture(skin) {
|
|
15
|
+
const tex = new THREE.DataTexture(skin.data, skin.width, skin.height, THREE.RGBAFormat);
|
|
16
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
17
|
+
tex.needsUpdate = true;
|
|
18
|
+
return tex;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The geometry as an indexed BufferGeometry with its bounds computed. The
|
|
23
|
+
* arrays are copied.
|
|
24
|
+
* @param {import('./wedge-mesh.js').Geometry} geometry
|
|
25
|
+
* @returns {THREE.BufferGeometry}
|
|
26
|
+
*/
|
|
27
|
+
export function toGeometry(geometry) {
|
|
28
|
+
const geo = new THREE.BufferGeometry();
|
|
29
|
+
geo.setAttribute('position', new THREE.Float32BufferAttribute(geometry.position, 3));
|
|
30
|
+
geo.setAttribute('normal', new THREE.Float32BufferAttribute(geometry.normal, 3));
|
|
31
|
+
geo.setAttribute('uv', new THREE.Float32BufferAttribute(geometry.uv, 2));
|
|
32
|
+
geo.setIndex(new THREE.Uint32BufferAttribute(geometry.index, 1));
|
|
33
|
+
geo.computeBoundingBox();
|
|
34
|
+
geo.computeBoundingSphere();
|
|
35
|
+
return geo;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The model as a flat-shaded Mesh with shadows on. The material takes the
|
|
40
|
+
* skin as `map`, or with no skin the packed sRGB `color`.
|
|
41
|
+
* @param {import('./wedge-mesh.js').Built} model
|
|
42
|
+
* @returns {THREE.Mesh}
|
|
43
|
+
*/
|
|
44
|
+
export function toMesh(model) {
|
|
45
|
+
const map = model.skin ? skinTexture(model.skin) : null;
|
|
46
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
47
|
+
flatShading: true,
|
|
48
|
+
metalness: 0,
|
|
49
|
+
roughness: 1,
|
|
50
|
+
...(map ? { map } : {}),
|
|
51
|
+
});
|
|
52
|
+
if (!map && model.color != null) {
|
|
53
|
+
const { r, g, b } = unpackRGBA(model.color);
|
|
54
|
+
mat.color.setRGB(r / 255, g / 255, b / 255, THREE.SRGBColorSpace);
|
|
55
|
+
}
|
|
56
|
+
const mesh = new THREE.Mesh(toGeometry(model.geometry), mat);
|
|
57
|
+
mesh.castShadow = true;
|
|
58
|
+
mesh.receiveShadow = true;
|
|
59
|
+
return mesh;
|
|
60
|
+
}
|
package/src/wedge-mesh.js
CHANGED
|
@@ -12,22 +12,21 @@
|
|
|
12
12
|
// - Slopes: the wedge cells of one 45° plane form a grid, greedy-merged by
|
|
13
13
|
// color into one quad per block.
|
|
14
14
|
// - Base faces: coplanar regions (regions.js) of exposed faces and cap halves,
|
|
15
|
-
// triangulated with earcut
|
|
15
|
+
// triangulated with earcut.
|
|
16
16
|
// Color comes from the skin (skin.js), so regions merge on occupancy alone.
|
|
17
17
|
// UVs are read from lattice positions after the T-junction repair.
|
|
18
18
|
//
|
|
19
19
|
// Not handled: convex staircases still step, and 3D corners where two ridges
|
|
20
20
|
// meet become a step.
|
|
21
21
|
|
|
22
|
-
import
|
|
23
|
-
import { mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js';
|
|
22
|
+
import earcut from 'earcut';
|
|
24
23
|
import { voxIndex } from './carve.js';
|
|
25
24
|
import { FACE_GEO, pointOf } from './faces.js';
|
|
26
25
|
import { faceRegions, planeKey } from './regions.js';
|
|
27
26
|
import { unpackRGBA } from './ingest.js';
|
|
28
27
|
import { bakeSkin, uvOfLattice, swatchUV } from './skin.js';
|
|
29
28
|
import { eliminateTJunctions } from './t-junction.js';
|
|
30
|
-
import {
|
|
29
|
+
import { weldVertices } from './weld.js';
|
|
31
30
|
import { AXIS_INDEX, FACE_INDEX, faceKeyOf } from './views.js';
|
|
32
31
|
import { DEFAULT_WORLD_SIZE } from './constants.js';
|
|
33
32
|
|
|
@@ -51,6 +50,28 @@ const RIDGES = [
|
|
|
51
50
|
* swatch:number|null}} Tri
|
|
52
51
|
*/
|
|
53
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Indexed triangle buffers: xyz positions centered on X and Z with Y as
|
|
55
|
+
* authored (see carve.js), unit normals, uv pairs in [0, 1] (zero in flat
|
|
56
|
+
* mode), CCW index triples and the positions' bounds.
|
|
57
|
+
* @typedef {{position:Float32Array, normal:Float32Array, uv:Float32Array,
|
|
58
|
+
* index:Uint32Array, bounds:{min:number[], max:number[]}}} Geometry
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The mesher's output: the geometry, the skin or, in flat mode, a packed flat
|
|
63
|
+
* color, and the counts. sprite-machine/three turns it into a THREE.Mesh.
|
|
64
|
+
* @typedef {{geometry:Geometry, skin:import('./skin.js').Skin|null,
|
|
65
|
+
* color:number|null, triangles:number, wedges:number,
|
|
66
|
+
* slopes:number, charts:number}} Built
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {object} result a buildVoxels result
|
|
71
|
+
* @param {{flat?:boolean, worldSize?:number}} [opts] `flat` skips the skin
|
|
72
|
+
* and the wedge gate; `worldSize` is the longest side's length
|
|
73
|
+
* @returns {Built}
|
|
74
|
+
*/
|
|
54
75
|
export function wedgeMesh(result, opts = {}) {
|
|
55
76
|
const { dims, solid, surfaceMask, faceColor } = result;
|
|
56
77
|
const { nx, ny, nz } = dims;
|
|
@@ -284,15 +305,18 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
284
305
|
const paint = chart
|
|
285
306
|
? { chart, region }
|
|
286
307
|
: { swatch: flat ? FLAT_COLOR : /** @type {number} */ (region.uniform) >>> 0 };
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
308
|
+
// earcut takes the loops flat, the holes by their first vertex's index.
|
|
309
|
+
const coords = [];
|
|
310
|
+
const holeStarts = [];
|
|
311
|
+
for (const hole of [region.outer, ...region.holes]) {
|
|
312
|
+
if (hole !== region.outer) holeStarts.push(coords.length / 2);
|
|
313
|
+
for (const [a, b] of hole) coords.push(a, b);
|
|
314
|
+
}
|
|
315
|
+
const faces = earcut(coords, holeStarts);
|
|
292
316
|
const verts = [region.outer, ...region.holes].flat();
|
|
293
317
|
let area = 0;
|
|
294
|
-
for (
|
|
295
|
-
const [p, q, r] = [verts[
|
|
318
|
+
for (let k = 0; k < faces.length; k += 3) {
|
|
319
|
+
const [p, q, r] = [verts[faces[k]], verts[faces[k + 1]], verts[faces[k + 2]]];
|
|
296
320
|
area += Math.abs((q[0] - p[0]) * (r[1] - p[1]) - (r[0] - p[0]) * (q[1] - p[1]));
|
|
297
321
|
pushTri(
|
|
298
322
|
pointOf(region.face, p[0], p[1], region.s),
|
|
@@ -336,28 +360,33 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
336
360
|
}
|
|
337
361
|
}
|
|
338
362
|
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
363
|
+
// 5. Weld by position, normal and uv, so vertices of differently facing
|
|
364
|
+
// triangles or of different charts stay split. Flat shading ignores the
|
|
365
|
+
// normals, but this weld and diag.js depend on them. Then center X and Z and
|
|
366
|
+
// take the bounds.
|
|
367
|
+
const welded = weldVertices(pos, nrm, uv);
|
|
368
|
+
const { position } = welded;
|
|
369
|
+
const dx = (-nx * s) / 2;
|
|
370
|
+
const dz = (-nz * s) / 2;
|
|
371
|
+
const min = [Infinity, Infinity, Infinity];
|
|
372
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
373
|
+
for (let i = 0; i < position.length; i += 3) {
|
|
374
|
+
position[i] += dx;
|
|
375
|
+
position[i + 2] += dz;
|
|
376
|
+
for (let k = 0; k < 3; k++) {
|
|
377
|
+
const v = position[i + k];
|
|
378
|
+
if (v < min[k]) min[k] = v;
|
|
379
|
+
if (v > max[k]) max[k] = v;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
348
382
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
wedges: wedges.length, // cells
|
|
359
|
-
slopes,
|
|
360
|
-
skin: skin ? { width: skin.width, height: skin.height, charts: charted } : null,
|
|
361
|
-
},
|
|
362
|
-
});
|
|
383
|
+
return {
|
|
384
|
+
geometry: { ...welded, bounds: { min, max } },
|
|
385
|
+
skin,
|
|
386
|
+
color: skin ? null : FLAT_COLOR,
|
|
387
|
+
triangles: welded.index.length / 3,
|
|
388
|
+
wedges: wedges.length, // cells
|
|
389
|
+
slopes,
|
|
390
|
+
charts: skin ? skin.charts.reduce((n, c) => n + (c ? 1 : 0), 0) : 0,
|
|
391
|
+
};
|
|
363
392
|
}
|
package/src/weld.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Vertex weld for the mesher. Indexes flat triangle buffers, merging the
|
|
2
|
+
// vertices whose position, normal and uv agree. A component's key is its value
|
|
3
|
+
// scaled by ten thousand, offset by a half and truncated, the rule three's
|
|
4
|
+
// mergeVertices applies at its default tolerance, so the welded buffers match
|
|
5
|
+
// what it produced. Vertices keep first-seen order.
|
|
6
|
+
|
|
7
|
+
const SCALE = 1e4;
|
|
8
|
+
const key = (v) => ~~(v * SCALE + 0.5);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {Float32Array} position xyz per vertex, three vertices per triangle
|
|
12
|
+
* @param {Float32Array} normal xyz per vertex
|
|
13
|
+
* @param {Float32Array} uv uv per vertex
|
|
14
|
+
* @returns {{position:Float32Array, normal:Float32Array, uv:Float32Array, index:Uint32Array}}
|
|
15
|
+
* the unique vertices and a triangle index into them
|
|
16
|
+
*/
|
|
17
|
+
export function weldVertices(position, normal, uv) {
|
|
18
|
+
const count = position.length / 3;
|
|
19
|
+
if (!Number.isInteger(count) || normal.length !== count * 3 || uv.length !== count * 2)
|
|
20
|
+
throw new Error('weldVertices: one normal and one uv per position');
|
|
21
|
+
const outPosition = new Float32Array(position.length);
|
|
22
|
+
const outNormal = new Float32Array(normal.length);
|
|
23
|
+
const outUv = new Float32Array(uv.length);
|
|
24
|
+
const index = new Uint32Array(count);
|
|
25
|
+
const seen = new Map();
|
|
26
|
+
let next = 0;
|
|
27
|
+
for (let i = 0; i < count; i++) {
|
|
28
|
+
const p = i * 3;
|
|
29
|
+
const t = i * 2;
|
|
30
|
+
const k =
|
|
31
|
+
`${key(position[p])},${key(position[p + 1])},${key(position[p + 2])},` +
|
|
32
|
+
`${key(normal[p])},${key(normal[p + 1])},${key(normal[p + 2])},` +
|
|
33
|
+
`${key(uv[t])},${key(uv[t + 1])}`;
|
|
34
|
+
let j = seen.get(k);
|
|
35
|
+
if (j === undefined) {
|
|
36
|
+
j = next++;
|
|
37
|
+
seen.set(k, j);
|
|
38
|
+
const q = j * 3;
|
|
39
|
+
outPosition[q] = position[p];
|
|
40
|
+
outPosition[q + 1] = position[p + 1];
|
|
41
|
+
outPosition[q + 2] = position[p + 2];
|
|
42
|
+
outNormal[q] = normal[p];
|
|
43
|
+
outNormal[q + 1] = normal[p + 1];
|
|
44
|
+
outNormal[q + 2] = normal[p + 2];
|
|
45
|
+
outUv[j * 2] = uv[t];
|
|
46
|
+
outUv[j * 2 + 1] = uv[t + 1];
|
|
47
|
+
}
|
|
48
|
+
index[i] = j;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
position: outPosition.slice(0, next * 3),
|
|
52
|
+
normal: outNormal.slice(0, next * 3),
|
|
53
|
+
uv: outUv.slice(0, next * 2),
|
|
54
|
+
index,
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/mesh-util.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// Mesh helpers for wedge-mesh.js: the skin as a THREE texture, and the final
|
|
2
|
-
// framing and material.
|
|
3
|
-
|
|
4
|
-
import * as THREE from 'three';
|
|
5
|
-
import { unpackRGBA } from './ingest.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The skin as a DataTexture. The class defaults match the bake: nearest
|
|
9
|
-
* filtering, no mipmaps, and flipY false so texel row 0 is v = 0. The bytes
|
|
10
|
-
* are sRGB.
|
|
11
|
-
* @param {import('./skin.js').Skin} skin
|
|
12
|
-
* @returns {THREE.DataTexture}
|
|
13
|
-
*/
|
|
14
|
-
export function skinTexture(skin) {
|
|
15
|
-
const tex = new THREE.DataTexture(skin.data, skin.width, skin.height, THREE.RGBAFormat);
|
|
16
|
-
tex.colorSpace = THREE.SRGBColorSpace;
|
|
17
|
-
tex.needsUpdate = true;
|
|
18
|
-
return tex;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Final assembly: center the geometry on X and Z, leave Y as authored (see
|
|
23
|
-
* carve.js), compute bounds, and wrap it in a flat-shaded material with
|
|
24
|
-
* shadows on. The material takes the skin as `map`, or with no map a packed
|
|
25
|
-
* sRGB `color`.
|
|
26
|
-
* @param {THREE.BufferGeometry} geo
|
|
27
|
-
* @param {{nx:number, nz:number, s:number, map?:THREE.Texture|null, color?:number|null,
|
|
28
|
-
* userData?:Record<string,unknown>}} opts
|
|
29
|
-
* @returns {THREE.Mesh}
|
|
30
|
-
*/
|
|
31
|
-
export function finishVoxelMesh(
|
|
32
|
-
geo,
|
|
33
|
-
{ nx, nz, s, map = null, color = null, userData = {} }
|
|
34
|
-
) {
|
|
35
|
-
geo.translate((-nx * s) / 2, 0, (-nz * s) / 2);
|
|
36
|
-
geo.computeBoundingBox();
|
|
37
|
-
geo.computeBoundingSphere();
|
|
38
|
-
|
|
39
|
-
const mat = new THREE.MeshStandardMaterial({
|
|
40
|
-
flatShading: true,
|
|
41
|
-
metalness: 0,
|
|
42
|
-
roughness: 1,
|
|
43
|
-
...(map ? { map } : {}),
|
|
44
|
-
});
|
|
45
|
-
if (!map && color != null) {
|
|
46
|
-
const { r, g, b } = unpackRGBA(color);
|
|
47
|
-
mat.color.setRGB(r / 255, g / 255, b / 255, THREE.SRGBColorSpace);
|
|
48
|
-
}
|
|
49
|
-
const mesh = new THREE.Mesh(geo, mat);
|
|
50
|
-
mesh.castShadow = true;
|
|
51
|
-
mesh.receiveShadow = true;
|
|
52
|
-
Object.assign(mesh.userData, userData);
|
|
53
|
-
return mesh;
|
|
54
|
-
}
|