本书是图形学编程的不错读物,通过浅显易懂,理论结合实践的方式介绍着色器的使用,我在翻译的过程中尽量保持原文的段落和含义,会删除比较无关的内容。
- 专有词汇会附加英文原词,用标签的形式表示,如 着色器(
shaders
)- 原文附录引用编号会放上引用书的Google搜索结果链接
- 如有异议或对书中技术感兴趣的欢迎留言讨论
有些图像处理操作的过程,是将一张图像中的像素和常量图或另一张图像中的像素进行线性混合。图11.19 展示了这种混合。在接下来的几个小节中,我们会以一个固定色值,或者直接用图像中获取的色值,作为图中的基础值(base value),只对需要被处理的图像进行使用。本章后面的几个小节里,我们会同时用上基础图和处理图。线性混合通常的形式是:
我们通过把参数 T 限制在范围 [0., 1.] 内来限制这个公式的输出。尽管如此,对于有些应用来说,并不需要做这种限制,因为对于一些效果,知道参数不是什么值比知道参数应该取什么值要更容易,放开 [0., 1.] 的范围会让我们更好地推演出想要得到的处理效果。
混合过程涉及的参数动态范围都很大,会有很不一样的结果,glman 所具有的把一个一致变量绑定到滑条控件上的能力,在推演此类效果的参数值时很有帮助,其内建的 GLSL mix() 方法能实现具体的混合计算。
下面是几个与单一常量数值进行像素混合的图像处理过程。这些操作和产生的结果都很常见并十分有用。其中的很多例子已经被设置到了 glman 的自带环境中,并可以通过一致滑条变量 T 来完成图11.19 的混合运算。
反色模拟的是照相底片的工作方式。底片(译注:这里特指负片)的作用是拦截呈现到相纸上的像素的色值,所以一张图像的反色是通过把每个像素的色值被白色减去算出来的:
vec3(1.0, 1.0, 1.0) - color.rgb
如果你把这个反色结果作为输出值,可以得到如11.20 中展示的照相底片效果。
下面的负片片元着色器代码允许你用变量 uT 来控制色值和其反色的混合效果。当 uT = 0 时,输出是原图,当 uT = 1 时,可以得到完全的反色图。
uniform sampler2D uImageUnit;
uniform float uT;
in vec2 vST;
out vec4 fFragColor;
void main( ) {
vec3 irgb = texture( uImageUnit, vST ).rgb;
vec3 neg = vec3(1.,1.,1.) - irgb;
fFragColor = vec4( mix( irgb, neg, uT ), 1. );
}
非正式地说,亮度可以被认为是色值中非黑色部分的量有多少。对于 RGB 色值,“更少”的黑色意味着色值更接近 1.0,而“更多”黑色意味着色值更接近 0.0。可以用纯黑色图作为基础图。
target = vec3(0.0, 0.0, 0.0)
小于 1.0 的 uT 值会加深色值,大于 1.0 的 uT 值使色值变亮,直到看不见颜色。可想而知,过大的 uT 值会导致那些已经很亮的色值过曝(译注:原文 wash out,直译颜色被洗去致褪色),如图11.21 所示。
下面是一段调节亮度的片元着色器代码,非常简单。从效果上来说,让图像变亮就是减去色值中黑色的部分。
uniform sampler2D uImageUnit;
uniform float uT;
in vec2 vST;
out vec4 fFragColor;
void main( ) {
vec3 irgb = texture( uImageUnit, vST ).rgb;
vec3 black = vec3( 0., 0., 0. );
fFragColor = vec4( mix( black, irgb, uT ), 1. );
}
对比度 形容的是一张图的色值和中灰的差异有多显著(译注:原文 how much the colors stand out from gray,区别于之后形容饱和度的 how far,突出的是在外部调节的作用下,差异会变得更明显的感觉,而不是简单的相对距离),可以用 50%灰的色值图作为基础图来控制对比度
target = vec3(0.5,0.5,0.5);
小于 1 的参数 T 会使色值靠近 0.5,降低对比度,大于 1 的 T 会使色值远离 0.5,增加对比度,如图11.22 所示。
下面是一段简单的片元着色器代码,既可以调节亮度,也能调节对比度。总的来说,让图像变亮就是减去黑色的量,让图像对比度提高就是减去中灰的量
#define BRIGHTNESS
#undef CONTRAST
uniform sampler2D uImageUnit;
uniform float uT;
in vec2 vST;
our vec4 fFragColor;
void main( ) {
vec3 irgb = texture( uImageUnit, vST ).rgb;
#ifdef BRIGHTNESS
vec3 target = vec3( 0., 0., 0. );
#else
vec3 target = vec3( 0.5,0.5,0.5 );
#endif
fFragColor = vec4( mix( target, irgb, uT ), 1. );
}
译注:从混合公式可以看出,T 为 1时输出结果是原图,所以对亮度和对比度,原图默认值就是 1, 完全的黑和中灰就是0,越往高调节,效果就越明显,同样默认值和效果的还有下面的饱和度。
另一类混合图像处理,基础色值是由图像自身的像素色值计算而来的,这可能是灰度图,或是一张模糊过的图。这同样是很常见和非常有用的处理手段。并且这些处理效果同样被内置到了 glman 中。
我们把色值的“纯度”形容为 饱和度,或者离灰色有多远。这和 HLS 色彩系统中,饱和度是与位于双圆锥体系统中心的纯灰色的距离,的概念是一致的。如果降低饱和度,色值就更灰,增加饱和度;颜色就更纯、更鲜艳。
可以用以明度值计算得出的灰度图作为计算饱和度的基础图(本章之前的内容提到了它的计算方法),
target = vec3( luminance, luminance, luminance);
并与原图色值进行混合,小于 1 的 uT 值会使色值靠近其明度,降低饱和度;大于 1 的 uT 值会使色值远离其明度,提高饱和度,如图11.23 所示。
const vec3 W = vec3( 0.2125, 0.7154, 0.0721 );
uniform sampler2D uImageUnit;
uniform float uT;
in vec2 vST;
out vec4 fFragColor;
void main( ) {
vec3 irgb = texture( uImageUnit, vST ).rgb;
float luminance = dot( irgb, W );
vec3 target = vec3( luminance, luminance, luminance );
fFragColor = vec4( mix( target, irgb, uT ), 1. );
}
我们把一张图中粗糙和精细部分的清晰程度叫做 锐度。你也可以把锐度理解成模糊的反义词。利用混合原理的优势,可以通过把图像自身的某个模糊版本作为基础图,来控制原图的锐度。本章之前的内容已经讨论过模糊图的生成过程。图11.24 展示了锐化一张图的例子;和图11.5 中的例子是同一原图。
实现上图锐化处理的片元着色器代码,和本章图像模糊的例子用的是同样的过滤算子,唯一不同的是最后会将模糊色值(“target”)和原图色值(“irgb”)进行混合。本书附带资料中有完整的着色器代码。
...
fFragColor = vec4( mix( target, irgb, uT ), 1. );
上面的小节讲的图像混合实际上都是对单张图像进行的处理,另一张参考图只是处理工具。如此同时,在有些场景下需要把两张内容图片混合在一起。有许多不同种类的混合模式,在下面几节里,我们会挑几个作为示例进行展示,代码可能给出得并不完全,但对你来说应该很容易实现其剩余比分。除此之外,我们还把一些其他的混合模式作为本章的练习。图11.25 就是我们将要用来讨论这许多混合模式的两张示例图。
我们可以用任何函数作用在两个 RGB 色值上,来得到一个 RGB 输出色值,这使得两张图像的插值混合复杂而有趣。这些函数即可以对作用于整个 RGB 向量,也可以只对某几个独立的颜色分量进行计算。下面我们展示了一部分函数,并把更多的函数放在了练习中。
举例来说,[20] 这个网站里的基于余弦函数的插值算法看上去就很有趣,如图11.26 所示。其过程是先从两张图中读出像素色值,再用余弦乘数将其组合起来。余弦函数被应用到每一个颜色分量上,所以越靠近 1 的分量值越会被增强(译注:余弦函数在输入为 [0, 1] 的范围内具有单调性,其结果范围是 [1, -1],所以输入越靠近1,结果越靠近 -1,对于公式的整体色值输出,会越大)。
color = ρ − α ∗ cos (π * Argb ) − β ∗ cos (π ∗ Brgb )
在上面的混合公式中, Argb 是“后”图的色值,Brgb 是“前”图的色值。ρ 是一个基础色值,一般是全局明度值,α 和 β 是衡量两张图的权重值(|ρ| + |α| + |β| 的和不能超过1,否则结果色值会溢出)。
下面是示例中的片元着色器代码。我们用 0.5 和 -0.25 作为基础色值和各自图像的余弦乘数,我们设置了一道练习题来鼓励你自己实验不同的参数(并且建议你用 glman 的一致滑条变量来做)。
const float PI = 3.14159265;
uniform sampler2D uBeforeUnit, uAfterUnit;
in vec2 vST;
out vec4 fFragColor;
void main( ) {
vec3 brgb = texture( uBeforeUnit, vST ).rgb;
vec3 argb = texture( uAfterUnit,vST).rgb;
vec3 target = 0.5 - 0.25*cos(PI*brgb) - 0.25*cos(PI*argb);
fFragColor = vec4( target, 1. );
}
乘法(正片叠底) 操作完全是按照其名称的意思来运算的,使两张图像像素的颜色分量依次相乘,就能得到最终的输出色值。通过这种方式,一张图可以作为另一张图的减除过滤器。
因为所有的颜色分量都小于等于1,所以输出图像会比两张原图看起来都要更暗。考虑到这个因素,可以计算 argb,brgb 和 target(乘法结果) 的明度值,使最终输出的色值明度是两张输原图的平均值,以此来平衡输出颜色。下面是其片元着色器代码,图11.27 中分别展示了没有颜色均衡和有颜色均衡处理的结果。
const vec3 W = vec3(0.2125, 0.7154, 0.0721)
uniform sampler2D uBeforeUnit, uAfterUnit;
in vec2 vST;
out vec4 fFragColor;
void main( ) {
vec3 brgb = texture( uBeforeUnit, vST ).rgb;
vec3 argb = texture( uAfterUnit, vST ).rgb;
vec3 target = argb * brgb;
float alum = dot( argb, W );
float blum = dot( brgb, W );
float tlum = dot( target, W );
target = (alum + blum)/(2.*tlum);
fFragColor = vec4( target, 1.);
}
变暗和变亮的操作很相似,所以我们把它们俩放一起来说。变暗模式取两个原图色值中较小的一个作为输出。相反,变亮模式是取两个原图色值中较大的一个。下面是变暗的片元着色器代码,我们把变亮作为练习留给你。图11.28 展示了两种模式的结果。
uniform sampler2D uBeforeUnit, uAfterUnit;
in vec2 vST;
out vec4 fFragColor;
void main( ) {
vec3 brgb = texture( uBeforeUnit, vST ).rgb;
vec3 argb = texture( uAfterUnit, vST ).rgb;
vec3 target = min( argb, brgb ); // alternately max(...)
fFragColor = vec4( target, 1.);
}
除了把两张图组合成一张之外,我们还可以考虑从一张图过渡到另一张有哪些方式,比如 PPT 里就有一系列滑动转场效果,但我们通过片元着色器所能控制的选项,远远超出了 PPT 中的效果。
过渡效果的基础原理是:从一张我们叫做“前”图(Before image)的原图中读出像素色值,对每个像素值应用一种算法,使其最终值是第二张原图中对应像素的色值,我们把它叫做“后”图(After image)。在做法上,无论用什么方法把前图替换成后图都可以,我们将用这种方式尝试创造一些有趣的效果。这一节中的所有例子,都用的是图11.25 中的两张图,华盛顿的樱花 和 宏村。
在我们将要介绍的第一种过渡方式中,前图向右移动离开画面,同时后图从画面左侧移入。因为我们现在讨论的是过渡效果,所以在整个过程中,两张图都会被完整显示出来(译注:这里不太理解,就算是简单的slide效果,也是从一张图到另一张的完整过渡),并分别被压缩在了允许其显示的范围之内。图11.29 是这个过程部分完成的一个例子。
所需的 .glib 文件和顶点着色器代码都和之前的图像混合示例完全一样,这里我们只给出片元着色器代码:
uniform float uT; //0. <= uT <= 1.
uniform sampler2D uBeforeUnit, uAfterUnit;
in vec2 vST;
out vec4 fFragColor;
void main( ) {
vec2 st = vST;
vec3 brgb = texture( uBeforeUnit, st ).rgb;
vec3 argb = texture( uAfterUnit, st ).rgb;
vec3 color;
if ( st.x < uT )
{
st = vec2( st.x/uT, st.y );
vec3 thisrgb = texture( AfterUnit, st ).rgb;
color = thisrgb;
} else {
st = vec2( (st.x-uT)/(1.-uT), st.y );
vec3 thatrgb = texture( BeforeUnit, st ).rgb;
color = thatrgb;
}
fFragColor = vec4( color, 1.);
}
这段代码中,if 语句的两个部分分别表示了显示画面的左右两部分:一边是纹理坐标系中 s 分量的值小于参数 uT,一边是大于 uT。对每个像素坐标,s 分量被按画面比例系数进行计算,得出用于获取显示纹素的纹理坐标。
因为 uT 的范围是 [0., 1.],这个示例的效果就是在这个范围内从前图过渡到后图。我们没办法给出演示整个过程的某张静态图,所以请你在练习中自己实现这个效果并观察是怎样的过程。
图像溶解模式在前图和后图之间算出一个加权平均值,以此来决定两个色值分别有多少会用在输出色值中。这个权重值也可以通过一个随时变化的外部参数给出,从而得到从一张图渐变过渡到另一张的效果,就像幻灯片里见过的那样,如图11.30 和下面的片元着色器代码所示。随着 uT 的值从 0. 变化到 1.,前图逐渐被溶解在后图中。
uniform sampler2D uBeforeUnit, uAfterUnit;
uniform float uT;
in vec2 vST;
out vec4 fFragColor;
void main( ) {
vec3 brgb = texture( uBeforeUnit, vST ).rgb;
vec3 argb = texture( uAfterUnit, vST ).rgb;
fFragColor = vec4( mix( argb, brgb, uT ), 1. );
}
译注:在 PS 中 Burn 的翻译是加深,这里采用文中的取名目的,按“烧”的效果来翻译
另一种可以模拟出的过渡效果是后图逐渐“烧穿”前图,在这个过程中,后图中明度最高的部分会首先替换掉前图。我们把这个过程留作练习给,这里的示例代码中,对于后图我们用 R、G、B 的平均值来近似计算明度(译注:sRGB的标准权重可以参照本章明度的小节)。之所以我们给这个效果取名烧穿,是因为看起来确实就像是前图慢慢燃烧,后图逐渐显示的过程。在图11.31 的过渡过程中,不难发现一些村庄场景中的暗色建筑特征正从樱花图中显露出来。
所需的 .glib 文件和顶点着色器同样和前面的那些完全一样,下面是片元着色器代码。
uniform float uT;
uniform sampler2D uBeforeUnit, uAfterUnit;
in vec2 vST;
out vec4 fFragColor;
void main( ) {
vec3 brgb = texture( uBeforeUnit, vST ).rgb;
vec3 argb = texture( uAfterUnit, vST ).rgb;
vec3 color;
if ( (argb.r + argb.g + argb.b)/3. < uT )
color = argb;
else
color = brgb;
fFragColor = vec4( color, 1.);
}
这其中只有很少的计算代码,首先算出后图颜色分量的平均值,和参数 uT 对比后,如果平均色值更低(即颜色更暗)则替换掉前图的颜色。随着参数 uT 的值从 0. 变化到 1.,越来越多的后图纹素会满足条件并成为最终图像的一部分。
如果我们用其他的方法来把前图逐渐替换成后图呢?如果,比如说,我们用 Noise() 方法生成一个随机噪声纹理,并用这个随机纹理中的像素值来决定显示前图还是后图像素呢?图11.32 展示了这种过渡效果。这个效果和烧穿过渡有点像,但用于控制像素选择的图像是隐藏的,且和两张原图没有过明显的关系。
因为这个过程用到了噪声处理,所以 .glib 文件和顶点着色器代码会和本章前面的那些例子有些许不同。这个 .glib 文件简单地选了一个 3D噪声纹理,并和之前的例子一样来使用它。
##OpenGL GLIB
Noise3D 128
Ortho -1. 1. -1. 1.
Texture 6 cherries.bmp
Texture 7 Hong.village.bmp
Vertex transition.vert
Fragment transition.frag
Program Transition uBeforeUnit 6 uAfterUnit 7
QuadXY .2 5.
顶点着色器代码中加入了我们熟悉的变量 MCposition,用来装载初始四边形中各顶点的模型坐标,当在四边形中进行插值计算时,这个变量将装载显示画面(译注:噪声纹理图)内各像素的模型坐标。
out vec3 vMCposition;
out vec2 vST;
void main( ) {
vMCposition = vec3(aVertex);
vST = aTexCoord0.st;
gl_Position = uModelViewProjectionMatrix * aVertex;
}
最后,片元着色器和其他例子一样获取到两张原图的像素色值,随即用由像素模型坐标算出的点来从3D采样器函数 Noise3 中得出噪声变量 nv。因为初始图像四边形是10个单位大小,所以把模型坐标除以10,来得到取得噪声变量的真实纹理坐标(译注1)。接着用取得的噪声分量值(译注2)得出一个具体数值,并用其小数部分作为选取输出图色值的选择条件。
译注1:这里的10个单位大小不是很明白,原文是 10 units across,意思应该是原图坐标是噪声纹理坐标的10倍
译注2:原文是倍频程,按下面着色器代码来看实际是色值各分量之和的归一化转换结果,这一段写得并不直白,可以直接看代码,目的就是将噪声函数的结果转化为归一化的判断条件
uniform float uT;
uniform sampler3D Noise3;
uniform sampler2D uBeforeUnit, uAfterUnit;
in vec3 vMCposition;
in vec2 vST;
out vec4 fFragColor;
void main( ) {
vec3 brgb = texture( uBeforeUnit, vST ).rgb;
vec3 argb = texture( uAfterUnit, vST ).rgb;
vec3 color;
vec4 nv = texture(Noise3, vMCposition/10.);
float sum = nv.r + nv.g + nv.b + nv.a;
sum = ( sum - 1. ) / 2.; // 0. to 1.
sum = fract( sum );
if ( sum < uT )
color = argb;
else
color = brgb;
fFragColor = vec4( color, 1.);
}
尽管我们没有将其存作他用,但这个 sum 值实际上起到了过渡效果控制器的作用,如果我们把输出色值设置成:
color = vec3( sum, sum, sum );
显然还有很多其他方式来控制原图到最终图像素之间的输出过程,几乎所有从一张图和另一张图中分别取一部分作为输出的图像混合处理,都可以通过改变一个参数值来产生过渡效果。对此更深的研究留给感兴趣的读者去探索。
本章讨论了一些很相似的处理技术,但在图像本身的处理、与参考图像的处理、图像间的组合处理上,又各有不同。这些技术的实现简单直观,但想要擅用这些技术来创造独特的图像效果,还需要多花些时间来积累经验。