import { LoadingManager, type Material, type Mesh, type Texture } from 'three'; import { OBJExporter } from 'three/addons/exporters/OBJExporter.js'; import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js'; const TRANSPARENT_PIXEL = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; /** * 将 Collada 几何转换为 MuJoCo WASM 可读取的 OBJ。 * ColladaLoader 会先统一为 Y-up;额外旋转到 MuJoCo 使用的 Z-up,并烘焙节点变换与单位缩放。 */ export function convertDaeToObj(data: Uint8Array, path: string): Uint8Array { const manager = new LoadingManager(); // 转换只需要几何。拦截贴图 URL,避免为浏览器内存文件发起无效网络请求。 manager.setURLModifier(() => TRANSPARENT_PIXEL); const loader = new ColladaLoader(manager); const text = new TextDecoder('utf-8').decode(data); const xml = new DOMParser().parseFromString(text, 'application/xml'); if (xml.querySelector('parsererror')) throw new Error('Collada XML 格式无效'); const upAxis = xml.getElementsByTagName('up_axis')[0]?.textContent?.trim().toUpperCase() ?? 'Y_UP'; // 禁用 ColladaLoader 自带的 Z-up → Y-up 旋转,改为直接统一到 MuJoCo 的 Z-up。 if (upAxis === 'Z_UP') xml.getElementsByTagName('up_axis')[0]!.textContent = 'Y_UP'; const normalized = new XMLSerializer().serializeToString(xml); const result = loader.parse(normalized, path.slice(0, path.lastIndexOf('/') + 1)); if (!result?.scene) throw new Error('Collada 文件无法解析'); const scene = result.scene; if (upAxis === 'Y_UP') scene.rotation.x += Math.PI / 2; else if (upAxis === 'X_UP') scene.rotation.y -= Math.PI / 2; scene.updateMatrixWorld(true); let meshCount = 0; scene.traverse((object) => { const mesh = object as Mesh; if (!mesh.isMesh) return; meshCount += 1; const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]; for (const material of materials) if (material) material.name = ''; }); if (!meshCount) throw new Error('Collada 文件不包含可转换的三角网格'); try { const output = new OBJExporter().parse(scene); if (!/^v\s/m.test(output) || !/^f\s/m.test(output)) throw new Error('Collada 文件未生成有效三角面'); return new TextEncoder().encode(output); } finally { scene.traverse((object) => { const mesh = object as Mesh; if (!mesh.isMesh) return; mesh.geometry?.dispose(); const materials: Material[] = Array.isArray(mesh.material) ? mesh.material : [mesh.material]; for (const material of materials) { for (const value of Object.values(material)) if (value && typeof value === 'object' && (value as Texture).isTexture) (value as Texture).dispose(); material.dispose(); } }); } }