增加转格式
This commit is contained in:
71
lib/model/config.js
Normal file
71
lib/model/config.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { listAllModelFiles } from "../utils/gltf.js";
|
||||
|
||||
const transformOptions = [
|
||||
{ value: "dedup", label: "dedup(去重)", hint: "删除重复的访问器、材质、网格" },
|
||||
{ value: "prune", label: "prune(清理无用节点)", hint: "移除未被引用的节点、材质、动画" },
|
||||
{ value: "resample", label: "resample(动画重采样)", hint: "统一动画关键帧间隔,减少多余数据" },
|
||||
{ value: "weld", label: "weld(合并顶点)", hint: "合并共享位置的顶点以减少面数" },
|
||||
{ value: "quantize", label: "quantize(量化顶点数据)", hint: "降低顶点属性精度减小模型体积" }
|
||||
];
|
||||
|
||||
const quantizePresets = [
|
||||
{ value: "high", label: "高质量(位置16位,法线12位,UV14位)", hint: "尽量保证细节,适合高保真场景" },
|
||||
{ value: "balanced", label: "均衡(位置14位,法线10位,UV12位)", hint: "默认推荐,兼顾体积与质量" },
|
||||
{ value: "aggressive", label: "极限压缩(位置12位,法线8位,UV10位)", hint: "最小体积,但可能损失细节" },
|
||||
{ value: "light", label: "轻量(位置10位,法线8位,UV10位)", hint: "适合移动端、卡通等对精度不敏感场景" }
|
||||
];
|
||||
|
||||
const outputFormats = [
|
||||
{ value: "auto", label: "保持原格式", hint: "glTF/GLB保持原格式,OBJ/FBX转为GLB" },
|
||||
{ value: "glb", label: "统一导出为 GLB(二进制)", hint: "单文件发布更方便" },
|
||||
{ value: "gltf", label: "统一导出为 glTF(JSON)", hint: "可读性好,调试方便" }
|
||||
];
|
||||
|
||||
const outputOptions = [
|
||||
{ value: "overwrite", label: "覆盖原文件", hint: "直接把结果写回源文件" },
|
||||
{ value: "backup", label: "保留备份 (_备份)", hint: "覆盖前在同目录生成 _备份 副本" },
|
||||
{ value: "copy", label: "输出副本 (_compressed)", hint: "保留原文件不动,结果写入新文件" }
|
||||
];
|
||||
|
||||
export function getSteps() {
|
||||
const files = listAllModelFiles();
|
||||
const fileStep = {
|
||||
name: "模型选择",
|
||||
type: "multiselect",
|
||||
message: files.length ? "选择要处理的模型(支持 glTF/GLB/OBJ/FBX)" : "当前目录未找到模型文件",
|
||||
options: files.map(file => ({ value: file, label: file })),
|
||||
default: [...files]
|
||||
};
|
||||
|
||||
return [
|
||||
fileStep,
|
||||
{
|
||||
name: "压缩命令",
|
||||
type: "multiselect",
|
||||
message: "请选择要执行的 gltf-transform 操作",
|
||||
options: transformOptions,
|
||||
default: ["dedup", "prune", "weld", "quantize"]
|
||||
},
|
||||
{
|
||||
name: "量化级别",
|
||||
type: "select",
|
||||
message: "量化可显著减小体积(如未启用 quantize 可直接 Tab)",
|
||||
options: quantizePresets,
|
||||
default: "balanced"
|
||||
},
|
||||
{
|
||||
name: "输出格式",
|
||||
type: "select",
|
||||
message: "选择最终模型格式",
|
||||
options: outputFormats,
|
||||
default: "auto"
|
||||
},
|
||||
{
|
||||
name: "输出选项",
|
||||
type: "multiselect",
|
||||
message: "请选择输出行为",
|
||||
options: outputOptions,
|
||||
default: ["overwrite", "backup"]
|
||||
}
|
||||
];
|
||||
}
|
||||
195
lib/model/index.js
Normal file
195
lib/model/index.js
Normal file
@ -0,0 +1,195 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import color from "picocolors";
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { dedup, prune, resample, weld, quantize } from "@gltf-transform/functions";
|
||||
import { BACKUP_SUFFIX, needsConversion } from "../utils/gltf.js";
|
||||
import { convert } from "../convert/converters.js";
|
||||
import { runInteractive, showSummary } from "./ui.js";
|
||||
import { stopKeypress, waitForKey } from "../keyboard.js";
|
||||
|
||||
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||
|
||||
const QUANTIZE_PRESETS = {
|
||||
high: { position: 16, normal: 12, texcoord: 14, color: 10, generic: 12 },
|
||||
balanced: { position: 14, normal: 10, texcoord: 12, color: 8, generic: 12 },
|
||||
aggressive: { position: 12, normal: 8, texcoord: 10, color: 8, generic: 10 },
|
||||
light: { position: 10, normal: 8, texcoord: 10, color: 8, generic: 10 }
|
||||
};
|
||||
|
||||
function buildTransforms(config) {
|
||||
const transforms = [];
|
||||
const selected = new Set(config.commands);
|
||||
if (selected.has("dedup")) transforms.push(dedup());
|
||||
if (selected.has("prune")) transforms.push(prune());
|
||||
if (selected.has("resample")) transforms.push(resample());
|
||||
if (selected.has("weld")) transforms.push(weld());
|
||||
if (selected.has("quantize")) {
|
||||
const preset = QUANTIZE_PRESETS[config.quantizePreset] || QUANTIZE_PRESETS.balanced;
|
||||
transforms.push(quantize({
|
||||
quantizePosition: preset.position,
|
||||
quantizeNormal: preset.normal,
|
||||
quantizeTexcoord: preset.texcoord,
|
||||
quantizeColor: preset.color,
|
||||
quantizeGeneric: preset.generic
|
||||
}));
|
||||
}
|
||||
return transforms;
|
||||
}
|
||||
|
||||
function resolveOutput(file, config) {
|
||||
const cwd = process.cwd();
|
||||
const originalExt = path.extname(file).toLowerCase() || ".gltf";
|
||||
const baseName = originalExt ? file.slice(0, -originalExt.length) : file;
|
||||
const isConvertible = needsConversion(file);
|
||||
|
||||
// OBJ/FBX 默认输出 GLB,除非指定了 gltf
|
||||
const targetExt = config.outputFormat === "gltf" ? ".gltf"
|
||||
: (config.outputFormat === "glb" || isConvertible) ? ".glb"
|
||||
: originalExt;
|
||||
|
||||
const wantsCopy = config.outputOptions.includes("copy");
|
||||
const wantsOverwrite = config.outputOptions.includes("overwrite");
|
||||
const mode = wantsCopy ? "copy" : (wantsOverwrite ? "overwrite" : "copy");
|
||||
const backup = config.outputOptions.includes("backup") && mode === "overwrite";
|
||||
|
||||
const targetName = mode === "copy"
|
||||
? `${baseName}_compressed${targetExt}`
|
||||
: `${baseName}${targetExt}`;
|
||||
|
||||
return {
|
||||
cwd,
|
||||
mode,
|
||||
backup,
|
||||
sourcePath: path.join(cwd, file),
|
||||
sourceExt: originalExt,
|
||||
targetExt,
|
||||
targetPath: path.join(cwd, targetName),
|
||||
baseName,
|
||||
needsConversion: isConvertible
|
||||
};
|
||||
}
|
||||
|
||||
async function processFile(file, config, transforms) {
|
||||
const output = resolveOutput(file, config);
|
||||
if (!fs.existsSync(output.sourcePath)) {
|
||||
console.log(color.yellow("跳过,文件不存在: " + file));
|
||||
return { ok: false, file, reason: "missing" };
|
||||
}
|
||||
|
||||
if (!transforms.length) {
|
||||
console.log(color.yellow("未选择任何 gltf-transform 操作,已中止"));
|
||||
return { ok: false, file, reason: "no-transform" };
|
||||
}
|
||||
|
||||
if (output.backup) {
|
||||
const backupName = `${output.baseName}${BACKUP_SUFFIX}${output.sourceExt}`;
|
||||
const backupPath = path.join(output.cwd, backupName);
|
||||
fs.copyFileSync(output.sourcePath, backupPath);
|
||||
}
|
||||
|
||||
let inputPath = output.sourcePath;
|
||||
|
||||
// 如果是 OBJ/FBX,先转换为临时 GLB
|
||||
if (output.needsConversion) {
|
||||
const tempPath = path.join(output.cwd, `${output.baseName}_temp.glb`);
|
||||
console.log(color.dim("转换中: " + file + " → GLB"));
|
||||
await convert(output.sourcePath, tempPath, { binary: true });
|
||||
inputPath = tempPath;
|
||||
}
|
||||
|
||||
const document = io.read(inputPath);
|
||||
await document.transform(...transforms);
|
||||
io.write(output.targetPath, document);
|
||||
|
||||
// 清理临时文件
|
||||
if (output.needsConversion) {
|
||||
fs.rmSync(inputPath, { force: true });
|
||||
}
|
||||
|
||||
if (output.mode === "overwrite" && output.targetPath !== output.sourcePath) {
|
||||
fs.rmSync(output.sourcePath, { force: true });
|
||||
}
|
||||
|
||||
if (output.mode === "overwrite" && output.targetPath !== output.sourcePath) {
|
||||
console.log(color.dim("已生成新文件: " + path.basename(output.targetPath)));
|
||||
}
|
||||
|
||||
return { ok: true, file, output: output.targetPath };
|
||||
}
|
||||
|
||||
export async function run() {
|
||||
const result = await runInteractive();
|
||||
if (!result) return "back";
|
||||
|
||||
stopKeypress();
|
||||
|
||||
const { results } = result;
|
||||
const config = {
|
||||
files: results[0] || [],
|
||||
commands: results[1] || [],
|
||||
quantizePreset: results[2] || "balanced",
|
||||
outputFormat: results[3] || "auto",
|
||||
outputOptions: results[4] || []
|
||||
};
|
||||
|
||||
showSummary([
|
||||
"模型文件: " + (config.files.length ? config.files.join(", ") : "未选择"),
|
||||
"gltf-transform 命令: " + (config.commands.length ? config.commands.join(", ") : "无"),
|
||||
"量化级别: " + config.quantizePreset,
|
||||
"输出格式: " + config.outputFormat,
|
||||
"输出选项: " + (config.outputOptions.length ? config.outputOptions.join(", ") : "默认")
|
||||
]);
|
||||
|
||||
if (!config.files.length) {
|
||||
console.log(color.yellow("未选择任何模型文件"));
|
||||
await waitForKey();
|
||||
return "back";
|
||||
}
|
||||
|
||||
const transforms = buildTransforms(config);
|
||||
if (!transforms.length) {
|
||||
console.log(color.yellow("未配置任何压缩命令,请至少选择一项 gltf-transform 操作"));
|
||||
await waitForKey();
|
||||
return "back";
|
||||
}
|
||||
|
||||
const total = config.files.length;
|
||||
let success = 0;
|
||||
const failed = [];
|
||||
|
||||
console.log(color.cyan(`开始压缩 ${total} 个文件...\n`));
|
||||
|
||||
for (let i = 0; i < config.files.length; i++) {
|
||||
const file = config.files[i];
|
||||
const progress = `[${i + 1}/${total}]`;
|
||||
console.log(color.dim(`${progress} 处理中: ${file}`));
|
||||
|
||||
try {
|
||||
const result = await processFile(file, config, transforms);
|
||||
if (result.ok) {
|
||||
success++;
|
||||
console.log(color.green(`${progress} ✓ ${file} → ${path.basename(result.output)}`));
|
||||
} else {
|
||||
failed.push(file);
|
||||
console.log(color.yellow(`${progress} ⊘ 跳过: ${file}`));
|
||||
}
|
||||
} catch (err) {
|
||||
failed.push(file);
|
||||
console.log(color.red(`${progress} ✖ 失败: ${file}`));
|
||||
console.log(color.dim(" " + String(err?.message || err)));
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n" + color.bgGreen(color.black(" 压缩完成 ")));
|
||||
if (success) {
|
||||
console.log(color.green(`成功: ${success} 个`));
|
||||
}
|
||||
if (failed.length) {
|
||||
console.log(color.yellow(`失败: ${failed.length} 个 (${failed.join(", ")})`));
|
||||
}
|
||||
|
||||
await waitForKey();
|
||||
return "back";
|
||||
}
|
||||
9
lib/model/ui.js
Normal file
9
lib/model/ui.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { createStepUI } from "../utils/stepui.js";
|
||||
import { getSteps } from "./config.js";
|
||||
|
||||
const ui = createStepUI({
|
||||
title: "模型压缩工具",
|
||||
getSteps
|
||||
});
|
||||
|
||||
export const { runInteractive, showSummary } = ui;
|
||||
Reference in New Issue
Block a user