图像处理是一种独立于顶点着色器的特殊处理程序。在不使用片段着色器的情况下绘制场景之后,可以按照各种方式应用卷积核。
为了保持着色器的简洁,使用硬件加速,我们限制总卷积的大小为3X3.
在示例程序中,调用glCopyTexIamge2D把帧缓冲区拷贝到纹理中。纹理的大下为小于窗口的2的最大N次方值(在2.0中则没有这个限制)。然后在窗口的中间绘制一个片段着色的四边形,大小与这个纹理相同,其纹理坐标从左下角(0,0)到右上角(1,1)。
片段着色器基于纹理坐标,在以其为核心的相邻的3X3纹理中进行采样,然后进行过滤,得到这个中心点的颜色。
模糊可能是最常见的过滤器。它能够平滑一些高频率的特性,例如物体边缘的锯齿。它也叫做低通滤波器。它允许低频率的特性通过,而截留高频率的特性。
如果我们只用3X3的卷积核,那么在单次采样时不会有太明显的变化。我们可以进行多次采样。
下面是着色器代码:
//blur.fs #version 120 //采样的纹理 uniform sampler2D sampler0; //采样的偏移 uniform vec2 tc_offset[9]; void main(void) { vec4 sampler[9]; for (int i = 0; i < 9; ++i) { //获得采样数据 sampler[i] = texture2D(sampler0, gl_TexCoord[0].st + tc_offset[i]); } //1 2 1 //2 1 2 /13 //1 2 1 //计算结果 gl_FragColor = (sampler[0] + sampler[1] * 2.0 + sampler[2] + sampler[3] * 2.0 + sampler[4] + sampler[5] * 2.0 + sampler[6] + sampler[7] * 2.0 + sampler[8])/ 13.0; }
在这个过程中,首先我们先不使用着色器绘制好图形,然后启用着色器程序,设置好sampler0和tc_offse,把帧缓冲拷贝到纹理中。再设置好纹理坐标,绘制一个正方形,使用着色器处理纹理。下面是部分关建代码:
void ChangeSize() { ... windowWidth = textureWidth = w; windowHeight = textureHeight = h; //不支持非2的n次纹理 if (!GLEE_ARB_texture_non_power_of_two) { int n = 1; while ((1 << n) < windowWidth) { n++; } textureWidth = (1 << (n-1)); n = 1; while ((1 << n) < windowHeight) { n++; } textureHeight = (1 << (n-1)); } glViewport(0 ,0, w, h); TwWindowSize(w, h); GLfloat xIn = 1.0f/textureWidth; GLfloat yIn = 1.0f/textureHeight; //构造偏移数组 for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { tc_offset[3 * i + j][0] = (i - 1.0f) * xIn; tc_offset[3 * i + j][1] = (j - 1.0f) * yIn; } } }
void RenderScene() { .... //不使用着色器,绘制图形到帧缓冲区 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(0); glLightfv(GL_LIGHT0, GL_POSITION, g_lightPos); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(35.0, (GLfloat)windowWidth/(GLfloat)windowHeight, 1.0, 100.0); glMatrixMode(GL_MODELVIEW); glPushMatrix(); gluLookAt(cameraPos[0], cameraPos[1], cameraPos[2], 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); glTranslatef(xTrans, yTrans, zTrans); float mat[4*4]; ConvertQuaternionToMatrix(g_Rotate, mat); glMultMatrixf(mat); DrawGround(); DrawObjects(); glPopMatrix(); //开启片段着色器,进行模糊处理 glDisable(GL_DEPTH_TEST); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glUseProgram(program[whichShader]); uniformLoc = glGetUniformLocation(program[whichShader], "sampler0"); if (uniformLoc != -1) { glUniform1i(uniformLoc, sampler); } uniformLoc = glGetUniformLocation(program[whichShader], "tc_offset"); if (uniformLoc != -1) { glUniform2fv(uniformLoc, 9, &tc_offset[0][0]); } //通过着色器的次数 for (int i = 0; i < numPass; ++i) { //从帧缓冲区中读取数据到纹理中 glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, windowWidth-textureWidth, windowHeight-textureHeight, textureWidth, textureHeight, 0); //清空帧缓冲区 glClear(GL_COLOR_BUFFER_BIT); glBegin(GL_QUADS); glTexCoord2f(0.0f, 0.0f); glVertex2f((-(GLfloat)textureWidth/(GLfloat)windowWidth), -((GLfloat)textureHeight/(GLfloat)windowHeight)); glTexCoord2f(1.0f, 0.0f); glVertex2f((GLfloat)textureWidth/(GLfloat)windowWidth, -((GLfloat)textureHeight/(GLfloat)windowHeight)); glTexCoord2f(1.0f, 1.0f); glVertex2f((GLfloat)textureWidth/(GLfloat)windowWidth, (GLfloat)textureHeight/(GLfloat)windowHeight); glTexCoord2f(0.0f, 1.0f); glVertex2f(-(GLfloat)textureWidth/(GLfloat)windowWidth, (GLfloat)textureHeight/(GLfloat)windowHeight); glEnd(); } glEnable(GL_DEPTH_TEST); glutSwapBuffers(); }
只进行一次采样:
5次采样:
锐化与模糊相反,它是使得物体的边缘更加明显和文字容易阅读。
锐化的着色器代码:
//sharpen.fs/ #version 120 uniform sampler2D sampler0; uniform vec2 tc_offset[9]; void main(void) { vec4 sampler[9]; for (int i = 0; i < 9; ++i) { sampler[i] = texture2D(sampler0, gl_TexCoord[0].st + tc_offset[i]); } //-1 -1 -1 //-1 9 -1 //锐化的卷积 和为1 //-1 -1 -1 gl_FragColor = (-sampler[0] - sampler[1] - sampler[2] - sampler[3] + 9 * sampler[4] -sampler[5] - sampler[6] - sampler[7] - sampler[8]); }
注意这个卷积核相加的结果为1,这和模糊过滤器相同。这个操作保证了这种过滤器不会增强或减弱亮度。
锐化效果图
膨胀只是简单的找到相邻的最大值。
//dilation.fs #version 120 uniform sampler2D sampler0; uniform vec2 tc_offset[9]; void main(void) { vec4 sampler[9]; //find the max value vec4 maxValue = vec4(0.0); for (int i = 0; i < 9; ++i) { sampler[i] = texture2D(sampler0, gl_TexCoord[0].st + tc_offset[i]); maxValue = max(sampler[i], maxValue); } gl_FragColor = maxValue; }
腐蚀取周围相邻的最小值。
//erosion.fs #version 120 uniform sampler2D sampler0; uniform vec2 tc_offset[9]; void main(void) { vec4 sampler[9]; vec4 minValue = vec4(1.0); for (int i = 0; i < 9; ++i) { sampler[i] = texture2D(sampler0, gl_TexCoord[0].st + tc_offset[i]); minValue = min(minValue, sampler[i]); } gl_FragColor = minValue; }
比较有价值的过滤器是边缘检测。图像的边缘是颜色变化快的地方,而边缘检测则是选取这部分颜色急剧变化的地方并高亮它们。
有三种边缘检测器Laplacian,Sobel和Prewitt. Sobel和Prewitt梯度过滤器,它们检测每个通道强度的一阶导数的变化,只是在单个方向上进行。Laplacian则检测二阶导数的零值,也就是颜色的强度梯度从暗变亮的地方(或相反)。它可以用于所有的边缘。
下面的代码使用Laplacian过滤器。
//edgedetetion.fs #version 120 uniform sampler2D sampler0; uniform vec2 tc_offset[9]; void main(void) { vec4 sampler[9]; for (int i = 0; i < 9; ++i) { sampler[i] = texture2D(sampler0, gl_TexCoord[0].st + tc_offset[i]); } //-1 -1 -1 //-1 8 -1 //-1 -1 -1 gl_FragColor = (8.0 * sampler[4]) - (sampler[0] + sampler[1] + sampler[2] + sampler[3] + sampler[5] + sampler[6] + sampler[7] + sampler[8]); }
它和锐化过滤器的区别就是中间的那个值是8不是9,这样系数之和就是0。这也说明了为何图形中间是黑的。因为图元中间的颜色相近,通过卷积核过滤之后就接近于0了。只有在图元边缘颜色变化剧烈的地方,才有较大的颜色值。
在此之前,我们讨论过逐顶点的光照。还讨论了通过分离镜面光和使用纹理查找的方式来提升光照效果。在这里我们使用片段着色器的方式来处理光照。算法是一样的。
在这里我们结合顶点着色器和片段着色器来实现。顶点着色器对法线、光照向量沿着线和三角形进行插值。然后,片段着色器处理顶点着色器产生的值得到最终的结果。
公式:
Cdiff = max{N • L, 0} * Cmat * Cli
// diffuse.vs // uniform vec3 lightPos[1]; varying vec3 N, L; void main(void) { // vertex MVP transform gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; // eye-space normal N = gl_NormalMatrix * gl_Normal; // eye-space light vector vec4 V = gl_ModelViewMatrix * gl_Vertex; L = lightPos[0] - V.xyz; // Copy the primary color gl_FrontColor = gl_Color; }
这与之前只用顶点着色器不同的是,这里用varyings修饰的标识符N和L作为输出,在片段着色器中用一样的名称就可以访问到N,L。这种方式比之前使用纹理坐标作为输出的方式更容易理解,也不容易出错(试想不小心把L输出到textureCoord[1]中,但实际使用的是textureCoord[0], 不会产生编译错误,但得不到想要的结果)。
下面是片段着色器代码:
//diffuse.fs #version 120 varying vec3 N, L; void main(void) { float intensity = max(0.0, dot(normalize(N), normalize(L))); gl_FragColor = gl_Color; gl_FragColor.rgb *= intensity; }
镜面光公式:
Cspec = max{N • H, 0}Sexp * Cmat * Cli
VS有多个L的输出
//3light.vs #version 120 uniform vec3 lightPos[3]; varying vec3 N, L[3]; void main(void) { //MVP transform gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; //eye-space vec4 V = gl_ModelViewMatrix * gl_Vertex; //eye-space noraml vector N = gl_NormalMatrix * gl_Normal; for (int i = 0; i < 3; ++i) { L[i] = lightPos[i] - V.xyz; } //primary vector gl_FrontColor = gl_Color; }
//3light.fs #version 120 varying vec3 N, L1[3]; void main(void) { vec3 NN = normalize(N); gl_FragColor = vec4(0.0); //3个光的颜色 vec3 lightCol[3]; lightCol[0] = vec3(0.5, 0.5, 1.0); lightCol[1] = vec3(0.2, 0.3, 0.5); lightCol[2] = vec3(0.8, 0.4, 0.8); const float expose = 128.0f; for (int i = 0; i < 3; ++i) { vec3 NL = normalize(L1[i]); vec3 H = normalize(NL + vec3(0.0, 0.0, 1.0)); float NdotL = max(0.0, dot(NN, NL)); //diffuse gl_FragColor.rgb += gl_Color.rgb * lightCol[i] * NdotL; //specular if (NdotL > 0.0) { gl_FragColor.rgb += lightCol[i] * pow(max(0.0, dot(NN, H)), expose); } } gl_FragColor.a = gl_Color.a; }
源码:https://github.com/sweetdark/openglex