Nodes/radiance/SDR → HDR Universal
ComfyUI Node

SDR → HDR Universal

SDR → HDR Universal is the Radiance node you'll actually use

By FXTD-Studios·Created 8 months ago·Updated about 18 hours ago· 246
SDR → HDR Universal
  • image
  • image
  • highlight_mask
  • shadow_mask
  • highlight_confidence
  • shadow_confidence
  • report
◄inverse_oetfsRGB►
◄peak_nits1000►
◄reference_white_nits203►
◄knee_modeadaptive►
◄knee0.75►
◄shoulder_gamma2.00►
◄temporal_smoothing0.85►
◄output_encodingLinear►
◄rudra_blend1.00►
◄batch_modeIndependent Images►
◄shadow_threshold0.050►
◄processing_modeHybrid►
◄highlight_threshold0.980►
◄temporal_window5►
◄temporal_checkpoint►
◄learned_backendAuto►
◄pixel_checkpoint►
◄pixel_tile_size512►
◄pixel_tile_overlap64►
◄pixel_recovery_modehighlights►
◄pixel_strength1.00►

If you only ever add one node from this pack, it's this one. SDR → HDR Universal is the pack's SDR-to-HDR orchestrator: a deterministic expansion, the learned RUDRA reconstruction, or both blended - one node, one dropdown, no VAE, no latent round trip. It's also the node the README's quick start tells you to wire first: Read → SDR → HDR Universal → Viewer.

The mechanism

Three stages, and knowing them makes the widgets make sense.

First the inverse EOTF: inverse_oetf decodes your display-encoded SDR (sRGB, Rec.709, Gamma 2.2, Gamma 2.4, or None) to scene-linear. Input is clamped to 0–1 first.

Then the expansion. Below knee nothing happens at all - the image is untouched. Above it, luminance expands up to peak_nits through a shoulder whose hardness is shoulder_gamma. The source comments are unusually careful here: the join between the two is made continuous in gradient, not just in value, so you don't get a visible ridge where the sky crosses the knee. That's the bug they fixed, and it's why 1.0 on shoulder_gamma is a documented mistake - it's a straight line to peak and it steps the gradient.

knee_mode decides how the knee is placed. adaptive measures it per frame as a luma percentile (knee 0.75 = the 75th percentile), which is right for footage that changes exposure. With batch_mode set to Video Frames, temporal_smoothing EMA-filters that knee across frames so it doesn't crawl; leave it adaptive and on Independent Images and you can watch the whole image pump between shots.

Then the learned pass. processing_mode is your choice: Expand (deterministic only), Recover (learned only), Hybrid (default - expansion plus masked learned recovery). The learned half is the same RUDRA pixel model as the dedicated Recover node, blended by rudra_blend; set that to 0 and you've disabled it without changing mode.

What to set, what to wire

Your two nits fields are the ones people confuse. peak_nits (default 1000) is the ceiling - what the PQ/HLG encode targets, and the hard limit on the output. reference_white_nits (default 203) is where SDR diffuse white lands: a white shirt, a page, a cloud. Headroom above 203 belongs to speculars.

The tooltip also mentions the escape hatch: setting reference_white_nits equal to peak_nits restores the old pre-3.4 behaviour of slamming SDR white to the display peak. Useful if you're matching an old graph, ugly otherwise.

pixel_recovery_mode defaults to highlights because, in the author's words, it's the safest and avoids hallucinating chroma in deep shadows. all will touch shadows too. pixel_strength (Direct Pixel backend only) doubles or kills the learned residual over its own inverse tone curve.

Outputs: image, highlight_mask, shadow_mask, highlight_confidence, shadow_confidence, and - the one to actually read - report. It's a string that tells you which path ran and why. Wire image into the Viewer for review, or into the pack's Write node with output_encoding set to Linear ACES2065-1 (AP0) for EXR delivery.

Install

Manager → search Radiance → Install → restart → refresh the browser. Manual:

cd ComfyUI/custom_nodes
git clone https://github.com/fxtd-studios/radiance.git
cd radiance
python -m pip install -r requirements.txt

Heavier than the average node pack - OpenEXR, OpenImageIO, opencolorio, transformers, scipy - and on Windows portable you should run the pip line with python_embeded\python.exe. The RUDRA weights (~5 MB) drop into ComfyUI/models/radiance/ on first use; they're non-commercial while the code is GPL-3.0.

Where it bites

The fallback is the big one, and it's documented: with no checkpoint, downloads disabled (RADIANCE_ALLOW_DOWNLOADS=0), HF_HUB_OFFLINE=1 or an unwritable models folder, Recover and Hybrid produce output bit-identical to Expand and don't complain loudly. Read report. A failed download is retried once per ComfyUI session, so restart to try again.

Second: Expand deliberately keeps SDR midtones at their SDR display level - below the knee the signal stays at 1.0 = 100 nits, and only the knee-to-white span opens up. If your grade looks flat compared to a BT.2446-style lift, that's the design. Raise reference_white_nits or grade after.

Third: the CLI-adjacent honesty from the community when this pack surfaced in March 2026 - someone in the announcement thread insisted it "won't create real 16/32-bit EXR." For Universal that's wrong, but only because of what you do downstream: pick Linear or AP0 out, write EXR from the pack's Write node, and pass float all the way. Encode to PQ and save a PNG and you've made a display-referred file with an HDR-shaped curve.

CategoryFXTD STUDIOS/Radiance/HDR

Inputs (22)

NameTypeDefaultDescription
imageIMAGESDR still, image batch, or video frames [B,H,W,C].
inverse_oetfCOMBOsRGBTransfer curve the SDR input is encoded with, decoded to linear before expansion. None treats the input as already linear. Input is clamped to 0-1 first.
peak_nitsFLOAT1000200–10000Mastering display peak. This is the ceiling the output is limited to and the value the PQ/HLG encode targets — not where SDR white lands. See reference_white_nits.
reference_white_nitsFLOAT203100–1000Where SDR diffuse white (code 1.0) lands, in nits. 203 is the ITU-R BT.2408 HDR Reference White — a white shirt, a page, a cloud. Headroom above it belongs to speculars. Raise it for a brighter grade; setting it to peak_nits restores the pre-3.4 behaviour of slamming SDR white to the display peak. Also the output unit: Linear/AP0 1.0 = this many nits, matching HDR Encode and Write's hdr_reference_nits.
knee_modeCOMBOadaptiveadaptive: the knee is measured per frame as a percentile of linear luma (see knee). manual: knee is used directly as a fixed linear-luma level.
kneeFLOAT0.750.05–0.99Where expansion starts. adaptive: the luma percentile (0.75 = 75th percentile of the frame). manual: a linear-luma level. Below the knee the image is unchanged.
shoulder_gammaFLOAT2.001–6How hard highlights ramp once past the knee. The join keeps a continuous gradient at any value ≥ 1; 2.0 and above sit within 0.2% of unity there. 1.0 is a straight line to peak and steps the gradient.
temporal_smoothingFLOAT0.850–0.98Frame-to-frame smoothing of the adaptive knee to stop flicker (0 = per-frame knee, higher = steadier). Used only with knee_mode adaptive and batch_mode Video Frames.
output_encodingCOMBOLinearLinear: scene-linear Rec.709, 1.0 = reference_white_nits. AP0: the same in ACES2065-1. PQ (HDR10) and HLG: Rec.2020 delivery code values (BT.2100).
rudra_blendoptFLOAT1.000–1How much of the learned reconstruction is blended into the recovery masks. 0 disables it.
batch_modeoptCOMBOIndependent ImagesVideo Frames treats the batch as an ordered clip: enables temporal knee smoothing and the temporal learned backend. Independent Images processes each frame alone.
shadow_thresholdoptFLOAT0.0500.001–0.5Linear-luma level below which shadows count as crushed. Shapes the shadow_mask output; changes the image only when pixel_recovery_mode is 'all' or 'shadows'.
processing_modeoptCOMBOHybridExpand: deterministic only. Recover: learned reconstruction only. Hybrid: expansion plus masked learned recovery.
highlight_thresholdoptFLOAT0.9800.8–0.999Clipping threshold for Recover/Hybrid. Appended for saved-workflow compatibility.
temporal_windowoptCOMBO5Adjacent video frames used by temporal RUDRA.
temporal_checkpointoptSTRINGOptional Phase 3 checkpoint path. Empty uses the configured default.
learned_backendoptCOMBOAutoAuto: temporal model on ordered video when installed, else the direct-pixel model. Direct Pixel: pixel model per frame. Temporal: temporal model only (video, 5+ frames).
pixel_checkpointoptSTRINGDirect-pixel .pt checkpoint. Empty searches models/radiance and RADIANCE_SDR2HDR_PIXEL, and downloads the default RUDRA model (~5 MB) on first use unless RADIANCE_ALLOW_DOWNLOADS=0.
pixel_tile_sizeoptINT512128–2048Tile size used only when the whole frame does not fit in memory. The model normalises over its input, so whole-frame inference is more accurate and is always tried first.
pixel_tile_overlapoptINT640–512Overlap in pixels between tiles, feathered to hide seams. Used only when tiling kicks in; must be smaller than pixel_tile_size.
pixel_recovery_modeoptCOMBOhighlightsHighlights is safest and avoids hallucinating chroma in deep shadows.
pixel_strengthoptFLOAT1.000–2Multiplier on the pixel model's learned residual over its own inverse tone curve (0 = curve only, 1 = as trained, 2 = doubled). Direct Pixel backend only.

Outputs (6)

NameTypeDescription
imageIMAGE—
highlight_maskMASK—
shadow_maskMASK—
highlight_confidenceMASK—
shadow_confidenceMASK—
reportSTRING—