mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
[3d] initial version of 3d viewer (#3968)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { nextTick } from 'vue'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
import Load3DAnimation from '@/components/load3d/Load3DAnimation.vue'
|
||||
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
@@ -9,10 +10,13 @@ import { t } from '@/i18n'
|
||||
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { api } from '@/scripts/api'
|
||||
import { ComfyApp, app } from '@/scripts/app'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLoad3dService } from '@/services/load3dService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { isLoad3dNode } from '@/utils/litegraphUtil'
|
||||
|
||||
async function handleModelUpload(files: FileList, node: any) {
|
||||
if (!files?.length) return
|
||||
@@ -174,6 +178,51 @@ useExtensionService().registerExtension({
|
||||
},
|
||||
defaultValue: 0.5,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Load3D.3DViewerEnable',
|
||||
category: ['3D', '3DViewer', 'Enable'],
|
||||
name: 'Enable 3D Viewer (Beta)',
|
||||
tooltip:
|
||||
'Enables the 3D Viewer (Beta) for selected nodes. This feature allows you to visualize and interact with 3D models directly within the full size 3d viewer.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
experimental: true
|
||||
}
|
||||
],
|
||||
commands: [
|
||||
{
|
||||
id: 'Comfy.3DViewer.Open3DViewer',
|
||||
icon: 'pi pi-pencil',
|
||||
label: 'Open 3D Viewer (Beta) for Selected Node',
|
||||
function: () => {
|
||||
const selectedNodes = app.canvas.selected_nodes
|
||||
if (!selectedNodes || Object.keys(selectedNodes).length !== 1) return
|
||||
|
||||
const selectedNode = selectedNodes[Object.keys(selectedNodes)[0]]
|
||||
|
||||
if (!isLoad3dNode(selectedNode)) return
|
||||
|
||||
ComfyApp.copyToClipspace(selectedNode)
|
||||
// @ts-expect-error clipspace_return_node is an extension property added at runtime
|
||||
ComfyApp.clipspace_return_node = selectedNode
|
||||
|
||||
const props = { node: selectedNode }
|
||||
|
||||
useDialogStore().showDialog({
|
||||
key: 'global-load3d-viewer',
|
||||
title: t('load3d.viewer.title'),
|
||||
component: Load3DViewerContent,
|
||||
props: props,
|
||||
dialogComponentProps: {
|
||||
style: 'width: 80vw; height: 80vh;',
|
||||
maximizable: true,
|
||||
onClose: async () => {
|
||||
await useLoad3dService().handleViewerClose(props.node)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
getCustomWidgets() {
|
||||
|
||||
@@ -179,12 +179,16 @@ export class CameraManager implements CameraManagerInterface {
|
||||
}
|
||||
|
||||
handleResize(width: number, height: number): void {
|
||||
const aspect = width / height
|
||||
this.updateAspectRatio(aspect)
|
||||
}
|
||||
|
||||
updateAspectRatio(aspect: number): void {
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.aspect = width / height
|
||||
this.perspectiveCamera.aspect = aspect
|
||||
this.perspectiveCamera.updateProjectionMatrix()
|
||||
} else {
|
||||
const frustumSize = 10
|
||||
const aspect = width / height
|
||||
this.orthographicCamera.left = (-frustumSize * aspect) / 2
|
||||
this.orthographicCamera.right = (frustumSize * aspect) / 2
|
||||
this.orthographicCamera.top = frustumSize / 2
|
||||
|
||||
@@ -9,11 +9,11 @@ import { EventManager } from './EventManager'
|
||||
import { LightingManager } from './LightingManager'
|
||||
import { LoaderManager } from './LoaderManager'
|
||||
import { ModelExporter } from './ModelExporter'
|
||||
import { ModelManager } from './ModelManager'
|
||||
import { NodeStorage } from './NodeStorage'
|
||||
import { PreviewManager } from './PreviewManager'
|
||||
import { RecordingManager } from './RecordingManager'
|
||||
import { SceneManager } from './SceneManager'
|
||||
import { SceneModelManager } from './SceneModelManager'
|
||||
import { ViewHelperManager } from './ViewHelperManager'
|
||||
import {
|
||||
CameraState,
|
||||
@@ -29,22 +29,28 @@ class Load3d {
|
||||
protected animationFrameId: number | null = null
|
||||
node: LGraphNode
|
||||
|
||||
protected eventManager: EventManager
|
||||
protected nodeStorage: NodeStorage
|
||||
protected sceneManager: SceneManager
|
||||
protected cameraManager: CameraManager
|
||||
protected controlsManager: ControlsManager
|
||||
protected lightingManager: LightingManager
|
||||
protected viewHelperManager: ViewHelperManager
|
||||
protected previewManager: PreviewManager
|
||||
protected loaderManager: LoaderManager
|
||||
protected modelManager: ModelManager
|
||||
protected recordingManager: RecordingManager
|
||||
eventManager: EventManager
|
||||
nodeStorage: NodeStorage
|
||||
sceneManager: SceneManager
|
||||
cameraManager: CameraManager
|
||||
controlsManager: ControlsManager
|
||||
lightingManager: LightingManager
|
||||
viewHelperManager: ViewHelperManager
|
||||
previewManager: PreviewManager
|
||||
loaderManager: LoaderManager
|
||||
modelManager: SceneModelManager
|
||||
recordingManager: RecordingManager
|
||||
|
||||
STATUS_MOUSE_ON_NODE: boolean
|
||||
STATUS_MOUSE_ON_SCENE: boolean
|
||||
STATUS_MOUSE_ON_VIEWER: boolean
|
||||
INITIAL_RENDER_DONE: boolean = false
|
||||
|
||||
targetWidth: number = 512
|
||||
targetHeight: number = 512
|
||||
targetAspectRatio: number = 1
|
||||
isViewerMode: boolean = false
|
||||
|
||||
constructor(
|
||||
container: Element | HTMLElement,
|
||||
options: Load3DOptions = {
|
||||
@@ -54,6 +60,16 @@ class Load3d {
|
||||
) {
|
||||
this.node = options.node || ({} as LGraphNode)
|
||||
this.clock = new THREE.Clock()
|
||||
this.isViewerMode = options.isViewerMode || false
|
||||
|
||||
const widthWidget = this.node.widgets?.find((w) => w.name === 'width')
|
||||
const heightWidget = this.node.widgets?.find((w) => w.name === 'height')
|
||||
|
||||
if (widthWidget && heightWidget) {
|
||||
this.targetWidth = widthWidget.value as number
|
||||
this.targetHeight = heightWidget.value as number
|
||||
this.targetAspectRatio = this.targetWidth / this.targetHeight
|
||||
}
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
|
||||
this.renderer.setSize(300, 300)
|
||||
@@ -109,7 +125,11 @@ class Load3d {
|
||||
this.sceneManager.backgroundCamera
|
||||
)
|
||||
|
||||
this.modelManager = new ModelManager(
|
||||
if (options.disablePreview) {
|
||||
this.previewManager.togglePreview(false)
|
||||
}
|
||||
|
||||
this.modelManager = new SceneModelManager(
|
||||
this.sceneManager.scene,
|
||||
this.renderer,
|
||||
this.eventManager,
|
||||
@@ -142,6 +162,7 @@ class Load3d {
|
||||
|
||||
this.STATUS_MOUSE_ON_NODE = false
|
||||
this.STATUS_MOUSE_ON_SCENE = false
|
||||
this.STATUS_MOUSE_ON_VIEWER = false
|
||||
|
||||
this.handleResize()
|
||||
this.startAnimation()
|
||||
@@ -151,6 +172,41 @@ class Load3d {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
getEventManager(): EventManager {
|
||||
return this.eventManager
|
||||
}
|
||||
|
||||
getNodeStorage(): NodeStorage {
|
||||
return this.nodeStorage
|
||||
}
|
||||
getSceneManager(): SceneManager {
|
||||
return this.sceneManager
|
||||
}
|
||||
getCameraManager(): CameraManager {
|
||||
return this.cameraManager
|
||||
}
|
||||
getControlsManager(): ControlsManager {
|
||||
return this.controlsManager
|
||||
}
|
||||
getLightingManager(): LightingManager {
|
||||
return this.lightingManager
|
||||
}
|
||||
getViewHelperManager(): ViewHelperManager {
|
||||
return this.viewHelperManager
|
||||
}
|
||||
getPreviewManager(): PreviewManager {
|
||||
return this.previewManager
|
||||
}
|
||||
getLoaderManager(): LoaderManager {
|
||||
return this.loaderManager
|
||||
}
|
||||
getModelManager(): SceneModelManager {
|
||||
return this.modelManager
|
||||
}
|
||||
getRecordingManager(): RecordingManager {
|
||||
return this.recordingManager
|
||||
}
|
||||
|
||||
forceRender(): void {
|
||||
const delta = this.clock.getDelta()
|
||||
this.viewHelperManager.update(delta)
|
||||
@@ -172,12 +228,43 @@ class Load3d {
|
||||
}
|
||||
|
||||
renderMainScene(): void {
|
||||
const width = this.renderer.domElement.clientWidth
|
||||
const height = this.renderer.domElement.clientHeight
|
||||
const containerWidth = this.renderer.domElement.clientWidth
|
||||
const containerHeight = this.renderer.domElement.clientHeight
|
||||
|
||||
this.renderer.setViewport(0, 0, width, height)
|
||||
this.renderer.setScissor(0, 0, width, height)
|
||||
this.renderer.setScissorTest(true)
|
||||
if (this.isViewerMode) {
|
||||
const containerAspectRatio = containerWidth / containerHeight
|
||||
|
||||
let renderWidth: number
|
||||
let renderHeight: number
|
||||
let offsetX: number = 0
|
||||
let offsetY: number = 0
|
||||
|
||||
if (containerAspectRatio > this.targetAspectRatio) {
|
||||
renderHeight = containerHeight
|
||||
renderWidth = renderHeight * this.targetAspectRatio
|
||||
offsetX = (containerWidth - renderWidth) / 2
|
||||
} else {
|
||||
renderWidth = containerWidth
|
||||
renderHeight = renderWidth / this.targetAspectRatio
|
||||
offsetY = (containerHeight - renderHeight) / 2
|
||||
}
|
||||
|
||||
this.renderer.setViewport(0, 0, containerWidth, containerHeight)
|
||||
this.renderer.setScissor(0, 0, containerWidth, containerHeight)
|
||||
this.renderer.setScissorTest(true)
|
||||
this.renderer.setClearColor(0x0a0a0a)
|
||||
this.renderer.clear()
|
||||
|
||||
this.renderer.setViewport(offsetX, offsetY, renderWidth, renderHeight)
|
||||
this.renderer.setScissor(offsetX, offsetY, renderWidth, renderHeight)
|
||||
|
||||
const renderAspectRatio = renderWidth / renderHeight
|
||||
this.cameraManager.updateAspectRatio(renderAspectRatio)
|
||||
} else {
|
||||
this.renderer.setViewport(0, 0, containerWidth, containerHeight)
|
||||
this.renderer.setScissor(0, 0, containerWidth, containerHeight)
|
||||
this.renderer.setScissorTest(true)
|
||||
}
|
||||
|
||||
this.sceneManager.renderBackground()
|
||||
this.renderer.render(
|
||||
@@ -243,10 +330,15 @@ class Load3d {
|
||||
this.STATUS_MOUSE_ON_SCENE = onScene
|
||||
}
|
||||
|
||||
updateStatusMouseOnViewer(onViewer: boolean): void {
|
||||
this.STATUS_MOUSE_ON_VIEWER = onViewer
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return (
|
||||
this.STATUS_MOUSE_ON_NODE ||
|
||||
this.STATUS_MOUSE_ON_SCENE ||
|
||||
this.STATUS_MOUSE_ON_VIEWER ||
|
||||
this.isRecording() ||
|
||||
!this.INITIAL_RENDER_DONE
|
||||
)
|
||||
@@ -308,6 +400,34 @@ class Load3d {
|
||||
this.sceneManager.backgroundTexture
|
||||
)
|
||||
|
||||
if (
|
||||
this.isViewerMode &&
|
||||
this.sceneManager.backgroundTexture &&
|
||||
this.sceneManager.backgroundMesh
|
||||
) {
|
||||
const containerWidth = this.renderer.domElement.clientWidth
|
||||
const containerHeight = this.renderer.domElement.clientHeight
|
||||
const containerAspectRatio = containerWidth / containerHeight
|
||||
|
||||
let renderWidth: number
|
||||
let renderHeight: number
|
||||
|
||||
if (containerAspectRatio > this.targetAspectRatio) {
|
||||
renderHeight = containerHeight
|
||||
renderWidth = renderHeight * this.targetAspectRatio
|
||||
} else {
|
||||
renderWidth = containerWidth
|
||||
renderHeight = renderWidth / this.targetAspectRatio
|
||||
}
|
||||
|
||||
this.sceneManager.updateBackgroundSize(
|
||||
this.sceneManager.backgroundTexture,
|
||||
this.sceneManager.backgroundMesh,
|
||||
renderWidth,
|
||||
renderHeight
|
||||
)
|
||||
}
|
||||
|
||||
this.forceRender()
|
||||
}
|
||||
|
||||
@@ -340,6 +460,10 @@ class Load3d {
|
||||
return this.cameraManager.getCurrentCameraType()
|
||||
}
|
||||
|
||||
getCurrentModel(): THREE.Object3D | null {
|
||||
return this.modelManager.currentModel
|
||||
}
|
||||
|
||||
setCameraState(state: CameraState): void {
|
||||
this.cameraManager.setCameraState(state)
|
||||
|
||||
@@ -397,6 +521,9 @@ class Load3d {
|
||||
}
|
||||
|
||||
setTargetSize(width: number, height: number): void {
|
||||
this.targetWidth = width
|
||||
this.targetHeight = height
|
||||
this.targetAspectRatio = width / height
|
||||
this.previewManager.setTargetSize(width, height)
|
||||
this.forceRender()
|
||||
}
|
||||
@@ -422,13 +549,30 @@ class Load3d {
|
||||
return
|
||||
}
|
||||
|
||||
const width = parentElement.clientWidth
|
||||
const height = parentElement.clientHeight
|
||||
const containerWidth = parentElement.clientWidth
|
||||
const containerHeight = parentElement.clientHeight
|
||||
|
||||
this.cameraManager.handleResize(width, height)
|
||||
this.sceneManager.handleResize(width, height)
|
||||
if (this.isViewerMode) {
|
||||
const containerAspectRatio = containerWidth / containerHeight
|
||||
let renderWidth: number
|
||||
let renderHeight: number
|
||||
|
||||
this.renderer.setSize(width, height)
|
||||
if (containerAspectRatio > this.targetAspectRatio) {
|
||||
renderHeight = containerHeight
|
||||
renderWidth = renderHeight * this.targetAspectRatio
|
||||
} else {
|
||||
renderWidth = containerWidth
|
||||
renderHeight = renderWidth / this.targetAspectRatio
|
||||
}
|
||||
|
||||
this.cameraManager.handleResize(renderWidth, renderHeight)
|
||||
this.sceneManager.handleResize(renderWidth, renderHeight)
|
||||
} else {
|
||||
this.cameraManager.handleResize(containerWidth, containerHeight)
|
||||
this.sceneManager.handleResize(containerWidth, containerHeight)
|
||||
}
|
||||
|
||||
this.renderer.setSize(containerWidth, containerHeight)
|
||||
|
||||
this.previewManager.handleResize()
|
||||
this.forceRender()
|
||||
|
||||
@@ -27,10 +27,6 @@ class Load3dAnimation extends Load3d {
|
||||
this.overrideAnimationLoop()
|
||||
}
|
||||
|
||||
private getCurrentModel(): THREE.Object3D | null {
|
||||
return this.modelManager.currentModel
|
||||
}
|
||||
|
||||
private overrideAnimationLoop(): void {
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId)
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
UpDirection
|
||||
} from './interfaces'
|
||||
|
||||
export class ModelManager implements ModelManagerInterface {
|
||||
export class SceneModelManager implements ModelManagerInterface {
|
||||
currentModel: THREE.Object3D | null = null
|
||||
originalModel:
|
||||
| THREE.Object3D
|
||||
@@ -663,6 +663,12 @@ export class ModelManager implements ModelManagerInterface {
|
||||
this.originalMaterials = new WeakMap()
|
||||
}
|
||||
|
||||
addModelToScene(model: THREE.Object3D): void {
|
||||
this.currentModel = model
|
||||
|
||||
this.scene.add(this.currentModel)
|
||||
}
|
||||
|
||||
async setupModel(model: THREE.Object3D): Promise<void> {
|
||||
this.currentModel = model
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface EventCallback {
|
||||
export interface Load3DOptions {
|
||||
node?: LGraphNode
|
||||
inputSpec?: CustomInputSpec
|
||||
disablePreview?: boolean
|
||||
isViewerMode?: boolean
|
||||
}
|
||||
|
||||
export interface CaptureResult {
|
||||
@@ -159,6 +161,7 @@ export interface ModelManagerInterface {
|
||||
clearModel(): void
|
||||
reset(): void
|
||||
setupModel(model: THREE.Object3D): Promise<void>
|
||||
addModelToScene(model: THREE.Object3D): void
|
||||
setOriginalModel(model: THREE.Object3D | THREE.BufferGeometry | GLTF): void
|
||||
setUpDirection(direction: UpDirection): void
|
||||
materialMode: MaterialMode
|
||||
|
||||
Reference in New Issue
Block a user