@driftengine/assets 4.3.0 → 4.4.1

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,395 @@
1
+ /**
2
+ * A textured mesh brought down to the triangles its shape needs, at bake time, within a distance.
3
+ *
4
+ * **A bought scene is modelled for a renderer that is not in a hurry.** A courtyard measured here
5
+ * holds two million triangles of curtain, five million of ivy leaf and fifteen hundred a candle: at
6
+ * 4K a triangle of that cloth covers two pixels, and a GPU shades whole 2 × 2 quads, so most of the
7
+ * work is shading pixels nobody sees, in every shadow pass and every probe face as well as the frame.
8
+ *
9
+ * **Quadric error, half-edge collapses, and a hard bound.** Each vertex carries the planes of the
10
+ * faces around it, and a collapse is costed by the mean squared distance the moving vertex's planes
11
+ * are from where it lands, so a flat stretch goes first and a fold holds (Garland and Heckbert). The
12
+ * vertex moves onto a neighbour rather than to an optimal point, so every vertex that is left is one
13
+ * the author placed, with the normal, uv, colour and tangent they gave it: nothing is interpolated,
14
+ * and a texture stays where it was painted. Nothing collapses once the cheapest collapse would move
15
+ * the surface further than `maxError`.
16
+ *
17
+ * **What is held.** A vertex that shares its place with another is on a uv seam or a hard edge, and
18
+ * moving either copy opens the seam, so both stay. An open border simplifies only along itself,
19
+ * costed by planes standing on its edges, so a leaf keeps its outline to the same bound. A collapse
20
+ * that turns a face more than about seventy degrees, folds the texture over, stretches it more than
21
+ * half again, or joins two vertices whose normals part by more than twenty-five degrees is refused,
22
+ * and so is one after which a vertex taken in, by it or any collapse before, no longer finds its own
23
+ * uv on the surface that is left (`collapseMeasures.ts` measures both). A mesh with joints or morph
24
+ * targets comes back as it went in: a vertex there is a promise to a rig.
25
+ *
26
+ * **What it gives up** is a better placed vertex at the same count, which a full-edge collapse to
27
+ * the quadric's minimum would find, at the price of interpolating every attribute. What would make
28
+ * it wrong is a model whose detail is in its geometry rather than its maps at the scale of the bound.
29
+ */
30
+ import type { MeshData } from '@driftengine/drft';
31
+ import { keepVertices } from '../weld.ts';
32
+ import { CollapseHeap } from './collapseHeap.ts';
33
+ import {
34
+ addBorderPlanes,
35
+ addFacePlanes,
36
+ edgeKey,
37
+ quadricAt,
38
+ triangleNormal,
39
+ uvArea,
40
+ uvMiss,
41
+ } from './collapseMeasures.ts';
42
+
43
+ export interface SimplifyOptions {
44
+ /** The furthest the surface may move, in the mesh's units: metres, for a baked scene. */
45
+ readonly maxError: number;
46
+ /**
47
+ * The furthest a texture may slide under the surface, in uv units: a texel of a 1,024 map by
48
+ * default. Checked at every vertex a collapse has removed, so it cannot accumulate.
49
+ */
50
+ readonly maxUvError?: number;
51
+ /** Stop once this share of the triangles is left, whatever the error. 0 by default. */
52
+ readonly minRatio?: number;
53
+ }
54
+
55
+ /** The most a collapse may turn a face: cos 70°. */
56
+ const MIN_FACE_TURN = 0.34;
57
+ /** The widest two shading normals a collapse may join: cos 25°. */
58
+ const MIN_NORMAL_AGREEMENT = 0.906;
59
+ /** How far a face's texture may stretch or shrink, as a ratio of its uv area to its area. */
60
+ const MAX_UV_STRETCH = 1.5;
61
+ /** A texel of a 1,024 map. */
62
+ const DEFAULT_MAX_UV_ERROR = 1 / 1024;
63
+
64
+ export function simplifyMesh(mesh: MeshData, options: SimplifyOptions): MeshData {
65
+ if (mesh.joints !== undefined || mesh.weights !== undefined || mesh.morphTargets !== undefined) {
66
+ return mesh;
67
+ }
68
+ const faceCount = mesh.indices.length / 3;
69
+ const vertexCount = mesh.positions.length / 3;
70
+ if (faceCount === 0) return mesh;
71
+ const s = new Simplifier(mesh, options.maxError, options.maxUvError ?? DEFAULT_MAX_UV_ERROR);
72
+ const floor = Math.ceil(faceCount * Math.max(0, Math.min(1, options.minRatio ?? 0)));
73
+ s.run(floor);
74
+ if (s.alive === faceCount) return mesh;
75
+ return s.output(vertexCount);
76
+ }
77
+
78
+ class Simplifier {
79
+ private readonly mesh: MeshData;
80
+ private readonly maxCost: number;
81
+ private readonly p: Float64Array;
82
+ private readonly faces: Uint32Array;
83
+ private readonly faceAlive: Uint8Array;
84
+ private readonly facesOf: number[][];
85
+ private readonly quadric: Float64Array;
86
+ private readonly area: Float64Array;
87
+ private readonly locked: Uint8Array;
88
+ private readonly removed: Uint8Array;
89
+ private readonly version: Uint32Array;
90
+ /** The vertices each survivor has taken in, whose painted uvs every later collapse must keep. */
91
+ private readonly absorbed: number[][];
92
+ private readonly maxUvError: number;
93
+ private readonly heap = new CollapseHeap();
94
+ alive: number;
95
+
96
+ constructor(mesh: MeshData, maxError: number, maxUvError: number) {
97
+ this.mesh = mesh;
98
+ this.maxCost = maxError * maxError;
99
+ this.maxUvError = maxUvError;
100
+ const v = mesh.positions.length / 3;
101
+ this.p = Float64Array.from(mesh.positions);
102
+ this.faces = Uint32Array.from(mesh.indices);
103
+ const f = this.faces.length / 3;
104
+ this.alive = f;
105
+ this.faceAlive = new Uint8Array(f).fill(1);
106
+ this.facesOf = Array.from({ length: v }, () => []);
107
+ this.quadric = new Float64Array(v * 10);
108
+ this.area = new Float64Array(v);
109
+ this.locked = new Uint8Array(v);
110
+ this.removed = new Uint8Array(v);
111
+ this.version = new Uint32Array(v);
112
+ this.absorbed = Array.from({ length: v }, () => []);
113
+ for (let face = 0; face < f; face++) {
114
+ for (let k = 0; k < 3; k++)
115
+ (this.facesOf[this.faces[face * 3 + k] as number] as number[]).push(face);
116
+ }
117
+ const edges = countEdges(this.faces, v);
118
+ this.lockShared(edges);
119
+ addFacePlanes(this.p, this.faces, this.quadric, this.area);
120
+ addBorderPlanes(this.p, this.faces, edges, this.quadric);
121
+ for (let u = 0; u < v; u++) this.consider(u);
122
+ }
123
+
124
+ /** Every vertex that shares its place with another, and both ends of an edge three faces share. */
125
+ private lockShared(edges: ReadonlyMap<number, number>): void {
126
+ const v = this.p.length / 3;
127
+ const bits = new Float32Array(
128
+ this.mesh.positions.buffer,
129
+ this.mesh.positions.byteOffset,
130
+ v * 3,
131
+ );
132
+ const words = new Uint32Array(bits.buffer, bits.byteOffset, v * 3);
133
+ const first = new Map<string, number>();
134
+ for (let u = 0; u < v; u++) {
135
+ const key = `${words[u * 3]},${words[u * 3 + 1]},${words[u * 3 + 2]}`;
136
+ const seen = first.get(key);
137
+ if (seen === undefined) first.set(key, u);
138
+ else {
139
+ this.locked[u] = 1;
140
+ this.locked[seen] = 1;
141
+ }
142
+ }
143
+ for (const [key, count] of edges) {
144
+ if (count <= 2) continue;
145
+ this.locked[Math.floor(key / v)] = 1;
146
+ this.locked[key % v] = 1;
147
+ }
148
+ }
149
+
150
+ /** The alive faces around `u`, in order, without the dead ones it still lists. */
151
+ private around(u: number): number[] {
152
+ const list = this.facesOf[u] as number[];
153
+ let write = 0;
154
+ for (let i = 0; i < list.length; i++) {
155
+ const face = list[i] as number;
156
+ if (this.faceAlive[face] === 1 && this.hasVertex(face, u)) list[write++] = face;
157
+ }
158
+ list.length = write;
159
+ return list;
160
+ }
161
+
162
+ private hasVertex(face: number, u: number): boolean {
163
+ return (
164
+ this.faces[face * 3] === u || this.faces[face * 3 + 1] === u || this.faces[face * 3 + 2] === u
165
+ );
166
+ }
167
+
168
+ /** How many alive faces hold the edge `a`–`b`. */
169
+ private edgeFaces(a: number, b: number): number {
170
+ let n = 0;
171
+ for (const face of this.around(a)) if (this.hasVertex(face, b)) n++;
172
+ return n;
173
+ }
174
+
175
+ /** The best collapse of `u` onto a neighbour, pushed on the heap under `u`'s current version. */
176
+ private consider(u: number): void {
177
+ if (this.removed[u] === 1 || this.locked[u] === 1) return;
178
+ const faces = this.around(u);
179
+ if (faces.length === 0) return;
180
+ /* A border vertex moves only along its border, and only if it has exactly two border edges. */
181
+ const neighbours: number[] = [];
182
+ const borderNeighbours: number[] = [];
183
+ for (const face of faces) {
184
+ for (let k = 0; k < 3; k++) {
185
+ const w = this.faces[face * 3 + k] as number;
186
+ if (w === u || neighbours.includes(w)) continue;
187
+ neighbours.push(w);
188
+ if (this.edgeFaces(u, w) === 1) borderNeighbours.push(w);
189
+ }
190
+ }
191
+ if (borderNeighbours.length !== 0 && borderNeighbours.length !== 2) return;
192
+ const candidates = borderNeighbours.length === 2 ? borderNeighbours : neighbours;
193
+ let best = -1;
194
+ let bestCost = Infinity;
195
+ for (const v of candidates) {
196
+ const cost = this.cost(u, v);
197
+ if (cost >= bestCost || cost > this.maxCost) continue;
198
+ if (!this.valid(u, v, faces)) continue;
199
+ best = v;
200
+ bestCost = cost;
201
+ }
202
+ if (best >= 0) this.heap.push(bestCost, u, best, this.version[u] as number);
203
+ }
204
+
205
+ /** The mean squared distance from both vertices' planes to where `u` lands, at `v`. */
206
+ private cost(u: number, v: number): number {
207
+ const x = this.p[v * 3]!;
208
+ const y = this.p[v * 3 + 1]!;
209
+ const z = this.p[v * 3 + 2]!;
210
+ const sum = quadricAt(this.quadric, u, x, y, z) + quadricAt(this.quadric, v, x, y, z);
211
+ return Math.max(0, sum) / Math.max(this.area[u]! + this.area[v]!, 1e-30);
212
+ }
213
+
214
+ private valid(u: number, v: number, faces: readonly number[]): boolean {
215
+ if (this.removed[v] === 1) return false;
216
+ const normals = this.mesh.normals;
217
+ const agree =
218
+ normals[u * 3]! * normals[v * 3]! +
219
+ normals[u * 3 + 1]! * normals[v * 3 + 1]! +
220
+ normals[u * 3 + 2]! * normals[v * 3 + 2]!;
221
+ if (agree < MIN_NORMAL_AGREEMENT) return false;
222
+ if (!this.linkHolds(u, v, faces)) return false;
223
+ const before = [0, 0, 0];
224
+ const after = [0, 0, 0];
225
+ const uvs = this.mesh.uvs;
226
+ for (const face of faces) {
227
+ if (this.hasVertex(face, v)) continue;
228
+ const a = this.faces[face * 3] as number;
229
+ const b = this.faces[face * 3 + 1] as number;
230
+ const c = this.faces[face * 3 + 2] as number;
231
+ const areaBefore = triangleNormal(this.p, a, b, c, before);
232
+ const areaAfter = triangleNormal(
233
+ this.p,
234
+ a === u ? v : a,
235
+ b === u ? v : b,
236
+ c === u ? v : c,
237
+ after,
238
+ );
239
+ if (areaAfter <= areaBefore * 1e-6) return false;
240
+ const turn = before[0]! * after[0]! + before[1]! * after[1]! + before[2]! * after[2]!;
241
+ if (turn < MIN_FACE_TURN) return false;
242
+ if (uvs !== undefined && areaBefore > 0) {
243
+ const uvBefore = uvArea(uvs, a, b, c);
244
+ const uvAfter = uvArea(uvs, a === u ? v : a, b === u ? v : b, c === u ? v : c);
245
+ if (uvBefore !== 0 && Math.sign(uvBefore) !== Math.sign(uvAfter)) return false;
246
+ const stretch =
247
+ Math.abs(uvAfter) / areaAfter / Math.max(Math.abs(uvBefore) / areaBefore, 1e-30);
248
+ if (uvBefore !== 0 && (stretch > MAX_UV_STRETCH || stretch < 1 / MAX_UV_STRETCH))
249
+ return false;
250
+ }
251
+ }
252
+ return uvs === undefined || this.texturesHold(u, v, faces, uvs);
253
+ }
254
+
255
+ /**
256
+ * Whether every vertex the collapse leaves behind, `u` and all it and `v` have taken in, still finds
257
+ * its own uv on the surface that is left: interpolated by the face it lies over, within
258
+ * `maxUvError`. The per-face checks above see only corners, and a curved mapping can pass them
259
+ * collapse by collapse while the texture slides a long way; this is measured against what was
260
+ * painted, so it cannot accumulate.
261
+ */
262
+ private texturesHold(u: number, v: number, faces: readonly number[], uvs: Float32Array): boolean {
263
+ const after: number[] = [];
264
+ for (const face of faces) {
265
+ if (this.hasVertex(face, v)) continue;
266
+ for (let k = 0; k < 3; k++) {
267
+ const w = this.faces[face * 3 + k] as number;
268
+ after.push(w === u ? v : w);
269
+ }
270
+ }
271
+ for (const face of this.around(v)) {
272
+ if (this.hasVertex(face, u)) continue;
273
+ for (let k = 0; k < 3; k++) after.push(this.faces[face * 3 + k] as number);
274
+ }
275
+ if (after.length === 0) return false;
276
+ const samples = [u, ...(this.absorbed[u] as number[]), ...(this.absorbed[v] as number[])];
277
+ for (const sample of samples) {
278
+ if (uvMiss(this.p, uvs, after, sample) > this.maxUvError) return false;
279
+ }
280
+ return true;
281
+ }
282
+
283
+ /**
284
+ * The link condition: the neighbours `u` and `v` share are exactly the far corners of the faces
285
+ * they share. Otherwise the collapse pinches the surface into a fin or a non-manifold edge.
286
+ *
287
+ * No test here isolates it: every pinch built for one also folded a face, which the check before
288
+ * it refuses. It stays as the standard guard for a closed surface, where that need not hold.
289
+ */
290
+ private linkHolds(u: number, v: number, faces: readonly number[]): boolean {
291
+ const opposite: number[] = [];
292
+ const ofU: number[] = [];
293
+ for (const face of faces) {
294
+ const shared = this.hasVertex(face, v);
295
+ for (let k = 0; k < 3; k++) {
296
+ const w = this.faces[face * 3 + k] as number;
297
+ if (w === u || w === v) continue;
298
+ if (shared) opposite.push(w);
299
+ if (!ofU.includes(w)) ofU.push(w);
300
+ }
301
+ }
302
+ for (const face of this.around(v)) {
303
+ for (let k = 0; k < 3; k++) {
304
+ const w = this.faces[face * 3 + k] as number;
305
+ if (w === u || w === v) continue;
306
+ if (ofU.includes(w) && !opposite.includes(w)) return false;
307
+ }
308
+ }
309
+ return opposite.length > 0;
310
+ }
311
+
312
+ run(floor: number): void {
313
+ const heap = this.heap;
314
+ while (heap.size > 0 && this.alive > floor) {
315
+ const cost = heap.topCost();
316
+ if (cost > this.maxCost) break;
317
+ const { u, v, version } = heap.pop();
318
+ if (version !== this.version[u] || this.removed[u] === 1 || this.removed[v] === 1) continue;
319
+ const faces = this.around(u);
320
+ /* The world may have moved under a queued collapse; ask again before making it. */
321
+ if (!this.valid(u, v, faces)) {
322
+ this.version[u] = this.version[u]! + 1;
323
+ this.consider(u);
324
+ continue;
325
+ }
326
+ this.collapse(u, v, faces);
327
+ }
328
+ }
329
+
330
+ private collapse(u: number, v: number, faces: readonly number[]): void {
331
+ this.removed[u] = 1;
332
+ const toV = this.facesOf[v] as number[];
333
+ for (const face of faces) {
334
+ if (this.hasVertex(face, v)) {
335
+ this.faceAlive[face] = 0;
336
+ this.alive--;
337
+ continue;
338
+ }
339
+ for (let k = 0; k < 3; k++) if (this.faces[face * 3 + k] === u) this.faces[face * 3 + k] = v;
340
+ toV.push(face);
341
+ }
342
+ for (let k = 0; k < 10; k++)
343
+ this.quadric[v * 10 + k] = this.quadric[v * 10 + k]! + this.quadric[u * 10 + k]!;
344
+ this.area[v] = this.area[v]! + this.area[u]!;
345
+ (this.absorbed[v] as number[]).push(u, ...(this.absorbed[u] as number[]));
346
+ (this.absorbed[u] as number[]).length = 0;
347
+ (this.facesOf[u] as number[]).length = 0;
348
+ /* Everything whose best move could have changed: `v` and every vertex round it. */
349
+ const touched: number[] = [v];
350
+ for (const face of this.around(v)) {
351
+ for (let k = 0; k < 3; k++) {
352
+ const w = this.faces[face * 3 + k] as number;
353
+ if (!touched.includes(w)) touched.push(w);
354
+ }
355
+ }
356
+ for (const w of touched) {
357
+ this.version[w] = this.version[w]! + 1;
358
+ this.consider(w);
359
+ }
360
+ }
361
+
362
+ output(vertexCount: number): MeshData {
363
+ const keep = new Int32Array(vertexCount).fill(-1);
364
+ const indices = new Uint32Array(this.alive * 3);
365
+ let count = 0;
366
+ let at = 0;
367
+ const f = this.faces.length / 3;
368
+ for (let face = 0; face < f; face++) {
369
+ if (this.faceAlive[face] !== 1) continue;
370
+ for (let k = 0; k < 3; k++) {
371
+ const old = this.faces[face * 3 + k] as number;
372
+ if (keep[old] === -1) keep[old] = count++;
373
+ indices[at++] = keep[old] as number;
374
+ }
375
+ }
376
+ return keepVertices(this.mesh, keep, count, indices);
377
+ }
378
+ }
379
+
380
+ /** How many faces hold each edge, keyed by `edgeKey`. */
381
+ function countEdges(faces: Uint32Array, v: number): Map<number, number> {
382
+ const edges = new Map<number, number>();
383
+ const f = faces.length / 3;
384
+ for (let face = 0; face < f; face++) {
385
+ for (let k = 0; k < 3; k++) {
386
+ const key = edgeKey(
387
+ faces[face * 3 + k] as number,
388
+ faces[face * 3 + ((k + 1) % 3)] as number,
389
+ v,
390
+ );
391
+ edges.set(key, (edges.get(key) ?? 0) + 1);
392
+ }
393
+ }
394
+ return edges;
395
+ }
@@ -0,0 +1,102 @@
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
+ /**
27
+ * The walk's margin over the step. A surface through a grid point is the equality case of the
28
+ * rule, a point at zero beside one a step away, and float rounding decides it either way; a cube's
29
+ * faces land on grid points whenever its size is a whole number of voxels. A hundredth of a step
30
+ * settles it on the sealed side, and seals a gap only that much sooner.
31
+ */
32
+ const WALK_MARGIN = 1.01;
33
+
34
+ /**
35
+ * Each point's depth, from an unsigned `field` on a grid of `dims` points `step` apart, `x`
36
+ * fastest. Depth 0 is the air reachable from the grid's faces; odd depths are inside.
37
+ */
38
+ export function surfaceDepth(
39
+ field: Float32Array,
40
+ dims: readonly [number, number, number],
41
+ step: number,
42
+ ): Int32Array {
43
+ const [nx, ny, nz] = dims;
44
+ const count = field.length;
45
+ const depth = new Int32Array(count).fill(-1);
46
+ const queue = new Int32Array(count);
47
+ /** Blocked neighbours of this depth's walk, which seed the next, each listed once. */
48
+ let seeds = new Int32Array(count);
49
+ let spare = new Int32Array(count);
50
+ const listed = new Uint8Array(count);
51
+ let seedCount = 0;
52
+ for (let k = 0; k < nz; k++) {
53
+ for (let j = 0; j < ny; j++) {
54
+ for (let i = 0; i < nx; i++) {
55
+ if (i > 0 && j > 0 && k > 0 && i < nx - 1 && j < ny - 1 && k < nz - 1) continue;
56
+ seeds[seedCount++] = i + nx * (j + ny * k);
57
+ }
58
+ }
59
+ }
60
+ const plane = nx * ny;
61
+ const limit = step * WALK_MARGIN;
62
+ for (let current = 0; seedCount > 0; current++) {
63
+ let head = 0;
64
+ let tail = 0;
65
+ for (let s = 0; s < seedCount; s++) {
66
+ const seed = seeds[s] as number;
67
+ listed[seed] = 0;
68
+ if (depth[seed] !== -1) continue;
69
+ depth[seed] = current;
70
+ queue[tail++] = seed;
71
+ }
72
+ let nextCount = 0;
73
+ while (head < tail) {
74
+ const index = queue[head++] as number;
75
+ const here = Math.abs(field[index] as number);
76
+ const i = index % nx;
77
+ const j = Math.floor(index / nx) % ny;
78
+ const k = Math.floor(index / plane);
79
+ for (let n = 0; n < 6; n++) {
80
+ const axis = n >> 1;
81
+ const delta = (n & 1) === 0 ? -1 : 1;
82
+ const at = axis === 0 ? i : axis === 1 ? j : k;
83
+ const size = axis === 0 ? nx : axis === 1 ? ny : nz;
84
+ if (at + delta < 0 || at + delta >= size) continue;
85
+ const neighbour = index + delta * (axis === 0 ? 1 : axis === 1 ? nx : plane);
86
+ if (depth[neighbour] !== -1) continue;
87
+ if (here + Math.abs(field[neighbour] as number) > limit) {
88
+ depth[neighbour] = current;
89
+ queue[tail++] = neighbour;
90
+ } else if (listed[neighbour] === 0) {
91
+ listed[neighbour] = 1;
92
+ spare[nextCount++] = neighbour;
93
+ }
94
+ }
95
+ }
96
+ const used = seeds;
97
+ seeds = spare;
98
+ spare = used;
99
+ seedCount = nextCount;
100
+ }
101
+ return depth;
102
+ }
@@ -0,0 +1,61 @@
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
+
16
+ /** The size `width` by `height` is halved to until its longer side is at most `maxSide`. */
17
+ export function cappedSize(
18
+ width: number,
19
+ height: number,
20
+ maxSide: number,
21
+ ): { width: number; height: number } {
22
+ let w = width;
23
+ let h = height;
24
+ while (Math.max(w, h) > maxSide) {
25
+ w = Math.max(1, Math.floor(w / 2));
26
+ h = Math.max(1, Math.floor(h / 2));
27
+ }
28
+ return { width: w, height: h };
29
+ }
30
+
31
+ /**
32
+ * A box average from `width`×`height` down to `outWidth`×`outHeight`, four bytes a texel.
33
+ *
34
+ * Each output texel averages the source rectangle it covers, rounded to nearest.
35
+ */
36
+ export function downscaleRgba(
37
+ rgba: Uint8Array,
38
+ width: number,
39
+ height: number,
40
+ outWidth: number,
41
+ outHeight: number,
42
+ ): Uint8Array {
43
+ const out = new Uint8Array(outWidth * outHeight * 4);
44
+ for (let oy = 0; oy < outHeight; oy++) {
45
+ const y0 = Math.floor((oy * height) / outHeight);
46
+ const y1 = Math.max(y0 + 1, Math.floor(((oy + 1) * height) / outHeight));
47
+ for (let ox = 0; ox < outWidth; ox++) {
48
+ const x0 = Math.floor((ox * width) / outWidth);
49
+ const x1 = Math.max(x0 + 1, Math.floor(((ox + 1) * width) / outWidth));
50
+ const count = (x1 - x0) * (y1 - y0);
51
+ for (let c = 0; c < 4; c++) {
52
+ let sum = 0;
53
+ for (let y = y0; y < y1; y++) {
54
+ for (let x = x0; x < x1; x++) sum += rgba[(y * width + x) * 4 + c] ?? 0;
55
+ }
56
+ out[(oy * outWidth + ox) * 4 + c] = Math.round(sum / count);
57
+ }
58
+ }
59
+ }
60
+ return out;
61
+ }
package/src/drawKey.ts CHANGED
@@ -18,6 +18,13 @@ import type { DrftMaterial } from '@driftengine/drft';
18
18
  export interface DrawSurfaceOverride {
19
19
  readonly opacity?: number;
20
20
  readonly reflectivity?: number;
21
+ /**
22
+ * The scale over the ORM map's metallic channel, in place of the material's. For a map whose
23
+ * channel says metal where the surface is not: a bought courtyard's stone carried up to 0.37 on
24
+ * its clean stone, and each patch in shade swapped its diffuse light for a reflection of a dark
25
+ * gallery and read as a black blotch. 0 says dielectric.
26
+ */
27
+ readonly metallicScale?: number;
21
28
  }
22
29
 
23
30
  /**
@@ -38,6 +45,11 @@ export interface DrawGrouping {
38
45
  readonly metallicScale: number;
39
46
  readonly occlusionStrength: number;
40
47
  readonly cutout: number;
48
+ /** Whether the texture's alpha is coverage, so the part is drawn blended whatever its opacity. */
49
+ readonly blend: boolean;
50
+ /** Whether both faces are drawn and lit. A merge of a two-sided part and a one-sided one would cull
51
+ * one of them or unhide the back of the other, so it is part of the key. */
52
+ readonly doubleSided: boolean;
41
53
  }
42
54
 
43
55
  /**
@@ -48,7 +60,7 @@ export interface DrawGrouping {
48
60
  * 0, which is the identity that lets a file saying nothing about a property draw the same as one
49
61
  * that did not have the property to say.
50
62
  *
51
- * The override wins over the material for the two fields it may carry, because a consumer dressing
63
+ * The override wins over the material for the three fields it may carry, because a consumer dressing
52
64
  * a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
53
65
  * a fact about the *image* a material wears, so nothing outside the file has anything to say
54
66
  * about it.
@@ -65,9 +77,11 @@ export function resolveDrawGrouping(
65
77
  opacity: override?.opacity ?? material?.opacity ?? 1,
66
78
  reflectivity: override?.reflectivity ?? material?.reflectivity ?? 0,
67
79
  roughnessScale: material?.roughnessScale ?? 1,
68
- metallicScale: material?.metallicScale ?? 1,
80
+ metallicScale: override?.metallicScale ?? material?.metallicScale ?? 1,
69
81
  occlusionStrength: material?.occlusionStrength ?? 0,
70
82
  cutout: material?.cutout ?? 0,
83
+ blend: material?.blend === true,
84
+ doubleSided: material?.doubleSided === true,
71
85
  };
72
86
  }
73
87
 
@@ -87,5 +101,5 @@ export function drawKeyOf(
87
101
  override?: DrawSurfaceOverride,
88
102
  ): string {
89
103
  const g = resolveDrawGrouping(material, override);
90
- return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}`;
104
+ 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}`;
91
105
  }