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/src/wedge-mesh.js
CHANGED
|
@@ -1,84 +1,40 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Additive-wedge low-poly engine.
|
|
1
|
+
// Low-poly wedge mesher over the voxel model (solid, surfaceMask, faceColor).
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Wedges: an empty cell with solid neighbors on two adjacent in-plane sides,
|
|
4
|
+
// and empty cells on the other two, is the inner corner of a staircase. It is
|
|
5
|
+
// filled with a triangular prism whose hypotenuse is a 45° slope. The two faces
|
|
6
|
+
// it covers are culled and its open ends get triangular gable caps. A wedge
|
|
7
|
+
// fires only when the two covered faces are the same material, so a color
|
|
8
|
+
// boundary stays a step. Wedges only fill notches, so convex corners stay
|
|
9
|
+
// sharp. One wedge per cell, and the first ridge in RIDGES wins.
|
|
7
10
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// Geometry is emitted per plane:
|
|
12
|
+
// - Slopes: the wedge cells of one 45° plane form a grid, greedy-merged by
|
|
13
|
+
// color into one quad per block.
|
|
14
|
+
// - Base faces: coplanar regions (regions.js) of exposed faces and cap halves,
|
|
15
|
+
// triangulated with earcut.
|
|
16
|
+
// Color comes from the skin (skin.js), so regions merge on occupancy alone.
|
|
17
|
+
// UVs are read from lattice positions after the T-junction repair.
|
|
13
18
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// eat the object (unlike the melted planar-remesh / slab-loft dead ends).
|
|
17
|
-
// - A wedge is ONE MATERIAL by its gate: it fires only where the two covered
|
|
18
|
-
// faces already agree on colour, so a window/body seam stays a sharp 45°
|
|
19
|
-
// edge with no depth guessing (the whole point of the reverted per-color-
|
|
20
|
-
// parts task), and its whole surface points at that colour's swatch.
|
|
21
|
-
// - A lone cube has no concave notch, so it gets NO wedges and stays sharp —
|
|
22
|
-
// the additive rule self-guards convex structural corners.
|
|
23
|
-
//
|
|
24
|
-
// COLOR IS THE SKIN, NOT THE GEOMETRY (Sep 7 2026). The base faces merge on
|
|
25
|
-
// occupancy alone and are painted by a texture (skin.js): a region that
|
|
26
|
-
// crosses a colour boundary carries a chart — a texel per cell — and a
|
|
27
|
-
// one-colour region, like every wedge, points at its colour's swatch; every
|
|
28
|
-
// triangle's UVs are an affine read of its vertices' lattice positions, taken
|
|
29
|
-
// AFTER the T-junction repair, so no UV is ever plumbed through a split.
|
|
30
|
-
// Until then every triangle carried a vertex colour and the merge could only
|
|
31
|
-
// join faces of one colour — a painted wall shattered into a rect per
|
|
32
|
-
// region, each boundary feeding the repair. The Car: 1784 → 900 triangles,
|
|
33
|
-
// the same 236 wedges. Opening the wedge gate to match was measured and
|
|
34
|
-
// rejected (1092: more wedges are more caps and more split faces), so the
|
|
35
|
-
// strict same-material gate stays exactly as it was.
|
|
36
|
-
//
|
|
37
|
-
// THE PLANAR MERGE (Sep 7 2026, the same day, in two steps). The scan fires
|
|
38
|
-
// per notch cell, but the geometry is emitted per PLANE:
|
|
39
|
-
// - A SLOPE is one quad per BLOCK: the wedge cells of one 45° plane (one
|
|
40
|
-
// orientation, one intercept) form a grid — t along the staircase, r along
|
|
41
|
-
// the ridge — that is greedy-merged on one colour. Emitted per cell, a
|
|
42
|
-
// windshield was a grid of unit quads, and every unit edge along a roof's
|
|
43
|
-
// rim pinned a vertex on it that the repair then had to fan the roof
|
|
44
|
-
// around (the Car's 17×10 roof: 20 triangles). Merging the runs along the
|
|
45
|
-
// ridge alone took the Car 900 → 408; across the staircase too it would
|
|
46
|
-
// have gained nothing (392) as long as the gable caps stayed a sawtooth
|
|
47
|
-
// of triangles whose corners split the slope's long diagonal edge back.
|
|
48
|
-
// - So the base faces are coplanar REGIONS (regions.js), not greedy rects:
|
|
49
|
-
// a plane's exposed faces AND the cap half-faces the blocks end on, traced
|
|
50
|
-
// as one polygon with every collinear run merged — the wall beside a
|
|
51
|
-
// windshield has one straight diagonal edge, and the slope beside it is
|
|
52
|
-
// two triangles. Each region is triangulated by earcut (THREE's
|
|
53
|
-
// ShapeUtils, holes included) and painted as one: a chart over its box
|
|
54
|
-
// where it crosses a colour, a swatch where it does not.
|
|
55
|
-
// The T-junction repair stays, now reading 45° edges too: a region's edge
|
|
56
|
-
// and a slope's ridge still meet to different extents where a corner of
|
|
57
|
-
// another plane lands on them, and every vertex is on the lattice.
|
|
58
|
-
//
|
|
59
|
-
// Scope: additive wedges only. Convex staircases (a hood sloping
|
|
60
|
-
// down-and-out) still step, and true 3-D corners where two ridges meet degrade
|
|
61
|
-
// to a step rather than a corner tile. One wedge per cell (first ridge wins).
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
19
|
+
// Not handled: convex staircases still step, and 3D corners where two ridges
|
|
20
|
+
// meet become a step.
|
|
63
21
|
|
|
64
|
-
import
|
|
65
|
-
import { mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js';
|
|
22
|
+
import earcut from 'earcut';
|
|
66
23
|
import { voxIndex } from './carve.js';
|
|
67
24
|
import { FACE_GEO, pointOf } from './faces.js';
|
|
68
25
|
import { faceRegions, planeKey } from './regions.js';
|
|
69
26
|
import { unpackRGBA } from './ingest.js';
|
|
70
27
|
import { bakeSkin, uvOfLattice, swatchUV } from './skin.js';
|
|
71
28
|
import { eliminateTJunctions } from './t-junction.js';
|
|
72
|
-
import {
|
|
29
|
+
import { weldVertices } from './weld.js';
|
|
73
30
|
import { AXIS_INDEX, FACE_INDEX, faceKeyOf } from './views.js';
|
|
74
31
|
import { DEFAULT_WORLD_SIZE } from './constants.js';
|
|
75
32
|
|
|
76
|
-
const AXI = AXIS_INDEX;
|
|
33
|
+
const AXI = AXIS_INDEX;
|
|
77
34
|
const FLAT_COLOR = 0xffcfcfd6;
|
|
78
35
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// common extruded roof/windshield) win the one-wedge-per-cell tie.
|
|
36
|
+
// Ridge axes (R, the axis a prism extends along) with their in-plane axes A and
|
|
37
|
+
// B. Order matters: z comes first, so z-ridges win the one-wedge-per-cell tie.
|
|
82
38
|
const RIDGES = [
|
|
83
39
|
{ R: 'z', A: 'x', B: 'y' },
|
|
84
40
|
{ R: 'x', A: 'z', B: 'y' },
|
|
@@ -86,16 +42,36 @@ const RIDGES = [
|
|
|
86
42
|
];
|
|
87
43
|
|
|
88
44
|
/**
|
|
89
|
-
* A lattice triangle and its paint:
|
|
90
|
-
*
|
|
91
|
-
* one-colour region's, a slope's — carries the packed colour whose swatch it
|
|
92
|
-
* samples. The T-junction repair copies the paint onto every piece.
|
|
45
|
+
* A lattice triangle and its paint: its chart and region when the region is
|
|
46
|
+
* charted, otherwise the packed color of the swatch it samples.
|
|
93
47
|
* @typedef {{a:number[], b:number[], c:number[], normal:number[],
|
|
94
48
|
* chart:import('./skin.js').Chart|null,
|
|
95
49
|
* region:import('./regions.js').Region|null,
|
|
96
50
|
* swatch:number|null}} Tri
|
|
97
51
|
*/
|
|
98
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
|
+
*/
|
|
99
75
|
export function wedgeMesh(result, opts = {}) {
|
|
100
76
|
const { dims, solid, surfaceMask, faceColor } = result;
|
|
101
77
|
const { nx, ny, nz } = dims;
|
|
@@ -103,18 +79,13 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
103
79
|
const worldSize = opts.worldSize ?? DEFAULT_WORLD_SIZE;
|
|
104
80
|
const s = worldSize / Math.max(nx, ny, nz);
|
|
105
81
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
// entries. sameMat therefore compares with a small squared-L2 tolerance;
|
|
111
|
-
// distinct authored materials sit ~180 apart, far above the ~12 slack, so a
|
|
112
|
-
// colour boundary the artist drew still gates crisply.
|
|
113
|
-
const TOL2 = 12 * 12; // ~12 per-channel slack (squared L2): covers farble + AA
|
|
82
|
+
// Privacy browsers perturb getImageData by about ±1 per channel, which can
|
|
83
|
+
// split one material into near-duplicate palette entries. sameMat allows a
|
|
84
|
+
// squared RGB distance up to TOL2, which also absorbs antialiasing.
|
|
85
|
+
const TOL2 = 12 * 12;
|
|
114
86
|
const sameMat = (a, b) => {
|
|
115
87
|
if (a == null || b == null) return false;
|
|
116
|
-
// Compare RGB only
|
|
117
|
-
// the exact fast path and the tolerant path judging identity on the same bits.
|
|
88
|
+
// Compare RGB only. Alpha is always 255 here.
|
|
118
89
|
if (((a >>> 0) & 0xffffff) === ((b >>> 0) & 0xffffff)) return true;
|
|
119
90
|
const A = unpackRGBA(a);
|
|
120
91
|
const B = unpackRGBA(b);
|
|
@@ -129,9 +100,9 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
129
100
|
z + sg * +(ax === 'z'),
|
|
130
101
|
];
|
|
131
102
|
|
|
132
|
-
//
|
|
103
|
+
// Scan for wedges.
|
|
133
104
|
const wedgeCell = new Map(); // cellIdx -> chosen {R,A,B,sA,sB,color}
|
|
134
|
-
const removed = new Set(); // base faces (idx*6+f)
|
|
105
|
+
const removed = new Set(); // base faces (idx*6+f) covered by a wedge
|
|
135
106
|
const wedges = [];
|
|
136
107
|
|
|
137
108
|
for (const ridge of RIDGES) {
|
|
@@ -140,18 +111,18 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
140
111
|
for (let y = 0; y < ny; y++)
|
|
141
112
|
for (let x = 0; x < nx; x++) {
|
|
142
113
|
const cidx = voxIndex(x, y, z, dims);
|
|
143
|
-
if (solid[cidx] || wedgeCell.has(cidx)) continue; //
|
|
114
|
+
if (solid[cidx] || wedgeCell.has(cidx)) continue; // empty and unclaimed
|
|
144
115
|
for (const sA of [-1, 1]) {
|
|
145
116
|
let placed = false;
|
|
146
117
|
for (const sB of [-1, 1]) {
|
|
147
|
-
const aN = step(x, y, z, A, sA); // solid
|
|
148
|
-
const bN = step(x, y, z, B, sB); // solid
|
|
118
|
+
const aN = step(x, y, z, A, sA); // solid neighbor on A side
|
|
119
|
+
const bN = step(x, y, z, B, sB); // solid neighbor on B side
|
|
149
120
|
if (!solidAt(...aN) || !solidAt(...bN)) continue;
|
|
150
121
|
// opposite sides must be empty -> exactly two adjacent solids
|
|
151
122
|
if (solidAt(...step(x, y, z, A, -sA))) continue;
|
|
152
123
|
if (solidAt(...step(x, y, z, B, -sB))) continue;
|
|
153
124
|
|
|
154
|
-
//
|
|
125
|
+
// the covered faces: each neighbor's face pointing back at the cell
|
|
155
126
|
const faceA = faceKeyOf(A, -sA);
|
|
156
127
|
const faceB = faceKeyOf(B, -sB);
|
|
157
128
|
const aKey = voxIndex(...aN, dims) * 6 + FACE_INDEX[faceA];
|
|
@@ -159,23 +130,9 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
159
130
|
const cA = faceColor.get(aKey);
|
|
160
131
|
const cB = faceColor.get(bKey);
|
|
161
132
|
|
|
162
|
-
// Gate:
|
|
163
|
-
// surfaces the prism merges (the riser cA and the tread cB) — are
|
|
164
|
-
// the same material. Nothing else is consulted: no profile/facing
|
|
165
|
-
// view sampling, no occlusion march. This is deliberate and gives
|
|
166
|
-
// the sprite author exact, local control over every wedge: paint the
|
|
167
|
-
// two faces a corner joins the same colour and it ramps; paint them
|
|
168
|
-
// differently and it stays a crisp step. A slope smooths only where
|
|
169
|
-
// its riser and its up-facing tread read the same colour, so the
|
|
170
|
-
// top-view art over a slope must match the face it caps — the author
|
|
171
|
-
// decides which corners round, not a heuristic guess about "slopes".
|
|
172
|
-
// (And, since the skin: a looser gate would COST triangles — more
|
|
173
|
-
// wedges are more caps and more split base faces, measured on the
|
|
174
|
-
// Car — so the strictness is the count's too.)
|
|
133
|
+
// Gate: the covered riser (cA) and tread (cB) must be the same material.
|
|
175
134
|
if (!flat && !sameMat(cA, cB)) continue;
|
|
176
|
-
//
|
|
177
|
-
// surface's true colour. (In flat mode the wedge colour is overridden
|
|
178
|
-
// to FLAT_COLOR downstream, so a null here can never render.)
|
|
135
|
+
// cA and cB agree unless flat, where FLAT_COLOR replaces the color.
|
|
179
136
|
const color = (cA != null ? cA : cB) >>> 0;
|
|
180
137
|
wedgeCell.set(cidx, { R, A, B, sA, sB, color });
|
|
181
138
|
removed.add(aKey);
|
|
@@ -189,15 +146,13 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
189
146
|
}
|
|
190
147
|
}
|
|
191
148
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// T-junctions the merges leave, THEN build the scaled buffers.
|
|
149
|
+
// Emit integer lattice triangles, repair T-junctions, then build the scaled
|
|
150
|
+
// buffers.
|
|
195
151
|
/** @type {Tri[]} */
|
|
196
|
-
const tris = []; // CCW
|
|
197
|
-
// paint: { chart, region } for a charted region, { swatch }
|
|
198
|
-
// one-material primitive (a one-colour region, a slope)
|
|
152
|
+
const tris = []; // CCW about normal
|
|
153
|
+
// paint: { chart, region } for a charted region, otherwise { swatch }
|
|
199
154
|
const pushTri = (a, b, c, N, paint) => {
|
|
200
|
-
// wind
|
|
155
|
+
// wind CCW about the outward normal N (backface culling is on)
|
|
201
156
|
const ux = b[0] - a[0],
|
|
202
157
|
uy = b[1] - a[1],
|
|
203
158
|
uz = b[2] - a[2];
|
|
@@ -241,8 +196,7 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
241
196
|
const L = Math.hypot(p[0], p[1], p[2]) || 1;
|
|
242
197
|
return [p[0] / L, p[1] / L, p[2] / L];
|
|
243
198
|
};
|
|
244
|
-
// a
|
|
245
|
-
// one toward the two solids) and the opposite one
|
|
199
|
+
// a wedge cell's corners along A and B: c toward the two solids, o opposite
|
|
246
200
|
const cornersOf = (w) => {
|
|
247
201
|
const p = { x: w.x, y: w.y, z: w.z };
|
|
248
202
|
const aC = p[w.A];
|
|
@@ -255,14 +209,11 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
255
209
|
};
|
|
256
210
|
};
|
|
257
211
|
|
|
258
|
-
// 1.
|
|
259
|
-
// orientation (R, A, B, sA, sB) and
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// a block's slope is one quad from its first staircase cell's far corners to
|
|
264
|
-
// its last's, swept over its r-range. One material each by the gate, so
|
|
265
|
-
// every slope samples its colour's swatch.
|
|
212
|
+
// 1. Slopes, one quad per block. The cells of one 45° plane share an
|
|
213
|
+
// orientation (R, A, B, sA, sB) and an intercept sA·a + sB·b. They form a grid
|
|
214
|
+
// with t = sA·a up the staircase and r along the ridge, greedy-merged by
|
|
215
|
+
// color along r first. A block's quad runs from its first cell's corners to
|
|
216
|
+
// its last cell's, across its r range, and samples its color's swatch.
|
|
266
217
|
const planes = new Map(); // plane key -> Map<'t,r', {t, r, w}>
|
|
267
218
|
for (const w of wedges) {
|
|
268
219
|
const p = { x: w.x, y: w.y, z: w.z };
|
|
@@ -313,11 +264,10 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
313
264
|
}
|
|
314
265
|
}
|
|
315
266
|
|
|
316
|
-
// 2.
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
// that face's plane in the face's own tangent frame.
|
|
267
|
+
// 2. Gable caps, as half pieces of the planes they lie on. A cell end gets a
|
|
268
|
+
// cap unless it meets solid or a wedge of the same orientation. The cap is
|
|
269
|
+
// the right triangle in the cell's ±R face with its right angle at the filled
|
|
270
|
+
// corner (Ac, Bc), in that face's tangent frame.
|
|
321
271
|
/** @type {Map<string, import('./regions.js').Half[]>} */
|
|
322
272
|
const halves = new Map();
|
|
323
273
|
for (const w of wedges) {
|
|
@@ -327,7 +277,7 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
327
277
|
if (solidAt(ex, ey, ez)) continue; // internal against solid
|
|
328
278
|
if (inBounds(ex, ey, ez)) {
|
|
329
279
|
const wn = wedgeCell.get(voxIndex(ex, ey, ez, dims));
|
|
330
|
-
if (wn && wn.R === w.R && wn.sA === w.sA && wn.sB === w.sB) continue;
|
|
280
|
+
if (wn && wn.R === w.R && wn.sA === w.sA && wn.sB === w.sB) continue;
|
|
331
281
|
}
|
|
332
282
|
const face = faceKeyOf(w.R, sg);
|
|
333
283
|
const g = FACE_GEO[face];
|
|
@@ -343,10 +293,9 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
343
293
|
}
|
|
344
294
|
}
|
|
345
295
|
|
|
346
|
-
// 3.
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
// has no skin: every primitive is the flat grey, the UVs zero.
|
|
296
|
+
// 3. Regions: each plane's uncovered exposed faces plus its caps. Bake the
|
|
297
|
+
// skin once, then triangulate each region with earcut. Flat mode has no
|
|
298
|
+
// skin: every triangle is FLAT_COLOR and the UVs are zero.
|
|
350
299
|
const baseMask = surfaceMask.slice();
|
|
351
300
|
for (const rk of removed) baseMask[(rk / 6) | 0] &= ~(1 << rk % 6);
|
|
352
301
|
const regions = faceRegions(dims, baseMask, faceColor, halves);
|
|
@@ -356,15 +305,18 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
356
305
|
const paint = chart
|
|
357
306
|
? { chart, region }
|
|
358
307
|
: { swatch: flat ? FLAT_COLOR : /** @type {number} */ (region.uniform) >>> 0 };
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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);
|
|
364
316
|
const verts = [region.outer, ...region.holes].flat();
|
|
365
317
|
let area = 0;
|
|
366
|
-
for (
|
|
367
|
-
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]]];
|
|
368
320
|
area += Math.abs((q[0] - p[0]) * (r[1] - p[1]) - (r[0] - p[0]) * (q[1] - p[1]));
|
|
369
321
|
pushTri(
|
|
370
322
|
pointOf(region.face, p[0], p[1], region.s),
|
|
@@ -380,15 +332,13 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
380
332
|
);
|
|
381
333
|
});
|
|
382
334
|
|
|
383
|
-
// 4.
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
// repair inserted along an edge included), a swatch triangle's three sit at
|
|
387
|
-
// its texel's centre. Texel coords over the skin's size make the [0,1] UV.
|
|
335
|
+
// 4. Repair T-junctions, then flatten to scaled buffers. UVs are read after
|
|
336
|
+
// the repair: a charted vertex maps into its chart by lattice position, and
|
|
337
|
+
// a swatch triangle's vertices sit at the swatch texel's center.
|
|
388
338
|
const repaired = eliminateTJunctions(tris);
|
|
389
339
|
const pos = new Float32Array(repaired.length * 9);
|
|
390
340
|
const nrm = new Float32Array(repaired.length * 9);
|
|
391
|
-
const uv = new Float32Array(repaired.length * 6); //
|
|
341
|
+
const uv = new Float32Array(repaired.length * 6); // zero in flat mode
|
|
392
342
|
let o = 0;
|
|
393
343
|
let q = 0;
|
|
394
344
|
for (const t of repaired) {
|
|
@@ -410,32 +360,33 @@ export function wedgeMesh(result, opts = {}) {
|
|
|
410
360
|
}
|
|
411
361
|
}
|
|
412
362
|
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
+
}
|
|
425
382
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
triangles: geo.index ? geo.index.count / 3 : repaired.length,
|
|
436
|
-
wedges: wedges.length, // cells
|
|
437
|
-
slopes,
|
|
438
|
-
skin: skin ? { width: skin.width, height: skin.height, charts: charted } : null,
|
|
439
|
-
},
|
|
440
|
-
});
|
|
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
|
+
};
|
|
441
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,65 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Shared helpers for the wedge mesh builder (wedge-mesh.js), and the ONE place
|
|
3
|
-
// THREE meets the skin: the texture the pure bake (skin.js) becomes, and the
|
|
4
|
-
// framing + material the builder finishes with. (The plain voxel builder that
|
|
5
|
-
// once shared them, mesh.js, went with the test trim of Sep 5 2026 — dead
|
|
6
|
-
// code with no consumer; the vertex-color linearizer went with the skin on
|
|
7
|
-
// Sep 7 2026 — the GPU's sampler decodes sRGB now, where the CPU used to.)
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
import * as THREE from 'three';
|
|
11
|
-
import { unpackRGBA } from './ingest.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The skin as a texture. DataTexture's constructor already states the skin's
|
|
15
|
-
* sampling contract — nearest filtering both ways, no mipmaps, flipY false
|
|
16
|
-
* (texel row 0 is v = 0, the bake's orientation), unpackAlignment 1 — those
|
|
17
|
-
* are the class's own defaults, named here as documentation, never restated
|
|
18
|
-
* as a correction. The bytes are sRGB, so the texture says so and the
|
|
19
|
-
* sampler decodes them on the way to the renderers' sRGB output.
|
|
20
|
-
* @param {import('./skin.js').Skin} skin
|
|
21
|
-
* @returns {THREE.DataTexture}
|
|
22
|
-
*/
|
|
23
|
-
export function skinTexture(skin) {
|
|
24
|
-
const tex = new THREE.DataTexture(skin.data, skin.width, skin.height, THREE.RGBAFormat);
|
|
25
|
-
tex.colorSpace = THREE.SRGBColorSpace;
|
|
26
|
-
tex.needsUpdate = true;
|
|
27
|
-
return tex;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Shared final assembly: center the geometry on X/Z, leave Y exactly as
|
|
32
|
-
* authored (no ground-rest — the Y translate is a hard 0; where the object
|
|
33
|
-
* sits vertically is wherever the artist painted it, see carve.js), compute
|
|
34
|
-
* bounds, and wrap it in the standard flat-shaded material with shadows on —
|
|
35
|
-
* the skin as its `map`, or, with no map (flat mode), one packed `color`
|
|
36
|
-
* (sRGB bytes) as its albedo.
|
|
37
|
-
* @param {THREE.BufferGeometry} geo
|
|
38
|
-
* @param {{nx:number, nz:number, s:number, map?:THREE.Texture|null, color?:number|null,
|
|
39
|
-
* userData?:Record<string,unknown>}} opts
|
|
40
|
-
* @returns {THREE.Mesh}
|
|
41
|
-
*/
|
|
42
|
-
export function finishVoxelMesh(
|
|
43
|
-
geo,
|
|
44
|
-
{ nx, nz, s, map = null, color = null, userData = {} }
|
|
45
|
-
) {
|
|
46
|
-
geo.translate((-nx * s) / 2, 0, (-nz * s) / 2); // center X/Z; Y left as authored
|
|
47
|
-
geo.computeBoundingBox();
|
|
48
|
-
geo.computeBoundingSphere();
|
|
49
|
-
|
|
50
|
-
const mat = new THREE.MeshStandardMaterial({
|
|
51
|
-
flatShading: true,
|
|
52
|
-
metalness: 0,
|
|
53
|
-
roughness: 1,
|
|
54
|
-
...(map ? { map } : {}),
|
|
55
|
-
});
|
|
56
|
-
if (!map && color != null) {
|
|
57
|
-
const { r, g, b } = unpackRGBA(color);
|
|
58
|
-
mat.color.setRGB(r / 255, g / 255, b / 255, THREE.SRGBColorSpace);
|
|
59
|
-
}
|
|
60
|
-
const mesh = new THREE.Mesh(geo, mat);
|
|
61
|
-
mesh.castShadow = true;
|
|
62
|
-
mesh.receiveShadow = true;
|
|
63
|
-
Object.assign(mesh.userData, userData);
|
|
64
|
-
return mesh;
|
|
65
|
-
}
|