Skillsbench threejs
Three.js scene-graph parsing and export workflows: mesh baking, InstancedMesh expansion, part partitioning, per-link OBJ export, and URDF articulation.
install
source · Clone the upstream repo
git clone https://github.com/benchflow-ai/skillsbench
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/benchflow-ai/skillsbench "$T" && mkdir -p ~/.claude/skills && cp -r "$T/tasks/threejs-to-obj/environment/skills/threejs" ~/.claude/skills/benchflow-ai-skillsbench-threejs-f5aad7 && rm -rf "$T"
manifest:
tasks/threejs-to-obj/environment/skills/threejs/SKILL.mdsource content
Three.js Scene Graph + Export
Quick start
- Load the module, call
, thencreateScene()
.updateMatrixWorld(true) - Treat named
nodes as parts/links.THREE.Group - Assign each mesh to its nearest named ancestor.
- Bake world transforms into geometry before export.
- Export per-part OBJ (and optionally URDF) using deterministic ordering.
Scene graph essentials
Object3D (root) ├── Group (part) │ ├── Mesh │ └── Group (child part) │ └── Mesh └── Group (another part) └── Mesh
/THREE.Scene
: containersTHREE.Object3D
: logical part (no geometry)THREE.Group
: geometry + materialTHREE.Mesh
Part partitioning rules
- Parts are named groups at any depth.
- Each mesh belongs to the nearest named ancestor.
- Nested named groups are separate parts; do not merge their meshes into parents.
- Skip empty parts; sort part names for determinism.
function buildPartMap(root) { const parts = new Map(); root.traverse((obj) => { if (obj.isGroup && obj.name) parts.set(obj.name, { group: obj, meshes: [] }); }); root.traverse((obj) => { if (!obj.isMesh) return; let parent = obj.parent; while (parent && !(parent.isGroup && parent.name)) parent = parent.parent; if (parent && parts.has(parent.name)) parts.get(parent.name).meshes.push(obj); }); return Array.from(parts.values()).filter((p) => p.meshes.length > 0); }
Mesh baking (world transforms)
Always bake transforms before export:
root.updateMatrixWorld(true); let geom = mesh.geometry.clone(); geom.applyMatrix4(mesh.matrixWorld); if (geom.index) geom = geom.toNonIndexed(); if (!geom.attributes.normal) geom.computeVertexNormals();
InstancedMesh expansion
const tempMatrix = new THREE.Matrix4(); const instanceMatrix = new THREE.Matrix4(); obj.getMatrixAt(i, instanceMatrix); tempMatrix.copy(obj.matrixWorld).multiply(instanceMatrix);
Axis conversion (Y-up to Z-up)
const axisMatrix = new THREE.Matrix4().makeRotationX(-Math.PI / 2); geom.applyMatrix4(axisMatrix);
Per-part OBJ export
- Collect meshes owned by each part.
- Do not traverse into child named groups when exporting a part.
- Merge baked geometries per part and write
.<part>.obj
Reference:
references/link-export-rules.md.
Relation between Three.js and URDF articulation
- Use named groups as links.
- Parent is the nearest named ancestor link.
- Joint type defaults to
unless evidence suggestsfixed
/revolute
.prismatic - Sort link and joint names for determinism.
References:
references/joint-type-heuristics.mdreferences/urdf-minimal.md
Scripts
- InstancedMesh OBJ exporter:
node scripts/export_instanced_obj.mjs - Per-link OBJ exporter:
node scripts/export_link_objs.mjs --input <scene_js> --out-dir <dir> - URDF builder:
node scripts/build_urdf_from_scene.mjs --input <scene_js> --output <file.urdf> --mesh-dir <mesh_dir>
Adjust inputs/outputs inside scripts as needed.