Files
Mujoco_WASM/web_platform/src/project/daeConverter.ts
T
chenlin 60d3a6d68c
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
chore(web-platform): release V0.6.1 工程质量优化
2026-08-28 15:38:10 +08:00

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