@driftengine/assets 3.61.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +9 -0
- package/dist/assetPath.d.ts +51 -0
- package/dist/assetPath.js +59 -0
- package/dist/coarseLevel.d.ts +94 -0
- package/dist/coarseLevel.js +606 -0
- package/dist/dds.d.ts +41 -0
- package/dist/dds.js +425 -0
- package/dist/drawKey.d.ts +64 -0
- package/dist/drawKey.js +42 -0
- package/dist/drftLoader.d.ts +420 -0
- package/dist/drftLoader.js +996 -0
- package/dist/drftTextures.d.ts +87 -0
- package/dist/drftTextures.js +138 -0
- package/dist/fbx.d.ts +86 -0
- package/dist/fbx.js +792 -0
- package/dist/fbxHarness.d.ts +68 -0
- package/dist/fbxHarness.js +185 -0
- package/dist/fbxInflate.d.ts +60 -0
- package/dist/fbxInflate.js +204 -0
- package/dist/fbxMaterial.d.ts +87 -0
- package/dist/fbxMaterial.js +345 -0
- package/dist/fbxRig.d.ts +130 -0
- package/dist/fbxRig.js +601 -0
- package/dist/gltf.d.ts +242 -0
- package/dist/gltf.js +904 -0
- package/dist/gltfAccessor.d.ts +2 -0
- package/dist/gltfAccessor.js +106 -0
- package/dist/gltfSkin.d.ts +32 -0
- package/dist/gltfSkin.js +144 -0
- package/dist/iesPhotometry.d.ts +44 -0
- package/dist/iesPhotometry.js +94 -0
- package/dist/imageInfo.d.ts +31 -0
- package/dist/imageInfo.js +119 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +132 -0
- package/dist/kn5.d.ts +158 -0
- package/dist/kn5.js +557 -0
- package/dist/loadProgress.d.ts +118 -0
- package/dist/loadProgress.js +1 -0
- package/dist/localise.d.ts +35 -0
- package/dist/localise.js +253 -0
- package/dist/obj.d.ts +54 -0
- package/dist/obj.js +335 -0
- package/dist/orient.d.ts +104 -0
- package/dist/orient.js +241 -0
- package/dist/radianceHdr.d.ts +36 -0
- package/dist/radianceHdr.js +172 -0
- package/dist/readModel.d.ts +153 -0
- package/dist/readModel.js +386 -0
- package/dist/recognise.d.ts +38 -0
- package/dist/recognise.js +97 -0
- package/dist/stl.d.ts +22 -0
- package/dist/stl.js +131 -0
- package/dist/tangentOrder.d.ts +24 -0
- package/dist/tangentOrder.js +31 -0
- package/dist/threemf.d.ts +46 -0
- package/dist/threemf.js +254 -0
- package/dist/uploadBudget.d.ts +82 -0
- package/dist/uploadBudget.js +86 -0
- package/dist/usd.d.ts +49 -0
- package/dist/usd.js +542 -0
- package/dist/weld.d.ts +44 -0
- package/dist/weld.js +237 -0
- package/dist/zip.d.ts +35 -0
- package/dist/zip.js +110 -0
- package/package.json +61 -0
- package/src/assetPath.ts +79 -0
- package/src/coarseLevel.ts +720 -0
- package/src/dds.ts +507 -0
- package/src/drawKey.ts +91 -0
- package/src/drftLoader.ts +1197 -0
- package/src/drftTextures.ts +153 -0
- package/src/fbx.ts +934 -0
- package/src/fbxHarness.ts +215 -0
- package/src/fbxInflate.ts +258 -0
- package/src/fbxMaterial.ts +410 -0
- package/src/fbxRig.ts +652 -0
- package/src/gltf.ts +1161 -0
- package/src/gltfAccessor.ts +127 -0
- package/src/gltfSkin.ts +210 -0
- package/src/iesPhotometry.ts +136 -0
- package/src/imageInfo.ts +130 -0
- package/src/index.ts +162 -0
- package/src/kn5.ts +705 -0
- package/src/loadProgress.ts +131 -0
- package/src/localise.ts +275 -0
- package/src/obj.ts +386 -0
- package/src/orient.ts +262 -0
- package/src/radianceHdr.ts +195 -0
- package/src/readModel.ts +515 -0
- package/src/recognise.ts +124 -0
- package/src/stl.ts +147 -0
- package/src/tangentOrder.ts +32 -0
- package/src/threemf.ts +303 -0
- package/src/uploadBudget.ts +90 -0
- package/src/usd.ts +642 -0
- package/src/weld.ts +264 -0
- package/src/zip.ts +130 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One coarse mesh standing in for a whole asset, so a load can start with an outline.
|
|
3
|
+
*
|
|
4
|
+
* docs/FORMAT.md §4.6 argues for this and states the two things that make it worth building:
|
|
5
|
+
* a coarse whole model has to *exist as geometry* before it can be drawn, so it is baked
|
|
6
|
+
* rather than derived at load; and **level 0 for the whole asset is worth more than level 0
|
|
7
|
+
* for each of 187 parts**, because a single decimated body reads as a car while 187 coarse
|
|
8
|
+
* fragments read as a mess. So this takes every mesh and returns exactly one.
|
|
9
|
+
*
|
|
10
|
+
* **The surface of an occupancy grid, not a cluster of vertices.** The first version of this
|
|
11
|
+
* clustered vertices onto a grid and connected three clusters wherever a source triangle
|
|
12
|
+
* spanned three cells, which is the classic Rossignac-Borrel decimation and was wrong for
|
|
13
|
+
* this job in a way that only showed on screen: nothing stops the three clusters of a
|
|
14
|
+
* triangle from lying on *different surfaces* of the model, so a car whose seats and engine
|
|
15
|
+
* bay share cells with its shell grew triangles straight through the bodywork. It was
|
|
16
|
+
* reported, correctly, as white cliffs.
|
|
17
|
+
*
|
|
18
|
+
* This marks every cell any surface passes through, then emits the boundary between the
|
|
19
|
+
* occupied cells and the empty ones. That boundary is **closed by construction** and cannot
|
|
20
|
+
* contain a triangle joining two sides of the model, because it is the outside of a solid
|
|
21
|
+
* rather than a decimation of a surface. Interior geometry disappears into cells that are
|
|
22
|
+
* already occupied, which is exactly what should happen to it in an outline.
|
|
23
|
+
*
|
|
24
|
+
* The blocky result is then pulled onto the model: each corner of the boundary sits at the
|
|
25
|
+
* average of the real surface inside the cells around it. So a cube grid becomes a shape
|
|
26
|
+
* that follows the car, at the resolution the caller asked for.
|
|
27
|
+
*
|
|
28
|
+
* Offline work in the baker, so it may allocate freely. Nothing here runs per frame.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The grid this uses when nobody names a resolution, exported because two other places used
|
|
32
|
+
* to carry their own copy of the number and disagreed with it.
|
|
33
|
+
*
|
|
34
|
+
* The baker printed 32 and the browser's converter passed nothing, so one path wrote an
|
|
35
|
+
* outline at 32 cells and the other at 40, and the same model taken through the two arrived
|
|
36
|
+
* differently for no stated reason. A default that callers restate is a default in name only.
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_COARSE_CELLS = 40;
|
|
39
|
+
const DEFAULT_CELLS = DEFAULT_COARSE_CELLS;
|
|
40
|
+
const MIN_CELLS = 4;
|
|
41
|
+
const MAX_CELLS = 192;
|
|
42
|
+
const DEFAULT_INSET = 0.3;
|
|
43
|
+
/**
|
|
44
|
+
* How finely a triangle bigger than a cell is sampled when marking cells it passes through.
|
|
45
|
+
*
|
|
46
|
+
* Marking only the three corners is right for the overwhelming majority of a real model,
|
|
47
|
+
* whose triangles are centimetres across against a cell of twelve. It is wrong for the few
|
|
48
|
+
* that are not: a backdrop plane or a wall is two triangles the size of the asset, and a hull
|
|
49
|
+
* built from their corners alone would have a hole the shape of the wall. So a triangle is
|
|
50
|
+
* walked at a spacing below one cell, which costs nothing on the small ones because a spacing
|
|
51
|
+
* below one cell is one step.
|
|
52
|
+
*/
|
|
53
|
+
const MAX_SAMPLE_STEPS = 96;
|
|
54
|
+
/**
|
|
55
|
+
* How much the cells around a node must agree about which way the surface faces before the node
|
|
56
|
+
* is moved at all. See where it is used: it is what keeps a thin feature from sliding off itself.
|
|
57
|
+
*/
|
|
58
|
+
const DIRECTED = 0.5;
|
|
59
|
+
/**
|
|
60
|
+
* How closely a surface must face the direction of travel before its plane is honoured.
|
|
61
|
+
*
|
|
62
|
+
* A surface at more than about seventy degrees to it is one the node moves *along* rather than
|
|
63
|
+
* toward, and the arithmetic divides by that alignment, so at ninety degrees it asks for an
|
|
64
|
+
* infinite push to get behind something it can never get behind by moving this way.
|
|
65
|
+
*/
|
|
66
|
+
const MIN_ALIGN = 0.35;
|
|
67
|
+
function clamp(value, low, high) {
|
|
68
|
+
return value < low ? low : value > high ? high : value;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* What one cell of the grid knows about the surface inside it, as one flat accumulator.
|
|
72
|
+
*
|
|
73
|
+
* Position, normal, colour, emissive, then how many samples went in: `0..2`, `3..5`, `6..8`,
|
|
74
|
+
* `9`, `10`. One array rather than several because the cells are addressed by a slot and a
|
|
75
|
+
* second array indexed by the same slot is a second thing to keep in step.
|
|
76
|
+
*/
|
|
77
|
+
const CELL_STRIDE = 11;
|
|
78
|
+
const AT_POSITION = 0;
|
|
79
|
+
const AT_NORMAL = 3;
|
|
80
|
+
const AT_COLOUR = 6;
|
|
81
|
+
const AT_EMISSIVE = 9;
|
|
82
|
+
const AT_COUNT = 10;
|
|
83
|
+
/**
|
|
84
|
+
* Build the coarse level, or `null` when there is nothing to build one from.
|
|
85
|
+
*
|
|
86
|
+
* `null` rather than an empty mesh: an asset with no extent, or one whose geometry all falls
|
|
87
|
+
* inside a single cell, has no outline worth a chunk in the file, and every caller would
|
|
88
|
+
* otherwise have to check the vertex count to find that out.
|
|
89
|
+
*
|
|
90
|
+
* Whether the result is *worth writing* is a separate question with a separate answer:
|
|
91
|
+
* `isOutlineWorthWriting`.
|
|
92
|
+
*/
|
|
93
|
+
export function buildCoarseLevel(meshes, options = {}) {
|
|
94
|
+
const cells = clamp(Math.round(options.cells ?? DEFAULT_CELLS), MIN_CELLS, MAX_CELLS);
|
|
95
|
+
const inset = Math.max(0, options.inset ?? DEFAULT_INSET);
|
|
96
|
+
let minX = Infinity;
|
|
97
|
+
let minY = Infinity;
|
|
98
|
+
let minZ = Infinity;
|
|
99
|
+
let maxX = -Infinity;
|
|
100
|
+
let maxY = -Infinity;
|
|
101
|
+
let maxZ = -Infinity;
|
|
102
|
+
for (const mesh of meshes) {
|
|
103
|
+
for (let at = 0; at + 2 < mesh.positions.length; at += 3) {
|
|
104
|
+
const x = mesh.positions[at];
|
|
105
|
+
const y = mesh.positions[at + 1];
|
|
106
|
+
const z = mesh.positions[at + 2];
|
|
107
|
+
if (x < minX)
|
|
108
|
+
minX = x;
|
|
109
|
+
if (y < minY)
|
|
110
|
+
minY = y;
|
|
111
|
+
if (z < minZ)
|
|
112
|
+
minZ = z;
|
|
113
|
+
if (x > maxX)
|
|
114
|
+
maxX = x;
|
|
115
|
+
if (y > maxY)
|
|
116
|
+
maxY = y;
|
|
117
|
+
if (z > maxZ)
|
|
118
|
+
maxZ = z;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!Number.isFinite(minX))
|
|
122
|
+
return null;
|
|
123
|
+
const cell = Math.max(maxX - minX, maxY - minY, maxZ - minZ) / cells;
|
|
124
|
+
if (!(cell > 0))
|
|
125
|
+
return null;
|
|
126
|
+
/* At least one cell on every axis, so a flat asset has a grid rather than none. */
|
|
127
|
+
const nx = Math.max(1, Math.ceil((maxX - minX) / cell));
|
|
128
|
+
const ny = Math.max(1, Math.ceil((maxY - minY) / cell));
|
|
129
|
+
const nz = Math.max(1, Math.ceil((maxZ - minZ) / cell));
|
|
130
|
+
/**
|
|
131
|
+
* Which cells hold surface, and what that surface is: summed position, normal, colour and
|
|
132
|
+
* emissive, then the sample count, so a cell can report the average of what passed through it.
|
|
133
|
+
*/
|
|
134
|
+
const occupied = new Map();
|
|
135
|
+
const cellSums = [];
|
|
136
|
+
const ixOf = (x) => clamp(Math.floor((x - minX) / cell), 0, nx - 1);
|
|
137
|
+
const iyOf = (y) => clamp(Math.floor((y - minY) / cell), 0, ny - 1);
|
|
138
|
+
const izOf = (z) => clamp(Math.floor((z - minZ) / cell), 0, nz - 1);
|
|
139
|
+
const cellId = (ix, iy, iz) => (iz * ny + iy) * nx + ix;
|
|
140
|
+
/** Mark the cell a point falls in, and fold the surface there into it. */
|
|
141
|
+
const mark = (x, y, z, nxs, nys, nzs, r, g, b, emissive) => {
|
|
142
|
+
const id = cellId(ixOf(x), iyOf(y), izOf(z));
|
|
143
|
+
let slot = occupied.get(id);
|
|
144
|
+
if (slot === undefined) {
|
|
145
|
+
slot = cellSums.length;
|
|
146
|
+
occupied.set(id, slot);
|
|
147
|
+
for (let i = 0; i < CELL_STRIDE; i++)
|
|
148
|
+
cellSums.push(0);
|
|
149
|
+
}
|
|
150
|
+
cellSums[slot + AT_POSITION] = cellSums[slot + AT_POSITION] + x;
|
|
151
|
+
cellSums[slot + AT_POSITION + 1] = cellSums[slot + AT_POSITION + 1] + y;
|
|
152
|
+
cellSums[slot + AT_POSITION + 2] = cellSums[slot + AT_POSITION + 2] + z;
|
|
153
|
+
cellSums[slot + AT_NORMAL] = cellSums[slot + AT_NORMAL] + nxs;
|
|
154
|
+
cellSums[slot + AT_NORMAL + 1] = cellSums[slot + AT_NORMAL + 1] + nys;
|
|
155
|
+
cellSums[slot + AT_NORMAL + 2] = cellSums[slot + AT_NORMAL + 2] + nzs;
|
|
156
|
+
cellSums[slot + AT_COLOUR] = cellSums[slot + AT_COLOUR] + r;
|
|
157
|
+
cellSums[slot + AT_COLOUR + 1] = cellSums[slot + AT_COLOUR + 1] + g;
|
|
158
|
+
cellSums[slot + AT_COLOUR + 2] = cellSums[slot + AT_COLOUR + 2] + b;
|
|
159
|
+
cellSums[slot + AT_EMISSIVE] = cellSums[slot + AT_EMISSIVE] + emissive;
|
|
160
|
+
cellSums[slot + AT_COUNT] = cellSums[slot + AT_COUNT] + 1;
|
|
161
|
+
};
|
|
162
|
+
for (const mesh of meshes) {
|
|
163
|
+
const p = mesh.positions;
|
|
164
|
+
const n = mesh.normals;
|
|
165
|
+
const c = mesh.colors;
|
|
166
|
+
const e = mesh.emissive;
|
|
167
|
+
for (let at = 0; at + 2 < mesh.indices.length; at += 3) {
|
|
168
|
+
const i0 = mesh.indices[at];
|
|
169
|
+
const i1 = mesh.indices[at + 1];
|
|
170
|
+
const i2 = mesh.indices[at + 2];
|
|
171
|
+
const ax = p[i0 * 3], ay = p[i0 * 3 + 1], az = p[i0 * 3 + 2];
|
|
172
|
+
const bx = p[i1 * 3], by = p[i1 * 3 + 1], bz = p[i1 * 3 + 2];
|
|
173
|
+
const cx = p[i2 * 3], cy = p[i2 * 3 + 1], cz = p[i2 * 3 + 2];
|
|
174
|
+
/* One step for anything smaller than a cell, which is nearly everything. */
|
|
175
|
+
const longest = Math.max(Math.hypot(bx - ax, by - ay, bz - az), Math.hypot(cx - bx, cy - by, cz - bz), Math.hypot(ax - cx, ay - cy, az - cz));
|
|
176
|
+
const steps = clamp(Math.ceil(longest / cell), 1, MAX_SAMPLE_STEPS);
|
|
177
|
+
for (let i = 0; i <= steps; i++) {
|
|
178
|
+
for (let j = 0; i + j <= steps; j++) {
|
|
179
|
+
const u = i / steps;
|
|
180
|
+
const v = j / steps;
|
|
181
|
+
const w = 1 - u - v;
|
|
182
|
+
mark(ax * w + bx * u + cx * v, ay * w + by * u + cy * v, az * w + bz * u + cz * v, n[i0 * 3] * w + n[i1 * 3] * u + n[i2 * 3] * v, n[i0 * 3 + 1] * w +
|
|
183
|
+
n[i1 * 3 + 1] * u +
|
|
184
|
+
n[i2 * 3 + 1] * v, n[i0 * 3 + 2] * w +
|
|
185
|
+
n[i1 * 3 + 2] * u +
|
|
186
|
+
n[i2 * 3 + 2] * v, c[i0 * 3] * w + c[i1 * 3] * u + c[i2 * 3] * v, c[i0 * 3 + 1] * w +
|
|
187
|
+
c[i1 * 3 + 1] * u +
|
|
188
|
+
c[i2 * 3 + 1] * v, c[i0 * 3 + 2] * w +
|
|
189
|
+
c[i1 * 3 + 2] * u +
|
|
190
|
+
c[i2 * 3 + 2] * v, e[i0] * w + e[i1] * u + e[i2] * v);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (occupied.size === 0)
|
|
196
|
+
return null;
|
|
197
|
+
return buildBoundary({ occupied, cellSums, nx, ny, nz, cell, minX, minY, minZ, inset });
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Whether a built level is worth writing into a file as an outline.
|
|
201
|
+
*
|
|
202
|
+
* **A level of detail that is bigger than the model is not one.** The grid is a fixed number of
|
|
203
|
+
* cells across whatever it is given, so its cost follows the *shape* rather than the detail: four
|
|
204
|
+
* hand-written triangles come back as a 9,840-triangle shell, which is 308 KB of outline arriving
|
|
205
|
+
* ahead of a model measured in bytes. Everything an outline is for, being on screen before the
|
|
206
|
+
* geometry at a fraction of the download, is inverted.
|
|
207
|
+
*
|
|
208
|
+
* Found the day the default flipped from off to on, by a fixture. While an outline was asked for
|
|
209
|
+
* per asset by somebody who had looked, the case could not arise. A default has to hold for the
|
|
210
|
+
* assets nobody looks at, which is what makes it a default.
|
|
211
|
+
*
|
|
212
|
+
* **Separate from `buildCoarseLevel`, which stays a geometry function.** This is a budget, and a
|
|
213
|
+
* caller that wants the hull of a small mesh for some other purpose is entitled to it. It lives
|
|
214
|
+
* here rather than in the two callers because the baker and the browser's converter have to agree
|
|
215
|
+
* about what a file carries, and a rule stated twice is a rule until somebody edits one copy.
|
|
216
|
+
*
|
|
217
|
+
* Half rather than merely smaller: an outline that saves a few per cent is a chunk, an upload and
|
|
218
|
+
* a swap for no visible gain.
|
|
219
|
+
*/
|
|
220
|
+
export function isOutlineWorthWriting(level, meshes) {
|
|
221
|
+
let sourceTriangles = 0;
|
|
222
|
+
for (const mesh of meshes)
|
|
223
|
+
sourceTriangles += mesh.indices.length / 3;
|
|
224
|
+
return level.indices.length / 3 <= sourceTriangles / 2;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The surface between the occupied cells and the empty ones, pulled onto the model.
|
|
228
|
+
*
|
|
229
|
+
* Every quad is a face an occupied cell shares with a cell that holds nothing, wound so it
|
|
230
|
+
* faces out of the solid. Its four corners are grid nodes shared with the neighbouring faces,
|
|
231
|
+
* so the result is one welded, closed shell rather than a pile of squares — and because the
|
|
232
|
+
* corners then move to the average of the real surface around them, the shell follows the
|
|
233
|
+
* model instead of stepping around it.
|
|
234
|
+
*/
|
|
235
|
+
function buildBoundary(grid) {
|
|
236
|
+
const { occupied, cellSums, nx, ny, nz, cell, minX, minY, minZ, inset } = grid;
|
|
237
|
+
const cellId = (ix, iy, iz) => (iz * ny + iy) * nx + ix;
|
|
238
|
+
const holds = (ix, iy, iz) => ix >= 0 &&
|
|
239
|
+
iy >= 0 &&
|
|
240
|
+
iz >= 0 &&
|
|
241
|
+
ix < nx &&
|
|
242
|
+
iy < ny &&
|
|
243
|
+
iz < nz &&
|
|
244
|
+
occupied.has(cellId(ix, iy, iz));
|
|
245
|
+
/**
|
|
246
|
+
* Whether a point in space falls in a cell that holds surface.
|
|
247
|
+
*
|
|
248
|
+
* Unclamped, deliberately: clamping a point that is outside the grid into the nearest cell
|
|
249
|
+
* would report the asset as occupying places it does not, which is the opposite of what this
|
|
250
|
+
* is asked for.
|
|
251
|
+
*/
|
|
252
|
+
const holdsPoint = (x, y, z) => holds(Math.floor((x - minX) / cell), Math.floor((y - minY) / cell), Math.floor((z - minZ) / cell));
|
|
253
|
+
/** Node ids run over the grid's corners, one more than the cells on every axis. */
|
|
254
|
+
const nodeId = (ix, iy, iz) => (iz * (ny + 1) + iy) * (nx + 1) + ix;
|
|
255
|
+
const nodes = new Map();
|
|
256
|
+
const nodeAt = [];
|
|
257
|
+
const vertexOf = (ix, iy, iz) => {
|
|
258
|
+
const id = nodeId(ix, iy, iz);
|
|
259
|
+
let found = nodes.get(id);
|
|
260
|
+
if (found === undefined) {
|
|
261
|
+
found = nodeAt.length / 3;
|
|
262
|
+
nodes.set(id, found);
|
|
263
|
+
nodeAt.push(ix, iy, iz);
|
|
264
|
+
}
|
|
265
|
+
return found;
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* The six faces, each as the four corner offsets in the order that faces outward.
|
|
269
|
+
*
|
|
270
|
+
* Written out rather than derived, because a face wound the wrong way is invisible in the
|
|
271
|
+
* source and shows up as a hole in the hull from one side only.
|
|
272
|
+
*/
|
|
273
|
+
const FACES = [
|
|
274
|
+
[
|
|
275
|
+
1,
|
|
276
|
+
0,
|
|
277
|
+
0,
|
|
278
|
+
[
|
|
279
|
+
[1, 0, 0],
|
|
280
|
+
[1, 1, 0],
|
|
281
|
+
[1, 1, 1],
|
|
282
|
+
[1, 0, 1],
|
|
283
|
+
],
|
|
284
|
+
],
|
|
285
|
+
[
|
|
286
|
+
-1,
|
|
287
|
+
0,
|
|
288
|
+
0,
|
|
289
|
+
[
|
|
290
|
+
[0, 0, 0],
|
|
291
|
+
[0, 0, 1],
|
|
292
|
+
[0, 1, 1],
|
|
293
|
+
[0, 1, 0],
|
|
294
|
+
],
|
|
295
|
+
],
|
|
296
|
+
[
|
|
297
|
+
0,
|
|
298
|
+
1,
|
|
299
|
+
0,
|
|
300
|
+
[
|
|
301
|
+
[0, 1, 0],
|
|
302
|
+
[0, 1, 1],
|
|
303
|
+
[1, 1, 1],
|
|
304
|
+
[1, 1, 0],
|
|
305
|
+
],
|
|
306
|
+
],
|
|
307
|
+
[
|
|
308
|
+
0,
|
|
309
|
+
-1,
|
|
310
|
+
0,
|
|
311
|
+
[
|
|
312
|
+
[0, 0, 0],
|
|
313
|
+
[1, 0, 0],
|
|
314
|
+
[1, 0, 1],
|
|
315
|
+
[0, 0, 1],
|
|
316
|
+
],
|
|
317
|
+
],
|
|
318
|
+
[
|
|
319
|
+
0,
|
|
320
|
+
0,
|
|
321
|
+
1,
|
|
322
|
+
[
|
|
323
|
+
[0, 0, 1],
|
|
324
|
+
[1, 0, 1],
|
|
325
|
+
[1, 1, 1],
|
|
326
|
+
[0, 1, 1],
|
|
327
|
+
],
|
|
328
|
+
],
|
|
329
|
+
[
|
|
330
|
+
0,
|
|
331
|
+
0,
|
|
332
|
+
-1,
|
|
333
|
+
[
|
|
334
|
+
[0, 0, 0],
|
|
335
|
+
[0, 1, 0],
|
|
336
|
+
[1, 1, 0],
|
|
337
|
+
[1, 0, 0],
|
|
338
|
+
],
|
|
339
|
+
],
|
|
340
|
+
];
|
|
341
|
+
/**
|
|
342
|
+
* Which empty cells can be reached from outside the asset, found by flooding inward.
|
|
343
|
+
*
|
|
344
|
+
* **This is what keeps the outline to the outside of the model, and it halves it.** A car body
|
|
345
|
+
* is a shell, so the cells it occupies have empty space on both sides, and a boundary drawn
|
|
346
|
+
* against every empty neighbour produces two shells: the one a viewer sees and a second one
|
|
347
|
+
* lining the cabin. The inner one is invisible, doubles the triangle count, and where the
|
|
348
|
+
* shell is a single cell thick both sheets snap to the same surface and fight over the depth
|
|
349
|
+
* buffer, which reads as dark speckle across the model.
|
|
350
|
+
*
|
|
351
|
+
* A cavity is exactly an empty region the flood cannot reach, so this is the whole test.
|
|
352
|
+
*/
|
|
353
|
+
const outside = new Uint8Array(nx * ny * nz);
|
|
354
|
+
const queue = [];
|
|
355
|
+
for (let iz = 0; iz < nz; iz++) {
|
|
356
|
+
for (let iy = 0; iy < ny; iy++) {
|
|
357
|
+
for (let ix = 0; ix < nx; ix++) {
|
|
358
|
+
const onRim = ix === 0 || iy === 0 || iz === 0 || ix === nx - 1 || iy === ny - 1 || iz === nz - 1;
|
|
359
|
+
if (!onRim)
|
|
360
|
+
continue;
|
|
361
|
+
const id = cellId(ix, iy, iz);
|
|
362
|
+
if (occupied.has(id) || outside[id] === 1)
|
|
363
|
+
continue;
|
|
364
|
+
outside[id] = 1;
|
|
365
|
+
queue.push(ix, iy, iz);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const STEPS = [
|
|
370
|
+
[1, 0, 0],
|
|
371
|
+
[-1, 0, 0],
|
|
372
|
+
[0, 1, 0],
|
|
373
|
+
[0, -1, 0],
|
|
374
|
+
[0, 0, 1],
|
|
375
|
+
[0, 0, -1],
|
|
376
|
+
];
|
|
377
|
+
/* Breadth first over an explicit list rather than by recursion: a grid of a hundred thousand
|
|
378
|
+
cells would be a hundred thousand frames deep in the worst case. */
|
|
379
|
+
for (let head = 0; head + 2 < queue.length; head += 3) {
|
|
380
|
+
const ix = queue[head];
|
|
381
|
+
const iy = queue[head + 1];
|
|
382
|
+
const iz = queue[head + 2];
|
|
383
|
+
for (const [dx, dy, dz] of STEPS) {
|
|
384
|
+
const jx = ix + dx;
|
|
385
|
+
const jy = iy + dy;
|
|
386
|
+
const jz = iz + dz;
|
|
387
|
+
if (jx < 0 || jy < 0 || jz < 0 || jx >= nx || jy >= ny || jz >= nz)
|
|
388
|
+
continue;
|
|
389
|
+
const id = cellId(jx, jy, jz);
|
|
390
|
+
if (occupied.has(id) || outside[id] === 1)
|
|
391
|
+
continue;
|
|
392
|
+
outside[id] = 1;
|
|
393
|
+
queue.push(jx, jy, jz);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** Whether this neighbour is open air a viewer could be standing in. */
|
|
397
|
+
const open = (ix, iy, iz) => {
|
|
398
|
+
if (ix < 0 || iy < 0 || iz < 0 || ix >= nx || iy >= ny || iz >= nz)
|
|
399
|
+
return true;
|
|
400
|
+
const id = cellId(ix, iy, iz);
|
|
401
|
+
return !occupied.has(id) && outside[id] === 1;
|
|
402
|
+
};
|
|
403
|
+
const triangles = [];
|
|
404
|
+
for (const id of occupied.keys()) {
|
|
405
|
+
const ix = id % nx;
|
|
406
|
+
const iy = Math.floor(id / nx) % ny;
|
|
407
|
+
const iz = Math.floor(id / (nx * ny));
|
|
408
|
+
for (const [dx, dy, dz, corners] of FACES) {
|
|
409
|
+
if (!open(ix + dx, iy + dy, iz + dz))
|
|
410
|
+
continue;
|
|
411
|
+
const a = vertexOf(ix + corners[0]?.[0], iy + corners[0]?.[1], iz + corners[0]?.[2]);
|
|
412
|
+
const b = vertexOf(ix + corners[1]?.[0], iy + corners[1]?.[1], iz + corners[1]?.[2]);
|
|
413
|
+
const c = vertexOf(ix + corners[2]?.[0], iy + corners[2]?.[1], iz + corners[2]?.[2]);
|
|
414
|
+
const d = vertexOf(ix + corners[3]?.[0], iy + corners[3]?.[1], iz + corners[3]?.[2]);
|
|
415
|
+
triangles.push(a, b, c, a, c, d);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const count = nodeAt.length / 3;
|
|
419
|
+
if (count < 3 || triangles.length === 0)
|
|
420
|
+
return null;
|
|
421
|
+
const positions = new Float32Array(count * 3);
|
|
422
|
+
const normals = new Float32Array(count * 3);
|
|
423
|
+
const colors = new Float32Array(count * 3);
|
|
424
|
+
const emissive = new Float32Array(count);
|
|
425
|
+
for (let at = 0; at < count; at++) {
|
|
426
|
+
const ix = nodeAt[at * 3];
|
|
427
|
+
const iy = nodeAt[at * 3 + 1];
|
|
428
|
+
const iz = nodeAt[at * 3 + 2];
|
|
429
|
+
/*
|
|
430
|
+
* The average of the real surface in the cells touching this node, which is what turns a
|
|
431
|
+
* staircase of cubes into the shape of the thing. Up to eight cells meet at a node and only
|
|
432
|
+
* the occupied ones have anything to say.
|
|
433
|
+
*/
|
|
434
|
+
let px = 0, py = 0, pz = 0;
|
|
435
|
+
let nxs = 0, nys = 0, nzs = 0;
|
|
436
|
+
let r = 0, g = 0, b = 0, glow = 0;
|
|
437
|
+
let found = 0;
|
|
438
|
+
/** The planes this node has to end up behind: one per occupied cell around it. */
|
|
439
|
+
const planes = [];
|
|
440
|
+
for (let ox = -1; ox <= 0; ox++) {
|
|
441
|
+
for (let oy = -1; oy <= 0; oy++) {
|
|
442
|
+
for (let oz = -1; oz <= 0; oz++) {
|
|
443
|
+
const cx = ix + ox;
|
|
444
|
+
const cy = iy + oy;
|
|
445
|
+
const cz = iz + oz;
|
|
446
|
+
if (cx < 0 || cy < 0 || cz < 0 || cx >= nx || cy >= ny || cz >= nz)
|
|
447
|
+
continue;
|
|
448
|
+
const slot = occupied.get(cellId(cx, cy, cz));
|
|
449
|
+
if (slot === undefined)
|
|
450
|
+
continue;
|
|
451
|
+
const samples = cellSums[slot + AT_COUNT];
|
|
452
|
+
if (samples <= 0)
|
|
453
|
+
continue;
|
|
454
|
+
const qx = cellSums[slot + AT_POSITION] / samples;
|
|
455
|
+
const qy = cellSums[slot + AT_POSITION + 1] / samples;
|
|
456
|
+
const qz = cellSums[slot + AT_POSITION + 2] / samples;
|
|
457
|
+
px += qx;
|
|
458
|
+
py += qy;
|
|
459
|
+
pz += qz;
|
|
460
|
+
/*
|
|
461
|
+
* Each cell's facing, as a unit vector, so the node's own facing is an average of
|
|
462
|
+
* directions rather than of magnitudes. A cell whose samples cancel has no facing to
|
|
463
|
+
* offer and is left out of both the average and the planes: it is a fold or a doubled
|
|
464
|
+
* face, and it has no side for anything to be behind.
|
|
465
|
+
*/
|
|
466
|
+
let mx = cellSums[slot + AT_NORMAL];
|
|
467
|
+
let my = cellSums[slot + AT_NORMAL + 1];
|
|
468
|
+
let mz = cellSums[slot + AT_NORMAL + 2];
|
|
469
|
+
const facing = Math.hypot(mx, my, mz);
|
|
470
|
+
if (facing > 1e-6) {
|
|
471
|
+
mx /= facing;
|
|
472
|
+
my /= facing;
|
|
473
|
+
mz /= facing;
|
|
474
|
+
nxs += mx;
|
|
475
|
+
nys += my;
|
|
476
|
+
nzs += mz;
|
|
477
|
+
planes.push(qx, qy, qz, mx, my, mz);
|
|
478
|
+
}
|
|
479
|
+
r += cellSums[slot + AT_COLOUR] / samples;
|
|
480
|
+
g += cellSums[slot + AT_COLOUR + 1] / samples;
|
|
481
|
+
b += cellSums[slot + AT_COLOUR + 2] / samples;
|
|
482
|
+
glow += cellSums[slot + AT_EMISSIVE] / samples;
|
|
483
|
+
found++;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (found === 0) {
|
|
488
|
+
/* Cannot happen for a node of a boundary face, and if it ever does, the node's own grid
|
|
489
|
+
position is the honest answer rather than a division by zero. */
|
|
490
|
+
positions[at * 3] = minX + ix * cell;
|
|
491
|
+
positions[at * 3 + 1] = minY + iy * cell;
|
|
492
|
+
positions[at * 3 + 2] = minZ + iz * cell;
|
|
493
|
+
normals[at * 3 + 1] = 1;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
px /= found;
|
|
497
|
+
py /= found;
|
|
498
|
+
pz /= found;
|
|
499
|
+
/*
|
|
500
|
+
* How much the cells around this node agree about which way the surface faces, which is the
|
|
501
|
+
* sum of unit facings over their count: one when they all agree, zero when they cancel.
|
|
502
|
+
*
|
|
503
|
+
* **A node that has no agreed facing is left exactly where it is**, and that is what keeps a
|
|
504
|
+
* thin feature intact. A wing mirror or a spoiler thinner than a cell has both of its faces
|
|
505
|
+
* inside the same cells, so there is no side to be behind, and pushing along whichever
|
|
506
|
+
* direction the arithmetic happened to produce would slide the outline off the part it
|
|
507
|
+
* stands for.
|
|
508
|
+
*/
|
|
509
|
+
const agreement = Math.hypot(nxs, nys, nzs) / found;
|
|
510
|
+
const length = Math.hypot(nxs, nys, nzs);
|
|
511
|
+
if (length > 1e-6) {
|
|
512
|
+
nxs /= length;
|
|
513
|
+
nys /= length;
|
|
514
|
+
nzs /= length;
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
nxs = 0;
|
|
518
|
+
nys = 1;
|
|
519
|
+
nzs = 0;
|
|
520
|
+
}
|
|
521
|
+
/*
|
|
522
|
+
* **How far back this node has to sit, measured against the surfaces around it.**
|
|
523
|
+
*
|
|
524
|
+
* The outline stays on screen while the real parts land on top of it, so anywhere it ends up
|
|
525
|
+
* in *front* of the surface it stands for, it shows through the finished model. A constant
|
|
526
|
+
* cannot do this job: a smooth panel needs almost nothing and a concave crease needs most of
|
|
527
|
+
* a cell, because a node there averages two surfaces and lands in the air between them.
|
|
528
|
+
*
|
|
529
|
+
* So each surrounding cell states a plane and the node has to end up behind all of them. For
|
|
530
|
+
* a plane at `q` facing `m`, a node at `c - t·n` is behind it while
|
|
531
|
+
* `dot(c - q, m) <= t·dot(n, m)`, so the cell demands `t >= dot(c - q, m) / dot(n, m)` and
|
|
532
|
+
* the node takes the largest demand made of it. A cell facing nearly across the direction of
|
|
533
|
+
* travel is skipped rather than honoured: dividing by an alignment approaching zero asks for
|
|
534
|
+
* an unbounded push to clear a surface this node is sliding along rather than facing.
|
|
535
|
+
*/
|
|
536
|
+
let behind = 0;
|
|
537
|
+
if (agreement >= DIRECTED) {
|
|
538
|
+
for (let plane = 0; plane + 5 < planes.length; plane += 6) {
|
|
539
|
+
const mx = planes[plane + 3];
|
|
540
|
+
const my = planes[plane + 4];
|
|
541
|
+
const mz = planes[plane + 5];
|
|
542
|
+
const align = nxs * mx + nys * my + nzs * mz;
|
|
543
|
+
if (align < MIN_ALIGN)
|
|
544
|
+
continue;
|
|
545
|
+
const overshoot = (px - planes[plane]) * mx +
|
|
546
|
+
(py - planes[plane + 1]) * my +
|
|
547
|
+
(pz - planes[plane + 2]) * mz;
|
|
548
|
+
if (overshoot <= 0)
|
|
549
|
+
continue;
|
|
550
|
+
const need = overshoot / align;
|
|
551
|
+
if (need > behind)
|
|
552
|
+
behind = need;
|
|
553
|
+
}
|
|
554
|
+
/* Capped at a cell: a larger demand comes from a fold narrower than the grid, where no
|
|
555
|
+
position clears every surface at once, and honouring it would tear the hull. */
|
|
556
|
+
behind = Math.min(behind, cell);
|
|
557
|
+
/*
|
|
558
|
+
* **And bounded by the cells the asset actually occupies, which is the half that had to be
|
|
559
|
+
* measured to be found.** A demand comes from whichever surfaces happen to sit in the cells
|
|
560
|
+
* around a node, and a cell whose only surface is an interior lining facing downward
|
|
561
|
+
* demands a push *upward* — so the outline ended up standing 0.14 m above the roof of a car
|
|
562
|
+
* 1.49 m tall, which is 0.92 of a cell and was visible as a shape floating over the model.
|
|
563
|
+
*
|
|
564
|
+
* A push that leaves the occupied region is not a push behind anything, so it is halved
|
|
565
|
+
* until it lands inside or given up. The margin below is applied regardless, because at 0.3
|
|
566
|
+
* of a cell it is small enough to be worth the certainty of never coinciding with the
|
|
567
|
+
* surface it hides under.
|
|
568
|
+
*/
|
|
569
|
+
for (let tries = 0; tries < 4 && behind > 0; tries++) {
|
|
570
|
+
if (holdsPoint(px - nxs * behind, py - nys * behind, pz - nzs * behind))
|
|
571
|
+
break;
|
|
572
|
+
behind = tries === 3 ? 0 : behind / 2;
|
|
573
|
+
}
|
|
574
|
+
/*
|
|
575
|
+
* The **demand** is bounded by occupancy and the margin is not, which is a deliberate split
|
|
576
|
+
* and the only one that serves both shapes this has to handle. A car is a shell inside a
|
|
577
|
+
* volume of parts, where a large push that leaves the occupied cells is going the wrong way.
|
|
578
|
+
* A bare floor meeting a bare wall is a shell and nothing else, where the push that clears
|
|
579
|
+
* both surfaces *has* to leave the two cells holding them — there is no solid to stay in.
|
|
580
|
+
* Bounding the demand fixes the first and bounding the margin as well would break the
|
|
581
|
+
* second, so the margin stays free at 0.3 of a cell.
|
|
582
|
+
*/
|
|
583
|
+
behind += inset * cell;
|
|
584
|
+
}
|
|
585
|
+
positions[at * 3] = px - nxs * behind;
|
|
586
|
+
positions[at * 3 + 1] = py - nys * behind;
|
|
587
|
+
positions[at * 3 + 2] = pz - nzs * behind;
|
|
588
|
+
normals[at * 3] = nxs;
|
|
589
|
+
normals[at * 3 + 1] = nys;
|
|
590
|
+
normals[at * 3 + 2] = nzs;
|
|
591
|
+
colors[at * 3] = r / found;
|
|
592
|
+
colors[at * 3 + 1] = g / found;
|
|
593
|
+
colors[at * 3 + 2] = b / found;
|
|
594
|
+
emissive[at] = glow / found;
|
|
595
|
+
}
|
|
596
|
+
/*
|
|
597
|
+
* The four mandatory attributes and nothing else, deliberately.
|
|
598
|
+
*
|
|
599
|
+
* Colour is carried because a grey outline of a painted model is a worse first frame than no
|
|
600
|
+
* outline at all, and emissive because a lamp that glows in the model should glow in its
|
|
601
|
+
* outline. Everything optional is left absent, so the engine's own constants apply and the
|
|
602
|
+
* chunk stays small: a level of detail is a *silhouette*, and spending bytes on the roughness
|
|
603
|
+
* of a surface nobody will look at for half a second is the opposite of the point.
|
|
604
|
+
*/
|
|
605
|
+
return { positions, normals, colors, emissive, indices: new Uint32Array(triangles) };
|
|
606
|
+
}
|
package/dist/dds.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DDS surfaces to RGBA: BC1, BC2, BC3 and BC5 in either header spelling, and the uncompressed
|
|
3
|
+
* layouts, by the masks their own header carries.
|
|
4
|
+
*
|
|
5
|
+
* **It exists because a third of a shipped vehicle's textures are in it.** The car this was built
|
|
6
|
+
* against carries 81 textures and 34 of them are DDS — 25 DXT5 and 9 DXT1 — which are its paint,
|
|
7
|
+
* its interior and most of its normal maps. `imageInfo.ts` identifies PNG, JPEG and WEBP, so
|
|
8
|
+
* without this the model imports correctly shaped and visibly half-painted.
|
|
9
|
+
*
|
|
10
|
+
* **It runs in the baker and never in a frame.** RGBA is larger than the compressed source it came
|
|
11
|
+
* from, and the bake re-encodes to whatever the texture pipeline wants; nothing ships a block
|
|
12
|
+
* decoder into a running game. That is what makes the cost acceptable and it is the only reason
|
|
13
|
+
* decoding to RGBA rather than uploading the blocks directly is the right call here.
|
|
14
|
+
*
|
|
15
|
+
* **Only the mips are dropped, deliberately.** A DDS carries a full chain and the container
|
|
16
|
+
* generates its own, so decoding level 0 and discarding the rest is not a loss — it is declining
|
|
17
|
+
* to carry two answers to the same question.
|
|
18
|
+
*
|
|
19
|
+
* **Both header spellings, as of 3.30.2.** A `DX10` header names the format in a 20-byte extension
|
|
20
|
+
* rather than in the four characters at byte 84, and the blocks behind it are the same blocks. This
|
|
21
|
+
* reader took the classic spelling and refused the other, which cost one consumer 20 textures of a
|
|
22
|
+
* single car — its paint, its wheels, its lamps, its plate and seven interior maps — and cost them
|
|
23
|
+
* as a *lighting* bug rather than as a missing texture: their baker substitutes a 1x1 white pixel,
|
|
24
|
+
* and white in an ORM map is roughness 1 **and** metallic 1, which in the flat shader leaves no
|
|
25
|
+
* diffuse and no sun term at all. The bodywork became a blurred mirror of the field it was parked
|
|
26
|
+
* in, and the report said so in those words.
|
|
27
|
+
*
|
|
28
|
+
* What it refuses, by name: BC7, BC6H, BC4, signed BC5, cube maps, arrays and volumes. A format
|
|
29
|
+
* identified and declined is worth more than one half-decoded, which is `recognise.ts`'s principle
|
|
30
|
+
* applied one level down — and the last three of those are refused *because* a classic header has
|
|
31
|
+
* no way to state them, so accepting one would answer with a picture instead of an error.
|
|
32
|
+
*/
|
|
33
|
+
/** Whether these bytes are a DDS at all. The magic, and nothing inferred from a file name. */
|
|
34
|
+
export declare function isDds(bytes: Uint8Array): boolean;
|
|
35
|
+
/** What a decoded surface is: level 0, RGBA, eight bits a channel. */
|
|
36
|
+
export interface DdsImage {
|
|
37
|
+
readonly width: number;
|
|
38
|
+
readonly height: number;
|
|
39
|
+
readonly rgba: Uint8Array;
|
|
40
|
+
}
|
|
41
|
+
export declare function ddsToRgba(bytes: Uint8Array): DdsImage;
|