# Threejs

> Three.js scene-graph parsing and export workflows for mesh baking, InstancedMesh expansion, part partitioning, per-link OBJ export, and URDF articulation.

- Skill: `jsgforever/threejs` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add jsgforever/threejs`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jsgforever/threejs/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: jsgforever (https://skillmd.com/u/jsgforever)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jsgforever/threejs

---


# Three.js Scene Graph + Export

## Quick start

1) Load the module, call `createScene()`, then `updateMatrixWorld(true)`.
2) Treat **named `THREE.Group` nodes as parts/links**.
3) Assign each mesh to its **nearest named ancestor**.
4) Bake world transforms into geometry before export.
5) 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` / `THREE.Object3D`: containers
- `THREE.Group`: logical part (no geometry)
- `THREE.Mesh`: geometry + material

## 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.

```javascript
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:

```javascript
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

```javascript
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)

```javascript
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 `fixed` unless evidence suggests `revolute`/`prismatic`.
- Sort link and joint names for determinism.

References:
- `references/joint-type-heuristics.md`
- `references/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.

