Extensions/Reframe 360
ComfyUI Extension

Reframe 360

Virtual camera inside a 360 image. Real focal lengths, sensor sizes, lens projections, interactive drag framing.

By senyilmaz·Created about a month ago·Updated about a month ago· 8
senyilmaz/ComfyUI-Reframe360
Nodes2
On cloudLocal install
CategoryReframe360
Stars8
Updatedabout a month ago
Readme

Reframe 360

Put a virtual camera inside a 360° image and pull real shots out of it.

Drag the preview to frame by eye, then read exact numbers off the sliders. Both write to the same values, so rough framing and precise repeatable output are the same operation.

Built for VFX work: real focal lengths, real sensor sizes, correct field-of-view maths, and four lens projections.

node

Why another 360 node

pytorch360convert already does equirectangular → perspective and does it well. What it doesn't have is a camera you can grab, a lens you can specify in millimetres, or anything other than rectilinear projection. This node is the camera layer.

The camera model

The panorama is mapped to the inside of a unit sphere. A camera sits inside it, looks down +Z, and casts rays. Where each ray hits the sphere is what that pixel sees.

sensor position (mm)
      ↓  lens projection
ray angle from optical axis
      ↓  yaw / pitch / roll
ray direction in world space
      ↓  intersect unit sphere from (0, 0, perspective_warp)
hit point
      ↓
equirectangular UV → sample

One grid_sample at the end. Batches come free, so video costs no more per frame than a still.

Parameters

Lens

| | | |---|---| | sensor | Real sensor presets, or Custom. Focal length is meaningless without this — 18mm on Super16 is a different shot than 18mm on full frame. | | sensor_fit | auto / horizontal / vertical. Which sensor dimension survives when the output aspect doesn't match the sensor aspect. Same idea as Blender. | | focal_length | 1–300mm, default 18. | | lens_projection | rectilinear / stereographic / equisolid / equidistant. |

Pick the right projection. Rectilinear keeps straight lines straight, which is what a normal lens does — but it works by tan(θ), and that heads for infinity as the FOV approaches 180°. On full frame:

| Focal | hFOV | Corner stretch | |---|---|---| | 12mm | 112.6° | 1.7× | | 8mm | 132.1° | 2.2× | | 4mm | 154.9° | 3.9× | | 1mm | 173.6° | 14.2× |

Below roughly 12mm on full frame, rectilinear stops being useful. That's optics, not a bug — it's why real fisheye lenses aren't rectilinear. The node warns above 150°.

  • stereographic — the most natural-looking ultra-wide. The "little planet" projection.
  • equisolid — what most real fisheye lenses actually are. Matches Blender's default fisheye.
  • equidistant — scientific / mapping fisheye. Angle is linear in radius.

Camera

| | | |---|---| | yaw / pitch / roll | Pan / tilt / dutch, in degrees. Applied as Ry · Rx · Rz. Positive pitch looks up, matching C4D, Blender and Nuke. | | perspective_warp | −0.95 to 0.95. Camera position along its own Z axis, as a fraction of sphere radius. Rotate the camera and the offset direction rotates with it. |

Read this before using perspective_warp. It is not a dolly. There is no geometry in the sphere, so there is no parallax — nothing occludes anything else, and moving the camera reveals nothing new. What it does do is bend straight lines. A building edge will bow. A horizon will curve.

It's an artistic warp. Good on organic subjects — sky, clouds, foliage, water — and useful for putting a subtle wrongness into a plate. Bad on architecture or anything with hard straight lines. Named perspective_warp rather than "dolly" or "Z depth" so nobody expects a camera move they aren't going to get.

There is deliberately no sphere_diameter. Only the ratio of camera offset to radius affects the image — a 10m sphere with the camera 5m off-centre is pixel-identical to a 100m sphere at 50m. perspective_warp is that ratio. A diameter control would be a second knob secretly wired to the first one.

Output

| | | |---|---| | aspect_preset | 16:9, 2.39:1, 1.85:1, 4:3, 1:1, 9:16, 4:5, or Custom. Drives height from width. | | output_width / output_height | Pixels. | | interpolation | bicubic / bilinear / nearest. |

Interface

The preview box is derived from your output aspect ratio: node height is a function of node width and that aspect. It cannot disagree with what you're rendering, so there is nothing to letterbox and nothing to squash. Drag the node wider and the preview scales with it; drag it taller and it snaps back, because any other height would mean bars or stretch.

The preview is drawn straight onto the graph canvas rather than as a DOM widget — ComfyUI's DOM widget sizing varies between frontend versions and wouldn't honour the aspect, which stretched the image and made it appear to warp under rotation.

Preview controls

Widgets are grouped under collapsible LENS / CAMERA / OUTPUT / ADVANCED headers — click a header to fold it. Two widgets appear only when they mean something: the custom sensor dimensions show up only when sensor is Custom, and output_height hides whenever an aspect preset is driving it.

The panorama is cached server-side after the first run and fetched over HTTP, so the preview reloads on its own and survives a page refresh. There's nothing to show before the first run, because the image doesn't exist yet.

| | | |---|---| | Drag | yaw + pitch. The pixel you grab stays under the cursor — angles are differenced through the lens projection, so it tracks exactly at any focal length and anywhere in frame. Drag down to look up. | | Shift-drag / right-drag | roll, following the cursor around the frame centre | | Wheel | focal length | | Double-click | reset orientation, keep the lens |

The preview is a WebGL2 fragment shader that is a direct port of projection.py — same lens maths, same ray-sphere trace. It is not a textured sphere with a stock perspective camera, because that approach cannot render fisheye projections or perspective_warp and would have to lie about exactly the parameters this node exists to control.

If you change the maths in projection.py, change it in js/reframe360.js too. Watch out for mat3() — it fills columns, so the GLSL matrices are transposes of the Python ones.

Load 360 Image (URL)

Fetches any direct link to a 2:1 equirectangular image. Tested against:

  • Poly Haven — CC0, thousands of HDRIs, no attribution required
  • Mapillary — CC-BY-SA, street-level 360s with decent European coverage, open API
  • KartaView — open street-level imagery
  • Wikimedia Commons — many CC-licensed panoramas
  • Your own server

On Google Street View: this node ships no site-specific scraper, by design. Google's Geo Guidelines prohibit downloading Street View imagery for use outside Google's services, stitching tiles into larger images, and removing attribution — and state that those restrictions apply to academic, nonprofit, and commercial projects alike. Street View imagery is also copyrighted, which puts it in a different position from the public-facts scraping that most "scraping is legal" commentary is about. Sourcing is the user's business; it isn't this node's to ship.

Saved workflows

Widget values are stored by name, not by array position. ComfyUI's default serialization is positional, which breaks the moment a node grows a header widget or reorders an input — every value shifts into the wrong slot and the graph fails validation with things like roll: could not convert 'rectilinear' to float.

On load, every value is also checked against its own declared range or option list, and anything impossible is replaced with the default (with a console note saying what was repaired). A graph saved by an older version repairs itself on open.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/YOURNAME/ComfyUI-Reframe360
pip install -r ComfyUI-Reframe360/requirements.txt

Requires a WebGL2-capable browser for the interactive preview. The sliders work without it.

Credits

The projection maths is standard spherical camera geometry. Prior art worth knowing about:

MIT.