@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,84 @@
|
|
|
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 { type ChannelSpec, type DecodeGraph, type LatentImage, type NetworkShape } from '@driftengine/texture';
|
|
33
|
+
export interface ChannelInput {
|
|
34
|
+
readonly spec: ChannelSpec;
|
|
35
|
+
/** `width * height` values, row-major with x fastest. */
|
|
36
|
+
readonly data: Float32Array;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The most latent components a sampled texel can carry: three, not four.
|
|
40
|
+
*
|
|
41
|
+
* `SAMPLE_LATENT` fills a four-lane register and the fourth lane is the format's alpha, which an
|
|
42
|
+
* image with fewer channels reads as 1. A fourth component would be read by the network as a
|
|
43
|
+
* constant — silently, and only where the encoder had wanted to use it.
|
|
44
|
+
*/
|
|
45
|
+
export declare const LATENT_MAX_COMPONENTS = 3;
|
|
46
|
+
export interface EncodedMaterial {
|
|
47
|
+
readonly latent: Uint8Array;
|
|
48
|
+
readonly latentWidth: number;
|
|
49
|
+
readonly latentHeight: number;
|
|
50
|
+
readonly components: number;
|
|
51
|
+
readonly weights: Float32Array;
|
|
52
|
+
readonly shape: NetworkShape;
|
|
53
|
+
readonly graph: DecodeGraph;
|
|
54
|
+
readonly mips: readonly {
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
data: Uint8Array;
|
|
58
|
+
}[];
|
|
59
|
+
/**
|
|
60
|
+
* The roughness a variance-preserving mip reduction handed back, or `null`.
|
|
61
|
+
*
|
|
62
|
+
* Non-null exactly when a channel's semantic says mipping it must preserve the normal
|
|
63
|
+
* distribution. A normal map averaged naively loses the disagreement between neighbouring
|
|
64
|
+
* normals, and the surface reads as polished at distance — the roughness is where that
|
|
65
|
+
* disagreement goes instead of being thrown away.
|
|
66
|
+
*/
|
|
67
|
+
readonly mipRoughness: Float32Array | null;
|
|
68
|
+
}
|
|
69
|
+
export interface EncodeMaterialOptions {
|
|
70
|
+
/** 0 to 1. Buys latent resolution, and with it size and accuracy. */
|
|
71
|
+
readonly quality: number;
|
|
72
|
+
/**
|
|
73
|
+
* One of `ADDRESS_MODE`. Defaults to lattice clamp, which is what every bake before 2026-09-17
|
|
74
|
+
* produced. A tiling surface texture wants `ADDRESS_MODE.CENTRE_WRAP`.
|
|
75
|
+
*/
|
|
76
|
+
readonly addressMode?: number;
|
|
77
|
+
}
|
|
78
|
+
export declare function encodeMaterial(channels: readonly ChannelInput[], width: number, height: number, options: EncodeMaterialOptions): EncodedMaterial;
|
|
79
|
+
/** The latent as the interpreter takes it: normalised floats, `components` wide, with its chain. */
|
|
80
|
+
export declare function latentImageOf(encoded: EncodedMaterial): LatentImage;
|
|
81
|
+
/** Decode at a coordinate, the way the graph does, without building a resource table. */
|
|
82
|
+
export declare function decodeMaterialAt(encoded: EncodedMaterial, u: number, v: number, out: Float32Array): void;
|
|
83
|
+
/** What the encoded material costs: the latent, its mips, and the weights. */
|
|
84
|
+
export declare function materialBytes(encoded: EncodedMaterial): number;
|
|
@@ -0,0 +1,339 @@
|
|
|
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 { DECODE_OP, addDecodeNode, createDecodeGraph, evalNetwork, reduceNormalMip, needsVarianceMips, } from '@driftengine/texture';
|
|
33
|
+
/**
|
|
34
|
+
* The most latent components a sampled texel can carry: three, not four.
|
|
35
|
+
*
|
|
36
|
+
* `SAMPLE_LATENT` fills a four-lane register and the fourth lane is the format's alpha, which an
|
|
37
|
+
* image with fewer channels reads as 1. A fourth component would be read by the network as a
|
|
38
|
+
* constant — silently, and only where the encoder had wanted to use it.
|
|
39
|
+
*/
|
|
40
|
+
export const LATENT_MAX_COMPONENTS = 3;
|
|
41
|
+
/** How much of the channels' variance a component has to explain before it is worth its bytes. */
|
|
42
|
+
const COMPONENT_THRESHOLD = 0.01;
|
|
43
|
+
/** Latent grid size for a quality, as a fraction of the source. Monotonic by construction. */
|
|
44
|
+
function latentSizeFor(width, height, quality) {
|
|
45
|
+
const q = Math.min(1, Math.max(0, quality));
|
|
46
|
+
/* An eighth at quality 0 up to the full size at 1, rounded to whole texels and never below 2. */
|
|
47
|
+
const scale = 0.125 + 0.875 * q;
|
|
48
|
+
return [Math.max(2, Math.round(width * scale)), Math.max(2, Math.round(height * scale))];
|
|
49
|
+
}
|
|
50
|
+
/** Average the source down to the latent grid, by box filter over whole source texels. */
|
|
51
|
+
function downsample(data, width, height, outWidth, outHeight, out, stride, offset) {
|
|
52
|
+
for (let y = 0; y < outHeight; y += 1) {
|
|
53
|
+
const y0 = Math.floor((y * height) / outHeight);
|
|
54
|
+
const y1 = Math.max(y0 + 1, Math.floor(((y + 1) * height) / outHeight));
|
|
55
|
+
for (let x = 0; x < outWidth; x += 1) {
|
|
56
|
+
const x0 = Math.floor((x * width) / outWidth);
|
|
57
|
+
const x1 = Math.max(x0 + 1, Math.floor(((x + 1) * width) / outWidth));
|
|
58
|
+
let sum = 0;
|
|
59
|
+
let count = 0;
|
|
60
|
+
for (let sy = y0; sy < y1; sy += 1) {
|
|
61
|
+
for (let sx = x0; sx < x1; sx += 1) {
|
|
62
|
+
sum += data[sy * width + sx];
|
|
63
|
+
count += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
out[(y * outWidth + x) * stride + offset] = count === 0 ? 0 : sum / count;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The top `want` principal directions of a small covariance, by power iteration with deflation.
|
|
72
|
+
*
|
|
73
|
+
* Deterministic: the start vector is fixed rather than random, and the iteration count is fixed
|
|
74
|
+
* rather than a convergence test — a bake whose output depends on how quickly something converged
|
|
75
|
+
* is a bake that produces a different container from the same source.
|
|
76
|
+
*/
|
|
77
|
+
function principalAxes(covariance, size, want) {
|
|
78
|
+
const work = Float64Array.from(covariance);
|
|
79
|
+
const axes = [];
|
|
80
|
+
const eigenvalues = [];
|
|
81
|
+
for (let k = 0; k < want; k += 1) {
|
|
82
|
+
let vector = Array.from({ length: size }, (_, i) => (i === k % size ? 1 : 0.5));
|
|
83
|
+
let value = 0;
|
|
84
|
+
for (let step = 0; step < 64; step += 1) {
|
|
85
|
+
const next = new Array(size).fill(0);
|
|
86
|
+
for (let r = 0; r < size; r += 1) {
|
|
87
|
+
for (let c = 0; c < size; c += 1) {
|
|
88
|
+
next[r] = next[r] + work[r * size + c] * vector[c];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const length = Math.hypot(...next);
|
|
92
|
+
if (length < 1e-12)
|
|
93
|
+
break;
|
|
94
|
+
for (let i = 0; i < size; i += 1)
|
|
95
|
+
next[i] = next[i] / length;
|
|
96
|
+
vector = next;
|
|
97
|
+
value = length;
|
|
98
|
+
}
|
|
99
|
+
axes.push(vector);
|
|
100
|
+
eigenvalues.push(value);
|
|
101
|
+
/* Deflate, so the next iteration finds the next direction rather than the same one. */
|
|
102
|
+
for (let r = 0; r < size; r += 1) {
|
|
103
|
+
for (let c = 0; c < size; c += 1) {
|
|
104
|
+
work[r * size + c] =
|
|
105
|
+
work[r * size + c] - value * vector[r] * vector[c];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
AXIS_VALUES.length = 0;
|
|
110
|
+
for (const value of eigenvalues)
|
|
111
|
+
AXIS_VALUES.push(value);
|
|
112
|
+
return axes;
|
|
113
|
+
}
|
|
114
|
+
/** The eigenvalues the last `principalAxes` found, so the caller can drop the useless ones. */
|
|
115
|
+
const AXIS_VALUES = [];
|
|
116
|
+
export function encodeMaterial(channels, width, height, options) {
|
|
117
|
+
if (channels.length === 0)
|
|
118
|
+
throw new Error('encodeMaterial: no channels');
|
|
119
|
+
const count = channels.length;
|
|
120
|
+
const [latentWidth, latentHeight] = latentSizeFor(width, height, options.quality);
|
|
121
|
+
const texels = latentWidth * latentHeight;
|
|
122
|
+
/* Every channel, averaged onto the latent grid, interleaved so a texel's values are adjacent. */
|
|
123
|
+
const grid = new Float32Array(texels * count);
|
|
124
|
+
for (let c = 0; c < count; c += 1) {
|
|
125
|
+
const channel = channels[c];
|
|
126
|
+
downsample(channel.data, width, height, latentWidth, latentHeight, grid, count, c);
|
|
127
|
+
}
|
|
128
|
+
const mean = new Float64Array(count);
|
|
129
|
+
for (let at = 0; at < texels; at += 1) {
|
|
130
|
+
for (let c = 0; c < count; c += 1)
|
|
131
|
+
mean[c] = mean[c] + grid[at * count + c];
|
|
132
|
+
}
|
|
133
|
+
for (let c = 0; c < count; c += 1)
|
|
134
|
+
mean[c] = mean[c] / texels;
|
|
135
|
+
const covariance = new Float64Array(count * count);
|
|
136
|
+
for (let at = 0; at < texels; at += 1) {
|
|
137
|
+
for (let r = 0; r < count; r += 1) {
|
|
138
|
+
const dr = grid[at * count + r] - mean[r];
|
|
139
|
+
for (let c = 0; c < count; c += 1) {
|
|
140
|
+
const dc = grid[at * count + c] - mean[c];
|
|
141
|
+
covariance[r * count + c] = covariance[r * count + c] + dr * dc;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (let i = 0; i < covariance.length; i += 1) {
|
|
146
|
+
covariance[i] = covariance[i] / Math.max(1, texels - 1);
|
|
147
|
+
}
|
|
148
|
+
const wanted = Math.min(count, LATENT_MAX_COMPONENTS);
|
|
149
|
+
const axes = principalAxes(covariance, count, wanted);
|
|
150
|
+
let total = 0;
|
|
151
|
+
for (const value of AXIS_VALUES)
|
|
152
|
+
total += value;
|
|
153
|
+
/*
|
|
154
|
+
* Keep the components that explain something. Two channels carrying the same structure need one;
|
|
155
|
+
* two carrying different structure need two, and the encoder says so rather than spending bytes
|
|
156
|
+
* either way.
|
|
157
|
+
*/
|
|
158
|
+
let components = 0;
|
|
159
|
+
for (let k = 0; k < axes.length; k += 1) {
|
|
160
|
+
if (k > 0 && AXIS_VALUES[k] < total * COMPONENT_THRESHOLD)
|
|
161
|
+
break;
|
|
162
|
+
components += 1;
|
|
163
|
+
}
|
|
164
|
+
/* Project every latent texel onto the basis, and find each component's range as we go. */
|
|
165
|
+
const projected = new Float32Array(texels * components);
|
|
166
|
+
const low = new Float64Array(components).fill(Infinity);
|
|
167
|
+
const high = new Float64Array(components).fill(-Infinity);
|
|
168
|
+
for (let at = 0; at < texels; at += 1) {
|
|
169
|
+
for (let k = 0; k < components; k += 1) {
|
|
170
|
+
const axis = axes[k];
|
|
171
|
+
let value = 0;
|
|
172
|
+
for (let c = 0; c < count; c += 1) {
|
|
173
|
+
value += (grid[at * count + c] - mean[c]) * axis[c];
|
|
174
|
+
}
|
|
175
|
+
projected[at * components + k] = value;
|
|
176
|
+
if (value < low[k])
|
|
177
|
+
low[k] = value;
|
|
178
|
+
if (value > high[k])
|
|
179
|
+
high[k] = value;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const latent = new Uint8Array(texels * components);
|
|
183
|
+
for (let at = 0; at < texels; at += 1) {
|
|
184
|
+
for (let k = 0; k < components; k += 1) {
|
|
185
|
+
const span = high[k] - low[k];
|
|
186
|
+
const normalised = span === 0 ? 0 : (projected[at * components + k] - low[k]) / span;
|
|
187
|
+
latent[at * components + k] = Math.round(Math.min(1, Math.max(0, normalised)) * 255);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/*
|
|
191
|
+
* One layer, `components` in, and **one output per texel component rather than one per input
|
|
192
|
+
* channel**. A `ChannelSpec` says which of a texel's four lanes its channel occupies, so a
|
|
193
|
+
* decode that wrote its outputs in the order the channels were handed in would put roughness in
|
|
194
|
+
* the red lane whenever somebody passed roughness first — right in every test that encoded all
|
|
195
|
+
* four in order, wrong for every other caller, and silent either way. Lanes nothing was encoded
|
|
196
|
+
* into stay zero.
|
|
197
|
+
*
|
|
198
|
+
* The dequantisation is folded in: `value = Σ axis[k][c] * (low[k] + q[k] * span[k]) + mean[c]`.
|
|
199
|
+
*/
|
|
200
|
+
let outputs = 0;
|
|
201
|
+
for (const channel of channels)
|
|
202
|
+
outputs = Math.max(outputs, channel.spec.component + 1);
|
|
203
|
+
const shape = { inputs: components, hidden: [], outputs };
|
|
204
|
+
const weights = new Float32Array(components * outputs + outputs);
|
|
205
|
+
for (let c = 0; c < count; c += 1) {
|
|
206
|
+
const lane = channels[c].spec.component;
|
|
207
|
+
let bias = mean[c];
|
|
208
|
+
for (let k = 0; k < components; k += 1) {
|
|
209
|
+
const axis = axes[k];
|
|
210
|
+
const span = high[k] - low[k];
|
|
211
|
+
weights[lane * components + k] = axis[c] * span;
|
|
212
|
+
bias += axis[c] * low[k];
|
|
213
|
+
}
|
|
214
|
+
weights[components * outputs + lane] = bias;
|
|
215
|
+
}
|
|
216
|
+
const graph = createDecodeGraph(2);
|
|
217
|
+
addDecodeNode(graph, DECODE_OP.SAMPLE_LATENT, 0, 0, 0);
|
|
218
|
+
addDecodeNode(graph, DECODE_OP.EVAL_NETWORK, 0, 0, 1);
|
|
219
|
+
graph.result = 1;
|
|
220
|
+
graph.addressMode = options.addressMode ?? 0;
|
|
221
|
+
const { mips } = buildMips(latent, latentWidth, latentHeight, components);
|
|
222
|
+
const roughness = varianceRoughness(channels, width, height);
|
|
223
|
+
return {
|
|
224
|
+
latent,
|
|
225
|
+
latentWidth,
|
|
226
|
+
latentHeight,
|
|
227
|
+
components,
|
|
228
|
+
weights,
|
|
229
|
+
shape,
|
|
230
|
+
graph,
|
|
231
|
+
mips,
|
|
232
|
+
mipRoughness: roughness,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The roughness a variance-preserving reduction hands back, or `null` for channels that need none.
|
|
237
|
+
*
|
|
238
|
+
* **Computed from the source channels and not from the latent**, which is the mistake the first
|
|
239
|
+
* version made. The latent is a principal-component projection: three normal channels whose
|
|
240
|
+
* structure is one direction collapse to *one* component, so a check for three latent components
|
|
241
|
+
* finds none and silently skips the reduction on exactly the surfaces that need it. What decides
|
|
242
|
+
* is the semantic of the channels that went in.
|
|
243
|
+
*
|
|
244
|
+
* Averaging normals that disagree flattens them, and a surface that reads as polished at distance
|
|
245
|
+
* is the defect `needsVarianceMips` exists to prevent. The disagreement goes into a roughness the
|
|
246
|
+
* caller ships beside the mip rather than being thrown away.
|
|
247
|
+
*/
|
|
248
|
+
function varianceRoughness(channels, width, height) {
|
|
249
|
+
const normal = channels.filter((channel) => needsVarianceMips(channel.spec.semantic));
|
|
250
|
+
/* Three components or it is not a normal, and reducing two of one would invent the third. */
|
|
251
|
+
if (normal.length < 3)
|
|
252
|
+
return null;
|
|
253
|
+
const src = new Float32Array(width * height * 3);
|
|
254
|
+
for (const channel of normal) {
|
|
255
|
+
const component = channel.spec.component;
|
|
256
|
+
if (component > 2)
|
|
257
|
+
continue;
|
|
258
|
+
for (let at = 0; at < width * height; at += 1) {
|
|
259
|
+
src[at * 3 + component] = channel.data[at];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const outNormal = new Float32Array(Math.max(1, width >> 1) * Math.max(1, height >> 1) * 3);
|
|
263
|
+
const outRoughness = new Float32Array(Math.max(1, width >> 1) * Math.max(1, height >> 1));
|
|
264
|
+
reduceNormalMip(src, width, height, outNormal, outRoughness);
|
|
265
|
+
return outRoughness;
|
|
266
|
+
}
|
|
267
|
+
/** The mip chain, halving to one texel. */
|
|
268
|
+
function buildMips(base, width, height, components) {
|
|
269
|
+
const mips = [
|
|
270
|
+
{ width, height, data: base },
|
|
271
|
+
];
|
|
272
|
+
let current = mips[0];
|
|
273
|
+
while (current.width > 1 || current.height > 1) {
|
|
274
|
+
const nextWidth = Math.max(1, current.width >> 1);
|
|
275
|
+
const nextHeight = Math.max(1, current.height >> 1);
|
|
276
|
+
const data = new Uint8Array(nextWidth * nextHeight * components);
|
|
277
|
+
for (let y = 0; y < nextHeight; y += 1) {
|
|
278
|
+
for (let x = 0; x < nextWidth; x += 1) {
|
|
279
|
+
for (let k = 0; k < components; k += 1) {
|
|
280
|
+
let sum = 0;
|
|
281
|
+
let taken = 0;
|
|
282
|
+
for (let dy = 0; dy < 2; dy += 1) {
|
|
283
|
+
for (let dx = 0; dx < 2; dx += 1) {
|
|
284
|
+
const sx = Math.min(current.width - 1, x * 2 + dx);
|
|
285
|
+
const sy = Math.min(current.height - 1, y * 2 + dy);
|
|
286
|
+
sum += current.data[(sy * current.width + sx) * components + k];
|
|
287
|
+
taken += 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
data[(y * nextWidth + x) * components + k] = Math.round(sum / taken);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
current = { width: nextWidth, height: nextHeight, data };
|
|
295
|
+
mips.push(current);
|
|
296
|
+
}
|
|
297
|
+
return { mips };
|
|
298
|
+
}
|
|
299
|
+
/** The latent as the interpreter takes it: normalised floats, `components` wide, with its chain. */
|
|
300
|
+
export function latentImageOf(encoded) {
|
|
301
|
+
return {
|
|
302
|
+
data: Float32Array.from(encoded.latent, (value) => value / 255),
|
|
303
|
+
width: encoded.latentWidth,
|
|
304
|
+
height: encoded.latentHeight,
|
|
305
|
+
channels: encoded.components,
|
|
306
|
+
mips: encoded.mips.slice(1).map((level) => ({
|
|
307
|
+
data: Float32Array.from(level.data, (value) => value / 255),
|
|
308
|
+
width: level.width,
|
|
309
|
+
height: level.height,
|
|
310
|
+
})),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const SCRATCH = new Float32Array(64);
|
|
314
|
+
const INPUT = new Float32Array(4);
|
|
315
|
+
/** Decode at a coordinate, the way the graph does, without building a resource table. */
|
|
316
|
+
export function decodeMaterialAt(encoded, u, v, out) {
|
|
317
|
+
const su = Math.min(1, Math.max(0, u)) * (encoded.latentWidth - 1);
|
|
318
|
+
const sv = Math.min(1, Math.max(0, v)) * (encoded.latentHeight - 1);
|
|
319
|
+
const x0 = Math.floor(su);
|
|
320
|
+
const y0 = Math.floor(sv);
|
|
321
|
+
const x1 = Math.min(encoded.latentWidth - 1, x0 + 1);
|
|
322
|
+
const y1 = Math.min(encoded.latentHeight - 1, y0 + 1);
|
|
323
|
+
const fx = su - x0;
|
|
324
|
+
const fy = sv - y0;
|
|
325
|
+
for (let k = 0; k < encoded.components; k += 1) {
|
|
326
|
+
const at = (x, y) => encoded.latent[(y * encoded.latentWidth + x) * encoded.components + k] / 255;
|
|
327
|
+
const top = at(x0, y0) * (1 - fx) + at(x1, y0) * fx;
|
|
328
|
+
const bottom = at(x0, y1) * (1 - fx) + at(x1, y1) * fx;
|
|
329
|
+
INPUT[k] = top * (1 - fy) + bottom * fy;
|
|
330
|
+
}
|
|
331
|
+
evalNetwork(encoded.shape, encoded.weights, INPUT, out, SCRATCH);
|
|
332
|
+
}
|
|
333
|
+
/** What the encoded material costs: the latent, its mips, and the weights. */
|
|
334
|
+
export function materialBytes(encoded) {
|
|
335
|
+
let bytes = 0;
|
|
336
|
+
for (const mip of encoded.mips)
|
|
337
|
+
bytes += mip.data.byteLength;
|
|
338
|
+
return bytes + encoded.weights.byteLength;
|
|
339
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** A signed distance field per object, baked offline, for tracing light through a world. */
|
|
2
|
+
import type { MeshData } from '@driftengine/drft';
|
|
3
|
+
/**
|
|
4
|
+
* What an object's distance field is, and why it is baked rather than computed.
|
|
5
|
+
*
|
|
6
|
+
* **Indirect light needs to ask what is in a direction, and a triangle list cannot answer that
|
|
7
|
+
* cheaply.** A ray against a mesh is a traversal; a ray against a distance field is a march that
|
|
8
|
+
* steps by the field's own value and converges in a handful of steps, because the value *is* a
|
|
9
|
+
* safe step. That is the whole of sphere tracing, and the one property it rests on is that the
|
|
10
|
+
* field is a distance — gradient of unit length — rather than merely something with the right
|
|
11
|
+
* sign. `sdf.test.ts` measures that property rather than assuming it.
|
|
12
|
+
*
|
|
13
|
+
* **Per object rather than per world**, because the world moves: `globalField.ts` composes these
|
|
14
|
+
* into a field around the camera each frame, and an instance that rotates or streams in has to
|
|
15
|
+
* bring its field with it. A world-scale bake would have to be redone whenever anything moved.
|
|
16
|
+
*/
|
|
17
|
+
export interface ObjectSdf {
|
|
18
|
+
/** Distance in metres, negative inside. `x` fastest, then `y`, then `z`. */
|
|
19
|
+
readonly field: Float32Array;
|
|
20
|
+
/** Samples on each axis. The spacing is the same on all three; see `bakeObjectSdf`. */
|
|
21
|
+
readonly dims: readonly [number, number, number];
|
|
22
|
+
/** The field's own extent, `[minX, minY, minZ, maxX, maxY, maxZ]`. Padded past the mesh. */
|
|
23
|
+
readonly bounds: Float32Array;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Bake one mesh into a signed distance field.
|
|
27
|
+
*
|
|
28
|
+
* `resolution` is voxels along the mesh's **longest** axis; the other two get however many the
|
|
29
|
+
* same spacing needs. **Cubic voxels are not tidiness** — a field with a different step per axis
|
|
30
|
+
* has a gradient whose length varies with direction, which is the one property a sphere trace
|
|
31
|
+
* depends on, and the failure is a march that overshoots along the coarse axis.
|
|
32
|
+
*
|
|
33
|
+
* **The sign comes from crossing parity, not from the winding.** A face normal, an
|
|
34
|
+
* angle-weighted pseudonormal and a generalised winding number all flip when a mesh is exported
|
|
35
|
+
* with its triangles the other way round, and importers disagree about that constantly — so a
|
|
36
|
+
* model from the wrong tool would come back solid where it is empty. Counting how many times a
|
|
37
|
+
* ray along `x` crosses the surface does not care which way a triangle faces.
|
|
38
|
+
*
|
|
39
|
+
* **The magnitude comes from exact point-to-triangle distance, not from voxelisation.** Marking
|
|
40
|
+
* the cells a triangle passes through and flooding outward loses any wall thinner than a cell,
|
|
41
|
+
* because no cell centre is inside it — and a wall thinner than a cell is most of them. This
|
|
42
|
+
* measures the distance from every grid point in a band around each triangle to that triangle,
|
|
43
|
+
* so a plate a fifth of a voxel thick is still a plate at a fifth of a voxel.
|
|
44
|
+
*/
|
|
45
|
+
export declare function bakeObjectSdf(mesh: MeshData, resolution: number): ObjectSdf;
|