diff --git a/src/components/load3d/Load3DAnimationControls.vue b/src/components/load3d/Load3DAnimationControls.vue index cdb280b5f..cda246c15 100644 --- a/src/components/load3d/Load3DAnimationControls.vue +++ b/src/components/load3d/Load3DAnimationControls.vue @@ -3,9 +3,18 @@ @@ -56,15 +65,24 @@ const props = defineProps<{ playing: boolean backgroundColor: string showGrid: boolean + showPreview: boolean + lightIntensity: number + showLightIntensityButton: boolean + fov: number + showFOVButton: boolean + showPreviewButton: boolean }>() const emit = defineEmits<{ (e: 'toggleCamera'): void (e: 'toggleGrid', value: boolean): void + (e: 'togglePreview', value: boolean): void (e: 'updateBackgroundColor', color: string): void (e: 'togglePlay', value: boolean): void (e: 'speedChange', value: number): void (e: 'animationChange', value: number): void + (e: 'updateLightIntensity', value: number): void + (e: 'updateFOV', value: number): void }>() const animations = ref(props.animations) @@ -73,6 +91,12 @@ const selectedSpeed = ref(1) const selectedAnimation = ref(0) const backgroundColor = ref(props.backgroundColor) const showGrid = ref(props.showGrid) +const showPreview = ref(props.showPreview) +const lightIntensity = ref(props.lightIntensity) +const showLightIntensityButton = ref(props.showLightIntensityButton) +const fov = ref(props.fov) +const showFOVButton = ref(props.showFOVButton) +const showPreviewButton = ref(props.showPreviewButton) const load3dControlsRef = ref(null) const speedOptions = [ @@ -87,13 +111,36 @@ watch(backgroundColor, (newValue) => { load3dControlsRef.value.backgroundColor = newValue }) +watch(showLightIntensityButton, (newValue) => { + load3dControlsRef.value.showLightIntensityButton = newValue +}) + +watch(showFOVButton, (newValue) => { + load3dControlsRef.value.showFOVButton = newValue +}) + +watch(showPreviewButton, (newValue) => { + load3dControlsRef.value.showPreviewButton = newValue +}) + const onToggleCamera = () => { emit('toggleCamera') } const onToggleGrid = (value: boolean) => emit('toggleGrid', value) +const onTogglePreview = (value: boolean) => { + emit('togglePreview', value) +} const onUpdateBackgroundColor = (color: string) => emit('updateBackgroundColor', color) +const onUpdateLightIntensity = (lightIntensity: number) => { + emit('updateLightIntensity', lightIntensity) +} + +const onUpdateFOV = (fov: number) => { + emit('updateFOV', fov) +} + const togglePlay = () => { playing.value = !playing.value emit('togglePlay', playing.value) @@ -112,6 +159,10 @@ defineExpose({ selectedAnimation, playing, backgroundColor, - showGrid + showGrid, + lightIntensity, + showLightIntensityButton, + fov, + showFOVButton }) diff --git a/src/components/load3d/Load3DControls.vue b/src/components/load3d/Load3DControls.vue index a0eb99304..b0fd515e6 100644 --- a/src/components/load3d/Load3DControls.vue +++ b/src/components/load3d/Load3DControls.vue @@ -26,27 +26,100 @@ class="absolute opacity-0 w-0 h-0 p-0 m-0 pointer-events-none" /> + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
diff --git a/src/extensions/core/load3d.ts b/src/extensions/core/load3d.ts index 5ee981d77..90d41206c 100644 --- a/src/extensions/core/load3d.ts +++ b/src/extensions/core/load3d.ts @@ -26,7 +26,7 @@ app.registerExtension({ container.id = `comfy-load-3d-${load3dNode.length}` container.classList.add('comfy-load-3d') - const load3d = new Load3d(container) + const load3d = new Load3d(container, { createPreview: true }) containerToLoad3D.set(container.id, load3d) @@ -130,40 +130,34 @@ app.registerExtension({ const material = node.widgets.find((w: IWidget) => w.name === 'material') - const lightIntensity = node.widgets.find( - (w: IWidget) => w.name === 'light_intensity' - ) - const upDirection = node.widgets.find( (w: IWidget) => w.name === 'up_direction' ) - const fov = node.widgets.find((w: IWidget) => w.name === 'fov') - let cameraState = node.properties['Camera Info'] const config = new Load3DConfiguration(load3d) + const width = node.widgets.find((w: IWidget) => w.name === 'width') + const height = node.widgets.find((w: IWidget) => w.name === 'height') + config.configure( 'input', modelWidget, material, - lightIntensity, upDirection, - fov, - cameraState + cameraState, + width, + height ) - const w = node.widgets.find((w: IWidget) => w.name === 'width') - const h = node.widgets.find((w: IWidget) => w.name === 'height') - // @ts-expect-error hacky override sceneWidget.serializeValue = async () => { node.properties['Camera Info'] = load3d.getCameraState() const { scene: imageData, mask: maskData } = await load3d.captureScene( - w.value, - h.value + width.value, + height.value ) const [data, dataMask] = await Promise.all([ @@ -195,7 +189,7 @@ app.registerExtension({ container.id = `comfy-load-3d-animation-${load3dNode.length}` container.classList.add('comfy-load-3d-animation') - const load3d = new Load3dAnimation(container) + const load3d = new Load3dAnimation(container, { createPreview: true }) containerToLoad3D.set(container.id, load3d) @@ -299,33 +293,27 @@ app.registerExtension({ const material = node.widgets.find((w: IWidget) => w.name === 'material') - const lightIntensity = node.widgets.find( - (w: IWidget) => w.name === 'light_intensity' - ) - const upDirection = node.widgets.find( (w: IWidget) => w.name === 'up_direction' ) - const fov = node.widgets.find((w: IWidget) => w.name === 'fov') - let cameraState = node.properties['Camera Info'] const config = new Load3DConfiguration(load3d) + const width = node.widgets.find((w: IWidget) => w.name === 'width') + const height = node.widgets.find((w: IWidget) => w.name === 'height') + config.configure( 'input', modelWidget, material, - lightIntensity, upDirection, - fov, - cameraState + cameraState, + width, + height ) - const w = node.widgets.find((w: IWidget) => w.name === 'width') - const h = node.widgets.find((w: IWidget) => w.name === 'height') - // @ts-expect-error hacky override sceneWidget.serializeValue = async () => { node.properties['Camera Info'] = load3d.getCameraState() @@ -333,8 +321,8 @@ app.registerExtension({ load3d.toggleAnimation(false) const { scene: imageData, mask: maskData } = await load3d.captureScene( - w.value, - h.value + width.value, + height.value ) const [data, dataMask] = await Promise.all([ @@ -371,7 +359,7 @@ app.registerExtension({ container.id = `comfy-preview-3d-${load3dNode.length}` container.classList.add('comfy-preview-3d') - const load3d = new Load3d(container) + const load3d = new Load3d(container, { createPreview: false }) containerToLoad3D.set(container.id, load3d) @@ -433,16 +421,10 @@ app.registerExtension({ const material = node.widgets.find((w: IWidget) => w.name === 'material') - const lightIntensity = node.widgets.find( - (w: IWidget) => w.name === 'light_intensity' - ) - const upDirection = node.widgets.find( (w: IWidget) => w.name === 'up_direction' ) - const fov = node.widgets.find((w: IWidget) => w.name === 'fov') - const onExecuted = node.onExecuted node.onExecuted = function (message: any) { @@ -462,14 +444,7 @@ app.registerExtension({ const config = new Load3DConfiguration(load3d) - config.configure( - 'output', - modelWidget, - material, - lightIntensity, - upDirection, - fov - ) + config.configure('output', modelWidget, material, upDirection) } } }) @@ -497,7 +472,7 @@ app.registerExtension({ container.id = `comfy-preview-3d-animation-${load3dNode.length}` container.classList.add('comfy-preview-3d-animation') - const load3d = new Load3dAnimation(container) + const load3d = new Load3dAnimation(container, { createPreview: false }) containerToLoad3D.set(container.id, load3d) @@ -563,16 +538,10 @@ app.registerExtension({ const material = node.widgets.find((w: IWidget) => w.name === 'material') - const lightIntensity = node.widgets.find( - (w: IWidget) => w.name === 'light_intensity' - ) - const upDirection = node.widgets.find( (w: IWidget) => w.name === 'up_direction' ) - const fov = node.widgets.find((w: IWidget) => w.name === 'fov') - const onExecuted = node.onExecuted node.onExecuted = function (message: any) { @@ -592,14 +561,7 @@ app.registerExtension({ const config = new Load3DConfiguration(load3d) - config.configure( - 'output', - modelWidget, - material, - lightIntensity, - upDirection, - fov - ) + config.configure('output', modelWidget, material, upDirection) } } }) diff --git a/src/extensions/core/load3d/Load3DConfiguration.ts b/src/extensions/core/load3d/Load3DConfiguration.ts index 089123baf..341e215ee 100644 --- a/src/extensions/core/load3d/Load3DConfiguration.ts +++ b/src/extensions/core/load3d/Load3DConfiguration.ts @@ -11,10 +11,10 @@ class Load3DConfiguration { loadFolder: 'input' | 'output', modelWidget: IWidget, material: IWidget, - lightIntensity: IWidget, upDirection: IWidget, - fov: IWidget, cameraState?: any, + width: IWidget | null = null, + height: IWidget | null = null, postModelUpdateFunc?: (load3d: Load3d) => void ) { this.setupModelHandling( @@ -24,12 +24,25 @@ class Load3DConfiguration { postModelUpdateFunc ) this.setupMaterial(material) - this.setupLighting(lightIntensity) this.setupDirection(upDirection) - this.setupCamera(fov) + this.setupTargetSize(width, height) this.setupDefaultProperties() } + private setupTargetSize(width: IWidget | null, height: IWidget | null) { + if (width && height) { + this.load3d.setTargetSize(width.value as number, height.value as number) + + width.callback = (value: number) => { + this.load3d.setTargetSize(value, height.value as number) + } + + height.callback = (value: number) => { + this.load3d.setTargetSize(width.value as number, value) + } + } + } + private setupModelHandling( modelWidget: IWidget, loadFolder: 'input' | 'output', @@ -56,13 +69,6 @@ class Load3DConfiguration { ) } - private setupLighting(lightIntensity: IWidget) { - lightIntensity.callback = (value: number) => { - this.load3d.setLightIntensity(value) - } - this.load3d.setLightIntensity(lightIntensity.value as number) - } - private setupDirection(upDirection: IWidget) { upDirection.callback = ( value: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z' @@ -74,13 +80,6 @@ class Load3DConfiguration { ) } - private setupCamera(fov: IWidget) { - fov.callback = (value: number) => { - this.load3d.setFOV(value) - } - this.load3d.setFOV(fov.value as number) - } - private setupDefaultProperties() { const cameraType = this.load3d.loadNodeProperty( 'Camera Type', @@ -94,6 +93,14 @@ class Load3DConfiguration { const bgColor = this.load3d.loadNodeProperty('Background Color', '#282828') this.load3d.setBackgroundColor(bgColor) + + const lightIntensity = this.load3d.loadNodeProperty('Light Intensity', '5') + + this.load3d.setLightIntensity(lightIntensity) + + const fov = this.load3d.loadNodeProperty('FOV', '75') + + this.load3d.setFOV(fov) } private createModelUpdateHandler( diff --git a/src/extensions/core/load3d/Load3d.ts b/src/extensions/core/load3d/Load3d.ts index ffa560dc6..eb1be68e6 100644 --- a/src/extensions/core/load3d/Load3d.ts +++ b/src/extensions/core/load3d/Load3d.ts @@ -43,14 +43,21 @@ class Load3d { originalRotation: THREE.Euler | null = null viewHelper: ViewHelper = {} as ViewHelper viewHelperContainer: HTMLDivElement = {} as HTMLDivElement - cameraSwitcherContainer: HTMLDivElement = {} as HTMLDivElement - gridSwitcherContainer: HTMLDivElement = {} as HTMLDivElement + previewRenderer: THREE.WebGLRenderer | null = null + previewCamera: THREE.Camera | null = null + previewContainer: HTMLDivElement = {} as HTMLDivElement + targetWidth: number = 1024 + targetHeight: number = 1024 + showPreview: boolean = true node: LGraphNode = {} as LGraphNode protected controlsApp: App | null = null protected controlsContainer: HTMLDivElement - constructor(container: Element | HTMLElement) { + constructor( + container: Element | HTMLElement, + options: { createPreview?: boolean } = {} + ) { this.scene = new THREE.Scene() this.perspectiveCamera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000) @@ -128,6 +135,10 @@ class Load3d { this.createViewHelper(container) + if (options && options.createPreview) { + this.createCapturePreview(container) + } + this.controlsContainer = document.createElement('div') this.controlsContainer.style.position = 'absolute' this.controlsContainer.style.top = '0' @@ -138,14 +149,14 @@ class Load3d { this.controlsContainer.style.zIndex = '1' container.appendChild(this.controlsContainer) - this.mountControls() + this.mountControls(options) this.handleResize() this.startAnimation() } - protected mountControls() { + protected mountControls(options: { createPreview?: boolean } = {}) { const controlsMount = document.createElement('div') controlsMount.style.pointerEvents = 'auto' this.controlsContainer.appendChild(controlsMount) @@ -153,9 +164,20 @@ class Load3d { this.controlsApp = createApp(Load3DControls, { backgroundColor: '#282828', showGrid: true, + showPreview: options.createPreview, + lightIntensity: 5, + showLightIntensityButton: true, + fov: 75, + showFOVButton: true, + showPreviewButton: options.createPreview, onToggleCamera: () => this.toggleCamera(), onToggleGrid: (show: boolean) => this.toggleGrid(show), - onUpdateBackgroundColor: (color: string) => this.setBackgroundColor(color) + onTogglePreview: (show: boolean) => this.togglePreview(show), + onUpdateBackgroundColor: (color: string) => + this.setBackgroundColor(color), + onUpdateLightIntensity: (lightIntensity: number) => + this.setLightIntensity(lightIntensity), + onUpdateFOV: (fov: number) => this.setFOV(fov) }) this.controlsApp.mount(controlsMount) @@ -182,6 +204,106 @@ class Load3d { return this.node.properties[name] } + createCapturePreview(container: Element | HTMLElement) { + this.previewRenderer = new THREE.WebGLRenderer({ + alpha: true, + antialias: true + }) + this.previewRenderer.setSize(this.targetWidth, this.targetHeight) + this.previewRenderer.setClearColor(0x282828) + + this.previewContainer = document.createElement('div') + this.previewContainer.style.cssText = ` + position: absolute; + right: 0px; + bottom: 0px; + background: rgba(0, 0, 0, 0.2); + display: block; + ` + this.previewContainer.appendChild(this.previewRenderer.domElement) + + this.previewContainer.style.display = this.showPreview ? 'block' : 'none' + + container.appendChild(this.previewContainer) + } + + updatePreviewRender() { + if (!this.previewRenderer || !this.previewContainer || !this.showPreview) + return + + if ( + !this.previewCamera || + (this.activeCamera instanceof THREE.PerspectiveCamera && + !(this.previewCamera instanceof THREE.PerspectiveCamera)) || + (this.activeCamera instanceof THREE.OrthographicCamera && + !(this.previewCamera instanceof THREE.OrthographicCamera)) + ) { + this.previewCamera = this.activeCamera.clone() + } + + this.previewCamera.position.copy(this.activeCamera.position) + this.previewCamera.rotation.copy(this.activeCamera.rotation) + + const aspect = this.targetWidth / this.targetHeight + + if (this.activeCamera instanceof THREE.OrthographicCamera) { + const activeOrtho = this.activeCamera as THREE.OrthographicCamera + const previewOrtho = this.previewCamera as THREE.OrthographicCamera + + const frustumHeight = + (activeOrtho.top - activeOrtho.bottom) / activeOrtho.zoom + + const frustumWidth = frustumHeight * aspect + + previewOrtho.top = frustumHeight / 2 + previewOrtho.left = -frustumWidth / 2 + previewOrtho.right = frustumWidth / 2 + previewOrtho.bottom = -frustumHeight / 2 + previewOrtho.zoom = 1 + + previewOrtho.updateProjectionMatrix() + } else { + ;(this.previewCamera as THREE.PerspectiveCamera).aspect = aspect + ;(this.previewCamera as THREE.PerspectiveCamera).fov = ( + this.activeCamera as THREE.PerspectiveCamera + ).fov + } + + this.previewCamera.lookAt(this.controls.target) + + const previewWidth = 120 + const previewHeight = (previewWidth * this.targetHeight) / this.targetWidth + this.previewRenderer.setSize(previewWidth, previewHeight, false) + this.previewRenderer.render(this.scene, this.previewCamera) + } + + updatePreviewSize() { + if (!this.previewContainer) return + + const previewWidth = 120 + const previewHeight = (previewWidth * this.targetHeight) / this.targetWidth + + this.previewRenderer?.setSize(previewWidth, previewHeight, false) + } + + setTargetSize(width: number, height: number) { + this.targetWidth = width + this.targetHeight = height + this.updatePreviewSize() + if (this.previewRenderer && this.previewCamera) { + if (this.previewCamera instanceof THREE.PerspectiveCamera) { + this.previewCamera.aspect = width / height + this.previewCamera.updateProjectionMatrix() + } else if (this.previewCamera instanceof THREE.OrthographicCamera) { + const frustumSize = 10 + const aspect = width / height + this.previewCamera.left = (-frustumSize * aspect) / 2 + this.previewCamera.right = (frustumSize * aspect) / 2 + this.previewCamera.updateProjectionMatrix() + } + } + } + createViewHelper(container: Element | HTMLElement) { this.viewHelperContainer = document.createElement('div') @@ -215,6 +337,17 @@ class Load3d { this.perspectiveCamera.fov = fov this.perspectiveCamera.updateProjectionMatrix() this.renderer.render(this.scene, this.activeCamera) + + this.storeNodeProperty('FOV', fov) + } + + if ( + this.previewRenderer && + this.previewCamera instanceof THREE.PerspectiveCamera + ) { + this.previewCamera.fov = fov + this.previewCamera.updateProjectionMatrix() + this.previewRenderer.render(this.scene, this.previewCamera) } } @@ -295,6 +428,11 @@ class Load3d { setMaterialMode(mode: 'original' | 'normal' | 'wireframe' | 'depth') { this.materialMode = mode + if (this.controlsApp?._instance?.exposed) { + this.controlsApp._instance.exposed.showLightIntensityButton.value = + mode == 'original' + } + if (this.currentModel) { if (mode === 'depth') { this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace @@ -445,6 +583,11 @@ class Load3d { } } + if (this.previewCamera) { + this.previewCamera = null + } + this.previewCamera = this.activeCamera.clone() + this.activeCamera.position.copy(position) this.activeCamera.rotation.copy(rotation) @@ -463,8 +606,14 @@ class Load3d { ) this.viewHelper.center = this.controls.target + if (this.controlsApp?._instance?.exposed) { + this.controlsApp._instance.exposed.showFOVButton.value = + this.getCurrentCameraType() == 'perspective' + } + this.storeNodeProperty('Camera Type', this.getCurrentCameraType()) this.handleResize() + this.updatePreviewRender() } getCurrentCameraType(): 'perspective' | 'orthographic' { @@ -481,6 +630,16 @@ class Load3d { } } + togglePreview(showPreview: boolean) { + if (this.previewRenderer) { + this.showPreview = showPreview + + this.previewContainer.style.display = this.showPreview ? 'block' : 'none' + + this.storeNodeProperty('Show Preview', showPreview) + } + } + setLightIntensity(intensity: number) { this.lights.forEach((light) => { if (light instanceof THREE.DirectionalLight) { @@ -497,11 +656,18 @@ class Load3d { light.intensity = intensity * 0.5 } }) + + this.storeNodeProperty('Light Intensity', intensity) } startAnimation() { const animate = () => { this.animationFrameId = requestAnimationFrame(animate) + + if (this.showPreview) { + this.updatePreviewRender() + } + const delta = this.clock.getDelta() if (this.viewHelper.animating) { @@ -803,6 +969,7 @@ class Load3d { } this.renderer.setSize(width, height) + this.setTargetSize(this.targetWidth, this.targetHeight) } animate = () => { @@ -818,6 +985,7 @@ class Load3d { ): Promise<{ scene: string; mask: string }> { return new Promise(async (resolve, reject) => { try { + this.updatePreviewSize() const originalWidth = this.renderer.domElement.width const originalHeight = this.renderer.domElement.height const originalClearColor = this.renderer.getClearColor( diff --git a/src/extensions/core/load3d/Load3dAnimation.ts b/src/extensions/core/load3d/Load3dAnimation.ts index cb8a713f4..318bee855 100644 --- a/src/extensions/core/load3d/Load3dAnimation.ts +++ b/src/extensions/core/load3d/Load3dAnimation.ts @@ -14,11 +14,14 @@ class Load3dAnimation extends Load3d { animationSpeed: number = 1.0 - constructor(container: Element | HTMLElement) { - super(container) + constructor( + container: Element | HTMLElement, + options: { createPreview?: boolean } = {} + ) { + super(container, options) } - protected mountControls() { + protected mountControls(options: { createPreview?: boolean } = {}) { const controlsMount = document.createElement('div') controlsMount.style.pointerEvents = 'auto' this.controlsContainer.appendChild(controlsMount) @@ -26,16 +29,26 @@ class Load3dAnimation extends Load3d { this.controlsApp = createApp(Load3DAnimationControls, { backgroundColor: '#282828', showGrid: true, + showPreview: options.createPreview, animations: [], playing: false, + lightIntensity: 5, + showLightIntensityButton: true, + fov: 75, + showFOVButton: true, + showPreviewButton: options.createPreview, onToggleCamera: () => this.toggleCamera(), onToggleGrid: (show: boolean) => this.toggleGrid(show), + onTogglePreview: (show: boolean) => this.togglePreview(show), onUpdateBackgroundColor: (color: string) => this.setBackgroundColor(color), onTogglePlay: (play: boolean) => this.toggleAnimation(play), onSpeedChange: (speed: number) => this.setAnimationSpeed(speed), onAnimationChange: (selectedAnimation: number) => - this.updateSelectedAnimation(selectedAnimation) + this.updateSelectedAnimation(selectedAnimation), + onUpdateLightIntensity: (lightIntensity: number) => + this.setLightIntensity(lightIntensity), + onUpdateFOV: (fov: number) => this.setFOV(fov) }) this.controlsApp.use(PrimeVue) @@ -185,6 +198,11 @@ class Load3dAnimation extends Load3d { startAnimation() { const animate = () => { this.animationFrameId = requestAnimationFrame(animate) + + if (this.showPreview) { + this.updatePreviewRender() + } + const delta = this.clock.getDelta() if (this.currentAnimation && this.isAnimationPlaying) {