优化第一阶段

This commit is contained in:
yinsx
2025-12-22 12:07:12 +08:00
parent dd99e932b4
commit 1df41ac4ab
38 changed files with 340 additions and 300 deletions

View File

@ -0,0 +1,115 @@
import fs from "fs";
import path from "path";
import { spawn } from "child_process";
import color from "picocolors";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const toktx = path.join(__dirname, "..", "..", "..", "bin", "texture_tool.exe");
// 检查 toktx 是否存在
export function checkToktx() {
if (!fs.existsSync(toktx)) {
console.error("❌ 找不到 texture_tool.exe");
process.exit(1);
}
}
// 扫描图片文件
export function scanImages(exts) {
console.log("🔍 扫描目标文件中...");
const cwd = process.cwd();
const images = fs.readdirSync(cwd).filter(f =>
exts.some(ext => f.toLowerCase().endsWith("." + ext))
);
return { images, cwd };
}
// 构建压缩参数
export function buildArgs(input, output, config) {
const args = ["--t2"];
if (config.encoding === "uastc") {
args.push("--encode", "uastc");
const zcmpLevel = { none: "0", standard: "10", high: "18", extreme: "22" };
args.push("--zcmp", zcmpLevel[config.quality] || "10");
} else if (config.encoding === "etc1s") {
args.push("--encode", "etc1s");
} else if (config.encoding === "astc") {
args.push("--encode", "astc");
const blkSize = { none: "8x8", standard: "6x6", high: "5x5", extreme: "4x4" };
args.push("--astc_blk_d", blkSize[config.quality] || "6x6");
}
if (config.mipmap === "auto") {
args.push("--genmipmap");
}
args.push(output, input);
return args;
}
// 压缩单个文件
export function compressFile(img, config, cwd, progress) {
const baseName = img.replace(/\.[^.]+$/, "");
const out = baseName + ".ktx2";
// 点动画
let dots = 0;
const dotAnim = setInterval(() => {
const dotStr = ".".repeat(dots);
process.stdout.write(`\r${progress} ${img} 正在转换中${dotStr} `);
dots = dots >= 3 ? 0 : dots + 1;
}, 300);
process.stdout.write(`${progress} ${img} 正在转换中.`);
const args = buildArgs(img, out, config);
return new Promise((resolve) => {
const proc = spawn(toktx, args, { cwd });
let stderr = "";
proc.stderr?.on("data", data => {
stderr += data.toString();
});
proc.on("close", code => {
clearInterval(dotAnim);
if (code === 0) {
console.log(`\r${progress} ${color.green("✓")} ${out} `);
resolve({ success: true });
} else {
console.log(`\r${progress} ${color.red("✗")} ${img} 失败 `);
if (stderr) {
console.log(color.dim(` 错误: ${stderr.trim()}`));
}
resolve({ success: false, error: stderr });
}
});
proc.on("error", err => {
clearInterval(dotAnim);
console.log(`\r${progress} ${color.red("✗")} ${img} 失败 `);
console.log(color.dim(` 错误: ${err.message}`));
resolve({ success: false, error: err.message });
});
});
}
// 批量压缩
export async function compressAll(images, config, cwd) {
const total = images.length;
let finished = 0;
let failed = 0;
for (const img of images) {
finished++;
const progress = `(${finished}/${total})`;
const result = await compressFile(img, config, cwd, progress);
if (!result.success) failed++;
}
return { total, failed };
}

View File

@ -0,0 +1,63 @@
// 步骤配置
export const steps = [
{
name: "文件格式",
type: "multiselect",
message: "请选择要压缩的图片类型",
options: [
{ value: "png", label: "PNG (.png)(无损格式,适合图标和透明图)" },
{ value: "jpg", label: "JPG (.jpg)(有损格式,适合照片和复杂图像)" },
{ value: "jpeg", label: "JPEG (.jpeg)同JPG仅扩展名不同" },
{ value: "webp", label: "WebP (.webp)(新一代格式,体积更小)" },
{ value: "tga", label: "TGA (.tga)(游戏纹理常用格式)" }
],
default: ["png", "jpg"]
},
{
name: "压缩程度",
type: "select",
message: "请选择压缩级别",
options: [
{ value: "none", label: "无压缩(原始质量)", hint: "保持原始文件大小,图片和内容无损" },
{ value: "standard", label: "标准压缩(推荐)", hint: "平衡文件大小与质量压缩率约40%" },
{ value: "high", label: "高度压缩(最小体积)", hint: "最大程度减小文件体积,可能轻微影响清晰度" },
{ value: "extreme", label: "极限压缩(极致压缩)", hint: "牺牲部分质量换取最小体积,适合网络传输" }
],
default: "standard"
},
{
name: "编码格式",
type: "select",
message: "请选择编码格式",
options: [
{ value: "uastc", label: "UASTC通用超压缩纹理", hint: "高质量GPU纹理解码快适合实时渲染" },
{ value: "etc1s", label: "ETC1S基础压缩纹理", hint: "文件体积最小,兼容性好,适合移动端" },
{ value: "astc", label: "ASTC自适应纹理压缩", hint: "灵活块大小,质量与体积可调,适合高端设备" }
],
default: "uastc"
},
{
name: "Mipmap",
type: "select",
message: "请选择Mipmap生成方式",
options: [
{ value: "auto", label: "自动生成(推荐)", hint: "根据图片尺寸自动生成多级纹理,优化远距离渲染" },
{ value: "none", label: "不生成Mipmap", hint: "仅保留原始尺寸,文件更小但可能出现锯齿" },
{ value: "custom", label: "自定义层级", hint: "手动指定Mipmap层数精细控制纹理细节" }
],
default: "auto"
},
{
name: "输出选项",
type: "multiselect",
message: "请选择输出选项",
options: [
{ value: "overwrite", label: "覆盖已存在文件(自动替换同名文件)" },
{ value: "keepOriginal", label: "保留原文件(压缩后不删除源文件)" },
{ value: "report", label: "生成压缩报告(输出详细的压缩统计信息)" },
{ value: "silent", label: "静默模式(减少控制台输出信息)" },
{ value: "gltfExtension", label: "修改glTF扩展添加KHR_texture_basisu", dynamic: true }
],
default: ["overwrite", "keepOriginal"]
}
];

3
lib/plugins/ktx2/gltf.js Normal file
View File

@ -0,0 +1,3 @@
// 从 utils 导出,保持兼容
export { hasGltfFile, checkRequiredFiles, modifyGltfContent } from "../../utils/gltf.js";
export { runGltfExtension } from "../gltf/index.js";

56
lib/plugins/ktx2/index.js Normal file
View File

@ -0,0 +1,56 @@
import color from "picocolors";
import { checkToktx, scanImages, compressAll } from "./compressor.js";
import { runInteractive, showSummary } from "./ui.js";
import { runGltfExtension } from "./gltf.js";
import { stopKeypress, waitForKey } from "../../keyboard.js";
async function run() {
checkToktx();
const result = await runInteractive();
if (!result) return "back";
stopKeypress();
const { results } = result;
const [exts, quality, encoding, mipmap, outputOpts] = results;
const config = { exts, quality, encoding, mipmap, outputOpts };
showSummary([
"文件格式: " + config.exts.join(", "),
"压缩程度: " + config.quality,
"编码格式: " + config.encoding,
"Mipmap: " + config.mipmap,
"输出选项: " + config.outputOpts.join(", ")
]);
const { images, cwd } = scanImages(exts);
if (images.length === 0) {
console.log(color.yellow("当前目录没有匹配的图片"));
await waitForKey();
return "back";
}
console.log("📁 找到 " + color.cyan(images.length) + " 个待转换文件\n");
const { total, failed } = await compressAll(images, config, cwd);
if (failed > 0) {
console.log(color.yellow("\n⚠ 完成,但有 " + failed + " 个文件失败"));
} else {
console.log(color.green("\n🎉 全部文件压缩完成!"));
}
if (outputOpts.includes("gltfExtension")) {
console.log(color.cyan("\n正在修改 glTF 文件..."));
const { success, count } = runGltfExtension();
if (success) console.log(color.green("✓ 已修改 " + count + " 个 glTF 文件"));
}
await waitForKey();
return "back";
}
export default {
id: "ktx2",
name: "KTX2压缩",
desc: "纹理压缩为KTX2",
run,
};

20
lib/plugins/ktx2/ui.js Normal file
View File

@ -0,0 +1,20 @@
import { createStepUI } from "../../utils/stepui.js";
import { steps } from "./config.js";
import { hasGltfFile } from "./gltf.js";
function getFilteredSteps() {
const hasGltf = hasGltfFile();
return steps.map(step => {
if (step.name === "输出选项") {
return { ...step, options: step.options.filter(opt => !opt.dynamic || hasGltf) };
}
return step;
});
}
const ui = createStepUI({
title: "KTX2 纹理压缩工具",
getSteps: getFilteredSteps
});
export const { runInteractive, showSummary } = ui;