SDR → HDR Universal
SDR → HDR Universal is the Radiance node you'll actually use
- image
- image
- highlight_mask
- shadow_mask
- highlight_confidence
- shadow_confidence
- report
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.
Inputs (22)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | SDR still, image batch, or video frames [B,H,W,C]. | |
| inverse_oetf | COMBO | sRGB | Transfer 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_nits | FLOAT | 1000200–10000 | Mastering 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_nits | FLOAT | 203100–1000 | Where 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_mode | COMBO | adaptive | adaptive: 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. |
| knee | FLOAT | 0.750.05–0.99 | Where 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_gamma | FLOAT | 2.001–6 | How 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_smoothing | FLOAT | 0.850–0.98 | Frame-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_encoding | COMBO | Linear | Linear: 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_blendopt | FLOAT | 1.000–1 | How much of the learned reconstruction is blended into the recovery masks. 0 disables it. |
| batch_modeopt | COMBO | Independent Images | Video Frames treats the batch as an ordered clip: enables temporal knee smoothing and the temporal learned backend. Independent Images processes each frame alone. |
| shadow_thresholdopt | FLOAT | 0.0500.001–0.5 | Linear-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_modeopt | COMBO | Hybrid | Expand: deterministic only. Recover: learned reconstruction only. Hybrid: expansion plus masked learned recovery. |
| highlight_thresholdopt | FLOAT | 0.9800.8–0.999 | Clipping threshold for Recover/Hybrid. Appended for saved-workflow compatibility. |
| temporal_windowopt | COMBO | 5 | Adjacent video frames used by temporal RUDRA. |
| temporal_checkpointopt | STRING | Optional Phase 3 checkpoint path. Empty uses the configured default. | |
| learned_backendopt | COMBO | Auto | Auto: 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_checkpointopt | STRING | Direct-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_sizeopt | INT | 512128–2048 | Tile 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_overlapopt | INT | 640–512 | Overlap in pixels between tiles, feathered to hide seams. Used only when tiling kicks in; must be smaller than pixel_tile_size. |
| pixel_recovery_modeopt | COMBO | highlights | Highlights is safest and avoids hallucinating chroma in deep shadows. |
| pixel_strengthopt | FLOAT | 1.000–2 | Multiplier 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)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| highlight_mask | MASK | — |
| shadow_mask | MASK | — |
| highlight_confidence | MASK | — |
| shadow_confidence | MASK | — |
| report | STRING | — |