ComfyUI Node

LatLong Animated Rotation

Turn one equirectangular still into a camera move

By cedarconnor·Created about a year ago·Updated about 15 hours ago· 2
LatLong Animated Rotation
  • image
  • IMAGE
keyframes[{"frame":0,"yaw":0,"pitch":0,"roll":0},{"frame":29,"yaw":90,"pitch":0,"roll":0}]
frame_count30
interpolation

You have a 2:1 equirectangular panorama and you want it to move - a slow yaw sweep for a skybox turntable, a look-around clip for a VR teaser, or just frames to hand to an upscaler. The traditional answer is Blender and a rotating camera, and the 360 skybox crowd still does exactly that. This node does it inside the graph instead: feed it a panorama, describe the camera path in JSON, get an IMAGE batch back.

The reason it's useful rather than cute is that the output is still a panorama at every frame - same equirectangular geometry, same pixel contract as the input, because the pack's whole job is doing these transforms without breaking the projection. Wire the batch into Video Combine, a preview, or a video model and nothing downstream needs to know a sphere was involved.

How it works

The keyframes list is degree angles, and the node interpolates the rotations, not the numbers. It builds one rotation per keyframe with SciPy's Rotation.from_euler("zyx", ...) - yaw around Z, pitch around Y, roll around X - and then uses Slerp, which is spherical linear interpolation: shortest-path blending on the rotation manifold. Each sampled pose is applied with the pack's equirectangular rotate solver, which is seam-aware and tiles automatically on big sources.

Two consequences are worth knowing before you build anything:

Shortest path means a 0 → 360 keyframe pair is a no-op. The two poses are the same rotation, so every interpolated frame is identical. For a full revolution, use a couple of keyframes (0 → 180 → 350) and let the endpoints clamp, or build it as two sweeps.

Pure yaw on integer pixel steps is lossless. The rotate solver short-circuits to a numpy roll when pitch, roll and horizon offset are zero and the yaw offset lands exactly on a pixel - 360*x/W for one pixel. So a yaw ramp on a 4096-wide panorama at 90° is a byte-exact shift, not a resample. Pitch or roll in the mix and it's a real resample, which is where interpolation starts to matter.

Inputs and outputs that matter

  • image - the batch to rotate. One still is fine; the node recycles it.
  • keyframes (multiline JSON, default [{"frame":0,"yaw":0,"pitch":0,"roll":0},{"frame":29,"yaw":90,"pitch":0,"roll":0}]) - a list of frame/yaw/pitch/roll objects. Any angle you omit defaults to 0. Frame numbers are your own timeline, not seconds.
  • frame_count (1–4096, default 30) - how many frames come out. Frames past the last keyframe time clamp to the final pose, so asking for 300 frames from a 30-frame keyframe range gives you 270 frozen ones.
  • interpolation - bilinear, lanczos, bicubic, nearest. Bicubic and Lanczos are the CPU filters in this pack, so they cost real time on huge panoramas; bilinear is the sane default for a sweep you're going to upscale anyway.

One output: a plain IMAGE batch, frame_count frames long, input resolution. There's no fps field - frame rate belongs to whatever you combine the batch with.

Install

Same as every node in the pack. From ComfyUI Manager, search for the pack title (ComfyUI LatLong); or by hand:

cd ComfyUI/custom_nodes
git clone https://github.com/cedarconnor/comfyui-LatLong
cd comfyui-LatLong
python -m pip install -r requirements.txt

Use ComfyUI's own Python - the embedded python.exe on a portable install. Needs Python 3.10+. Dependencies are numpy, opencv-python, scipy, torch and Pillow, which you almost certainly already have. There are no model downloads. Restart ComfyUI afterwards and the node shows up under LatLong/Animation.

Common issues

The JSON is parsed literally, so a trailing comma or a typo'd key throws instead of guessing. The validation error you'll actually see is "Provide at least two finite keyframes in increasing frame order" - that means duplicate frame numbers, a decreasing pair, or NaN in an angle.

Watch the batch size. These are full-resolution float32 images, not compressed frames: thirty 4096×2048 RGB panoramas is roughly 3 GB of batch sitting in memory before any downstream node touches it. If you only need a preview, generate at 1024-wide and upscale the winner.

And mind the recycling. If image has fewer frames than frame_count the node wraps with i % len(image) - a single still across 30 frames is the intended turntable case, but a 16-frame batch asked for 30 frames gets you frame 0 again at step 17, which looks like a stutter until you realise what happened.

CategoryLatLong/Animation

Inputs (4)

NameTypeDefaultDescription
imageIMAGE
keyframesSTRING[{"frame":0,"yaw":0,"pitch":0,"roll":0},{"frame":29,"yaw":90,"pitch":0,"roll":0}]
frame_countINT301–4096
interpolationCOMBO4 options: bilinear, lanczos, bicubic, nearest

Outputs (1)

NameTypeDescription
IMAGEIMAGE