optimize-glb.mjs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /**
  2. * glTF 模型重新优化脚本
  3. *
  4. * 修复问题:
  5. * - 网格简化 (simplify) 导致纹理拉伸
  6. * - 网格简化导致墙体孔洞,室内漫游碰撞失效
  7. *
  8. * 策略:
  9. * 只做无损/几何保真压缩,不做网格简化:
  10. * 1. dedupe - 去重访问器/纹理(不合并材质)
  11. * 2. prune - 清理未引用资源
  12. * 3. weld - 仅合并同位置同 UV 顶点(保守容差,保证不破坏 UV)
  13. * 4. draco - 几何压缩(保留全部顶点和 UV)
  14. * 5. textureCompress - WebP 纹理压缩(保留 UV 映射)
  15. *
  16. * 用法:
  17. * node scripts/optimize-glb.mjs
  18. */
  19. import { NodeIO } from '@gltf-transform/core';
  20. import { dedup, prune, weld, draco, textureCompress } from '@gltf-transform/functions';
  21. import { EXTTextureWebP, KHRDracoMeshCompression, KHRMaterialsSpecular } from '@gltf-transform/extensions';
  22. import draco3d from 'draco3d';
  23. import fs from 'node:fs';
  24. const INPUT = 'd:/Web/PlatformModel/RuoYi-Vue3/public/models/莒口水闸3_original.glb';
  25. const OUTPUT = 'd:/Web/PlatformModel/RuoYi-Vue3/public/models/莒口水闸3_reoptimized.glb';
  26. async function main() {
  27. console.log('读取原始模型:', INPUT);
  28. // 注册扩展和 Draco 编解码器依赖
  29. const [encoder, decoder] = await Promise.all([
  30. draco3d.createEncoderModule(),
  31. draco3d.createDecoderModule(),
  32. ]);
  33. const io = new NodeIO()
  34. .registerExtensions([KHRDracoMeshCompression, EXTTextureWebP, KHRMaterialsSpecular])
  35. .registerDependencies({
  36. 'draco3d.encoder': encoder,
  37. 'draco3d.decoder': decoder,
  38. });
  39. const doc = await io.read(INPUT);
  40. const root = doc.getRoot();
  41. console.log('原始模型资源统计:');
  42. console.log(' 网格数:', root.listMeshes().length);
  43. console.log(' 节点数:', root.listNodes().length);
  44. console.log(' 材质数:', root.listMaterials().length);
  45. console.log(' 纹理数:', root.listTextures().length);
  46. let totalVerts = 0, totalIdx = 0;
  47. for (const mesh of root.listMeshes()) {
  48. for (const prim of mesh.listPrimitives()) {
  49. const pos = prim.getAttribute('POSITION');
  50. if (pos) totalVerts += pos.getCount();
  51. const idx = prim.getIndices();
  52. if (idx) totalIdx += idx.getCount();
  53. }
  54. }
  55. console.log(' 原始顶点总数:', totalVerts);
  56. console.log(' 原始三角形索引总数:', totalIdx);
  57. // 创建 WebP 扩展(textureCompress 会自动注册并使用)
  58. doc.createExtension(EXTTextureWebP);
  59. console.log('\n应用优化变换...');
  60. // 1. 去重访问器/纹理(不会合并不同材质)
  61. console.log(' [1/5] dedup - 去重访问器和纹理...');
  62. await doc.transform(dedup());
  63. // 2. 清理未引用资源
  64. console.log(' [2/5] prune - 清理未引用资源...');
  65. await doc.transform(prune());
  66. // 3. 保守的 weld:仅合并位置完全相同且 UV 相同的顶点
  67. // 容差设为极小值,避免合并不同 UV 的顶点导致纹理拉伸
  68. console.log(' [3/5] weld - 保守顶点合并(容差 1e-4)...');
  69. await doc.transform(weld({ tolerance: 1e-4 }));
  70. // 4. Draco 几何压缩(保留全部顶点和 UV,仅压缩存储)
  71. console.log(' [4/5] draco - 几何压缩...');
  72. await doc.transform(
  73. draco({
  74. encodeSpeed: 5,
  75. decodeSpeed: 5,
  76. quantizePosition: 14,
  77. quantizeNormal: 10,
  78. quantizeColor: 8,
  79. quantizeTexcoord: 12,
  80. quantizeGeneric: 12,
  81. })
  82. );
  83. // 5. 纹理压缩为 WebP(保留 UV 映射,仅压缩图像数据)
  84. console.log(' [5/5] textureCompress - WebP 纹理压缩...');
  85. await doc.transform(
  86. textureCompress({
  87. targetFormat: 'webp',
  88. encoderOptions: { quality: 80 },
  89. resizeFilter: 'lanczos4',
  90. })
  91. );
  92. console.log('\n输出优化模型:', OUTPUT);
  93. await io.write(OUTPUT, doc);
  94. const outStat = fs.statSync(OUTPUT);
  95. console.log('输出文件大小:', (outStat.size / 1024 / 1024).toFixed(2), 'MB');
  96. // 验证输出
  97. console.log('\n=== 验证输出模型 ===');
  98. const outDoc = await io.read(OUTPUT);
  99. const outRoot = outDoc.getRoot();
  100. console.log('输出模型资源统计:');
  101. console.log(' 网格数:', outRoot.listMeshes().length);
  102. console.log(' 节点数:', outRoot.listNodes().length);
  103. console.log(' 材质数:', outRoot.listMaterials().length);
  104. console.log(' 纹理数:', outRoot.listTextures().length);
  105. let outVerts = 0, outIdx = 0;
  106. for (const mesh of outRoot.listMeshes()) {
  107. for (const prim of mesh.listPrimitives()) {
  108. const pos = prim.getAttribute('POSITION');
  109. if (pos) outVerts += pos.getCount();
  110. const idx = prim.getIndices();
  111. if (idx) outIdx += idx.getCount();
  112. }
  113. }
  114. console.log(' 输出顶点总数:', outVerts, '(原始:', totalVerts, ')');
  115. console.log(' 输出三角形索引总数:', outIdx, '(原始:', totalIdx, ')');
  116. const vertLoss = ((1 - outVerts / totalVerts) * 100).toFixed(2);
  117. const idxLoss = ((1 - outIdx / totalIdx ) * 100).toFixed(2);
  118. console.log(' 顶点变化:', vertLoss + '% (应接近 0%,仅 weld 合并的重复顶点)');
  119. console.log(' 索引变化:', idxLoss + '% (应接近 0%)');
  120. console.log('\n✅ 优化完成');
  121. }
  122. main().catch(err => {
  123. console.error('❌ 优化失败:', err);
  124. process.exit(1);
  125. });