Visualizing Three.js bone orientations

THREE.SkeletonHelper is commonly used to visualize skeletons in Three.js. This draws line segments between bone positions, but doesn’t show their orientation which can be deceiving.

Here’s some helpers to visualize a THREE.AxesHelper at each bone to show its true orientation.

Xbot mixamo character with XYZ axes shown at each bone position

Functional implementation

The simplest approach is to add a THREE.AxesHelper to each bone. I set the material to be transparent and disable depth testing so that axes render on top. Bones are traversed and setup in separate iteration passes so traversal doesn’t recurse through the axes as we add them.

The function adds the helpers and returns a cleanup function to dispose them.

      function addBoneAxesHelpers(root, size = 0.05) {
	const axesHelpers = []
	const bones = []

	root.traverse((object) => object.isBone && bones.push(object))

	for (let i = 0; i < bones.length; ++i) {
		const bone = bones[i]
		const axesHelper = new THREE.AxesHelper(size)

		axesHelper.material.transparent = true
		axesHelper.material.depthTest = false

		axesHelpers.push(axesHelper)

		bone.add(axesHelper)
	}

	return () => {
		for (let i = 0; i < axesHelpers.length; ++i) {
			const axesHelper = axesHelpers[i]

			axesHelper.removeFromParent()
			axesHelper.dispose()
		}
	}
}

    
TypeScript version
      function addBoneAxesHelpers(root: THREE.Object3D, size = 0.05) {
	const axesHelpers: THREE.AxesHelper[] = []
	const bones: THREE.Bone[] = []

	root.traverse((object) => {
		if (object instanceof THREE.Bone) {
			bones.push(object)
		}
	})

	for (let i = 0; i < bones.length; ++i) {
		const bone = bones[i]
		const axesHelper = new THREE.AxesHelper(size)

		if (!(axesHelper.material instanceof THREE.Material)) {
			throw new Error("Invalid material")
		}

		axesHelper.material.transparent = true
		axesHelper.material.depthTest = false

		axesHelpers.push(axesHelper)

		bone.add(axesHelper)
	}

	root.traverse((object) => {
		if (object instanceof THREE.Bone) {
			const axesHelper = new THREE.AxesHelper(size)

			if (!(axesHelper.material instanceof THREE.Material)) {
				throw new Error("Invalid material")
			}

			axesHelper.material.transparent = true
			axesHelper.material.depthTest = false

			object.add(axesHelper)

			axesHelpers.push(axesHelper)
		}
	})

	return () => {
		for (let i = 0; i < axesHelpers.length; ++i) {
			const axesHelper = axesHelpers[i]
			axesHelper.removeFromParent()
			axesHelper.dispose()
		}
	}
}

    

Usage:

      const dispose = addBoneAxesHelpers(skinnedMesh)

// later...
dispose()

    

Class implementation

Built-in Three.js helpers like THREE.SkeletonHelper inherit from THREE.Object3D and get added to the root of the scene like normal objects. Updates are handled by the renderer through updateMatrixWorld. Here’s a class based implementation that behaves similarly, leaving the bones untouched.

      class SkeletonAxesHelper extends THREE.Object3D {
	constructor(object, size = 0.05) {
		super()

		this.bones = []
		this.axesHelpers = []

		object.traverse((object) => object.isBone && this.bones.push(object))

		for (let i = 0; i < this.bones.length; i++) {
			const axesHelper = new THREE.AxesHelper(size)

			axesHelper.material.transparent = true
			axesHelper.material.depthTest = false
			axesHelper.matrixAutoUpdate = false

			this.axesHelpers.push(axesHelper)
			this.add(axesHelper)
		}
	}

	updateMatrixWorld(force) {
		for (let i = 0; i < this.bones.length; i++) {
			const bone = this.bones[i]
			const axesHelper = this.axesHelpers[i]

			axesHelper.matrix.copy(bone.matrixWorld)
		}

		super.updateMatrixWorld(force)
	}

	dispose() {
		for (let i = 0; i < this.axesHelpers.length; i++) {
			this.axesHelpers[i].dispose()
		}
	}
}

    
TypeScript version
      class SkeletonAxesHelper extends THREE.Object3D {
	bones: THREE.Bone[]
	axesHelpers: THREE.AxesHelper[]

	constructor(object: THREE.Object3D, size = 0.05) {
		super()

		this.bones = []
		this.axesHelpers = []

		object.traverse((object) => {
			if (object instanceof THREE.Bone) {
				this.bones.push(object)
			}
		})

		for (let i = 0; i < this.bones.length; i++) {
			const axesHelper = new THREE.AxesHelper(size)

			if (!(axesHelper.material instanceof THREE.Material)) {
				throw new Error("Invalid material")
			}

			axesHelper.material.transparent = true
			axesHelper.material.depthTest = false

			axesHelper.matrixAutoUpdate = false

			this.axesHelpers.push(axesHelper)
			this.add(axesHelper)
		}
	}

	updateMatrixWorld(force: boolean) {
		for (let i = 0; i < this.bones.length; i++) {
			const bone = this.bones[i]
			const axesHelper = this.axesHelpers[i]

			axesHelper.matrix.copy(bone.matrixWorld)
		}

		super.updateMatrixWorld(force)
	}

	dispose() {
		for (let i = 0; i < this.axesHelpers.length; i++) {
			this.axesHelpers[i].dispose()
		}
	}
}

    

Usage in Three.js editor

I often want to visualize bone orientations while inspecting models on https://three.js/editor/

After selecting an object in the hierarchy, you can access it from the browser console via editor.selected.

      addBoneAxesHelpers(editor.selected)

    

After running this, you’ll need to wiggle the camera slightly to see the scene update.

Soldier glTF sample with XYZ axes shown at each bone position

Usage in React-Three-Fiber

One benefit of the class implementation is integration with Drei’s useHelper() hook.

      useHelper(skinnedMeshRef, SkeletonAxesHelper)

    

Otherwise you can use the functional variant in a layout effect.

      useLayoutEffect(() => addBoneAxesHelpers(skinnedMesh), [skinnedMesh])