@driftengine/assets 3.63.0 → 4.1.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/NOTICE +20 -0
- package/dist/bake/cluster.d.ts +49 -0
- package/dist/bake/cluster.js +277 -0
- package/dist/bake/clusterLod.d.ts +40 -0
- package/dist/bake/clusterLod.js +162 -0
- package/dist/bake/dtexChunk.d.ts +52 -0
- package/dist/bake/dtexChunk.js +103 -0
- package/dist/bake/hlod.d.ts +129 -0
- package/dist/bake/hlod.js +403 -0
- package/dist/bake/latent.d.ts +84 -0
- package/dist/bake/latent.js +339 -0
- package/dist/bake/sdf.d.ts +45 -0
- package/dist/bake/sdf.js +370 -0
- package/dist/drftLoader.d.ts +16 -0
- package/dist/drftLoader.js +23 -4
- package/dist/dtexTiles.d.ts +34 -0
- package/dist/dtexTiles.js +37 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +35 -0
- package/package.json +4 -3
- package/src/bake/cluster.ts +361 -0
- package/src/bake/clusterLod.ts +228 -0
- package/src/bake/dtexChunk.ts +156 -0
- package/src/bake/hlod.ts +487 -0
- package/src/bake/latent.ts +427 -0
- package/src/bake/sdf.ts +468 -0
- package/src/drftLoader.ts +41 -3
- package/src/dtexTiles.ts +61 -0
- package/src/index.ts +40 -0
package/dist/bake/sdf.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/** A signed distance field per object, baked offline, for tracing light through a world. */
|
|
2
|
+
/**
|
|
3
|
+
* Voxels of clearance around the mesh, on every axis.
|
|
4
|
+
*
|
|
5
|
+
* **A field that stops at the mesh cannot be marched into.** A ray approaching from outside needs
|
|
6
|
+
* somewhere to take its first steps, and a gradient at the boundary needs a neighbour on both
|
|
7
|
+
* sides — the central difference at an edge texel has neither. Two is the smallest number that
|
|
8
|
+
* gives both and it is cheap on the thin axis of a wall, which is exactly where padding by a
|
|
9
|
+
* fraction of the extent would cost the most.
|
|
10
|
+
*/
|
|
11
|
+
const PAD_VOXELS = 2;
|
|
12
|
+
/**
|
|
13
|
+
* Voxels past a triangle's own box that get an exactly computed distance.
|
|
14
|
+
*
|
|
15
|
+
* Everything further away is filled by propagation, which carries the *closest triangle* outward
|
|
16
|
+
* and recomputes the exact distance to it rather than accumulating a stepped approximation — so
|
|
17
|
+
* the far field is exact wherever the nearest triangle is reachable from a neighbour, which for
|
|
18
|
+
* anything short of a spiral is everywhere.
|
|
19
|
+
*/
|
|
20
|
+
const EXACT_BAND = 1;
|
|
21
|
+
/** Larger than any distance a padded grid can hold, and finite so arithmetic on it behaves. */
|
|
22
|
+
const UNREACHED = 1e30;
|
|
23
|
+
/**
|
|
24
|
+
* Bake one mesh into a signed distance field.
|
|
25
|
+
*
|
|
26
|
+
* `resolution` is voxels along the mesh's **longest** axis; the other two get however many the
|
|
27
|
+
* same spacing needs. **Cubic voxels are not tidiness** — a field with a different step per axis
|
|
28
|
+
* has a gradient whose length varies with direction, which is the one property a sphere trace
|
|
29
|
+
* depends on, and the failure is a march that overshoots along the coarse axis.
|
|
30
|
+
*
|
|
31
|
+
* **The sign comes from crossing parity, not from the winding.** A face normal, an
|
|
32
|
+
* angle-weighted pseudonormal and a generalised winding number all flip when a mesh is exported
|
|
33
|
+
* with its triangles the other way round, and importers disagree about that constantly — so a
|
|
34
|
+
* model from the wrong tool would come back solid where it is empty. Counting how many times a
|
|
35
|
+
* ray along `x` crosses the surface does not care which way a triangle faces.
|
|
36
|
+
*
|
|
37
|
+
* **The magnitude comes from exact point-to-triangle distance, not from voxelisation.** Marking
|
|
38
|
+
* the cells a triangle passes through and flooding outward loses any wall thinner than a cell,
|
|
39
|
+
* because no cell centre is inside it — and a wall thinner than a cell is most of them. This
|
|
40
|
+
* measures the distance from every grid point in a band around each triangle to that triangle,
|
|
41
|
+
* so a plate a fifth of a voxel thick is still a plate at a fifth of a voxel.
|
|
42
|
+
*/
|
|
43
|
+
export function bakeObjectSdf(mesh, resolution) {
|
|
44
|
+
const positions = mesh.positions;
|
|
45
|
+
const indices = mesh.indices;
|
|
46
|
+
const triangles = Math.floor(indices.length / 3);
|
|
47
|
+
if (triangles < 1) {
|
|
48
|
+
throw new Error('bakeObjectSdf: the mesh has no triangle; a field of nothing has no surface.');
|
|
49
|
+
}
|
|
50
|
+
const voxels = Math.trunc(resolution);
|
|
51
|
+
if (!(voxels >= 2)) {
|
|
52
|
+
throw new Error(`bakeObjectSdf: resolution is ${String(resolution)} and the minimum is 2. One sample on an ` +
|
|
53
|
+
'axis has no neighbour to take a gradient against.');
|
|
54
|
+
}
|
|
55
|
+
const min = [Infinity, Infinity, Infinity];
|
|
56
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
57
|
+
for (let at = 0; at + 2 < positions.length; at += 3) {
|
|
58
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
59
|
+
const value = positions[at + axis];
|
|
60
|
+
if (value < min[axis])
|
|
61
|
+
min[axis] = value;
|
|
62
|
+
if (value > max[axis])
|
|
63
|
+
max[axis] = value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/*
|
|
67
|
+
* A degenerate axis still needs a step, and the longest axis is what sets it. A perfectly flat
|
|
68
|
+
* quad has zero extent on one axis and is a completely ordinary thing to bake.
|
|
69
|
+
*/
|
|
70
|
+
const span = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
|
|
71
|
+
if (!(span > 0)) {
|
|
72
|
+
throw new Error('bakeObjectSdf: the mesh has no extent on any axis; there is nothing to bake.');
|
|
73
|
+
}
|
|
74
|
+
const step = span / voxels;
|
|
75
|
+
const pad = PAD_VOXELS * step;
|
|
76
|
+
const origin = new Float32Array(3);
|
|
77
|
+
const dims = [0, 0, 0];
|
|
78
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
79
|
+
origin[axis] = min[axis] - pad;
|
|
80
|
+
const reach = max[axis] + pad - origin[axis];
|
|
81
|
+
dims[axis] = Math.ceil(reach / step) + 1;
|
|
82
|
+
}
|
|
83
|
+
const [nx, ny, nz] = dims;
|
|
84
|
+
const bounds = new Float32Array([
|
|
85
|
+
origin[0],
|
|
86
|
+
origin[1],
|
|
87
|
+
origin[2],
|
|
88
|
+
origin[0] + (nx - 1) * step,
|
|
89
|
+
origin[1] + (ny - 1) * step,
|
|
90
|
+
origin[2] + (nz - 1) * step,
|
|
91
|
+
]);
|
|
92
|
+
const count = nx * ny * nz;
|
|
93
|
+
const field = new Float32Array(count).fill(UNREACHED);
|
|
94
|
+
/** The triangle each grid point's distance was measured to, and -1 where none has reached. */
|
|
95
|
+
const closest = new Int32Array(count).fill(-1);
|
|
96
|
+
/** How many times the surface is crossed between grid point `i - 1` and `i` along `x`. */
|
|
97
|
+
const crossings = new Int32Array(count);
|
|
98
|
+
const ox = origin[0];
|
|
99
|
+
const oy = origin[1];
|
|
100
|
+
const oz = origin[2];
|
|
101
|
+
for (let tri = 0; tri < triangles; tri++) {
|
|
102
|
+
const ia = indices[tri * 3] * 3;
|
|
103
|
+
const ib = indices[tri * 3 + 1] * 3;
|
|
104
|
+
const ic = indices[tri * 3 + 2] * 3;
|
|
105
|
+
const ax = positions[ia];
|
|
106
|
+
const ay = positions[ia + 1];
|
|
107
|
+
const az = positions[ia + 2];
|
|
108
|
+
const bx = positions[ib];
|
|
109
|
+
const by = positions[ib + 1];
|
|
110
|
+
const bz = positions[ib + 2];
|
|
111
|
+
const cx = positions[ic];
|
|
112
|
+
const cy = positions[ic + 1];
|
|
113
|
+
const cz = positions[ic + 2];
|
|
114
|
+
const i0 = clampIndex(Math.floor((Math.min(ax, bx, cx) - ox) / step) - EXACT_BAND, nx);
|
|
115
|
+
const i1 = clampIndex(Math.ceil((Math.max(ax, bx, cx) - ox) / step) + EXACT_BAND, nx);
|
|
116
|
+
const j0 = clampIndex(Math.floor((Math.min(ay, by, cy) - oy) / step) - EXACT_BAND, ny);
|
|
117
|
+
const j1 = clampIndex(Math.ceil((Math.max(ay, by, cy) - oy) / step) + EXACT_BAND, ny);
|
|
118
|
+
const k0 = clampIndex(Math.floor((Math.min(az, bz, cz) - oz) / step) - EXACT_BAND, nz);
|
|
119
|
+
const k1 = clampIndex(Math.ceil((Math.max(az, bz, cz) - oz) / step) + EXACT_BAND, nz);
|
|
120
|
+
for (let k = k0; k <= k1; k++) {
|
|
121
|
+
const gz = oz + k * step;
|
|
122
|
+
for (let j = j0; j <= j1; j++) {
|
|
123
|
+
const gy = oy + j * step;
|
|
124
|
+
for (let i = i0; i <= i1; i++) {
|
|
125
|
+
const gx = ox + i * step;
|
|
126
|
+
const distance = pointTriangleDistance(gx, gy, gz, ax, ay, az, bx, by, bz, cx, cy, cz);
|
|
127
|
+
const index = i + nx * (j + ny * k);
|
|
128
|
+
if (distance < field[index]) {
|
|
129
|
+
field[index] = distance;
|
|
130
|
+
closest[index] = tri;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/*
|
|
136
|
+
* Where a ray along `x` through each grid column crosses this triangle. Recorded at the first
|
|
137
|
+
* grid point past the crossing, so a scan along `x` accumulating these counts knows, at every
|
|
138
|
+
* point, how many surfaces stand between it and the grid's `-x` face.
|
|
139
|
+
*/
|
|
140
|
+
for (let k = clampIndex(Math.ceil((Math.min(az, bz, cz) - oz) / step), nz); k < nz; k++) {
|
|
141
|
+
const gz = oz + k * step;
|
|
142
|
+
if (gz > Math.max(az, bz, cz))
|
|
143
|
+
break;
|
|
144
|
+
for (let j = clampIndex(Math.ceil((Math.min(ay, by, cy) - oy) / step), ny); j < ny; j++) {
|
|
145
|
+
const gy = oy + j * step;
|
|
146
|
+
if (gy > Math.max(ay, by, cy))
|
|
147
|
+
break;
|
|
148
|
+
const bary = barycentric2d(gy, gz, ay, az, by, bz, cy, cz);
|
|
149
|
+
if (bary === null)
|
|
150
|
+
continue;
|
|
151
|
+
const x = bary[0] * ax + bary[1] * bx + bary[2] * cx;
|
|
152
|
+
const i = Math.ceil((x - ox) / step);
|
|
153
|
+
/*
|
|
154
|
+
* **Unreachable while `PAD_VOXELS` is at least one, and kept because it is a constant.**
|
|
155
|
+
* The grid reaches two voxels past the mesh on every axis, so no crossing can land past
|
|
156
|
+
* the `+x` face; with no padding one could, and clamping it would mark the last grid point
|
|
157
|
+
* as having one more surface behind it than it does — flipping the parity of a point that
|
|
158
|
+
* is outside. A crossing before the `-x` face is clamped to zero, where it is true: the
|
|
159
|
+
* grid's first point really does have that surface behind it.
|
|
160
|
+
*/
|
|
161
|
+
if (i >= nx)
|
|
162
|
+
continue;
|
|
163
|
+
crossings[Math.max(0, i) + nx * (j + ny * k)]++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/*
|
|
168
|
+
* Carry the closest triangle outward and recompute against it, eight directions twice. Two
|
|
169
|
+
* passes rather than one because a point whose nearest triangle lies around a corner is reached
|
|
170
|
+
* by a sweep that has itself only just been filled, and the second pass is what lets that
|
|
171
|
+
* propagate the rest of the way.
|
|
172
|
+
*/
|
|
173
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
174
|
+
for (const [dx, dy, dz] of SWEEPS) {
|
|
175
|
+
sweep(field, closest, positions, indices, dims, origin, step, dx, dy, dz);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/* Anything nothing ever reached is as far away as the grid can express, not a sentinel. */
|
|
179
|
+
const reach = step * (nx + ny + nz);
|
|
180
|
+
for (let index = 0; index < count; index++) {
|
|
181
|
+
if (field[index] >= UNREACHED)
|
|
182
|
+
field[index] = reach;
|
|
183
|
+
}
|
|
184
|
+
for (let k = 0; k < nz; k++) {
|
|
185
|
+
for (let j = 0; j < ny; j++) {
|
|
186
|
+
let total = 0;
|
|
187
|
+
const row = nx * (j + ny * k);
|
|
188
|
+
for (let i = 0; i < nx; i++) {
|
|
189
|
+
total += crossings[row + i];
|
|
190
|
+
if (total % 2 === 1)
|
|
191
|
+
field[row + i] = -field[row + i];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { field, dims, bounds };
|
|
196
|
+
}
|
|
197
|
+
/** The eight diagonal orders a propagation runs in, so every direction has one that follows it. */
|
|
198
|
+
const SWEEPS = [
|
|
199
|
+
[1, 1, 1],
|
|
200
|
+
[-1, -1, -1],
|
|
201
|
+
[1, 1, -1],
|
|
202
|
+
[-1, -1, 1],
|
|
203
|
+
[1, -1, 1],
|
|
204
|
+
[-1, 1, -1],
|
|
205
|
+
[1, -1, -1],
|
|
206
|
+
[-1, 1, 1],
|
|
207
|
+
];
|
|
208
|
+
/** One propagation pass: every point takes its neighbours' triangles if one of them is nearer. */
|
|
209
|
+
function sweep(field, closest, positions, indices, dims, origin, step, dx, dy, dz) {
|
|
210
|
+
const [nx, ny, nz] = dims;
|
|
211
|
+
const i0 = dx > 0 ? 1 : nx - 2;
|
|
212
|
+
const i1 = dx > 0 ? nx : -1;
|
|
213
|
+
const j0 = dy > 0 ? 1 : ny - 2;
|
|
214
|
+
const j1 = dy > 0 ? ny : -1;
|
|
215
|
+
const k0 = dz > 0 ? 1 : nz - 2;
|
|
216
|
+
const k1 = dz > 0 ? nz : -1;
|
|
217
|
+
for (let k = k0; k !== k1; k += dz) {
|
|
218
|
+
const gz = origin[2] + k * step;
|
|
219
|
+
for (let j = j0; j !== j1; j += dy) {
|
|
220
|
+
const gy = origin[1] + j * step;
|
|
221
|
+
for (let i = i0; i !== i1; i += dx) {
|
|
222
|
+
const gx = origin[0] + i * step;
|
|
223
|
+
const index = i + nx * (j + ny * k);
|
|
224
|
+
/* The three already-visited axis neighbours. See `NEIGHBOURS` for why three and not seven. */
|
|
225
|
+
for (const [ni, nj, nk] of NEIGHBOURS) {
|
|
226
|
+
const at = i - ni * dx + nx * (j - nj * dy + ny * (k - nk * dz));
|
|
227
|
+
const triangle = closest[at];
|
|
228
|
+
if (triangle < 0)
|
|
229
|
+
continue;
|
|
230
|
+
const ia = indices[triangle * 3] * 3;
|
|
231
|
+
const ib = indices[triangle * 3 + 1] * 3;
|
|
232
|
+
const ic = indices[triangle * 3 + 2] * 3;
|
|
233
|
+
const distance = pointTriangleDistance(gx, gy, gz, positions[ia], positions[ia + 1], positions[ia + 2], positions[ib], positions[ib + 1], positions[ib + 2], positions[ic], positions[ic + 1], positions[ic + 2]);
|
|
234
|
+
if (distance < field[index]) {
|
|
235
|
+
field[index] = distance;
|
|
236
|
+
closest[index] = triangle;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The already-visited neighbours of a point, in the sweep's own frame.
|
|
245
|
+
*
|
|
246
|
+
* **Three, and the reference implementation this follows uses seven.** The other four are the face
|
|
247
|
+
* and body diagonals, and the argument for them is that a field propagating along the axes alone
|
|
248
|
+
* acquires a plus-shaped bias. **That argument does not survive being measured here**, because the
|
|
249
|
+
* diagonals are already carried by the eight *sweep orders*: a unit sphere at resolution 40 comes
|
|
250
|
+
* out with a worst error against the analytic distance of 9.6e-3 either way — the same number to
|
|
251
|
+
* two digits — while the seven-neighbour version takes 1123 ms against 529 ms.
|
|
252
|
+
*
|
|
253
|
+
* What does move is the worst gradient error, 4.0e-2 to 4.6e-2, both of them dominated by the
|
|
254
|
+
* tessellation's own facets and both far under what a march needs. **Reproduce** by adding the
|
|
255
|
+
* four diagonals back and running `sdf.test.ts`, which passes either way and is why this is a
|
|
256
|
+
* measurement rather than a test.
|
|
257
|
+
*/
|
|
258
|
+
const NEIGHBOURS = [
|
|
259
|
+
[1, 0, 0],
|
|
260
|
+
[0, 1, 0],
|
|
261
|
+
[0, 0, 1],
|
|
262
|
+
];
|
|
263
|
+
/** A whole index forced inside a grid, for a triangle's box which may hang over an edge. */
|
|
264
|
+
function clampIndex(value, count) {
|
|
265
|
+
return value < 0 ? 0 : value > count - 1 ? count - 1 : value;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Where a point stands in a triangle projected onto one plane, or null if it stands outside.
|
|
269
|
+
*
|
|
270
|
+
* **The tie-break is what stops a shared edge being counted twice or not at all.** A grid column
|
|
271
|
+
* passing exactly along the edge two triangles share is inside both or neither depending on the
|
|
272
|
+
* sign of a zero, and either answer flips the parity of an entire row. Breaking the tie on the
|
|
273
|
+
* coordinates themselves makes the choice consistent between the two triangles: whichever way it
|
|
274
|
+
* goes, exactly one of them claims the column.
|
|
275
|
+
*/
|
|
276
|
+
function barycentric2d(px, py, ax, ay, bx, by, cx, cy) {
|
|
277
|
+
const bxr = bx - px;
|
|
278
|
+
const byr = by - py;
|
|
279
|
+
const cxr = cx - px;
|
|
280
|
+
const cyr = cy - py;
|
|
281
|
+
const axr = ax - px;
|
|
282
|
+
const ayr = ay - py;
|
|
283
|
+
const areaA = orientation(bxr, byr, cxr, cyr);
|
|
284
|
+
if (areaA.sign === 0)
|
|
285
|
+
return null;
|
|
286
|
+
const areaB = orientation(cxr, cyr, axr, ayr);
|
|
287
|
+
if (areaB.sign !== areaA.sign)
|
|
288
|
+
return null;
|
|
289
|
+
const areaC = orientation(axr, ayr, bxr, byr);
|
|
290
|
+
if (areaC.sign !== areaA.sign)
|
|
291
|
+
return null;
|
|
292
|
+
const sum = areaA.area + areaB.area + areaC.area;
|
|
293
|
+
if (sum === 0)
|
|
294
|
+
return null;
|
|
295
|
+
return [areaA.area / sum, areaB.area / sum, areaC.area / sum];
|
|
296
|
+
}
|
|
297
|
+
/** Twice the signed area of a triangle with the origin, with zero broken deterministically. */
|
|
298
|
+
function orientation(x1, y1, x2, y2) {
|
|
299
|
+
const area = y1 * x2 - x1 * y2;
|
|
300
|
+
if (area > 0)
|
|
301
|
+
return { sign: 1, area };
|
|
302
|
+
if (area < 0)
|
|
303
|
+
return { sign: -1, area };
|
|
304
|
+
if (y2 > y1)
|
|
305
|
+
return { sign: 1, area };
|
|
306
|
+
if (y2 < y1)
|
|
307
|
+
return { sign: -1, area };
|
|
308
|
+
if (x1 > x2)
|
|
309
|
+
return { sign: 1, area };
|
|
310
|
+
if (x1 < x2)
|
|
311
|
+
return { sign: -1, area };
|
|
312
|
+
return { sign: 0, area };
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The distance from a point to the nearest point of a triangle.
|
|
316
|
+
*
|
|
317
|
+
* The seven regions of Voronoi space around a triangle — three vertices, three edges, the face —
|
|
318
|
+
* tested in the order that lets each test assume the ones before it failed. Written out rather
|
|
319
|
+
* than solved as a projection with a clamp, because the clamped projection is wrong outside the
|
|
320
|
+
* face's own region and the error is largest exactly at the silhouette, which is where a marching
|
|
321
|
+
* ray spends its time.
|
|
322
|
+
*/
|
|
323
|
+
function pointTriangleDistance(px, py, pz, ax, ay, az, bx, by, bz, cx, cy, cz) {
|
|
324
|
+
const abx = bx - ax;
|
|
325
|
+
const aby = by - ay;
|
|
326
|
+
const abz = bz - az;
|
|
327
|
+
const acx = cx - ax;
|
|
328
|
+
const acy = cy - ay;
|
|
329
|
+
const acz = cz - az;
|
|
330
|
+
const apx = px - ax;
|
|
331
|
+
const apy = py - ay;
|
|
332
|
+
const apz = pz - az;
|
|
333
|
+
const d1 = abx * apx + aby * apy + abz * apz;
|
|
334
|
+
const d2 = acx * apx + acy * apy + acz * apz;
|
|
335
|
+
if (d1 <= 0 && d2 <= 0)
|
|
336
|
+
return Math.hypot(apx, apy, apz);
|
|
337
|
+
const bpx = px - bx;
|
|
338
|
+
const bpy = py - by;
|
|
339
|
+
const bpz = pz - bz;
|
|
340
|
+
const d3 = abx * bpx + aby * bpy + abz * bpz;
|
|
341
|
+
const d4 = acx * bpx + acy * bpy + acz * bpz;
|
|
342
|
+
if (d3 >= 0 && d4 <= d3)
|
|
343
|
+
return Math.hypot(bpx, bpy, bpz);
|
|
344
|
+
const vc = d1 * d4 - d3 * d2;
|
|
345
|
+
if (vc <= 0 && d1 >= 0 && d3 <= 0) {
|
|
346
|
+
const v = d1 / (d1 - d3);
|
|
347
|
+
return Math.hypot(apx - v * abx, apy - v * aby, apz - v * abz);
|
|
348
|
+
}
|
|
349
|
+
const cpx = px - cx;
|
|
350
|
+
const cpy = py - cy;
|
|
351
|
+
const cpz = pz - cz;
|
|
352
|
+
const d5 = abx * cpx + aby * cpy + abz * cpz;
|
|
353
|
+
const d6 = acx * cpx + acy * cpy + acz * cpz;
|
|
354
|
+
if (d6 >= 0 && d5 <= d6)
|
|
355
|
+
return Math.hypot(cpx, cpy, cpz);
|
|
356
|
+
const vb = d5 * d2 - d1 * d6;
|
|
357
|
+
if (vb <= 0 && d2 >= 0 && d6 <= 0) {
|
|
358
|
+
const w = d2 / (d2 - d6);
|
|
359
|
+
return Math.hypot(apx - w * acx, apy - w * acy, apz - w * acz);
|
|
360
|
+
}
|
|
361
|
+
const va = d3 * d6 - d5 * d4;
|
|
362
|
+
if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
|
|
363
|
+
const w = (d4 - d3) / (d4 - d3 + (d5 - d6));
|
|
364
|
+
return Math.hypot(bpx - w * (cx - bx), bpy - w * (cy - by), bpz - w * (cz - bz));
|
|
365
|
+
}
|
|
366
|
+
const denom = 1 / (va + vb + vc);
|
|
367
|
+
const v = vb * denom;
|
|
368
|
+
const w = vc * denom;
|
|
369
|
+
return Math.hypot(apx - (v * abx + w * acx), apy - (v * aby + w * acy), apz - (v * abz + w * acz));
|
|
370
|
+
}
|
package/dist/drftLoader.d.ts
CHANGED
|
@@ -27,7 +27,23 @@ export type DrftFit = {
|
|
|
27
27
|
/** Scale 1 and no offset: the model arrives in the coordinates the file states. */
|
|
28
28
|
readonly fit: 'none';
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* How a container is reached. `typeof fetch`, narrowed to the one call shape this file makes.
|
|
32
|
+
*
|
|
33
|
+
* `@driftengine/audio` has had the identical seam since it was written, under the same name, and
|
|
34
|
+
* this package went without it — so the loader for the engine's *own* format was the one that
|
|
35
|
+
* could not be pointed at a service worker, a packed archive, a memory map or a fixture.
|
|
36
|
+
*/
|
|
37
|
+
export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
30
38
|
export interface DrftLoaderOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Where the bytes come from. The global `fetch` unless a consumer says otherwise.
|
|
41
|
+
*
|
|
42
|
+
* Optional and defaulting to the browser's, so no existing caller changes — the shape
|
|
43
|
+
* `AGENTS.md` asks for: take the capability as a parameter, ship a browser implementation as
|
|
44
|
+
* the default.
|
|
45
|
+
*/
|
|
46
|
+
readonly fetchImpl?: FetchLike;
|
|
31
47
|
/**
|
|
32
48
|
* The most parts one `update` may take, however cheap they turn out to be.
|
|
33
49
|
*
|
package/dist/drftLoader.js
CHANGED
|
@@ -272,7 +272,8 @@ export class DrftLoader {
|
|
|
272
272
|
* to the *baker* appears to have changed nothing, and the time lost to that is out of all
|
|
273
273
|
* proportion to the saving.
|
|
274
274
|
*/
|
|
275
|
-
|
|
275
|
+
// platform: browser default — `DrftLoaderOptions.fetchImpl` is the seam
|
|
276
|
+
const response = await (this.options.fetchImpl ?? fetch)(url, { cache: 'no-store' });
|
|
276
277
|
if (!response.ok) {
|
|
277
278
|
this.set({ phase: 'absent', message: `no model at ${url}` });
|
|
278
279
|
return;
|
|
@@ -948,6 +949,23 @@ export function isRawCodec(codec) {
|
|
|
948
949
|
* The `.slice()` matters: the bytes are a view into the streamed buffer, and a `Blob` built on
|
|
949
950
|
* the view would keep the whole file alive for as long as the image does.
|
|
950
951
|
*/
|
|
952
|
+
/**
|
|
953
|
+
* How every image a model carries is decoded: **straight alpha, and no colour conversion.**
|
|
954
|
+
*
|
|
955
|
+
* `createImageBitmap` premultiplies unless told not to, and the upload into a straight-alpha
|
|
956
|
+
* texture then divides back out. A texel with no alpha comes back with no colour, so a cutout or an
|
|
957
|
+
* emblem loses the colour its author padded past its edge, and filtering pulls black into the edge;
|
|
958
|
+
* a partly transparent one comes back rounded to a coarser step. Colour management rewrites a normal
|
|
959
|
+
* or ORM map, whose values are not colours at all, and glTF — which these assets are baked from —
|
|
960
|
+
* says an image's own colour metadata is ignored. `render/imageTexels.ts` asks for the same two.
|
|
961
|
+
*
|
|
962
|
+
* What it gives up: an albedo authored in a wide-gamut space with a profile saying so is read as
|
|
963
|
+
* sRGB. The baker is where that conversion belongs, and it has no colour management either.
|
|
964
|
+
*/
|
|
965
|
+
const AS_AUTHORED = {
|
|
966
|
+
premultiplyAlpha: 'none',
|
|
967
|
+
colorSpaceConversion: 'none',
|
|
968
|
+
};
|
|
951
969
|
async function decodeImage(texture, longestSide) {
|
|
952
970
|
/*
|
|
953
971
|
* **A raw texture has no decoder, because it is already decoded.**
|
|
@@ -969,11 +987,11 @@ async function decodeImage(texture, longestSide) {
|
|
|
969
987
|
if (pixels <= 0 || texture.bytes.length < pixels * 4) {
|
|
970
988
|
throw new DrftError(`a raw texture needs ${pixels * 4} bytes for ${texture.width}x${texture.height} and carries ${texture.bytes.length}`);
|
|
971
989
|
}
|
|
972
|
-
return await createImageBitmap(new ImageData(new Uint8ClampedArray(texture.bytes.slice(0, pixels * 4)), texture.width, texture.height));
|
|
990
|
+
return await createImageBitmap(new ImageData(new Uint8ClampedArray(texture.bytes.slice(0, pixels * 4)), texture.width, texture.height), AS_AUTHORED);
|
|
973
991
|
}
|
|
974
992
|
const blob = new Blob([texture.bytes.slice()], { type: imageTypeFor(texture.codec) });
|
|
975
993
|
if (longestSide === undefined)
|
|
976
|
-
return await createImageBitmap(blob);
|
|
994
|
+
return await createImageBitmap(blob, AS_AUTHORED);
|
|
977
995
|
/*
|
|
978
996
|
* Resized while it decodes, which is the point: asking for a 256 pixel version of a 2048
|
|
979
997
|
* square is a fraction of the work of decoding one and throwing most of it away, and the
|
|
@@ -986,9 +1004,10 @@ async function decodeImage(texture, longestSide) {
|
|
|
986
1004
|
*/
|
|
987
1005
|
const longest = Math.max(texture.width, texture.height);
|
|
988
1006
|
if (longest <= 0 || longest <= longestSide)
|
|
989
|
-
return await createImageBitmap(blob);
|
|
1007
|
+
return await createImageBitmap(blob, AS_AUTHORED);
|
|
990
1008
|
const scale = longestSide / longest;
|
|
991
1009
|
return await createImageBitmap(blob, {
|
|
1010
|
+
...AS_AUTHORED,
|
|
992
1011
|
resizeWidth: Math.max(1, Math.round(texture.width * scale)),
|
|
993
1012
|
resizeHeight: Math.max(1, Math.round(texture.height * scale)),
|
|
994
1013
|
resizeQuality: 'medium',
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `DTEX` chunk's tile table as the grid residency asks about.
|
|
3
|
+
*
|
|
4
|
+
* **Two constructors of one grid, and this is the one a streaming consumer uses.**
|
|
5
|
+
* `latentTileGrid` cuts a grid from a latent it has in memory, which is what a baker has;
|
|
6
|
+
* a consumer loading a file has the table instead, and the table is the point — it says where each
|
|
7
|
+
* tile's bytes are and what the tile *is*, so `tilesForView` can name the tiles a camera needs and
|
|
8
|
+
* a loader can fetch exactly those.
|
|
9
|
+
*
|
|
10
|
+
* **The hash is the file's**, over the tile's stored bytes, which is the number the file, the
|
|
11
|
+
* fetch and the cache all have to agree on. `latentTileGrid`'s hashes are over *decoded* texels and
|
|
12
|
+
* are a different number for the same tile — a baker's key rather than a file's. A consumer that
|
|
13
|
+
* mixed them would fetch every tile it already had.
|
|
14
|
+
*
|
|
15
|
+
* **One level, because a `DTEX` carries one.** The chunk holds a single latent grid, so this hands
|
|
16
|
+
* back a single level; a mip chain in the container is a further format step and not an omission
|
|
17
|
+
* here.
|
|
18
|
+
*
|
|
19
|
+
* This lives in `@driftengine/assets` because it is the one package that sees both the container
|
|
20
|
+
* and the texture runtime — `drft` has no dependencies by rule, and `texture` does not know what a
|
|
21
|
+
* chunk is.
|
|
22
|
+
*/
|
|
23
|
+
import type { DtexMaterial } from '@driftengine/drft';
|
|
24
|
+
import type { MaterialTileGrid } from '@driftengine/texture';
|
|
25
|
+
/** The grid a chunk's table describes, or `null` where the material is one image and not streamed. */
|
|
26
|
+
export declare function tileGridFromDtex(material: DtexMaterial): MaterialTileGrid | null;
|
|
27
|
+
/**
|
|
28
|
+
* The bytes of one tile, by where it is rather than by its number.
|
|
29
|
+
*
|
|
30
|
+
* The table is in the grid's order — across a row, then down — so this is an index and a
|
|
31
|
+
* subarray. It is here rather than left to a caller because the order is a format rule, and a
|
|
32
|
+
* caller working it out again is a second place for it to be wrong.
|
|
33
|
+
*/
|
|
34
|
+
export declare function dtexTileBytes(material: DtexMaterial, tx: number, ty: number): Uint8Array | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** The grid a chunk's table describes, or `null` where the material is one image and not streamed. */
|
|
2
|
+
export function tileGridFromDtex(material) {
|
|
3
|
+
const tiles = material.tileOffset.length;
|
|
4
|
+
if (tiles === 0 || material.tileSize === 0)
|
|
5
|
+
return null;
|
|
6
|
+
const hashes = new Array(tiles);
|
|
7
|
+
for (let at = 0; at < tiles; at += 1) {
|
|
8
|
+
const high = material.tileHash[at * 2] >>> 0;
|
|
9
|
+
const low = material.tileHash[at * 2 + 1] >>> 0;
|
|
10
|
+
hashes[at] = high.toString(16).padStart(8, '0') + low.toString(16).padStart(8, '0');
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
width: material.latentWidth,
|
|
14
|
+
height: material.latentHeight,
|
|
15
|
+
tileSize: material.tileSize,
|
|
16
|
+
addressMode: material.addressMode,
|
|
17
|
+
levels: [hashes],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The bytes of one tile, by where it is rather than by its number.
|
|
22
|
+
*
|
|
23
|
+
* The table is in the grid's order — across a row, then down — so this is an index and a
|
|
24
|
+
* subarray. It is here rather than left to a caller because the order is a format rule, and a
|
|
25
|
+
* caller working it out again is a second place for it to be wrong.
|
|
26
|
+
*/
|
|
27
|
+
export function dtexTileBytes(material, tx, ty) {
|
|
28
|
+
if (material.tileSize === 0)
|
|
29
|
+
return null;
|
|
30
|
+
const across = Math.ceil(material.latentWidth / material.tileSize);
|
|
31
|
+
const down = Math.ceil(material.latentHeight / material.tileSize);
|
|
32
|
+
if (tx < 0 || ty < 0 || tx >= across || ty >= down)
|
|
33
|
+
return null;
|
|
34
|
+
const at = ty * across + tx;
|
|
35
|
+
const from = material.tileOffset[at];
|
|
36
|
+
return material.latent.subarray(from, from + material.tileLength[at]);
|
|
37
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* world imports none of this and pays for none of it.
|
|
8
8
|
*/
|
|
9
9
|
export { DrftLoader } from './drftLoader.ts';
|
|
10
|
+
export { dtexTileBytes, tileGridFromDtex } from './dtexTiles.ts';
|
|
10
11
|
export type { DrftFit, DrftLoaderOptions } from './drftLoader.ts';
|
|
11
12
|
export { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
|
|
12
13
|
export type { DrawGrouping, DrawSurfaceOverride } from './drawKey.ts';
|
|
@@ -21,6 +22,14 @@ export type { GltfDocument } from './gltf.ts';
|
|
|
21
22
|
export { parseObj, parseMtl } from './obj.ts';
|
|
22
23
|
export { parseStl } from './stl.ts';
|
|
23
24
|
export { readRadianceHdr } from './radianceHdr.ts';
|
|
25
|
+
/**
|
|
26
|
+
* The material bake, exported for the GPU-driven pipeline's rig: it encodes a material to the
|
|
27
|
+
* decode programs that pipeline samples on the device.
|
|
28
|
+
*/
|
|
29
|
+
export { encodeMaterial, latentImageOf } from './bake/latent.ts';
|
|
30
|
+
export { dtexFromEncoded } from './bake/dtexChunk.ts';
|
|
31
|
+
export type { DtexTiles } from './bake/dtexChunk.ts';
|
|
32
|
+
export type { ChannelInput, EncodeMaterialOptions, EncodedMaterial } from './bake/latent.ts';
|
|
24
33
|
export { readIesProfile } from './iesPhotometry.ts';
|
|
25
34
|
export type { IesProfile } from './iesPhotometry.ts';
|
|
26
35
|
export type { RadianceImage } from './radianceHdr.ts';
|
|
@@ -56,3 +65,9 @@ export type { Localised } from './localise.ts';
|
|
|
56
65
|
export { colliderBeside, levelsBeside } from './readModel.ts';
|
|
57
66
|
export { ddsToRgba, isDds } from './dds.ts';
|
|
58
67
|
export type { DdsImage } from './dds.ts';
|
|
68
|
+
export { buildClusters } from './bake/cluster.ts';
|
|
69
|
+
export type { ClusterOptions, ClusterSet } from './bake/cluster.ts';
|
|
70
|
+
export { buildClusterDag } from './bake/clusterLod.ts';
|
|
71
|
+
export type { ClusterDag, ClusterLevel } from './bake/clusterLod.ts';
|
|
72
|
+
export { bakeObjectSdf } from './bake/sdf.ts';
|
|
73
|
+
export type { ObjectSdf } from './bake/sdf.ts';
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
* consumer writes the words, because what to call a stage is a decision about an audience.
|
|
13
13
|
*/
|
|
14
14
|
export { DrftLoader } from './drftLoader.js';
|
|
15
|
+
/*
|
|
16
|
+
* **A `DTEX` chunk's tile table as the grid residency asks about.** The other constructor of a
|
|
17
|
+
* `MaterialTileGrid` — `latentTileGrid` cuts one from a latent a baker holds, and this reads one a
|
|
18
|
+
* consumer downloaded. The hashes are the file's, over the bytes a fetch returns, which is what
|
|
19
|
+
* makes a tile that arrived for one material already resident for another.
|
|
20
|
+
*/
|
|
21
|
+
export { dtexTileBytes, tileGridFromDtex } from './dtexTiles.js';
|
|
15
22
|
/*
|
|
16
23
|
* The loader's own draw-call grouping, exported because a consumer counting draws needs it and was
|
|
17
24
|
* keeping a copy that could not be kept in step. See `drawKey.ts`.
|
|
@@ -34,6 +41,13 @@ export { parseStl } from './stl.js';
|
|
|
34
41
|
* keeps the zero-texture-files rule met on its own terms rather than waived.
|
|
35
42
|
*/
|
|
36
43
|
export { readRadianceHdr } from './radianceHdr.js';
|
|
44
|
+
/**
|
|
45
|
+
* The material bake, exported for the GPU-driven pipeline's rig: it encodes a material to the
|
|
46
|
+
* decode programs that pipeline samples on the device.
|
|
47
|
+
*/
|
|
48
|
+
export { encodeMaterial, latentImageOf } from './bake/latent.js';
|
|
49
|
+
/* An encoded material as the chunk a file carries it in: the one place that knows both packages. */
|
|
50
|
+
export { dtexFromEncoded } from './bake/dtexChunk.js';
|
|
37
51
|
/* IESNA LM-63, on the same opt-in terms: a consumer that loads no profile ships no parser. */
|
|
38
52
|
export { readIesProfile } from './iesPhotometry.js';
|
|
39
53
|
/*
|
|
@@ -130,3 +144,24 @@ export { colliderBeside, levelsBeside } from './readModel.js';
|
|
|
130
144
|
* is larger than the source and nothing ships a block decoder into a frame.
|
|
131
145
|
*/
|
|
132
146
|
export { ddsToRgba, isDds } from './dds.js';
|
|
147
|
+
/*
|
|
148
|
+
* **The offline half of the GPU-driven pipeline, which nothing outside this package could reach.**
|
|
149
|
+
*
|
|
150
|
+
* `bake/cluster.ts` and `bake/clusterLod.ts` produce exactly what `@driftengine/core`'s
|
|
151
|
+
* `buildGpuDrivenScene` consumes, and until this line neither was exported — so a consumer could
|
|
152
|
+
* hold the pipeline and had no way to make its input. Every other gate was green about it: the
|
|
153
|
+
* modules are tested, the package is sized, the licence is in place. Nothing asks whether a thing
|
|
154
|
+
* with no route out of the package is reachable, and the first scene that tried to use one found
|
|
155
|
+
* it by failing the demo boundary test.
|
|
156
|
+
*
|
|
157
|
+
* Baker work rather than frame work, and the split is the same one `ddsToRgba` above makes: a
|
|
158
|
+
* million triangles cluster in 648 ms, which is a build step and not a mount.
|
|
159
|
+
*/
|
|
160
|
+
export { buildClusters } from './bake/cluster.js';
|
|
161
|
+
export { buildClusterDag } from './bake/clusterLod.js';
|
|
162
|
+
/*
|
|
163
|
+
* The offline half of global illumination, exported for the same reason and against the same
|
|
164
|
+
* mistake: `bake/sdf.ts` produces what `globalField.ts` composes and what the `SDFV` chunk
|
|
165
|
+
* carries, and a field a consumer cannot bake is a pipeline with no input.
|
|
166
|
+
*/
|
|
167
|
+
export { bakeObjectSdf } from './bake/sdf.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/assets",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Model readers and the streaming loader for the .drft container",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@driftengine/drft": "
|
|
30
|
+
"@driftengine/drft": "4.1.0",
|
|
31
|
+
"@driftengine/texture": "4.1.0"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
|
-
"@driftengine/core": "
|
|
34
|
+
"@driftengine/core": "4.1.0"
|
|
34
35
|
},
|
|
35
36
|
"author": "Drift Technologies",
|
|
36
37
|
"repository": {
|