three-vat 4.0.0 → 4.2.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/README.md +39 -10
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +585 -0
- package/dist/carrier-DmKhB4QL.d.ts +13 -0
- package/dist/{chunk-BSAJL5PG.js → chunk-4IKOXUSX.js} +99 -26
- package/dist/chunk-7TT7B5WS.js +263 -0
- package/dist/chunk-P6AFXEBT.js +1104 -0
- package/dist/index.d.ts +46 -9
- package/dist/index.js +145 -1021
- package/dist/tsl.d.ts +2 -1
- package/dist/tsl.js +40 -20
- package/dist/{carrier-D4pkftSk.d.ts → types-CDPXKe0_.d.ts} +119 -36
- package/dist/webgl.d.ts +2 -1
- package/dist/webgl.js +43 -23
- package/dist/write.d.ts +53 -0
- package/dist/write.js +3 -0
- package/package.json +88 -81
|
@@ -3,6 +3,20 @@ import { FloatType, DataTexture, RGBAFormat, RGFormat, UnsignedByteType, HalfFlo
|
|
|
3
3
|
// src/vat-texture.ts
|
|
4
4
|
var MAX_TEXTURE_SIZE = 16384;
|
|
5
5
|
var HALF_FLOAT_MAX = 65504;
|
|
6
|
+
function vertexLayoutFor(vertexCount, totalFrames, maxTextureSize) {
|
|
7
|
+
const rowsPerFrame = Math.max(1, Math.ceil(vertexCount / maxTextureSize));
|
|
8
|
+
const width = vertexWidthOf({ vertexCount, rowsPerFrame });
|
|
9
|
+
const height = totalFrames * rowsPerFrame;
|
|
10
|
+
if (height > maxTextureSize) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`three-vat: totalFrames ${totalFrames} at ${rowsPerFrame} rows a frame is ${height} rows, which exceeds maxTextureSize ${maxTextureSize} \u2014 the vertex encoding spans each frame's ${vertexCount} vertices across ${rowsPerFrame} rows of ${width}; lower fps or bake fewer clips`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return { rowsPerFrame, width, height, frameStride: rowsPerFrame * width };
|
|
16
|
+
}
|
|
17
|
+
function vertexWidthOf({ vertexCount, rowsPerFrame }) {
|
|
18
|
+
return Math.ceil(vertexCount / rowsPerFrame);
|
|
19
|
+
}
|
|
6
20
|
function sampledExactly(tex) {
|
|
7
21
|
tex.minFilter = NearestFilter;
|
|
8
22
|
tex.magFilter = NearestFilter;
|
|
@@ -77,6 +91,7 @@ function crossfadeOf(instance) {
|
|
|
77
91
|
if (!from || !asksToBlend(instance)) return null;
|
|
78
92
|
return { from, duration: instance.fadeDuration };
|
|
79
93
|
}
|
|
94
|
+
var fadeStartOf = (instance) => instance.fadeStart ?? instance.startTime;
|
|
80
95
|
var asksToBlend = (instance) => (instance.fadeDuration ?? 0) > 0;
|
|
81
96
|
function resolveVATFrame(instance, time) {
|
|
82
97
|
const { clip } = instance;
|
|
@@ -84,16 +99,14 @@ function resolveVATFrame(instance, time) {
|
|
|
84
99
|
const last = frames - 1;
|
|
85
100
|
const duration = frames / clip.fps;
|
|
86
101
|
const { loopMode, repetitions, endMode, speed } = resolvedPlaybackOf(instance);
|
|
87
|
-
const
|
|
88
|
-
const
|
|
102
|
+
const reversed = speed < 0;
|
|
103
|
+
const local = (time - instance.startTime) * Math.abs(speed);
|
|
89
104
|
const started = local >= 0;
|
|
105
|
+
const loops = Math.max(local, 0) / duration;
|
|
90
106
|
const finished = started && repetitions !== INFINITE_REPETITIONS && loops >= repetitions;
|
|
91
107
|
let phase;
|
|
92
108
|
let looping;
|
|
93
|
-
if (
|
|
94
|
-
phase = 0;
|
|
95
|
-
looping = false;
|
|
96
|
-
} else if (finished) {
|
|
109
|
+
if (finished) {
|
|
97
110
|
phase = endMode === EndMode.Clamp ? 1 : 0;
|
|
98
111
|
looping = false;
|
|
99
112
|
} else if (loopMode === LoopMode.PingPong) {
|
|
@@ -104,15 +117,18 @@ function resolveVATFrame(instance, time) {
|
|
|
104
117
|
phase = loops % 1;
|
|
105
118
|
looping = true;
|
|
106
119
|
}
|
|
107
|
-
const holds = looping && endMode === EndMode.Clamp &&
|
|
120
|
+
const holds = looping && repetitions !== INFINITE_REPETITIONS && (reversed ? loops < 1 : endMode === EndMode.Clamp && Math.floor(loops) + 1 >= repetitions);
|
|
108
121
|
const wraps = looping && !holds;
|
|
109
|
-
const
|
|
122
|
+
const mirrored = 1 - phase;
|
|
123
|
+
phase = reversed ? wraps && mirrored >= 1 ? 0 : mirrored : phase;
|
|
124
|
+
const spread = phase * (looping ? frames : last);
|
|
125
|
+
const f = holds ? Math.min(spread, last) : spread;
|
|
110
126
|
const f0 = Math.min(Math.floor(f), last);
|
|
111
127
|
const next = f0 + 1;
|
|
112
128
|
const f1 = wraps ? next >= frames ? 0 : next : Math.min(next, last);
|
|
113
129
|
const row = clip.startFrame + f0;
|
|
114
130
|
const crossfade = crossfadeOf(instance);
|
|
115
|
-
const elapsed = crossfade ? (time - instance
|
|
131
|
+
const elapsed = crossfade ? (time - fadeStartOf(instance)) / crossfade.duration : 0;
|
|
116
132
|
return {
|
|
117
133
|
row,
|
|
118
134
|
rowNext: clip.startFrame + f1,
|
|
@@ -155,16 +171,8 @@ function createVATPlaybackTexture(instances, options = {}) {
|
|
|
155
171
|
for (let i = 0; i < count; i++) writePack(data, i, instances[i] ?? RESERVED_ROW);
|
|
156
172
|
return { texture: makeVATTexture(data, PACK_WIDTH, count), count };
|
|
157
173
|
}
|
|
158
|
-
var FORWARD_ONLY_REASON = "a baked band plays forward from its own first row, so a negative speed would freeze it on that row rather than run it backwards \u2014 bake a reversed clip instead. A speed of 0 is a held first row, and is fine";
|
|
159
174
|
var rowStart = (index) => index * PACK_STRIDE;
|
|
160
175
|
var texelStart = (index, field) => rowStart(index) + field * 4;
|
|
161
|
-
function checkedPolicyOf(state, index, what) {
|
|
162
|
-
const policy = resolvedPlaybackOf(state);
|
|
163
|
-
if (policy.speed < 0) {
|
|
164
|
-
throw new Error(`three-vat: ${what} ${index} has speed ${policy.speed}; ${FORWARD_ONLY_REASON}.`);
|
|
165
|
-
}
|
|
166
|
-
return policy;
|
|
167
|
-
}
|
|
168
176
|
function putBand(data, index, at, state, policy) {
|
|
169
177
|
const clip = texelStart(index, at.clip);
|
|
170
178
|
const playback = texelStart(index, at.playback);
|
|
@@ -186,18 +194,24 @@ function checkedCrossfadeOf(instance, index) {
|
|
|
186
194
|
`three-vat: instance ${index} has fadeDuration ${duration}; a transition lasts a finite number of seconds, and 0 (or no fadeDuration at all) is the cut.`
|
|
187
195
|
);
|
|
188
196
|
}
|
|
197
|
+
const start = instance.fadeStart;
|
|
198
|
+
if (start !== void 0 && !Number.isFinite(start)) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`three-vat: instance ${index} has fadeStart ${start}; a blend begins at a finite clock time, and no fadeStart at all begins it at startTime.`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
189
203
|
return crossfadeOf(instance);
|
|
190
204
|
}
|
|
191
205
|
function writePack(data, index, instance) {
|
|
192
206
|
const crossfade = checkedCrossfadeOf(instance, index);
|
|
193
|
-
const live =
|
|
194
|
-
const outgoing = crossfade ? { state: crossfade.from, policy:
|
|
207
|
+
const live = resolvedPlaybackOf(instance);
|
|
208
|
+
const outgoing = crossfade ? { state: crossfade.from, policy: resolvedPlaybackOf(crossfade.from) } : null;
|
|
195
209
|
const crossfadeTexel = texelStart(index, PACK_TEXELS.crossfade);
|
|
196
210
|
putBand(data, index, LIVE_PAIR, instance, live);
|
|
197
211
|
if (outgoing) putBand(data, index, OUTGOING_PAIR, outgoing.state, outgoing.policy);
|
|
198
212
|
else clearTexels(data, index, OUTGOING_PAIR);
|
|
199
213
|
data[crossfadeTexel] = crossfade ? crossfade.duration : 0;
|
|
200
|
-
data[crossfadeTexel + 1] =
|
|
214
|
+
data[crossfadeTexel + 1] = fadeStartOf(instance);
|
|
201
215
|
data[crossfadeTexel + 2] = 0;
|
|
202
216
|
data[crossfadeTexel + 3] = 0;
|
|
203
217
|
}
|
|
@@ -205,9 +219,9 @@ function clearTexels(data, index, at) {
|
|
|
205
219
|
data.fill(0, texelStart(index, at.clip), texelStart(index, at.clip) + 4);
|
|
206
220
|
data.fill(0, texelStart(index, at.playback), texelStart(index, at.playback) + 4);
|
|
207
221
|
}
|
|
208
|
-
function readPack(data, index) {
|
|
209
|
-
const clip = texelStart(index,
|
|
210
|
-
const playback = texelStart(index,
|
|
222
|
+
function readPack(data, index, at = LIVE_PAIR) {
|
|
223
|
+
const clip = texelStart(index, at.clip);
|
|
224
|
+
const playback = texelStart(index, at.playback);
|
|
211
225
|
return {
|
|
212
226
|
clip: { startFrame: data[clip], frames: data[clip + 1], fps: data[clip + 2] },
|
|
213
227
|
startTime: data[playback],
|
|
@@ -217,6 +231,13 @@ function readPack(data, index) {
|
|
|
217
231
|
endMode: data[playback + 3]
|
|
218
232
|
};
|
|
219
233
|
}
|
|
234
|
+
function readInstance(data, index) {
|
|
235
|
+
const live = readPack(data, index);
|
|
236
|
+
const crossfade = texelStart(index, PACK_TEXELS.crossfade);
|
|
237
|
+
const fadeDuration = data[crossfade];
|
|
238
|
+
if (fadeDuration === 0) return live;
|
|
239
|
+
return { ...live, from: readPack(data, index, OUTGOING_PAIR), fadeDuration, fadeStart: data[crossfade + 1] };
|
|
240
|
+
}
|
|
220
241
|
function assertInstance(playback, index) {
|
|
221
242
|
if (!Number.isInteger(index) || index < 0 || index >= playback.count) {
|
|
222
243
|
throw new Error(`three-vat: instance ${index} is outside this crowd's ${playback.count} rows`);
|
|
@@ -227,14 +248,66 @@ function setVATInstance(playback, index, instance) {
|
|
|
227
248
|
const data = playback.texture.image.data;
|
|
228
249
|
const transitioning = instance.from === void 0 && asksToBlend(instance) ? { ...instance, from: readPack(data, index) } : instance;
|
|
229
250
|
writePack(data, index, transitioning);
|
|
251
|
+
flagRow(playback, index);
|
|
252
|
+
}
|
|
253
|
+
function flagRow(playback, index) {
|
|
230
254
|
playback.texture.addUpdateRange(rowStart(index), PACK_STRIDE);
|
|
231
255
|
playback.texture.needsUpdate = true;
|
|
232
256
|
}
|
|
257
|
+
function turnVATInstance(playback, index, time) {
|
|
258
|
+
assertInstance(playback, index);
|
|
259
|
+
const data = playback.texture.image.data;
|
|
260
|
+
const current = readInstance(data, index);
|
|
261
|
+
const { from, fadeDuration, fadeStart: _fadeStart, ...live } = current;
|
|
262
|
+
const outgoing = resolveVATFrame(current, time).outgoing;
|
|
263
|
+
const back = outgoing && outgoing.weight > 0 ? {
|
|
264
|
+
...turnedAt(from, time, true),
|
|
265
|
+
from: turnedAt(live, time, true),
|
|
266
|
+
fadeDuration,
|
|
267
|
+
fadeStart: 2 * time - fadeStartOf(current) - fadeDuration
|
|
268
|
+
} : turnedAt(live, time);
|
|
269
|
+
writePack(data, index, back);
|
|
270
|
+
flagRow(playback, index);
|
|
271
|
+
return back;
|
|
272
|
+
}
|
|
273
|
+
function turnedAt(instance, time, retracesHold = false) {
|
|
274
|
+
const { loopMode, repetitions, endMode, speed } = resolvedPlaybackOf(instance);
|
|
275
|
+
const { startFrame, frames, fps } = instance.clip;
|
|
276
|
+
const clip = { startFrame, frames, fps };
|
|
277
|
+
const rate = Math.abs(speed);
|
|
278
|
+
if (rate === 0) return { clip, startTime: instance.startTime, speed, loopMode, repetitions, endMode };
|
|
279
|
+
const duration = frames / fps;
|
|
280
|
+
const shown = resolveVATFrame(instance, time);
|
|
281
|
+
const endless = repetitions === INFINITE_REPETITIONS;
|
|
282
|
+
const loops = (time - instance.startTime) * rate / duration;
|
|
283
|
+
const waiting = loops < 0;
|
|
284
|
+
const rewound = shown.finished && endMode === EndMode.Rewind;
|
|
285
|
+
const retraced = shown.finished ? repetitions : loops;
|
|
286
|
+
const play = (startTime, speed2, repetitions2) => ({
|
|
287
|
+
clip,
|
|
288
|
+
startTime,
|
|
289
|
+
speed: speed2,
|
|
290
|
+
loopMode,
|
|
291
|
+
repetitions: repetitions2,
|
|
292
|
+
endMode: EndMode.Clamp
|
|
293
|
+
});
|
|
294
|
+
if (waiting || rewound || !endless && retraced === 0) {
|
|
295
|
+
return play(time, shown.phase > 0.5 ? rate : -rate, 0);
|
|
296
|
+
}
|
|
297
|
+
const pingPong = loopMode === LoopMode.PingPong;
|
|
298
|
+
const lead = !pingPong && speed > 0 && endMode === EndMode.Rewind ? 1 : 0;
|
|
299
|
+
const count = endless ? INFINITE_REPETITIONS : pingPong ? 2 * Math.ceil((repetitions - 1) / 2) + 1 : Math.ceil(repetitions) + lead;
|
|
300
|
+
const retracedInBlend = retracesHold && shown.finished && count === repetitions ? loops : retraced;
|
|
301
|
+
const into = endless ? pingPong ? modulo(-retraced - 1, 2) : modulo(-retraced, 1) : count - retracedInBlend;
|
|
302
|
+
return play(time - into * duration / rate, -speed, count);
|
|
303
|
+
}
|
|
304
|
+
var modulo = (a, n) => (a % n + n) % n;
|
|
233
305
|
function endsAt(instance) {
|
|
234
306
|
const { repetitions, speed } = resolvedPlaybackOf(instance);
|
|
235
|
-
|
|
307
|
+
const rate = Math.abs(speed);
|
|
308
|
+
if (repetitions === INFINITE_REPETITIONS || rate === 0) return null;
|
|
236
309
|
const duration = instance.clip.frames / instance.clip.fps;
|
|
237
|
-
return instance.startTime + duration * repetitions /
|
|
310
|
+
return instance.startTime + duration * repetitions / rate;
|
|
238
311
|
}
|
|
239
312
|
|
|
240
313
|
// src/rig-texture.ts
|
|
@@ -246,4 +319,4 @@ var RIG_TEXELS = {
|
|
|
246
319
|
};
|
|
247
320
|
var RIG_TEXELS_PER_SLOT = 2;
|
|
248
321
|
|
|
249
|
-
export { EndMode,
|
|
322
|
+
export { EndMode, HALF_FLOAT_MAX, INFINITE_REPETITIONS, LIBRARY_PLAYBACK_DEFAULTS, LoopMode, MAX_TEXTURE_SIZE, PACK_TEXELS, RIG_TEXELS, RIG_TEXELS_PER_SLOT, createVATPlaybackTexture, endsAt, makeVATNormalTexture, makeVATTexture, resolveVATFrame, setVATInstance, turnVATInstance, vertexLayoutFor, vertexWidthOf };
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { BAKED_FILE_EXTENSION, positionDigest, BAKED_FILE_VERSION, BAKED_LAYER_FORMATS, restSlotsOf } from './chunk-P6AFXEBT.js';
|
|
2
|
+
import { LoaderUtils, Mesh, BufferAttribute, Matrix4, Bone, SkinnedMesh, Skeleton, Group } from 'three';
|
|
3
|
+
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
|
4
|
+
|
|
5
|
+
var IMAGE_EXTENSIONS = ["KHR_texture_basisu", "EXT_texture_webp", "EXT_texture_avif"];
|
|
6
|
+
var MIME_TYPES = {
|
|
7
|
+
png: "image/png",
|
|
8
|
+
jpg: "image/jpeg",
|
|
9
|
+
jpeg: "image/jpeg",
|
|
10
|
+
webp: "image/webp",
|
|
11
|
+
avif: "image/avif",
|
|
12
|
+
ktx2: "image/ktx2"
|
|
13
|
+
};
|
|
14
|
+
async function readSourceImages(parser, readFile = fetchBeside(parser)) {
|
|
15
|
+
const json = parser.json;
|
|
16
|
+
const read = /* @__PURE__ */ new Map();
|
|
17
|
+
const image = async (index) => {
|
|
18
|
+
const cached = read.get(index);
|
|
19
|
+
if (cached) return cached;
|
|
20
|
+
const def = json.images[index];
|
|
21
|
+
const bytes = def.bufferView !== void 0 ? new Uint8Array((await parser.getDependency("bufferView", def.bufferView)).slice(0)) : new Uint8Array(dataURI(def.uri) ?? await readFile(def.uri));
|
|
22
|
+
const mimeType = def.mimeType ?? mimeTypeOf(def.uri);
|
|
23
|
+
if (mimeType === void 0) {
|
|
24
|
+
const where = def.uri.startsWith("data:") ? "embedded" : def.uri;
|
|
25
|
+
throw new Error(`image ${index} (${where}) is of no type a baked file knows`);
|
|
26
|
+
}
|
|
27
|
+
const loaded = { bytes, mimeType, ...def.name ? { name: def.name } : {} };
|
|
28
|
+
read.set(index, loaded);
|
|
29
|
+
return loaded;
|
|
30
|
+
};
|
|
31
|
+
const images = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const [object, reference] of parser.associations) {
|
|
33
|
+
const texture = object;
|
|
34
|
+
if (!texture.isTexture || reference.textures === void 0 || images.has(texture.source)) continue;
|
|
35
|
+
const def = json.textures[reference.textures];
|
|
36
|
+
const entry = { extensions: {}, required: [] };
|
|
37
|
+
if (def.source !== void 0) entry.source = await image(def.source);
|
|
38
|
+
for (const name of IMAGE_EXTENSIONS) {
|
|
39
|
+
const source = def.extensions?.[name]?.source;
|
|
40
|
+
if (source === void 0) continue;
|
|
41
|
+
entry.extensions[name] = await image(source);
|
|
42
|
+
if (json.extensionsRequired?.includes(name)) entry.required.push(name);
|
|
43
|
+
}
|
|
44
|
+
images.set(texture.source, entry);
|
|
45
|
+
}
|
|
46
|
+
return images;
|
|
47
|
+
}
|
|
48
|
+
function dataURI(uri) {
|
|
49
|
+
const data = /^data:([^,]*),(.*)$/s.exec(uri);
|
|
50
|
+
if (!data) return void 0;
|
|
51
|
+
if (/;base64$/.test(data[1])) return Uint8Array.from(atob(data[2]), (c) => c.charCodeAt(0)).buffer;
|
|
52
|
+
return Uint8Array.from(unescape(data[2]), (c) => c.charCodeAt(0)).buffer;
|
|
53
|
+
}
|
|
54
|
+
function fetchBeside(parser) {
|
|
55
|
+
const path = parser.options.path ?? "";
|
|
56
|
+
return async (uri) => {
|
|
57
|
+
const url = LoaderUtils.resolveURL(uri, path);
|
|
58
|
+
const response = await fetch(url);
|
|
59
|
+
if (!response.ok) throw new Error(`cannot fetch ${url}: ${response.status} ${response.statusText}`);
|
|
60
|
+
return response.arrayBuffer();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function mimeTypeOf(uri) {
|
|
64
|
+
const data = /^data:([^;,]+)/.exec(uri);
|
|
65
|
+
if (data) return data[1];
|
|
66
|
+
return MIME_TYPES[/\.([^./]+)$/.exec(uri)?.[1]?.toLowerCase() ?? ""];
|
|
67
|
+
}
|
|
68
|
+
var SLOTS = ["map", "normalMap", "emissiveMap", "aoMap", "metalnessMap", "roughnessMap"];
|
|
69
|
+
function strippedMaterials(materials, images) {
|
|
70
|
+
const originals = /* @__PURE__ */ new Map();
|
|
71
|
+
const stripped = materials.map((material) => {
|
|
72
|
+
const textured = material;
|
|
73
|
+
const keys = Object.keys(material).filter((key) => textured[key]?.isTexture);
|
|
74
|
+
if (keys.length === 0) return material;
|
|
75
|
+
for (const key of keys) refuseUncarriedTexture(material, key, textured[key], images);
|
|
76
|
+
const { metalnessMap, roughnessMap } = textured;
|
|
77
|
+
if (metalnessMap && roughnessMap && metalnessMap.source !== roughnessMap.source) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`three-vat: material "${material.name}" reads metalness and roughness from two images, which glTF packs into one; a baked file carries them as its source packed them`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const copy = material.clone();
|
|
83
|
+
for (const key of keys) copy[key] = null;
|
|
84
|
+
originals.set(copy, textured);
|
|
85
|
+
return copy;
|
|
86
|
+
});
|
|
87
|
+
return { stripped, originals };
|
|
88
|
+
}
|
|
89
|
+
function refuseUncarriedTexture(material, key, texture, images) {
|
|
90
|
+
if (!SLOTS.includes(key)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`three-vat: material "${material.name}" carries a texture in ${key}, which a baked file does not carry; it carries ${SLOTS.join(", ")}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (!images.has(texture.source)) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`three-vat: material "${material.name}" carries a texture (${key}) whose image was not read from a glTF source, so there are no bytes to copy into a baked file`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
var BakedMaterialsWriter = class {
|
|
102
|
+
constructor(writer, originals, sources) {
|
|
103
|
+
this.writer = writer;
|
|
104
|
+
this.originals = originals;
|
|
105
|
+
this.sources = sources;
|
|
106
|
+
this.name = "three-vat:materials";
|
|
107
|
+
this.images = /* @__PURE__ */ new Map();
|
|
108
|
+
this.textures = /* @__PURE__ */ new Map();
|
|
109
|
+
}
|
|
110
|
+
async writeMaterialAsync(material, def) {
|
|
111
|
+
const original = this.originals.get(material);
|
|
112
|
+
if (original === void 0) return;
|
|
113
|
+
const { map, normalMap, emissiveMap, aoMap, metalnessMap, roughnessMap } = original;
|
|
114
|
+
def.pbrMetallicRoughness ?? (def.pbrMetallicRoughness = {});
|
|
115
|
+
if (map) def.pbrMetallicRoughness.baseColorTexture = this.info(map);
|
|
116
|
+
const metalRough = metalnessMap ?? roughnessMap;
|
|
117
|
+
if (metalRough) def.pbrMetallicRoughness.metallicRoughnessTexture = this.info(metalRough);
|
|
118
|
+
if (normalMap) {
|
|
119
|
+
def.normalTexture = this.info(normalMap);
|
|
120
|
+
const scale = original.normalScale.x;
|
|
121
|
+
if (scale !== 1) def.normalTexture.scale = scale;
|
|
122
|
+
}
|
|
123
|
+
if (emissiveMap) def.emissiveTexture = this.info(emissiveMap);
|
|
124
|
+
if (aoMap) {
|
|
125
|
+
def.occlusionTexture = this.info(aoMap);
|
|
126
|
+
if (original.aoMapIntensity !== 1) def.occlusionTexture.strength = original.aoMapIntensity;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
info(texture) {
|
|
130
|
+
const info = { index: this.texture(texture), texCoord: texture.channel };
|
|
131
|
+
this.writer.applyTextureTransform(info, texture);
|
|
132
|
+
return info;
|
|
133
|
+
}
|
|
134
|
+
/** A texture definition: the source's image layout, with this map's sampler. */
|
|
135
|
+
texture(texture) {
|
|
136
|
+
var _a;
|
|
137
|
+
const { writer } = this;
|
|
138
|
+
const { magFilter, minFilter, wrapS, wrapT } = texture;
|
|
139
|
+
const key = [texture.source.uuid, texture.name, magFilter, minFilter, wrapS, wrapT].join(":");
|
|
140
|
+
const cached = this.textures.get(key);
|
|
141
|
+
if (cached !== void 0) return cached;
|
|
142
|
+
const source = this.sources.get(texture.source);
|
|
143
|
+
const def = {
|
|
144
|
+
sampler: writer.processSampler(texture)
|
|
145
|
+
};
|
|
146
|
+
if (texture.name) def.name = texture.name;
|
|
147
|
+
if (source.source) def.source = this.image(source.source);
|
|
148
|
+
for (const [name, image] of Object.entries(source.extensions)) {
|
|
149
|
+
def.extensions = { ...def.extensions, [name]: { source: this.image(image) } };
|
|
150
|
+
writer.extensionsUsed[name] = true;
|
|
151
|
+
if (source.required.includes(name)) writer.extensionsRequired[name] = true;
|
|
152
|
+
}
|
|
153
|
+
const index = ((_a = writer.json).textures ?? (_a.textures = [])).push(def) - 1;
|
|
154
|
+
this.textures.set(key, index);
|
|
155
|
+
return index;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* An image: its bytes as they were, in a buffer view of its own. Not the
|
|
159
|
+
* exporter's `processBufferViewImage`, whose view spans the padding to four
|
|
160
|
+
* bytes as well, so the image it holds would end in up to three zeros.
|
|
161
|
+
*/
|
|
162
|
+
image(image) {
|
|
163
|
+
var _a, _b;
|
|
164
|
+
const cached = this.images.get(image);
|
|
165
|
+
if (cached !== void 0) return cached;
|
|
166
|
+
const { writer } = this;
|
|
167
|
+
const padded = new Uint8Array(Math.ceil(image.bytes.byteLength / 4) * 4);
|
|
168
|
+
padded.set(image.bytes);
|
|
169
|
+
const view = { buffer: writer.processBuffer(padded.buffer), byteOffset: writer.byteOffset, byteLength: image.bytes.byteLength };
|
|
170
|
+
writer.byteOffset += padded.byteLength;
|
|
171
|
+
const bufferView = ((_a = writer.json).bufferViews ?? (_a.bufferViews = [])).push(view) - 1;
|
|
172
|
+
const def = { mimeType: image.mimeType, bufferView };
|
|
173
|
+
if (image.name) def.name = image.name;
|
|
174
|
+
const index = ((_b = writer.json).images ?? (_b.images = [])).push(def) - 1;
|
|
175
|
+
this.images.set(image, index);
|
|
176
|
+
return index;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var KEPT = ["MeshStandardMaterial", "MeshPhysicalMaterial", "MeshBasicMaterial"];
|
|
180
|
+
var LOST = {
|
|
181
|
+
MeshPhongMaterial: ["specular", "shininess"]
|
|
182
|
+
};
|
|
183
|
+
function pbrConversions(materials) {
|
|
184
|
+
return materials.filter((material) => !KEPT.includes(material.type)).map((material) => ({ name: material.name, from: material.type, lost: LOST[material.type] ?? [] }));
|
|
185
|
+
}
|
|
186
|
+
async function writeBakedFile(vat, { images = /* @__PURE__ */ new Map() } = {}) {
|
|
187
|
+
const { stripped, originals } = strippedMaterials(vat.materials, images);
|
|
188
|
+
const mesh = vat.encoding === "rig" ? previewSkin(vat, stripped) : new Mesh(vat.geometry, stripped);
|
|
189
|
+
const exporter = new GLTFExporter().register((writer) => new VATExtensionWriter(writer, vat)).register((writer) => new BakedMaterialsWriter(writer, originals, images));
|
|
190
|
+
const glb = await exporter.parseAsync(mesh, { binary: true });
|
|
191
|
+
return new Uint8Array(glb);
|
|
192
|
+
}
|
|
193
|
+
var VATExtensionWriter = class {
|
|
194
|
+
constructor(writer, vat) {
|
|
195
|
+
this.writer = writer;
|
|
196
|
+
this.vat = vat;
|
|
197
|
+
this.name = BAKED_FILE_EXTENSION;
|
|
198
|
+
this.mesh = -1;
|
|
199
|
+
}
|
|
200
|
+
writeMesh() {
|
|
201
|
+
this.mesh = this.writer.json.meshes?.length ?? 0;
|
|
202
|
+
}
|
|
203
|
+
afterParse() {
|
|
204
|
+
const { vat, writer } = this;
|
|
205
|
+
const common = {
|
|
206
|
+
version: BAKED_FILE_VERSION,
|
|
207
|
+
mesh: this.mesh,
|
|
208
|
+
groups: vat.geometry.groups.map(({ start, count, materialIndex }) => ({ start, count, materialIndex })),
|
|
209
|
+
vertexCount: vat.vertexCount,
|
|
210
|
+
totalFrames: vat.totalFrames,
|
|
211
|
+
clips: vat.clips,
|
|
212
|
+
bounds: { min: vat.bounds.min.toArray(), max: vat.bounds.max.toArray() },
|
|
213
|
+
digest: positionDigest(vat.geometry.attributes.position)
|
|
214
|
+
};
|
|
215
|
+
let extension;
|
|
216
|
+
if (vat.encoding === "rig") {
|
|
217
|
+
extension = {
|
|
218
|
+
...common,
|
|
219
|
+
encoding: "rig",
|
|
220
|
+
slotCount: vat.slotCount,
|
|
221
|
+
layers: { rig: this.layer(vat.rigTexture, "RGBA32F") }
|
|
222
|
+
};
|
|
223
|
+
} else {
|
|
224
|
+
extension = {
|
|
225
|
+
...common,
|
|
226
|
+
encoding: "delta",
|
|
227
|
+
fallback: vat.fallback,
|
|
228
|
+
rowsPerFrame: vat.rowsPerFrame,
|
|
229
|
+
layers: { position: this.layer(vat.positionTexture, "RGBA16F") }
|
|
230
|
+
};
|
|
231
|
+
if (vat.normalTexture) extension.layers.normal = this.layer(vat.normalTexture, "RG8");
|
|
232
|
+
}
|
|
233
|
+
writer.json.extensions = { ...writer.json.extensions, [BAKED_FILE_EXTENSION]: extension };
|
|
234
|
+
writer.extensionsUsed[BAKED_FILE_EXTENSION] = true;
|
|
235
|
+
}
|
|
236
|
+
/** One texture's texels as a buffer view, written as the integers or floats they are. */
|
|
237
|
+
layer(texture, format) {
|
|
238
|
+
const data = texture.image.data;
|
|
239
|
+
const { componentType } = BAKED_LAYER_FORMATS[format];
|
|
240
|
+
const view = this.writer.processBufferView(new BufferAttribute(data, 1), componentType, 0, data.length);
|
|
241
|
+
return { bufferView: view.id, width: texture.image.width, height: texture.image.height, format };
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
function previewSkin(vat, materials) {
|
|
245
|
+
const rest = restSlotsOf(vat);
|
|
246
|
+
if (rest === void 0) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
"three-vat: this rig-encoded VAT does not know its slots' rest matrices, which its baked file's preview skin is built from. Only a VAT bakeVAT or bakeVATInWorker returned carries them, not one loadVAT read; bake the asset and write that"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const matrix = new Matrix4();
|
|
252
|
+
const joints = Array.from({ length: vat.slotCount }, (_, slot) => {
|
|
253
|
+
const joint = new Bone();
|
|
254
|
+
joint.name = `slot ${slot}`;
|
|
255
|
+
matrix.fromArray(rest, slot * 16).decompose(joint.position, joint.quaternion, joint.scale);
|
|
256
|
+
return joint;
|
|
257
|
+
});
|
|
258
|
+
const mesh = new SkinnedMesh(vat.geometry, materials);
|
|
259
|
+
mesh.bind(new Skeleton(joints, joints.map(() => new Matrix4())), new Matrix4());
|
|
260
|
+
return new Group().add(mesh, ...joints);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export { dataURI, pbrConversions, readSourceImages, writeBakedFile };
|