IceShard 1
A personal game engine project, with development focused on 2D/2.5D games.
Loading...
Searching...
No Matches
color_details.hxx
Go to the documentation of this file.
1
3
4#pragma once
5#include <ice/base.hxx>
6#include <ice/math.hxx>
9#include <ice/profiler.hxx>
10#include <array>
11
12namespace ice::detail
13{
14
24
25 // Linear to sRGB
26 // Condition Value
27 // 0 ≤ L ≤ 0.0031308 S = L * 12.92
28 // 0.0031308 < L ≤ 1 S = 1.055 * L1/2.4 - 0.055
29 constexpr auto linear_to_srgb(ice::f32 x) noexcept -> ice::f32
30 {
31 if (x <= 0.0031308f)
32 {
33 return 12.92f * x;
34 }
35 else
36 {
37 return 1.055f * ice::math::pow(x, 1.0f / 2.4f) - 0.055f;
38 }
39 }
40
41 // sRGB to Linear
42 // Condition Value
43 // 0 ≤ S ≤ 0.04045 L = S/12.92
44 // 0.04045 < S ≤ 1 L = ((S+0.055)/1.055)2.4
45 constexpr auto srgb_to_linear(ice::f32 x) noexcept -> ice::f32
46 {
47 if (x <= 0.04045f)
48 {
49 return x / 12.92f;
50 }
51 else
52 {
53 return ice::math::pow((x + 0.055f) / 1.055f, 2.4f);
54 }
55 }
56
57 // FROM: https://bottosson.github.io/posts/oklab/
58 // FROM: https://bottosson.github.io/posts/gamutclipping/
59 //
60 // Copyright (c) 2021 Björn Ottosson
61 //
62 // Permission is hereby granted, free of charge, to any person obtaining a copy of
63 // this software and associated documentation files (the "Software"), to deal in
64 // the Software without restriction, including without limitation the rights to
65 // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
66 // of the Software, and to permit persons to whom the Software is furnished to do
67 // so, subject to the following conditions:
68 //
69 // The above copyright notice and this permission notice shall be included in all
70 // copies or substantial portions of the Software.
71 //
72 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
73 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
74 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
75 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
76 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
77 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
78 // SOFTWARE.
79
81 constexpr auto from_oklab_to_lrgb(ice::math::vec3f from) noexcept -> ice::math::vec3f
82 {
84 // OKLab → LMS
85 ice::f32 const l2 = (from.v[0][0] * 1.0f) + (from.v[0][1] * 0.3963377774f) + (from.v[0][2] * 0.2158037573f);
86 ice::f32 const m2 = (from.v[0][0] * 1.0f) + (from.v[0][1] * -0.1055613458f) + (from.v[0][2] * -0.0638541728f);
87 ice::f32 const s2 = (from.v[0][0] * 1.0f) + (from.v[0][1] * -0.0894841775f) + (from.v[0][2] * -1.291485548f);
88
89 // Cube
90 ice::f32 const l3 = l2 * l2 * l2;
91 ice::f32 const m3 = m2 * m2 * m2;
92 ice::f32 const s3 = s2 * s2 * s2;
93
94 ice::f32 const r_lin = (+4.0767416621f * l3) - (3.3077115913f * m3) + (0.2309699292f * s3);
95 ice::f32 const g_lin = (-1.2684380046f * l3) + (2.6097574011f * m3) - (0.3413193965f * s3);
96 ice::f32 const b_lin = (-0.0041960863f * l3) - (0.7034186147f * m3) + (1.7076147010f * s3);
97
98 // Oklch to Oklab
99 return { r_lin, g_lin, b_lin };
100 }
101
106 constexpr auto compute_max_saturation(ice::f32 a, ice::f32 b) noexcept -> ice::f32
107 {
108 // Max saturation will be when one of r, g or b goes below zero.
109 // Select different coefficients depending on which component goes below zero first
110 ice::f32 k0, k1, k2, k3, k4, wl, wm, ws;
111
112 if (-1.88170328f * a - 0.80936493f * b > 1)
113 {
114 // Red component
115 k0 = +1.19086277f; k1 = +1.76576728f; k2 = +0.59662641f; k3 = +0.75515197f; k4 = +0.56771245f;
116 wl = +4.0767416621f; wm = -3.3077115913f; ws = +0.2309699292f;
117 }
118 else if (1.81444104f * a - 1.19445276f * b > 1)
119 {
120 // Green component
121 k0 = +0.73956515f; k1 = -0.45954404f; k2 = +0.08285427f; k3 = +0.12541070f; k4 = +0.14503204f;
122 wl = -1.2684380046f; wm = +2.6097574011f; ws = -0.3413193965f;
123 }
124 else
125 {
126 // Blue component
127 k0 = +1.35733652f; k1 = -0.00915799f; k2 = -1.15130210f; k3 = -0.50559606f; k4 = +0.00692167f;
128 wl = -0.0041960863f; wm = -0.7034186147f; ws = +1.7076147010f;
129 }
130
131 // Approximate max saturation using a polynomial:
132 ice::f32 const s_initial = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b;
133
134 // Do one step Halley's method to get closer
135 // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite
136 // this should be sufficient for most applications, otherwise do two/three steps
137 ice::f32 const k_l = +0.3963377774f * a + 0.2158037573f * b;
138 ice::f32 const k_m = -0.1055613458f * a - 0.0638541728f * b;
139 ice::f32 const k_s = -0.0894841775f * a - 1.2914855480f * b;
140
141 {
142 ice::f32 const l_ = 1.f + s_initial * k_l;
143 ice::f32 const m_ = 1.f + s_initial * k_m;
144 ice::f32 const s_ = 1.f + s_initial * k_s;
145
146 ice::f32 const l = l_ * l_ * l_;
147 ice::f32 const m = m_ * m_ * m_;
148 ice::f32 const s = s_ * s_ * s_;
149
150 ice::f32 const l_ds = 3.f * k_l * l_ * l_;
151 ice::f32 const m_ds = 3.f * k_m * m_ * m_;
152 ice::f32 const s_ds = 3.f * k_s * s_ * s_;
153
154 ice::f32 const l_ds2 = 6.f * k_l * k_l * l_;
155 ice::f32 const m_ds2 = 6.f * k_m * k_m * m_;
156 ice::f32 const s_ds2 = 6.f * k_s * k_s * s_;
157
158 ice::f32 const f = wl * l + wm * m + ws * s;
159 ice::f32 const f1 = wl * l_ds + wm * m_ds + ws * s_ds;
160 ice::f32 const f2 = wl * l_ds2 + wm * m_ds2 + ws * s_ds2;
161
162 return s_initial - f * f1 / (f1*f1 - 0.5f * f * f2);
163 }
164 }
165
168 constexpr auto find_cusp(ice::f32 a, ice::f32 b) noexcept -> ice::detail::OkLCH_HueCusp
169 {
171 // First, find the maximum saturation (saturation S = C/L)
172 ice::f32 const s_cusp = compute_max_saturation(a, b);
173
174 // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1:
175 ice::vec3f const rgb_at_max = ice::detail::from_oklab_to_lrgb({ 1, s_cusp * a, s_cusp * b });
176 ice::f32 const l_cusp = ice::math::cbrt(
177 1.f / ice::max_of(rgb_at_max.v[0][0], rgb_at_max.v[0][1], rgb_at_max.v[0][2])
178 );
179
180 ice::f32 const c_cusp = l_cusp * s_cusp;
181 return { a, b, l_cusp , c_cusp };
182 }
183
186 constexpr auto find_cusp_ch(ice::f32 chroma, ice::rad hue) noexcept -> ice::detail::OkLCH_HueCusp
187 {
188 ice::f32 const C = std::max(ice::f32_eps, chroma);
189 ice::f32 const oklab_a = chroma * ice::math::cos(hue);
190 ice::f32 const oklab_b = chroma * ice::math::sin(hue);
191 ice::f32 const oklab_a_ = oklab_a / C;
192 ice::f32 const oklab_b_ = oklab_b / C;
193
194 return find_cusp(oklab_a_, oklab_b_);
195 }
196
197#if 0 // The lookup into the table can be much slower than calculating this due to reaching into an uncached memory location.
198 static constexpr std::array<ice::vec2f, 3600> Constant_HueCuspTable = []() noexcept -> std::array<ice::vec2f, 3600>
199 {
200 ice::deg hue = { 0.0f };
201 ice::f32 const hue_last = hue.value - ice::f32_eps;
202 std::array<ice::vec2f, 3600> result{};
203 while (hue.value < hue_last)
204 {
207
208 ice::f32 const C = ice::max(ice::f32_eps, ice::sqrt(a * a + b * b));
209 ice::f32 const a_ = a / C;
210 ice::f32 const b_ = b / C;
211
212 OkLCH_HueCusp const cusp = find_cusp(a_, b_);
213 result[ice::u32(hue.value * 10.f)] = { cusp.lightness, cusp.chroma };
214 hue.value += 0.1f;
215 }
216
217 return result;
218 }();
219
222 constexpr auto find_cusp(ice::deg hue) noexcept -> ice::vec2f
223 {
225 return Constant_HueCuspTable[ice::u32(hue.value * 10.f)];
226 }
227#endif
228
229 // Finds intersection of the line defined by
230 // L = L0 * (1 - t) + t * L1;
231 // C = t * C1;
232 // a and b must be normalized so a^2 + b^2 == 1
236 ice::f32 L1,
237 ice::f32 C1,
238 ice::f32 L0
239 ) noexcept -> ice::f32
240 {
241 // Find the intersection for upper and lower half seprately
242 ice::f32 t = 0.0f;
243 if (((L1 - L0) * cusp.chroma - (cusp.lightness - L0) * C1) <= 0.f)
244 {
245 // Lower half
246 t = cusp.chroma * L0 / (C1 * cusp.lightness + cusp.chroma * (L0 - L1));
247 }
248 else
249 {
250 // Upper half
251
252 // First intersect with triangle
253 t = cusp.chroma * (L0 - 1.f) / (C1 * (cusp.lightness - 1.f) + cusp.chroma * (L0 - L1));
254
255 // Then one step Halley's method
256 {
257 ice::f32 const dL = L1 - L0;
258 ice::f32 const dC = C1;
259
260 ice::f32 const k_l = +0.3963377774f * cusp.a + 0.2158037573f * cusp.b;
261 ice::f32 const k_m = -0.1055613458f * cusp.a - 0.0638541728f * cusp.b;
262 ice::f32 const k_s = -0.0894841775f * cusp.a - 1.2914855480f * cusp.b;
263
264 ice::f32 const l_dt = dL + dC * k_l;
265 ice::f32 const m_dt = dL + dC * k_m;
266 ice::f32 const s_dt = dL + dC * k_s;
267
268
269 // If higher accuracy is required, 2 or 3 iterations of the following block can be used:
270 //for (ice::u32 iter = 0; iter < 3; ++iter)
271 {
272 ice::f32 const L = L0 * (1.f - t) + t * L1;
273 ice::f32 const C = t * C1;
274
275 ice::f32 const l_ = L + C * k_l;
276 ice::f32 const m_ = L + C * k_m;
277 ice::f32 const s_ = L + C * k_s;
278
279 ice::f32 const l = l_ * l_ * l_;
280 ice::f32 const m = m_ * m_ * m_;
281 ice::f32 const s = s_ * s_ * s_;
282
283 ice::f32 const ldt = 3 * l_dt * l_ * l_;
284 ice::f32 const mdt = 3 * m_dt * m_ * m_;
285 ice::f32 const sdt = 3 * s_dt * s_ * s_;
286
287 ice::f32 const ldt2 = 6 * l_dt * l_dt * l_;
288 ice::f32 const mdt2 = 6 * m_dt * m_dt * m_;
289 ice::f32 const sdt2 = 6 * s_dt * s_dt * s_;
290
291 ice::f32 const r = 4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s - 1;
292 ice::f32 const r1 = 4.0767416621f * ldt - 3.3077115913f * mdt + 0.2309699292f * sdt;
293 ice::f32 const r2 = 4.0767416621f * ldt2 - 3.3077115913f * mdt2 + 0.2309699292f * sdt2;
294
295 ice::f32 const u_r = r1 / (r1 * r1 - 0.5f * r * r2);
296 ice::f32 const t_r = -r * u_r;
297
298 ice::f32 const g = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s - 1;
299 ice::f32 const g1 = -1.2684380046f * ldt + 2.6097574011f * mdt - 0.3413193965f * sdt;
300 ice::f32 const g2 = -1.2684380046f * ldt2 + 2.6097574011f * mdt2 - 0.3413193965f * sdt2;
301
302 ice::f32 const u_g = g1 / (g1 * g1 - 0.5f * g * g2);
303 ice::f32 const t_g = -g * u_g;
304
305 ice::f32 const b0 = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s - 1;
306 ice::f32 const b1 = -0.0041960863f * ldt - 0.7034186147f * mdt + 1.7076147010f * sdt;
307 ice::f32 const b2 = -0.0041960863f * ldt2 - 0.7034186147f * mdt2 + 1.7076147010f * sdt2;
308
309 ice::f32 const u_b = b1 / (b1 * b1 - 0.5f * b0 * b2);
310 ice::f32 const t_b = -b0 * u_b;
311
312 ice::f32 const t_r2 = u_r >= 0.f ? t_r : ice::f32_max;
313 ice::f32 const t_g2 = u_g >= 0.f ? t_g : ice::f32_max;
314 ice::f32 const t_b2 = u_b >= 0.f ? t_b : ice::f32_max;
315
316 t += ice::min_of(t_r2, t_g2, t_b2);
317 }
318 }
319 }
320
321 return t;
322 }
323
324} // namespace ice::detail
Definition hashmap_details.hxx:13
constexpr auto find_cusp(ice::f32 a, ice::f32 b) noexcept -> ice::detail::OkLCH_HueCusp
Finds L_cusp and C_cusp for a given 'a' and 'b' values of OKLAB color.
Definition color_details.hxx:168
constexpr auto find_cusp_ch(ice::f32 chroma, ice::rad hue) noexcept -> ice::detail::OkLCH_HueCusp
Finds L_cusp and C_cusp for a given 'a' and 'b' values of OKLAB color.
Definition color_details.hxx:186
constexpr auto from_oklab_to_lrgb(ice::math::vec3f from) noexcept -> ice::math::vec3f
from: https://bottosson.github.io/posts/oklab/
Definition color_details.hxx:81
constexpr auto linear_to_srgb(ice::f32 x) noexcept -> ice::f32
Definition color_details.hxx:29
constexpr auto compute_max_saturation(ice::f32 a, ice::f32 b) noexcept -> ice::f32
Compute max saturation for sRGB.
Definition color_details.hxx:106
constexpr auto srgb_to_linear(ice::f32 x) noexcept -> ice::f32
Definition color_details.hxx:45
constexpr auto find_gamut_intersection(ice::detail::OkLCH_HueCusp cusp, ice::f32 L1, ice::f32 C1, ice::f32 L0) noexcept -> ice::f32
Definition color_details.hxx:234
rad32 rad
Definition angles.hxx:170
constexpr auto max(arr_t< Size, T > left, arr_t< Size, U > right) noexcept -> arr_t< Size, T >
Definition array_operations.hxx:49
deg32 deg
Definition angles.hxx:169
constexpr auto cbrt(f32 val) noexcept -> f32
Definition common.hxx:68
constexpr auto max_of(U first, Args... args) noexcept
Definition algorithm.hxx:54
constexpr auto min_of(U first, Args... args) noexcept
Definition algorithm.hxx:73
constexpr auto sin(rad radians) noexcept -> f32
Definition common.hxx:156
static constexpr ice::f32 f32_eps
Definition constants.hxx:10
constexpr auto cos(rad radians) noexcept -> f32
Definition common.hxx:186
auto pow(f32 base, f32 exp) noexcept -> f32
Definition common.hxx:63
vec< 2, f32 > vec2f
Definition vector.hxx:178
constexpr auto sqrt(f32 val) noexcept -> f32
Definition common.hxx:93
constexpr auto radians(deg degrees) noexcept -> rad
Definition common.hxx:33
vec< 3, f32 > vec3f
Definition vector.hxx:182
constexpr ice::f32 const f32_max
Definition constants.hxx:29
std::uint32_t u32
Definition types.hxx:26
float f32
Definition types.hxx:16
#define IPT_ZONE_SCOPED
Definition profiler.hxx:57
Holds information where the 'lightness' and 'chroma' are at the highest point of the OkLCH color curv...
Definition color_details.hxx:19
ice::f32 chroma
Definition color_details.hxx:22
ice::f32 lightness
Definition color_details.hxx:21
ice::f32 b
Definition color_details.hxx:20
ice::f32 a
Definition color_details.hxx:20
T v[count_columns][count_rows]
Definition matrix.hxx:17