@driftengine/assets 3.62.0 → 4.0.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
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encoding a material's channels together, because they are correlated.
|
|
3
|
+
*
|
|
4
|
+
* **This is the claim the DriftTexture design rests on and the one thing about it that has to be
|
|
5
|
+
* measured rather than asserted.** A material's albedo, roughness and normal are not independent
|
|
6
|
+
* pictures of the same surface — they are three descriptions of the same scratches, the same
|
|
7
|
+
* wear, the same grain. Compressing them separately pays for that structure three times. So the
|
|
8
|
+
* encoder fits **one** latent that every channel is decoded from, and the test beside this file
|
|
9
|
+
* measures what that saves against encoding them apart.
|
|
10
|
+
*
|
|
11
|
+
* **And it measures the case where the saving is not there.** Two channels that share nothing need
|
|
12
|
+
* a second latent component and get one; a design that claimed a saving for independent channels
|
|
13
|
+
* would be claiming one that does not exist.
|
|
14
|
+
*
|
|
15
|
+
* ---
|
|
16
|
+
*
|
|
17
|
+
* **A fitted linear decoder rather than a trained one, deliberately.** The plan says to start with
|
|
18
|
+
* a fitted grid-plus-small-network scheme, and the reason is worth keeping: a trained encoder is
|
|
19
|
+
* not needed for the *format* to be correct, and making the format work first means the training
|
|
20
|
+
* in a later wave is an improvement to something shipping rather than a prerequisite for it.
|
|
21
|
+
*
|
|
22
|
+
* The fit is a principal-component basis over the channel vectors, which is exact, deterministic
|
|
23
|
+
* and cheap: the covariance is at most four by four. The "network" is one layer with no hidden
|
|
24
|
+
* width, so `evalNetwork` runs it with no activation on the output — which is what the header of
|
|
25
|
+
* `inference.ts` insists on, and what a linear decode needs anyway.
|
|
26
|
+
*
|
|
27
|
+
* **The dequantisation is folded into the weights.** A latent byte is 0–1; the projection it stands
|
|
28
|
+
* for is somewhere else entirely. Rescaling at decode would be a second place for the range to be
|
|
29
|
+
* wrong, so the scale goes into the weight matrix and the offset into the bias, and the decode is
|
|
30
|
+
* one matrix multiply against exactly what the format already stores.
|
|
31
|
+
*/
|
|
32
|
+
import {
|
|
33
|
+
DECODE_OP,
|
|
34
|
+
addDecodeNode,
|
|
35
|
+
createDecodeGraph,
|
|
36
|
+
evalNetwork,
|
|
37
|
+
reduceNormalMip,
|
|
38
|
+
needsVarianceMips,
|
|
39
|
+
type ChannelSpec,
|
|
40
|
+
type DecodeGraph,
|
|
41
|
+
type LatentImage,
|
|
42
|
+
type NetworkShape,
|
|
43
|
+
} from '@driftengine/texture';
|
|
44
|
+
|
|
45
|
+
export interface ChannelInput {
|
|
46
|
+
readonly spec: ChannelSpec;
|
|
47
|
+
/** `width * height` values, row-major with x fastest. */
|
|
48
|
+
readonly data: Float32Array;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The most latent components a sampled texel can carry: three, not four.
|
|
53
|
+
*
|
|
54
|
+
* `SAMPLE_LATENT` fills a four-lane register and the fourth lane is the format's alpha, which an
|
|
55
|
+
* image with fewer channels reads as 1. A fourth component would be read by the network as a
|
|
56
|
+
* constant — silently, and only where the encoder had wanted to use it.
|
|
57
|
+
*/
|
|
58
|
+
export const LATENT_MAX_COMPONENTS = 3;
|
|
59
|
+
|
|
60
|
+
/** How much of the channels' variance a component has to explain before it is worth its bytes. */
|
|
61
|
+
const COMPONENT_THRESHOLD = 0.01;
|
|
62
|
+
|
|
63
|
+
export interface EncodedMaterial {
|
|
64
|
+
readonly latent: Uint8Array;
|
|
65
|
+
readonly latentWidth: number;
|
|
66
|
+
readonly latentHeight: number;
|
|
67
|
+
readonly components: number;
|
|
68
|
+
readonly weights: Float32Array;
|
|
69
|
+
readonly shape: NetworkShape;
|
|
70
|
+
readonly graph: DecodeGraph;
|
|
71
|
+
readonly mips: readonly { width: number; height: number; data: Uint8Array }[];
|
|
72
|
+
/**
|
|
73
|
+
* The roughness a variance-preserving mip reduction handed back, or `null`.
|
|
74
|
+
*
|
|
75
|
+
* Non-null exactly when a channel's semantic says mipping it must preserve the normal
|
|
76
|
+
* distribution. A normal map averaged naively loses the disagreement between neighbouring
|
|
77
|
+
* normals, and the surface reads as polished at distance — the roughness is where that
|
|
78
|
+
* disagreement goes instead of being thrown away.
|
|
79
|
+
*/
|
|
80
|
+
readonly mipRoughness: Float32Array | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface EncodeMaterialOptions {
|
|
84
|
+
/** 0 to 1. Buys latent resolution, and with it size and accuracy. */
|
|
85
|
+
readonly quality: number;
|
|
86
|
+
/**
|
|
87
|
+
* One of `ADDRESS_MODE`. Defaults to lattice clamp, which is what every bake before 2026-09-17
|
|
88
|
+
* produced. A tiling surface texture wants `ADDRESS_MODE.CENTRE_WRAP`.
|
|
89
|
+
*/
|
|
90
|
+
readonly addressMode?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Latent grid size for a quality, as a fraction of the source. Monotonic by construction. */
|
|
94
|
+
function latentSizeFor(width: number, height: number, quality: number): [number, number] {
|
|
95
|
+
const q = Math.min(1, Math.max(0, quality));
|
|
96
|
+
/* An eighth at quality 0 up to the full size at 1, rounded to whole texels and never below 2. */
|
|
97
|
+
const scale = 0.125 + 0.875 * q;
|
|
98
|
+
return [Math.max(2, Math.round(width * scale)), Math.max(2, Math.round(height * scale))];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Average the source down to the latent grid, by box filter over whole source texels. */
|
|
102
|
+
function downsample(
|
|
103
|
+
data: Float32Array,
|
|
104
|
+
width: number,
|
|
105
|
+
height: number,
|
|
106
|
+
outWidth: number,
|
|
107
|
+
outHeight: number,
|
|
108
|
+
out: Float32Array,
|
|
109
|
+
stride: number,
|
|
110
|
+
offset: number,
|
|
111
|
+
): void {
|
|
112
|
+
for (let y = 0; y < outHeight; y += 1) {
|
|
113
|
+
const y0 = Math.floor((y * height) / outHeight);
|
|
114
|
+
const y1 = Math.max(y0 + 1, Math.floor(((y + 1) * height) / outHeight));
|
|
115
|
+
for (let x = 0; x < outWidth; x += 1) {
|
|
116
|
+
const x0 = Math.floor((x * width) / outWidth);
|
|
117
|
+
const x1 = Math.max(x0 + 1, Math.floor(((x + 1) * width) / outWidth));
|
|
118
|
+
let sum = 0;
|
|
119
|
+
let count = 0;
|
|
120
|
+
for (let sy = y0; sy < y1; sy += 1) {
|
|
121
|
+
for (let sx = x0; sx < x1; sx += 1) {
|
|
122
|
+
sum += data[sy * width + sx] as number;
|
|
123
|
+
count += 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
out[(y * outWidth + x) * stride + offset] = count === 0 ? 0 : sum / count;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The top `want` principal directions of a small covariance, by power iteration with deflation.
|
|
133
|
+
*
|
|
134
|
+
* Deterministic: the start vector is fixed rather than random, and the iteration count is fixed
|
|
135
|
+
* rather than a convergence test — a bake whose output depends on how quickly something converged
|
|
136
|
+
* is a bake that produces a different container from the same source.
|
|
137
|
+
*/
|
|
138
|
+
function principalAxes(covariance: Float64Array, size: number, want: number): number[][] {
|
|
139
|
+
const work = Float64Array.from(covariance);
|
|
140
|
+
const axes: number[][] = [];
|
|
141
|
+
const eigenvalues: number[] = [];
|
|
142
|
+
for (let k = 0; k < want; k += 1) {
|
|
143
|
+
let vector: number[] = Array.from({ length: size }, (_, i) => (i === k % size ? 1 : 0.5));
|
|
144
|
+
let value = 0;
|
|
145
|
+
for (let step = 0; step < 64; step += 1) {
|
|
146
|
+
const next = new Array<number>(size).fill(0);
|
|
147
|
+
for (let r = 0; r < size; r += 1) {
|
|
148
|
+
for (let c = 0; c < size; c += 1) {
|
|
149
|
+
next[r] = (next[r] as number) + (work[r * size + c] as number) * (vector[c] as number);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const length = Math.hypot(...next);
|
|
153
|
+
if (length < 1e-12) break;
|
|
154
|
+
for (let i = 0; i < size; i += 1) next[i] = (next[i] as number) / length;
|
|
155
|
+
vector = next;
|
|
156
|
+
value = length;
|
|
157
|
+
}
|
|
158
|
+
axes.push(vector);
|
|
159
|
+
eigenvalues.push(value);
|
|
160
|
+
/* Deflate, so the next iteration finds the next direction rather than the same one. */
|
|
161
|
+
for (let r = 0; r < size; r += 1) {
|
|
162
|
+
for (let c = 0; c < size; c += 1) {
|
|
163
|
+
work[r * size + c] =
|
|
164
|
+
(work[r * size + c] as number) - value * (vector[r] as number) * (vector[c] as number);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
AXIS_VALUES.length = 0;
|
|
169
|
+
for (const value of eigenvalues) AXIS_VALUES.push(value);
|
|
170
|
+
return axes;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The eigenvalues the last `principalAxes` found, so the caller can drop the useless ones. */
|
|
174
|
+
const AXIS_VALUES: number[] = [];
|
|
175
|
+
|
|
176
|
+
export function encodeMaterial(
|
|
177
|
+
channels: readonly ChannelInput[],
|
|
178
|
+
width: number,
|
|
179
|
+
height: number,
|
|
180
|
+
options: EncodeMaterialOptions,
|
|
181
|
+
): EncodedMaterial {
|
|
182
|
+
if (channels.length === 0) throw new Error('encodeMaterial: no channels');
|
|
183
|
+
const count = channels.length;
|
|
184
|
+
const [latentWidth, latentHeight] = latentSizeFor(width, height, options.quality);
|
|
185
|
+
const texels = latentWidth * latentHeight;
|
|
186
|
+
|
|
187
|
+
/* Every channel, averaged onto the latent grid, interleaved so a texel's values are adjacent. */
|
|
188
|
+
const grid = new Float32Array(texels * count);
|
|
189
|
+
for (let c = 0; c < count; c += 1) {
|
|
190
|
+
const channel = channels[c] as ChannelInput;
|
|
191
|
+
downsample(channel.data, width, height, latentWidth, latentHeight, grid, count, c);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const mean = new Float64Array(count);
|
|
195
|
+
for (let at = 0; at < texels; at += 1) {
|
|
196
|
+
for (let c = 0; c < count; c += 1)
|
|
197
|
+
mean[c] = (mean[c] as number) + (grid[at * count + c] as number);
|
|
198
|
+
}
|
|
199
|
+
for (let c = 0; c < count; c += 1) mean[c] = (mean[c] as number) / texels;
|
|
200
|
+
|
|
201
|
+
const covariance = new Float64Array(count * count);
|
|
202
|
+
for (let at = 0; at < texels; at += 1) {
|
|
203
|
+
for (let r = 0; r < count; r += 1) {
|
|
204
|
+
const dr = (grid[at * count + r] as number) - (mean[r] as number);
|
|
205
|
+
for (let c = 0; c < count; c += 1) {
|
|
206
|
+
const dc = (grid[at * count + c] as number) - (mean[c] as number);
|
|
207
|
+
covariance[r * count + c] = (covariance[r * count + c] as number) + dr * dc;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (let i = 0; i < covariance.length; i += 1) {
|
|
212
|
+
covariance[i] = (covariance[i] as number) / Math.max(1, texels - 1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const wanted = Math.min(count, LATENT_MAX_COMPONENTS);
|
|
216
|
+
const axes = principalAxes(covariance, count, wanted);
|
|
217
|
+
let total = 0;
|
|
218
|
+
for (const value of AXIS_VALUES) total += value;
|
|
219
|
+
/*
|
|
220
|
+
* Keep the components that explain something. Two channels carrying the same structure need one;
|
|
221
|
+
* two carrying different structure need two, and the encoder says so rather than spending bytes
|
|
222
|
+
* either way.
|
|
223
|
+
*/
|
|
224
|
+
let components = 0;
|
|
225
|
+
for (let k = 0; k < axes.length; k += 1) {
|
|
226
|
+
if (k > 0 && (AXIS_VALUES[k] as number) < total * COMPONENT_THRESHOLD) break;
|
|
227
|
+
components += 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Project every latent texel onto the basis, and find each component's range as we go. */
|
|
231
|
+
const projected = new Float32Array(texels * components);
|
|
232
|
+
const low = new Float64Array(components).fill(Infinity);
|
|
233
|
+
const high = new Float64Array(components).fill(-Infinity);
|
|
234
|
+
for (let at = 0; at < texels; at += 1) {
|
|
235
|
+
for (let k = 0; k < components; k += 1) {
|
|
236
|
+
const axis = axes[k] as number[];
|
|
237
|
+
let value = 0;
|
|
238
|
+
for (let c = 0; c < count; c += 1) {
|
|
239
|
+
value += ((grid[at * count + c] as number) - (mean[c] as number)) * (axis[c] as number);
|
|
240
|
+
}
|
|
241
|
+
projected[at * components + k] = value;
|
|
242
|
+
if (value < (low[k] as number)) low[k] = value;
|
|
243
|
+
if (value > (high[k] as number)) high[k] = value;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const latent = new Uint8Array(texels * components);
|
|
248
|
+
for (let at = 0; at < texels; at += 1) {
|
|
249
|
+
for (let k = 0; k < components; k += 1) {
|
|
250
|
+
const span = (high[k] as number) - (low[k] as number);
|
|
251
|
+
const normalised =
|
|
252
|
+
span === 0 ? 0 : ((projected[at * components + k] as number) - (low[k] as number)) / span;
|
|
253
|
+
latent[at * components + k] = Math.round(Math.min(1, Math.max(0, normalised)) * 255);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/*
|
|
258
|
+
* One layer, `components` in, and **one output per texel component rather than one per input
|
|
259
|
+
* channel**. A `ChannelSpec` says which of a texel's four lanes its channel occupies, so a
|
|
260
|
+
* decode that wrote its outputs in the order the channels were handed in would put roughness in
|
|
261
|
+
* the red lane whenever somebody passed roughness first — right in every test that encoded all
|
|
262
|
+
* four in order, wrong for every other caller, and silent either way. Lanes nothing was encoded
|
|
263
|
+
* into stay zero.
|
|
264
|
+
*
|
|
265
|
+
* The dequantisation is folded in: `value = Σ axis[k][c] * (low[k] + q[k] * span[k]) + mean[c]`.
|
|
266
|
+
*/
|
|
267
|
+
let outputs = 0;
|
|
268
|
+
for (const channel of channels) outputs = Math.max(outputs, channel.spec.component + 1);
|
|
269
|
+
const shape: NetworkShape = { inputs: components, hidden: [], outputs };
|
|
270
|
+
const weights = new Float32Array(components * outputs + outputs);
|
|
271
|
+
for (let c = 0; c < count; c += 1) {
|
|
272
|
+
const lane = (channels[c] as ChannelInput).spec.component;
|
|
273
|
+
let bias = mean[c] as number;
|
|
274
|
+
for (let k = 0; k < components; k += 1) {
|
|
275
|
+
const axis = axes[k] as number[];
|
|
276
|
+
const span = (high[k] as number) - (low[k] as number);
|
|
277
|
+
weights[lane * components + k] = (axis[c] as number) * span;
|
|
278
|
+
bias += (axis[c] as number) * (low[k] as number);
|
|
279
|
+
}
|
|
280
|
+
weights[components * outputs + lane] = bias;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const graph = createDecodeGraph(2);
|
|
284
|
+
addDecodeNode(graph, DECODE_OP.SAMPLE_LATENT, 0, 0, 0);
|
|
285
|
+
addDecodeNode(graph, DECODE_OP.EVAL_NETWORK, 0, 0, 1);
|
|
286
|
+
graph.result = 1;
|
|
287
|
+
graph.addressMode = options.addressMode ?? 0;
|
|
288
|
+
|
|
289
|
+
const { mips } = buildMips(latent, latentWidth, latentHeight, components);
|
|
290
|
+
const roughness = varianceRoughness(channels, width, height);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
latent,
|
|
294
|
+
latentWidth,
|
|
295
|
+
latentHeight,
|
|
296
|
+
components,
|
|
297
|
+
weights,
|
|
298
|
+
shape,
|
|
299
|
+
graph,
|
|
300
|
+
mips,
|
|
301
|
+
mipRoughness: roughness,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The roughness a variance-preserving reduction hands back, or `null` for channels that need none.
|
|
307
|
+
*
|
|
308
|
+
* **Computed from the source channels and not from the latent**, which is the mistake the first
|
|
309
|
+
* version made. The latent is a principal-component projection: three normal channels whose
|
|
310
|
+
* structure is one direction collapse to *one* component, so a check for three latent components
|
|
311
|
+
* finds none and silently skips the reduction on exactly the surfaces that need it. What decides
|
|
312
|
+
* is the semantic of the channels that went in.
|
|
313
|
+
*
|
|
314
|
+
* Averaging normals that disagree flattens them, and a surface that reads as polished at distance
|
|
315
|
+
* is the defect `needsVarianceMips` exists to prevent. The disagreement goes into a roughness the
|
|
316
|
+
* caller ships beside the mip rather than being thrown away.
|
|
317
|
+
*/
|
|
318
|
+
function varianceRoughness(
|
|
319
|
+
channels: readonly ChannelInput[],
|
|
320
|
+
width: number,
|
|
321
|
+
height: number,
|
|
322
|
+
): Float32Array | null {
|
|
323
|
+
const normal = channels.filter((channel) => needsVarianceMips(channel.spec.semantic));
|
|
324
|
+
/* Three components or it is not a normal, and reducing two of one would invent the third. */
|
|
325
|
+
if (normal.length < 3) return null;
|
|
326
|
+
const src = new Float32Array(width * height * 3);
|
|
327
|
+
for (const channel of normal) {
|
|
328
|
+
const component = channel.spec.component;
|
|
329
|
+
if (component > 2) continue;
|
|
330
|
+
for (let at = 0; at < width * height; at += 1) {
|
|
331
|
+
src[at * 3 + component] = channel.data[at] as number;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const outNormal = new Float32Array(Math.max(1, width >> 1) * Math.max(1, height >> 1) * 3);
|
|
335
|
+
const outRoughness = new Float32Array(Math.max(1, width >> 1) * Math.max(1, height >> 1));
|
|
336
|
+
reduceNormalMip(src, width, height, outNormal, outRoughness);
|
|
337
|
+
return outRoughness;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The mip chain, halving to one texel. */
|
|
341
|
+
function buildMips(
|
|
342
|
+
base: Uint8Array,
|
|
343
|
+
width: number,
|
|
344
|
+
height: number,
|
|
345
|
+
components: number,
|
|
346
|
+
): { mips: { width: number; height: number; data: Uint8Array }[] } {
|
|
347
|
+
const mips: { width: number; height: number; data: Uint8Array }[] = [
|
|
348
|
+
{ width, height, data: base },
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
let current = mips[0] as { width: number; height: number; data: Uint8Array };
|
|
352
|
+
while (current.width > 1 || current.height > 1) {
|
|
353
|
+
const nextWidth = Math.max(1, current.width >> 1);
|
|
354
|
+
const nextHeight = Math.max(1, current.height >> 1);
|
|
355
|
+
const data = new Uint8Array(nextWidth * nextHeight * components);
|
|
356
|
+
for (let y = 0; y < nextHeight; y += 1) {
|
|
357
|
+
for (let x = 0; x < nextWidth; x += 1) {
|
|
358
|
+
for (let k = 0; k < components; k += 1) {
|
|
359
|
+
let sum = 0;
|
|
360
|
+
let taken = 0;
|
|
361
|
+
for (let dy = 0; dy < 2; dy += 1) {
|
|
362
|
+
for (let dx = 0; dx < 2; dx += 1) {
|
|
363
|
+
const sx = Math.min(current.width - 1, x * 2 + dx);
|
|
364
|
+
const sy = Math.min(current.height - 1, y * 2 + dy);
|
|
365
|
+
sum += current.data[(sy * current.width + sx) * components + k] as number;
|
|
366
|
+
taken += 1;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
data[(y * nextWidth + x) * components + k] = Math.round(sum / taken);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
current = { width: nextWidth, height: nextHeight, data };
|
|
374
|
+
mips.push(current);
|
|
375
|
+
}
|
|
376
|
+
return { mips };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The latent as the interpreter takes it: normalised floats, `components` wide, with its chain. */
|
|
380
|
+
export function latentImageOf(encoded: EncodedMaterial): LatentImage {
|
|
381
|
+
return {
|
|
382
|
+
data: Float32Array.from(encoded.latent, (value) => value / 255),
|
|
383
|
+
width: encoded.latentWidth,
|
|
384
|
+
height: encoded.latentHeight,
|
|
385
|
+
channels: encoded.components,
|
|
386
|
+
mips: encoded.mips.slice(1).map((level) => ({
|
|
387
|
+
data: Float32Array.from(level.data, (value) => value / 255),
|
|
388
|
+
width: level.width,
|
|
389
|
+
height: level.height,
|
|
390
|
+
})),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const SCRATCH = new Float32Array(64);
|
|
395
|
+
const INPUT = new Float32Array(4);
|
|
396
|
+
|
|
397
|
+
/** Decode at a coordinate, the way the graph does, without building a resource table. */
|
|
398
|
+
export function decodeMaterialAt(
|
|
399
|
+
encoded: EncodedMaterial,
|
|
400
|
+
u: number,
|
|
401
|
+
v: number,
|
|
402
|
+
out: Float32Array,
|
|
403
|
+
): void {
|
|
404
|
+
const su = Math.min(1, Math.max(0, u)) * (encoded.latentWidth - 1);
|
|
405
|
+
const sv = Math.min(1, Math.max(0, v)) * (encoded.latentHeight - 1);
|
|
406
|
+
const x0 = Math.floor(su);
|
|
407
|
+
const y0 = Math.floor(sv);
|
|
408
|
+
const x1 = Math.min(encoded.latentWidth - 1, x0 + 1);
|
|
409
|
+
const y1 = Math.min(encoded.latentHeight - 1, y0 + 1);
|
|
410
|
+
const fx = su - x0;
|
|
411
|
+
const fy = sv - y0;
|
|
412
|
+
for (let k = 0; k < encoded.components; k += 1) {
|
|
413
|
+
const at = (x: number, y: number): number =>
|
|
414
|
+
(encoded.latent[(y * encoded.latentWidth + x) * encoded.components + k] as number) / 255;
|
|
415
|
+
const top = at(x0, y0) * (1 - fx) + at(x1, y0) * fx;
|
|
416
|
+
const bottom = at(x0, y1) * (1 - fx) + at(x1, y1) * fx;
|
|
417
|
+
INPUT[k] = top * (1 - fy) + bottom * fy;
|
|
418
|
+
}
|
|
419
|
+
evalNetwork(encoded.shape, encoded.weights, INPUT, out, SCRATCH);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** What the encoded material costs: the latent, its mips, and the weights. */
|
|
423
|
+
export function materialBytes(encoded: EncodedMaterial): number {
|
|
424
|
+
let bytes = 0;
|
|
425
|
+
for (const mip of encoded.mips) bytes += mip.data.byteLength;
|
|
426
|
+
return bytes + encoded.weights.byteLength;
|
|
427
|
+
}
|