游戏引擎中的渲染组件的作用是生成一个二维图片,在特定的时间从给定的视点观察的方向看到的一个三维空间的状态。他们的生成每一张图片都会被称为帧,他们生成的速度称为帧率。
像素
在每一帧中,游戏引擎的视觉输出基本上是一大堆彩色像素,像素(Pixel)是图像基本的单元,图像尺寸通常是像素的长宽。
常见的显示器由数百万个像素,每个像素都会以一个比例发出红蓝绿三种颜色的光,一种通用制作颜色的系统称为RGB系统,他是游戏引擎通过计算颜色信息并发送给GPU最终显示给用户的方式。
图像的RGB颜色数据存储在一个称之为 FrameBuffer 帧缓冲区的内存块中。根据像素的格式不同,每个像素可以占据多达 128 位的空间。通常用于显示的图像 32位 或者 64位。每个像素的存储被分为4个通道 RGBA,在每像素 32 位的情况下,通常是 8位无符号整数,在每个像素64位的情况下,通常是16位浮点值。RGBA的前三个分量表示为红, 绿, 蓝 3种分量的强度,第4个分量为 alpha 分量,它表示是RGB分量额外的信息。(应该是计算机使用4的倍数比3的倍数更快)
颜色科学
在渲染3D图像之前,我先介绍下人类是如何感知色彩的以及RGB原理,正如我之前(彩色成像基础和应用[还未翻译完成])的篇章,在这篇我也将在下面的内容中继续阐述。
RGB系统作为色彩制作的一种系统之所以盛行的原因是与人类视觉的解剖结构有关。在光线明亮的情况下,眼球中心附近的光感受由视网膜上三种锥体细胞主导,他们对光谱中不同范围(400-750nm)的电磁辐射敏感。这三种锥体细胞被称为光视觉(photopic vision)。它与暗视觉(scotopic vision)不同,在黑暗的情况下,杆状细胞占主导(锥体细胞不活跃),杆状细胞不具备辨别颜色的能力。所以我们晚上很难分辨颜色。在光的辐射强度保持不变的情况下,下图显示了三种锥状细胞对光的相对敏感度与波长的函数关系。L 锥状细胞对感知光亮度的贡献最大,因为视网膜上大约 63% 的锥状细胞( L 锥状细胞),M 锥状细胞约占 31%,S 锥状细胞占其余的 6%。
CIE RGB颜色空间
三个刺激值的线性能力意味着我们可以构建一个三维向量空间。然而,这个叠加的量有点特殊,因为人类视觉物理上可能存在值不一致的情况(视觉心理量)。所以,20世纪的配色实验采用了红色,绿色和蓝色作为LMS刺激的基色。配色实验是以绿光 546.1 nm 蓝光 435.8 nm 红光 700 nm 。如下图(汞蒸汽灯的发射线)
图中具有负值,请注意,单一波长的光如果不等于某个基色(Primary Colors【在混合某种颜色所作为的基础颜色 例如在RGB中R G B颜色的通道所占比例】)那么想要某种的颜色将无法混合出来。在颜色匹配函数中而且还存在负值。对于红色,这一点很明显。所以,这些负值所代表的颜色无法被RGB空间所表示出来,这就限制定义了颜色空间的色域,即可以通过以任何比例混合基色生成的所有颜色的集合。
CIE XYZ 颜色空间
CIE RGB 如果它被用作所有颜色测量的国际标准,颜色空间在其标准化中具有一些被视为不太理想的特性,(例如上图存在负值RGB无法表示),还有光的亮度(不考虑颜色感知的亮度)混合在RGB的三个值中。一般更好的做法是用三刺激值表达,并将亮度直接用三刺激之一给出:这就是下面所讲的内容CIE XYZ颜色空间。
当将上面公式应用于r(λ) g(λ) b(λ)中时,它生成了CIE XYZ颜色空间,它们在整个可见光谱上都有正值。此外,XYZ矩阵的第二行条目Y经过精心选择,以使函数 y(λ) 尽可能接近V(λ)显示的亮度函数 ,这两个函数通常被接受为完全相等。因此,颜色的 Y 组件直接对应于其在 XYZ 颜色空间中的亮度。
X 和 Z 组件是一些抽象量,对应于色度,这是一个在不考虑亮度的情况下的感知颜色的度量。然而,随着颜色变得更亮或更暗,这些组件会随着亮度 Y 进行缩放。通过缩放颜色 (X, Y, Z) 使其位于平面 X + Y + Z = 1 上,可以计算出规范化的色度坐标 x、y 和 z,其公式为
由于这些缩放值的总和为一,因此只需要其中两个就可以完全描述颜色的色度,第三个值是冗余的。我们可以选择任意两个,但标准做法是取 x 和 y 色度坐标,并将它们与原始的 Y 亮度值结合形成所谓的 xyY 颜色空间。这是一个通用标准,通过它可以为任何可被普通人观察者感知的颜色(无论是单色还是多波长混合)提供精确的数值规格,并作为定义其他颜色空间的基础。给定某一特定颜色的 x、y 和 Y 值,我们可以使用逆公式计算 X 和 Z 值。这些值对于在由其主色的色度定义的颜色空间之间转换是有用的。
上图显示了与 xy 色度坐标相对应的颜色的二维图。这个图称为 CIE xy 色度图,尽管无法在印刷中准确再现所有颜色,但每种普通人眼可以感知的颜色都被包括在内。形成大多数外边界的曲线是光谱轨迹,由单色颜色组成。光谱轨迹上某一点的色度坐标是通过使用 x(λ)、y(λ) 和 z(λ) 颜色匹配函数来确定单一波长的 X、Y 和 Z 组件,然后应用方程 x = X / X+Y+Z, y = X / X+Y+Z 来获得 x 和 y。任何不位于光谱轨迹上的颜色都是多波长的混合。特别是,光谱轨迹上的最短和最长波长通过图的底部附近的紫色线连接。该线包含所有无法通过任何单一波长的光产生的完全饱和紫色。位于图的彩色区域之外的任何一对 xy 坐标被认为是“虚构”颜色的色度,因为它们没有实际的光源或物理对应物,仅在理论上存在。
x、y 和 z 颜色匹配函数经过归一化,以便在每个可见波长上具有相同的值。 这意味着,对于这样的理想光源,有 XYZ = 1,因此完美白光的 xy 色度坐标为 (1/3, 1/3)。当然,平坦的光谱功率分布在日常环境中并不实际发生,因此 CIE 定义了具有特定相对功率的标准光源,这些功率在可见波长的表中列出。与计算机图形学相关的标准光源称为 D65,其光谱功率分布如下图所示。标准光源 D65 的设计旨在近似不同地理位置在一年中的平均日光。数字 65 表示该光源与根据普朗克黑体辐射定律预测的6500K温度的理想光谱功率分布相关联,如图中的虚线所示。
当我们将 CIE 标准观察者的颜色匹配函数 x(λ)、y(λ) 和 z(λ) 与 D65 光源的光谱功率分布 SD65(λ)相乘并对所有波长积分时,可以得到白点的色度坐标 (xw, yw)。具体步骤如下:
首先,计算三刺激值 Xw、Yw、和 Zw:
然后,通过归一化得到色度坐标 xw 和 yw:
得出的结论:
这些坐标定义了光源的白点,这个位置在CIE xy 色度图中显示。加色空间中基色的亮度通常通过要求基色的混合成等比例产生在亮度 Yw = 1 时的白点的给定色度来定义。
sRGB 颜色空间
在1990年代末,选择了红、绿、蓝基色的色度,以与当时的显示设备紧密匹配,这定义了所谓的标准 RGB 颜色空间,或通常称为 sRGB 颜色(这与 CIE RGB 颜色空间不同,但由于 sRGB 基色是用 CIE XYZ 颜色空间中的色度定义的,因此两者之间存在具体关系)。sRGB 颜色空间是大多数计算机图形应用程序默认使用的颜色空间,当没有指定其他颜色空间时,可以认为它是正确的颜色空间。
sRGB 基色的确切色度坐标 (xr, yr)、(xg, yg) 和 (xb, yb) 被定义为:(最大亮度下的 亮度=1)
这些基色在CIE xy 色度图中作为点 R、G 和 B 显示,并且连接它们的三角形表示 sRGB 颜色空间的色域。该三角形外的颜色无法在仅使用每个基色的正值的设备上产生。图的左上部分有大量颜色被排除在色域之外,但光谱轨迹在该区域被拉伸。因此,尽管某些颜色无法在标准 sRGB 设备上完全还原,但它们仍然通过色域外的光谱轨迹被拉伸,以呈现其与其他颜色相比的可感知差异。这意味着即使超出了 sRGB 色域的颜色,它们在视觉上的表现仍然是相对良好的,这并不是一个严重的问题。
sRGB 颜色空间定义了颜色白的色度坐标,如方程xw =0.3127 yw =0.3290 所示。这使我们可以通过要求颜色 (xr, yr, Yr)、(xg, yg, Yg) 和 (xb, yb, Yb) 的总和为白点 (xw, yw, 1) 来计算红、绿和蓝基色的亮度 Yr、Yg 和 Yb。我们不能直接用其 (x, y, Y) 坐标相加颜色,但可以通过应用下列方程 来相加其 (X, Y, Z) 坐标。(绿色基色的亮度通常被设为1)
这将产生以下等式:
从中,我们可以通过反转 3x3 矩阵求解亮度:
这些值告诉我们需要混合多少基色,以产生在 sRGB 颜色空间中被认为是白光的颜色。(例如要使Yr的亮度=Yb的亮度需要 0.212639 ⋅ R = 0.072192 ⋅ B)
为了找到将任何颜色从 XYZ 空间转换到 sRGB 空间的 3x3 矩阵,我们可以强制要求每个基色的色度如色度方程 公式#0 所示,并且亮度如上述亮度方程 公式#2 所示,映射到一个颜色 (R, G, B),其中对应于基色的分量为1,其他两个分量为0。这可以写成矩阵方程:
其中,右边的单位矩阵列表示sRGB空间中最大亮度下的红、绿、蓝主函数,左边的矩阵列为XYZ空间中对应的坐标。我们通过简单地对主矩阵XYZ坐标的反转来求解转换矩阵MsRGB
其中每个条目都被四舍五入到小数点后六位。正如预期的那样,矩阵MsRGB将D65的白点映射到sRGB的颜色(1,1,1),用方程表示为:
MsRGB 的逆是方程 MsRGB(公式#4) 中显示的 XYZ 基色矩阵,它提供了从 sRGB 颜色空间回到 XYZ 颜色空间的转换。其条目为:
同样,每个条目都已四舍五入到小数点后六位。MsRGB^-1 的第二行条目是方程 公式#3 中给出的亮度 Yr、Yg 和 Yb,它们是通过这些分量计算颜色 (R, G, B) 的亮度 Y 的系数,如下所示:
这是将彩色图像转换为仅包含亮度信息的灰度图像的公式。这三个不同的系数代表了基色的相对明显亮度,基于亮度函数,并且它们的总和为1,因为全强度的 RGB 颜色 (1, 1, 1)(即白色)必须具有亮度为1。
渲染图像的着色器程序通常通过将每种颜色视为一个三维向量,其中 x、y 和 z 组件表示标准红、绿、蓝基色的强度来执行颜色相关的计算。两个颜色可以像向量一样逐分量相加,这是一种完全有效的操作,因为它符合Grassmann定律。将颜色乘以标量以增加或减少其亮度也是有效的,方式与向量相同。这两种操作可以应用于作为基向量的全强度红、绿和蓝颜色,其值分别为 (1, 0, 0)、(0, 1, 0) 和 (0, 0, 1),以生成图 RGB颜色立方体 所示的 RGB 颜色立方体。在图中,我们可以看到三个面,其中一个分量的值为1,其他两个分量的值从0变化到1。该立方体展示了红色和绿色如何组合形成黄色,绿色和蓝色组合形成青色,蓝色和红色组合形成洋红色,所有三种基色组合形成白色。在立方体的背面,每个面的颜色都有一个分量的值为0,并且它们在 (0, 0, 0) 相交形成黑色。连接 (0, 0, 0) 和 (1, 1, 1) 的对角线包含了黑色和白色之间的所有灰度级。
在许多情况下,需要将两种颜色相乘。例如,光的颜色可能表示为一种 RGB 颜色,而表面反射的颜色可能表示为另一种 RGB 颜色。为了确定达到观察者的光颜色,因此表面呈现的颜色,可以简单地将光颜色和反射颜色相乘。这种颜色之间的乘积通过逐分量乘法计算,通常会产生对物理现实的可接受近似。以这种方式乘法并不总是正确的,但在没有额外信息的情况下,我们无法做得更好。正确的方式是计算它们的光谱在足够离散波长下的乘积,以获得准确的结果。继续以反射光为例,发射光的光谱功率分布需要与表面的反射光谱相乘。然后,该乘积需要与 sRGB 颜色空间的红、绿和蓝颜色匹配曲线相乘并积分,以确定应该显示的每个分量的强度。由于计算开销和存储要求等原因,游戏引擎通常不会为生成物理正确的结果而如此复杂,而是仅在乘法上下文中使用 RGB 颜色。
用于RGB运算的代码如下
struct ColorRGBA
{float red, green, blue, alpha;ColorRGBA() = default;ColorRGBA(float r, float g, float b, float a = 1.0F){red = r; green = g; blue = b; alpha = a;}ColorRGBA& operator *=(float s){red *= s; green *= s; blue *= s; alpha *= s;return (*this);}ColorRGBA& operator /=(float s){s = 1.0F / s;red *= s; green *= s; blue *= s; alpha *= s;return (*this);}ColorRGBA& operator +=(const ColorRGBA& c){red += c.red; green += c.green; blue += c.blue; alpha += c.alpha;}ColorRGBA& operator −= (const ColorRGBA& c){red − = c.red; green −= c.green; blue − = c.blue; alpha −= c.alpha;}ColorRGBA& operator *=(const ColorRGBA& c){red *= c.red; green *= c.green; blue *= c.blue; alpha *= c.alpha;}
};inline ColorRGBA operator *(const ColorRGBA& c, float s)
{return (ColorRGBA(c.red * s, c.green * s, c.blue * s, c.alpha * s));
}inline ColorRGBA operator /(const ColorRGBA& c, float s)
{s = 1.0F / s;return (ColorRGBA(c.red * s, c.green * s, c.blue * s, c.alpha * s));
}
inline ColorRGBA operator +(const ColorRGBA & a, const ColorRGBA & b)
{return (ColorRGBA(a.red + b.red, a.green + b.green,a.blue + b.blue, a.alpha + b.alpha));
}
inline ColorRGBA operator −(const ColorRGBA & a, const ColorRGBA & b)
{return (ColorRGBA(a.red − b.red, a.green − b.green,a.blue − b.blue, a.alpha − b.alpha));
}
inline ColorRGBA operator *(const ColorRGBA & a, const ColorRGBA & b)
{return (ColorRGBA(a.red * b.red, a.green * b.green,a.blue * b.blue, a.alpha * b.alpha));
}
Gamma Correction 伽马矫正
在阴极射线管(CRT)显示器的时代,每个像素的每个颜色通道的亮度是由输入信号的非线性函数决定的,这与电路的工作方式有关。某一输入值 Vsignal 的显示亮度 Vdisplay 由以下关系给出:
其中,指数 γ 通常在 2.0 到 2.5 的范围内。这个函数被称为伽马曲线(gamma curve),得名于用于指数的希腊字母。虽然从工程角度来看并不必要,但新型显示技术故意保持这一关系以确保一致性。
伽马曲线的存在意味着在渲染世界时计算出的最终颜色不应直接显示,因为大多数强度将显得过暗。图 gamma 展示了线性灰度强度梯度在 gamma 值约为 2.2 的普通显示器上的显示效果。根据公式#6 ,强度值 0.5 的实际亮度在显示时约为 22%,远低于预期的 50% 亮度。为了补偿这一效果,必须在将颜色值存储到帧缓冲区之前进行伽马校正。强度值通过将其提升到 $1/\gamma$ 次方来进行伽马校正,从而执行 公式#6 的逆操作。图 gamma 中的第二个灰度梯度展示了在所有强度值下显示的正确亮度。
当描述颜色为编码为 sRGB 时,通常意味着每个分量的强度已进行伽马校正并以非线性值存储,我们将使用 sRGB 术语表示这一确切含义。未进行伽马校正的颜色被简单地称为线性颜色。(这两种颜色都使用相同的 sRGB 原色集。)sRGB 标准定义了线性分量与 sRGB 分量之间的转换函数,这些函数与 公式#6 中的关系稍有不同,以避免在零附近出现无限导数。线性颜色的每个分量 c 通过函数 fsRGB 进行伽马校正以转换为 sRGB 颜色的分量。被定义为
在相反方向上,sRGB 颜色的每个分量 c 通过函数 flinear 转换为线性颜色的分量。
这两个函数在图 gamma转换函数 中绘出,且它们是精确的逆函数。尽管这些转换函数使用 2.4 的指数,但 flinear 的整体形状与大多数显示器使用的 2.2 伽马值的 公式#6 非常接近,曲线 f(c) = c^{2.2} 也在图中用于对比。这意味着 fsRGB 函数非常接近显示器应用的伽马曲线的逆函数,因此 fsRGB 应用的伽马校正使得显示的强度看起来是线性的。
当图像存储在低精度格式(例如每通道 8 位)时,伽马校正还有额外的好处。人眼在低强度下能够检测到较小的亮度差异,而在高强度下则难以区分。因此,如果以线性方式存储强度,则过多的离散值会分配给相互无法区分的高强度,而较少的离散值则分配给低强度,而这些低强度的相邻值相对容易区分。应用伽马校正可以重新分配 256 个可用值,使得更少的值对应于高强度,而更多的值对应于低强度。这在眼睛更加敏感的强度水平上提供了更高的精度。此方法适用于直接在伽马校正空间中创作的图像,或在更高位深度中存储的图像,这些图像在转换到每通道 8 位的低精度之前进行了伽马校正。因此,伽马校正有时被称为伽马压缩,因为通过重新分配强度值保留了部分高精度。
大多数数字图像格式(如 JPEG 和 PNG)默认将颜色以伽马校正的强度存储在 sRGB 色彩空间中。然而,涉及光照、反射和大气效应的渲染计算都需要在一个线性空间中进行。最终的颜色在显示前需要进行伽马校正。为了简化这一过程,图形硬件能够在适当的时刻自动执行 公式#7 和 公式#8 所定义的转换。