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