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 = QB(t)/QA(t) - 0.1425081342
r = P1(x)/P2(x) // cubic ratios
v>=1: t = P6C/(1 + exp((r - 1.3757956)*xp)) + P70
v<1: t = ln(clamp(P6C/((r - 0.875795603) - P70) - 1, P74, P78))/xp + 0.5
prep: xp = v>=1 ? -v : (v-1)*10; P6C = 0.5/(0.5 - 1/(exp(-xp/2) + 1)); P70 = 0.5 - 0.5*P6C
identity mask x<=1e-6 / x>=0.999998987; max(0) // EXACT Legion::ContrastOperator::mapImpl
- ·EXACT closed form transliterated from the real Legion::ContrastOperator (mapImpl @0x1800a9740 + prepare @0x1800aa610); verified by calling the DLL's mapImpl in-process and bit-comparing (agreement 3.5e-7 = float rounding).
- ·Replaced the earlier 19x512 measured LUT (removed in commit 0e796fc). Residuals seen against some older CIE fixture rows (~1.5e-4) are the legacy chain's other default-op evaluation wobble, not this operator.
- ·All constants read bit-exact from .rdata; evidence trail: RE/REPORT_CONTRAST_CLOSED_FORM.md in the RE tree.
- ·contrast < 1 -> lift shadows, flatten highlights (low-contrast film); contrast > 1 -> crush shadows, expand highlights.
Green-Magenta Tint
gmtreverse-engineeredA green/magenta rebalance via per-channel power law. Positive tint darkens R/B and brightens G in the midtones (green), and does the reverse in HDR highlights (magenta); negative inverts the axis.
out_c = in_c^k\nE = (1-|v|) + 1.4142135382*max(v,0) + 0.7071067691*max(-v,0)
E2 = (1-|v|) + 0.7071067691*max(v,0) + 1.4142135382*max(-v,0)
out_R = out_B = sign(x)*|x|^E ; out_G = sign(x)*|x|^E2
- ·EXACT (decompiled mapImpl): |v| <= 1e-4 is a dead zone; magnitudes are raised and signs restored.
- ·The G channel uses the SWAPPED constant pair (not 1/E): at v=0.5, E=1.20711, E2=0.85355 (validated against the CIE at rmse 8e-5).
- ·Exponent >1 darkens values below 1 and brightens values above 1, so positive tint reads green in midtones and magenta in highlights.
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.
lift' = 2 - lift
x1 = max(0, (x-1)*lift' + 1) * gain
out = x1^(1/gamma)
- ·EXACT (decompiled prepareForApplicationImpl + mapImpl): one multiply-add and one pow - the earlier three sRGB-space LUTs are gone, along with their round-trip residue.
- ·lift' = 2-lift comes from the prepare function; the gamma exponent is stored as 1/gamma.
- ·The legacy property that drives this operator in the CLI could not be confirmed, so this form is validated from the disassembly rather than a ramp sweep.
Photographic Exposure
isoreverse-engineeredCamera-style exposure: a single gain derived from ISO, f-stop and shutter speed.
factor = 683 / ((fstop^2 * 78) / ((0.65 / shutter) * iso))
= 5.6916 * iso / (shutter * fstop^2)
out = in * factor- ·EXACT (decompiled prepareForApplicationImpl): validated against a CIE gray-ramp run (iso 50, f/8, 1/200 -> pure gain 0.022232, predicted 0.0222327).
- ·Defaults (iso 100, f/8, 1/200 -> 0.0445) match CoronaImageCmd's defaults; the .cxr metadata carries corona.cm.iso / fstop / shutterspeed and is applied when present.
- ·The operator is gated by colorMap.usePhotographic in Corona's config; the enable toggle here plays that role.
Saturation
saturationreverse-engineeredAdditive HSV-style saturation: L = max(in, 0); S = (max - min) / max; S' = max(S + sat, 0). Neutral is 0.
L = max(rgb, 0) // input zero-clamp (DLL maxps, runs first)
gray = max(L); minv = min(L)
S = (gray - minv) / gray // 0 when gray == 0
amt = (S <= 1e-4) ? 0 : min(sat, +1) // guard on the INPUT S; property cap +1
Sp = max(S + amt, 0) // the ONLY clamp (no upper clamp)
kk = Sp / S // chroma scales by Sp/S
out = gray + (L - gray) * kk // achromatic returns L; NO [0, gray] clamp
sat > 0 -> saturate; sat < 0 -> desaturate; S + sat <= 0 collapses to gray
- ·EXACT (decompiled Legion::SaturationOperator::mapImpl @0x1800a9dd0 + the identical inline copy in evalRgb; validated by a 14-row x 13-amount CoronaImageCmd battery: 546 lanes under the capped model, 36/36 collapse rows bit-equal to the literal-gray control rows, 0 failures).
- ·Corona's saturation is ADDITIVE in HSV saturation, not the linear gray + (rgb-gray)*(1+s) mix. Because chroma scales by Sp/S, low-saturation (pastel) colors get a much stronger boost than already-vivid colors, which 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).
- ·The guard is S <= 1e-4 (const bits 0x38d1b717) on the INPUT saturation: guarded pixels keep the zero-clamped input. The amount is range-capped at +1: +1, +1.25 and +2 render pixel-identical in the CIE.
- ·No upper clamp on S' (S' = 1.25 is used at sat +0.5) and no [0, gray] output clamp: the operator emits negatives, and it is the CIE raw save that reads them as 0 afterwards.
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 smootherstep falloff of its distance from the image centre.
S = 1/max(w,h); Bx = S*w/2; By = S*h/2
k1 = max(0, 1-intensity)^2; k2 = exp(2.5*falloff) / (2/max(1,intensity) - k1)
t = clamp( ((x*S - Bx)^2 + (y*S - By)^2 - k1) * k2, 0, 1 )
u2 = (1-t)^4
m = 6*u2^5 - 15*u2^4 + 10*u2^3 (= smootherstep(u2))
out = rgb * m (identity when intensity <= 0)
- ·EXACT: transliterated from the real Legion::VignetteOperator (mapImpl @0x1800a9ed0 + prepare @0x1800ab310); validated against flat-input CIE differentials at 64x64 / 256x256 / 300x200 (rmse 2e-7 small-signal; the few-% big-signal residual is downstream transfer curvature).
- ·The f=1 disc of radius (1-intensity)*max(w,h) is reproduced exactly; k2 = exp(2.5*falloff)/(2/max(1,intensity) - k1) fits the measured maps.
- ·The radial falloff is normalized by max(w,h) (not the old elliptical w/h normalization): resolution-independent, and exposure to corners depends on the image aspect.
- ·The GPU point-op path builds its vignette LUT from this same apply along the centre-to-corner diagonal.
White Balance (Improved)
wbreverse-engineeredFull 3x3 matrix white balance; 6500 K is neutral. Matrices are tabulated from the real LegionLib export.
out = M(kelvin) . rgb (full 3x3; interpolation between 23 anchors)
M = Legion::colorAdaptationTm(kelvin, 6500) - the exact DLL export
- ·EXACT: matrices tabulated from the real LegionLib export at 23 log-spaced anchors (2000..99999 + 6500 identity); interpolated element-wise.
- ·A full spectral blackbody-to-XYZ port is available (see whiteBalanceGains in the tonemap core); the anchor table keeps it to a 3x3 multiply per pixel.
White Balance (Legacy)
wblegacyreverse-engineeredThe older kelvin -> RGB-gain white balance, kept for compatibility. 6500 K is neutral.
out = rgb * gains(kelvin)
gains = white(6500)/white(kelvin), mean-normalized
white(T) = M3 . ( integral(planck(T, l) * CMF(l) dl) ), l = 360..830nm @5nm
- ·EXACT: a JS port of Legion::colorAdaptation - Planck blackbody x CIE 1931 CMFs x the XYZ->sRGB matrix, computed per temp and cached.
- ·Validated against the real DLL export AND the CIE: gains(5000,6500) = [0.7957630, 0.9477397, 1.2564972] matches to 1.5e-6.
Advanced Filmic
advfilmicreverse-engineeredA film-style contrast curve with an independent toe (shadow crush) and shoulder (highlight clip), plus a shoulder angle.
b = toeStrength * 0.5 * toeLength^2.2 (toe black point)
cap = 1 - 0.8912 * shoulderLength (shoulder white point)
out = clamp(in - b, 0, cap - b/2) (toe pulls the cap down by b/2)
White point: the operator always clamps to 1.0 (values above 1 are
clipped to white even at shoulder strength 0).
shoulderAngle: gain = 1/(1 + (strength/2)*angle); shadows scaled by
gain, highlights lifted to 1 - gain (knee at cap).
strength >= 25 is a hard clip; below that a ~0.35 blend toward the cap.
- ·Reverse-engineered from headless CIE sweeps: toe, hard-clip shoulder, angle rotation, and the toe+shoulder composition are verified to 4e-4.
- ·The soft shoulder regime (strength < 25) is a ~0.35 blend approximation (exact soft curve not recovered).
Curves
curvesreverse-engineeredA user-defined 1D curve applied per channel. Control points are interpolated with a Catmull-Rom spline.
Piecewise cubic Hermite between control points (p0, p1):
cubic basis h00/h10/h01/h11 with tangents:
interior m = secant across neighbouring points; endpoints one-sided
Linear extrapolation beyond the first/last point.
- ·EXACT evaluator ported from the decompiled Curve::operator() (Legion's monotone piecewise cubic); validated against CLI outputs.
- ·The operator LOADS WITH NO POINTS (empty list = neutral identity via TM.curves). The CIE's NOIR look control points remain available as TM.NOIR_CURVE_POINTS but are never seeded automatically.
Filmic
filmicreverse-engineeredA two-knob filmic response: highlight compression plus a 'rich shadows' lift.
t = (1.0003055334*x + 0.0021391737)*x / ((0.4303440154 + x)*x + 0.0360290632)
m = min(1, x)
w = m*(p_hi - p_lo) + p_lo with p_hi = highlightCompression, p_lo = richShadows
out = (t - x)*w + x
- ·EXACT (decompiled mapImpl + prepare): a rational core blended toward identity; both sliders are raw values (validated against the CIE at rmse 7e-5 for richShadows=1).
- ·The 256-point LUTs are gone - the closed form replaces them.
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 = x * (1 + k*x) / (1 + x), k = 1/hc^2
- ·EXACT (decompiled mapImpl + prepare): the prepare stores 1/v^2; validated against the CIE (highlightCompression=2.0 ramp, rmse 4e-5).
- ·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_in . rgb [0.59719 0.35458 0.04823 / 0.07600 0.90834 0.01566 / 0.02840 0.13383 0.83777]
cr = (c*(c + 0.0245785993) - 0.0000905370034) / (c*(0.9837290049*c + 0.4329510033) + 0.2380809933)
y = M_out . cr [1.60475 -0.53108 -0.07367 / -0.10208 1.10813 -0.00605 / -0.00327 -0.07276 1.07602]
out = lerp(rgb, y, opacity)
- ·opacity 0 = linear pass-through; opacity 1 = full ACES film look (Corona's default).
- ·EXACT: the REAL matrices (read from the live constant pool in the DLL's .data tail) and the RRT rational - not the single-matrix fit used before.
- ·Validated: default-chain gray ramp mean abs diff 3.4e-5; aces(0.5)=0.3743, aces(1.0)=0.6191 match the CIE.
- ·The post-M_out output clamp is [0,1] (clamp01). The 0.9998354 saturation plateau seen in CIE saves is the legacy powApprox pair's output gs(1.0)=0.999835432 (RE/agents/vs_test probe), applied AFTER this op at chain end; clamping to the plateau here starves the pair (gs(0.9998354)=0.999710083) - see the clamp-order comment in lib/tonemap/corona-tonemap.js.
LUT (3D)
lutnot reverse-engineeredApplies a 3D .cube LUT with trilinear interpolation. Supports gamma-2.2 (CIE default), logarithmic, or linear working space.
encode = default: in^(1/2.2) log: powApprox( L(in), 1/2.2 ) linear: identity
out = trilinear( LUT, encode ) // size^3 x 3, red-fastest .cube layout
decode = default: out^2.2 log: powf( out, 2.2 ) linear: identity
L(x) = ln(max(x,0)*189.94085693359375 + 2.073321580886841) / 7.849999904632568
final = lerp(in, decode(out), opacity)
- ·The built-in LUT is Corona's NOIR_OFF_night.cube (33^3). You can load any .cube file.
- ·Default working space = pure gamma 2.2 (x^(1/2.2) in, ^2.2 out); matched to the CIE by differential runs (rmse 1e-4 on Advantix_100, 1.8e-4 on 400).
- ·Corona log mode (RE close/04-lut-2table corrected model, spec 02 section 13): the log coordinate L(x) is built first and then runs through the same pipeline-level gamma pair as the default mode - input = powApprox(L(x), 1/2.2) (the Legion fast pow port), output = exact powf(., 2.2). Real-cube CIE log fixture fit rmse 8.4e-5; the earlier 'cube(L(x)) with no post-decode' reading (from an identity-LUT probe, which is blind to an inverse pair) is rejected at 1.47e-2.
- ·Arbitrary user LUTs/curves cannot be reverse-engineered (they depend on the asset), so this operator consumes the .cube directly.