| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- /**
- * glTF 模型重新优化脚本
- *
- * 修复问题:
- * - 网格简化 (simplify) 导致纹理拉伸
- * - 网格简化导致墙体孔洞,室内漫游碰撞失效
- *
- * 策略:
- * 只做无损/几何保真压缩,不做网格简化:
- * 1. dedupe - 去重访问器/纹理(不合并材质)
- * 2. prune - 清理未引用资源
- * 3. weld - 仅合并同位置同 UV 顶点(保守容差,保证不破坏 UV)
- * 4. draco - 几何压缩(保留全部顶点和 UV)
- * 5. textureCompress - WebP 纹理压缩(保留 UV 映射)
- *
- * 用法:
- * node scripts/optimize-glb.mjs
- */
- import { NodeIO } from '@gltf-transform/core';
- import { dedup, prune, weld, draco, textureCompress } from '@gltf-transform/functions';
- import { EXTTextureWebP, KHRDracoMeshCompression, KHRMaterialsSpecular } from '@gltf-transform/extensions';
- import draco3d from 'draco3d';
- import fs from 'node:fs';
- const INPUT = 'd:/Web/PlatformModel/RuoYi-Vue3/public/models/莒口水闸3_original.glb';
- const OUTPUT = 'd:/Web/PlatformModel/RuoYi-Vue3/public/models/莒口水闸3_reoptimized.glb';
- async function main() {
- console.log('读取原始模型:', INPUT);
- // 注册扩展和 Draco 编解码器依赖
- const [encoder, decoder] = await Promise.all([
- draco3d.createEncoderModule(),
- draco3d.createDecoderModule(),
- ]);
- const io = new NodeIO()
- .registerExtensions([KHRDracoMeshCompression, EXTTextureWebP, KHRMaterialsSpecular])
- .registerDependencies({
- 'draco3d.encoder': encoder,
- 'draco3d.decoder': decoder,
- });
- const doc = await io.read(INPUT);
- const root = doc.getRoot();
- console.log('原始模型资源统计:');
- console.log(' 网格数:', root.listMeshes().length);
- console.log(' 节点数:', root.listNodes().length);
- console.log(' 材质数:', root.listMaterials().length);
- console.log(' 纹理数:', root.listTextures().length);
- let totalVerts = 0, totalIdx = 0;
- for (const mesh of root.listMeshes()) {
- for (const prim of mesh.listPrimitives()) {
- const pos = prim.getAttribute('POSITION');
- if (pos) totalVerts += pos.getCount();
- const idx = prim.getIndices();
- if (idx) totalIdx += idx.getCount();
- }
- }
- console.log(' 原始顶点总数:', totalVerts);
- console.log(' 原始三角形索引总数:', totalIdx);
- // 创建 WebP 扩展(textureCompress 会自动注册并使用)
- doc.createExtension(EXTTextureWebP);
- console.log('\n应用优化变换...');
- // 1. 去重访问器/纹理(不会合并不同材质)
- console.log(' [1/5] dedup - 去重访问器和纹理...');
- await doc.transform(dedup());
- // 2. 清理未引用资源
- console.log(' [2/5] prune - 清理未引用资源...');
- await doc.transform(prune());
- // 3. 保守的 weld:仅合并位置完全相同且 UV 相同的顶点
- // 容差设为极小值,避免合并不同 UV 的顶点导致纹理拉伸
- console.log(' [3/5] weld - 保守顶点合并(容差 1e-4)...');
- await doc.transform(weld({ tolerance: 1e-4 }));
- // 4. Draco 几何压缩(保留全部顶点和 UV,仅压缩存储)
- console.log(' [4/5] draco - 几何压缩...');
- await doc.transform(
- draco({
- encodeSpeed: 5,
- decodeSpeed: 5,
- quantizePosition: 14,
- quantizeNormal: 10,
- quantizeColor: 8,
- quantizeTexcoord: 12,
- quantizeGeneric: 12,
- })
- );
- // 5. 纹理压缩为 WebP(保留 UV 映射,仅压缩图像数据)
- console.log(' [5/5] textureCompress - WebP 纹理压缩...');
- await doc.transform(
- textureCompress({
- targetFormat: 'webp',
- encoderOptions: { quality: 80 },
- resizeFilter: 'lanczos4',
- })
- );
- console.log('\n输出优化模型:', OUTPUT);
- await io.write(OUTPUT, doc);
- const outStat = fs.statSync(OUTPUT);
- console.log('输出文件大小:', (outStat.size / 1024 / 1024).toFixed(2), 'MB');
- // 验证输出
- console.log('\n=== 验证输出模型 ===');
- const outDoc = await io.read(OUTPUT);
- const outRoot = outDoc.getRoot();
- console.log('输出模型资源统计:');
- console.log(' 网格数:', outRoot.listMeshes().length);
- console.log(' 节点数:', outRoot.listNodes().length);
- console.log(' 材质数:', outRoot.listMaterials().length);
- console.log(' 纹理数:', outRoot.listTextures().length);
- let outVerts = 0, outIdx = 0;
- for (const mesh of outRoot.listMeshes()) {
- for (const prim of mesh.listPrimitives()) {
- const pos = prim.getAttribute('POSITION');
- if (pos) outVerts += pos.getCount();
- const idx = prim.getIndices();
- if (idx) outIdx += idx.getCount();
- }
- }
- console.log(' 输出顶点总数:', outVerts, '(原始:', totalVerts, ')');
- console.log(' 输出三角形索引总数:', outIdx, '(原始:', totalIdx, ')');
- const vertLoss = ((1 - outVerts / totalVerts) * 100).toFixed(2);
- const idxLoss = ((1 - outIdx / totalIdx ) * 100).toFixed(2);
- console.log(' 顶点变化:', vertLoss + '% (应接近 0%,仅 weld 合并的重复顶点)');
- console.log(' 索引变化:', idxLoss + '% (应接近 0%)');
- console.log('\n✅ 优化完成');
- }
- main().catch(err => {
- console.error('❌ 优化失败:', err);
- process.exit(1);
- });
|