Files
Mujoco_WASM/wasm/web_platform/src/project/daeConverter.ts
T
chenlin 1eb05132f1
build / setup (compute matrix) (pull_request) Has been cancelled
build / ${{ matrix.label }} (pull_request) Has been cancelled
build / macos-15-arm64-studio (pull_request) Has been cancelled
build / ubuntu-24.04-clang-18-studio (pull_request) Has been cancelled
build / ubuntu-24.04-gcc-14-studio (pull_request) Has been cancelled
build / windows-2025-ninja-studio (pull_request) Has been cancelled
build / ubuntu-24.04-clang-18-wasm (pull_request) Has been cancelled
build / ubuntu-24.04-clang-18-mjx (pull_request) Has been cancelled
lint / pre-commit (pull_request) Has been cancelled
feat: add browser-based MuJoCo simulation platform
2026-08-20 15:12:08 +08:00

55 lines
2.6 KiB
TypeScript

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