diff --git a/README.md b/README.md index 44475fc..0d778b5 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,10 @@ Copy the files inside folder [__New_ComfyUI_Bats](./_New_ComfyUI_Bats/) to your ### Currently support: - For use case please check [example workflows](./_Example_Workflows/) - - **Note:** you need to put [Images_enhanced](_Example_Workflows/_Example_Inputs_Files/Images_enhanced) and [Meshes](./_Example_Workflows/_Example_Inputs_Files/Meshes) under ComfyUI Root Directory\ComfyUI\input folder before you can run the example workflow + - **Note:** you need to put [Example Inputs Files & Folders](_Example_Workflows/_Example_Inputs_Files/) under ComfyUI Root Directory\ComfyUI\input folder before you can run the example workflow +- Stack Orbit Camera Poses node to automatically generate all range of camera pose combinations + - You can use it to conditioning the [StableZero123 (You need to Download the checkpoint first)](https://comfyanonymous.github.io/ComfyUI_examples/3d/), with full range of camera poses in one prompt pass + - You can use it to generate the orbit camera poses and directly input to other 3D process node (e.g. GaussianSplatting and BakeTextureToMesh) - Load 3D file (.obj, .ply, .glb) - 3D Gaussian Splatting, with: - [Improved Differential Gaussian Rasterization](https://github.com/ashawkey/diff-gaussian-rasterization) @@ -80,4 +83,27 @@ Copy the files inside folder [__New_ComfyUI_Bats](./_New_ComfyUI_Bats/) to your - Add interactive 3D UI inside ComfuUI to visulaize training and generated results for 3D representations - Add DMTet algorithm to allow convertion from points cloud(Gaussian/.ply) to mesh (.obj, .ply, .glb) - Add a general SDS/VSD Optimization algorithm to allow training 3D representations with diffusion model, *The real fun begins here* ;) -- Add a few best Nerf algorithms (No idea yet, [instant-ngp](https://github.com/NVlabs/instant-ngp) maybe?) \ No newline at end of file +- Add a few best Nerf algorithms (No idea yet, [instant-ngp](https://github.com/NVlabs/instant-ngp) maybe?) + + +--- + +## Tips +* The world & camera coordinate system is the same as OpenGL: +``` + World Camera + + +y up target + | | / + | | / + |______+x |/______right + / / + / / + / / + +z forward + +elevation: in (-90, 90), from +y to -y is (-90, 90) +azimuth: in (-180, 180), from +z to +x is (0, 90) +``` + +* If you encounter OpenGL errors (e.g., `[F glutil.cpp:338] eglInitialize() failed`), then set `force_cuda_rasterize` to true on corresponding node \ No newline at end of file diff --git a/_Example_Workflows/3DGS_Multi-View_Renders_to_ply_with_Stack_Orbit_Cam_Poses_node.json b/_Example_Workflows/3DGS_Multi-View_Renders_to_ply_with_Stack_Orbit_Cam_Poses_node.json new file mode 100644 index 0000000..96ae54d --- /dev/null +++ b/_Example_Workflows/3DGS_Multi-View_Renders_to_ply_with_Stack_Orbit_Cam_Poses_node.json @@ -0,0 +1,492 @@ +{ + "last_node_id": 22, + "last_link_id": 25, + "nodes": [ + { + "id": 10, + "type": "PreviewImage", + "pos": [ + 1410, + -320 + ], + "size": { + "0": 210, + "1": 246 + }, + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 4 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 9, + "type": "MaskToImage", + "pos": [ + 1110, + -320 + ], + "size": { + "0": 210, + "1": 26 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "mask", + "type": "MASK", + "link": 8 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 4 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "MaskToImage" + } + }, + { + "id": 17, + "type": "[ComfyUI-3D] Save 3DGS", + "pos": [ + 2122, + 54 + ], + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "raw_3DGS", + "type": "GS_RAW", + "link": 21 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Save 3DGS" + }, + "widgets_values": [ + "MeshTest\\3DGS_01.ply" + ] + }, + { + "id": 8, + "type": "InvertMask", + "pos": [ + 746, + -14 + ], + "size": { + "0": 210, + "1": 26 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "mask", + "type": "MASK", + "link": 2 + } + ], + "outputs": [ + { + "name": "MASK", + "type": "MASK", + "links": [ + 8, + 18 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "InvertMask" + } + }, + { + "id": 18, + "type": "[ComfyUI-3D] Gaussian Splatting", + "pos": [ + 1486, + 53 + ], + "size": { + "0": 468.5999755859375, + "1": 718 + }, + "flags": { + "collapsed": false + }, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "reference_images", + "type": "IMAGE", + "link": 17 + }, + { + "name": "reference_masks", + "type": "MASK", + "link": 18 + }, + { + "name": "reference_orbit_camera_poses", + "type": "ORBIT_CAMPOSES", + "link": 25 + }, + { + "name": "mesh_to_initialize_gaussian", + "type": "MESH", + "link": null + } + ], + "outputs": [ + { + "name": "raw_3DGS", + "type": "GS_RAW", + "links": [ + 21 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Gaussian Splatting" + }, + "widgets_values": [ + 49.1, + 1000, + 1, + 10000, + 0.2, + 3, + 0, + 0, + 0.5, + 0.01, + 0.05, + 0.005, + 0.005, + 0.001, + 0.00002, + 0.02, + 500, + 5000, + 3, + 0.01, + 100, + 100000, + 100, + 700, + 0.01, + 0 + ] + }, + { + "id": 7, + "type": "VHS_LoadImages", + "pos": [ + 113, + 54 + ], + "size": [ + 243.60000610351562, + 194 + ], + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 17, + 22 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": [ + 2 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "INT", + "type": "INT", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "VHS_LoadImages" + }, + "widgets_values": { + "directory": "Images_enhanced", + "image_load_cap": 0, + "skip_first_images": 0, + "select_every_nth": 1, + "choose folder to upload": "image", + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "frame_load_cap": 0, + "skip_first_images": 0, + "filename": "Images_enhanced", + "type": "input", + "format": "folder", + "select_every_nth": 1 + } + } + } + }, + { + "id": 19, + "type": "PreviewImage", + "pos": [ + 420, + -320 + ], + "size": { + "0": 210, + "1": 246 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 22 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 13, + "type": "[ComfyUI-3D] Load 3D Mesh", + "pos": [ + 1100, + 315 + ], + "size": { + "0": 315, + "1": 154 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "mesh", + "type": "MESH", + "links": [], + "shape": 3, + "slot_index": 0 + } + ], + "title": "Load 3D Mesh [Optional]", + "properties": { + "Node name for S&R": "[ComfyUI-3D] Load 3D Mesh" + }, + "widgets_values": [ + "Meshes\\Cammy_Uniform.obj", + false, + true, + false, + false + ] + }, + { + "id": 22, + "type": "[ComfyUI-3D] Stack Orbit Camera Poses", + "pos": [ + 726, + 160 + ], + "size": { + "0": 315, + "1": 586 + }, + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "orbit_camposes", + "type": "ORBIT_CAMPOSES", + "links": [ + 25 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "orbit_radius_list", + "type": "FLOAT", + "links": null, + "shape": 6 + }, + { + "name": "elevation_list", + "type": "FLOAT", + "links": null, + "shape": 6 + }, + { + "name": "azimuth_list", + "type": "FLOAT", + "links": null, + "shape": 6 + }, + { + "name": "orbit_center_X_list", + "type": "FLOAT", + "links": null, + "shape": 6 + }, + { + "name": "orbit_center_Y_list", + "type": "FLOAT", + "links": null, + "shape": 6 + }, + { + "name": "orbit_center_Z_list", + "type": "FLOAT", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Stack Orbit Camera Poses" + }, + "widgets_values": [ + 1.75, + 1.75, + 0.1, + 0, + 0, + 0, + 0, + -0.1, + 3, + 0, + 0, + 0.1, + 0, + 0, + 0.1, + 0, + 0, + 0.1 + ] + } + ], + "links": [ + [ + 2, + 7, + 1, + 8, + 0, + "MASK" + ], + [ + 4, + 9, + 0, + 10, + 0, + "IMAGE" + ], + [ + 8, + 8, + 0, + 9, + 0, + "MASK" + ], + [ + 17, + 7, + 0, + 18, + 0, + "IMAGE" + ], + [ + 18, + 8, + 0, + 18, + 1, + "MASK" + ], + [ + 21, + 18, + 0, + 17, + 0, + "GS_RAW" + ], + [ + 22, + 7, + 0, + 19, + 0, + "IMAGE" + ], + [ + 25, + 22, + 0, + 18, + 2, + "ORBIT_CAMPOSES" + ] + ], + "groups": [], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file diff --git a/_Example_Workflows/Using_Stack_Orbit_Camera_Poses_to_conditioning_StableZero123.json b/_Example_Workflows/Using_Stack_Orbit_Camera_Poses_to_conditioning_StableZero123.json new file mode 100644 index 0000000..4077dd6 --- /dev/null +++ b/_Example_Workflows/Using_Stack_Orbit_Camera_Poses_to_conditioning_StableZero123.json @@ -0,0 +1,610 @@ +{ + "last_node_id": 41, + "last_link_id": 80, + "nodes": [ + { + "id": 15, + "type": "ImageOnlyCheckpointLoader", + "pos": [ + -60, + 239 + ], + "size": { + "0": 369.6000061035156, + "1": 98 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 42 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "CLIP_VISION", + "type": "CLIP_VISION", + "links": [ + 50 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 26, + 52 + ], + "shape": 3, + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "ImageOnlyCheckpointLoader" + }, + "widgets_values": [ + "stable_zero123.ckpt" + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + 864, + 344 + ], + "size": { + "0": 315, + "1": 262 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 42 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 53 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 54 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 55 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 7 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 999999999, + "fixed", + 10, + 3, + "euler_ancestral", + "karras", + 1 + ] + }, + { + "id": 26, + "type": "StableZero123_Conditioning", + "pos": [ + 436, + 410 + ], + "size": { + "0": 315, + "1": 194 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "clip_vision", + "type": "CLIP_VISION", + "link": 50 + }, + { + "name": "init_image", + "type": "IMAGE", + "link": 51 + }, + { + "name": "vae", + "type": "VAE", + "link": 52 + }, + { + "name": "elevation", + "type": "FLOAT", + "link": 80, + "widget": { + "name": "elevation" + } + }, + { + "name": "azimuth", + "type": "FLOAT", + "link": 79, + "widget": { + "name": "azimuth" + } + } + ], + "outputs": [ + { + "name": "positive", + "type": "CONDITIONING", + "links": [ + 53 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "negative", + "type": "CONDITIONING", + "links": [ + 54 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "latent", + "type": "LATENT", + "links": [ + 55 + ], + "shape": 3, + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "StableZero123_Conditioning" + }, + "widgets_values": [ + 256, + 256, + 1, + 0, + 145 + ] + }, + { + "id": 23, + "type": "LoadImage", + "pos": [ + -477, + 430 + ], + "size": { + "0": 316.5220947265625, + "1": 405.7098693847656 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 51 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "Cammy_Style_0-NoBG_Square.png", + "image" + ] + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 1280, + 258 + ], + "size": { + "0": 210, + "1": 46 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 7 + }, + { + "name": "vae", + "type": "VAE", + "link": 26 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 49, + 74 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + }, + { + "id": 39, + "type": "ImageListToImageBatch", + "pos": [ + 1584, + 491 + ], + "size": { + "0": 210, + "1": 26 + }, + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 74 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 75 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ImageListToImageBatch" + } + }, + { + "id": 38, + "type": "VHS_VideoCombine", + "pos": [ + 1831, + 491 + ], + "size": [ + 315, + 559 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 75 + }, + { + "name": "audio", + "type": "VHS_AUDIO", + "link": null + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "VHS_VideoCombine" + }, + "widgets_values": { + "frame_rate": 8, + "loop_count": 0, + "filename_prefix": "StableZero123/Test/Cammy", + "format": "video/h265-mp4", + "pix_fmt": "yuv420p10le", + "crf": 22, + "save_metadata": true, + "pingpong": false, + "save_output": true, + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "Cammy_00492.mp4", + "subfolder": "StableZero123\\Test", + "type": "output", + "format": "video/h265-mp4" + } + } + } + }, + { + "id": 25, + "type": "SaveImage", + "pos": [ + 1582, + 121 + ], + "size": { + "0": 671.7539672851562, + "1": 270.0038757324219 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 49 + } + ], + "properties": {}, + "widgets_values": [ + "StableZero123/Test/Cammy" + ] + }, + { + "id": 41, + "type": "[ComfyUI-3D] Stack Orbit Camera Poses", + "pos": [ + -10, + 540 + ], + "size": { + "0": 315, + "1": 586 + }, + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "orbit_camposes", + "type": "ORBIT_CAMPOSES", + "links": null, + "shape": 3 + }, + { + "name": "orbit_radius_list", + "type": "FLOAT", + "links": null, + "shape": 6 + }, + { + "name": "elevation_list", + "type": "FLOAT", + "links": [ + 80 + ], + "shape": 6, + "slot_index": 2 + }, + { + "name": "azimuth_list", + "type": "FLOAT", + "links": [ + 79 + ], + "shape": 6, + "slot_index": 3 + }, + { + "name": "orbit_center_X_list", + "type": "FLOAT", + "links": null, + "shape": 6 + }, + { + "name": "orbit_center_Y_list", + "type": "FLOAT", + "links": null, + "shape": 6 + }, + { + "name": "orbit_center_Z_list", + "type": "FLOAT", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Stack Orbit Camera Poses" + }, + "widgets_values": [ + 0, + 0, + 0.1, + -45, + 45, + 45, + 0, + 0.1, + -45, + 0, + 0, + 0.1, + 0, + 0, + 0.1, + 0, + 0, + 0.1 + ] + } + ], + "links": [ + [ + 7, + 3, + 0, + 8, + 0, + "LATENT" + ], + [ + 26, + 15, + 2, + 8, + 1, + "VAE" + ], + [ + 42, + 15, + 0, + 3, + 0, + "MODEL" + ], + [ + 49, + 8, + 0, + 25, + 0, + "IMAGE" + ], + [ + 50, + 15, + 1, + 26, + 0, + "CLIP_VISION" + ], + [ + 51, + 23, + 0, + 26, + 1, + "IMAGE" + ], + [ + 52, + 15, + 2, + 26, + 2, + "VAE" + ], + [ + 53, + 26, + 0, + 3, + 1, + "CONDITIONING" + ], + [ + 54, + 26, + 1, + 3, + 2, + "CONDITIONING" + ], + [ + 55, + 26, + 2, + 3, + 3, + "LATENT" + ], + [ + 74, + 8, + 0, + 39, + 0, + "IMAGE" + ], + [ + 75, + 39, + 0, + 38, + 0, + "IMAGE" + ], + [ + 79, + 41, + 3, + 26, + 4, + "FLOAT" + ], + [ + 80, + 41, + 2, + 26, + 3, + "FLOAT" + ] + ], + "groups": [], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file diff --git a/_Example_Workflows/_Example_Inputs_Files/Cammy_Style_0-NoBG_Square.png b/_Example_Workflows/_Example_Inputs_Files/Cammy_Style_0-NoBG_Square.png new file mode 100644 index 0000000..3e495fc Binary files /dev/null and b/_Example_Workflows/_Example_Inputs_Files/Cammy_Style_0-NoBG_Square.png differ diff --git a/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Clockwise.mp4 b/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Clockwise.mp4 new file mode 100644 index 0000000..8f34b68 Binary files /dev/null and b/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Clockwise.mp4 differ diff --git a/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Clockwise_Camposes.png b/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Clockwise_Camposes.png new file mode 100644 index 0000000..12d2529 Binary files /dev/null and b/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Clockwise_Camposes.png differ diff --git a/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Counter_Clockwise.mp4 b/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Counter_Clockwise.mp4 new file mode 100644 index 0000000..3fd947d Binary files /dev/null and b/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Counter_Clockwise.mp4 differ diff --git a/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Counter_Clockwise_Camposes.png b/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Counter_Clockwise_Camposes.png new file mode 100644 index 0000000..24fa726 Binary files /dev/null and b/_Example_Workflows/_Example_Outputs/Cammy_Cam_Rotate_Counter_Clockwise_Camposes.png differ diff --git a/nodes.py b/nodes.py index a4e68d2..4a08b4c 100644 --- a/nodes.py +++ b/nodes.py @@ -1,8 +1,12 @@ import os import re +import math +import copy +from enum import Enum import folder_paths as comfy_paths import torch +import numpy as np from .diff_rast.diff_texturing import DiffTextureBaker from .shared_utils.common_utils import cstr @@ -23,6 +27,11 @@ SUPPORTED_3D_EXTENSIONS = ( '.glb', ) +ELEVATION_MIN = -90 +ELEVATION_MAX = 90.0 +AZIMUTH_MIN = -180.0 +AZIMUTH_MAX = 180.0 + class Load_3D_Mesh: @classmethod @@ -127,6 +136,194 @@ class Save_3DGS: return () +class Stack_Orbit_Camera_Poses: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "orbit_radius_start": ("FLOAT", {"default": 0.0, 'step': 0.0001}), + "orbit_radius_stop": ("FLOAT", {"default": 0.0, 'step': 0.0001}), + "orbit_radius_step": ("FLOAT", {"default": 0.1, 'step': 0.0001}), + "elevation_start": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, 'step': 0.0001}), + "elevation_stop": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, 'step': 0.0001}), + "elevation_step": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, 'step': 0.0001}), + "azimuth_start": ("FLOAT", {"default": 0.0, "min": AZIMUTH_MIN, "max": AZIMUTH_MAX, 'step': 0.0001}), + "azimuth_stop": ("FLOAT", {"default": 0.0, "min": AZIMUTH_MIN, "max": AZIMUTH_MAX, 'step': 0.0001}), + "azimuth_step": ("FLOAT", {"default": 0.0, "min": AZIMUTH_MIN, "max": AZIMUTH_MAX, 'step': 0.0001}), + "orbit_center_X_start": ("FLOAT", {"default": 0.0, 'step': 0.0001}), + "orbit_center_X_stop": ("FLOAT", {"default": 0.0, 'step': 0.0001}), + "orbit_center_X_step": ("FLOAT", {"default": 0.1, 'step': 0.0001}), + "orbit_center_Y_start": ("FLOAT", {"default": 0.0, 'step': 0.0001}), + "orbit_center_Y_stop": ("FLOAT", {"default": 0.0, 'step': 0.0001}), + "orbit_center_Y_step": ("FLOAT", {"default": 0.1, 'step': 0.0001}), + "orbit_center_Z_start": ("FLOAT", {"default": 0.0, 'step': 0.0001}), + "orbit_center_Z_stop": ("FLOAT", {"default": 0.0, 'step': 0.0001}), + "orbit_center_Z_step": ("FLOAT", {"default": 0.1, 'step': 0.0001}), + }, + } + + RETURN_TYPES = ( + "ORBIT_CAMPOSES", # (orbit radius, elevation, azimuth, orbit center X, orbit center Y, orbit center Z) + "FLOAT", + "FLOAT", + "FLOAT", + "FLOAT", + "FLOAT", + "FLOAT", + ) + RETURN_NAMES = ( + "orbit_camposes", + "orbit_radius_list", + "elevation_list", + "azimuth_list", + "orbit_center_X_list", + "orbit_center_Y_list", + "orbit_center_Z_list", + ) + OUTPUT_IS_LIST = ( + False, + True, + True, + True, + True, + True, + True, + ) + + FUNCTION = "get_camposes" + CATEGORY = "ComfyUI3D/Preprocessor" + + class Pose_Config(Enum): + STOP_LARGER_STEP_POS = 0 + START_LARGER_STEP_POS = 1 + START_LARGER_STEP_NEG = 2 + STOP_LARGER_STEP_NEG = 3 + + class Pose_Type: + def __init__(self, start, stop, step, min_value=-math.inf, max_value=math.inf, is_linear = True): + if abs(step) < 0.0001: + step = 0.0001 * (-1.0 if step < 0 else 1.0) + + if is_linear and ( (step > 0 and stop < start) or (step < 0 and stop > start)): + cstr(f"[{self.__class__.__name__}] stop value: {stop} cannot be reached from start value {start} with step value {step}, will reverse the sign of step value to {-step}").warning.print() + self.step = -step + else: + self.step = step + + self.start = start + self.stop = stop + + self.min = min_value + self.max = max_value + + self.is_linear = is_linear # linear or circular (i.e. min and max value are connected, e.g. -180 & 180 degree in azimuth angle) value + + + def stack_camposes(self, pose_type_index=None, last_camposes=[[]]): + if pose_type_index == None: + pose_type_index = len(self.all_pose_types) - 1 + + if pose_type_index == -1: + return last_camposes + else: + current_pose_type = self.all_pose_types[pose_type_index] + + all_camposes = [] + + # There are four different kind of situation we need to deal with to make this function generalize for any combination of inputs + if current_pose_type.step > 0: + if current_pose_type.start < current_pose_type.stop or current_pose_type.is_linear: + pose_config = Stack_Orbit_Camera_Poses.Pose_Config.STOP_LARGER_STEP_POS + else: + pose_config = Stack_Orbit_Camera_Poses.Pose_Config.START_LARGER_STEP_POS + else: + if current_pose_type.start > current_pose_type.stop or current_pose_type.is_linear: + pose_config = Stack_Orbit_Camera_Poses.Pose_Config.START_LARGER_STEP_NEG + else: + pose_config = Stack_Orbit_Camera_Poses.Pose_Config.STOP_LARGER_STEP_NEG + + p = current_pose_type.start + p_passed_min_max_seam = False + + while ( (pose_config == Stack_Orbit_Camera_Poses.Pose_Config.STOP_LARGER_STEP_POS and p <= current_pose_type.stop) or + (pose_config == Stack_Orbit_Camera_Poses.Pose_Config.START_LARGER_STEP_POS and (not p_passed_min_max_seam or p <= current_pose_type.stop)) or + (pose_config == Stack_Orbit_Camera_Poses.Pose_Config.START_LARGER_STEP_NEG and p >= current_pose_type.stop) or + (pose_config == Stack_Orbit_Camera_Poses.Pose_Config.STOP_LARGER_STEP_NEG and (not p_passed_min_max_seam or p >= current_pose_type.stop)) ): + + # If current pose value surpass the either min/max value then we map its vaule to the oppsite sign + if pose_config == Stack_Orbit_Camera_Poses.Pose_Config.START_LARGER_STEP_POS and p > current_pose_type.max: + p = current_pose_type.min + p % current_pose_type.max + p_passed_min_max_seam = True + elif pose_config == Stack_Orbit_Camera_Poses.Pose_Config.STOP_LARGER_STEP_NEG and p < current_pose_type.min: + p = current_pose_type.max + p % current_pose_type.min + p_passed_min_max_seam = True + + new_camposes = copy.deepcopy(last_camposes) + + for campose in new_camposes: + campose.insert(0, p) + + all_camposes.extend(new_camposes) + + p += current_pose_type.step + + return self.stack_camposes(pose_type_index-1, all_camposes) + + def get_camposes(self, + orbit_radius_start, + orbit_radius_stop, + orbit_radius_step, + elevation_start, + elevation_stop, + elevation_step, + azimuth_start, + azimuth_stop, + azimuth_step, + orbit_center_X_start, + orbit_center_X_stop, + orbit_center_X_step, + orbit_center_Y_start, + orbit_center_Y_stop, + orbit_center_Y_step, + orbit_center_Z_start, + orbit_center_Z_stop, + orbit_center_Z_step): + + """ + Return the combination of all the pose types interpolation values + Return values in two ways: + orbit_camposes: CAMPOSES type list can directly input to other 3D process node (e.g. GaussianSplatting) + all the camera pose types seperated in different list, becasue some 3D model's conditioner only takes a sub set of all camera pose types (e.g. StableZero123) + """ + + orbit_radius_list = [] + elevation_list = [] + azimuth_list = [] + orbit_center_X_list = [] + orbit_center_Y_list = [] + orbit_center_Z_list = [] + + self.all_pose_types = [] + self.all_pose_types.append( Stack_Orbit_Camera_Poses.Pose_Type(orbit_radius_start, orbit_radius_stop, orbit_radius_step) ) + self.all_pose_types.append( Stack_Orbit_Camera_Poses.Pose_Type(elevation_start, elevation_stop, elevation_step, ELEVATION_MIN, ELEVATION_MAX) ) + self.all_pose_types.append( Stack_Orbit_Camera_Poses.Pose_Type(azimuth_start, azimuth_stop, azimuth_step, AZIMUTH_MIN, AZIMUTH_MAX, False) ) + self.all_pose_types.append( Stack_Orbit_Camera_Poses.Pose_Type(orbit_center_X_start, orbit_center_X_stop, orbit_center_X_step) ) + self.all_pose_types.append( Stack_Orbit_Camera_Poses.Pose_Type(orbit_center_Y_start, orbit_center_Y_stop, orbit_center_Y_step) ) + self.all_pose_types.append( Stack_Orbit_Camera_Poses.Pose_Type(orbit_center_Z_start, orbit_center_Z_stop, orbit_center_Z_step) ) + + orbit_camposes = self.stack_camposes() + + for campose in orbit_camposes: + orbit_radius_list.append(campose[0]) + elevation_list.append(campose[1]) + azimuth_list.append(campose[2]) + orbit_center_X_list.append(campose[3]) + orbit_center_Y_list.append(campose[4]) + orbit_center_Z_list.append(campose[5]) + + return (orbit_camposes, orbit_radius_list, elevation_list, azimuth_list, orbit_center_X_list, orbit_center_Y_list, orbit_center_Z_list, ) + class Generate_Orbit_Camera_Poses: @classmethod