Skip to content

Commit

Permalink
Add 'optimize' command (CLI) (#819)
Browse files Browse the repository at this point in the history
* [cli] Add optimize command
* [functions] Add 'min' option to instance()
* [functions] Fix draco() default options.
* Add Listr2 for optimize command.
* Overhaul roundtrip tests
* Opt out skinned nodes from join()
  • Loading branch information
donmccurdy authored Feb 15, 2023
1 parent 59144a2 commit a925cc5
Show file tree
Hide file tree
Showing 17 changed files with 368 additions and 153 deletions.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
134 changes: 122 additions & 12 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 <input> to <output> 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.
Expand All @@ -145,6 +145,116 @@ certain aspects of data layout may change slightly with this process:
.argument('<output>', OUTPUT_DESC)
.action(({args, logger}) => Session.create(io, logger, args.input, args.output).transform());

// OPTIMIZE
program
.command('optimize', '✨ Optimize model by all available methods')
.help(`
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>', INPUT_DESC)
.argument('<output>', OUTPUT_DESC)
.option('--instance <min>', 'Create GPU instances from shared mesh references', {
validator: program.NUMBER,
default: 5,
required: false
})
.option(
'--simplify <error>',
'Simplification error limit, as a fraction of mesh radius.' +
'Disable with --simplify 0.', {
validator: program.NUMBER,
required: false,
default: SIMPLIFY_DEFAULTS.error,
})
.option(
'--compress <method>',
'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-compress <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 <size>', 'Maximum texture dimensions, in pixels.', {
validator: program.NUMBER,
default: 2048,
required: false
})
.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,
};

// Baseline transforms.
const transforms = [
dedup(),
instance({min: options.instance as number}),
flatten(),
join(),
];

// 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') {
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.textureCompress !== false) {
transforms.push(
textureCompress({
encoder: sharp,
targetFormat: opts.textureCompress === 'auto' ? undefined : opts.textureCompress,
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);
});

// MERGE
program
.command('merge', 'Merge two or more models into one')
Expand Down Expand Up @@ -268,7 +378,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
Expand Down Expand Up @@ -373,7 +483,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
Expand All @@ -391,7 +501,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
Expand Down Expand Up @@ -733,7 +843,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
Expand Down Expand Up @@ -1075,7 +1185,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>', INPUT_DESC)
.argument('<output>', OUTPUT_DESC)
Expand Down Expand Up @@ -1311,7 +1421,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
Expand Down
47 changes: 41 additions & 6 deletions packages/cli/src/session.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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<void> {
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);
Expand All @@ -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 = 'warn';

// 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 {
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) {
Expand Down
16 changes: 8 additions & 8 deletions packages/cli/src/transforms/toktx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ 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';

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();
Expand Down Expand Up @@ -112,7 +112,7 @@ export const toktx = function (options: ETC1SOptions | UASTCOptions): Transform
...options,
};

return async (doc: Document): Promise<void> => {
return createTransform(options.mode, async (doc: Document): Promise<void> => {
const logger = doc.getLogger();

// Confirm recent version of KTX-Software is installed.
Expand Down Expand Up @@ -221,7 +221,7 @@ export const toktx = function (options: ETC1SOptions | UASTCOptions): Transform
if (!usesKTX2) {
basisuExtension.dispose();
}
};
});
};

/**********************************************************************************************
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -357,15 +357,15 @@ async function checkKTXSoftware(logger: ILogger): Promise<string> {
.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 {
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,7 @@ export function formatXMP(value: string | number | boolean | Record<string, unkn
export function underline(str: string): string {
return `\x1b[4m${str}\x1b[0m`;
}

export function dim(str: string): string {
return `\x1b[2m${str}\x1b[0m`;
}
Loading

0 comments on commit a925cc5

Please sign in to comment.