OpenGL超级宝典学习笔记——顶点着色器示例
以下内容只针对GLSL1.20的版本进行说明的,有些内置的变量在1.20之后,已经被废弃了。
初次实验
每个顶点着色器都至少输出一个裁剪空间的位置坐标。光照、纹理坐标的生成和其他的一些操作是可选的。例如,你要创建了深度纹理,那你只需要最终的深度值,你就没必要在着色器中处理颜色和纹理坐标,也不需要输出它们。但至少需要输出裁剪空间的坐标给后面的图元组装和光栅化。如果不输出任何东西,行为将是未定义的。如果要让颜色在后面的管道中可见,则至少要把输入的颜色拷贝到输出颜色,虽然着色器不对其进行任何处理。
举个简单的例子来模仿固定管线的方式。在固定管线中,会对顶点进行模型视图变换和投影变换变为裁剪空间的位置坐标。在GLSL中,提供了gl_ModelViewProjectionMatrix,这个矩阵包括模型视图变换和投影变换。所以我们只要把顶点左乘以这个矩阵就能够得到裁剪空间的位置坐标。
//simple.vs //执行顶点变换 //拷贝主颜色 #version 120 void main(void) { //顶点变换到裁剪空间位置,作为输出 gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; //把主颜色拷贝的正面颜色 gl_FrontColor = gl_Color; }
上面的gl_ModelViewProjectionMatrx也可以分成两个来写,写成gl_ProjectionMatrix * gl_ModelViewMatrix;
自己执行变换的另一种方式是使用内置函数ftransform,它对需要处理的顶点模拟了固定功能管线的顶点变换。这在混合固定功能和顶点着色器绘制同一个几何图形时很有用,可以防止Z值的细微差异导致的Z-fighting。
简单的写 就是 gl_Position = ftransform();
效果如下
散射光照
之前介绍过散射的光照,散射的光照要考虑到物体的面与输入光源的角度。其公式如下:
Cdiff = max{N • L, 0} * Cmat * Cli
其中N代表顶点的单位法线, L代表从顶点指向光源的单位向量。Cmat 是表面的材料颜色, Cli是光源的颜色。Cdiff则计算出来的结果。在例子中我们使用的是白光,所以我们可以直接忽略掉Cli 因为乘以{1, 1, 1, 1}结果不变。下面简单实现散射光照方程。
//基于白色光的散射光照 uniform vec3 lightPos; void main(void) { gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; //获得顶点的法线 vec3 N = normalize(gl_NormalMatrix * gl_Normal); //获得经过视图模型变换后的顶点位置 vec4 V = gl_ModelViewMatrix * gl_Vertex; //计算得到从顶点指向光源的单位向量 vec3 L = normalize(lightPos - V.xyz); //计算散射颜色 float NdotL = dot(N, L); gl_FrontColor = gl_Color * vec4(max(0.0, NdotL)); }
其中gl_NormalMatrix是GLSL内置的变量为法线变换矩阵,gl_Normal代表顶点的法线。dot也是GLSL内置的函数提供向量的点乘, normalize也是内置函数。其余的已经在前面介绍过了。,那么我们该如何设置这个lightPos向量呢。GLSL提供了一系列设置uniform变量的方法。这里用到其中一个。
void <strong>glUniform3fv</strong>(
GLintlocation, GLsizeicount, const GLfloat *value)
;
你可以在渲染函数中, 随意设置这个参数值,来改变光源的位置。整个编译和链接shader并设置变量的函数如下:
float g_lightPos[3] = {20.0f, 10.0f, 20.0f, 1.0f}; void SetupRC() { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glEnable(GL_DEPTH_TEST); glCullFace(GL_BACK); glFrontFace(GL_CCW); glEnable(GL_CULL_FACE); glEnable(GL_POLYGON_SMOOTH); glEnable(GL_LINE_SMOOTH); GLint success; const GLchar* vsSource[1]; vsSource[0] = vsChar; //这里的vsChar就是着色器代码字符串 GLuint vs = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vs, 1, vsSource, NULL); glCompileShader(vs); glGetShaderiv(vs, GL_COMPILE_STATUS, &success); if(!success) { GLchar infoLog[MAX_LENGTH]; glGetShaderInfoLog(vs, MAX_LENGTH, NULL, infoLog); printf(infoLog); getchar(); exit(0); } GLuint program = glCreateProgram(); glAttachShader(program, vs); glLinkProgram(program); glGetProgramiv(program, GL_LINK_STATUS, &success); if (!success) { GLchar infoLog[MAX_LENGTH]; glGetProgramInfoLog(program, MAX_LENGTH, NULL, infoLog); printf(infoLog); getchar(); exit(0); } glValidateProgram(program); glGetProgramiv(program, GL_VALIDATE_STATUS, &success); if (!success) { GLchar infoLog[MAX_LENGTH]; glGetProgramInfoLog(program, MAX_LENGTH, NULL, infoLog); printf(infoLog); getchar(); exit(0); } glUseProgram(program); lightPosLocation = glGetUniformLocation(program, "lightPos"); if (lightPosLocation != -1) { glUniform3fv(lightPosLocation, 1, g_lightPos); } }
效果如下:(我光照的位置和物体的位置调的不是很好)
如果你还是想使用固定功能管线的glLight*来设置光源的位置的话,需要改一下shader代码。GLSL提供了一个内置的变量gl_LightSource[n].position 其中n为第几个光源。改一下上面的shader.
#define FIX_FUNCTION 1 char vsChar[] = { "#version 120\n" "uniform vec3 lightPos;\n" "void main(void)" "{" " gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;" " vec3 N = normalize(gl_NormalMatrix * gl_Normal);" " vec4 V = gl_ModelViewMatrix * gl_Vertex;" #if FIX_FUNCTION " vec3 L = normalize(gl_LightSource[0].position.xyz - V.xyz);" #else " vec3 L = normalize(lightPos - V.xyz);" #endif " float NdotL = dot(N, L);" " gl_FrontColor = gl_Color * vec4(max(0.0, NdotL));" "}"}; void SetupRC() { ... #if FIX_FUNCTION glLightfv(GL_LIGHT0, GL_POSITION, g_lightPos); #else lightPosLocation = glGetUniformLocation(program, "lightPos"); if (lightPosLocation != -1) { glUniform3fv(lightPosLocation, 1, g_lightPos); } #endif }
这样效果是等价的。你会发现我并没有调用
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
来开启光照。这就是shader的好处,不需要这一堆的开关指令。需要用到的就拿去用吧,没用到就相当于关闭了。shader 可编程管线更加灵活。
镜面光照
镜面光照要考虑光的入射方向,以及眼睛所在的位置。由顶点指向光源的向量,顶点的法线,顶点指向照相机的向量就可以决定镜面光在该顶点的强度。简单起见默认默认照相机在z的正方向上,假设顶点到照相机的单位向量为(0.0, 0.0, 1.0)。根据镜面光的公式:
Cspec = max{N • H, 0}Sexp * Cmat * Cli
H是光线向量与视角向量之间夹角正中方向的单位向量。Sexp代表镜面指数,用于控制镜面光照的紧聚程度。Cmat是材料的颜色,Cli是光的颜色。Cspec 是最终求得的镜面颜色。在下面简单的例子中,假设光是白光(1.0, 1.0, 1.0, 1.0),镜面材料的镜面光属性也为(1.0, 1.0, 1.0, 1.0),所以我们可以忽略掉这一项乘的操作。其中N,L,Cmat 和 Cli 和散射光是一样的。这里镜面指数固定为128.
编写如下的specular.vs:
#version 120 uniform vec3 lightPos; void main(void) { //MVP transform gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; //caculate diffuse //normal vec3 N = normalize(gl_NormalMatrix * gl_Normal); //transform to view coordinate vec4 V = (gl_ModelViewMatrix * gl_Vertex); //light vector vec3 L = normalize(lightPos - V.xyz); float NdotL = dot(N,L); vec4 diffuse = vec4(max(NdotL, 0.0)) * gl_Color; //specular vec3 H = normalize(vec3(0.0, 0.0, 1.0) + L); float NdotH = max(0.0, dot(N,H)); const float expose = 128.0; vec4 specular = vec4(0.0); if (NdotL > 0.0) specular = vec4(pow(NdotH, expose)); gl_FrontColor = diffuse + specular; }
效果图:
提升镜面光照
由上图可以看出,镜面光照的高亮在物体表面变化的非常快。在这里我们只是逐顶点的计算镜面亮点然后在三角形内部进行插值。这样的效果较差。我们并不能获得一个漂亮的圆形的亮点,亮点看起来是不规则多边形的。
一种改善的方式是把散射光的效果和镜面光的效果区分开,把散射光照结果输出为主颜色,镜面光照的结果设置为辅助颜色。相比于之前的逐个顶点计算好光照结果,再进行光栅化插值然后进入片段处理。现在是把镜面光的效果放到辅助颜色,而辅助颜色是在纹理等片段处理之后加到片段上的,这样就能够呈现更真实的光照效果。这种基于片段的求和通过启用GL_COLOR_SUM就可以实现了。
#version 120 uniform vec3 lightPos; void main(void) { // normal MVP transform gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; vec3 N = normalize(gl_NormalMatrix * gl_Normal); vec4 V = gl_ModelViewMatrix * gl_Vertex; vec3 L = normalize(lightPos - V.xyz); vec3 H = normalize(L + vec3(0.0, 0.0, 1.0)); const float specularExp = 128.0; // put diffuse into primary color float NdotL = max(0.0, dot(N, L)); gl_FrontColor = gl_Color * vec4(NdotL); // put specular into secondary color float NdotH = max(0.0, dot(N, H)); gl_FrontSecondaryColor = (NdotL > 0.0) ? vec4(pow(NdotH, specularExp)) : vec4(0.0); }
还需glEanble(GL_COLOR_SUM);
这种方式好像提升了一点点效果。但本质上的原因没有解决,那就是镜面指数的问题。随着镜面系数的提高(N • H),这种基于顶点插值的方式变化的非常快。如果你的物体没有很细的分格化,有可能整个物体都没有得到镜面加亮(比如一个大三角形的三个顶点,都没有得到镜面加亮,那么这个三角形就没有镜面加亮的效果了)。
要避免这个问题的一种有效方法是只输出一个镜面系数(N • H),但是等到片段着色时,才进行幂操作。使用这种方式,可以安全地对变换更慢的镜面系数进行插值。由于现在还没接触到片段着色器。我们可以使用纹理查找的方式来实现这种功能。我们需要做的就是用一个包含S128 个值的表设置一个1D纹理,然后把镜面系数输出为一个纹理坐标。然后再用固定功能管线的方式设置纹理环境把从纹理坐标查找到的镜面颜色与散射光的颜色相加。
着色器代码如下:
#version 120 uniform vec3 lightPos; void main(void) { // normal MVP transform gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; vec3 N = normalize(gl_NormalMatrix * gl_Normal); vec4 V = gl_ModelViewMatrix * gl_Vertex; vec3 L = normalize(lightPos - V.xyz); vec3 H = normalize(L + vec3(0.0, 0.0, 1.0)); // put diffuse lighting result in primary color float NdotL = max(0.0, dot(N, L)); gl_FrontColor = gl_Color * vec4(NdotL); // copy (N.H)*8-7 into texcoord if N.L is positive float NdotH = 0.0;if (NdotL > 0.0) NdotH = max(0.0, dot(N, H) * 8.0 - 7.0); gl_TexCoord[0] = vec4(NdotH, 0.0, 0.0, 1.0); }
在这里N.H的范围会被截取为[0,1],但如果你对其进行128次幂,那么在[0,1]之间的大部分值,都会非常接近0,这样大部分顶点的纹理坐标就是0了。只有[7/8, 1]的值经过幂之后,会有可度量的纹理值。为了充分利用1D纹理,我们可以把几种在上面的八分之一范围的值填充到整个纹理中,来提高结果的精度。我们可以把(N • H)放大8倍,然后左移7个单位,那么[0,1]就被映射为[-7,1],然后使用GL_CLAMP_TO_EDGE环绕模式,[-7,0]的值将会被截取为0.我们所感兴趣的范围[0,1]中的值将接受(7/8)128 和 1之间的纹理单元值。
//创建一个一维的纹理单元 void CreateTexture(float r, float g, float b) { GLfloat texels[512 * 4]; GLint texSize = (maxTexSize > 512) ? 512 : maxTexSize; GLint x; for (x = 0; x < texSize; x++) { texels[x*4+0] = r * (float)pow(((double)x / (double)(texSize-1)) * 0.125f + 0.875f, 128.0); texels[x*4+1] = g * (float)pow(((double)x / (double)(texSize-1)) * 0.125f + 0.875f, 128.0); texels[x*4+2] = b * (float)pow(((double)x / (double)(texSize-1)) * 0.125f + 0.875f, 128.0); texels[x*4+3] = 1.0f; } // Make sure the first texel is exactly zero. Most // incoming texcoords will clamp to this texel. texels[0] = texels[1] = texels[2] = 0.0f; glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA16, texSize, 0, GL_RGBA, GL_FLOAT, texels); } ....//设置纹理模式 glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTexSize); glActiveTexture(GL_TEXTURE0); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexEnvi(GL_TEXTURE_1D, GL_TEXTURE_ENV_MODE, GL_ADD); CreateTexture(1.0f, 1.0f, 1.0f);
获得了更好效果
使用前面方式的效果。
下面的shader使用三个光源:
#version 120 uniform vec3 lightPos[3]; varying vec4 gl_TexCoord[3]; uniform vec3 camaraPos;void main(void) { //MVP transform gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; //经过视图变换后的点 vec4 V = gl_ModelViewMatrix * gl_Vertex; vec3 N[3], L[3], H[3]; gl_FrontColor = vec4(0.0); for (int i = 0; i < 3; ++i) { N[i] = normalize(gl_NormalMatrix * gl_Normal); L[i] = normalize(lightPos[i] - V.xyz); float NdotL = dot(N[i], L[i]); //accumalte diffuse light gl_FrontColor += vec4(max(0.0, NdotL)) * gl_Color; //指向光源的向量,与指向照相机的向量的。半角向量。 H[i] = normalize(L[i] + normalize(camaraPos)); float NdotH = 0.0;if (NdotL > 0.0) NdotH = max(0.0, dot(N, H) * 8.0 - 7.0); gl_TexCoord[i] = vec4(NdotH, 0.0, 0.0, 1.0); } }
上面的例子也用了对应的三个纹理。
... glActiveTexture(GL_TEXTURE0); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD); glBindTexture(GL_TEXTURE_1D, textures[0]); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); CreateTexture(1.0f, 0.25f, 0.25f); glEnable(GL_TEXTURE_1D); glActiveTexture(GL_TEXTURE1); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD); glBindTexture(GL_TEXTURE_1D, textures[1]); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); CreateTexture(0.25, 1.0, 0.25); glEnable(GL_TEXTURE_1D); glActiveTexture(GL_TEXTURE2); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD); glBindTexture(GL_TEXTURE_1D, textures[2]); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); CreateTexture(0.25, 0.25, 1.0); glEnable(GL_TEXTURE_1D); ...
基于顶点的雾
尽管雾是在片段处理阶段中处理的, 但出于性能的考虑,我们可以在顶点阶段对其进行处理,而且也不影响真实性。下面是雾的二次方雾因子的方程式:
ff = e-(d * fc)2
其中d代表雾的浓度,fc是雾坐标,通常情况下是顶点到照相机的距离。下面我们只在shader中计算 雾坐标,雾的方程式用固定功能管线来实现。其中length是GLSL内置的函数,求向量的长度。
float fogColor[4] = {0.5f, 0.8f, 0.5f, 1.0f}; //雾颜色为浅绿
#version 120 uniform vec3 lightPos[1]; uniform vec3 camaraPos;void main(void) { //MVP transform gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; vec4 V = gl_ModelViewMatrix * gl_Vertex; vec3 N = normalize(gl_NormalMatrix * gl_Normal); vec3 L = normalize(lightPos[0] - V.xyz); float NdotL = dot(N, L); vec4 diffuse = max(0.0, NdotL) * gl_Color; const float expose = 128.0; vec3 H = normalize(L + normalize(camaraPos)); float NdotH = 0.0;if (NdotL > 0.0) NdotH = max(0.0, dot(N, H)); vec4 specular = vec4(pow(NdotH, expose)); gl_FrontColor = diffuse + specular; //计算雾坐标 gl_FogFragCoord = length(V); }
.. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND); glFogfv(GL_FOG_COLOR, fogColor); glFogf(GL_FOG_DENSITY, density); glFogi(GL_FOG_MODE, GL_EXP2); glFogi(GL_FOG_COORD_SRC, GL_FOG_COORD); glEnable(GL_FOG); ..
当然我们也可以在shader中直接实现该方程。
#version 120 uniform vec3 lightPos[1]; uniform vec3 camaraPos; uniform float density; void main(void) { //MVP transform gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; vec4 V = gl_ModelViewMatrix * gl_Vertex; vec3 N = normalize(gl_NormalMatrix * gl_Normal); vec3 L = normalize(lightPos[0] - V.xyz); float NdotL = dot(N, L); vec4 diffuse = max(0.0, NdotL) * gl_Color; const float expose = 128.0; vec3 H = normalize(L + normalize(camaraPos)); float NdotH = 0.0;if (NdotL > 0.0) NdotH = max(0.0, dot(N, H)); vec4 specular = vec4(pow(NdotH, expose)); //计算雾因子 const float e = 2.71828; float fogFactor = density * length(V); fogFactor *= fogFactor; fogFactor = clamp(pow(e, -fogFactor), 0.0, 1.0); const vec4 fogColor = vec4(0.5, 0.8, 0.5, 1.0); //把雾颜色和 光的颜色 根据雾因子进行混合 gl_FrontColor = mix(fogColor, clamp(diffuse + specular, 0.0, 1.0), fogFactor); }
genType <strong>mix</strong>(
genTypex, genTypey, genTypea)
;
mix在x,y之间进行插值,a是权值。 插值的公式是x⋅(1−a)+y⋅a.
源码参考:https://github.com/sweetdark/openglex 项目下 specular multilight 和 fogvs。 shader在 shadersource目录下。