TkAstral3D/packages/editor/src/plugin/glTFHandler/glTFHandler.ts

249 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @author ErSan
* @email mlt131220@163.com
* @date 2024/9/16 23:19
* @description glTF处理器插件
*/
import {h, ref} from "vue";
import type {ModalReactive} from "naive-ui";
import {t} from "@/language";
import type {Plugin} from "@astral3d/engine";
import GLTFHandlerComponent from "@/components/es/plugin/builtin/GLTFHandler.vue";
import { MeshoptEncoder, MeshoptSimplifier } from 'meshoptimizer';
// @ts-ignore
import { ready as resampleReady, resample as resampleWASM } from 'keyframe-resample';
import { Logger,WebIO, Transform } from '@gltf-transform/core';
import {
dedup,
instance,
prune,
quantize,
resample,
weld,
meshopt,
draco,
simplify,
textureCompress,
flatten,
join,
sparse,
palette,
} from '@gltf-transform/functions';
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
import {Session} from "./session";
import {loadScript} from "@/utils/common/utils";
import { optimizePNG } from "@/plugin/glTFHandler/optimizePng";
//使用'micromatch',因为'contains: true'没有像预期的那样在minimatch中工作。需要确保'*'匹配的模式,如'image/png'。
export const MICROMATCH_OPTIONS = { nocase: true, contains: true };
export default class GLTFHandler implements Plugin{
icon= "";
name = "glTF处理器";
version = 1;
logger = new Logger(Logger.Verbosity.INFO);
io = new WebIO({credentials: 'include'}).registerExtensions(ALL_EXTENSIONS);
modalInstance:ModalReactive | undefined = undefined;
GLTFHandlerComponentRef = ref();
dracoScript = new Proxy({
encoder:false,
decoder:false,
failMsg:""
},{
set:(target: {decoder: boolean;encoder: boolean;failMsg: string;}, p: string | symbol, newValue: any): boolean => {
target[p] = newValue;
if(p === "failMsg"){
window.$message?.error(newValue)
return true;
}
if(target.encoder && target.decoder){
this.registerDependencies()
}
return true;
}
})
async install() {
// console.log(`%c glTF处理器 %c 版本1.0.0`, 'background: #35495e; padding: 4px; border-radius: 3px 0 0 3px; color: #fff',
// 'background: #41b883; padding: 4px; border-radius: 0 3px 3px 0; color: #fff');
}
async run() {
// 运行时再加载draco相关js
if(!this.dracoScript.encoder){
loadScript("/libs/draco/draco_encoder.js",false).then(() => {
this.dracoScript.encoder = true;
}).catch(() => {
this.dracoScript.failMsg = t("plugin.gltfHandler['Draco encoder load fail,Refresh the page and try again.']");
})
}
if(!this.dracoScript.decoder) {
loadScript("/libs/draco/draco_decoder.js", false).then(() => {
this.dracoScript.decoder = true;
}).catch(() => {
this.dracoScript.failMsg = t("plugin.gltfHandler['Draco decoder load fail,Refresh the page and try again.']");
})
}
this.GLTFHandlerComponentRef = ref();
this.modalInstance = window.$modal.create({
title: this.name,
preset:"card",
maskClosable:false,
style: {
width: '90%',
maxWidth: '800px'
},
onAfterLeave: () => this.finish(),
content: () => {
return h(GLTFHandlerComponent,{
onOptimize: this.optimize.bind(this),
onFinish: () => this.finish(),
ref: this.GLTFHandlerComponentRef
},"")
},
})
}
// 关闭插件
finish(){
this.modalInstance && this.modalInstance.destroy();
this.GLTFHandlerComponentRef = ref();
}
uninstall(): void {}
setLogger(log:string){
if(!this.GLTFHandlerComponentRef.value) return;
this.GLTFHandlerComponentRef.value.addLog(log);
}
async registerDependencies(){
this.io.registerDependencies({
// @ts-ignore
'draco3d.encoder': await new DracoEncoderModule(),
// @ts-ignore
'draco3d.decoder': await new DracoDecoderModule(),
})
}
/* 下面是实现的自定义的处理器方法 */
async optimize(opts:IPlugin.GLTFHandlerOptimizeModel,inputFile:File,outputFileName = ""){
this.setLogger(`Optimize ${inputFile.name}`);
if(this.dracoScript.failMsg){
window.$message?.error(this.dracoScript.failMsg);
return;
}
if(!this.dracoScript.encoder || !this.dracoScript.decoder){
setTimeout(()=>this.optimize(opts,inputFile,outputFileName),1000)
return;
}
/* 文件准备就绪,开始优化 */
const transforms: Transform[] = [
optimizePNG(),
dedup()
];
if (opts.instance) transforms.push(instance({ min: opts.instanceMin }));
if (opts.palette) {
transforms.push(
palette({
min: opts.paletteMin,
keepAttributes: !opts.prune || !opts.pruneAttributes,
}),
);
}
if (opts.flatten) transforms.push(flatten());
if (opts.join) transforms.push(join());
if (opts.weld) transforms.push(weld());
if (opts.simplify) {
transforms.push(
simplify({
simplifier: MeshoptSimplifier,
// simplifyError 用%显示时扩大了100倍需要高精度计算减小100倍
error: opts.simplifyError / 100,
ratio: opts.simplifyRatio,
lockBorder: opts.simplifyLockBorder,
}),
);
}
// 重新采样动画通道,无损地删除重复的关键帧以减小文件大小。重复的关键帧通常出现在由创作软件“烘焙”的动画中,以应用 IK 约束或其他软件特定功能。
transforms.push(resample({ ready: resampleReady, resample: resampleWASM }));
if (opts.prune) {
transforms.push(
prune({
keepAttributes: !opts.pruneAttributes,
keepIndices: false,
keepLeaves: !opts.pruneLeaves,
keepSolidTextures: !opts.pruneSolidTextures,
}),
);
}
transforms.push(sparse());
if (opts.textureCompress !== 'none') {
transforms.push(
textureCompress({
resize: [opts.textureSize, opts.textureSize],
targetFormat: opts.textureCompress === 'auto' ? undefined : opts.textureCompress,
// limitInputPixels: options.limitInputPixels as boolean,
limitInputPixels: false,
}),
);
}
// 最后进行网格压缩
if (opts.compress === 'draco') {
transforms.push(draco());
} else if (opts.compress === 'meshopt') {
transforms.push(meshopt({ encoder: MeshoptEncoder, level: opts.meshoptLevel }));
} else if (opts.compress === 'quantize') {
transforms.push(quantize());
}
// 设置输出文件名
if(!outputFileName){
const format = inputFile.name.split(".").pop();
outputFileName = inputFile.name.replace(`.${format}`,`_astral3d.optimize.${format}`)
}
// 生成临时 URL
const inputFileUrl = URL.createObjectURL(inputFile);
let outputFile:File | undefined = undefined
try {
outputFile = await Session.create(this,inputFileUrl, inputFile.name, outputFileName)
.setDisplay(true)
.transform(...transforms);
this.setLogger(`Optimize ${inputFile.name} success!`);
}catch (e:unknown){
if (e instanceof Error) {
window.$message?.error(e.message);
this.setLogger(`Optimize ${inputFile.name} error: ${e.message}`);
} else {
window.$message?.error(e as string);
this.setLogger(`Optimize ${inputFile.name} error: ${e}`);
}
}
return outputFile;
}
}