Skip to content

Instantly share code, notes, and snippets.

@michaeldll
Last active March 11, 2025 13:41
Show Gist options
  • Save michaeldll/ea976e723967df998ca346c6ff9e053b to your computer and use it in GitHub Desktop.
Save michaeldll/ea976e723967df998ca346c6ff9e053b to your computer and use it in GitHub Desktop.
Compress KTX2 with toktx using Node.js
import { exec } from 'child_process';
import fs from 'fs';
import path from 'path';
// This script compresses a single PNG or JPG texture to KTX2 using the Khronos toktx tool.
// Uses low quality but highly compressed ETC1S compression by default.
// Feel free to extend it!
// REQUIREMENTS:
// Install toktx 4.3.1 from https://github.com/KhronosGroup/KTX-Software/releases/tag/v4.3.1
// USAGE:
// node compress-textures.js [options] input.jpg
// OPTIONS:
// --high - Use high quality "lossless" UASTC compression
// --medium - Use regular "lossless" UASTC compression
// --low - Use lossy ETC1S compression
// --flip - Flip the texture vertically
// --noMipmaps - Do not generate mipmaps
// --linear - Use linear color space, e.g. for normal maps and other non-color textures such as ORM maps
// EXAMPLE USAGE FOR TEXTURES:
// Albedo: --low (=etc1s)
// Emissive: --low (=etc1s)
// Normal: --high --linear
// ORM: --high --linear OR --medium --linear
// Transmission: --medium --linear
// Clearcoat: --medium --linear
// Additional information: https://github.com/KhronosGroup/3D-Formats-Guidelines/blob/main/KTXArtistGuide.md
const args = process.argv.slice(2);
// Find the input file as the last argument
let input = args[args.length - 1];
// Check if input file exists
if (!fs.existsSync(input)) {
throw new Error('Input file does not exist');
}
// Find compression, use ETC1S by default
let compression = 'etc1s';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--medium') {
compression = 'uastc';
} else if (args[i] === '--high') {
compression = 'uastcHighQuality';
}
}
// Check if --flip flag is present
let flip = '';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--flip') {
flip = '--lower_left_maps_to_s0t0';
}
}
// Build mipmaps by default unless --noMipmaps flag is present
let mipmaps = '--genmipmap';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--noMipmaps') {
mipmaps = '';
}
}
// Linear color space, e.g. for normal maps
let linear = '';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--linear') {
linear = '--assign_oetf linear --assign_primaries none';
}
}
// Set compression command
let command = '';
if (compression === 'etc1s') {
command = '--t2 --encode etc1s --clevel 4 --qlevel 255';
} else if (compression === 'uastc') {
command = '--t2 --encode uastc --uastc_quality 4 --uastc_rdo_l .5 --uastc_rdo_d 65536 --zcmp 22';
} else if (compression === 'uastcHighQuality') {
command = '--t2 --encode uastc --uastc_quality 4 --uastc_rdo_l .2 --uastc_rdo_d 65536 --zcmp 22';
}
// Add new target directory
const parent = path.dirname(input);
const targetDirectory = parent + '/ktx2';
if (!fs.existsSync(targetDirectory)) {
fs.mkdirSync(targetDirectory);
}
// Set output extensions and path to new targetDirectory
const output = targetDirectory + '/' + path.basename(input.replace(/\.[^/.]+$/, '.ktx2'));
exec(`toktx ${command} ${flip} ${mipmaps} ${output} ${input}`, (error, stdout, stderr) => {
if (error) {
console.log(`error when building KTX2: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr when building KTX2: ${stderr}`);
return;
}
console.log('Successfully built KTX2 texture');
});
@arpu
Copy link

arpu commented Jan 28, 2025

should we not use ktx create ?

@michaeldll
Copy link
Author

You can use the same logic with either basisu or ktx create, yes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment