@threenative/assets 0.3.7 → 0.3.9
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/dist/apply-worker.js +227 -22
- package/dist/index.d.ts +46 -19
- package/dist/index.js +303 -29
- package/package.json +2 -2
package/dist/apply-worker.js
CHANGED
|
@@ -15563,9 +15563,9 @@ function readTextureQuality(value) {
|
|
|
15563
15563
|
const row = value;
|
|
15564
15564
|
const positive = (v) => typeof v === "number" && Number.isSafeInteger(v) && v > 0;
|
|
15565
15565
|
const bounded = (v, min2, max2) => typeof v === "number" && Number.isFinite(v) && v >= min2 && v <= max2;
|
|
15566
|
-
if (row.version !== IMAGE_QUALITY_VERSION || ![row.width, row.height, row.sourceWidth, row.sourceHeight].every(positive) || !bounded(row.ssim, -1, 1) || row.meanDeltaE00 !== null && !bounded(row.meanDeltaE00, 0, Number.POSITIVE_INFINITY) || !["pass", "below-floor", "unvalidated-slots"].includes(row.status) || !Array.isArray(row.slots) || !row.slots.every((slot) => typeof slot === "string") || !["uastc", "etc1s", "none"].includes(row.codec) || row.compressionSkipped !== void 0 && !["block-size", "not-smaller", "below-floor"].includes(row.compressionSkipped) || !bounded(row.floor?.ssim, 0, 1) || !bounded(row.floor?.meanDeltaE00, 0, Number.POSITIVE_INFINITY) || row.rung !== void 0 && !["etc1s@150", "uastc+rdo \u03BB3 +zstd", "uastc+rdo \u03BB1 +zstd", "uastc", "none"].includes(
|
|
15566
|
+
if (row.version !== IMAGE_QUALITY_VERSION || ![row.width, row.height, row.sourceWidth, row.sourceHeight].every(positive) || !bounded(row.ssim, -1, 1) || row.meanDeltaE00 !== null && !bounded(row.meanDeltaE00, 0, Number.POSITIVE_INFINITY) || !["pass", "below-floor", "unvalidated-slots"].includes(row.status) || !Array.isArray(row.slots) || !row.slots.every((slot) => typeof slot === "string") || !["uastc", "etc1s", "jpeg", "none"].includes(row.codec) || row.compressionSkipped !== void 0 && !["block-size", "not-smaller", "below-floor"].includes(row.compressionSkipped) || !bounded(row.floor?.ssim, 0, 1) || !bounded(row.floor?.meanDeltaE00, 0, Number.POSITIVE_INFINITY) || row.rung !== void 0 && !["etc1s@150", "uastc+rdo \u03BB3 +zstd", "uastc+rdo \u03BB1 +zstd", "uastc", "none"].includes(
|
|
15567
15567
|
row.rung
|
|
15568
|
-
) && !/^etc1s@\d+$/u.test(row.rung) || !bounded(row.alpha?.ssim, -1, 1) || !bounded(row.alpha?.meanAbsoluteError, 0, 1) || !Array.isArray(row.alpha?.coverage) || !row.alpha.coverage.every(
|
|
15568
|
+
) && !/^etc1s@\d+$/u.test(row.rung) && !/^jpeg@\d+$/u.test(row.rung) || !bounded(row.alpha?.ssim, -1, 1) || !bounded(row.alpha?.meanAbsoluteError, 0, 1) || !Array.isArray(row.alpha?.coverage) || !row.alpha.coverage.every(
|
|
15569
15569
|
(c) => c !== null && typeof c === "object" && bounded(c.threshold, 0, 1) && [c.source, c.decoded, c.changedPixels].every(
|
|
15570
15570
|
(v) => Number.isSafeInteger(v) && v >= 0 && v <= row.width * row.height
|
|
15571
15571
|
)
|
|
@@ -20110,7 +20110,7 @@ function scriptsDirectory() {
|
|
|
20110
20110
|
// Built: `dist/index.js` sits beside `dist/blender-gpl/`.
|
|
20111
20111
|
path2.join(here, "blender-gpl"),
|
|
20112
20112
|
// Source: `src/passes/` -> the workspace sibling.
|
|
20113
|
-
path2.resolve(here, "..", "..", "..", "blender-
|
|
20113
|
+
path2.resolve(here, "..", "..", "..", "blender-bridge", "gpl")
|
|
20114
20114
|
];
|
|
20115
20115
|
const found = candidates.find((candidate) => existsSync(path2.join(candidate, "convert.py")));
|
|
20116
20116
|
if (found === void 0) {
|
|
@@ -20531,6 +20531,7 @@ function bakeStaticLightmap(document, width, height, padding) {
|
|
|
20531
20531
|
width
|
|
20532
20532
|
};
|
|
20533
20533
|
}
|
|
20534
|
+
var BASIS_TRANSCODER_BYTES = 600 * 1024;
|
|
20534
20535
|
var DEFAULT_MAX_SIZE = 2048;
|
|
20535
20536
|
var DEFAULT_ETC1S_QUALITY = 150;
|
|
20536
20537
|
var BLOCK_SIZE = 4;
|
|
@@ -20825,6 +20826,29 @@ async function encodeTextureLadder(data, width, height, options) {
|
|
|
20825
20826
|
if (smallest !== void 0) return smallest;
|
|
20826
20827
|
throw new Error("TN_ASSETS_TEXTURE_FLOOR: forced codec fails its configured floor.");
|
|
20827
20828
|
}
|
|
20829
|
+
var JPEG_QUALITY_RUNGS = [75, 80, 85, 90];
|
|
20830
|
+
async function encodeJpegCandidate(data, width, height, options) {
|
|
20831
|
+
const sharp = (await import('sharp')).default;
|
|
20832
|
+
for (const quality of JPEG_QUALITY_RUNGS) {
|
|
20833
|
+
const buffer = await sharp(Buffer.from(data), {
|
|
20834
|
+
raw: { channels: 4, height, width }
|
|
20835
|
+
}).jpeg({ mozjpeg: true, quality }).toBuffer();
|
|
20836
|
+
const decoded = await decodeImageBytes(buffer, "jpeg-candidate");
|
|
20837
|
+
const score = imageQuality(data, decoded.data, width, height, options);
|
|
20838
|
+
if (score.status !== "pass" || score.alpha.meanAbsoluteError !== 0) continue;
|
|
20839
|
+
return {
|
|
20840
|
+
buffer,
|
|
20841
|
+
quality: {
|
|
20842
|
+
...score,
|
|
20843
|
+
codec: "jpeg",
|
|
20844
|
+
rung: `jpeg@${String(quality)}`,
|
|
20845
|
+
sourceHeight: height,
|
|
20846
|
+
sourceWidth: width
|
|
20847
|
+
}
|
|
20848
|
+
};
|
|
20849
|
+
}
|
|
20850
|
+
return void 0;
|
|
20851
|
+
}
|
|
20828
20852
|
function gpuBytes(width, height, codec) {
|
|
20829
20853
|
return Math.round(width * height * (GPU_BYTES_PER_PIXEL[codec] ?? 4) * MIP_FACTOR);
|
|
20830
20854
|
}
|
|
@@ -20836,6 +20860,7 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
20836
20860
|
const textures = root.listTextures();
|
|
20837
20861
|
if (textures.length === 0) return void 0;
|
|
20838
20862
|
const decoderFree = options.decoderFree === true;
|
|
20863
|
+
const transcoderBytes = Math.max(0, options.transcoderBytes ?? BASIS_TRANSCODER_BYTES);
|
|
20839
20864
|
const quality = Math.min(255, Math.max(1, Math.round(options.quality ?? DEFAULT_ETC1S_QUALITY)));
|
|
20840
20865
|
const keys = textureKeys(root);
|
|
20841
20866
|
const formats = {};
|
|
@@ -20847,6 +20872,9 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
20847
20872
|
let gpuBytesAfter = 0;
|
|
20848
20873
|
let resized = 0;
|
|
20849
20874
|
let compressed = 0;
|
|
20875
|
+
let transcoderSavings = 0;
|
|
20876
|
+
let transcoderRequired = false;
|
|
20877
|
+
const transcoderByImage = {};
|
|
20850
20878
|
for (const [index, texture] of textures.entries()) {
|
|
20851
20879
|
const key = keys[index] ?? `texture#${String(index)}`;
|
|
20852
20880
|
const image = texture.getImage();
|
|
@@ -20875,6 +20903,15 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
20875
20903
|
if (shape.width !== score.sourceWidth || shape.height !== score.sourceHeight) resized += 1;
|
|
20876
20904
|
}
|
|
20877
20905
|
if (fromStore.codec !== "none") compressed += 1;
|
|
20906
|
+
if (fromStore.transcoderSavings !== void 0)
|
|
20907
|
+
transcoderSavings += fromStore.transcoderSavings;
|
|
20908
|
+
if (fromStore.transcoderRequired === true) transcoderRequired = true;
|
|
20909
|
+
if (fromStore.transcoderSavings !== void 0 || fromStore.transcoderRequired === true) {
|
|
20910
|
+
transcoderByImage[key] = {
|
|
20911
|
+
...fromStore.transcoderSavings === void 0 ? {} : { savings: fromStore.transcoderSavings },
|
|
20912
|
+
...fromStore.transcoderRequired === true ? { required: true } : {}
|
|
20913
|
+
};
|
|
20914
|
+
}
|
|
20878
20915
|
continue;
|
|
20879
20916
|
}
|
|
20880
20917
|
const slots = listTextureSlots(texture);
|
|
@@ -20890,6 +20927,10 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
20890
20927
|
}
|
|
20891
20928
|
}
|
|
20892
20929
|
if (texture.getMimeType() === "image/ktx2" && !authoredBasis) {
|
|
20930
|
+
if (!isRawAstc(texture)) {
|
|
20931
|
+
transcoderRequired = true;
|
|
20932
|
+
transcoderByImage[key] = { ...transcoderByImage[key], required: true };
|
|
20933
|
+
}
|
|
20893
20934
|
const shape = imageShape(image, "image/ktx2");
|
|
20894
20935
|
bytesBefore += image.byteLength;
|
|
20895
20936
|
bytesAfter += image.byteLength;
|
|
@@ -21006,6 +21047,14 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
21006
21047
|
};
|
|
21007
21048
|
continue;
|
|
21008
21049
|
}
|
|
21050
|
+
const authoredContainer = !explicit && !authoredBasis && options.jpeg !== false && options.astc !== true && target.width === decoded.width && target.height === decoded.height && UNIVERSAL_IMAGE_CONTAINERS.has(texture.getMimeType()) && slots.some((slot) => COLOUR_SLOTS.has(slot));
|
|
21051
|
+
const jpeg = authoredContainer ? await encodeJpegCandidate(data, target.width, target.height, {
|
|
21052
|
+
...textureQualitySemantics(texture),
|
|
21053
|
+
floor: options.floor
|
|
21054
|
+
}) : void 0;
|
|
21055
|
+
const jpegLimit = jpeg !== void 0 && jpeg.buffer.byteLength < image.byteLength ? jpeg.buffer.byteLength : image.byteLength;
|
|
21056
|
+
const automatic = !explicit && target.width === decoded.width && target.height === decoded.height && UNIVERSAL_IMAGE_CONTAINERS.has(texture.getMimeType());
|
|
21057
|
+
const charged = automatic && options.astc !== true;
|
|
21009
21058
|
let selected;
|
|
21010
21059
|
try {
|
|
21011
21060
|
if (authoredBasis && target.width === decoded.width && target.height === decoded.height) {
|
|
@@ -21035,7 +21084,7 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
21035
21084
|
srgb,
|
|
21036
21085
|
quality,
|
|
21037
21086
|
...options.astc === true ? { astc: true, gltfColorPrimaries: srgb ? 1 : 0 } : {},
|
|
21038
|
-
|
|
21087
|
+
...automatic ? { maxBytes: jpegLimit } : {},
|
|
21039
21088
|
...forced === void 0 ? {} : { forced }
|
|
21040
21089
|
});
|
|
21041
21090
|
}
|
|
@@ -21047,7 +21096,21 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
21047
21096
|
if (!authoredBasis) throw error;
|
|
21048
21097
|
throw new Error(`TN_ASSETS_MODEL_BASIS_ASTC: '${logicalPath}#${key}': ${messageOf2(error)}`);
|
|
21049
21098
|
}
|
|
21050
|
-
|
|
21099
|
+
let encoded = selected.encoded;
|
|
21100
|
+
if (charged && encoded !== void 0) {
|
|
21101
|
+
const savings = jpegLimit - encoded.byteLength;
|
|
21102
|
+
transcoderSavings += savings;
|
|
21103
|
+
if (savings > 0) transcoderByImage[key] = { ...transcoderByImage[key], savings };
|
|
21104
|
+
if (encoded.byteLength >= jpegLimit - transcoderBytes) encoded = void 0;
|
|
21105
|
+
}
|
|
21106
|
+
if (encoded === void 0 && jpeg !== void 0 && jpeg.buffer.byteLength < image.byteLength) {
|
|
21107
|
+
texture.setImage(jpeg.buffer).setMimeType("image/jpeg");
|
|
21108
|
+
bytesAfter += jpeg.buffer.byteLength;
|
|
21109
|
+
gpuBytesAfter += gpuBytes(target.width, target.height, "none");
|
|
21110
|
+
formats[key] = "none";
|
|
21111
|
+
if (options.measureQuality !== false) scores[key] = jpeg.quality;
|
|
21112
|
+
continue;
|
|
21113
|
+
}
|
|
21051
21114
|
if (encoded === void 0 || encoded.byteLength >= image.byteLength && target.width === decoded.width && target.height === decoded.height && !explicit && UNIVERSAL_IMAGE_CONTAINERS.has(texture.getMimeType())) {
|
|
21052
21115
|
const downsampled = target.width !== decoded.width || target.height !== decoded.height;
|
|
21053
21116
|
let retained = image;
|
|
@@ -21090,6 +21153,10 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
21090
21153
|
};
|
|
21091
21154
|
}
|
|
21092
21155
|
texture.setImage(encoded).setMimeType("image/ktx2");
|
|
21156
|
+
if (!charged && options.astc !== true) {
|
|
21157
|
+
transcoderRequired = true;
|
|
21158
|
+
transcoderByImage[key] = { ...transcoderByImage[key], required: true };
|
|
21159
|
+
}
|
|
21093
21160
|
formats[key] = options.astc === true ? "astc4x4" : selected.codec;
|
|
21094
21161
|
bytesAfter += encoded.byteLength;
|
|
21095
21162
|
gpuBytesAfter += gpuBytes(target.width, target.height, formats[key] ?? selected.codec);
|
|
@@ -21117,7 +21184,10 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
21117
21184
|
gpuBytesAfter,
|
|
21118
21185
|
gpuBytesBefore,
|
|
21119
21186
|
resized,
|
|
21120
|
-
skippedCompression
|
|
21187
|
+
skippedCompression,
|
|
21188
|
+
...transcoderSavings > 0 ? { transcoderSavings } : {},
|
|
21189
|
+
...transcoderRequired ? { transcoderRequired: true } : {},
|
|
21190
|
+
...Object.keys(transcoderByImage).length === 0 ? {} : { transcoderByImage }
|
|
21121
21191
|
};
|
|
21122
21192
|
}
|
|
21123
21193
|
function textureQualitySemantics(texture) {
|
|
@@ -21175,12 +21245,6 @@ function texturePass(options = {}) {
|
|
|
21175
21245
|
},
|
|
21176
21246
|
apply: async (input, logicalPath) => {
|
|
21177
21247
|
if (classify2(logicalPath) !== "texture") return input;
|
|
21178
|
-
const stats = textureStats(input);
|
|
21179
|
-
if (stats.width <= 0 || stats.height <= 0) {
|
|
21180
|
-
throw new Error(
|
|
21181
|
-
`TN_ASSETS_TEXTURE_UNREADABLE: '${logicalPath}' has no readable PNG/JPEG header; the KTX2 pass cannot encode it.`
|
|
21182
|
-
);
|
|
21183
|
-
}
|
|
21184
21248
|
const decoded = await decodeImageBytes(input, logicalPath);
|
|
21185
21249
|
const choice = texturePolicy(logicalPath, options);
|
|
21186
21250
|
if (choice.codec === "none")
|
|
@@ -22609,6 +22673,90 @@ function resolveCompactOptions(options) {
|
|
|
22609
22673
|
};
|
|
22610
22674
|
}
|
|
22611
22675
|
|
|
22676
|
+
// src/passes/glb-spec-defaults.ts
|
|
22677
|
+
var JSON_CHUNK = 1313821514;
|
|
22678
|
+
var HEADER = 12;
|
|
22679
|
+
function withoutSpecDefaults(glb) {
|
|
22680
|
+
if (glb.readUInt32LE(16) !== JSON_CHUNK) return glb;
|
|
22681
|
+
const jsonLength = glb.readUInt32LE(12);
|
|
22682
|
+
const json = JSON.parse(glb.subarray(20, 20 + jsonLength).toString("utf8"));
|
|
22683
|
+
for (const accessor of json.accessors ?? []) {
|
|
22684
|
+
if (accessor.normalized === false) accessor.normalized = void 0;
|
|
22685
|
+
if (accessor.byteOffset === 0) accessor.byteOffset = void 0;
|
|
22686
|
+
}
|
|
22687
|
+
for (const view of json.bufferViews ?? []) if (view.byteOffset === 0) view.byteOffset = void 0;
|
|
22688
|
+
for (const animation of json.animations ?? [])
|
|
22689
|
+
for (const sampler of animation.samplers)
|
|
22690
|
+
if (sampler.interpolation === "LINEAR") sampler.interpolation = void 0;
|
|
22691
|
+
const text = Buffer.from(JSON.stringify(json), "utf8");
|
|
22692
|
+
const padded2 = Buffer.alloc(Math.ceil(text.length / 4) * 4, 32);
|
|
22693
|
+
text.copy(padded2);
|
|
22694
|
+
const rest = glb.subarray(HEADER + 8 + jsonLength);
|
|
22695
|
+
const head = Buffer.alloc(HEADER + 8);
|
|
22696
|
+
glb.copy(head, 0, 0, HEADER);
|
|
22697
|
+
head.writeUInt32LE(HEADER + 8 + padded2.length + rest.length, 8);
|
|
22698
|
+
head.writeUInt32LE(padded2.length, 12);
|
|
22699
|
+
head.writeUInt32LE(JSON_CHUNK, 16);
|
|
22700
|
+
return Buffer.concat([head, padded2, rest]);
|
|
22701
|
+
}
|
|
22702
|
+
|
|
22703
|
+
// src/passes/prune-rest-tracks.ts
|
|
22704
|
+
var COMPONENTS = { rotation: 4, scale: 3, translation: 3 };
|
|
22705
|
+
var REST_TOLERANCE = 1e-6;
|
|
22706
|
+
function restOf(node, path7) {
|
|
22707
|
+
if (path7 === "rotation") return node.getRotation();
|
|
22708
|
+
return path7 === "translation" ? node.getTranslation() : node.getScale();
|
|
22709
|
+
}
|
|
22710
|
+
function holdsRest(channel) {
|
|
22711
|
+
const path7 = channel.getTargetPath();
|
|
22712
|
+
const node = channel.getTargetNode();
|
|
22713
|
+
const output = channel.getSampler()?.getOutput();
|
|
22714
|
+
if (path7 === null || !(path7 in COMPONENTS) || node === null || !output) return false;
|
|
22715
|
+
const rest = restOf(node, path7);
|
|
22716
|
+
const element = new Array(COMPONENTS[path7]).fill(0);
|
|
22717
|
+
for (let key = 0; key < output.getCount(); key += 1) {
|
|
22718
|
+
output.getElement(key, element);
|
|
22719
|
+
if (element.some((value, c) => Math.abs(value - (rest[c] ?? 0)) > REST_TOLERANCE)) return false;
|
|
22720
|
+
}
|
|
22721
|
+
return output.getCount() > 0;
|
|
22722
|
+
}
|
|
22723
|
+
var lastKey = (channel) => {
|
|
22724
|
+
const input = channel.getSampler()?.getInput();
|
|
22725
|
+
return input === null || input === void 0 ? 0 : input.getMax([0])[0] ?? 0;
|
|
22726
|
+
};
|
|
22727
|
+
function dropChannel(animation, channel) {
|
|
22728
|
+
const sampler = channel.getSampler();
|
|
22729
|
+
animation.removeChannel(channel);
|
|
22730
|
+
channel.dispose();
|
|
22731
|
+
const shared = sampler?.listParents().some((parent) => parent !== animation && parent.propertyType === "AnimationChannel");
|
|
22732
|
+
if (sampler === null || sampler === void 0 || shared === true) return;
|
|
22733
|
+
animation.removeSampler(sampler);
|
|
22734
|
+
sampler.dispose();
|
|
22735
|
+
}
|
|
22736
|
+
function pruneRestPoseTracks(document, logicalPath) {
|
|
22737
|
+
let dropped = 0;
|
|
22738
|
+
let total = 0;
|
|
22739
|
+
for (const animation of document.getRoot().listAnimations()) {
|
|
22740
|
+
const kept = new Set(animation.listChannels());
|
|
22741
|
+
total += kept.size;
|
|
22742
|
+
const duration = Math.max(0, ...[...kept].map(lastKey));
|
|
22743
|
+
for (const channel of animation.listChannels()) {
|
|
22744
|
+
if (kept.size === 1 || !holdsRest(channel)) continue;
|
|
22745
|
+
kept.delete(channel);
|
|
22746
|
+
if (Math.max(0, ...[...kept].map(lastKey)) < duration) kept.add(channel);
|
|
22747
|
+
else {
|
|
22748
|
+
dropChannel(animation, channel);
|
|
22749
|
+
dropped += 1;
|
|
22750
|
+
}
|
|
22751
|
+
}
|
|
22752
|
+
}
|
|
22753
|
+
if (dropped > 0 && logicalPath !== void 0)
|
|
22754
|
+
console.log(
|
|
22755
|
+
`animation ${logicalPath}: dropped ${String(dropped)} of ${String(total)} tracks that hold the rest pose`
|
|
22756
|
+
);
|
|
22757
|
+
return { dropped, total };
|
|
22758
|
+
}
|
|
22759
|
+
|
|
22612
22760
|
// src/passes/quantize-static.ts
|
|
22613
22761
|
async function quantizeStaticGeometry(document, quantize2) {
|
|
22614
22762
|
const held = holdDeformingMeshes(document);
|
|
@@ -22773,6 +22921,14 @@ function parseFileName(name) {
|
|
|
22773
22921
|
if (key === void 0 || codec === void 0 || mimeType === void 0) return void 0;
|
|
22774
22922
|
return { codec, key, mimeType };
|
|
22775
22923
|
}
|
|
22924
|
+
function readTranscoderRecord(value) {
|
|
22925
|
+
if (typeof value !== "object" || value === null) return {};
|
|
22926
|
+
const record = value;
|
|
22927
|
+
return {
|
|
22928
|
+
...typeof record.savings === "number" && Number.isFinite(record.savings) && record.savings > 0 ? { transcoderSavings: record.savings } : {},
|
|
22929
|
+
...record.required === true ? { transcoderRequired: true } : {}
|
|
22930
|
+
};
|
|
22931
|
+
}
|
|
22776
22932
|
function sharedImageKey(source, settings) {
|
|
22777
22933
|
return createHash("sha256").update(source).update(JSON.stringify(settings), "utf8").digest("hex").slice(0, KEY_LENGTH);
|
|
22778
22934
|
}
|
|
@@ -22849,6 +23005,17 @@ function createSharedImageStore(outputRoot, options = {}) {
|
|
|
22849
23005
|
return {};
|
|
22850
23006
|
throw error;
|
|
22851
23007
|
}
|
|
23008
|
+
})(),
|
|
23009
|
+
...await (async () => {
|
|
23010
|
+
try {
|
|
23011
|
+
return readTranscoderRecord(
|
|
23012
|
+
JSON.parse(await readFile(path2.join(directory, `${name}.transcoder.json`), "utf8"))
|
|
23013
|
+
);
|
|
23014
|
+
} catch (error) {
|
|
23015
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError)
|
|
23016
|
+
return {};
|
|
23017
|
+
throw error;
|
|
23018
|
+
}
|
|
22852
23019
|
})()
|
|
22853
23020
|
};
|
|
22854
23021
|
memory.set(key, image);
|
|
@@ -22860,7 +23027,8 @@ function createSharedImageStore(outputRoot, options = {}) {
|
|
|
22860
23027
|
if (directory === void 0 || options.writeThrough === false) return;
|
|
22861
23028
|
const name = fileNameFor(key, image);
|
|
22862
23029
|
const known = await onDisk();
|
|
22863
|
-
if (known.has(key) && image.quality === void 0)
|
|
23030
|
+
if (known.has(key) && image.quality === void 0 && image.transcoderSavings === void 0 && image.transcoderRequired !== true)
|
|
23031
|
+
return;
|
|
22864
23032
|
await mkdir(directory, { recursive: true });
|
|
22865
23033
|
const target = path2.join(directory, name);
|
|
22866
23034
|
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -22881,6 +23049,23 @@ function createSharedImageStore(outputRoot, options = {}) {
|
|
|
22881
23049
|
await rm(qualityTemporary, { force: true }).catch(() => void 0);
|
|
22882
23050
|
}
|
|
22883
23051
|
}
|
|
23052
|
+
if (image.transcoderSavings !== void 0 || image.transcoderRequired === true) {
|
|
23053
|
+
const transcoderTarget = `${target}.transcoder.json`;
|
|
23054
|
+
const transcoderTemporary = `${transcoderTarget}.${process.pid}.${randomUUID()}.tmp`;
|
|
23055
|
+
try {
|
|
23056
|
+
await writeFile(
|
|
23057
|
+
transcoderTemporary,
|
|
23058
|
+
`${JSON.stringify({
|
|
23059
|
+
...image.transcoderSavings === void 0 ? {} : { savings: image.transcoderSavings },
|
|
23060
|
+
...image.transcoderRequired === true ? { required: true } : {}
|
|
23061
|
+
})}
|
|
23062
|
+
`
|
|
23063
|
+
);
|
|
23064
|
+
await rename(transcoderTemporary, transcoderTarget);
|
|
23065
|
+
} finally {
|
|
23066
|
+
await rm(transcoderTemporary, { force: true }).catch(() => void 0);
|
|
23067
|
+
}
|
|
23068
|
+
}
|
|
22884
23069
|
known.set(key, name);
|
|
22885
23070
|
}
|
|
22886
23071
|
};
|
|
@@ -23003,6 +23188,7 @@ async function readSharedGlb(io, buffer, resolve2) {
|
|
|
23003
23188
|
var DRACO_EXTENSION2 = "KHR_draco_mesh_compression";
|
|
23004
23189
|
var EXT_MESHOPT_EXTENSION = "EXT_meshopt_compression";
|
|
23005
23190
|
var MATERIAL_MERGE_VERSION = 1;
|
|
23191
|
+
var SHARED_TRANSCODER_ACCOUNTING_VERSION = 1;
|
|
23006
23192
|
var MODEL_DEDUPE_VERSION = 1;
|
|
23007
23193
|
var MODEL_REORDER_VERSION = 1;
|
|
23008
23194
|
var MODEL_QUANTIZE_VERSION = 1;
|
|
@@ -23032,7 +23218,7 @@ async function writeDocument(document, logicalPath, vertexLayout = "interleaved"
|
|
|
23032
23218
|
await MeshoptEncoder.ready;
|
|
23033
23219
|
const io = new NodeIO().setVertexLayout(vertexLayout === "separate" ? VertexLayout.SEPARATE : VertexLayout.INTERLEAVED).registerExtensions([...ALL_EXTENSIONS, EXTTextureAstc, TNVirtualGeometry, TNDiscreteLod]).registerDependencies({ "meshopt.encoder": MeshoptEncoder });
|
|
23034
23220
|
try {
|
|
23035
|
-
const buffer = Buffer.from(await io.writeBinary(document));
|
|
23221
|
+
const buffer = withoutSpecDefaults(Buffer.from(await io.writeBinary(document)));
|
|
23036
23222
|
return { buffer, extensions: jsonOfGlb(buffer).extensionsUsed ?? [] };
|
|
23037
23223
|
} catch (error) {
|
|
23038
23224
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -23406,7 +23592,8 @@ function modelPass(options = {}) {
|
|
|
23406
23592
|
meshopt: options.passes?.meshopt ?? true,
|
|
23407
23593
|
prune: options.passes?.prune ?? true,
|
|
23408
23594
|
quantize: options.passes?.quantize ?? true,
|
|
23409
|
-
reorder: options.passes?.reorder ?? true
|
|
23595
|
+
reorder: options.passes?.reorder ?? true,
|
|
23596
|
+
restTracks: options.passes?.restTracks ?? true
|
|
23410
23597
|
},
|
|
23411
23598
|
...options.vertexLayout === void 0 ? {} : { vertexLayout: options.vertexLayout },
|
|
23412
23599
|
quantize: {
|
|
@@ -23441,13 +23628,17 @@ function modelPass(options = {}) {
|
|
|
23441
23628
|
bakeVersion: VIRTUAL_BAKE_VERSION
|
|
23442
23629
|
},
|
|
23443
23630
|
sharedImages: options.sharedImages !== void 0,
|
|
23631
|
+
...options.sharedImages === void 0 ? {} : { sharedTranscoderAccounting: SHARED_TRANSCODER_ACCOUNTING_VERSION },
|
|
23444
23632
|
textures: options.textures === "none" ? "none" : {
|
|
23445
23633
|
...options.textures?.astc === true ? { astc: "raw4x4-gltf-authored-basis-v2" } : {},
|
|
23446
23634
|
decoderFree: options.textures?.decoderFree ?? false,
|
|
23447
23635
|
encoder: KTX2_ENCODER_VERSION,
|
|
23448
23636
|
instrument: IMAGE_QUALITY_IDENTITY,
|
|
23637
|
+
jpeg: options.textures?.jpeg ?? null,
|
|
23638
|
+
jpegLadder: "colour-opaque-q75-90-v1",
|
|
23449
23639
|
floor: resolveImageQualityFloor(options.textures?.floor),
|
|
23450
23640
|
maxSize: options.textures?.maxSize ?? null,
|
|
23641
|
+
transcoderBytes: options.textures?.transcoderBytes ?? null,
|
|
23451
23642
|
// Retention is now conditioned on the container, so it is spelled out rather than
|
|
23452
23643
|
// named by a boolean: a warm cache keyed on the old rule would keep shipping the
|
|
23453
23644
|
// WebP-requiring models this replaced.
|
|
@@ -23468,7 +23659,8 @@ function modelPass(options = {}) {
|
|
|
23468
23659
|
meshopt: passes2.meshopt ?? true,
|
|
23469
23660
|
prune: passes2.prune ?? true,
|
|
23470
23661
|
quantize: passes2.quantize ?? true,
|
|
23471
|
-
reorder: passes2.reorder ?? true
|
|
23662
|
+
reorder: passes2.reorder ?? true,
|
|
23663
|
+
restTracks: passes2.restTracks ?? true
|
|
23472
23664
|
};
|
|
23473
23665
|
const textureOptions = options.textures === "none" ? void 0 : options.textures ?? {};
|
|
23474
23666
|
const virtualOptions = options.virtual === "none" ? void 0 : options.virtual ?? {};
|
|
@@ -23486,6 +23678,7 @@ function modelPass(options = {}) {
|
|
|
23486
23678
|
if (enabled.dedup) await dedup()(document);
|
|
23487
23679
|
const namesBeforeCompact = compactRequested(options.compact) ? sceneNodeNames(document) : void 0;
|
|
23488
23680
|
let compact = compactRequested(options.compact) ? await compactModel(document, resolveCompactOptions(options.compact)) : void 0;
|
|
23681
|
+
if (enabled.restTracks) pruneRestPoseTracks(document, logicalPath);
|
|
23489
23682
|
if (enabled.prune) {
|
|
23490
23683
|
await prune({ keepAttributes: options.preserveLightmapUv === true })(document);
|
|
23491
23684
|
if (compact !== void 0 && namesBeforeCompact !== void 0) {
|
|
@@ -23543,7 +23736,8 @@ function modelPass(options = {}) {
|
|
|
23543
23736
|
store,
|
|
23544
23737
|
shared,
|
|
23545
23738
|
embeddedTextures?.formats,
|
|
23546
|
-
embeddedTextures?.quality
|
|
23739
|
+
embeddedTextures?.quality,
|
|
23740
|
+
embeddedTextures?.transcoderByImage
|
|
23547
23741
|
);
|
|
23548
23742
|
if (enabled.meshopt) {
|
|
23549
23743
|
document.createExtension(EXTMeshoptCompression).setRequired(true).setEncoderOptions({ method: EXTMeshoptCompression.EncoderMethod.QUANTIZE });
|
|
@@ -23616,11 +23810,14 @@ function sharedSettings(texture, textureOptions) {
|
|
|
23616
23810
|
decoderFree: textureOptions.decoderFree ?? false,
|
|
23617
23811
|
encoder: KTX2_ENCODER_VERSION,
|
|
23618
23812
|
instrument: IMAGE_QUALITY_IDENTITY,
|
|
23813
|
+
jpeg: textureOptions.jpeg ?? null,
|
|
23814
|
+
jpegLadder: "colour-opaque-q75-90-v1",
|
|
23619
23815
|
floor: resolveImageQualityFloor(textureOptions.floor),
|
|
23620
23816
|
keepSmallerSource: "universal-containers",
|
|
23621
23817
|
maxSize: resolveTextureMaxSize(textureOptions.maxSize, listTextureSlots(texture)),
|
|
23622
23818
|
overrides: textureOptions.overrides ?? [],
|
|
23623
|
-
quality: textureOptions.quality ?? null
|
|
23819
|
+
quality: textureOptions.quality ?? null,
|
|
23820
|
+
transcoderBytes: textureOptions.transcoderBytes ?? null
|
|
23624
23821
|
}
|
|
23625
23822
|
};
|
|
23626
23823
|
}
|
|
@@ -23644,7 +23841,9 @@ async function recallSharedImages(document, store, textureOptions) {
|
|
|
23644
23841
|
recalled.set(index, {
|
|
23645
23842
|
codec: stored.codec,
|
|
23646
23843
|
sourceBytes: image.byteLength,
|
|
23647
|
-
quality: stored.quality
|
|
23844
|
+
quality: stored.quality,
|
|
23845
|
+
...stored.transcoderSavings === void 0 ? {} : { transcoderSavings: stored.transcoderSavings },
|
|
23846
|
+
...stored.transcoderRequired === true ? { transcoderRequired: true } : {}
|
|
23648
23847
|
});
|
|
23649
23848
|
}
|
|
23650
23849
|
return { keys, recalled };
|
|
@@ -23653,7 +23852,7 @@ function codecOf(mimeType, formats, name) {
|
|
|
23653
23852
|
if (mimeType !== "image/ktx2") return "none";
|
|
23654
23853
|
return formats?.[name] ?? "uastc";
|
|
23655
23854
|
}
|
|
23656
|
-
async function rememberSharedImages(document, store, plan, formats, quality) {
|
|
23855
|
+
async function rememberSharedImages(document, store, plan, formats, quality, transcoder) {
|
|
23657
23856
|
const textures = document.getRoot().listTextures();
|
|
23658
23857
|
const names = textureKeys(document.getRoot());
|
|
23659
23858
|
for (const [index, texture] of textures.entries()) {
|
|
@@ -23661,11 +23860,15 @@ async function rememberSharedImages(document, store, plan, formats, quality) {
|
|
|
23661
23860
|
const key = plan.keys[index];
|
|
23662
23861
|
const image = texture.getImage();
|
|
23663
23862
|
if (key === void 0 || key === "" || image === null) continue;
|
|
23863
|
+
const name = names[index] ?? "";
|
|
23864
|
+
const contribution = transcoder?.[name];
|
|
23664
23865
|
await store.put(key, {
|
|
23665
23866
|
buffer: Buffer.from(image.buffer, image.byteOffset, image.byteLength),
|
|
23666
|
-
codec: codecOf(texture.getMimeType(), formats,
|
|
23867
|
+
codec: codecOf(texture.getMimeType(), formats, name),
|
|
23667
23868
|
mimeType: texture.getMimeType(),
|
|
23668
|
-
...quality?.[
|
|
23869
|
+
...quality?.[name] === void 0 ? {} : { quality: quality?.[name] },
|
|
23870
|
+
...contribution?.savings === void 0 ? {} : { transcoderSavings: contribution.savings },
|
|
23871
|
+
...contribution?.required === true ? { transcoderRequired: true } : {}
|
|
23669
23872
|
});
|
|
23670
23873
|
}
|
|
23671
23874
|
}
|
|
@@ -23678,6 +23881,8 @@ function declareSharedImage(store, logicalPath, auxiliaryOutputs, candidate) {
|
|
|
23678
23881
|
metadata: {
|
|
23679
23882
|
codec: candidate.image.codec,
|
|
23680
23883
|
key: candidate.key,
|
|
23884
|
+
transcoderRequired: candidate.image.transcoderRequired === true,
|
|
23885
|
+
transcoderSavings: candidate.image.transcoderSavings ?? 0,
|
|
23681
23886
|
...candidate.image.quality === void 0 ? {} : { quality: candidate.image.quality }
|
|
23682
23887
|
},
|
|
23683
23888
|
outputPath,
|
package/dist/index.d.ts
CHANGED
|
@@ -32,25 +32,6 @@ interface ITextureQuality extends IImageQuality {
|
|
|
32
32
|
readonly codec: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Compresses the images embedded *inside* a `.glb` — the half of a model the geometry passes
|
|
37
|
-
* never touched. A prop carrying three 2048x2048 JPEGs is a small file and 67 MB of VRAM once
|
|
38
|
-
* the driver decodes it; meshopt shrinks the other, cheaper half. This stage transcodes each
|
|
39
|
-
* embedded image to KTX2/Basis and declares `KHR_texture_basisu`, so the GPU stores blocks
|
|
40
|
-
* instead of RGBA, and caps the resolution so a 4K texture an image model produced cannot
|
|
41
|
-
* silently eat a phone's whole budget.
|
|
42
|
-
*
|
|
43
|
-
* Slot constraints feed one measured ladder: normal/data slots retain UASTC/none, while
|
|
44
|
-
* colour escalates from ETC1S through RDO and plain UASTC. Alpha coverage is checked separately;
|
|
45
|
-
* a forced codec must still meet the floor. Colour space comes from the slot too — `getTextureColorSpace`
|
|
46
|
-
* knows base colour and emissive are sRGB and that metallic-roughness and normals are data —
|
|
47
|
-
* so a linear map is never encoded through a perceptual metric.
|
|
48
|
-
*
|
|
49
|
-
* The encoder is the same in-process Basis WASM the standalone texture pass uses, so no new
|
|
50
|
-
* dependency and nothing extra to install. Everything fails closed: an image no decoder reads,
|
|
51
|
-
* an image whose dimensions are not a whole number of 4x4 blocks, and a texture whose binding
|
|
52
|
-
* did not survive the round trip all throw naming the model and the image.
|
|
53
|
-
*/
|
|
54
35
|
interface IModelTextureOverride {
|
|
55
36
|
readonly codec: TextureCodec;
|
|
56
37
|
/** glTF slot the override applies to, e.g. `normalTexture` or `baseColorTexture`. */
|
|
@@ -70,6 +51,20 @@ interface IModelTexturesOptions {
|
|
|
70
51
|
* per-image deduplication, colour space — is unchanged.
|
|
71
52
|
*/
|
|
72
53
|
readonly decoderFree?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Bytes the runtime pays once for the Basis transcoder (`basis_transcoder.js` plus
|
|
56
|
+
* `basis_transcoder.wasm`) when any model publishes KTX2. An image is encoded only when its
|
|
57
|
+
* KTX2 output saves at least this many bytes against the authored container, so one small
|
|
58
|
+
* texture can never make every game download a transcoder larger than the saving. Defaults to
|
|
59
|
+
* {@link BASIS_TRANSCODER_BYTES}. `compileAssets` passes 0 when the whole build repays it
|
|
60
|
+
* (see {@link IEmbeddedTextureSummary.transcoderSavings}).
|
|
61
|
+
*/
|
|
62
|
+
readonly transcoderBytes?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Internal switch for the authored-container JPEG rung (default on). A caller that needs the
|
|
65
|
+
* KTX2 ladder alone, such as a test of rung selection, turns it off.
|
|
66
|
+
*/
|
|
67
|
+
readonly jpeg?: boolean;
|
|
73
68
|
/**
|
|
74
69
|
* Longest edge an embedded image may keep, default 2048. Larger images are box-resampled
|
|
75
70
|
* down preserving aspect ratio; smaller ones are never upscaled.
|
|
@@ -93,6 +88,22 @@ interface IEmbeddedTextureSummary {
|
|
|
93
88
|
readonly resized: number;
|
|
94
89
|
/** Images retained at their authored bytes, by name, with the reason each one was not cooked. */
|
|
95
90
|
readonly skippedCompression: Readonly<Record<string, TextureSkipReason>>;
|
|
91
|
+
/**
|
|
92
|
+
* Bytes KTX2 saves over the decoder-free container across this model's automatic images,
|
|
93
|
+
* whether or not the transcoder charge let it ship. The build sums it over every model to
|
|
94
|
+
* decide once whether the shared transcoder pays for itself. Absent when zero.
|
|
95
|
+
*/
|
|
96
|
+
readonly transcoderSavings?: number;
|
|
97
|
+
/** Some image ships Basis KTX2 whatever the charge (a codec override, a resize, authored KTX2). */
|
|
98
|
+
readonly transcoderRequired?: true;
|
|
99
|
+
/**
|
|
100
|
+
* What each image contributed to the two fields above, keyed like `formats`. Internal: the
|
|
101
|
+
* shared-image store replays it so a warm cook reports the same summary as a cold one.
|
|
102
|
+
*/
|
|
103
|
+
readonly transcoderByImage?: Readonly<Record<string, {
|
|
104
|
+
readonly savings?: number;
|
|
105
|
+
readonly required?: true;
|
|
106
|
+
}>>;
|
|
96
107
|
}
|
|
97
108
|
/** One embedded image as the verification compares it, source against re-read output. */
|
|
98
109
|
interface IModelTextureBinding {
|
|
@@ -544,6 +555,10 @@ interface ISharedImage {
|
|
|
544
555
|
/** `uastc`, `etc1s` or `none`, spelled into the filename so a cache hit knows what it found. */
|
|
545
556
|
readonly codec: string;
|
|
546
557
|
readonly mimeType: string;
|
|
558
|
+
/** Bytes KTX2 saved against the decoder-free container here, so a recall reports it too. */
|
|
559
|
+
readonly transcoderSavings?: number;
|
|
560
|
+
/** This image ships Basis KTX2 whatever the transcoder charge; replayed on a recall. */
|
|
561
|
+
readonly transcoderRequired?: true;
|
|
547
562
|
}
|
|
548
563
|
interface ISharedImageStore {
|
|
549
564
|
/** The image encoded from this key, from memory or from a previous build's output. */
|
|
@@ -583,6 +598,8 @@ interface IModelPassesOptions {
|
|
|
583
598
|
readonly prune?: boolean;
|
|
584
599
|
readonly quantize?: boolean;
|
|
585
600
|
readonly reorder?: boolean;
|
|
601
|
+
/** Drop animation tracks that only hold the pose their node already has. Default true. */
|
|
602
|
+
readonly restTracks?: boolean;
|
|
586
603
|
}
|
|
587
604
|
interface IModelQuantizeOptions {
|
|
588
605
|
/** Normal precision in bits, default 8. */
|
|
@@ -690,6 +707,10 @@ interface IEmbeddedTextureRow {
|
|
|
690
707
|
readonly gpuBytesAfter: number;
|
|
691
708
|
readonly gpuBytesBefore: number;
|
|
692
709
|
readonly resized: number;
|
|
710
|
+
/** Bytes KTX2 saves over the decoder-free container here; the build sums it once per bake. */
|
|
711
|
+
readonly transcoderSavings?: number;
|
|
712
|
+
/** Some image ships Basis KTX2 whatever the transcoder charge. */
|
|
713
|
+
readonly transcoderRequired?: true;
|
|
693
714
|
}
|
|
694
715
|
/** What the cluster-DAG bake produced for one model. */
|
|
695
716
|
interface IVirtualRow {
|
|
@@ -1117,6 +1138,12 @@ interface IAssetCompileOptions {
|
|
|
1117
1138
|
readonly source?: string;
|
|
1118
1139
|
/** Overrides resolution of three's Basis transcoder for the copy into the output root. */
|
|
1119
1140
|
readonly transcoder?: IBasisTranscoder;
|
|
1141
|
+
/**
|
|
1142
|
+
* Internal: the bytes an embedded model image must save before KTX2 pays for the shared Basis
|
|
1143
|
+
* transcoder. Absent charges the measured default; a caller that already ships the transcoder
|
|
1144
|
+
* passes 0.
|
|
1145
|
+
*/
|
|
1146
|
+
readonly modelTranscoderBytes?: number;
|
|
1120
1147
|
}
|
|
1121
1148
|
interface IBasisTranscoder {
|
|
1122
1149
|
readonly javascriptPath: string;
|
package/dist/index.js
CHANGED
|
@@ -531,7 +531,8 @@ var MODEL_PASS_KEYS = [
|
|
|
531
531
|
"meshopt",
|
|
532
532
|
"prune",
|
|
533
533
|
"quantize",
|
|
534
|
-
"reorder"
|
|
534
|
+
"reorder",
|
|
535
|
+
"restTracks"
|
|
535
536
|
];
|
|
536
537
|
var MODEL_QUANTIZE_KEYS = ["normalBits", "positionBits", "uvBits"];
|
|
537
538
|
var MODEL_VIRTUAL_COUNT_KEYS = [
|
|
@@ -16008,9 +16009,9 @@ function readTextureQuality(value) {
|
|
|
16008
16009
|
const row = value;
|
|
16009
16010
|
const positive = (v) => typeof v === "number" && Number.isSafeInteger(v) && v > 0;
|
|
16010
16011
|
const bounded = (v, min2, max2) => typeof v === "number" && Number.isFinite(v) && v >= min2 && v <= max2;
|
|
16011
|
-
if (row.version !== IMAGE_QUALITY_VERSION || ![row.width, row.height, row.sourceWidth, row.sourceHeight].every(positive) || !bounded(row.ssim, -1, 1) || row.meanDeltaE00 !== null && !bounded(row.meanDeltaE00, 0, Number.POSITIVE_INFINITY) || !["pass", "below-floor", "unvalidated-slots"].includes(row.status) || !Array.isArray(row.slots) || !row.slots.every((slot) => typeof slot === "string") || !["uastc", "etc1s", "none"].includes(row.codec) || row.compressionSkipped !== void 0 && !["block-size", "not-smaller", "below-floor"].includes(row.compressionSkipped) || !bounded(row.floor?.ssim, 0, 1) || !bounded(row.floor?.meanDeltaE00, 0, Number.POSITIVE_INFINITY) || row.rung !== void 0 && !["etc1s@150", "uastc+rdo \u03BB3 +zstd", "uastc+rdo \u03BB1 +zstd", "uastc", "none"].includes(
|
|
16012
|
+
if (row.version !== IMAGE_QUALITY_VERSION || ![row.width, row.height, row.sourceWidth, row.sourceHeight].every(positive) || !bounded(row.ssim, -1, 1) || row.meanDeltaE00 !== null && !bounded(row.meanDeltaE00, 0, Number.POSITIVE_INFINITY) || !["pass", "below-floor", "unvalidated-slots"].includes(row.status) || !Array.isArray(row.slots) || !row.slots.every((slot) => typeof slot === "string") || !["uastc", "etc1s", "jpeg", "none"].includes(row.codec) || row.compressionSkipped !== void 0 && !["block-size", "not-smaller", "below-floor"].includes(row.compressionSkipped) || !bounded(row.floor?.ssim, 0, 1) || !bounded(row.floor?.meanDeltaE00, 0, Number.POSITIVE_INFINITY) || row.rung !== void 0 && !["etc1s@150", "uastc+rdo \u03BB3 +zstd", "uastc+rdo \u03BB1 +zstd", "uastc", "none"].includes(
|
|
16012
16013
|
row.rung
|
|
16013
|
-
) && !/^etc1s@\d+$/u.test(row.rung) || !bounded(row.alpha?.ssim, -1, 1) || !bounded(row.alpha?.meanAbsoluteError, 0, 1) || !Array.isArray(row.alpha?.coverage) || !row.alpha.coverage.every(
|
|
16014
|
+
) && !/^etc1s@\d+$/u.test(row.rung) && !/^jpeg@\d+$/u.test(row.rung) || !bounded(row.alpha?.ssim, -1, 1) || !bounded(row.alpha?.meanAbsoluteError, 0, 1) || !Array.isArray(row.alpha?.coverage) || !row.alpha.coverage.every(
|
|
16014
16015
|
(c) => c !== null && typeof c === "object" && bounded(c.threshold, 0, 1) && [c.source, c.decoded, c.changedPixels].every(
|
|
16015
16016
|
(v) => Number.isSafeInteger(v) && v >= 0 && v <= row.width * row.height
|
|
16016
16017
|
)
|
|
@@ -21682,7 +21683,7 @@ function scriptsDirectory() {
|
|
|
21682
21683
|
// Built: `dist/index.js` sits beside `dist/blender-gpl/`.
|
|
21683
21684
|
path8.join(here, "blender-gpl"),
|
|
21684
21685
|
// Source: `src/passes/` -> the workspace sibling.
|
|
21685
|
-
path8.resolve(here, "..", "..", "..", "blender-
|
|
21686
|
+
path8.resolve(here, "..", "..", "..", "blender-bridge", "gpl")
|
|
21686
21687
|
];
|
|
21687
21688
|
const found = candidates.find((candidate) => existsSync(path8.join(candidate, "convert.py")));
|
|
21688
21689
|
if (found === void 0) {
|
|
@@ -22065,6 +22066,7 @@ function bakeStaticLightmap(document, width, height, padding) {
|
|
|
22065
22066
|
width
|
|
22066
22067
|
};
|
|
22067
22068
|
}
|
|
22069
|
+
var BASIS_TRANSCODER_BYTES = 600 * 1024;
|
|
22068
22070
|
var DEFAULT_MAX_SIZE = 2048;
|
|
22069
22071
|
var DEFAULT_ETC1S_QUALITY = 150;
|
|
22070
22072
|
var BLOCK_SIZE = 4;
|
|
@@ -22359,6 +22361,29 @@ async function encodeTextureLadder(data, width, height, options) {
|
|
|
22359
22361
|
if (smallest !== void 0) return smallest;
|
|
22360
22362
|
throw new Error("TN_ASSETS_TEXTURE_FLOOR: forced codec fails its configured floor.");
|
|
22361
22363
|
}
|
|
22364
|
+
var JPEG_QUALITY_RUNGS = [75, 80, 85, 90];
|
|
22365
|
+
async function encodeJpegCandidate(data, width, height, options) {
|
|
22366
|
+
const sharp = (await import('sharp')).default;
|
|
22367
|
+
for (const quality2 of JPEG_QUALITY_RUNGS) {
|
|
22368
|
+
const buffer = await sharp(Buffer.from(data), {
|
|
22369
|
+
raw: { channels: 4, height, width }
|
|
22370
|
+
}).jpeg({ mozjpeg: true, quality: quality2 }).toBuffer();
|
|
22371
|
+
const decoded = await decodeImageBytes(buffer, "jpeg-candidate");
|
|
22372
|
+
const score = imageQuality(data, decoded.data, width, height, options);
|
|
22373
|
+
if (score.status !== "pass" || score.alpha.meanAbsoluteError !== 0) continue;
|
|
22374
|
+
return {
|
|
22375
|
+
buffer,
|
|
22376
|
+
quality: {
|
|
22377
|
+
...score,
|
|
22378
|
+
codec: "jpeg",
|
|
22379
|
+
rung: `jpeg@${String(quality2)}`,
|
|
22380
|
+
sourceHeight: height,
|
|
22381
|
+
sourceWidth: width
|
|
22382
|
+
}
|
|
22383
|
+
};
|
|
22384
|
+
}
|
|
22385
|
+
return void 0;
|
|
22386
|
+
}
|
|
22362
22387
|
function gpuBytes(width, height, codec) {
|
|
22363
22388
|
return Math.round(width * height * (GPU_BYTES_PER_PIXEL[codec] ?? 4) * MIP_FACTOR);
|
|
22364
22389
|
}
|
|
@@ -22370,6 +22395,7 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
22370
22395
|
const textures = root.listTextures();
|
|
22371
22396
|
if (textures.length === 0) return void 0;
|
|
22372
22397
|
const decoderFree = options.decoderFree === true;
|
|
22398
|
+
const transcoderBytes = Math.max(0, options.transcoderBytes ?? BASIS_TRANSCODER_BYTES);
|
|
22373
22399
|
const quality2 = Math.min(255, Math.max(1, Math.round(options.quality ?? DEFAULT_ETC1S_QUALITY)));
|
|
22374
22400
|
const keys = textureKeys(root);
|
|
22375
22401
|
const formats = {};
|
|
@@ -22381,6 +22407,9 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
22381
22407
|
let gpuBytesAfter = 0;
|
|
22382
22408
|
let resized = 0;
|
|
22383
22409
|
let compressed = 0;
|
|
22410
|
+
let transcoderSavings = 0;
|
|
22411
|
+
let transcoderRequired = false;
|
|
22412
|
+
const transcoderByImage = {};
|
|
22384
22413
|
for (const [index, texture] of textures.entries()) {
|
|
22385
22414
|
const key = keys[index] ?? `texture#${String(index)}`;
|
|
22386
22415
|
const image = texture.getImage();
|
|
@@ -22409,6 +22438,15 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
22409
22438
|
if (shape.width !== score.sourceWidth || shape.height !== score.sourceHeight) resized += 1;
|
|
22410
22439
|
}
|
|
22411
22440
|
if (fromStore.codec !== "none") compressed += 1;
|
|
22441
|
+
if (fromStore.transcoderSavings !== void 0)
|
|
22442
|
+
transcoderSavings += fromStore.transcoderSavings;
|
|
22443
|
+
if (fromStore.transcoderRequired === true) transcoderRequired = true;
|
|
22444
|
+
if (fromStore.transcoderSavings !== void 0 || fromStore.transcoderRequired === true) {
|
|
22445
|
+
transcoderByImage[key] = {
|
|
22446
|
+
...fromStore.transcoderSavings === void 0 ? {} : { savings: fromStore.transcoderSavings },
|
|
22447
|
+
...fromStore.transcoderRequired === true ? { required: true } : {}
|
|
22448
|
+
};
|
|
22449
|
+
}
|
|
22412
22450
|
continue;
|
|
22413
22451
|
}
|
|
22414
22452
|
const slots = listTextureSlots(texture);
|
|
@@ -22424,6 +22462,10 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
22424
22462
|
}
|
|
22425
22463
|
}
|
|
22426
22464
|
if (texture.getMimeType() === "image/ktx2" && !authoredBasis) {
|
|
22465
|
+
if (!isRawAstc(texture)) {
|
|
22466
|
+
transcoderRequired = true;
|
|
22467
|
+
transcoderByImage[key] = { ...transcoderByImage[key], required: true };
|
|
22468
|
+
}
|
|
22427
22469
|
const shape = imageShape(image, "image/ktx2");
|
|
22428
22470
|
bytesBefore += image.byteLength;
|
|
22429
22471
|
bytesAfter += image.byteLength;
|
|
@@ -22540,6 +22582,14 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
22540
22582
|
};
|
|
22541
22583
|
continue;
|
|
22542
22584
|
}
|
|
22585
|
+
const authoredContainer = !explicit && !authoredBasis && options.jpeg !== false && options.astc !== true && target.width === decoded.width && target.height === decoded.height && UNIVERSAL_IMAGE_CONTAINERS.has(texture.getMimeType()) && slots.some((slot) => COLOUR_SLOTS.has(slot));
|
|
22586
|
+
const jpeg = authoredContainer ? await encodeJpegCandidate(data, target.width, target.height, {
|
|
22587
|
+
...textureQualitySemantics(texture),
|
|
22588
|
+
floor: options.floor
|
|
22589
|
+
}) : void 0;
|
|
22590
|
+
const jpegLimit = jpeg !== void 0 && jpeg.buffer.byteLength < image.byteLength ? jpeg.buffer.byteLength : image.byteLength;
|
|
22591
|
+
const automatic = !explicit && target.width === decoded.width && target.height === decoded.height && UNIVERSAL_IMAGE_CONTAINERS.has(texture.getMimeType());
|
|
22592
|
+
const charged = automatic && options.astc !== true;
|
|
22543
22593
|
let selected;
|
|
22544
22594
|
try {
|
|
22545
22595
|
if (authoredBasis && target.width === decoded.width && target.height === decoded.height) {
|
|
@@ -22569,7 +22619,7 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
22569
22619
|
srgb,
|
|
22570
22620
|
quality: quality2,
|
|
22571
22621
|
...options.astc === true ? { astc: true, gltfColorPrimaries: srgb ? 1 : 0 } : {},
|
|
22572
|
-
|
|
22622
|
+
...automatic ? { maxBytes: jpegLimit } : {},
|
|
22573
22623
|
...forced === void 0 ? {} : { forced }
|
|
22574
22624
|
});
|
|
22575
22625
|
}
|
|
@@ -22581,7 +22631,21 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
22581
22631
|
if (!authoredBasis) throw error;
|
|
22582
22632
|
throw new Error(`TN_ASSETS_MODEL_BASIS_ASTC: '${logicalPath}#${key}': ${messageOf3(error)}`);
|
|
22583
22633
|
}
|
|
22584
|
-
|
|
22634
|
+
let encoded = selected.encoded;
|
|
22635
|
+
if (charged && encoded !== void 0) {
|
|
22636
|
+
const savings = jpegLimit - encoded.byteLength;
|
|
22637
|
+
transcoderSavings += savings;
|
|
22638
|
+
if (savings > 0) transcoderByImage[key] = { ...transcoderByImage[key], savings };
|
|
22639
|
+
if (encoded.byteLength >= jpegLimit - transcoderBytes) encoded = void 0;
|
|
22640
|
+
}
|
|
22641
|
+
if (encoded === void 0 && jpeg !== void 0 && jpeg.buffer.byteLength < image.byteLength) {
|
|
22642
|
+
texture.setImage(jpeg.buffer).setMimeType("image/jpeg");
|
|
22643
|
+
bytesAfter += jpeg.buffer.byteLength;
|
|
22644
|
+
gpuBytesAfter += gpuBytes(target.width, target.height, "none");
|
|
22645
|
+
formats[key] = "none";
|
|
22646
|
+
if (options.measureQuality !== false) scores[key] = jpeg.quality;
|
|
22647
|
+
continue;
|
|
22648
|
+
}
|
|
22585
22649
|
if (encoded === void 0 || encoded.byteLength >= image.byteLength && target.width === decoded.width && target.height === decoded.height && !explicit && UNIVERSAL_IMAGE_CONTAINERS.has(texture.getMimeType())) {
|
|
22586
22650
|
const downsampled = target.width !== decoded.width || target.height !== decoded.height;
|
|
22587
22651
|
let retained = image;
|
|
@@ -22624,6 +22688,10 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
22624
22688
|
};
|
|
22625
22689
|
}
|
|
22626
22690
|
texture.setImage(encoded).setMimeType("image/ktx2");
|
|
22691
|
+
if (!charged && options.astc !== true) {
|
|
22692
|
+
transcoderRequired = true;
|
|
22693
|
+
transcoderByImage[key] = { ...transcoderByImage[key], required: true };
|
|
22694
|
+
}
|
|
22627
22695
|
formats[key] = options.astc === true ? "astc4x4" : selected.codec;
|
|
22628
22696
|
bytesAfter += encoded.byteLength;
|
|
22629
22697
|
gpuBytesAfter += gpuBytes(target.width, target.height, formats[key] ?? selected.codec);
|
|
@@ -22651,7 +22719,10 @@ async function compressEmbeddedTextures(document, logicalPath, options = {}, rec
|
|
|
22651
22719
|
gpuBytesAfter,
|
|
22652
22720
|
gpuBytesBefore,
|
|
22653
22721
|
resized,
|
|
22654
|
-
skippedCompression
|
|
22722
|
+
skippedCompression,
|
|
22723
|
+
...transcoderSavings > 0 ? { transcoderSavings } : {},
|
|
22724
|
+
...transcoderRequired ? { transcoderRequired: true } : {},
|
|
22725
|
+
...Object.keys(transcoderByImage).length === 0 ? {} : { transcoderByImage }
|
|
22655
22726
|
};
|
|
22656
22727
|
}
|
|
22657
22728
|
function textureQualitySemantics(texture) {
|
|
@@ -22709,12 +22780,6 @@ function texturePass(options = {}) {
|
|
|
22709
22780
|
},
|
|
22710
22781
|
apply: async (input, logicalPath) => {
|
|
22711
22782
|
if (classify(logicalPath) !== "texture") return input;
|
|
22712
|
-
const stats = textureStats(input);
|
|
22713
|
-
if (stats.width <= 0 || stats.height <= 0) {
|
|
22714
|
-
throw new Error(
|
|
22715
|
-
`TN_ASSETS_TEXTURE_UNREADABLE: '${logicalPath}' has no readable PNG/JPEG header; the KTX2 pass cannot encode it.`
|
|
22716
|
-
);
|
|
22717
|
-
}
|
|
22718
22783
|
const decoded = await decodeImageBytes(input, logicalPath);
|
|
22719
22784
|
const choice = texturePolicy(logicalPath, options);
|
|
22720
22785
|
if (choice.codec === "none")
|
|
@@ -24146,6 +24211,90 @@ function resolveCompactOptions(options) {
|
|
|
24146
24211
|
};
|
|
24147
24212
|
}
|
|
24148
24213
|
|
|
24214
|
+
// src/passes/glb-spec-defaults.ts
|
|
24215
|
+
var JSON_CHUNK = 1313821514;
|
|
24216
|
+
var HEADER = 12;
|
|
24217
|
+
function withoutSpecDefaults(glb) {
|
|
24218
|
+
if (glb.readUInt32LE(16) !== JSON_CHUNK) return glb;
|
|
24219
|
+
const jsonLength = glb.readUInt32LE(12);
|
|
24220
|
+
const json = JSON.parse(glb.subarray(20, 20 + jsonLength).toString("utf8"));
|
|
24221
|
+
for (const accessor of json.accessors ?? []) {
|
|
24222
|
+
if (accessor.normalized === false) accessor.normalized = void 0;
|
|
24223
|
+
if (accessor.byteOffset === 0) accessor.byteOffset = void 0;
|
|
24224
|
+
}
|
|
24225
|
+
for (const view of json.bufferViews ?? []) if (view.byteOffset === 0) view.byteOffset = void 0;
|
|
24226
|
+
for (const animation of json.animations ?? [])
|
|
24227
|
+
for (const sampler of animation.samplers)
|
|
24228
|
+
if (sampler.interpolation === "LINEAR") sampler.interpolation = void 0;
|
|
24229
|
+
const text = Buffer.from(JSON.stringify(json), "utf8");
|
|
24230
|
+
const padded2 = Buffer.alloc(Math.ceil(text.length / 4) * 4, 32);
|
|
24231
|
+
text.copy(padded2);
|
|
24232
|
+
const rest = glb.subarray(HEADER + 8 + jsonLength);
|
|
24233
|
+
const head = Buffer.alloc(HEADER + 8);
|
|
24234
|
+
glb.copy(head, 0, 0, HEADER);
|
|
24235
|
+
head.writeUInt32LE(HEADER + 8 + padded2.length + rest.length, 8);
|
|
24236
|
+
head.writeUInt32LE(padded2.length, 12);
|
|
24237
|
+
head.writeUInt32LE(JSON_CHUNK, 16);
|
|
24238
|
+
return Buffer.concat([head, padded2, rest]);
|
|
24239
|
+
}
|
|
24240
|
+
|
|
24241
|
+
// src/passes/prune-rest-tracks.ts
|
|
24242
|
+
var COMPONENTS = { rotation: 4, scale: 3, translation: 3 };
|
|
24243
|
+
var REST_TOLERANCE = 1e-6;
|
|
24244
|
+
function restOf(node, path10) {
|
|
24245
|
+
if (path10 === "rotation") return node.getRotation();
|
|
24246
|
+
return path10 === "translation" ? node.getTranslation() : node.getScale();
|
|
24247
|
+
}
|
|
24248
|
+
function holdsRest(channel) {
|
|
24249
|
+
const path10 = channel.getTargetPath();
|
|
24250
|
+
const node = channel.getTargetNode();
|
|
24251
|
+
const output = channel.getSampler()?.getOutput();
|
|
24252
|
+
if (path10 === null || !(path10 in COMPONENTS) || node === null || !output) return false;
|
|
24253
|
+
const rest = restOf(node, path10);
|
|
24254
|
+
const element = new Array(COMPONENTS[path10]).fill(0);
|
|
24255
|
+
for (let key = 0; key < output.getCount(); key += 1) {
|
|
24256
|
+
output.getElement(key, element);
|
|
24257
|
+
if (element.some((value, c) => Math.abs(value - (rest[c] ?? 0)) > REST_TOLERANCE)) return false;
|
|
24258
|
+
}
|
|
24259
|
+
return output.getCount() > 0;
|
|
24260
|
+
}
|
|
24261
|
+
var lastKey = (channel) => {
|
|
24262
|
+
const input = channel.getSampler()?.getInput();
|
|
24263
|
+
return input === null || input === void 0 ? 0 : input.getMax([0])[0] ?? 0;
|
|
24264
|
+
};
|
|
24265
|
+
function dropChannel(animation, channel) {
|
|
24266
|
+
const sampler = channel.getSampler();
|
|
24267
|
+
animation.removeChannel(channel);
|
|
24268
|
+
channel.dispose();
|
|
24269
|
+
const shared = sampler?.listParents().some((parent) => parent !== animation && parent.propertyType === "AnimationChannel");
|
|
24270
|
+
if (sampler === null || sampler === void 0 || shared === true) return;
|
|
24271
|
+
animation.removeSampler(sampler);
|
|
24272
|
+
sampler.dispose();
|
|
24273
|
+
}
|
|
24274
|
+
function pruneRestPoseTracks(document, logicalPath) {
|
|
24275
|
+
let dropped = 0;
|
|
24276
|
+
let total = 0;
|
|
24277
|
+
for (const animation of document.getRoot().listAnimations()) {
|
|
24278
|
+
const kept = new Set(animation.listChannels());
|
|
24279
|
+
total += kept.size;
|
|
24280
|
+
const duration = Math.max(0, ...[...kept].map(lastKey));
|
|
24281
|
+
for (const channel of animation.listChannels()) {
|
|
24282
|
+
if (kept.size === 1 || !holdsRest(channel)) continue;
|
|
24283
|
+
kept.delete(channel);
|
|
24284
|
+
if (Math.max(0, ...[...kept].map(lastKey)) < duration) kept.add(channel);
|
|
24285
|
+
else {
|
|
24286
|
+
dropChannel(animation, channel);
|
|
24287
|
+
dropped += 1;
|
|
24288
|
+
}
|
|
24289
|
+
}
|
|
24290
|
+
}
|
|
24291
|
+
if (dropped > 0 && logicalPath !== void 0)
|
|
24292
|
+
console.log(
|
|
24293
|
+
`animation ${logicalPath}: dropped ${String(dropped)} of ${String(total)} tracks that hold the rest pose`
|
|
24294
|
+
);
|
|
24295
|
+
return { dropped, total };
|
|
24296
|
+
}
|
|
24297
|
+
|
|
24149
24298
|
// src/passes/quantize-static.ts
|
|
24150
24299
|
async function quantizeStaticGeometry(document, quantize2) {
|
|
24151
24300
|
const held = holdDeformingMeshes(document);
|
|
@@ -24310,6 +24459,14 @@ function parseFileName(name) {
|
|
|
24310
24459
|
if (key === void 0 || codec === void 0 || mimeType === void 0) return void 0;
|
|
24311
24460
|
return { codec, key, mimeType };
|
|
24312
24461
|
}
|
|
24462
|
+
function readTranscoderRecord(value) {
|
|
24463
|
+
if (typeof value !== "object" || value === null) return {};
|
|
24464
|
+
const record = value;
|
|
24465
|
+
return {
|
|
24466
|
+
...typeof record.savings === "number" && Number.isFinite(record.savings) && record.savings > 0 ? { transcoderSavings: record.savings } : {},
|
|
24467
|
+
...record.required === true ? { transcoderRequired: true } : {}
|
|
24468
|
+
};
|
|
24469
|
+
}
|
|
24313
24470
|
function sharedImageKey(source, settings) {
|
|
24314
24471
|
return createHash("sha256").update(source).update(JSON.stringify(settings), "utf8").digest("hex").slice(0, KEY_LENGTH);
|
|
24315
24472
|
}
|
|
@@ -24386,6 +24543,17 @@ function createSharedImageStore(outputRoot, options = {}) {
|
|
|
24386
24543
|
return {};
|
|
24387
24544
|
throw error;
|
|
24388
24545
|
}
|
|
24546
|
+
})(),
|
|
24547
|
+
...await (async () => {
|
|
24548
|
+
try {
|
|
24549
|
+
return readTranscoderRecord(
|
|
24550
|
+
JSON.parse(await readFile(path8.join(directory, `${name}.transcoder.json`), "utf8"))
|
|
24551
|
+
);
|
|
24552
|
+
} catch (error) {
|
|
24553
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError)
|
|
24554
|
+
return {};
|
|
24555
|
+
throw error;
|
|
24556
|
+
}
|
|
24389
24557
|
})()
|
|
24390
24558
|
};
|
|
24391
24559
|
memory.set(key, image);
|
|
@@ -24397,7 +24565,8 @@ function createSharedImageStore(outputRoot, options = {}) {
|
|
|
24397
24565
|
if (directory === void 0 || options.writeThrough === false) return;
|
|
24398
24566
|
const name = fileNameFor(key, image);
|
|
24399
24567
|
const known = await onDisk();
|
|
24400
|
-
if (known.has(key) && image.quality === void 0)
|
|
24568
|
+
if (known.has(key) && image.quality === void 0 && image.transcoderSavings === void 0 && image.transcoderRequired !== true)
|
|
24569
|
+
return;
|
|
24401
24570
|
await mkdir(directory, { recursive: true });
|
|
24402
24571
|
const target = path8.join(directory, name);
|
|
24403
24572
|
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -24418,6 +24587,23 @@ function createSharedImageStore(outputRoot, options = {}) {
|
|
|
24418
24587
|
await rm(qualityTemporary, { force: true }).catch(() => void 0);
|
|
24419
24588
|
}
|
|
24420
24589
|
}
|
|
24590
|
+
if (image.transcoderSavings !== void 0 || image.transcoderRequired === true) {
|
|
24591
|
+
const transcoderTarget = `${target}.transcoder.json`;
|
|
24592
|
+
const transcoderTemporary = `${transcoderTarget}.${process.pid}.${randomUUID()}.tmp`;
|
|
24593
|
+
try {
|
|
24594
|
+
await writeFile(
|
|
24595
|
+
transcoderTemporary,
|
|
24596
|
+
`${JSON.stringify({
|
|
24597
|
+
...image.transcoderSavings === void 0 ? {} : { savings: image.transcoderSavings },
|
|
24598
|
+
...image.transcoderRequired === true ? { required: true } : {}
|
|
24599
|
+
})}
|
|
24600
|
+
`
|
|
24601
|
+
);
|
|
24602
|
+
await rename(transcoderTemporary, transcoderTarget);
|
|
24603
|
+
} finally {
|
|
24604
|
+
await rm(transcoderTemporary, { force: true }).catch(() => void 0);
|
|
24605
|
+
}
|
|
24606
|
+
}
|
|
24421
24607
|
known.set(key, name);
|
|
24422
24608
|
}
|
|
24423
24609
|
};
|
|
@@ -24540,6 +24726,7 @@ async function readSharedGlb(io, buffer, resolve2) {
|
|
|
24540
24726
|
var DRACO_EXTENSION2 = "KHR_draco_mesh_compression";
|
|
24541
24727
|
var EXT_MESHOPT_EXTENSION = "EXT_meshopt_compression";
|
|
24542
24728
|
var MATERIAL_MERGE_VERSION = 1;
|
|
24729
|
+
var SHARED_TRANSCODER_ACCOUNTING_VERSION = 1;
|
|
24543
24730
|
var MODEL_DEDUPE_VERSION = 1;
|
|
24544
24731
|
var MODEL_REORDER_VERSION = 1;
|
|
24545
24732
|
var MODEL_QUANTIZE_VERSION = 1;
|
|
@@ -24569,7 +24756,7 @@ async function writeDocument(document, logicalPath, vertexLayout = "interleaved"
|
|
|
24569
24756
|
await MeshoptEncoder.ready;
|
|
24570
24757
|
const io = new NodeIO().setVertexLayout(vertexLayout === "separate" ? VertexLayout.SEPARATE : VertexLayout.INTERLEAVED).registerExtensions([...ALL_EXTENSIONS, EXTTextureAstc, TNVirtualGeometry, TNDiscreteLod]).registerDependencies({ "meshopt.encoder": MeshoptEncoder });
|
|
24571
24758
|
try {
|
|
24572
|
-
const buffer = Buffer.from(await io.writeBinary(document));
|
|
24759
|
+
const buffer = withoutSpecDefaults(Buffer.from(await io.writeBinary(document)));
|
|
24573
24760
|
return { buffer, extensions: jsonOfGlb(buffer).extensionsUsed ?? [] };
|
|
24574
24761
|
} catch (error) {
|
|
24575
24762
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -24943,7 +25130,8 @@ function modelPass(options = {}) {
|
|
|
24943
25130
|
meshopt: options.passes?.meshopt ?? true,
|
|
24944
25131
|
prune: options.passes?.prune ?? true,
|
|
24945
25132
|
quantize: options.passes?.quantize ?? true,
|
|
24946
|
-
reorder: options.passes?.reorder ?? true
|
|
25133
|
+
reorder: options.passes?.reorder ?? true,
|
|
25134
|
+
restTracks: options.passes?.restTracks ?? true
|
|
24947
25135
|
},
|
|
24948
25136
|
...options.vertexLayout === void 0 ? {} : { vertexLayout: options.vertexLayout },
|
|
24949
25137
|
quantize: {
|
|
@@ -24978,13 +25166,17 @@ function modelPass(options = {}) {
|
|
|
24978
25166
|
bakeVersion: VIRTUAL_BAKE_VERSION
|
|
24979
25167
|
},
|
|
24980
25168
|
sharedImages: options.sharedImages !== void 0,
|
|
25169
|
+
...options.sharedImages === void 0 ? {} : { sharedTranscoderAccounting: SHARED_TRANSCODER_ACCOUNTING_VERSION },
|
|
24981
25170
|
textures: options.textures === "none" ? "none" : {
|
|
24982
25171
|
...options.textures?.astc === true ? { astc: "raw4x4-gltf-authored-basis-v2" } : {},
|
|
24983
25172
|
decoderFree: options.textures?.decoderFree ?? false,
|
|
24984
25173
|
encoder: KTX2_ENCODER_VERSION,
|
|
24985
25174
|
instrument: IMAGE_QUALITY_IDENTITY,
|
|
25175
|
+
jpeg: options.textures?.jpeg ?? null,
|
|
25176
|
+
jpegLadder: "colour-opaque-q75-90-v1",
|
|
24986
25177
|
floor: resolveImageQualityFloor(options.textures?.floor),
|
|
24987
25178
|
maxSize: options.textures?.maxSize ?? null,
|
|
25179
|
+
transcoderBytes: options.textures?.transcoderBytes ?? null,
|
|
24988
25180
|
// Retention is now conditioned on the container, so it is spelled out rather than
|
|
24989
25181
|
// named by a boolean: a warm cache keyed on the old rule would keep shipping the
|
|
24990
25182
|
// WebP-requiring models this replaced.
|
|
@@ -25005,7 +25197,8 @@ function modelPass(options = {}) {
|
|
|
25005
25197
|
meshopt: passes.meshopt ?? true,
|
|
25006
25198
|
prune: passes.prune ?? true,
|
|
25007
25199
|
quantize: passes.quantize ?? true,
|
|
25008
|
-
reorder: passes.reorder ?? true
|
|
25200
|
+
reorder: passes.reorder ?? true,
|
|
25201
|
+
restTracks: passes.restTracks ?? true
|
|
25009
25202
|
};
|
|
25010
25203
|
const textureOptions = options.textures === "none" ? void 0 : options.textures ?? {};
|
|
25011
25204
|
const virtualOptions = options.virtual === "none" ? void 0 : options.virtual ?? {};
|
|
@@ -25023,6 +25216,7 @@ function modelPass(options = {}) {
|
|
|
25023
25216
|
if (enabled.dedup) await dedup()(document);
|
|
25024
25217
|
const namesBeforeCompact = compactRequested(options.compact) ? sceneNodeNames(document) : void 0;
|
|
25025
25218
|
let compact = compactRequested(options.compact) ? await compactModel(document, resolveCompactOptions(options.compact)) : void 0;
|
|
25219
|
+
if (enabled.restTracks) pruneRestPoseTracks(document, logicalPath);
|
|
25026
25220
|
if (enabled.prune) {
|
|
25027
25221
|
await prune({ keepAttributes: options.preserveLightmapUv === true })(document);
|
|
25028
25222
|
if (compact !== void 0 && namesBeforeCompact !== void 0) {
|
|
@@ -25080,7 +25274,8 @@ function modelPass(options = {}) {
|
|
|
25080
25274
|
store,
|
|
25081
25275
|
shared,
|
|
25082
25276
|
embeddedTextures?.formats,
|
|
25083
|
-
embeddedTextures?.quality
|
|
25277
|
+
embeddedTextures?.quality,
|
|
25278
|
+
embeddedTextures?.transcoderByImage
|
|
25084
25279
|
);
|
|
25085
25280
|
if (enabled.meshopt) {
|
|
25086
25281
|
document.createExtension(EXTMeshoptCompression).setRequired(true).setEncoderOptions({ method: EXTMeshoptCompression.EncoderMethod.QUANTIZE });
|
|
@@ -25153,11 +25348,14 @@ function sharedSettings(texture, textureOptions) {
|
|
|
25153
25348
|
decoderFree: textureOptions.decoderFree ?? false,
|
|
25154
25349
|
encoder: KTX2_ENCODER_VERSION,
|
|
25155
25350
|
instrument: IMAGE_QUALITY_IDENTITY,
|
|
25351
|
+
jpeg: textureOptions.jpeg ?? null,
|
|
25352
|
+
jpegLadder: "colour-opaque-q75-90-v1",
|
|
25156
25353
|
floor: resolveImageQualityFloor(textureOptions.floor),
|
|
25157
25354
|
keepSmallerSource: "universal-containers",
|
|
25158
25355
|
maxSize: resolveTextureMaxSize(textureOptions.maxSize, listTextureSlots(texture)),
|
|
25159
25356
|
overrides: textureOptions.overrides ?? [],
|
|
25160
|
-
quality: textureOptions.quality ?? null
|
|
25357
|
+
quality: textureOptions.quality ?? null,
|
|
25358
|
+
transcoderBytes: textureOptions.transcoderBytes ?? null
|
|
25161
25359
|
}
|
|
25162
25360
|
};
|
|
25163
25361
|
}
|
|
@@ -25181,7 +25379,9 @@ async function recallSharedImages(document, store, textureOptions) {
|
|
|
25181
25379
|
recalled.set(index, {
|
|
25182
25380
|
codec: stored.codec,
|
|
25183
25381
|
sourceBytes: image.byteLength,
|
|
25184
|
-
quality: stored.quality
|
|
25382
|
+
quality: stored.quality,
|
|
25383
|
+
...stored.transcoderSavings === void 0 ? {} : { transcoderSavings: stored.transcoderSavings },
|
|
25384
|
+
...stored.transcoderRequired === true ? { transcoderRequired: true } : {}
|
|
25185
25385
|
});
|
|
25186
25386
|
}
|
|
25187
25387
|
return { keys, recalled };
|
|
@@ -25190,7 +25390,7 @@ function codecOf(mimeType, formats, name) {
|
|
|
25190
25390
|
if (mimeType !== "image/ktx2") return "none";
|
|
25191
25391
|
return formats?.[name] ?? "uastc";
|
|
25192
25392
|
}
|
|
25193
|
-
async function rememberSharedImages(document, store, plan, formats, quality2) {
|
|
25393
|
+
async function rememberSharedImages(document, store, plan, formats, quality2, transcoder) {
|
|
25194
25394
|
const textures = document.getRoot().listTextures();
|
|
25195
25395
|
const names = textureKeys(document.getRoot());
|
|
25196
25396
|
for (const [index, texture] of textures.entries()) {
|
|
@@ -25198,11 +25398,15 @@ async function rememberSharedImages(document, store, plan, formats, quality2) {
|
|
|
25198
25398
|
const key = plan.keys[index];
|
|
25199
25399
|
const image = texture.getImage();
|
|
25200
25400
|
if (key === void 0 || key === "" || image === null) continue;
|
|
25401
|
+
const name = names[index] ?? "";
|
|
25402
|
+
const contribution = transcoder?.[name];
|
|
25201
25403
|
await store.put(key, {
|
|
25202
25404
|
buffer: Buffer.from(image.buffer, image.byteOffset, image.byteLength),
|
|
25203
|
-
codec: codecOf(texture.getMimeType(), formats,
|
|
25405
|
+
codec: codecOf(texture.getMimeType(), formats, name),
|
|
25204
25406
|
mimeType: texture.getMimeType(),
|
|
25205
|
-
...quality2?.[
|
|
25407
|
+
...quality2?.[name] === void 0 ? {} : { quality: quality2?.[name] },
|
|
25408
|
+
...contribution?.savings === void 0 ? {} : { transcoderSavings: contribution.savings },
|
|
25409
|
+
...contribution?.required === true ? { transcoderRequired: true } : {}
|
|
25206
25410
|
});
|
|
25207
25411
|
}
|
|
25208
25412
|
}
|
|
@@ -25215,6 +25419,8 @@ function declareSharedImage(store, logicalPath, auxiliaryOutputs, candidate) {
|
|
|
25215
25419
|
metadata: {
|
|
25216
25420
|
codec: candidate.image.codec,
|
|
25217
25421
|
key: candidate.key,
|
|
25422
|
+
transcoderRequired: candidate.image.transcoderRequired === true,
|
|
25423
|
+
transcoderSavings: candidate.image.transcoderSavings ?? 0,
|
|
25218
25424
|
...candidate.image.quality === void 0 ? {} : { quality: candidate.image.quality }
|
|
25219
25425
|
},
|
|
25220
25426
|
outputPath,
|
|
@@ -26414,7 +26620,9 @@ function embeddedTextureRow(value) {
|
|
|
26414
26620
|
} : {},
|
|
26415
26621
|
gpuBytesAfter: value.gpuBytesAfter,
|
|
26416
26622
|
gpuBytesBefore: value.gpuBytesBefore,
|
|
26417
|
-
resized: value.resized
|
|
26623
|
+
resized: value.resized,
|
|
26624
|
+
...typeof value.transcoderSavings === "number" && value.transcoderSavings > 0 ? { transcoderSavings: value.transcoderSavings } : {},
|
|
26625
|
+
...value.transcoderRequired === true ? { transcoderRequired: true } : {}
|
|
26418
26626
|
};
|
|
26419
26627
|
}
|
|
26420
26628
|
function audioRow(value) {
|
|
@@ -27128,7 +27336,16 @@ function resolveLayout(cwd, options) {
|
|
|
27128
27336
|
});
|
|
27129
27337
|
if (models !== void 0) {
|
|
27130
27338
|
const modelOptions = options.platform === void 0 || options.platform === "web" ? models : { ...models, vertexLayout: "separate" };
|
|
27131
|
-
const modelOptionsWithLod = {
|
|
27339
|
+
const modelOptionsWithLod = {
|
|
27340
|
+
...modelOptions,
|
|
27341
|
+
lod: configuredLod,
|
|
27342
|
+
...options.modelTranscoderBytes === void 0 || modelOptions.textures === "none" ? {} : {
|
|
27343
|
+
textures: {
|
|
27344
|
+
...modelOptions.textures ?? {},
|
|
27345
|
+
transcoderBytes: options.modelTranscoderBytes
|
|
27346
|
+
}
|
|
27347
|
+
}
|
|
27348
|
+
};
|
|
27132
27349
|
const pass = modelPass({
|
|
27133
27350
|
...modelOptionsWithLod,
|
|
27134
27351
|
preserveLightmapUv: lightmap !== void 0,
|
|
@@ -27469,7 +27686,8 @@ async function writeOutput(outputRoot, entry, buffer) {
|
|
|
27469
27686
|
}
|
|
27470
27687
|
async function nativeTextureEntry(name, data) {
|
|
27471
27688
|
const jpeg = data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255;
|
|
27472
|
-
|
|
27689
|
+
const webp = data.length >= 12 && data.toString("ascii", 0, 4) === "RIFF" && data.toString("ascii", 8, 12) === "WEBP";
|
|
27690
|
+
if (!jpeg && !webp && parsePng(data) === void 0) return void 0;
|
|
27473
27691
|
const image = await decodeImageBytes(data, name);
|
|
27474
27692
|
const header = Buffer.alloc(12);
|
|
27475
27693
|
header.writeUInt32LE(image.width, 0);
|
|
@@ -27673,7 +27891,62 @@ async function copyBasisTranscoder(outputRoot, transcoder) {
|
|
|
27673
27891
|
);
|
|
27674
27892
|
}
|
|
27675
27893
|
}
|
|
27894
|
+
function addTranscoderCharge(entry, state) {
|
|
27895
|
+
if (entry.kind === "texture" && entry.format !== void 0) {
|
|
27896
|
+
if (entry.format !== "none" && entry.format !== "astc4x4") state.paid = true;
|
|
27897
|
+
return;
|
|
27898
|
+
}
|
|
27899
|
+
const row = entry.embeddedTextures;
|
|
27900
|
+
if (row === void 0 || state.countedOutputs.has(entry.output)) return;
|
|
27901
|
+
state.countedOutputs.add(entry.output);
|
|
27902
|
+
state.paid ||= row.transcoderRequired === true;
|
|
27903
|
+
const images = entry.sharedImages;
|
|
27904
|
+
if (images === void 0 || images.length === 0) {
|
|
27905
|
+
state.savings += row.transcoderSavings ?? 0;
|
|
27906
|
+
return;
|
|
27907
|
+
}
|
|
27908
|
+
const detailed = images.filter(isAccountedSharedImage);
|
|
27909
|
+
if (detailed.length !== images.length) {
|
|
27910
|
+
state.needsSharedRecount ||= (row.transcoderSavings ?? 0) > 0;
|
|
27911
|
+
return;
|
|
27912
|
+
}
|
|
27913
|
+
for (const image of detailed) {
|
|
27914
|
+
if (state.countedImages.has(image.key)) continue;
|
|
27915
|
+
state.countedImages.add(image.key);
|
|
27916
|
+
state.paid ||= image.transcoderRequired;
|
|
27917
|
+
state.savings += image.transcoderSavings;
|
|
27918
|
+
}
|
|
27919
|
+
}
|
|
27920
|
+
function modelTranscoderCharge(entries) {
|
|
27921
|
+
const state = {
|
|
27922
|
+
countedImages: /* @__PURE__ */ new Set(),
|
|
27923
|
+
countedOutputs: /* @__PURE__ */ new Set(),
|
|
27924
|
+
needsSharedRecount: false,
|
|
27925
|
+
paid: false,
|
|
27926
|
+
savings: 0
|
|
27927
|
+
};
|
|
27928
|
+
for (const entry of Object.values(entries)) addTranscoderCharge(entry, state);
|
|
27929
|
+
if (state.needsSharedRecount) return 0;
|
|
27930
|
+
if (state.savings === 0) return "either";
|
|
27931
|
+
return state.paid || state.savings >= BASIS_TRANSCODER_BYTES ? 0 : void 0;
|
|
27932
|
+
}
|
|
27933
|
+
function isAccountedSharedImage(value) {
|
|
27934
|
+
return isRecord4(value) && typeof value.key === "string" && typeof value.transcoderRequired === "boolean" && typeof value.transcoderSavings === "number";
|
|
27935
|
+
}
|
|
27676
27936
|
async function compileAssets(options = {}) {
|
|
27937
|
+
if (options.modelTranscoderBytes !== void 0) return compileOnce(options);
|
|
27938
|
+
const cwd = path8.resolve(options.cwd ?? process.cwd());
|
|
27939
|
+
const manifestPath = path8.join(resolveLayout(cwd, options).outputRoot, MANIFEST_NAME);
|
|
27940
|
+
const charge = async () => modelTranscoderCharge((await readExistingManifest(manifestPath)).entries);
|
|
27941
|
+
const predicted = await charge();
|
|
27942
|
+
const used = predicted === "either" ? void 0 : predicted;
|
|
27943
|
+
const result = await compileOnce({ ...options, modelTranscoderBytes: used });
|
|
27944
|
+
const decided = await charge();
|
|
27945
|
+
if (decided === "either" || decided === used) return result;
|
|
27946
|
+
const second = await compileOnce({ ...options, modelTranscoderBytes: decided });
|
|
27947
|
+
return { ...second, written: result.written + second.written };
|
|
27948
|
+
}
|
|
27949
|
+
async function compileOnce(options) {
|
|
27677
27950
|
const cwd = path8.resolve(options.cwd ?? process.cwd());
|
|
27678
27951
|
const layout = resolveLayout(cwd, options);
|
|
27679
27952
|
const receiptPath = path8.join(layout.outputRoot, RECEIPT_NAME);
|
|
@@ -27809,7 +28082,7 @@ async function compileAssets(options = {}) {
|
|
|
27809
28082
|
let written = 0;
|
|
27810
28083
|
let skipped = 0;
|
|
27811
28084
|
let dedupeSavedBytes = 0;
|
|
27812
|
-
let
|
|
28085
|
+
let ktx2TextureCount = 0;
|
|
27813
28086
|
let compressedModelCount = 0;
|
|
27814
28087
|
if (logicals.length === 0) {
|
|
27815
28088
|
for (const line of formatBudget(
|
|
@@ -27884,7 +28157,8 @@ async function compileAssets(options = {}) {
|
|
|
27884
28157
|
format: entry.format,
|
|
27885
28158
|
logicalPath: logical
|
|
27886
28159
|
});
|
|
27887
|
-
|
|
28160
|
+
if (entry.format !== void 0 && entry.format !== "none" && entry.format !== "astc4x4")
|
|
28161
|
+
ktx2TextureCount += 1;
|
|
27888
28162
|
}
|
|
27889
28163
|
};
|
|
27890
28164
|
const concurrency = resolveConcurrency(options.concurrency ?? layout.concurrency);
|
|
@@ -28040,7 +28314,7 @@ async function compileAssets(options = {}) {
|
|
|
28040
28314
|
} finally {
|
|
28041
28315
|
await pool?.dispose();
|
|
28042
28316
|
}
|
|
28043
|
-
if (
|
|
28317
|
+
if (ktx2TextureCount > 0 || compressedModelCount > 0) {
|
|
28044
28318
|
const basisJs = path8.join(layout.outputRoot, BASIS_DIRECTORY, "basis_transcoder.js");
|
|
28045
28319
|
if (written > 0 || !await outputExists(basisJs)) {
|
|
28046
28320
|
await recordPendingOutputs([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threenative/assets",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Content-addressed asset compile step for ThreeNative games.",
|
|
6
6
|
"keywords": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"tsup": "8.5.1",
|
|
52
52
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
53
53
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
54
|
-
"threenative
|
|
54
|
+
"@threenative/blender-bridge": "0.2.1"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@gltf-transform/core": "4.4.2",
|