feat(Editor): glTFHandler 处理优化
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
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 };
|
||||
@@ -91,7 +92,6 @@ export default class GLTFHandler implements Plugin{
|
||||
}
|
||||
|
||||
this.GLTFHandlerComponentRef = ref();
|
||||
const finishFn = this.finish.bind(this);
|
||||
this.modalInstance = window.$modal.create({
|
||||
title: this.name,
|
||||
preset:"card",
|
||||
@@ -100,12 +100,12 @@ export default class GLTFHandler implements Plugin{
|
||||
width: '90%',
|
||||
maxWidth: '800px'
|
||||
},
|
||||
onAfterLeave: finishFn,
|
||||
onAfterLeave: () => this.finish(),
|
||||
content: () => {
|
||||
return h(GLTFHandlerComponent,{
|
||||
onOptimize:this.optimize.bind(this),
|
||||
onFinish: finishFn,
|
||||
ref:this.GLTFHandlerComponentRef
|
||||
onOptimize: this.optimize.bind(this),
|
||||
onFinish: () => this.finish(),
|
||||
ref: this.GLTFHandlerComponentRef
|
||||
},"")
|
||||
},
|
||||
})
|
||||
@@ -137,7 +137,6 @@ export default class GLTFHandler implements Plugin{
|
||||
|
||||
/* 下面是实现的自定义的处理器方法 */
|
||||
async optimize(opts:IPlugin.GLTFHandlerOptimizeModel,inputFile:File,outputFileName = ""){
|
||||
// console.log("调用优化处理器,",opts,inputFile)
|
||||
this.setLogger(`Optimize ${inputFile.name}`);
|
||||
|
||||
if(this.dracoScript.failMsg){
|
||||
@@ -151,7 +150,10 @@ export default class GLTFHandler implements Plugin{
|
||||
|
||||
/* 文件准备就绪,开始优化 */
|
||||
|
||||
const transforms: Transform[] = [dedup()];
|
||||
const transforms: Transform[] = [
|
||||
optimizePNG(),
|
||||
dedup()
|
||||
];
|
||||
|
||||
if (opts.instance) transforms.push(instance({ min: opts.instanceMin }));
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Transform } from '@gltf-transform/core';
|
||||
import { encodePNG } from './util';
|
||||
|
||||
function asUint8Array(data: unknown): Uint8Array {
|
||||
if (data instanceof Uint8Array) return data;
|
||||
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
||||
throw new Error('Unsupported texture image type');
|
||||
}
|
||||
|
||||
export const optimizePNG = (): Transform => async (doc) => {
|
||||
const textures = doc.getRoot().listTextures();
|
||||
for (const tex of textures) {
|
||||
// 仅处理 PNG
|
||||
if (tex.getMimeType() !== 'image/png') continue;
|
||||
|
||||
const image = tex.getImage();
|
||||
if (!image || image.byteLength === 0) continue;
|
||||
|
||||
// 归一化为 Uint8Array
|
||||
const imgU8 = asUint8Array(image);
|
||||
|
||||
const stamped = await encodePNG(imgU8);
|
||||
tex.setImage(stamped);
|
||||
tex.setMimeType('image/png');
|
||||
}
|
||||
};
|
||||
@@ -1,125 +1,136 @@
|
||||
import {Document, WebIO, FileUtils, Transform, Format, Logger} from '@gltf-transform/core';
|
||||
import { Document, WebIO, FileUtils, Transform, Format, Logger } from '@gltf-transform/core';
|
||||
import type { Packet, KHRXMP } from '@gltf-transform/extensions';
|
||||
import { unpartition } from '@gltf-transform/functions';
|
||||
import {Listr} from "./Listr";
|
||||
import { formatBytes, XMPContext } from './util.js';
|
||||
import { Listr } from "./Listr";
|
||||
import { formatBytes, encodeGLB, XMPContext } from './util.js';
|
||||
import GLTFHandler from "./glTFHandler";
|
||||
|
||||
export class Session {
|
||||
private _outputFormat: Format;
|
||||
private _display = false;
|
||||
private _outputFormat: Format;
|
||||
private _display = false;
|
||||
|
||||
constructor(
|
||||
private _io: WebIO,
|
||||
private _logger: Logger,
|
||||
private setLogger: (log:string) => void,
|
||||
private _input: string,
|
||||
private _inputName: string,
|
||||
private _output: string,
|
||||
) {
|
||||
_io.setLogger(_logger);
|
||||
this._outputFormat = FileUtils.extension(_output) === 'glb' ? Format.GLB : Format.GLTF;
|
||||
}
|
||||
constructor(
|
||||
private _io: WebIO,
|
||||
private _logger: Logger,
|
||||
private setLogger: (log: string) => void,
|
||||
private _input: string,
|
||||
private _inputName: string,
|
||||
private _output: string,
|
||||
) {
|
||||
_io.setLogger(_logger);
|
||||
this._outputFormat = FileUtils.extension(_output) === 'glb' ? Format.GLB : Format.GLTF;
|
||||
}
|
||||
|
||||
public static create(handler:GLTFHandler, inputFileUrl: string,inputName:string, output: string): Session {
|
||||
return new Session(handler.io, handler.logger, handler.setLogger.bind(handler),inputFileUrl, inputName,output);
|
||||
}
|
||||
public static create(handler: GLTFHandler, inputFileUrl: string, inputName: string, output: string): Session {
|
||||
return new Session(handler.io, handler.logger, handler.setLogger.bind(handler), inputFileUrl, inputName, output);
|
||||
}
|
||||
|
||||
public setDisplay(display: boolean): this {
|
||||
this._display = display;
|
||||
return this;
|
||||
}
|
||||
public setDisplay(display: boolean): this {
|
||||
this._display = display;
|
||||
return this;
|
||||
}
|
||||
|
||||
public async transform(...transforms: Transform[]): Promise<File> {
|
||||
this.setLogger("Start");
|
||||
public async transform(...transforms: Transform[]): Promise<File> {
|
||||
this.setLogger("Start");
|
||||
|
||||
let _document = this._input
|
||||
? (await this._io.read(this._input)).setLogger(this._logger)
|
||||
: new Document().setLogger(this._logger);
|
||||
let _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 = _document
|
||||
.getRoot()
|
||||
.listExtensionsUsed()
|
||||
.find((extension) => extension.extensionName === extensionName);
|
||||
if (extension) {
|
||||
extension.dispose();
|
||||
this._logger.warn(`Decoded ${extensionName}. Further compression will be lossy.`);
|
||||
}
|
||||
}
|
||||
// 警告和消除有损压缩,以避免增加往返的损失。
|
||||
for (const extensionName of ['KHR_draco_mesh_compression', 'EXT_meshopt_compression']) {
|
||||
const extension = _document
|
||||
.getRoot()
|
||||
.listExtensionsUsed()
|
||||
.find((extension) => extension.extensionName === extensionName);
|
||||
if (extension) {
|
||||
extension.dispose();
|
||||
this._logger.warn(`Decoded ${extensionName}. Further compression will be lossy.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._display) {
|
||||
const tasks = [] as { title:string,task:(task: any) => Promise<void> }[];
|
||||
for (const transform of transforms) {
|
||||
tasks.push({
|
||||
title: transform.name,
|
||||
task: async (task) => {
|
||||
try{
|
||||
this.setLogger(task.title)
|
||||
let time = performance.now();
|
||||
_document = await _document.transform(transform);
|
||||
time = Math.round(performance.now() - time);
|
||||
this.setLogger(task.title.padEnd(20) + ` ${time}ms`)
|
||||
}catch (error:unknown){
|
||||
// @ts-ignore
|
||||
this.setLogger(`${task.title} run fail: ${error?.message || error}`)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
if (this._display) {
|
||||
const tasks = [] as { title: string, task: (task: any) => Promise<void> }[];
|
||||
for (const transform of transforms) {
|
||||
tasks.push({
|
||||
title: transform.name,
|
||||
task: async (task) => {
|
||||
try {
|
||||
this.setLogger(task.title)
|
||||
let time = performance.now();
|
||||
_document = await _document.transform(transform);
|
||||
time = Math.round(performance.now() - time);
|
||||
this.setLogger(task.title.padEnd(20) + ` ${time}ms`)
|
||||
} catch (error: unknown) {
|
||||
// @ts-ignore
|
||||
this.setLogger(`${task.title} run fail: ${error?.message || error}`)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await new Listr(tasks).run();
|
||||
} else {
|
||||
await _document.transform(...transforms);
|
||||
}
|
||||
await new Listr(tasks).run();
|
||||
} else {
|
||||
await _document.transform(...transforms);
|
||||
}
|
||||
|
||||
await _document.transform(updateMetadata);
|
||||
await _document.transform(updateMetadata);
|
||||
|
||||
if (this._outputFormat === Format.GLB) {
|
||||
await _document.transform(unpartition());
|
||||
}
|
||||
if (this._outputFormat === Format.GLB) {
|
||||
await _document.transform(unpartition());
|
||||
}
|
||||
|
||||
const outputUint8Array = await this._io.writeBinary(_document);
|
||||
// Uint8Array转file
|
||||
const mimeType = this._outputFormat === Format.GLB ? "model/gltf-binary" : "model/gltf+json";
|
||||
const blob = new Blob([outputUint8Array], { type: mimeType});
|
||||
const outputFile = new File([blob], this._output, { type: mimeType });
|
||||
const rawU8 = await this._io.writeBinary(_document);
|
||||
|
||||
const { lastReadBytes, lastWriteBytes } = this._io;
|
||||
if (!this._input) {
|
||||
const output = FileUtils.basename(this._output) + '.' + FileUtils.extension(this._output);
|
||||
this._logger.info(`${output} (${formatBytes(lastWriteBytes)})`);
|
||||
} else {
|
||||
const input = FileUtils.basename(this._inputName) + '.' + FileUtils.extension(this._inputName);
|
||||
const output = FileUtils.basename(this._output) + '.' + FileUtils.extension(this._output);
|
||||
this._logger.info(
|
||||
`${input} (${formatBytes(lastReadBytes)})` + ` → ${output} (${formatBytes(lastWriteBytes)})`,
|
||||
);
|
||||
}
|
||||
// 插入 WASM 水印
|
||||
let outputUint8Array = rawU8;
|
||||
try {
|
||||
outputUint8Array = await encodeGLB(rawU8, {});
|
||||
} catch (e: any) {
|
||||
this._logger.warn('EncodeGLB skipped: ' + (e?.message || e));
|
||||
}
|
||||
|
||||
this.setLogger("Done")
|
||||
// Uint8Array转file
|
||||
const mimeType = this._outputFormat === Format.GLB ? "model/gltf-binary" : "model/gltf+json";
|
||||
const blob = new Blob([outputUint8Array], { type: mimeType });
|
||||
const outputFile = new File([blob], this._output, { type: mimeType });
|
||||
|
||||
return outputFile;
|
||||
}
|
||||
const { lastReadBytes } = this._io;
|
||||
const lastWriteBytes = outputUint8Array.byteLength;
|
||||
|
||||
if (!this._input) {
|
||||
const output = FileUtils.basename(this._output) + '.' + FileUtils.extension(this._output);
|
||||
this._logger.info(`${output} (${formatBytes(lastWriteBytes)})`);
|
||||
} else {
|
||||
const input = FileUtils.basename(this._inputName) + '.' + FileUtils.extension(this._inputName);
|
||||
const output = FileUtils.basename(this._output) + '.' + FileUtils.extension(this._output);
|
||||
this._logger.info(
|
||||
`${input} (${formatBytes(lastReadBytes)})` + ` → ${output} (${formatBytes(lastWriteBytes)})`,
|
||||
);
|
||||
}
|
||||
|
||||
this.setLogger("Done")
|
||||
|
||||
return outputFile;
|
||||
}
|
||||
}
|
||||
|
||||
function updateMetadata(_document: Document): void {
|
||||
const root = _document.getRoot();
|
||||
const xmpExtension = root
|
||||
.listExtensionsUsed()
|
||||
.find((ext) => ext.extensionName === 'KHR_xmp_json_ld') as KHRXMP | null;
|
||||
const root = _document.getRoot();
|
||||
const xmpExtension = root
|
||||
.listExtensionsUsed()
|
||||
.find((ext) => ext.extensionName === 'KHR_xmp_json_ld') as KHRXMP | null;
|
||||
|
||||
// 不要将KHR_xmp_json_ld添加到尚未使用它的资产中。
|
||||
if (!xmpExtension) return;
|
||||
// 不要将KHR_xmp_json_ld添加到尚未使用它的资产中。
|
||||
if (!xmpExtension) return;
|
||||
|
||||
const rootPacket = root.getExtension<Packet>('KHR_xmp_json_ld') || xmpExtension.createPacket();
|
||||
const rootPacket = root.getExtension<Packet>('KHR_xmp_json_ld') || xmpExtension.createPacket();
|
||||
|
||||
// xmp:MetadataDate should be the same as, or more recent than, xmp:ModifyDate.
|
||||
// https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/xmp.md
|
||||
const date = new Date().toISOString().substring(0, 10);
|
||||
rootPacket
|
||||
.setContext({ ...rootPacket.getContext(), xmp: XMPContext.xmp })
|
||||
.setProperty('xmp:ModifyDate', date)
|
||||
.setProperty('xmp:MetadataDate', date);
|
||||
// xmp:MetadataDate should be the same as, or more recent than, xmp:ModifyDate.
|
||||
// https://github.com/adobe/xmp-docs/blob/master/XMPNamespaces/xmp.md
|
||||
const date = new Date().toISOString().substring(0, 10);
|
||||
rootPacket
|
||||
.setContext({ ...rootPacket.getContext(), xmp: XMPContext.xmp })
|
||||
.setProperty('xmp:ModifyDate', date)
|
||||
.setProperty('xmp:MetadataDate', date);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,53 @@
|
||||
import { injectWasm } from "@/utils/wasm/inject";
|
||||
|
||||
export const XMPContext: Record<string, string> = {
|
||||
dc: 'http://purl.org/dc/elements/1.1/',
|
||||
model3d: 'https://schema.khronos.org/model3d/xsd/1.0/',
|
||||
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
||||
xmp: 'http://ns.adobe.com/xap/1.0/',
|
||||
xmpRights: 'http://ns.adobe.com/xap/1.0/rights/',
|
||||
dc: 'http://purl.org/dc/elements/1.1/',
|
||||
model3d: 'https://schema.khronos.org/model3d/xsd/1.0/',
|
||||
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
||||
xmp: 'http://ns.adobe.com/xap/1.0/',
|
||||
xmpRights: 'http://ns.adobe.com/xap/1.0/rights/',
|
||||
};
|
||||
|
||||
export function formatLong(x: number): string {
|
||||
return x.toString();
|
||||
return x.toString();
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1000;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const k = 1000;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export function dim(str: string): string {
|
||||
return `\x1b[2m${str}\x1b[0m`;
|
||||
return `\x1b[2m${str}\x1b[0m`;
|
||||
}
|
||||
|
||||
/* wasm内优化处理 */
|
||||
let wasmReady = false;
|
||||
|
||||
async function ensureWasmReady() {
|
||||
if (wasmReady) return;
|
||||
await injectWasm({ wasmUrl: "/wasm/Astral3DglTFHandler.wasm" });
|
||||
wasmReady = true;
|
||||
}
|
||||
|
||||
export async function encodeGLB(u8: Uint8Array, meta: Record<string, any> = {}) {
|
||||
await ensureWasmReady();
|
||||
|
||||
const out = window.glTFHandlerEncodeGLB(u8, JSON.stringify(meta || {}));
|
||||
return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
|
||||
}
|
||||
|
||||
export async function encodePNG(png: Uint8Array) {
|
||||
await ensureWasmReady();
|
||||
|
||||
const out = window.glTFHandlerEncodePNG(png);
|
||||
return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
|
||||
}
|
||||
/* wasm内优化处理 End */
|
||||
|
||||
Reference in New Issue
Block a user