5 个版本
0.1.0-rc.3 | 2021年7月15日 |
---|---|
0.1.0-rc.2 | 2021年5月19日 |
0.1.0-rc.1 | 2021年5月17日 |
0.1.0-alpha.1 | 2021年5月9日 |
#520 in 图形API
130KB
2K SLoC
colstodian
简介
colstodian
是一个为游戏和图形设计的实用、具有见解的颜色管理库。更多信息,请参阅由 git main
构建的最新文档 文档。
许可证
许可证为以下之一:
- Apache License, Version 2.0, (https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT 许可证 (http://opensource.org/licenses/MIT)
- Zlib 许可证 (https://opensource.org/licenses/Zlib)
任选其一。
贡献
除非您明确声明,否则根据 Apache-2.0 许可证定义的,您有意提交以包含在工作中的任何贡献,都将按上述方式双许可,不附加任何额外条款或条件。
lib.rs
:
基于 kolor
构建的具有见解的颜色管理库。
简介
colstodian
是一个为游戏和图形设计的实用颜色管理库。它将有关颜色的各种信息编码为静态的 Rust 类型系统(如强类型的 Color
),或者作为类型中的数据(如动态类型的 DynamicColor
)。
尽管它旨在尽可能防止错误,但为了使用此库,您应该对基本颜色科学和编码原理有良好的理解。因此,我强烈建议您阅读此文档的第2.0节和第2.1节,因为它构成了此库结构的主要灵感来源之一。
在《方舟》中,所有颜色现在都将伴随两种重要的元数据,静态或动态地,分别是颜色空间和状态。了解这些元数据是什么以及为什么它们很重要,需要基本的颜色编码背景知识。
颜色编码基础
这与一个3D向量(如glam::Vec3
)可以用来描述任何一样东西的方式非常相似
- 物体的运动向量,以每秒米为单位
- 物体相对于参考点的位置,以公里为单位
- 角色的“健康评分”,每个轴代表角色对其生活中某个方面的满意度
描述“颜色”的组件包可以实际以许多不同的方式解读,这些组件的含义最终结果也非常不同。关于颜色有两个重要的元数据,可以告知我们如何解读其组件值:颜色的颜色空间和其状态。
颜色空间
“颜色空间”是一个相对模糊的术语,其定义因人而异,但其基本思想是,它提供了一种在一致格式下对颜色数据进行特定组织的方式。颜色空间提供了几乎全部用于完全解释组件数据所需的信息。然而,它缺少一个重要的元数据,这在处理场景中可能具有比实际显示功能更高的动态范围的渲染场景时是相关的(电脑显示器无法复制太阳的亮度,但在渲染器内部,我们希望实际上模拟这些高亮度)。这就是颜色状态发挥作用的地方。
颜色状态
正如我们讨论的,所有颜色都有单位。有时颜色的单位是明确的,例如使用辐射测量工具测量显示器的发出的光并能够参考为该颜色空间构建的颜色空间中的像素值。其他时候,单位只是间接与真实世界相关,但带有数学转换到可测量数量的转换。例如,在显示技术的情况下,常见的颜色编码包括sRGB、DCI-P3和BT.2020,这些都是实际显示器试图复制的标准。
然而,仅将颜色视为显示量只能提供颜色编码故事的一部分。除了将颜色值与显示测量相关联,如我们上面所做的那样,人们还可以将颜色值与输入设备(即相机,或在我们这个例子中,3D渲染器中的虚拟相机)的性能特征相关联。在这种情况下,我们量化的是(虚拟)场景中起源的颜色值,而不是显示在显示器上的颜色值。这种颜色的测量也可以以现实世界的单位进行。在3D渲染器的情况下,这些单位通常在渲染器中定义为光度量,如亮度,其与参考颜色值的关系由定义的转换决定。
根据这种区分输入和输出参考来分类颜色是一种有意义的抽象。我们称这种差异为颜色的状态。与显示特征相关的颜色称为显示相关,而与输入设备(场景)相关的颜色空间称为场景相关。
概述
colstodian
被分为两个“部分”,一个是尽可能多地用于帮助你在编译时通过利用 Rust 类型系统来预防错误的静态类型部分,另一个是用于序列化和反序列化颜色以及与编译时不为人知的动态来源的颜色进行交互的动态类型部分。
静态类型部分的核心是 Color
类型,它在类型签名中编码了关于颜色的两个重要元数据(Color<Space, State>
):颜色的 色彩空间 和 状态。如果你阅读了上面的颜色编码基础知识(你读过了,不是吗? ;)),那么你应该对这两个概念有了相当的了解。为了明确起见,色彩空间 编码了基于颜色值的 基色、白点 和 转换函数。而 状态 编码了我们如何将颜色值与实际世界的数量联系起来:“场景相关”或“显示相关”。实现 ColorSpace
和 State
特性的类型将此信息静态化。色彩空间可以在 spaces
模块中找到,而状态可以在 states
模块中找到。
动态类型部分的核心是 DynamicColor
类型,它在运行时将色彩空间和状态编码为存储在类型中的数据。它将这些存储为 DynamicColorSpace
和 DynamicState
。
示例
假设我们从一个加载的颜色图像或颜色选择器中获取了一个颜色,这些通常编码在编码的 sRGB 色彩空间中。
let loaded_asset_color = color::srgb_u8(128, 128, 128);
但是等等,我们目前还不能对这个颜色做太多...
let my_other_color = loaded_asset_color * 5.0; // oops, compile error!
这个颜色被编码在一个非线性格式中。你可以把它想象成文件被压缩成 ZIP。直接在压缩的字节上执行操作是没有意义的。首先我们需要解码它来处理原始数据。同样地,在我们可以对这种颜色进行数学运算之前,我们需要将其转换为一个可工作的色彩空间。
所有编码的色彩空间都有一个可以直接解码到的可工作色彩空间。如果你想直接使用它们,这将是最经济且最自然的转换。例如,一个 [EncodedSrgb] 颜色将解码为 [LinearSrgb] 颜色
// 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!
你也可以将一个编码的颜色完全转换为你心中所想的特定工作空间。例如,如果你想混合两种颜色,你可能将它们转换为 [Oklab] 色彩空间
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
这也是我们第一次看到这个convert
方法,我们将使用它及其同族方法convert_to
来进行大部分转换。只要你保持在同一[状态]内,你就可以用它来做几乎任何你想要的转换。请参阅该方法的文档以获取更多信息。通常,你会在实际使用之前将颜色转换为某些输出色彩空间。使用[EncodedSrgb]做这个目的非常常见。使用convert
来做这个也非常简单。
// 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();
在这里,我们可以看到convert_to
可能比convert
更可取的例子。注意我们经常使用类型[Color<EncodedSrgb, Display>
]?你可能想为这个类型创建一个类型别名,比如Asset
。如果也能将类型别名Color
转换为类型别名,那岂不是很方便?使用convert_to
就可以做到这一点!这在你不希望直接绑定输出到变量时非常有用,因为你不能利用类型推断并需要使用turbofish运算符。例如,让我们重写之前的混合示例
// 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
也可以直接将ColorSpace
作为查询。然而,由于它比convert
更通用,Rust的类型系统通常无法推断Query的类型,例如,将类型注释为特定Color
类型并调用other_color.convert_to()
将导致需要类型注释的错误。
回到不同Color
类型可以做什么和不能做什么,注意你可以打破类型系统强加的限制或通过访问color.raw
来获取原始颜色。
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.
你也可以通过该组件的名称来访问颜色的组件。例如,一个[线性sRGB][LinearSrgb]颜色具有组件r
、g
和b
,因此你可以做
let red_component = linear_srgb_color.r;
但是,如果一个颜色在不同的色彩空间中,例如ICtCpPQ
,它有不同命名的组件,那么你应该相应地访问那些组件
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
还有一个非常有用的工具是ColorInto
特质。这个特质是为了替代在需要将类型转换为特定颜色类型的情况下使用的Into
特质而设计的。你可以在实现了ColorInto<T>
的类型的实例上调用.into
,这样你将得到一个T
。
下面的示例片段综合了我们迄今为止学到的很多东西。
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);
现在,让我们回到我们最初从解码得到的decoded
颜色。
假设我们不是在颜色之间进行感知混合,而是在创建一个3D渲染引擎。在这种情况下,我们可能希望在具有更宽(更尖锐)色域的颜色空间中进行实际的着色数学运算(这种做法的原因超出了本演示的范围)。[ACEScg][AcesCg]空间非常适合这个用途。
由于这两个颜色空间都是线性的,理想化的转换是一个简单的3x3矩阵乘以3分量向量。`colstodian`的设计允许我们仍然可以使用convert
方法在这两个空间之间进行转换,并且它确实会完全优化为仅乘法。
let col: Color<AcesCg, Display> = decoded.convert();
现在,我们来到一个有点微妙的操作。在这里,我们将颜色从显示参考状态转换为场景参考状态。这个操作并不一定具体,并且取决于你要转换的东西。从显示参考到场景参考,我们是从具有物理参考单位的有限动态范围(在适当校准的显示器上,颜色成分的物理参考单位是display standard specification
)转换为无限动态范围,颜色成分的范围是[0..inf)
,这些成分值的物理参考单位是场景中使用的单位,由渲染器本身定义。在大多数情况下,这些单位将是光度量亮度,如Cd/m^2 即 nits。
这种转换的一个可能用途是自发光纹理的情况,我们可能希望通过存储在其他地方的未绑定power
值来修改存储在纹理中的颜色的有限illuminance (i.e. lux)
。这样,我们可以使自发光材料与场景中的任何其他光源一样强大。
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);
现在我们可以使用这个场景参考颜色值进行实际的渲染数学运算。
// ... rendering math here ...
好吧,假设我们最终得到了一个像素的最终颜色,这个颜色仍然是ACEScg颜色空间中的场景参考,表示从特定方向到达相机的亮度(即我们要着色的像素的方向)。
let rendered_col = color::acescg::<Scene>(5.0, 4.0, 4.5); // let's just say this is the computed final color.
现在我们需要做与之前相反的事情,将渲染器输出的场景参考颜色无限动态范围映射到显示器上可以显示的有限动态范围。对于“SDR”(即不是HDR电视或显示器)的输出显示器,一个相当激进的S型曲线风格的色调映射是一个不错的选择。我们在tonemap
模块中提供了几个选项。
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();
现在,我们的颜色在有限的(代码[0..1]
)动态范围内显示。然而,我们还没有选择一个实际的特定显示器来对其进行编码。这就是sRGB标准能够帮助的地方,这可能是LDR显示器所基于的标准。我们可以像之前展示的那样,将我们的颜色转换为[编码的sRGB][EncodedSrgb]。
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();
或者,我们可以输出到不同的显示器,例如到一个宽色域但仍为LDR的BT.2020校准显示器。
let encoded = tonemapped.convert::<EncodedBt2020>();
这还没有涵盖到HDR显示的显示,也没有涉及到带有alpha通道的颜色,但很快就会!
其他资源
以下是一些精选的资源列表,供您查找有关颜色编码和管理的信息。
- 从电影视角的色彩管理概述(强烈推荐第2.0和2.1节):[电影色彩指南](http://github.com/jeremyselan/cinematiccolor/raw/master/ves/Cinematic_Color_VES.pdf)
- 数字色彩的旅行者指南:[数字色彩旅行者指南](https://hg2dc.com/)
- Alex Fry(DICE/Frostbite)在Frostbite中的HDR色彩管理:[Frostbite HDR色彩管理](https://www.youtube.com/watch?v=7z_EIjNG0pQ)
- Timothy Lottes(AMD)关于“可变”动态范围色彩管理:[高级图形技术教程](https://www.gdcvault.com/play/1023512/Advanced-Graphics-Techniques-Tutorial-Day)
- Hajime Uchimura和Kentaro Suzuki在《Gran Turismo SPORT》中的HDR和宽色域策略:[Gran Turismo SPORT HDR和宽色域策略](https://www.polyphony.co.jp/publications/sa2018/)
依赖关系
~3.5MB
~100K SLoC