There are 17 tone-mapping operators. Each card is generated from the operator catalog — the same data drives the editor UI, so the documentation and the controls cannot drift apart.
Contrast
contrastreverse-engineeredA pivot S-curve that compresses the low end and expands (or flattens) the high end around a neutral pivot. Neutral is 1.0.
out = LUT_2D(contrast, in) // 10 contrast stops x 64-point log ramp, linear interp in both dims
contrast < 1 -> lift shadows, flatten highlights (low-contrast film)
contrast > 1 -> crush shadows, expand highlights
- ·Fitted as a 2D lookup table: 10 contrast values (0, 0.5, 1, 2, 4, 8, 16, 32, 64, 99) x a 64-point log ramp spanning 1e-4 .. 1e4.
- ·Interpolation is linear in the parameter dimension and in log-space along the value axis (lut1d/lut2d).
- ·The value ramp is sampled in log space because the underlying Corona curve is applied to linear data over a huge dynamic range.
Green-Magenta Tint
gmtreverse-engineeredA green/magenta rebalance via diagonal channel gains. Positive tint pushes toward magenta (R and B up, G down); negative toward green.
out = [r*gr, g*gg, b*gb]
gr, gg, gb = interp over tint in [-1, 1] (5 captured samples)
at +1: R 1.35, G 0.84, B 1.35 (magenta)
at -1: R 0.84, G 1.35, B 0.84 (green)
- ·Captured gains are symmetric around 0: R and B gain with positive magenta, G loses, and vice-versa.
- ·The catalog stores the UI-facing sign; the engine applies the negation because Corona's slider and the captured gains are sign-inverted.
Lift / Gamma / Gain
lggreverse-engineeredThe classic lift/gamma/gain colour wheels collapsed to a neutral axis: three sequential 1D curves. Neutral is lift=1, gamma=1, gain=1.
out = gain( gamma( lift(in) ) )
lift: 0 .. 2 (lifts the black point)
gamma: 0.01 .. 10 (power-like midtone pivot)
gain: 0 .. 2 (scales the white point)
- ·Each of the three stages is a 2D LUT over its parameter (captured in the sRGB working space).
- ·The structural sRGB round-trips that surround this operator in Corona's pipeline are folded into these in-place curves, so they do not fully cancel — see the docs' 'Known limitations'.
Photographic Exposure
isoreverse-engineeredAn on/off toggle. EV = log2(ISO/100); at the fixed ISO 100 the operator is an identity, so the toggle has no visible effect here.
EV = log2(iso / 100); at iso = 100 -> EV = 0 -> out = in
- ·Corona serializes this operator with no numeric fields (enabled flag only), so ISO is pinned to 100 in the reverse-engineered model.
- ·Measured: ISO 50 -> 100 -> 200 -> 400 gives exactly 2x exposure per stop, confirming the log2 mapping.
Saturation
saturationreverse-engineeredAdditive HSV-style saturation: S = (max - min) / max, then S' = clamp(S + sat, 0, 1). Neutral is 0.
gray = max(r, g, b)
minv = min(r, g, b)
S = (gray - minv) / gray // original HSV saturation (0..1)
k = 1 + sat / S // chroma scales by S'/S = (S+sat)/S
out = clamp(gray + (rgb - gray) * k, 0, gray)
sat > 0 -> saturate; sat < 0 -> desaturate; sat = -1 -> full grayscale
- ·Corona's saturation is ADDITIVE in HSV saturation, not the linear gray + (rgb-gray)*(1+s) mix. Because chroma scales by 1/S, low-saturation (pastel) colors get a much stronger boost than already-vivid colors — this is why the CIE looks 'more intense' than a plain (1+s) mix.
- ·The gray anchor is the per-pixel max channel, not a luminance weighting (verified: red@1.0 desaturates to 1.0, not Rec.709's 0.21).
- ·Fitted from a 12-point saturation sweep (-1.0..+1.0) over the linear swatch through headless CoronaImageCmd: mean abs error ~4e-4 vs the old (1+s) formula's ~1.9.
Simple Exposure (EV)
exposurereverse-engineeredA pure linear gain in EV stops. Neutral is 0.
out = in * 2^EV
- ·Verified to <= 0.5% max error over the full ramp — the simplest, most exact operator in the set.
Tint
tintreverse-engineeredMultiplies each channel by a tint colour (diagonal gains). White = neutral.
out = [r*tr, g*tg, b*tb], (tr, tg, tb) = tint colour in [0, 1]
- ·The colour is stored as tintR/tintG/tintB on the instance (derived from the hex picker).
- ·Decoded from the EXR/CXR metadata as a vec3 when present.
Vignette
vignettereverse-engineeredA radial darkening toward the corners. Multiplies each pixel by a falloff that depends on its normalized distance from the centre.
cx = w/2, cy = h/2
r = sqrt( ((x-cx)/cx)^2 + ((y-cy)/cy)^2 ) / sqrt(2) // 0 at centre, 1 at corner
m = LUT_2D(intensity, r) // 7 intensity stops x 32 radial samples
out = rgb * m
- ·The radial falloff is elliptical (normalized by cx/cy) so it is resolution-independent.
- ·Applied per-pixel (not as a whole-buffer convolution), so it lives in the point-operator loop with x/y coordinates.
White Balance (Improved)
wbreverse-engineeredColour-temperature rebalance via per-channel gains, interpolated over kelvin. 6500 K is neutral.
out = rgb * gains(kelvin)
gains = interp over 8 kelvin anchors (2000 .. 99999)
e.g. 2000K -> [0.48, 1.08, 6.44], 10000K -> [1.19, 1.10, 0.84]
- ·The 'improved' variant is Corona's current (Bradford-style) white balance.
- ·Gains were captured at 8 kelvin stops and interpolated; the improved operator has a much stronger blue channel at low kelvin than the legacy one.
White Balance (Legacy)
wblegacyreverse-engineeredThe older kelvin -> RGB-gain white balance, kept for compatibility. 6500 K is neutral.
out = rgb * gains(kelvin)
gains = interp over 8 kelvin anchors (2000 .. 99999)
e.g. 2000K -> [0.011, 0.055, 2.72], 10000K -> [1.22, 1.02, 0.75]
- ·Lower-temperature responses differ markedly from the improved variant (stronger red cut, deeper blue).
Advanced Filmic
advfilmicreverse-engineeredA film-style S-curve with independent toe (shadow) and shoulder (highlight) rolloff, plus a shoulder angle.
out = shoulderAngle( shoulderLength( shoulderStrength( toeLength( toeStrength(in) ) ) ) )
toe -> lifts/lowers shadows with a soft knee
shoulder -> compresses highlights with a soft knee
Each stage is a 1D LUT over its parameter (5 samples each).
- ·toe/shoulder length and angle are ratio multipliers in Corona's model (confirmed empirically).
- ·Five sequential 1D LUTs, one per parameter, applied in the order listed above.
Curves
curvesreverse-engineeredA user-defined 1D curve applied per channel. Control points are interpolated with a Catmull-Rom spline.
Between control points (p1, p2):
t = (x - p1.x) / (p2.x - p1.x)
out = 0.5 * ( 2*p1.y + (-p0.y + p2.y)*t
+ (2*p0.y - 5*p1.y + 4*p2.y - p3.y)*t^2
+ (-p0.y + 3*p1.y - 3*p2.y + p3.y)*t^3 )
Linear extrapolation beyond the first/last point.- ·Corona's exact spline is a monotone piecewise cubic; Catmull-Rom is the current approximation (control-point decoding is exact).
- ·The default curve is Corona's NOIR look: (0,0), (0.352, 0.1905), (0.7453, 0.1164), (1,1).
Filmic
filmicreverse-engineeredA two-knob filmic response: highlight compression plus a 'rich shadows' lift.
out = richShadows( highlightCompression(in) )
highlightCompression: 0 .. 1 (softens the highlight rolloff)
richShadows: 0 .. 1 (adds shadow density)
- ·Two sequential 1D LUTs over their parameters.
Tone Curve
tonecurvereverse-engineeredFour independent zone curves (shadows, darks, lights, highlights), each a 1D LUT.
out = shadows( darks( lights( highlights(in) ) ) )
each zone: -1 .. +1, 0 neutral, applied via its own 1D LUT
- ·Four sequential 1D LUTs, one per zone.
Reinhard Highlight Compression
hcreverse-engineeredReinhard-style highlight compression to prevent blown highlights.
out = LUT_2D(hc, in) // 5 hc stops x 64-point log ramp
closed-form approximation:
out = y * (1 + y / hc^2) / (1 + y)
- ·The full operator is a 2D LUT over highlightCompression; the closed form is a good approximation used in earlier builds.
- ·Lower hc = stronger compression of the highlight tail.
ACES OT
acesreverse-engineeredThe ACES output transform: a 3x3 colour matrix followed by a per-channel S-curve, blended by opacity.
c = M . rgb (3x3 row-sum-normalized matrix)
cr, cg, cb = ACES_curve(c) (per-channel 1D curve)
out = lerp(rgb, [cr, cg, cb], opacity)
M = [0.7313 0.2294 0.0393]
[0.0567 0.9311 0.0122]
[0.0235 0.1185 0.8580]- ·opacity 0 = linear pass-through; opacity 1 = full ACES film look (Corona's default).
- ·The matrix is row-sum-normalized so gray maps to gray; the curve is captured on a 64-point ramp.
LUT (3D)
lutnot reverse-engineeredApplies a 3D .cube LUT with trilinear interpolation. Supports sRGB, logarithmic, or linear working space.
x = encode(in) // sRGB (default) | log10 | linear
out = trilinear( LUT, x ) // size^3 x 3, red-fastest .cube layout
result = decode(out)
final = lerp(in, result, opacity)
- ·The built-in LUT is Corona's NOIR_OFF_night.cube (33^3). You can load any .cube file.
- ·The log working space spans 1e-4 .. 1e4 (approximation of Corona's internal log encoding).
- ·Arbitrary user LUTs/curves cannot be reverse-engineered (they depend on the asset), so this operator consumes the .cube directly.