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.
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.
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])