LatLong Float Processor
Rotate an HDR panorama without clipping it to mush
- image
- IMAGE
ComfyUI's image tensors are, for almost every practical purpose, 0–1 sRGB-encoded data. That's fine for a diffusion model, which was trained on exactly that, and it's wrong for an HDRI environment map, where the interesting information - the sun disc, the bright window - lives five or six stops above 1.0 and some of the values are negative.
Skyboxes are where this bites. There's a real 360 HDRI EXR LoRA lineage for exactly this job (Z-Image on one end, Blender, Unity and Nuke on the other), and the usual way people break it is rotating the panorama through a node that clamps to [0,1] on the way out. Everything above white flattens, the highlights die, and you only notice when the render is lit wrong.
Float Processor exists to not do that. The author's own description is blunt about the contract: it preserves finite scene-linear values, including negatives and values above one, and preview tone mapping is a separate node. Five operations, same geometry as the rest of the pack, no clamp at the end.
How it works
Each frame is converted to float32 and handed to one of the pack's solvers. The operation field picks which, and it also changes what width and height mean - this is the part worth reading twice:
- rotate - yaw/pitch/roll, and
width/heightare ignored; the source dimensions are retained. CPU filtered and tiled. - perspective - a rectilinear view of
width×heightatfov, same camera basis as LatLong Extract Projection. - to_cubemap -
widthis the per-face size, not the atlas width.heightis unused. - from_cubemap -
width×heightare the output panorama dimensions. - resize - both used, obviously, with the note that the pack's resize can keep a 2:1 panorama aspect.
interpolation offers lanczos, bicubic, bilinear and nearest. On this path the high-order filters are the CPU ones, and the README is explicit that CPU filters preserve floating values - which is why the default is Lanczos rather than bilinear. You're choosing CPU by picking a filter, and paying for it in time: validation on the author's hardware puts a 16384×8192 CPU rotation at about 75 seconds.
One IMAGE output. Values go through untouched, so a negative pixel comes out negative and a 12.0 highlight comes out as 12.0.
The part nobody tells you
Preserving float in the graph is only half the job, because what you save it with decides whether the HDR survives. The stock SaveImage writes display-referred images; if that's your terminal node, the float work was for nothing. You need a saver that actually writes float data out - some EXR-capable node from the VFX corner of the ecosystem (the OpenImageIO-based packs are the usual answer) or an NPZ dump you process outside ComfyUI.
Same story at the other boundary: the moment an HDR panorama goes through a diffusion model, it's 0–1 sRGB again, because that's what VAEs speak. So the honest shape of this node's job is geometry on linear data - rotating, converting and resizing an environment map without destroying the stops - not routing HDR through a sampler. If your pipeline is scene-linear end to end, the scene-linear section of the post-processing doc is the right mental model; it's a compositor world (OCIO, ACES, EXR) that mostly doesn't live in r/StableDiffusion, so expect to assemble it yourself.
For the other direction - actually looking at your HDR values in ComfyUI - see LatLong Tone Map Preview, which is the intended partner to this node.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/cedarconnor/comfyui-LatLong
cd comfyui-LatLong
python -m pip install -r requirements.txt
Manager: search "ComfyUI LatLong". ComfyUI's Python (3.10+), dependencies numpy, opencv-python, scipy, torch and Pillow, no model files. Restart, then look under LatLong/HDR.
Common issues
The input validator is strict and the error is worth knowing: it wants a non-empty B,H,W,C tensor with 1, 3 or 4 channels, and it rejects NaN or infinity outright. That's not pedantry - infinity propagates into every resampled neighbour and turns a frame into a black hole of NaNs. If you feed it a tone-mapped preview by mistake you'll get plausible-looking output that's subtly wrong, because you clipped before the geometry instead of after.
Two more: fov is horizontal and bounded to 1–179, so 180 is not a valid extraction. And the tiling bounds the geometry buffers, not the tensors - a 16K panorama is 1.5 GiB per frame in float32, and this node holds input and output, so 3 GiB before anything downstream sees it.
Inputs (9)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| operation | COMBO | 5 options: rotate, perspective, to_cubemap, from_cubemap, resize | |
| yaw | FLOAT | 0.00 | — |
| pitch | FLOAT | 0.00 | — |
| roll | FLOAT | 0.00 | — |
| fov | FLOAT | 90.001–179 | — |
| width | INT | 20481–32768 | — |
| height | INT | 10241–16384 | — |
| interpolation | COMBO | 4 options: lanczos, bicubic, bilinear, nearest |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| IMAGE | IMAGE | — |