FastLED 3.10.6
Loading...
Searching...
No Matches

◆ buildGamutMapFromSolveQ16()

bool fl::buildGamutMapFromSolveQ16 ( const EmitterSolveMatrixQ16 & solve,
GamutMapQ16 * out )

Build a mapper from a bind-time solve already transformed into D65 working coordinates.

The neutral cap must be derived from this same solve.

Definition at line 300 of file gamut_map.cpp.hpp.

301 {
302 if (out == nullptr) return false;
303 out->solve = solve;
304
305 // The drives this device needs to reproduce D65 at unit luminance. The
306 // largest of them is what saturates first as the neutral is scaled up,
307 // so it sets the brightest neutral the device can reach.
308 i32 neutral_drives[3];
309 solveRgbDrivesQ16(out->solve, kGamutD65Q16, neutral_drives);
310 i32 largest = neutral_drives[0];
311 for (int i = 0; i < 3; ++i) {
312 // Every drive, not just the largest. A solve like {-x, y, z} has a
313 // positive largest but describes a device whose primaries do not
314 // enclose D65, so it cannot make a neutral at any brightness. Taking
315 // the largest alone would scale that infeasible point up and hand
316 // back the lightness of a colour the device cannot produce, leaving
317 // the mapper's lightness bound too permissive.
318 if (neutral_drives[i] <= 0) {
319 return false;
320 }
321 if (neutral_drives[i] > largest) {
322 largest = neutral_drives[i];
323 }
324 }
325
326 // s_max = 1 / largest, in s16.16. This is the one division in the whole
327 // module, and it runs once per bind rather than once per pixel.
328 //
329 // Computed and clamped in i64. `buildRgbSolveMatrixQ16` accepts emitter
330 // luminances up to 1e6, and a profile bright enough to reach D65 on a
331 // drive of one or two raw units puts 2^32 / largest at or past i32's
332 // range -- 2^31 exactly, at largest == 2. Narrowing that is
333 // implementation-defined, and the bound it produced would be nonsense.
334 //
335 // The clamp is at 64.0 because that is where the OKLab transform's
336 // domain ends: a neutral scaled past it would be clamped there anyway,
337 // so nothing downstream can tell the difference.
338 i64 scale_wide = (static_cast<i64>(kGamutFullDrive) << 16) /
339 static_cast<i64>(largest);
340 if (scale_wide > kOklabQ16MaxMagnitude) {
341 scale_wide = kOklabQ16MaxMagnitude;
342 }
343 const i32 scale = static_cast<i32>(scale_wide);
344 const i32 brightest_neutral[3] = {
345 scaleGamutQ16(kGamutD65Q16[0], scale),
346 scaleGamutQ16(kGamutD65Q16[1], scale),
347 scaleGamutQ16(kGamutD65Q16[2], scale),
348 };
349 i32 lab[3];
350 xyzToOklabQ16(brightest_neutral, lab);
351 out->max_neutral_lightness = lab[0];
352 return true;
353}
fl::UISlider scale("Scale", 4,.1, 4,.1)
i32 scaleGamutQ16(i32 value, i32 factor) FL_NO_EXCEPT
Scale an s16.16 value by an s16.16 factor, rounding to nearest.
constexpr i32 kGamutFullDrive
Full drive, as an s16.16 raw value.
constexpr i32 kOklabQ16MaxMagnitude
Largest magnitude either direction accepts, as an s16.16 raw value (64.0).
Definition oklab_q16.h:40
void xyzToOklabQ16(const i32(&xyz)[3], i32(&out_lab)[3]) FL_NO_EXCEPT
XYZ (s16.16, D65-relative) -> OKLab (s16.16), L then a then b.
void solveRgbDrivesQ16(const EmitterSolveMatrixQ16 &matrix, const i32(&xyz)[3], i32(&drives)[3]) FL_NO_EXCEPT
One pixel: XYZ in s16.16 to three emitter drives in s16.16.
i32 max_neutral_lightness
OKLab lightness of the brightest D65 neutral this device can reach.
Definition gamut_map.h:58
EmitterSolveMatrixQ16 solve
Definition gamut_map.h:44
fl::i64 i64
Definition stdint.h:221

References FL_NO_EXCEPT, kOklabQ16MaxMagnitude, scale, solveRgbDrivesQ16(), and xyzToOklabQ16().

Referenced by buildGamutMapQ16(), and buildStreamingPipelineQ16().

+ Here is the call graph for this function:
+ Here is the caller graph for this function: