openglshader实现虚拟场景_OpenGL超级宝典笔记——片段着色器(二)

图像处理

图像处理是一种独立于顶点着色器的特殊处理程序。在不使用片段着色器的情况下绘制场景之后,可以按照各种方式应用卷积核。

为了保持着色器的简洁,使用硬件加速,我们限制总卷积的大小为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://my.lmcjl.com/post/16631.html

展开阅读全文

4 评论

留下您的评论.