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(); } }); } }