diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index 58e49d8e2..11e653839 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -847,6 +847,16 @@ class Load3DAnimation(Load3D): ... +@comfytype(io_type="CAMERA_INFO_STATE") +class CameraInfoState(ComfyTypeI): + Type = dict + + class Input(WidgetInput): + def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, + socketless: bool=True, advanced: bool=None): + super().__init__(id, display_name, optional, tooltip, None, None, socketless, None, None, None, None, advanced) + + @comfytype(io_type="PHOTOMAKER") class Photomaker(ComfyTypeIO): Type = Any @@ -2403,6 +2413,7 @@ __all__ = [ "Load3DModelInfo", "Load3D", "Load3DAnimation", + "CameraInfoState", "Photomaker", "Point", "FaceAnalysis", diff --git a/comfy_extras/nodes_camera.py b/comfy_extras/nodes_camera.py new file mode 100644 index 000000000..5a3cde9f3 --- /dev/null +++ b/comfy_extras/nodes_camera.py @@ -0,0 +1,91 @@ +import math + +from typing_extensions import override + +import comfy.model_management +from comfy_api.latest import ComfyExtension, IO +from comfy_extras.nodes_gaussian_splat import _lookat_camera_info, _quat_camera_info + + +class CreateCameraInfo(IO.ComfyNode): + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="CreateCameraInfo", + display_name="Create Camera Info", + search_aliases=["camera position", "make camera info", "orbit camera", "look at camera"], + category="3d", + description="Build a camera_info" + "Mode 'orbit' aims with yaw/pitch/distance around the target; " + "'look_at' places the camera at world position. Coordinates are the viewer's world space (right-handed,Y-up).", + inputs=[ + IO.DynamicCombo.Input("mode", options=[ + IO.DynamicCombo.Option("orbit", [ + IO.Float.Input("yaw", default=35.0, min=-360.0, max=360.0, step=1.0), + IO.Float.Input("pitch", default=30.0, min=-89.0, max=89.0, step=1.0), + IO.Float.Input("distance", default=4.0, min=0.01, max=1000.0, step=0.01, + tooltip="Camera distance from the target."), + ]), + IO.DynamicCombo.Option("look_at", [ + IO.Float.Input("position_x", default=4.0, min=-1000.0, max=1000.0, step=0.01, + tooltip="Camera position in world space (right-handed, Y-up)."), + IO.Float.Input("position_y", default=4.0, min=-1000.0, max=1000.0, step=0.01), + IO.Float.Input("position_z", default=4.0, min=-1000.0, max=1000.0, step=0.01), + ]), + IO.DynamicCombo.Option("quaternion", [ + IO.Float.Input("position_x", default=4.0, min=-1000.0, max=1000.0, step=0.01, + tooltip="Camera position in world space (right-handed, Y-up)."), + IO.Float.Input("position_y", default=4.0, min=-1000.0, max=1000.0, step=0.01), + IO.Float.Input("position_z", default=4.0, min=-1000.0, max=1000.0, step=0.01), + IO.Float.Input("quat_x", default=0.0, min=-1.0, max=1.0, step=0.001), + IO.Float.Input("quat_y", default=0.0, min=-1.0, max=1.0, step=0.001), + IO.Float.Input("quat_z", default=0.0, min=-1.0, max=1.0, step=0.001), + IO.Float.Input("quat_w", default=1.0, min=-1.0, max=1.0, step=0.001, + tooltip="Camera world-rotation quaternion (three.js: looks down local -Z). Normalized for you."), + ]), + ], tooltip="How to define the camera: orbit angles, an explicit position, or a position + quaternion."), + IO.Float.Input("target_x", default=0.0, min=-1000.0, max=1000.0, step=0.01, advanced=True, + tooltip="Look-at point (orbit pivot / aim). In orbit mode, move it to pan/translate the " + "whole camera. Ignored in quaternion mode. Defaults to the origin."), + IO.Float.Input("target_y", default=0.0, min=-1000.0, max=1000.0, step=0.01, advanced=True), + IO.Float.Input("target_z", default=0.0, min=-1000.0, max=1000.0, step=0.01, advanced=True), + IO.Float.Input("roll", default=0.0, min=-180.0, max=180.0, step=1.0, + tooltip="Camera roll about the view axis, degrees."), + IO.Float.Input("fov", default=35.0, min=1.0, max=120.0, step=1.0, + tooltip="Vertical field of view in degrees."), + IO.Float.Input("zoom", default=1.0, min=0.01, max=100.0, step=0.01, + tooltip="Digital zoom (focal-length multiplier). >1 zooms in without moving the camera."), + IO.Combo.Input("camera_type", options=["perspective", "orthographic"], + tooltip="Projection used by Render Splat: perspective (foreshortening) or orthographic (parallel)."), + IO.CameraInfoState.Input("camera_info_state"), + ], + outputs=[IO.Load3DCamera.Output(display_name="camera_info")], + ) + + @classmethod + def execute(cls, mode, target_x, target_y, target_z, roll, fov, zoom=1.0, camera_type="perspective", camera_info_state=None) -> IO.NodeOutput: + dev = comfy.model_management.get_torch_device() + kind = mode["mode"] + if kind == "quaternion": # explicit world position + camera rotation + position = [mode["position_x"], mode["position_y"], mode["position_z"]] + quat = [mode["quat_x"], mode["quat_y"], mode["quat_z"], mode["quat_w"]] + return IO.NodeOutput(_quat_camera_info(position, quat, fov, dev, zoom=zoom, camera_type=camera_type)) + target = [target_x, target_y, target_z] # orbit pivot / aim; move it to pan the whole camera + if kind == "orbit": # yaw/pitch/distance about the target (world Y-up) + y, p = math.radians(mode["yaw"]), math.radians(mode["pitch"]) + cy, sy, cp, sp = math.cos(y), math.sin(y), math.cos(p), math.sin(p) + d = mode["distance"] + position = [target_x + d * cp * sy, target_y + d * sp, target_z + d * cp * cy] + else: # look_at: explicit world-space camera position + position = [mode["position_x"], mode["position_y"], mode["position_z"]] + return IO.NodeOutput(_lookat_camera_info(position, target, fov, dev, zoom=zoom, camera_type=camera_type, roll=roll)) + + +class CameraExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[IO.ComfyNode]]: + return [CreateCameraInfo] + + +async def comfy_entrypoint() -> CameraExtension: + return CameraExtension() diff --git a/comfy_extras/nodes_gaussian_splat.py b/comfy_extras/nodes_gaussian_splat.py index 116c14fde..5a4313d7a 100644 --- a/comfy_extras/nodes_gaussian_splat.py +++ b/comfy_extras/nodes_gaussian_splat.py @@ -1005,79 +1005,6 @@ class RenderSplat(IO.ComfyNode): return IO.NodeOutput(torch.stack(imgs), torch.stack(masks)) -class CreateCameraInfo(IO.ComfyNode): # TODO: move to better file - @classmethod - def define_schema(cls): - return IO.Schema( - node_id="CreateCameraInfo", - display_name="Create Camera Info", - search_aliases=["camera position", "make camera info", "orbit camera", "look at camera"], - category="3d", - description="Build a camera_info" - "Mode 'orbit' aims with yaw/pitch/distance around the target; " - "'look_at' places the camera at world position. Coordinates are the viewer's world space (right-handed,Y-up).", - inputs=[ - IO.DynamicCombo.Input("mode", options=[ - IO.DynamicCombo.Option("orbit", [ - IO.Float.Input("yaw", default=35.0, min=-360.0, max=360.0, step=1.0), - IO.Float.Input("pitch", default=30.0, min=-89.0, max=89.0, step=1.0), - IO.Float.Input("distance", default=4.0, min=0.01, max=1000.0, step=0.01, - tooltip="Camera distance from the target."), - ]), - IO.DynamicCombo.Option("look_at", [ - IO.Float.Input("position_x", default=4.0, min=-1000.0, max=1000.0, step=0.01, - tooltip="Camera position in world space (right-handed, Y-up)."), - IO.Float.Input("position_y", default=4.0, min=-1000.0, max=1000.0, step=0.01), - IO.Float.Input("position_z", default=4.0, min=-1000.0, max=1000.0, step=0.01), - ]), - IO.DynamicCombo.Option("quaternion", [ - IO.Float.Input("position_x", default=4.0, min=-1000.0, max=1000.0, step=0.01, - tooltip="Camera position in world space (right-handed, Y-up)."), - IO.Float.Input("position_y", default=4.0, min=-1000.0, max=1000.0, step=0.01), - IO.Float.Input("position_z", default=4.0, min=-1000.0, max=1000.0, step=0.01), - IO.Float.Input("quat_x", default=0.0, min=-1.0, max=1.0, step=0.001), - IO.Float.Input("quat_y", default=0.0, min=-1.0, max=1.0, step=0.001), - IO.Float.Input("quat_z", default=0.0, min=-1.0, max=1.0, step=0.001), - IO.Float.Input("quat_w", default=1.0, min=-1.0, max=1.0, step=0.001, - tooltip="Camera world-rotation quaternion (three.js: looks down local -Z). Normalized for you."), - ]), - ], tooltip="How to define the camera: orbit angles, an explicit position, or a position + quaternion."), - IO.Float.Input("target_x", default=0.0, min=-1000.0, max=1000.0, step=0.01, advanced=True, - tooltip="Look-at point (orbit pivot / aim). In orbit mode, move it to pan/translate the " - "whole camera. Ignored in quaternion mode. Defaults to the origin."), - IO.Float.Input("target_y", default=0.0, min=-1000.0, max=1000.0, step=0.01, advanced=True), - IO.Float.Input("target_z", default=0.0, min=-1000.0, max=1000.0, step=0.01, advanced=True), - IO.Float.Input("roll", default=0.0, min=-180.0, max=180.0, step=1.0, - tooltip="Camera roll about the view axis, degrees."), - IO.Float.Input("fov", default=35.0, min=1.0, max=120.0, step=1.0, - tooltip="Vertical field of view in degrees."), - IO.Float.Input("zoom", default=1.0, min=0.01, max=100.0, step=0.01, - tooltip="Digital zoom (focal-length multiplier). >1 zooms in without moving the camera."), - IO.Combo.Input("camera_type", options=["perspective", "orthographic"], - tooltip="Projection used by Render Splat: perspective (foreshortening) or orthographic (parallel)."), - ], - outputs=[IO.Load3DCamera.Output(display_name="camera_info")], - ) - - @classmethod - def execute(cls, mode, target_x, target_y, target_z, roll, fov, zoom=1.0, camera_type="perspective") -> IO.NodeOutput: - dev = comfy.model_management.get_torch_device() - kind = mode["mode"] - if kind == "quaternion": # explicit world position + camera rotation - position = [mode["position_x"], mode["position_y"], mode["position_z"]] - quat = [mode["quat_x"], mode["quat_y"], mode["quat_z"], mode["quat_w"]] - return IO.NodeOutput(_quat_camera_info(position, quat, fov, dev, zoom=zoom, camera_type=camera_type)) - target = [target_x, target_y, target_z] # orbit pivot / aim; move it to pan the whole camera - if kind == "orbit": # yaw/pitch/distance about the target (world Y-up) - y, p = math.radians(mode["yaw"]), math.radians(mode["pitch"]) - cy, sy, cp, sp = math.cos(y), math.sin(y), math.cos(p), math.sin(p) - d = mode["distance"] - position = [target_x + d * cp * sy, target_y + d * sp, target_z + d * cp * cy] - else: # look_at: explicit world-space camera position - position = [mode["position_x"], mode["position_y"], mode["position_z"]] - return IO.NodeOutput(_lookat_camera_info(position, target, fov, dev, zoom=zoom, camera_type=camera_type, roll=roll)) - - class TransformSplat(IO.ComfyNode): @classmethod def define_schema(cls): @@ -1656,7 +1583,7 @@ class SplatToMesh(IO.ComfyNode): class GaussianExtension(ComfyExtension): @override async def get_node_list(self) -> list[type[IO.ComfyNode]]: - return [SplatToFile3D, File3DToSplat, RenderSplat, CreateCameraInfo, TransformSplat, + return [SplatToFile3D, File3DToSplat, RenderSplat, TransformSplat, GetSplatCount, MergeSplat, SplatToMesh] diff --git a/nodes.py b/nodes.py index b03d6c603..fa5855449 100644 --- a/nodes.py +++ b/nodes.py @@ -2503,6 +2503,7 @@ async def init_builtin_extra_nodes(): "nodes_moge.py", "nodes_mediapipe.py", "nodes_gaussian_splat.py", + "nodes_camera.py", "nodes_triposplat.py", "nodes_depth_anything_3.py", "nodes_seed.py",