From 76d5f39607fcaededf09d26af138a70f00d5ce1c Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Sun, 12 Jan 2025 13:23:23 -0500 Subject: [PATCH] [3d] use threejs native viewHelper (#2230) --- src/extensions/core/load3d.ts | 279 ++++++++++++++++++++++------------ 1 file changed, 178 insertions(+), 101 deletions(-) diff --git a/src/extensions/core/load3d.ts b/src/extensions/core/load3d.ts index 5a00648e5..5be50af48 100644 --- a/src/extensions/core/load3d.ts +++ b/src/extensions/core/load3d.ts @@ -2,6 +2,7 @@ import { IWidget } from '@comfyorg/litegraph' import * as THREE from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' +import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper' import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader' @@ -115,8 +116,7 @@ class Load3d { stlLoader: STLLoader currentModel: THREE.Object3D | null = null originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null - node: any - private animationFrameId: number | null = null + animationFrameId: number | null = null gridHelper: THREE.GridHelper lights: THREE.Light[] = [] clock: THREE.Clock @@ -131,6 +131,10 @@ class Load3d { currentUpDirection: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z' = 'original' originalRotation: THREE.Euler | null = null + viewHelper: ViewHelper + viewHelperContainer: HTMLDivElement + cameraSwitcherContainer: HTMLDivElement + gridSwitcherContainer: HTMLDivElement constructor(container: Element | HTMLElement) { this.scene = new THREE.Scene() @@ -157,6 +161,7 @@ class Load3d { this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }) this.renderer.setSize(300, 300) this.renderer.setClearColor(0x282828) + this.renderer.autoClear = false const rendererDomElement: HTMLCanvasElement = this.renderer.domElement @@ -203,13 +208,143 @@ class Load3d { this.standardMaterial = this.createSTLMaterial() - this.animate() + this.createViewHelper(container) + + this.createGridSwitcher(container) + + this.createCameraSwitcher(container) this.handleResize() this.startAnimation() } + createViewHelper(container: Element | HTMLElement) { + this.viewHelperContainer = document.createElement('div') + + this.viewHelperContainer.style.position = 'absolute' + this.viewHelperContainer.style.bottom = '0' + this.viewHelperContainer.style.left = '0' + this.viewHelperContainer.style.width = '128px' + this.viewHelperContainer.style.height = '128px' + this.viewHelperContainer.addEventListener('pointerup', (event) => { + event.stopPropagation() + + this.viewHelper.handleClick(event) + }) + + this.viewHelperContainer.addEventListener('pointerdown', (event) => { + event.stopPropagation() + }) + + container.appendChild(this.viewHelperContainer) + + this.viewHelper = new ViewHelper( + this.activeCamera, + this.viewHelperContainer + ) + + this.viewHelper.center = this.controls.target + } + + createGridSwitcher(container: Element | HTMLElement) { + this.gridSwitcherContainer = document.createElement('div') + this.gridSwitcherContainer.style.position = 'absolute' + this.gridSwitcherContainer.style.top = '28px' // 修改这里,让按钮在相机按钮下方 + this.gridSwitcherContainer.style.left = '3px' // 与相机按钮左对齐 + this.gridSwitcherContainer.style.width = '20px' + this.gridSwitcherContainer.style.height = '20px' + this.gridSwitcherContainer.style.cursor = 'pointer' + this.gridSwitcherContainer.style.alignItems = 'center' + this.gridSwitcherContainer.style.justifyContent = 'center' + this.gridSwitcherContainer.style.transition = 'background-color 0.2s' + + const gridIcon = document.createElement('div') + gridIcon.innerHTML = ` + + + + + + + + ` + + const updateButtonState = () => { + if (this.gridHelper.visible) { + this.gridSwitcherContainer.style.backgroundColor = + 'rgba(255, 255, 255, 0.2)' + } else { + this.gridSwitcherContainer.style.backgroundColor = 'transparent' + } + } + + updateButtonState() + + this.gridSwitcherContainer.addEventListener('mouseenter', () => { + if (!this.gridHelper.visible) { + this.gridSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)' + } + }) + + this.gridSwitcherContainer.addEventListener('mouseleave', () => { + if (!this.gridHelper.visible) { + this.gridSwitcherContainer.style.backgroundColor = 'transparent' + } + }) + + this.gridSwitcherContainer.title = 'Toggle Grid' + + this.gridSwitcherContainer.addEventListener('click', (event) => { + event.stopPropagation() + this.toggleGrid(!this.gridHelper.visible) + updateButtonState() + }) + + this.gridSwitcherContainer.appendChild(gridIcon) + container.appendChild(this.gridSwitcherContainer) + } + + createCameraSwitcher(container: Element | HTMLElement) { + this.cameraSwitcherContainer = document.createElement('div') + this.cameraSwitcherContainer.style.position = 'absolute' + this.cameraSwitcherContainer.style.top = '3px' + this.cameraSwitcherContainer.style.left = '3px' + this.cameraSwitcherContainer.style.width = '20px' + this.cameraSwitcherContainer.style.height = '20px' + this.cameraSwitcherContainer.style.cursor = 'pointer' + this.cameraSwitcherContainer.style.alignItems = 'center' + this.cameraSwitcherContainer.style.justifyContent = 'center' + + const cameraIcon = document.createElement('div') + cameraIcon.innerHTML = ` + + + + + + ` + this.cameraSwitcherContainer.addEventListener('mouseenter', () => { + this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)' + }) + + this.cameraSwitcherContainer.addEventListener('mouseleave', () => { + this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.3)' + }) + + this.cameraSwitcherContainer.title = + 'Switch Camera (Perspective/Orthographic)' + + this.cameraSwitcherContainer.addEventListener('click', (event) => { + event.stopPropagation() + this.toggleCamera() + }) + + this.cameraSwitcherContainer.appendChild(cameraIcon) + + container.appendChild(this.cameraSwitcherContainer) + } + setFOV(fov: number) { if (this.activeCamera === this.perspectiveCamera) { this.perspectiveCamera.fov = fov @@ -465,6 +600,13 @@ class Load3d { this.controls.target.copy(target) this.controls.update() + this.viewHelper.dispose() + this.viewHelper = new ViewHelper( + this.activeCamera, + this.viewHelperContainer + ) + this.viewHelper.center = this.controls.target + this.handleResize() } @@ -501,8 +643,16 @@ class Load3d { startAnimation() { const animate = () => { this.animationFrameId = requestAnimationFrame(animate) + const delta = this.clock.getDelta() + + if (this.viewHelper.animating) { + this.viewHelper.update(delta) + } + + this.renderer.clear() this.controls.update() this.renderer.render(this.scene, this.activeCamera) + this.viewHelper.render(this.renderer) } animate() } @@ -588,6 +738,7 @@ class Load3d { } this.controls.dispose() + this.viewHelper.dispose() this.renderer.dispose() this.renderer.domElement.remove() this.scene.clear() @@ -818,10 +969,12 @@ class Load3d { this.orthographicCamera.updateProjectionMatrix() } + this.renderer.clear() this.renderer.render(this.scene, this.activeCamera) const sceneData = this.renderer.domElement.toDataURL('image/png') this.renderer.setClearColor(0x000000, 0) + this.renderer.clear() this.renderer.render(this.scene, this.activeCamera) const maskData = this.renderer.domElement.toDataURL('image/png') @@ -846,44 +999,6 @@ class Load3d { }) } - setViewPosition(position: 'front' | 'top' | 'right' | 'isometric') { - if (!this.currentModel) { - return - } - - const box = new THREE.Box3() - let center = new THREE.Vector3() - let size = new THREE.Vector3() - - if (this.currentModel) { - box.setFromObject(this.currentModel) - box.getCenter(center) - box.getSize(size) - } - - const maxDim = Math.max(size.x, size.y, size.z) - const distance = maxDim * 2 - - switch (position) { - case 'front': - this.activeCamera.position.set(0, 0, distance) - break - case 'top': - this.activeCamera.position.set(0, distance, 0) - break - case 'right': - this.activeCamera.position.set(distance, 0, 0) - break - case 'isometric': - this.activeCamera.position.set(distance, distance, distance) - break - } - - this.activeCamera.lookAt(center) - this.controls.target.copy(center) - this.controls.update() - } - setBackgroundColor(color: string) { this.renderer.setClearColor(new THREE.Color(color)) this.renderer.render(this.scene, this.activeCamera) @@ -1020,16 +1135,28 @@ class Load3dAnimation extends Load3d { }) } - animate = () => { - requestAnimationFrame(this.animate) - - if (this.currentAnimation && this.isAnimationPlaying) { + startAnimation() { + const animate = () => { + this.animationFrameId = requestAnimationFrame(animate) const delta = this.clock.getDelta() - this.currentAnimation.update(delta) - } - this.controls.update() - this.renderer.render(this.scene, this.activeCamera) + if (this.currentAnimation && this.isAnimationPlaying) { + this.currentAnimation.update(delta) + } + + this.controls.update() + + this.renderer.clear() + + this.renderer.render(this.scene, this.activeCamera) + + if (this.viewHelper.animating) { + this.viewHelper.update(delta) + } + + this.viewHelper.render(this.renderer) + } + animate() } } @@ -1076,9 +1203,6 @@ function configureLoad3D( load3d: Load3d, loadFolder: 'input' | 'output', modelWidget: IWidget, - showGrid: IWidget, - cameraType: IWidget, - view: IWidget, material: IWidget, bgColor: IWidget, lightIntensity: IWidget, @@ -1138,22 +1262,6 @@ function configureLoad3D( modelWidget.callback = onModelWidgetUpdate - load3d.toggleGrid(showGrid.value as boolean) - - showGrid.callback = (value: boolean) => { - load3d.toggleGrid(value) - } - - load3d.toggleCamera(cameraType.value as 'perspective' | 'orthographic') - - cameraType.callback = (value: 'perspective' | 'orthographic') => { - load3d.toggleCamera(value) - } - - view.callback = (value: 'front' | 'top' | 'right' | 'isometric') => { - load3d.setViewPosition(value) - } - material.callback = (value: 'original' | 'normal' | 'wireframe') => { load3d.setMaterialMode(value) } @@ -1312,14 +1420,6 @@ app.registerExtension({ (w: IWidget) => w.name === 'model_file' ) - const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid') - - const cameraType = node.widgets.find( - (w: IWidget) => w.name === 'camera_type' - ) - - const view = node.widgets.find((w: IWidget) => w.name === 'view') - const material = node.widgets.find((w: IWidget) => w.name === 'material') const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color') @@ -1353,9 +1453,6 @@ app.registerExtension({ load3d, 'input', modelWidget, - showGrid, - cameraType, - view, material, bgColor, lightIntensity, @@ -1569,14 +1666,6 @@ app.registerExtension({ (w: IWidget) => w.name === 'model_file' ) - const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid') - - const cameraType = node.widgets.find( - (w: IWidget) => w.name === 'camera_type' - ) - - const view = node.widgets.find((w: IWidget) => w.name === 'view') - const material = node.widgets.find((w: IWidget) => w.name === 'material') const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color') @@ -1621,9 +1710,6 @@ app.registerExtension({ load3d, 'input', modelWidget, - showGrid, - cameraType, - view, material, bgColor, lightIntensity, @@ -1652,6 +1738,8 @@ app.registerExtension({ sceneWidget.serializeValue = async () => { node.properties['Camera Info'] = JSON.stringify(load3d.getCameraState()) + load3d.toggleAnimation(false) + const { scene: imageData, mask: maskData } = await load3d.captureScene( w.value, h.value @@ -1758,14 +1846,6 @@ app.registerExtension({ (w: IWidget) => w.name === 'model_file' ) - const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid') - - const cameraType = node.widgets.find( - (w: IWidget) => w.name === 'camera_type' - ) - - const view = node.widgets.find((w: IWidget) => w.name === 'view') - const material = node.widgets.find((w: IWidget) => w.name === 'material') const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color') @@ -1801,9 +1881,6 @@ app.registerExtension({ load3d, 'output', modelWidget, - showGrid, - cameraType, - view, material, bgColor, lightIntensity,