@driftengine/assets 4.3.0 → 4.4.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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/dist/bake/collapseHeap.d.ts +24 -0
  3. package/dist/bake/collapseHeap.js +80 -0
  4. package/dist/bake/collapseMeasures.d.ts +31 -0
  5. package/dist/bake/collapseMeasures.js +183 -0
  6. package/dist/bake/sceneField.d.ts +6 -0
  7. package/dist/bake/sceneField.js +48 -0
  8. package/dist/bake/sdf.d.ts +5 -5
  9. package/dist/bake/sdf.js +15 -95
  10. package/dist/bake/simplify.d.ts +42 -0
  11. package/dist/bake/simplify.js +350 -0
  12. package/dist/bake/surfaceDepth.d.ts +29 -0
  13. package/dist/bake/surfaceDepth.js +101 -0
  14. package/dist/downscale.d.ts +25 -0
  15. package/dist/downscale.js +50 -0
  16. package/dist/drawKey.d.ts +13 -1
  17. package/dist/drawKey.js +5 -3
  18. package/dist/drftLoader.d.ts +38 -7
  19. package/dist/drftLoader.js +186 -30
  20. package/dist/fieldPlacement.d.ts +22 -0
  21. package/dist/fieldPlacement.js +49 -0
  22. package/dist/gltf.d.ts +46 -1
  23. package/dist/gltf.js +101 -2
  24. package/dist/index.d.ts +9 -1
  25. package/dist/index.js +15 -1
  26. package/dist/loadProgress.d.ts +22 -1
  27. package/dist/orient.d.ts +9 -1
  28. package/dist/orient.js +60 -2
  29. package/dist/partMaterial.d.ts +19 -0
  30. package/dist/partMaterial.js +16 -0
  31. package/dist/readModel.d.ts +3 -1
  32. package/dist/repeats.d.ts +39 -0
  33. package/dist/repeats.js +391 -0
  34. package/dist/weld.d.ts +8 -0
  35. package/dist/weld.js +155 -34
  36. package/package.json +4 -4
  37. package/src/bake/collapseHeap.ts +81 -0
  38. package/src/bake/collapseMeasures.ts +215 -0
  39. package/src/bake/sceneField.ts +50 -0
  40. package/src/bake/sdf.ts +14 -99
  41. package/src/bake/simplify.ts +395 -0
  42. package/src/bake/surfaceDepth.ts +102 -0
  43. package/src/downscale.ts +61 -0
  44. package/src/drawKey.ts +17 -3
  45. package/src/drftLoader.ts +224 -38
  46. package/src/fieldPlacement.ts +74 -0
  47. package/src/gltf.ts +139 -3
  48. package/src/index.ts +24 -1
  49. package/src/loadProgress.ts +23 -1
  50. package/src/orient.ts +62 -3
  51. package/src/partMaterial.ts +42 -0
  52. package/src/readModel.ts +3 -1
  53. package/src/repeats.ts +442 -0
  54. package/src/weld.ts +164 -34
@@ -0,0 +1,350 @@
1
+ import { keepVertices } from '../weld.js';
2
+ import { CollapseHeap } from './collapseHeap.js';
3
+ import { addBorderPlanes, addFacePlanes, edgeKey, quadricAt, triangleNormal, uvArea, uvMiss, } from './collapseMeasures.js';
4
+ /** The most a collapse may turn a face: cos 70°. */
5
+ const MIN_FACE_TURN = 0.34;
6
+ /** The widest two shading normals a collapse may join: cos 25°. */
7
+ const MIN_NORMAL_AGREEMENT = 0.906;
8
+ /** How far a face's texture may stretch or shrink, as a ratio of its uv area to its area. */
9
+ const MAX_UV_STRETCH = 1.5;
10
+ /** A texel of a 1,024 map. */
11
+ const DEFAULT_MAX_UV_ERROR = 1 / 1024;
12
+ export function simplifyMesh(mesh, options) {
13
+ if (mesh.joints !== undefined || mesh.weights !== undefined || mesh.morphTargets !== undefined) {
14
+ return mesh;
15
+ }
16
+ const faceCount = mesh.indices.length / 3;
17
+ const vertexCount = mesh.positions.length / 3;
18
+ if (faceCount === 0)
19
+ return mesh;
20
+ const s = new Simplifier(mesh, options.maxError, options.maxUvError ?? DEFAULT_MAX_UV_ERROR);
21
+ const floor = Math.ceil(faceCount * Math.max(0, Math.min(1, options.minRatio ?? 0)));
22
+ s.run(floor);
23
+ if (s.alive === faceCount)
24
+ return mesh;
25
+ return s.output(vertexCount);
26
+ }
27
+ class Simplifier {
28
+ mesh;
29
+ maxCost;
30
+ p;
31
+ faces;
32
+ faceAlive;
33
+ facesOf;
34
+ quadric;
35
+ area;
36
+ locked;
37
+ removed;
38
+ version;
39
+ /** The vertices each survivor has taken in, whose painted uvs every later collapse must keep. */
40
+ absorbed;
41
+ maxUvError;
42
+ heap = new CollapseHeap();
43
+ alive;
44
+ constructor(mesh, maxError, maxUvError) {
45
+ this.mesh = mesh;
46
+ this.maxCost = maxError * maxError;
47
+ this.maxUvError = maxUvError;
48
+ const v = mesh.positions.length / 3;
49
+ this.p = Float64Array.from(mesh.positions);
50
+ this.faces = Uint32Array.from(mesh.indices);
51
+ const f = this.faces.length / 3;
52
+ this.alive = f;
53
+ this.faceAlive = new Uint8Array(f).fill(1);
54
+ this.facesOf = Array.from({ length: v }, () => []);
55
+ this.quadric = new Float64Array(v * 10);
56
+ this.area = new Float64Array(v);
57
+ this.locked = new Uint8Array(v);
58
+ this.removed = new Uint8Array(v);
59
+ this.version = new Uint32Array(v);
60
+ this.absorbed = Array.from({ length: v }, () => []);
61
+ for (let face = 0; face < f; face++) {
62
+ for (let k = 0; k < 3; k++)
63
+ this.facesOf[this.faces[face * 3 + k]].push(face);
64
+ }
65
+ const edges = countEdges(this.faces, v);
66
+ this.lockShared(edges);
67
+ addFacePlanes(this.p, this.faces, this.quadric, this.area);
68
+ addBorderPlanes(this.p, this.faces, edges, this.quadric);
69
+ for (let u = 0; u < v; u++)
70
+ this.consider(u);
71
+ }
72
+ /** Every vertex that shares its place with another, and both ends of an edge three faces share. */
73
+ lockShared(edges) {
74
+ const v = this.p.length / 3;
75
+ const bits = new Float32Array(this.mesh.positions.buffer, this.mesh.positions.byteOffset, v * 3);
76
+ const words = new Uint32Array(bits.buffer, bits.byteOffset, v * 3);
77
+ const first = new Map();
78
+ for (let u = 0; u < v; u++) {
79
+ const key = `${words[u * 3]},${words[u * 3 + 1]},${words[u * 3 + 2]}`;
80
+ const seen = first.get(key);
81
+ if (seen === undefined)
82
+ first.set(key, u);
83
+ else {
84
+ this.locked[u] = 1;
85
+ this.locked[seen] = 1;
86
+ }
87
+ }
88
+ for (const [key, count] of edges) {
89
+ if (count <= 2)
90
+ continue;
91
+ this.locked[Math.floor(key / v)] = 1;
92
+ this.locked[key % v] = 1;
93
+ }
94
+ }
95
+ /** The alive faces around `u`, in order, without the dead ones it still lists. */
96
+ around(u) {
97
+ const list = this.facesOf[u];
98
+ let write = 0;
99
+ for (let i = 0; i < list.length; i++) {
100
+ const face = list[i];
101
+ if (this.faceAlive[face] === 1 && this.hasVertex(face, u))
102
+ list[write++] = face;
103
+ }
104
+ list.length = write;
105
+ return list;
106
+ }
107
+ hasVertex(face, u) {
108
+ return (this.faces[face * 3] === u || this.faces[face * 3 + 1] === u || this.faces[face * 3 + 2] === u);
109
+ }
110
+ /** How many alive faces hold the edge `a`–`b`. */
111
+ edgeFaces(a, b) {
112
+ let n = 0;
113
+ for (const face of this.around(a))
114
+ if (this.hasVertex(face, b))
115
+ n++;
116
+ return n;
117
+ }
118
+ /** The best collapse of `u` onto a neighbour, pushed on the heap under `u`'s current version. */
119
+ consider(u) {
120
+ if (this.removed[u] === 1 || this.locked[u] === 1)
121
+ return;
122
+ const faces = this.around(u);
123
+ if (faces.length === 0)
124
+ return;
125
+ /* A border vertex moves only along its border, and only if it has exactly two border edges. */
126
+ const neighbours = [];
127
+ const borderNeighbours = [];
128
+ for (const face of faces) {
129
+ for (let k = 0; k < 3; k++) {
130
+ const w = this.faces[face * 3 + k];
131
+ if (w === u || neighbours.includes(w))
132
+ continue;
133
+ neighbours.push(w);
134
+ if (this.edgeFaces(u, w) === 1)
135
+ borderNeighbours.push(w);
136
+ }
137
+ }
138
+ if (borderNeighbours.length !== 0 && borderNeighbours.length !== 2)
139
+ return;
140
+ const candidates = borderNeighbours.length === 2 ? borderNeighbours : neighbours;
141
+ let best = -1;
142
+ let bestCost = Infinity;
143
+ for (const v of candidates) {
144
+ const cost = this.cost(u, v);
145
+ if (cost >= bestCost || cost > this.maxCost)
146
+ continue;
147
+ if (!this.valid(u, v, faces))
148
+ continue;
149
+ best = v;
150
+ bestCost = cost;
151
+ }
152
+ if (best >= 0)
153
+ this.heap.push(bestCost, u, best, this.version[u]);
154
+ }
155
+ /** The mean squared distance from both vertices' planes to where `u` lands, at `v`. */
156
+ cost(u, v) {
157
+ const x = this.p[v * 3];
158
+ const y = this.p[v * 3 + 1];
159
+ const z = this.p[v * 3 + 2];
160
+ const sum = quadricAt(this.quadric, u, x, y, z) + quadricAt(this.quadric, v, x, y, z);
161
+ return Math.max(0, sum) / Math.max(this.area[u] + this.area[v], 1e-30);
162
+ }
163
+ valid(u, v, faces) {
164
+ if (this.removed[v] === 1)
165
+ return false;
166
+ const normals = this.mesh.normals;
167
+ const agree = normals[u * 3] * normals[v * 3] +
168
+ normals[u * 3 + 1] * normals[v * 3 + 1] +
169
+ normals[u * 3 + 2] * normals[v * 3 + 2];
170
+ if (agree < MIN_NORMAL_AGREEMENT)
171
+ return false;
172
+ if (!this.linkHolds(u, v, faces))
173
+ return false;
174
+ const before = [0, 0, 0];
175
+ const after = [0, 0, 0];
176
+ const uvs = this.mesh.uvs;
177
+ for (const face of faces) {
178
+ if (this.hasVertex(face, v))
179
+ continue;
180
+ const a = this.faces[face * 3];
181
+ const b = this.faces[face * 3 + 1];
182
+ const c = this.faces[face * 3 + 2];
183
+ const areaBefore = triangleNormal(this.p, a, b, c, before);
184
+ const areaAfter = triangleNormal(this.p, a === u ? v : a, b === u ? v : b, c === u ? v : c, after);
185
+ if (areaAfter <= areaBefore * 1e-6)
186
+ return false;
187
+ const turn = before[0] * after[0] + before[1] * after[1] + before[2] * after[2];
188
+ if (turn < MIN_FACE_TURN)
189
+ return false;
190
+ if (uvs !== undefined && areaBefore > 0) {
191
+ const uvBefore = uvArea(uvs, a, b, c);
192
+ const uvAfter = uvArea(uvs, a === u ? v : a, b === u ? v : b, c === u ? v : c);
193
+ if (uvBefore !== 0 && Math.sign(uvBefore) !== Math.sign(uvAfter))
194
+ return false;
195
+ const stretch = Math.abs(uvAfter) / areaAfter / Math.max(Math.abs(uvBefore) / areaBefore, 1e-30);
196
+ if (uvBefore !== 0 && (stretch > MAX_UV_STRETCH || stretch < 1 / MAX_UV_STRETCH))
197
+ return false;
198
+ }
199
+ }
200
+ return uvs === undefined || this.texturesHold(u, v, faces, uvs);
201
+ }
202
+ /**
203
+ * Whether every vertex the collapse leaves behind, `u` and all it and `v` have taken in, still finds
204
+ * its own uv on the surface that is left: interpolated by the face it lies over, within
205
+ * `maxUvError`. The per-face checks above see only corners, and a curved mapping can pass them
206
+ * collapse by collapse while the texture slides a long way; this is measured against what was
207
+ * painted, so it cannot accumulate.
208
+ */
209
+ texturesHold(u, v, faces, uvs) {
210
+ const after = [];
211
+ for (const face of faces) {
212
+ if (this.hasVertex(face, v))
213
+ continue;
214
+ for (let k = 0; k < 3; k++) {
215
+ const w = this.faces[face * 3 + k];
216
+ after.push(w === u ? v : w);
217
+ }
218
+ }
219
+ for (const face of this.around(v)) {
220
+ if (this.hasVertex(face, u))
221
+ continue;
222
+ for (let k = 0; k < 3; k++)
223
+ after.push(this.faces[face * 3 + k]);
224
+ }
225
+ if (after.length === 0)
226
+ return false;
227
+ const samples = [u, ...this.absorbed[u], ...this.absorbed[v]];
228
+ for (const sample of samples) {
229
+ if (uvMiss(this.p, uvs, after, sample) > this.maxUvError)
230
+ return false;
231
+ }
232
+ return true;
233
+ }
234
+ /**
235
+ * The link condition: the neighbours `u` and `v` share are exactly the far corners of the faces
236
+ * they share. Otherwise the collapse pinches the surface into a fin or a non-manifold edge.
237
+ *
238
+ * No test here isolates it: every pinch built for one also folded a face, which the check before
239
+ * it refuses. It stays as the standard guard for a closed surface, where that need not hold.
240
+ */
241
+ linkHolds(u, v, faces) {
242
+ const opposite = [];
243
+ const ofU = [];
244
+ for (const face of faces) {
245
+ const shared = this.hasVertex(face, v);
246
+ for (let k = 0; k < 3; k++) {
247
+ const w = this.faces[face * 3 + k];
248
+ if (w === u || w === v)
249
+ continue;
250
+ if (shared)
251
+ opposite.push(w);
252
+ if (!ofU.includes(w))
253
+ ofU.push(w);
254
+ }
255
+ }
256
+ for (const face of this.around(v)) {
257
+ for (let k = 0; k < 3; k++) {
258
+ const w = this.faces[face * 3 + k];
259
+ if (w === u || w === v)
260
+ continue;
261
+ if (ofU.includes(w) && !opposite.includes(w))
262
+ return false;
263
+ }
264
+ }
265
+ return opposite.length > 0;
266
+ }
267
+ run(floor) {
268
+ const heap = this.heap;
269
+ while (heap.size > 0 && this.alive > floor) {
270
+ const cost = heap.topCost();
271
+ if (cost > this.maxCost)
272
+ break;
273
+ const { u, v, version } = heap.pop();
274
+ if (version !== this.version[u] || this.removed[u] === 1 || this.removed[v] === 1)
275
+ continue;
276
+ const faces = this.around(u);
277
+ /* The world may have moved under a queued collapse; ask again before making it. */
278
+ if (!this.valid(u, v, faces)) {
279
+ this.version[u] = this.version[u] + 1;
280
+ this.consider(u);
281
+ continue;
282
+ }
283
+ this.collapse(u, v, faces);
284
+ }
285
+ }
286
+ collapse(u, v, faces) {
287
+ this.removed[u] = 1;
288
+ const toV = this.facesOf[v];
289
+ for (const face of faces) {
290
+ if (this.hasVertex(face, v)) {
291
+ this.faceAlive[face] = 0;
292
+ this.alive--;
293
+ continue;
294
+ }
295
+ for (let k = 0; k < 3; k++)
296
+ if (this.faces[face * 3 + k] === u)
297
+ this.faces[face * 3 + k] = v;
298
+ toV.push(face);
299
+ }
300
+ for (let k = 0; k < 10; k++)
301
+ this.quadric[v * 10 + k] = this.quadric[v * 10 + k] + this.quadric[u * 10 + k];
302
+ this.area[v] = this.area[v] + this.area[u];
303
+ this.absorbed[v].push(u, ...this.absorbed[u]);
304
+ this.absorbed[u].length = 0;
305
+ this.facesOf[u].length = 0;
306
+ /* Everything whose best move could have changed: `v` and every vertex round it. */
307
+ const touched = [v];
308
+ for (const face of this.around(v)) {
309
+ for (let k = 0; k < 3; k++) {
310
+ const w = this.faces[face * 3 + k];
311
+ if (!touched.includes(w))
312
+ touched.push(w);
313
+ }
314
+ }
315
+ for (const w of touched) {
316
+ this.version[w] = this.version[w] + 1;
317
+ this.consider(w);
318
+ }
319
+ }
320
+ output(vertexCount) {
321
+ const keep = new Int32Array(vertexCount).fill(-1);
322
+ const indices = new Uint32Array(this.alive * 3);
323
+ let count = 0;
324
+ let at = 0;
325
+ const f = this.faces.length / 3;
326
+ for (let face = 0; face < f; face++) {
327
+ if (this.faceAlive[face] !== 1)
328
+ continue;
329
+ for (let k = 0; k < 3; k++) {
330
+ const old = this.faces[face * 3 + k];
331
+ if (keep[old] === -1)
332
+ keep[old] = count++;
333
+ indices[at++] = keep[old];
334
+ }
335
+ }
336
+ return keepVertices(this.mesh, keep, count, indices);
337
+ }
338
+ }
339
+ /** How many faces hold each edge, keyed by `edgeKey`. */
340
+ function countEdges(faces, v) {
341
+ const edges = new Map();
342
+ const f = faces.length / 3;
343
+ for (let face = 0; face < f; face++) {
344
+ for (let k = 0; k < 3; k++) {
345
+ const key = edgeKey(faces[face * 3 + k], faces[face * 3 + ((k + 1) % 3)], v);
346
+ edges.set(key, (edges.get(key) ?? 0) + 1);
347
+ }
348
+ }
349
+ return edges;
350
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * How many surfaces stand between each point of a distance grid and the open air around it: the
3
+ * sign of a field that parity cannot give.
4
+ *
5
+ * **Parity is exact for a closed solid and wrong for a sheet**, and a bought scene is sheets:
6
+ * single-sided walls, rooms open to the sky, a floor with nothing under it. A ray along one axis
7
+ * that passes a sheet flips every point after it, which on a baked courtyard striped the open air
8
+ * by altitude, +2.6 at 4 m and -2.7 from 5 m to 9 m, and light traced through the striped half
9
+ * found black. A closed ball behind that sheet came back empty for the same reason.
10
+ *
11
+ * **So the grid is walked instead.** Everything reachable from the grid's own faces without
12
+ * crossing the surface is depth 0, outside. A region bordering depth 0 across the surface is
13
+ * depth 1, the inside of whatever it borders; a region bordering only depth 1 is sealed air inside
14
+ * that solid, depth 2; and so on. An odd depth is inside. No axis is preferred and no sheet
15
+ * reaches past the region it bounds.
16
+ *
17
+ * **A step between neighbours crosses no surface when their distances add to more than the step**:
18
+ * a surface crossing the segment would be nearer each end than the crossing is, and the two parts
19
+ * of the segment sum to the step. So the walk is exact where it moves and conservative where it
20
+ * stops. What that gives up is a gap narrower than about a voxel, which reads as sealed, and a point
21
+ * within a step of the surface along an inside corner, where every step is blocked and the point
22
+ * becomes a region of its own. `bakeObjectSdf` signs that shell by parity instead, which is exact
23
+ * there for a closed solid and wrong only by the voxel's own small distance next to a sheet.
24
+ */
25
+ /**
26
+ * Each point's depth, from an unsigned `field` on a grid of `dims` points `step` apart, `x`
27
+ * fastest. Depth 0 is the air reachable from the grid's faces; odd depths are inside.
28
+ */
29
+ export declare function surfaceDepth(field: Float32Array, dims: readonly [number, number, number], step: number): Int32Array;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * How many surfaces stand between each point of a distance grid and the open air around it: the
3
+ * sign of a field that parity cannot give.
4
+ *
5
+ * **Parity is exact for a closed solid and wrong for a sheet**, and a bought scene is sheets:
6
+ * single-sided walls, rooms open to the sky, a floor with nothing under it. A ray along one axis
7
+ * that passes a sheet flips every point after it, which on a baked courtyard striped the open air
8
+ * by altitude, +2.6 at 4 m and -2.7 from 5 m to 9 m, and light traced through the striped half
9
+ * found black. A closed ball behind that sheet came back empty for the same reason.
10
+ *
11
+ * **So the grid is walked instead.** Everything reachable from the grid's own faces without
12
+ * crossing the surface is depth 0, outside. A region bordering depth 0 across the surface is
13
+ * depth 1, the inside of whatever it borders; a region bordering only depth 1 is sealed air inside
14
+ * that solid, depth 2; and so on. An odd depth is inside. No axis is preferred and no sheet
15
+ * reaches past the region it bounds.
16
+ *
17
+ * **A step between neighbours crosses no surface when their distances add to more than the step**:
18
+ * a surface crossing the segment would be nearer each end than the crossing is, and the two parts
19
+ * of the segment sum to the step. So the walk is exact where it moves and conservative where it
20
+ * stops. What that gives up is a gap narrower than about a voxel, which reads as sealed, and a point
21
+ * within a step of the surface along an inside corner, where every step is blocked and the point
22
+ * becomes a region of its own. `bakeObjectSdf` signs that shell by parity instead, which is exact
23
+ * there for a closed solid and wrong only by the voxel's own small distance next to a sheet.
24
+ */
25
+ /**
26
+ * The walk's margin over the step. A surface through a grid point is the equality case of the
27
+ * rule, a point at zero beside one a step away, and float rounding decides it either way; a cube's
28
+ * faces land on grid points whenever its size is a whole number of voxels. A hundredth of a step
29
+ * settles it on the sealed side, and seals a gap only that much sooner.
30
+ */
31
+ const WALK_MARGIN = 1.01;
32
+ /**
33
+ * Each point's depth, from an unsigned `field` on a grid of `dims` points `step` apart, `x`
34
+ * fastest. Depth 0 is the air reachable from the grid's faces; odd depths are inside.
35
+ */
36
+ export function surfaceDepth(field, dims, step) {
37
+ const [nx, ny, nz] = dims;
38
+ const count = field.length;
39
+ const depth = new Int32Array(count).fill(-1);
40
+ const queue = new Int32Array(count);
41
+ /** Blocked neighbours of this depth's walk, which seed the next, each listed once. */
42
+ let seeds = new Int32Array(count);
43
+ let spare = new Int32Array(count);
44
+ const listed = new Uint8Array(count);
45
+ let seedCount = 0;
46
+ for (let k = 0; k < nz; k++) {
47
+ for (let j = 0; j < ny; j++) {
48
+ for (let i = 0; i < nx; i++) {
49
+ if (i > 0 && j > 0 && k > 0 && i < nx - 1 && j < ny - 1 && k < nz - 1)
50
+ continue;
51
+ seeds[seedCount++] = i + nx * (j + ny * k);
52
+ }
53
+ }
54
+ }
55
+ const plane = nx * ny;
56
+ const limit = step * WALK_MARGIN;
57
+ for (let current = 0; seedCount > 0; current++) {
58
+ let head = 0;
59
+ let tail = 0;
60
+ for (let s = 0; s < seedCount; s++) {
61
+ const seed = seeds[s];
62
+ listed[seed] = 0;
63
+ if (depth[seed] !== -1)
64
+ continue;
65
+ depth[seed] = current;
66
+ queue[tail++] = seed;
67
+ }
68
+ let nextCount = 0;
69
+ while (head < tail) {
70
+ const index = queue[head++];
71
+ const here = Math.abs(field[index]);
72
+ const i = index % nx;
73
+ const j = Math.floor(index / nx) % ny;
74
+ const k = Math.floor(index / plane);
75
+ for (let n = 0; n < 6; n++) {
76
+ const axis = n >> 1;
77
+ const delta = (n & 1) === 0 ? -1 : 1;
78
+ const at = axis === 0 ? i : axis === 1 ? j : k;
79
+ const size = axis === 0 ? nx : axis === 1 ? ny : nz;
80
+ if (at + delta < 0 || at + delta >= size)
81
+ continue;
82
+ const neighbour = index + delta * (axis === 0 ? 1 : axis === 1 ? nx : plane);
83
+ if (depth[neighbour] !== -1)
84
+ continue;
85
+ if (here + Math.abs(field[neighbour]) > limit) {
86
+ depth[neighbour] = current;
87
+ queue[tail++] = neighbour;
88
+ }
89
+ else if (listed[neighbour] === 0) {
90
+ listed[neighbour] = 1;
91
+ spare[nextCount++] = neighbour;
92
+ }
93
+ }
94
+ }
95
+ const used = seeds;
96
+ seeds = spare;
97
+ spare = used;
98
+ seedCount = nextCount;
99
+ }
100
+ return depth;
101
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shrinking a decoded image to a texture cap, offline, for the baker.
3
+ *
4
+ * **Halving rather than an arbitrary ratio**, so each output texel covers a whole block of source
5
+ * texels and a box average is exact. The cost is that a cap is reached in powers of two: a
6
+ * 3000-wide map capped at 1024 comes out 750, not 1024. What would make that wrong is a budget
7
+ * tight enough that the lost resolution is worth an arbitrary-ratio filter.
8
+ *
9
+ * **Averaged in the encoded values, not in linear light.** For a normal or ORM map that is
10
+ * correct, because those values are linear data. For an sRGB colour map it darkens fine,
11
+ * high-contrast detail slightly, since the mean of two sRGB values is darker than the sRGB of
12
+ * their mean. What would make that wrong is a capped bake whose distant albedo reads visibly
13
+ * darker than the uncapped one; the fix then is to linearise the colour slot before averaging.
14
+ */
15
+ /** The size `width` by `height` is halved to until its longer side is at most `maxSide`. */
16
+ export declare function cappedSize(width: number, height: number, maxSide: number): {
17
+ width: number;
18
+ height: number;
19
+ };
20
+ /**
21
+ * A box average from `width`×`height` down to `outWidth`×`outHeight`, four bytes a texel.
22
+ *
23
+ * Each output texel averages the source rectangle it covers, rounded to nearest.
24
+ */
25
+ export declare function downscaleRgba(rgba: Uint8Array, width: number, height: number, outWidth: number, outHeight: number): Uint8Array;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shrinking a decoded image to a texture cap, offline, for the baker.
3
+ *
4
+ * **Halving rather than an arbitrary ratio**, so each output texel covers a whole block of source
5
+ * texels and a box average is exact. The cost is that a cap is reached in powers of two: a
6
+ * 3000-wide map capped at 1024 comes out 750, not 1024. What would make that wrong is a budget
7
+ * tight enough that the lost resolution is worth an arbitrary-ratio filter.
8
+ *
9
+ * **Averaged in the encoded values, not in linear light.** For a normal or ORM map that is
10
+ * correct, because those values are linear data. For an sRGB colour map it darkens fine,
11
+ * high-contrast detail slightly, since the mean of two sRGB values is darker than the sRGB of
12
+ * their mean. What would make that wrong is a capped bake whose distant albedo reads visibly
13
+ * darker than the uncapped one; the fix then is to linearise the colour slot before averaging.
14
+ */
15
+ /** The size `width` by `height` is halved to until its longer side is at most `maxSide`. */
16
+ export function cappedSize(width, height, maxSide) {
17
+ let w = width;
18
+ let h = height;
19
+ while (Math.max(w, h) > maxSide) {
20
+ w = Math.max(1, Math.floor(w / 2));
21
+ h = Math.max(1, Math.floor(h / 2));
22
+ }
23
+ return { width: w, height: h };
24
+ }
25
+ /**
26
+ * A box average from `width`×`height` down to `outWidth`×`outHeight`, four bytes a texel.
27
+ *
28
+ * Each output texel averages the source rectangle it covers, rounded to nearest.
29
+ */
30
+ export function downscaleRgba(rgba, width, height, outWidth, outHeight) {
31
+ const out = new Uint8Array(outWidth * outHeight * 4);
32
+ for (let oy = 0; oy < outHeight; oy++) {
33
+ const y0 = Math.floor((oy * height) / outHeight);
34
+ const y1 = Math.max(y0 + 1, Math.floor(((oy + 1) * height) / outHeight));
35
+ for (let ox = 0; ox < outWidth; ox++) {
36
+ const x0 = Math.floor((ox * width) / outWidth);
37
+ const x1 = Math.max(x0 + 1, Math.floor(((ox + 1) * width) / outWidth));
38
+ const count = (x1 - x0) * (y1 - y0);
39
+ for (let c = 0; c < 4; c++) {
40
+ let sum = 0;
41
+ for (let y = y0; y < y1; y++) {
42
+ for (let x = x0; x < x1; x++)
43
+ sum += rgba[(y * width + x) * 4 + c] ?? 0;
44
+ }
45
+ out[(oy * outWidth + ox) * 4 + c] = Math.round(sum / count);
46
+ }
47
+ }
48
+ }
49
+ return out;
50
+ }
package/dist/drawKey.d.ts CHANGED
@@ -16,6 +16,13 @@ import type { DrftMaterial } from '@driftengine/drft';
16
16
  export interface DrawSurfaceOverride {
17
17
  readonly opacity?: number;
18
18
  readonly reflectivity?: number;
19
+ /**
20
+ * The scale over the ORM map's metallic channel, in place of the material's. For a map whose
21
+ * channel says metal where the surface is not: a bought courtyard's stone carried up to 0.37 on
22
+ * its clean stone, and each patch in shade swapped its diffuse light for a reflection of a dark
23
+ * gallery and read as a black blotch. 0 says dielectric.
24
+ */
25
+ readonly metallicScale?: number;
19
26
  }
20
27
  /**
21
28
  * The fields a merged group carries, resolved from a material and its override.
@@ -35,6 +42,11 @@ export interface DrawGrouping {
35
42
  readonly metallicScale: number;
36
43
  readonly occlusionStrength: number;
37
44
  readonly cutout: number;
45
+ /** Whether the texture's alpha is coverage, so the part is drawn blended whatever its opacity. */
46
+ readonly blend: boolean;
47
+ /** Whether both faces are drawn and lit. A merge of a two-sided part and a one-sided one would cull
48
+ * one of them or unhide the back of the other, so it is part of the key. */
49
+ readonly doubleSided: boolean;
38
50
  }
39
51
  /**
40
52
  * Resolve what a part will actually be drawn with.
@@ -44,7 +56,7 @@ export interface DrawGrouping {
44
56
  * 0, which is the identity that lets a file saying nothing about a property draw the same as one
45
57
  * that did not have the property to say.
46
58
  *
47
- * The override wins over the material for the two fields it may carry, because a consumer dressing
59
+ * The override wins over the material for the three fields it may carry, because a consumer dressing
48
60
  * a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
49
61
  * a fact about the *image* a material wears, so nothing outside the file has anything to say
50
62
  * about it.
package/dist/drawKey.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * 0, which is the identity that lets a file saying nothing about a property draw the same as one
7
7
  * that did not have the property to say.
8
8
  *
9
- * The override wins over the material for the two fields it may carry, because a consumer dressing
9
+ * The override wins over the material for the three fields it may carry, because a consumer dressing
10
10
  * a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
11
11
  * a fact about the *image* a material wears, so nothing outside the file has anything to say
12
12
  * about it.
@@ -20,9 +20,11 @@ export function resolveDrawGrouping(material, override) {
20
20
  opacity: override?.opacity ?? material?.opacity ?? 1,
21
21
  reflectivity: override?.reflectivity ?? material?.reflectivity ?? 0,
22
22
  roughnessScale: material?.roughnessScale ?? 1,
23
- metallicScale: material?.metallicScale ?? 1,
23
+ metallicScale: override?.metallicScale ?? material?.metallicScale ?? 1,
24
24
  occlusionStrength: material?.occlusionStrength ?? 0,
25
25
  cutout: material?.cutout ?? 0,
26
+ blend: material?.blend === true,
27
+ doubleSided: material?.doubleSided === true,
26
28
  };
27
29
  }
28
30
  /**
@@ -38,5 +40,5 @@ export function resolveDrawGrouping(material, override) {
38
40
  */
39
41
  export function drawKeyOf(material, override) {
40
42
  const g = resolveDrawGrouping(material, override);
41
- return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}`;
43
+ return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}:${g.blend ? 1 : 0}:${g.doubleSided ? 1 : 0}`;
42
44
  }