1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
//! An opinionated color management library built on top of [`kolor`](https://docs.rs/kolor). //! //! # Introduction //! //! `colstodian` is a practical color management library for games and graphics. //! It encodes various information about a color either statically //! in the Rust type system (as with the strongly-typed [`Color`]), //! or as data contained in the type (as with the dynamically-typed [`DynamicColor`]). //! //! Although it is designed to prevent footguns wherever possible, in order to make use of this library, //! you should have a well-working understanding of basic color science and encoding principles. //! As such, I highly recommend you give sections 2.0 and 2.1 of this document a read, as it is one //! of the main inspirations for how this library is structured: //! //! All colors in Ark will now be accompanied either statically or dynamically with two important pieces //! of metadata, a **color space** and a **state**. A basic background on color encoding is necessary to //! understand what these pieces of metadata are and why they are important. //! //! ## Color Encoding Basics //! //! Much like how a 3d vector like a `glam::Vec3` could be used to describe any of: //! //! * The motion vector of an object in meters per second //! * The position of an object relative to a reference point in kilometers //! * Three "wellness scores" for a character, which each axis representing how happy the charcter is about some aspect of their life //! //! A bag of components that describes "a color" could actually be interpreted in many different ways, and the end result of what //! those components mean is very different. There are two important pieces of metadata about a color which inform how we are meant //! to interpret its component values: the color's **Color Space** and its **State**. //! //! ### Color Spaces //! //! A "color space" is a fairly nebulous term which has different definitions depending on who you talk to, but the basic idea is that //! it provides a specific organization of color data in an agreed-upon format. The color space provides almost all the information needed //! to fully interpret the component data. However, it is missing one important piece of metadata which is relevant when working with rendered //! scenes that may have higher dynamic range within the scene than an actual display is capable of displaying (a computer monitor cannot replicate //! the brightness of the sun, but within the renderer, we want to actually simulate those high brightnesses). That is where the color state comes in. //! //! ### Color State //! //! As we have discussed, all colors have units. Sometimes a color’s units are explicit, such as measuring the emitted light from a display using a //! radiometric measurement tool and being able to reference pixel values in a color space built for that. Other times, the units are only indirectly //! related to the real world, but come with a mathematical conversion to measurable quantities. For example, in the case of display technology, common //! color encodings include sRGB, DCI-P3, and BT.2020, which are all standards which actual monitors attempt to replicate. //! //! However, considering color as a displayed quantity only provides part of the color encoding story. In addition to relating color values to //! **display measurements**, as we did above, one can also relate color values to the performance characteristics of an **input device** (i.e., a //! camera, or in our case, a virtual camera in a 3d renderer). In this case, we are quantifying color values which originated in the (virtual) **scene**, //! rather than ones being displayed on a display. This kind of color can be measured in real world units as well. In the case of a 3d renderer, these units //! are often defined in the renderer as a photometric quantity like luminance, with the relation to reference color values dictated by a defined transformation. //! //! It is a meaningful abstraction to categorize colors based on this distinction of *input* versus *output* reference. We refer to this //! difference as a color's **State**. Colors which are defined in relation to *display characteristic* are called **Display-referred**, while //! color spaces which are defined in relation to *input devices* (scenes) are **Scene-referred**. //! //! # Overview //! //! `colstodian` is broken up into two 'halves', a statically-typed half which is meant to be //! used as much as possible to help you prevent errors at compile time through leveraging the //! Rust type system, and a dynamically-typed half which is meant to be used when serializing //! and deserializing colors and otherwise interacting with colors from dynamic sources not //! known at compile time. //! //! The core of the statically-typed half is the [`Color`] type, which encodes //! two important pieces of metadata about the color in its type signature (`Color<Space, State>`): the //! color's **color space** and **state**. If you read the color encoding basics above (you did, didn't you? ;) ) //! then you should have a decent idea of what both of these things mean. To be clear, the **color space** //! encodes the **primaries**, **white point**, and **transfer functions** upon which the color values are //! based. The **state** encodes in which "direction" we relate the color values to real-world quantities: //! either **scene-referred** or **display-referred**. Types which implement the [`ColorSpace`] and //! [`State`] traits encode this information statically. Color spaces can be found in the `spaces` module //! and states can be found in the `states` module. //! //! The core of the dynamically-typed half is the [`DynamicColor`] type, which encodes the color space //! and state as data stored in the type at runtime. It stores these as [`DynamicColorSpace`]s and [`DynamicState`]s. //! //! # Example //! //! Let's say we have a color that we got from an asset loaded from a color image or a color picker, //! which are often in the encoded sRGB color space. //! //! ```rust //! # use colstodian::*; //! let loaded_asset_color = color::srgb_u8(128, 128, 128); //! ``` //! //! But wait, we can't do much with this color yet... //! //! ```rust //! # use colstodian::*; //! # let loaded_asset_color = color::linear_srgb::<Display>(0.5, 0.5, 0.5); // fake it so that doc tests pass //! let my_other_color = loaded_asset_color * 5.0; // oops, compile error! //! ``` //! //! This color is encoded in a non-linear format. You can think of this much like //! as if a file was compressed as a ZIP. Doing operations directly on the zipped //! bytes is nonsensical. First we need to decode it to work on the raw data. //! In the same way, before we can do math on this color, we need to convert it to a working color space. //! //! Encoded color spaces all have a working color space that they can decode to directly. This will be the //! least expensive and most natural conversion if you want to work with them directly. For example, //! an [EncodedSrgb] color will decode to a [LinearSrgb] color: //! //! ```rust //! # use colstodian::*; //! # let loaded_asset_color = color::srgb(0.5, 0.5, 0.5); //! // Note the type annotation here is unnecessary, but is useful for illustrative purposes. //! let decoded: Color<LinearSrgb, Display> = loaded_asset_color.decode(); //! //! let my_other_color = decoded * 0.5; // yay, it works! //! ``` //! //! You can also convert an encoded color fully to a specific working space if you have one //! in mind. For example, if you want to blend between two colors, you might convert them to //! the [Oklab] color space: //! //! ```rust //! # use colstodian::*; //! //! let oklab1 = color::srgb_u8(128, 12, 57).convert::<Oklab>(); //! let oklab2 = color::srgb_u8(25, 35, 68).convert::<Oklab>(); //! //! let blended = oklab1.blend(oklab2, 0.5); // Blend half way between the two colors //! ``` //! //! This is also the first time we see the [`convert`][Color::convert] method, which we'll be using, //! along with its sibling [`convert_to`][Color::convert_to], //! to do most of our conversions. You can use this to do pretty much any conversion you like, so long //! as you stay within the same [State]. See the docs of that method for more information. Generally, you'll //! want to convert the color to some output color space before actually using it. It's quite common to use //! [EncodedSrgb] for this purpose. This is also quite simple with `convert`: //! //! ```rust //! # use colstodian::*; //! # let blended: Color<Oklab, Display> = Color::new(1.0, 1.0, 1.0); //! // Note the slightly different style. Here we annotate the type of `output` //! // rather than using the turbofish operator to specify the destination color //! // space, and Rust infers the type on the `convert` method for us. //! let output: Color<EncodedSrgb, Display> = blended.convert(); //! //! // Some applications will want a color in the form of an array of `u8`s. //! // Certain encoded color spaces will allow you to convert a color in that //! // space to/from an array of `u8`s. EncodedSrgb is one of those: //! let output_u8: [u8; 3] = output.to_u8(); //! ``` //! //! Here we can see an example of where [`convert_to`][Color::convert_to] may be preferrable over //! [`convert`][Color::convert]. Notice how we are using the type [`Color<EncodedSrgb, Display>`] quite //! often? You might want to create a type alias for this type called, for example, `Asset`. Wouldn't it //! be convenient to also be able to `convert` to a type alias of [`Color`]? Well, with //! [`convert_to`][Color::convert_to], you can! This is particularly useful when you don't want to //! bind the output to a variable directly, so you can't take advantage of type inference and //! need to use the turbofish operator. For example, let's rewrite the previous blending example: //! //! ```rust //! # use colstodian::*; //! // You could have these defined and used throughout your codebase. //! type Perceptual = Color<Oklab, Display>; //! type Srgb = Color<EncodedSrgb, Display>; //! //! let color_1 = color::srgb_u8(128, 12, 57); //! let color_2 = color::srgb_u8(25, 35, 68); //! //! let blended_u8: [u8; 3] = color_1.convert_to::<Perceptual>().blend( //! color_2.convert_to::<Perceptual>(), //! 0.5 //! ).convert_to::<Srgb>().to_u8(); //! ``` //! //! [`convert_to`][Color::convert_to] can also take a [`ColorSpace`] as a Query directly. However, //! because it's more generic than [`convert`][Color::convert], Rust's type system will often not //! be able to infer the type of Query, for example annotating a type as being a specific [`Color`] //! type and then calling `other_color.convert_to()` will give you a type annotation needed error. //! //! Going back to what you can and cannot do with different [`Color`] types, //! note that you can break out of the restrictions imposed by the type system //! or otherwise get the raw color by accessing `color.raw`: //! //! ```rust //! # use colstodian::*; //! let mut encoded_color = color::srgb_u8(127, 127, 127); //! encoded_color.raw *= 0.5; // This works! But be careful that you know what you're doing. //! ``` //! //! You can also access the components of a color by that component's name. For example, //! a [Linear sRGB][LinearSrgb] color has components `r`, `g`, and `b`, so you can do: //! //! ```rust //! # use colstodian::*; //! # let linear_srgb_color = color::linear_srgb::<Display>(0.5, 0.5, 0.5); //! let red_component = linear_srgb_color.r; //! ``` //! //! However, if a color is in a different color space, for example [`ICtCpPQ`], which has different //! component names, then you would access those components accordingly: //! //! ```rust //! # use colstodian::*; //! let col: Color<ICtCpPQ, Display> = Color::new(1.0, 0.2, 0.2); //! //! let intensity = col.i; // acces I (Intensity) component through .i //! let ct = col.ct; // access Ct (Chroma-Tritan) component through .ct //! let cp = col.cp; // access Cp (Chroma-Protan) component through .cp //! ``` //! //! One more quite useful tool is the [`ColorInto`] trait. [`ColorInto`] is //! a trait meant to be used as a replacement for [`Into`] in situations where you want //! to bound a type as being able to be converted into a specific type of color. A you can //! call [`.into`][ColorInto::into] on a type that implements [`ColorInto<T>`] //! and you will get a `T`. //! //! This example snippet puts together much of what we've learned so far. //! //! ```rust //! # use colstodian::*; //! fn tint_color(input_color: impl ColorInto<Color<AcesCg, Display>>) -> Color<AcesCg, Display> { //! let color = input_color.into(); //! let tint: Color<AcesCg, Display> = Color::new(0.5, 0.8, 0.4); //! color * tint //! } //! //! let color = color::srgb_u8(225, 200, 86); //! let tinted: Color<EncodedSrgb, Display> = tint_color(color).convert(); //! //! println!("Pre-tint: {}, Post-tint: {}", color, tinted); //! ``` //! //! Now, let's go back to our previous `decoded` color from the begining. //! //! Let's say that instead of blending perceptually between colors, we are creating a 3d rendering //! engine. In this case, we probably want to do the actual shading math in a color space with a //! wider (sharper) gamut (the reasons for this are outside the scope of this demo). The [ACEScg][AcesCg] //! space is ideal for this. //! //! Since both color spaces are linear, the ideally optimized transformation is a simple //! 3x3 matrix * 3 component vector multiplication. `colstodian` is architected such that we can still //! just use the [`convert`][Color::convert] method to convert between these spaces and it will //! indeed optimize fully down to just that multiplication. //! //! ```rust //! # use colstodian::*; //! # let decoded = color::linear_srgb::<Display>(0.5, 0.5, 0.5); //! let col: Color<AcesCg, Display> = decoded.convert(); //! ``` //! //! Now, we come to a bit of a subtle operation. Here we will convert the color from being in a display-reffered //! state to being in a scene-referred state. This operation is not necessarily concrete, and is dependent on //! the thing you are converting. Going from display-referred to scene-referred, we are converting from a //! bounded dynamic range with physical reference units for the color component values being //! (with a properly calibrated monitor) the **display standard specification**, //! to an unbounded dynamic range, with color components in the range `[0..inf)` and the physical reference units //! for these component values being the units used in the **scene,** which are defined by the renderer itself. //! In most cases, these units will be in a measurement of photometric luminance like [Cd/m^2 aka nits](https://en.wikipedia.org/wiki/Candela_per_square_metre). //! //! One possible use of this conversion is the case of an emissive texture, where we may want to modify the bounded //! [illuminance (i.e. lux)](https://en.wikipedia.org/wiki/Illuminance) of the color we stored in the texture by some //! unbounded *power* value stored elsewhere. In this way, we can make emissive materials just as powerful as any other //! light in the scene. //! //! ```rust //! # use colstodian::*; //! # let col = color::acescg::<Display>(0.5, 0.5, 0.5); //! let power = 5.0; // Say you loaded this from an asset somewhere //! //! // Note the `Scene` state... previously, all colors have been in `Display` state. //! let emissive_col: Color<AcesCg, Scene> = col.convert_state(|c| c * power); //! ``` //! //! Now we can do the actual rendering math, using this scene-referred color value. //! //! ```text //! // ... rendering math here ... //! ``` //! //! Okay, so let's say we've ended up with a final color for a pixel, which is still scene-referred in the //! ACEScg color space, representing the luminance reaching the camera from a specific direction (namely, the direction //! corresponding to the pixel we are shading). //! //! ```rust //! # use colstodian::*; //! let rendered_col = color::acescg::<Scene>(5.0, 4.0, 4.5); // let's just say this is the computed final color. //! ``` //! //! Now we need to do the opposite of what we did before and map the infinite dynamic range of a //! scene-referred color outputted by the renderer to the finite dynamic range which can be displayed //! on a display. For an output display which is "SDR" (i.e. not an HDR-enabled TV or monitor), a fairly //! aggressive S-curve style tonemap is a good option. We provide a couple of options in the [`tonemap`] module. //! //! ```rust //! # use colstodian::*; //! # let rendered_col = color::acescg::<Scene>(5.0, 4.0, 4.5); //! use tonemap::{Tonemapper, PerceptualTonemapper, PerceptualTonemapperParams}; //! //! // In theory you could change the parameters to taste here. //! let params = PerceptualTonemapperParams::default(); //! let tonemapped: Color<AcesCg, Display> = PerceptualTonemapper::tonemap(rendered_col, params).convert(); //! ``` //! //! Now, our color is display-referred within a finite (`[0..1]`) dynamic range. However, we haven't chosen //! an actual specific display to encode it for. This is what the sRGB standard can help with, which //! is most likely the standard upon which an LDR monitor will be based. We can convert our color to //! [encoded sRGB][EncodedSrgb] just like we showed before. //! //! ```rust //! # use colstodian::*; //! # let tonemapped = color::acescg::<Display>(5.0, 4.0, 4.5); //! let encoded = tonemapped.convert::<EncodedSrgb>(); // Ready to display or write to an image. //! //! // Again, if your output format needs `u8`s (say, an 8-bit PNG image), you can use the `to_u8()` method. //! let u8s: [u8; 3] = encoded.to_u8(); //! ``` //! //! Alternatively, we could output to a different display, for example to a wide-gamut but still LDR //! BT.2020 calibrated display: //! //! ```rust //! # use colstodian::*; //! # let tonemapped = color::acescg::<Display>(5.0, 4.0, 4.5); //! let encoded = tonemapped.convert::<EncodedBt2020>(); //! ``` //! //! This doesn't cover displaying to an HDR display yet, nor the use of colors with an alpha channel, but it soon will! //! //! # Further Resources //! //! Here is a curated list of further resources to check out for information about color encoding and management. //! //! * An overview of color management from a cinematic perspective (HIGHLY recommend sections 2.0 and 2.1): <http://github.com/jeremyselan/cinematiccolor/raw/master/ves/Cinematic_Color_VES.pdf> //! * The Hitchhiker's Guide to Digital Color: <https://hg2dc.com/> //! * Alex Fry (DICE/Frostbite) on HDR color management in Frostbite: <https://www.youtube.com/watch?v=7z_EIjNG0pQ> //! * Timothy Lottes (AMD) on "variable" dynamic range color management: <https://www.gdcvault.com/play/1023512/Advanced-Graphics-Techniques-Tutorial-Day> //! * Hajime Uchimura and Kentaro Suzuki on HDR and Wide color strategies in Gran Turismo SPORT: <https://www.polyphony.co.jp/publications/sa2018/> #![cfg_attr(not(feature = "std"), no_std)] pub use kolor; /// Types representing different color spaces. /// /// For more information, see the documentation for the corresponding /// dynamic color space (you can figure out the corresponding dynamic color space by looking at the) /// implementation of the [`ColorSpace`] trait on a specific color space struct. #[rustfmt::skip] pub mod spaces; #[doc(inline)] pub use spaces::DynamicColorSpace; pub use spaces::*; /// Contains types relating to a color's state. pub mod states; #[doc(inline)] pub use states::{Display, DynamicState, Scene}; /// Contains types relating to a color's alpha state. pub mod alpha_states; #[doc(inline)] pub use alpha_states::{DynamicAlphaState, Premultiplied, Separate}; /// Contains tonemappers, useful for mapping scene-referred HDR values into display-referred values /// within the concrete dynamic range of a specific display. pub mod tonemap; pub mod component_structs; pub use component_structs::*; /// Contains color types and helper functions. pub mod color; #[doc(inline)] pub use color::{Color, ColorAlpha, DynamicColor, DynamicColorAlpha}; /// The traits which form the backbone of the strongly-typed [`Color`] & [`ColorAlpha`]. pub mod traits; #[doc(inline)] pub use traits::{AlphaState, ColorInto, ColorSpace, State}; /// Error handling types. pub mod error; #[doc(inline)] pub use error::{ColorError, ColorResult}; #[cfg(test)] mod tests { use super::*; trait EqualsEps { fn eq_eps(self, other: Self, eps: f32) -> bool; } impl EqualsEps for f32 { fn eq_eps(self, other: f32, eps: f32) -> bool { (self - other).abs() <= eps } } impl<Spc, St> EqualsEps for Color<Spc, St> { fn eq_eps(self, other: Color<Spc, St>, eps: f32) -> bool { self.raw.x.eq_eps(other.raw.x, eps) && self.raw.y.eq_eps(other.raw.y, eps) && self.raw.z.eq_eps(other.raw.z, eps) } } macro_rules! assert_eq_eps { ($left:expr, $right:expr, $eps:expr) => {{ match (&($left), &($right)) { (left_val, right_val) => { if !(left_val.eq_eps(*right_val, $eps)) { // The reborrows below are intentional. Without them, the stack slot for the // borrow is initialized even before the values are compared, leading to a // noticeable slow down. panic!( r#"assertion failed: `(left ~= right with epsilon {})` left: `{:?}`, right: `{:?}`"#, $eps, &*left_val, &*right_val ) } } } }}; } #[test] fn basic() { let orig = Color::<EncodedSrgb, Display>::new(0.5, 0.5, 0.5); let col: Color<LinearSrgb, Display> = orig.convert::<LinearSrgb>(); let col: Color<AcesCg, Display> = col.convert(); let oklab = col.convert::<Oklab>(); assert_eq_eps!(orig.convert::<AcesCg>(), col, 0.0001); assert_eq_eps!(orig.convert::<Oklab>(), oklab, 0.0001); } #[test] fn round_trip() { let orig = Color::<EncodedSrgb, Display>::new(0.5, 0.5, 0.5); let col: Color<LinearSrgb, Display> = orig.convert(); let col: Color<AcesCg, Display> = col.convert(); let col: Color<AcesCg, Scene> = col.convert_state(|c| c * 5.0); let col: Color<AcesCg, Display> = col.convert_state(|c| c / 5.0); let col: Color<LinearSrgb, Display> = col.convert(); let fin: Color<EncodedSrgb, Display> = col.convert(); assert_eq_eps!(orig, fin, 0.0001); } #[test] fn deref() { let col: Color<EncodedSrgb, Display> = Color::new(0.2, 0.3, 0.4); let r = col.r; let g = col.g; let b = col.b; assert_eq_eps!(r, 0.2, 0.0001); assert_eq_eps!(g, 0.3, 0.0001); assert_eq_eps!(b, 0.4, 0.0001); } #[test] fn deref_alpha() { let colalpha: ColorAlpha<EncodedSrgb, Premultiplied> = ColorAlpha::new(0.2, 0.3, 0.4, 0.5); let r = colalpha.col.r; let g = colalpha.col.g; let b = colalpha.col.b; let alpha = colalpha.alpha; assert_eq_eps!(r, 0.2, 0.0001); assert_eq_eps!(g, 0.3, 0.0001); assert_eq_eps!(b, 0.4, 0.0001); assert_eq_eps!(alpha, 0.5, 0.0001); } }