From a9bb5f521134ef541508239fb13b307c0f352848 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Tue, 14 Feb 2023 17:32:29 -0500 Subject: [PATCH 01/14] [functions] Add limit option to instance() --- packages/functions/src/instance.ts | 32 ++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/functions/src/instance.ts b/packages/functions/src/instance.ts index a4650fd8e..6a230728c 100644 --- a/packages/functions/src/instance.ts +++ b/packages/functions/src/instance.ts @@ -4,17 +4,33 @@ import { createTransform } from './utils'; const NAME = 'instance'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface InstanceOptions {} +export interface InstanceOptions { + /** Minimum number of meshes considered eligible for instancing. Default: 5. */ + limit?: number; +} -const INSTANCE_DEFAULTS: Required = {}; +const INSTANCE_DEFAULTS: Required = { + limit: 5, +}; /** - * Creates GPU instances (with `EXT_mesh_gpu_instancing`) for shared {@link Mesh} references. No - * options are currently implemented for this function. + * Creates GPU instances (with `EXT_mesh_gpu_instancing`) for shared {@link Mesh} references. In + * engines supporting the extension, reused Meshes will be drawn with GPU instancing, greatly + * reducing draw calls and improving performance in many cases. If you're not sure that identical + * Meshes share vertex data and materials ("linked duplicates"), run {@link dedup} first to link them. + * + * Example: + * + * ```javascript + * import { dedup, instance } from '@gltf-transform/functions'; + * + * await document.transform( + * dedup(), + * instance({limit: 5}), + * ); + * ``` */ export function instance(_options: InstanceOptions = INSTANCE_DEFAULTS): Transform { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const options = { ...INSTANCE_DEFAULTS, ..._options } as Required; return createTransform(NAME, (doc: Document): void => { @@ -44,7 +60,7 @@ export function instance(_options: InstanceOptions = INSTANCE_DEFAULTS): Transfo const modifiedNodes = []; for (const mesh of Array.from(meshInstances.keys())) { const nodes = Array.from(meshInstances.get(mesh)!); - if (nodes.length < 2) continue; + if (nodes.length < options.limit) continue; if (nodes.some((node) => node.getSkin())) continue; const batch = createBatch(doc, batchExtension, mesh, nodes.length); @@ -91,7 +107,7 @@ export function instance(_options: InstanceOptions = INSTANCE_DEFAULTS): Transfo if (numBatches > 0) { logger.info(`${NAME}: Created ${numBatches} batches, with ${numInstances} total instances.`); } else { - logger.info(`${NAME}: No meshes with multiple parent nodes were found.`); + logger.info(`${NAME}: No meshes with ā‰„${options.limit} parent nodes were found.`); } if (batchExtension.listProperties().length === 0) { From efe4e17854e04d670798b9416694af3b8b7a3b88 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Tue, 14 Feb 2023 17:32:42 -0500 Subject: [PATCH 02/14] [functions] Fix draco() default options. --- packages/functions/src/draco.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/functions/src/draco.ts b/packages/functions/src/draco.ts index 9da7bc568..352b520f8 100644 --- a/packages/functions/src/draco.ts +++ b/packages/functions/src/draco.ts @@ -31,7 +31,7 @@ export const DRACO_DEFAULTS: DracoOptions = { * * This function is a thin wrapper around the {@link KHRDracoMeshCompression} extension itself. */ -export const draco = (_options: DracoOptions): Transform => { +export const draco = (_options: DracoOptions = DRACO_DEFAULTS): Transform => { const options = { ...DRACO_DEFAULTS, ..._options } as Required; return (doc: Document): void => { doc.createExtension(KHRDracoMeshCompression) From 0814b9913855fbe6805c3d6b754c03c7da65554c Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Tue, 14 Feb 2023 17:32:52 -0500 Subject: [PATCH 03/14] [cli] Add optimize command --- packages/cli/src/cli.ts | 78 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 81685db14..08525e1ce 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -145,6 +145,84 @@ certain aspects of data layout may change slightly with this process: .argument('', OUTPUT_DESC) .action(({args, logger}) => Session.create(io, logger, args.input, args.output).transform()); +// OPTIMIZE +program + .command('optimize', 'TODO') + .help(` +TODO + `.trim()) + .argument('', INPUT_DESC) + .argument('', OUTPUT_DESC) + .option('--instance ', 'TODO', { + validator: program.NUMBER, + default: 5, + required: false + }) + .option('--compress ', 'TODO', { + validator: ['draco', 'meshopt', 'quantize'], + required: false, + }) + .option('--texture-format ', 'TODO', { + validator: ['ktx2', 'webp', 'avif'], + required: false, + }) + .option('--texture-size ', 'Maximum texture dimensions, in pixels.', { + validator: program.NUMBER, + default: 2048, + required: false + }) + .action(({args, options, logger}) => { + const opts = options as { + instance: number, + compression: 'none' | 'draco' | 'meshopt' | 'quantize', + textureFormat: 'ktx2' | 'webp' | 'webp' | undefined, + textureSize: number, + }; + + // TODO: really could use a progress spinner or checkboxes + const transforms = [ + dedup(), + instance(), // TODO: apply limit + flatten(), + join(), + prune(), // TODO: why here? + weld({ tolerance: 0.0001 }), + simplify({ simplifier: MeshoptSimplifier, ratio: 0.001, error: 0.0001 }), + prune(), // TODO: why here? + ]; + + if (opts.compression === 'draco') { + transforms.push(draco()); + } else if (opts.compression === 'meshopt') { + transforms.push(meshopt({encoder: MeshoptEncoder})); + } + + transforms.push( + resample(), + prune({ keepAttributes: false, keepLeaves: false }), + sparse(), + ); + + if (opts.textureFormat === 'ktx2') { + const slotsUASTC = '{normalTexture,occlusionTexture,metallicRoughnessTexture}'; + transforms.push( + toktx({ mode: Mode.UASTC, slots: slotsUASTC, level: 4, rdo: 4, zstd: 18 }), + toktx({ mode: Mode.ETC1S, quality: 255 }), + ); + } else { + transforms.push( + textureCompress({ + encoder: sharp, + targetFormat: opts.textureFormat, + resize: [opts.textureSize, opts.textureSize] + }), + ); + } + + return Session.create(io, logger, args.input, args.output) + .transform(...transforms); + }); + // MERGE program .command('merge', 'Merge two or more models into one') From 6f68a633ec52e285ca806d0d672cb5e75db9fec9 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Tue, 14 Feb 2023 17:43:58 -0500 Subject: [PATCH 04/14] Fix (new?) bug with 'semver' package ESM/CJS support --- packages/cli/src/transforms/toktx.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/transforms/toktx.ts b/packages/cli/src/transforms/toktx.ts index 70a417183..48e14c159 100644 --- a/packages/cli/src/transforms/toktx.ts +++ b/packages/cli/src/transforms/toktx.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync } from 'fs'; import { join } from 'path'; import micromatch from 'micromatch'; import os from 'os'; -import { clean, gte, lt, valid } from 'semver'; +import semver from 'semver'; import tmp from 'tmp'; import pLimit from 'p-limit'; @@ -286,7 +286,7 @@ function createParams( if (slots.find((slot) => micromatch.isMatch(slot, '*normal*', MICROMATCH_OPTIONS))) { // See: https://github.com/KhronosGroup/KTX-Software/issues/600 - if (gte(version, KTX_SOFTWARE_VERSION_ACTIVE)) { + if (semver.gte(version, KTX_SOFTWARE_VERSION_ACTIVE)) { params.push('--normal_mode', '--input_swizzle', 'rgb1'); } else if (options.mode === Mode.ETC1S) { params.push('--normal_map'); @@ -357,15 +357,15 @@ async function checkKTXSoftware(logger: ILogger): Promise { .replace(/~\d+/, '') .trim(); - if (status !== 0 || !valid(clean(version))) { + if (status !== 0 || !semver.valid(semver.clean(version))) { throw new Error('Unable to find "toktx" version. Confirm KTX-Software is installed.'); - } else if (lt(clean(version)!, KTX_SOFTWARE_VERSION_MIN)) { + } else if (semver.lt(semver.clean(version)!, KTX_SOFTWARE_VERSION_MIN)) { logger.warn(`toktx: Expected KTX-Software >= v${KTX_SOFTWARE_VERSION_MIN}, found ${version}.`); } else { logger.debug(`toktx: Found KTX-Software ${version}.`); } - return clean(version)!; + return semver.clean(version)!; } function isPowerOfTwo(value: number): boolean { From 8c7a1ee03cb0a8523ebe01d050bafb0cd83ce183 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 15 Feb 2023 10:13:39 -0500 Subject: [PATCH 05/14] Add Listr2 for optimize command. --- packages/cli/package.json | 1 + packages/cli/src/cli.ts | 6 +- packages/cli/src/session.ts | 47 ++++++++++++-- packages/cli/src/util.ts | 4 ++ packages/functions/src/texture-compress.ts | 6 +- yarn.lock | 71 +++++++++++++++++++++- 6 files changed, 120 insertions(+), 15 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index f34be8b4a..9b09dea22 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -43,6 +43,7 @@ "inquirer": "^9.1.4", "ktx-parse": "^0.4.5", "language-tags": "^1.0.7", + "listr2": "^5.0.7", "meshoptimizer": "^0.18.1", "micromatch": "^4.0.5", "mikktspace": "^1.1.1", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 08525e1ce..a8afc6189 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -171,7 +171,7 @@ TODO default: 2048, required: false }) - .action(({args, options, logger}) => { + .action(async ({args, options, logger}) => { const opts = options as { instance: number, compression: 'none' | 'draco' | 'meshopt' | 'quantize', @@ -179,10 +179,9 @@ TODO textureSize: number, }; - // TODO: really could use a progress spinner or checkboxes const transforms = [ dedup(), - instance(), // TODO: apply limit + instance({limit: options.limit as number}), flatten(), join(), prune(), // TODO: why here? @@ -220,6 +219,7 @@ TODO } return Session.create(io, logger, args.input, args.output) + .setDisplay(true) .transform(...transforms); }); diff --git a/packages/cli/src/session.ts b/packages/cli/src/session.ts index 5be92b4fa..30b5e6ebb 100644 --- a/packages/cli/src/session.ts +++ b/packages/cli/src/session.ts @@ -1,11 +1,14 @@ import { Document, NodeIO, Logger, FileUtils, Transform, Format, ILogger } from '@gltf-transform/core'; import type { Packet, KHRXMP } from '@gltf-transform/extensions'; import { unpartition } from '@gltf-transform/functions'; -import { formatBytes, XMPContext } from './util'; +import { Listr, ListrTask } from 'listr2'; +import { dim, formatBytes, formatLong, XMPContext } from './util'; +import type caporal from '@caporal/core'; /** Helper class for managing a CLI command session. */ export class Session { private _outputFormat: Format; + private _display = false; constructor(private _io: NodeIO, private _logger: ILogger, private _input: string, private _output: string) { _io.setLogger(_logger); @@ -16,14 +19,20 @@ export class Session { return new Session(io, logger as Logger, input as string, output as string); } + public setDisplay(display: boolean): this { + this._display = display; + return this; + } + public async transform(...transforms: Transform[]): Promise { - const doc = this._input + const logger = this._logger as caporal.Logger; + const document = this._input ? (await this._io.read(this._input)).setLogger(this._logger) : new Document().setLogger(this._logger); // Warn and remove lossy compression, to avoid increasing loss on round trip. for (const extensionName of ['KHR_draco_mesh_compression', 'EXT_meshopt_compression']) { - const extension = doc + const extension = document .getRoot() .listExtensionsUsed() .find((extension) => extension.extensionName === extensionName); @@ -33,13 +42,39 @@ export class Session { } } - await doc.transform(...transforms, updateMetadata); + if (this._display) { + const tasks = [] as ListrTask[]; + for (const transform of transforms) { + tasks.push({ + title: transform.name, + task: async (ctx, task) => { + let time = performance.now(); + await document.transform(transform); + time = Math.round(performance.now() - time); + task.title = task.title.padEnd(20) + dim(` ${formatLong(time)}ms`); + }, + }); + } + + const prevLevel = logger.level; + if (prevLevel === 'info') { + logger.level = 'warning'; + } + + await new Listr(tasks, { renderer: 'simple' }).run(); + + logger.level = prevLevel; + } else { + await document.transform(...transforms); + } + + await document.transform(updateMetadata); if (this._outputFormat === Format.GLB) { - await doc.transform(unpartition()); + await document.transform(unpartition()); } - await this._io.write(this._output, doc); + await this._io.write(this._output, document); const { lastReadBytes, lastWriteBytes } = this._io; if (!this._input) { diff --git a/packages/cli/src/util.ts b/packages/cli/src/util.ts index a7c88f906..fafa2d716 100644 --- a/packages/cli/src/util.ts +++ b/packages/cli/src/util.ts @@ -157,3 +157,7 @@ export function formatXMP(value: string | number | boolean | Record => { + return createTransform(NAME, async (document: Document): Promise => { const logger = document.getLogger(); const textures = document.getRoot().listTextures(); @@ -211,7 +211,7 @@ export const textureCompress = function (_options: TextureCompressOptions): Tran } logger.debug(`${NAME}: Complete.`); - }; + }); }; function getFormat(texture: Texture): Format { diff --git a/yarn.lock b/yarn.lock index 15bb18ddf..5aa5b8452 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2610,7 +2610,7 @@ ansi-escapes@^3.2.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== -ansi-escapes@^4.2.1: +ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== @@ -2791,6 +2791,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + async@0.9.x: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -3402,6 +3407,14 @@ cli-table3@^0.6.3: optionalDependencies: "@colors/colors" "1.5.0" +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cli-truncate@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-3.1.0.tgz#3f23ab12535e3d73e839bb43e73c9de487db1389" @@ -3559,6 +3572,11 @@ colorette@^1.2.1: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== +colorette@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + colornames@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96" @@ -6311,6 +6329,20 @@ lines-and-columns@~2.0.3: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz#b2f0badedb556b747020ab8ea7f0373e22efac1b" integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w== +listr2@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-5.0.7.tgz#de69ccc4caf6bea7da03c74f7a2ffecf3904bd53" + integrity sha512-MD+qXHPmtivrHIDRwPYdfNkrzqDiuaKU/rfBcec3WMyMF3xylQj3jMq344OtvQxz7zaCFViRAeqlr2AFhPvXHw== + dependencies: + cli-truncate "^2.1.0" + colorette "^2.0.19" + log-update "^4.0.0" + p-map "^4.0.0" + rfdc "^1.3.0" + rxjs "^7.8.0" + through "^2.3.8" + wrap-ansi "^7.0.0" + load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -6430,6 +6462,16 @@ log-symbols@^5.1.0: chalk "^5.0.0" is-unicode-supported "^1.1.0" +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + logform@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" @@ -8696,6 +8738,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + rgb-regex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" @@ -8809,7 +8856,7 @@ rxjs@^7.0.0, rxjs@^7.5.5: dependencies: tslib "^2.1.0" -rxjs@^7.2.0, rxjs@^7.5.7: +rxjs@^7.2.0, rxjs@^7.5.7, rxjs@^7.8.0: version "7.8.0" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== @@ -9028,6 +9075,24 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + slice-ansi@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" @@ -9614,7 +9679,7 @@ through2@^4.0.0: dependencies: readable-stream "3" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= From c586325a6afc691117590c334d4a2a0a4d82b624 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 15 Feb 2023 11:40:12 -0500 Subject: [PATCH 06/14] Refinement --- packages/cli/src/cli.ts | 84 ++++++++++++++++------------ packages/cli/src/session.ts | 7 +-- packages/cli/src/transforms/toktx.ts | 6 +- packages/functions/src/draco.ts | 7 ++- packages/functions/src/instance.ts | 10 ++-- packages/functions/src/join.ts | 20 +++---- packages/functions/src/meshopt.ts | 5 +- 7 files changed, 76 insertions(+), 63 deletions(-) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index a8afc6189..86e9bd0b0 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -46,13 +46,13 @@ const PACKAGE = JSON.parse( program .version(PACKAGE.version) - .description('Command-line interface (CLI) for the glTF-Transform SDK.'); + .description('Command-line interface (CLI) for the glTF Transform SDK.'); program.command('', '\n\nšŸ”Ž INSPECT ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€'); // INSPECT program - .command('inspect', 'Inspect the contents of the model') + .command('inspect', 'Inspect contents of the model') .help(` Inspect the contents of the model, printing a table with properties and statistics for scenes, meshes, materials, textures, and animations contained @@ -79,7 +79,7 @@ Use --format=csv or --format=md for alternative display formats. // VALIDATE program - .command('validate', 'Validate the model against the glTF spec') + .command('validate', 'Validate model against the glTF spec') .help(` Validate the model with official glTF validator. The validator detects whether a file conforms correctly to the glTF specification, and is useful for @@ -121,14 +121,14 @@ program.command('', '\n\nšŸ“¦ PACKAGE ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ // COPY program - .command('copy', 'Copy the model with minimal changes') + .command('copy', 'Copy model with minimal changes') .alias('cp') .help(` Copy the model from to with minimal changes. Unlike filesystem -\`cp\`, this command does parse the file into glTF-Transform's internal +\`cp\`, this command does parse the file into glTF Transform's internal representation before serializing it to disk again. No other intentional changes are made, so copying a model can be a useful first step to confirm that -glTF-Transform is reading and writing the model correctly when debugging issues +glTF Transform is reading and writing the model correctly when debugging issues in a larger script doing more complex processing of the file. Copying may also be used to ensure consistent data layout across glTF files from different exporters, e.g. if your engine always requires interleaved vertex attributes. @@ -147,24 +147,35 @@ certain aspects of data layout may change slightly with this process: // OPTIMIZE program - .command('optimize', 'TODO') + .command('optimize', 'āœØ Optimize model by all available methods') .help(` -TODO +Optimize the model by all available methods. Combines many features of the +glTF Transform CLI into a single command for convenience and faster results. +For more control over the optimization process, consider running individual +commands or using the scripting API. `.trim()) .argument('', INPUT_DESC) .argument('', OUTPUT_DESC) - .option('--instance ', 'TODO', { + .option('--instance ', 'Create GPU instances from shared mesh references', { validator: program.NUMBER, default: 5, required: false }) - .option('--compress ', 'TODO', { - validator: ['draco', 'meshopt', 'quantize'], + .option( + '--compress ', + 'Floating point compression method. Draco compresses geometry; Meshopt ' + + 'and quantization compress geometry and animation.', { + validator: ['draco', 'meshopt', 'quantize', false], required: false, + default: 'draco', }) - .option('--texture-format ', 'TODO', { - validator: ['ktx2', 'webp', 'avif'], + .option( + '--texture-format ', + 'Texture compression format. KTX2 optimizes VRAM usage and performance; ' + + 'AVIF and WebP optimize transmission size. Auto recompresses in original format.', { + validator: ['ktx2', 'webp', 'avif', 'auto', false], required: false, + default: 'auto', }) .option('--texture-size ', 'Maximum texture dimensions, in pixels.', { validator: program.NUMBER, @@ -174,50 +185,51 @@ TODO .action(async ({args, options, logger}) => { const opts = options as { instance: number, - compression: 'none' | 'draco' | 'meshopt' | 'quantize', - textureFormat: 'ktx2' | 'webp' | 'webp' | undefined, + compress: 'draco' | 'meshopt' | 'quantize' | false, + textureFormat: 'ktx2' | 'webp' | 'webp' | 'auto' | false, textureSize: number, }; + // Baseline transforms. const transforms = [ dedup(), - instance({limit: options.limit as number}), + instance({min: options.instance as number}), flatten(), join(), - prune(), // TODO: why here? weld({ tolerance: 0.0001 }), simplify({ simplifier: MeshoptSimplifier, ratio: 0.001, error: 0.0001 }), - prune(), // TODO: why here? - ]; - - if (opts.compression === 'draco') { - transforms.push(draco()); - } else if (opts.compression === 'meshopt') { - transforms.push(meshopt({encoder: MeshoptEncoder})); - } - - transforms.push( resample(), prune({ keepAttributes: false, keepLeaves: false }), sparse(), - ); + ]; + // Texture compression. if (opts.textureFormat === 'ktx2') { const slotsUASTC = '{normalTexture,occlusionTexture,metallicRoughnessTexture}'; transforms.push( toktx({ mode: Mode.UASTC, slots: slotsUASTC, level: 4, rdo: 4, zstd: 18 }), toktx({ mode: Mode.ETC1S, quality: 255 }), ); - } else { + } else if (opts.textureFormat !== false) { transforms.push( textureCompress({ encoder: sharp, - targetFormat: opts.textureFormat, + targetFormat: opts.textureFormat === 'auto' ? undefined : opts.textureFormat, resize: [opts.textureSize, opts.textureSize] }), ); } + // Mesh compression last. Doesn't matter here, but in one-off CLI + // commands we want to avoid recompressing mesh data. + if (opts.compress === 'draco') { + transforms.push(draco()); + } else if (opts.compress === 'meshopt') { + transforms.push(meshopt({encoder: MeshoptEncoder})); + } else if (opts.compress === 'quantize') { + transforms.push(quantize()); + } + return Session.create(io, logger, args.input, args.output) .setDisplay(true) .transform(...transforms); @@ -346,7 +358,7 @@ that are children of a scene. // GZIP program - .command('gzip', 'Compress the model with lossless gzip') + .command('gzip', 'Compress model with lossless gzip') .help(` Compress the model with gzip. Gzip is a general-purpose file compression technique, not specific to glTF models. On the web, decompression is @@ -451,7 +463,7 @@ https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_mesh_ // FLATTEN program - .command('flatten', 'Flatten scene graph') + .command('flatten', 'āœØ Flatten scene graph') .help(` Flattens the scene graph, leaving Nodes with Meshes, Cameras, and other attachments as direct children of the Scene. Skeletons and their @@ -469,7 +481,7 @@ moved. // JOIN program - .command('join', 'Join meshes and reduce draw calls') + .command('join', 'āœØ Join meshes and reduce draw calls') .help(` Joins compatible Primitives and reduces draw calls. Primitives are eligible for joining if they are members of the same Mesh or, optionally, attached to sibling @@ -811,7 +823,7 @@ Based on the meshoptimizer library (https://github.com/zeux/meshoptimizer). .transform(simplify({simplifier: MeshoptSimplifier, ...options})) ); -program.command('', '\n\nāœØ MATERIAL ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€'); +program.command('', '\n\nšŸŽØ MATERIAL ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€'); // METALROUGH program @@ -1153,7 +1165,7 @@ and require less tuning to achieve good visual and filesize results. // AVIF program - .command('avif', 'AVIF texture compression') + .command('avif', 'āœØ AVIF texture compression') .help(TEXTURE_COMPRESS_SUMMARY.replace(/{VARIANT}/g, 'AVIF')) .argument('', INPUT_DESC) .argument('', OUTPUT_DESC) @@ -1389,7 +1401,7 @@ so this workflow is not a replacement for video playback. // SPARSE program - .command('sparse', 'Reduces storage for zero-filled arrays') + .command('sparse', 'āœØ Reduces storage for zero-filled arrays') .help(` Scans all Accessors in the Document, detecting whether each Accessor would benefit from sparse data storage. Currently, sparse data storage is used only diff --git a/packages/cli/src/session.ts b/packages/cli/src/session.ts index 30b5e6ebb..67c18021f 100644 --- a/packages/cli/src/session.ts +++ b/packages/cli/src/session.ts @@ -57,11 +57,10 @@ export class Session { } const prevLevel = logger.level; - if (prevLevel === 'info') { - logger.level = 'warning'; - } + if (prevLevel === 'info') logger.level = 'warn'; - await new Listr(tasks, { renderer: 'simple' }).run(); + // Simple renderer shows warnings and errors. Disable signal listeners so Ctrl+C works. + await new Listr(tasks, { renderer: 'simple', registerSignalListeners: false }).run(); logger.level = prevLevel; } else { diff --git a/packages/cli/src/transforms/toktx.ts b/packages/cli/src/transforms/toktx.ts index 48e14c159..fe9a58cd4 100644 --- a/packages/cli/src/transforms/toktx.ts +++ b/packages/cli/src/transforms/toktx.ts @@ -9,7 +9,7 @@ import pLimit from 'p-limit'; import { Document, FileUtils, ILogger, ImageUtils, TextureChannel, Transform, vec2, uuid } from '@gltf-transform/core'; import { KHRTextureBasisu } from '@gltf-transform/extensions'; -import { getTextureChannelMask, listTextureSlots } from '@gltf-transform/functions'; +import { createTransform, getTextureChannelMask, listTextureSlots } from '@gltf-transform/functions'; import { spawn, commandExists, formatBytes, waitExit, MICROMATCH_OPTIONS } from '../util'; tmp.setGracefulCleanup(); @@ -112,7 +112,7 @@ export const toktx = function (options: ETC1SOptions | UASTCOptions): Transform ...options, }; - return async (doc: Document): Promise => { + return createTransform(options.mode, async (doc: Document): Promise => { const logger = doc.getLogger(); // Confirm recent version of KTX-Software is installed. @@ -221,7 +221,7 @@ export const toktx = function (options: ETC1SOptions | UASTCOptions): Transform if (!usesKTX2) { basisuExtension.dispose(); } - }; + }); }; /********************************************************************************************** diff --git a/packages/functions/src/draco.ts b/packages/functions/src/draco.ts index 352b520f8..9a869f060 100644 --- a/packages/functions/src/draco.ts +++ b/packages/functions/src/draco.ts @@ -1,5 +1,8 @@ import type { Document, Transform } from '@gltf-transform/core'; import { KHRDracoMeshCompression } from '@gltf-transform/extensions'; +import { createTransform } from './utils'; + +const NAME = 'draco'; export interface DracoOptions { method?: 'edgebreaker' | 'sequential'; @@ -33,7 +36,7 @@ export const DRACO_DEFAULTS: DracoOptions = { */ export const draco = (_options: DracoOptions = DRACO_DEFAULTS): Transform => { const options = { ...DRACO_DEFAULTS, ..._options } as Required; - return (doc: Document): void => { + return createTransform(NAME, (doc: Document): void => { doc.createExtension(KHRDracoMeshCompression) .setRequired(true) .setEncoderOptions({ @@ -52,5 +55,5 @@ export const draco = (_options: DracoOptions = DRACO_DEFAULTS): Transform => { }, quantizationVolume: options.quantizationVolume, }); - }; + }); }; diff --git a/packages/functions/src/instance.ts b/packages/functions/src/instance.ts index 6a230728c..b6b5cc507 100644 --- a/packages/functions/src/instance.ts +++ b/packages/functions/src/instance.ts @@ -6,11 +6,11 @@ const NAME = 'instance'; export interface InstanceOptions { /** Minimum number of meshes considered eligible for instancing. Default: 5. */ - limit?: number; + min?: number; } const INSTANCE_DEFAULTS: Required = { - limit: 5, + min: 5, }; /** @@ -26,7 +26,7 @@ const INSTANCE_DEFAULTS: Required = { * * await document.transform( * dedup(), - * instance({limit: 5}), + * instance({min: 5}), * ); * ``` */ @@ -60,7 +60,7 @@ export function instance(_options: InstanceOptions = INSTANCE_DEFAULTS): Transfo const modifiedNodes = []; for (const mesh of Array.from(meshInstances.keys())) { const nodes = Array.from(meshInstances.get(mesh)!); - if (nodes.length < options.limit) continue; + if (nodes.length < options.min) continue; if (nodes.some((node) => node.getSkin())) continue; const batch = createBatch(doc, batchExtension, mesh, nodes.length); @@ -107,7 +107,7 @@ export function instance(_options: InstanceOptions = INSTANCE_DEFAULTS): Transfo if (numBatches > 0) { logger.info(`${NAME}: Created ${numBatches} batches, with ${numInstances} total instances.`); } else { - logger.info(`${NAME}: No meshes with ā‰„${options.limit} parent nodes were found.`); + logger.info(`${NAME}: No meshes with ā‰„${options.min} parent nodes were found.`); } if (batchExtension.listProperties().length === 0) { diff --git a/packages/functions/src/join.ts b/packages/functions/src/join.ts index c2fe014e1..5f5475f40 100644 --- a/packages/functions/src/join.ts +++ b/packages/functions/src/join.ts @@ -13,7 +13,7 @@ import { invert, multiply } from 'gl-matrix/mat4'; import { joinPrimitives } from './join-primitives'; import { prune } from './prune'; import { transformPrimitive } from './transform-primitive'; -import { createPrimGroupKey, createTransform, formatLong, isTransformPending, isUsed } from './utils'; +import { createPrimGroupKey, createTransform, formatLong, isUsed } from './utils'; const NAME = 'join'; @@ -78,7 +78,7 @@ export const JOIN_DEFAULTS: Required = { export function join(_options: JoinOptions = JOIN_DEFAULTS): Transform { const options = { ...JOIN_DEFAULTS, ..._options } as Required; - return createTransform(NAME, async (document: Document, context): Promise => { + return createTransform(NAME, async (document: Document): Promise => { const root = document.getRoot(); const logger = document.getLogger(); @@ -89,15 +89,13 @@ export function join(_options: JoinOptions = JOIN_DEFAULTS): Transform { } // Clean up. - if (!isTransformPending(context, NAME, 'prune')) { - await document.transform( - prune({ - propertyTypes: [NODE, MESH, PRIMITIVE, ACCESSOR], - keepLeaves: false, - keepAttributes: true, - }) - ); - } + await document.transform( + prune({ + propertyTypes: [NODE, MESH, PRIMITIVE, ACCESSOR], + keepLeaves: false, + keepAttributes: true, + }) + ); logger.debug(`${NAME}: Complete.`); }); diff --git a/packages/functions/src/meshopt.ts b/packages/functions/src/meshopt.ts index 62d8528df..b8b9bda91 100644 --- a/packages/functions/src/meshopt.ts +++ b/packages/functions/src/meshopt.ts @@ -3,6 +3,7 @@ import { EXTMeshoptCompression } from '@gltf-transform/extensions'; import type { MeshoptEncoder } from 'meshoptimizer'; import { reorder } from './reorder'; import { quantize } from './quantize'; +import { createTransform } from './utils'; export interface MeshoptOptions { encoder: unknown; @@ -44,7 +45,7 @@ export const meshopt = (_options: MeshoptOptions): Transform => { throw new Error(`${NAME}: encoder dependency required ā€” install "meshoptimizer".`); } - return async (document: Document): Promise => { + return createTransform(NAME, async (document: Document): Promise => { await document.transform( reorder({ encoder: encoder, @@ -70,5 +71,5 @@ export const meshopt = (_options: MeshoptOptions): Transform => { ? EXTMeshoptCompression.EncoderMethod.QUANTIZE : EXTMeshoptCompression.EncoderMethod.FILTER, }); - }; + }); }; From 0f667e4f19b45e321f84be5f4e9d2b2ab2fdff00 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 15 Feb 2023 11:50:54 -0500 Subject: [PATCH 07/14] Clean up, fix roundtrip tests --- packages/cli/src/cli.ts | 10 +++++----- test/{clean.js => clean.cjs} | 2 +- test/{constants.js => constants.cjs} | 0 test/index.html | 2 +- test/{roundtrip.js => roundtrip.cjs} | 15 +++++---------- test/{validate.js => validate.cjs} | 0 6 files changed, 12 insertions(+), 17 deletions(-) rename test/{clean.js => clean.cjs} (99%) rename test/{constants.js => constants.cjs} (100%) rename test/{roundtrip.js => roundtrip.cjs} (58%) rename test/{validate.js => validate.cjs} (100%) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 86e9bd0b0..5c0c0aad5 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -170,7 +170,7 @@ commands or using the scripting API. default: 'draco', }) .option( - '--texture-format ', + '--texture-compress ', 'Texture compression format. KTX2 optimizes VRAM usage and performance; ' + 'AVIF and WebP optimize transmission size. Auto recompresses in original format.', { validator: ['ktx2', 'webp', 'avif', 'auto', false], @@ -186,7 +186,7 @@ commands or using the scripting API. const opts = options as { instance: number, compress: 'draco' | 'meshopt' | 'quantize' | false, - textureFormat: 'ktx2' | 'webp' | 'webp' | 'auto' | false, + textureCompress: 'ktx2' | 'webp' | 'webp' | 'auto' | false, textureSize: number, }; @@ -204,17 +204,17 @@ commands or using the scripting API. ]; // Texture compression. - if (opts.textureFormat === 'ktx2') { + if (opts.textureCompress === 'ktx2') { const slotsUASTC = '{normalTexture,occlusionTexture,metallicRoughnessTexture}'; transforms.push( toktx({ mode: Mode.UASTC, slots: slotsUASTC, level: 4, rdo: 4, zstd: 18 }), toktx({ mode: Mode.ETC1S, quality: 255 }), ); - } else if (opts.textureFormat !== false) { + } else if (opts.textureCompress !== false) { transforms.push( textureCompress({ encoder: sharp, - targetFormat: opts.textureFormat === 'auto' ? undefined : opts.textureFormat, + targetFormat: opts.textureCompress === 'auto' ? undefined : opts.textureCompress, resize: [opts.textureSize, opts.textureSize] }), ); diff --git a/test/clean.js b/test/clean.cjs similarity index 99% rename from test/clean.js rename to test/clean.cjs index 217b53239..68991c089 100644 --- a/test/clean.js +++ b/test/clean.cjs @@ -1,7 +1,7 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { SOURCE, TARGET, VARIANTS, SKIPLIST } = require('./constants.js'); +const { SOURCE, TARGET, VARIANTS, SKIPLIST } = require('./constants.cjs'); /** * Cleans the `out/` directory, then makes fresh copies of all supported sample diff --git a/test/constants.js b/test/constants.cjs similarity index 100% rename from test/constants.js rename to test/constants.cjs diff --git a/test/index.html b/test/index.html index 44f117cf9..30c1509fb 100644 --- a/test/index.html +++ b/test/index.html @@ -53,7 +53,7 @@

Round trip tests

el.setAttribute( 'src', - `./out/${asset.name}/${variant}/${filename.replace(/\.(gltf|glb)$/, '.copy.glb')}` + `./out/${asset.name}/${variant}/${filename.replace(/\.(gltf|glb)$/, '.opt.glb')}` ); el.alt = el.title = `${asset.name} / ${variant} / ${filename} (COPY)`; containerEl.appendChild(el); diff --git a/test/roundtrip.js b/test/roundtrip.cjs similarity index 58% rename from test/roundtrip.js rename to test/roundtrip.cjs index dfb4421b1..f3924dc47 100644 --- a/test/roundtrip.js +++ b/test/roundtrip.cjs @@ -1,11 +1,11 @@ const path = require('path'); const { execSync } = require('child_process'); -const { SOURCE, TARGET, VARIANTS } = require('./constants.js'); +const { SOURCE, TARGET, VARIANTS } = require('./constants.cjs'); /** - * Generates a copy of each sample model using `gltf-transform copy`. Does - * not apply any meaningful edits to the files: this is intended to be a - * lossless round trip test. + * Generates a copy of each sample model using `copy` and `optimize`. Copy + * not apply any meaningful edits to the files, and is intended to be a + * lossless round trip test. Optimize runs a number of other commands. */ const INDEX = require(path.join(TARGET, 'model-index.json')); @@ -21,12 +21,7 @@ INDEX.forEach((asset, assetIndex) => { try { execSync(`gltf-transform copy ${src} ${dst.replace('{v}', 'copy')}`); - execSync( - `gltf-transform quantize ${src} ${dst.replace('{v}', 'quantize-lo')}` + - ' --quantizePosition 14 --quantizeTexcoord 12 --quantizeColor 8 --quantizeNormal 8' - ); - execSync(`gltf-transform quantize ${src} ${dst.replace('{v}', 'quantize-hi')}`); - execSync(`gltf-transform draco ${src} ${dst.replace('{v}', 'draco')}`); + execSync(`gltf-transform optimize ${src} ${dst.replace('{v}', 'opt')} --texture-format webp`); console.info(` - āœ… ${variant}/${filename}`); } catch (e) { console.error(` - ā›”ļø ${variant}/${filename}: ${e.message}`); diff --git a/test/validate.js b/test/validate.cjs similarity index 100% rename from test/validate.js rename to test/validate.cjs From b0aef6154c09217423fe2f34082a9047b2749e14 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 15 Feb 2023 12:05:24 -0500 Subject: [PATCH 08/14] Restore original instance minimum. --- packages/functions/src/instance.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/functions/src/instance.ts b/packages/functions/src/instance.ts index b6b5cc507..0318250ba 100644 --- a/packages/functions/src/instance.ts +++ b/packages/functions/src/instance.ts @@ -5,12 +5,12 @@ import { createTransform } from './utils'; const NAME = 'instance'; export interface InstanceOptions { - /** Minimum number of meshes considered eligible for instancing. Default: 5. */ + /** Minimum number of meshes considered eligible for instancing. Default: 2. */ min?: number; } const INSTANCE_DEFAULTS: Required = { - min: 5, + min: 2, }; /** @@ -26,7 +26,7 @@ const INSTANCE_DEFAULTS: Required = { * * await document.transform( * dedup(), - * instance({min: 5}), + * instance({min: 2}), * ); * ``` */ From 9911c8a0172615c517f66579eef98448a742b9aa Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 15 Feb 2023 13:19:10 -0500 Subject: [PATCH 09/14] Overhaul RT tests --- test/clean.cjs | 43 ------------------------------------------- test/constants.cjs | 5 ++--- test/index.html | 13 +++++++------ test/roundtrip.cjs | 21 +++++++++++++++------ 4 files changed, 24 insertions(+), 58 deletions(-) delete mode 100644 test/clean.cjs diff --git a/test/clean.cjs b/test/clean.cjs deleted file mode 100644 index 68991c089..000000000 --- a/test/clean.cjs +++ /dev/null @@ -1,43 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); -const { SOURCE, TARGET, VARIANTS, SKIPLIST } = require('./constants.cjs'); - -/** - * Cleans the `out/` directory, then makes fresh copies of all supported sample - * model files, in their original/unmodified forms. - */ - -const INDEX = require(path.join(SOURCE, 'model-index.json')).filter((asset) => !SKIPLIST.has(asset.name)); - -INDEX.forEach((asset) => { - cleanDir(path.join(TARGET, asset.name)); - - const unsupported = []; - for (const variant in asset.variants) { - if (!VARIANTS.has(variant)) unsupported.push(variant); - } - for (const variant of unsupported) delete asset.variants[variant]; - - Object.entries(asset.variants).forEach(([variant, filename]) => { - if (!VARIANTS.has(variant)) return; - - const src = path.join(SOURCE, asset.name, variant); - const dst = path.join(TARGET, asset.name, variant); - execSync(`cp -r "${src}" "${dst}"`); - }); -}); - -fs.writeFileSync(path.join(TARGET, 'model-index.json'), JSON.stringify(INDEX, null, 2)); - -function cleanDir(dir) { - if (fs.existsSync(dir)) { - const relPath = path.relative(TARGET, dir); - const isSafe = relPath && !relPath.startsWith('..') && !path.isAbsolute(relPath); - if (!isSafe) { - throw new Error(`Path not safe: ${dir}`); - } - execSync(`rm -rf "${dir}"`); - } - fs.mkdirSync(dir); -} diff --git a/test/constants.cjs b/test/constants.cjs index 6b3ea44cf..6f1a3e14f 100644 --- a/test/constants.cjs +++ b/test/constants.cjs @@ -11,8 +11,7 @@ const SOURCE = path.resolve(__dirname, '../../glTF-Sample-Models/2.0/'); /** Output directory for generated roundtrip assets. */ const TARGET = path.resolve(__dirname, './out'); -/** Supported variants. */ -const VARIANTS = new Set(['glTF-Binary']); +const VARIANT = 'glTF-Binary'; /** * Assets to skip. @@ -24,4 +23,4 @@ const VARIANTS = new Set(['glTF-Binary']); */ const SKIPLIST = new Set(['AnimatedTriangle', 'SimpleMorph', 'SpecGlossVsMetalRough']); -module.exports = { SOURCE, TARGET, VARIANTS, SKIPLIST }; +module.exports = { SOURCE, TARGET, VARIANT, SKIPLIST }; diff --git a/test/index.html b/test/index.html index 30c1509fb..1ca6c7a6c 100644 --- a/test/index.html +++ b/test/index.html @@ -46,18 +46,19 @@

Round trip tests

Object.entries(asset.variants).forEach(([variant, filename]) => { const el = document.createElement('model-viewer'); - el.setAttribute('src', `./out/${asset.name}/${variant}/${filename}`); + el.setAttribute('src', `./out/${suffix(filename, '_')}`); el.setAttribute('autoplay', ''); el.alt = el.title = `${asset.name} / ${variant} / ${filename}`; containerEl.appendChild(el.cloneNode()); - el.setAttribute( - 'src', - `./out/${asset.name}/${variant}/${filename.replace(/\.(gltf|glb)$/, '.opt.glb')}` - ); - el.alt = el.title = `${asset.name} / ${variant} / ${filename} (COPY)`; + el.setAttribute('src', `./out/${suffix(filename, 'copy')}`); + el.alt = el.title = `${asset.name} / ${variant} / ${filename}`; containerEl.appendChild(el); }); }); }); + + function suffix(path, suffix) { + return path.replace('.glb', `.${suffix}.glb`); + } diff --git a/test/roundtrip.cjs b/test/roundtrip.cjs index f3924dc47..464807f9e 100644 --- a/test/roundtrip.cjs +++ b/test/roundtrip.cjs @@ -1,6 +1,7 @@ +const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); -const { SOURCE, TARGET, VARIANTS } = require('./constants.cjs'); +const { SOURCE, TARGET, VARIANT, SKIPLIST } = require('./constants.cjs'); /** * Generates a copy of each sample model using `copy` and `optimize`. Copy @@ -8,20 +9,26 @@ const { SOURCE, TARGET, VARIANTS } = require('./constants.cjs'); * lossless round trip test. Optimize runs a number of other commands. */ -const INDEX = require(path.join(TARGET, 'model-index.json')); +const INDEX = require(path.join(SOURCE, 'model-index.json')).filter((asset) => !SKIPLIST.has(asset.name)); + +execSync(`rm -r ${TARGET}/**`); INDEX.forEach((asset, assetIndex) => { console.info(`šŸ“¦ ${asset.name} (${assetIndex + 1} / ${INDEX.length})`); Object.entries(asset.variants).forEach(([variant, filename]) => { - if (!VARIANTS.has(variant)) return; + if (variant !== VARIANT) { + delete asset.variants[variant]; + return; + } - const src = path.join(SOURCE, asset.name, variant, filename); - const dst = path.join(TARGET, asset.name, variant, filename.replace(/\.(gltf|glb)$/, '.{v}.glb')); + const src = path.join(SOURCE, asset.name, VARIANT, filename); + const dst = path.join(TARGET, filename.replace(/\.(gltf|glb)$/, '.{v}.glb')); try { + execSync(`cp ${src} ${dst.replace('{v}', '_')}`); execSync(`gltf-transform copy ${src} ${dst.replace('{v}', 'copy')}`); - execSync(`gltf-transform optimize ${src} ${dst.replace('{v}', 'opt')} --texture-format webp`); + execSync(`gltf-transform optimize ${src} ${dst.replace('{v}', 'optimized')} --texture-compress webp`); console.info(` - āœ… ${variant}/${filename}`); } catch (e) { console.error(` - ā›”ļø ${variant}/${filename}: ${e.message}`); @@ -31,4 +38,6 @@ INDEX.forEach((asset, assetIndex) => { console.log('\n'); }); +fs.writeFileSync(`${TARGET}/model-index.json`, JSON.stringify(INDEX)); + console.info('šŸ» Done.'); From bd11979327a4b04c58b7b7395b64567a7772b652 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 15 Feb 2023 13:35:03 -0500 Subject: [PATCH 10/14] Expose --simplify option. --- packages/cli/src/cli.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 5c0c0aad5..bba3f4b7a 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -161,6 +161,14 @@ commands or using the scripting API. default: 5, required: false }) + .option( + '--simplify ', + 'Simplification error limit, as a fraction of mesh radius.' + + 'Disable with --limit 0.', { + validator: program.NUMBER, + required: false, + default: SIMPLIFY_DEFAULTS.error, + }) .option( '--compress ', 'Floating point compression method. Draco compresses geometry; Meshopt ' + @@ -185,6 +193,7 @@ commands or using the scripting API. .action(async ({args, options, logger}) => { const opts = options as { instance: number, + simplify: number, compress: 'draco' | 'meshopt' | 'quantize' | false, textureCompress: 'ktx2' | 'webp' | 'webp' | 'auto' | false, textureSize: number, @@ -196,12 +205,23 @@ commands or using the scripting API. instance({min: options.instance as number}), flatten(), join(), - weld({ tolerance: 0.0001 }), - simplify({ simplifier: MeshoptSimplifier, ratio: 0.001, error: 0.0001 }), + ]; + + // Simplification and welding. + if (opts.simplify > 0) { + transforms.push( + weld({ tolerance: opts.simplify }), + simplify({ simplifier: MeshoptSimplifier, ratio: 0.001, error: opts.simplify }), + ); + } else { + transforms.push(weld({ tolerance: 0.0001 })); + } + + transforms.push( resample(), prune({ keepAttributes: false, keepLeaves: false }), sparse(), - ]; + ); // Texture compression. if (opts.textureCompress === 'ktx2') { From 760880382a6e8f4f4294262d5fe47086aac0da4c Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 15 Feb 2023 13:57:18 -0500 Subject: [PATCH 11/14] Add labels to RT test --- test/index.html | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/test/index.html b/test/index.html index 1ca6c7a6c..3af78d868 100644 --- a/test/index.html +++ b/test/index.html @@ -17,16 +17,27 @@ .container { width: 630px; margin: 30px auto; - display: grid; - grid-template-columns: 300px 300px; - grid-auto-rows: 300px; - grid-gap: 30px; } model-viewer { width: 300px; height: 300px; border: 1px dashed #ccc; } + figure { + position: relative; + display: grid; + grid-template-columns: 300px 300px; + grid-auto-rows: 300px; + grid-gap: 30px; + width: 630px; + } + figcaption { + position: absolute; + top: calc(50% - 0.5em); + left: calc(100% + 30px); + font-style: italic; + font-family: monospace; + }

Round trip tests

@@ -44,16 +55,23 @@

Round trip tests

.then((index) => { index.forEach((asset) => { Object.entries(asset.variants).forEach(([variant, filename]) => { + const rowEl = document.createElement('figure'); + containerEl.appendChild(rowEl); + const el = document.createElement('model-viewer'); el.setAttribute('src', `./out/${suffix(filename, '_')}`); el.setAttribute('autoplay', ''); el.alt = el.title = `${asset.name} / ${variant} / ${filename}`; - containerEl.appendChild(el.cloneNode()); + rowEl.appendChild(el.cloneNode()); el.setAttribute('src', `./out/${suffix(filename, 'copy')}`); el.alt = el.title = `${asset.name} / ${variant} / ${filename}`; - containerEl.appendChild(el); + rowEl.appendChild(el); + + const labelEl = document.createElement('figcaption'); + labelEl.textContent = asset.name; + rowEl.appendChild(labelEl); }); }); }); From a425146b354b99d944043350959828b87373ddb7 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 15 Feb 2023 13:57:24 -0500 Subject: [PATCH 12/14] Clean up --- packages/cli/src/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index bba3f4b7a..54ffa3b88 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -164,7 +164,7 @@ commands or using the scripting API. .option( '--simplify ', 'Simplification error limit, as a fraction of mesh radius.' + - 'Disable with --limit 0.', { + 'Disable with --simplify 0.', { validator: program.NUMBER, required: false, default: SIMPLIFY_DEFAULTS.error, From 926c368c230aaf58892cd05128caa7ce424d57d8 Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 15 Feb 2023 14:14:45 -0500 Subject: [PATCH 13/14] Opt out skinned nodes from join() --- packages/functions/src/join.ts | 3 +++ test/index.html | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/functions/src/join.ts b/packages/functions/src/join.ts index 5f5475f40..9597110e4 100644 --- a/packages/functions/src/join.ts +++ b/packages/functions/src/join.ts @@ -130,6 +130,9 @@ function _joinLevel(document: Document, parent: Node | Scene, options: Required< // Skip nodes with instancing; unsupported. if (node.getExtension('EXT_mesh_gpu_instancing')) continue; + // Skip nodes with skinning; unsupported. + if (node.getSkin()) continue; + for (const prim of mesh.listPrimitives()) { // Skip prims with morph targets; unsupported. if (prim.listTargets().length > 0) continue; diff --git a/test/index.html b/test/index.html index 3af78d868..4555f05bb 100644 --- a/test/index.html +++ b/test/index.html @@ -65,7 +65,7 @@

Round trip tests

el.alt = el.title = `${asset.name} / ${variant} / ${filename}`; rowEl.appendChild(el.cloneNode()); - el.setAttribute('src', `./out/${suffix(filename, 'copy')}`); + el.setAttribute('src', `./out/${suffix(filename, 'optimized')}`); el.alt = el.title = `${asset.name} / ${variant} / ${filename}`; rowEl.appendChild(el); From 99a4b640d96046551f748e19cc98fcccee1f0aeb Mon Sep 17 00:00:00 2001 From: Don McCurdy Date: Wed, 15 Feb 2023 14:35:06 -0500 Subject: [PATCH 14/14] Add linebreak to cli output --- packages/cli/src/session.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/session.ts b/packages/cli/src/session.ts index 67c18021f..b8103e867 100644 --- a/packages/cli/src/session.ts +++ b/packages/cli/src/session.ts @@ -61,6 +61,7 @@ export class Session { // Simple renderer shows warnings and errors. Disable signal listeners so Ctrl+C works. await new Listr(tasks, { renderer: 'simple', registerSignalListeners: false }).run(); + console.log(''); logger.level = prevLevel; } else {