feat: add skeleton visualization toggle for 3D models (#7857)

## Summary

For better support animation 3d model custon node, such as
https://github.com/jtydhr88/ComfyUI-HY-Motion1, add ability to show/hide
skeleton bones in Load3D nodes for models with skeletal animation. Uses
THREE.SkeletonHelper with root bone detection to properly support both
FBX and GLB model formats.

## Screenshots


https://github.com/user-attachments/assets/df9de4a6-549e-4227-aa00-8859d71f43d1

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7857-feat-add-skeleton-visualization-toggle-for-3D-models-2e06d73d365081a39f49f81f72657a70)
by [Unito](https://www.unito.io)
This commit is contained in:
Terry Jia
2026-01-06 21:11:06 -05:00
committed by GitHub
parent 6e5dfc0109
commit dad1eafecc
10 changed files with 137 additions and 9 deletions

View File

@@ -156,8 +156,9 @@ class Load3DConfiguration {
return {
upDirection: 'original',
materialMode: 'original'
} as ModelConfig
materialMode: 'original',
showSkeleton: false
}
}
private applySceneConfig(config: SceneConfig, bgImagePath?: string) {

View File

@@ -727,6 +727,19 @@ class Load3d {
return this.animationManager.animationClips.length > 0
}
public hasSkeleton(): boolean {
return this.modelManager.hasSkeleton()
}
public setShowSkeleton(show: boolean): void {
this.modelManager.setShowSkeleton(show)
this.forceRender()
}
public getShowSkeleton(): boolean {
return this.modelManager.showSkeleton
}
public getAnimationTime(): number {
return this.animationManager.getAnimationTime()
}

View File

@@ -30,6 +30,8 @@ export class SceneModelManager implements ModelManagerInterface {
originalURL: string | null = null
appliedTexture: THREE.Texture | null = null
textureLoader: THREE.TextureLoader
skeletonHelper: THREE.SkeletonHelper | null = null
showSkeleton: boolean = false
private scene: THREE.Scene
private renderer: THREE.WebGLRenderer
@@ -414,9 +416,69 @@ export class SceneModelManager implements ModelManagerInterface {
this.appliedTexture = null
}
if (this.skeletonHelper) {
this.scene.remove(this.skeletonHelper)
this.skeletonHelper.dispose()
this.skeletonHelper = null
}
this.showSkeleton = false
this.originalMaterials = new WeakMap()
}
hasSkeleton(): boolean {
if (!this.currentModel) return false
let found = false
this.currentModel.traverse((child) => {
if (child instanceof THREE.SkinnedMesh && child.skeleton) {
found = true
}
})
return found
}
setShowSkeleton(show: boolean): void {
this.showSkeleton = show
if (show) {
if (!this.skeletonHelper && this.currentModel) {
let rootBone: THREE.Bone | null = null
this.currentModel.traverse((child) => {
if (child instanceof THREE.Bone && !rootBone) {
if (!(child.parent instanceof THREE.Bone)) {
rootBone = child
}
}
})
if (rootBone) {
this.skeletonHelper = new THREE.SkeletonHelper(rootBone)
this.scene.add(this.skeletonHelper)
} else {
let skinnedMesh: THREE.SkinnedMesh | null = null
this.currentModel.traverse((child) => {
if (child instanceof THREE.SkinnedMesh && !skinnedMesh) {
skinnedMesh = child
}
})
if (skinnedMesh) {
this.skeletonHelper = new THREE.SkeletonHelper(skinnedMesh)
this.scene.add(this.skeletonHelper)
}
}
} else if (this.skeletonHelper) {
this.skeletonHelper.visible = true
}
} else {
if (this.skeletonHelper) {
this.skeletonHelper.visible = false
}
}
this.eventManager.emitEvent('skeletonVisibilityChange', show)
}
addModelToScene(model: THREE.Object3D): void {
this.currentModel = model
model.name = 'MainModel'

View File

@@ -34,6 +34,7 @@ export interface SceneConfig {
export interface ModelConfig {
upDirection: UpDirection
materialMode: MaterialMode
showSkeleton: boolean
}
export interface CameraConfig {