//OpenGL抗锯齿代码
LISTING 3.3 Switching Between Antialiased and Normal Rendering
///
// Reset flags as appropriate in response to menu selections
void ProcessMenu(int value)
{
switch(value)
{
case 1:
// Turn on antialiasing, and give hint to do the best
// job possible.
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
glEnable(GL_POINT_SMOOTH);
glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
glEnable(GL_LINE_SMOOTH);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glEnable(GL_POLYGON_SMOOTH);
glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);
break;
case 2:
// Turn off blending and all smoothing
glDisable(GL_BLEND);
glDisable(GL_LINE_SMOOTH);
glDisable(GL_POINT_SMOOTH);
glDisable(GL_POLYGON_SMOOTH);
break;
default:
break;
}
// Trigger a redraw
glutPostRedisplay();
}
1 GlHint有操作和模式两个参数,功能是设置OpenGL中某个操作的模式
2 因为普通抗锯齿
glEnable(GL_POLYGON_SMOOTH);
glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);
是依赖于Blending,而blending又依赖于图元排序,所以多边形的抗锯齿很多时候都失效。所以目前更多的是采用多重采样,多重采样就是对一个像素点进行多次采样,并存储在一个缓存中,就像颜色缓存,深度缓存和模板缓存。要注意的是,多重采样和抗锯齿不能同时开。
3 OpenGL的glEnable和glDisable操作其实是在改变OpenGL的内部状态机状态,而状态的改变是十分影响rendering的效率的,所以很多时候程序员都需要对渲染操作进行状态排序,如将需要相同状态得redering operation·排在一起,这在游戏渲染中尤为常见。
4.理解变换
OpenGL中的变换大致分为视图变换,模型变换,(视图和模型变换有分为平移,旋转,放缩),投影变换和视口变换。这些变换都是在一个基础坐标系-视觉坐标系(eye coordinates)上完成的,我们可以将视觉坐标视为绝对的屏幕坐标,视图变换可以理解为找到一个合适的观察视觉坐标系的位置,其中透视投影的默认观察点在视觉坐标系原点,正投影在z轴正方向无穷远的地方。模型变换则可以理解为将模型所在的坐标系进行平移,旋转等操作。所以先旋转后平移和先平移后旋转的结果是不一样的。见中文版(p97)。由此可见,视图变换和模型变换本质是一样的,视图变换是将模型变换运用到一个虚拟对象(观察者)上。
5. OpenGL的矩阵构造
OpenGL的矩阵是一个二维数组,且以行优先的方式存储,有16个值,如下:
这十六个值(这里的图画的是列优先,先不看最后一行)在不同的应用场合可以有不同的理解,可以理解为坐标系的变换,也可以理解为空间中一个特定的位置,其中前三列给出了x,y,z轴的朝向,最后一列给出了平移向量或者位置向量(看怎么理解)。
6.关于GLTOOLS自带的画球等函数参数的理解
void gltMakeSphere(GLTriangleBatch& sphereBatch, GLfloat fRadius,
GLint iSlices, GLint iStacks);
7.OpenGL渲染管线
1视觉坐标系中的原始坐标经过模型视图转换---->投影变换---->透视除法 变换到标准的设备坐标(-1到1之间),最后经过视口变换,生成窗口坐标
8.OpenGL中的PushMatrix和PopMatrix
PushMatrix是OpenGL矩阵栈(通常栈的深度为64)的一个push函数,其功能是将当前模型视图矩阵复制一个,然后压入栈顶。这样做的好处就是操作的模型视图矩阵永远是保存的模型视图矩阵的拷贝值,无论再对这个矩阵再做什么变换,只要变换完成后执行PopMatrix(将栈顶矩阵弹出栈)就能恢复保存的模型视图矩阵。可以发现只要执行了PushMatrix,且栈的深度大于1,矩阵栈的栈顶两个矩阵总是相同,再往下总是不同的(往下保存的是场景中模型视图变换的各个阶段的状态值)。
9.3D环境中典型的渲染环境流程
图中的save操作就是pushMatrix,restore操作就是popMatrix
10.参考帧和角色帧
由渲染管线可知,OpenGL中绘制每个对象都需要一个变换矩阵,这个变换矩阵就是对象的参考帧。前面讲过模型视图矩阵既可以理解为场景中的某个位置,也可以理解为某个变换。角色帧是一种存储参考帧角色位置和朝向状态得数据结构,所以可以从这个数据结构推导出一个变换矩阵。有了模型变换矩阵,就可以继续OpenGL的渲染管线了!
class GLFrame
{
protected:
M3DVector3f vLocation; //Where I am
M3DVector3f vUp;//Where way is up
M3DVector3f vForward;//Where I am going
public:
. . .
};
11.照相机
OpenGL中是不存在照相机变换这样的东西的,这样取名字只是好帮助我们理解。现实世界中你会发现你移动照相机去观察一个对象和这个对象向相反方向移动所取得的效果是一样的,所以前面所说的参考帧既可以用照相机表示,也可以用角色帧表示。所以照相机变化其实就是虚拟出一个照相机角色,对它进行角色变换再进行反转。
12.光线变换
前面所讲的都是变换几何图形,但是光源并不是一个几何图形,也需要变换,对于固定光源来说,只要应用照相机变换就好了。下面是GlTools中提供的光线变换的代码:
// Transform the light position into eye coordinates
M3DVector4f vLightPos = { 0.0f, 10.0f, 5.0f, 1.0f };
M3DVector4f vLightEyePos;
m3dTransformVector4(vLightEyePos, vLightPos, mCamera);
shaderManager.UseStockShader(GLT_SHADER_POINT_LIGHT_DIFF,
transformPipeline.GetModelViewMatrix(),
transformPipeline.GetProjectionMatrix(),
vLightEyePos, vSphereColor);
13.纹理
纹理是一种能应用到场景中三角形上的图像数据,它由纹理单元(texel)组成,由0到1的纹理坐标寻址。关于纹理OpenGL有加载纹理和从从纹理提取图片两个重要操作,这种大面积的读和写需要适当的内存对齐方式来提高读写效率。OpenGL默认采用4字节对齐方式,而熟悉的bmp图片像素数据4字节排列,tga采用1字节排列。下面的两个函数提供了改变图像存储方式的接口:
void glPixelStorei(GLenum pname, GLint param);
void glPixelStoref(GLenum pname, GLfloat param);
OpenGL无法将像素图(pixmap)绘制到颜色缓存,但能够从颜色缓存读取像素图。glReadPixels提供了接口。
void glReadPixels(GLint x, GLint y, GLSizei width, GLSizei height,
GLenum format, GLenum type, const void *pixels);
glReadPixels是从图形硬件复制数据通过系统总线到内存,这个过程,应用程序会被阻塞,此外如果指定一个与图形硬件的排列不同的像素布局,在数据格式重定的过程中会产生额外性能开销。
GLTools中的gltWriteTGA函数可以将屏幕图像保存为1个Targa文件。
应用纹理贴图的第一个步骤就是将纹理载入内存,glTexImage提供了这个接口,纹理可以从存储器缓冲区(硬盘等)载入,也可以从颜色缓冲区(图形硬件)载入,glCopyTexImage提供了这个接口。
纹理加载甚至可以支持替换已加载纹理的一部分,这可以提高性能,在游戏中很常见。glTexSubImage提供了这个接口。当然glCopyTexSubImage允许从颜色缓存中加载纹理并插入或替换原来纹理的一部分。
14.纹理状态和纹理对象
当今的图形硬件能同时支持多种纹理,而OpenGL发展出了管理多种纹理并在他们之间转换的方法。首先介绍一下纹理状态,纹理状态包含纹理图像和控制过滤(filtering)和纹理坐标行为的参数。glTexParameter提供了设置参数的接口。
纹理对象允许我们一次加载一个以上的纹理状态,并在他们之间快速切换。(glGenTextures用来产生若干纹理对象)
也就是说同时可以有多个纹理对象,但纹理状态是由当前绑定的纹理对象维护的。(glBindTexture用来绑定某一个纹理对象)纹理状态直到绑定后才真正被创建。
15.应用纹理
前面已经讲过了应用纹理的第一个步骤,载入纹理。接下来还需要提供纹理坐标,设置纹理坐标环绕模式和纹理过滤。最后还可以选择Mip贴图来进一步提高性能。
纹理贴图就是将纹理坐标和模型坐标一一映射,将纹理坐标处的纹理单元贴在对应的模型坐标处,但是怎么选取坐标处的纹理单元,这就需要设置纹理过滤模式。
可以同时设置放大(GL_TEXTURE_MAX_FILTER)和(GL_TEXTURE_MIN_FILTER)缩小过滤器,基本的过滤模式有GL_NEAREST和GL_LINEAR。GL_NEAREST就是选取离纹理坐标最近的纹理单元,GL_LINEAR是对纹理坐标周围单元求加权平均值。
设置纹理坐标环绕模式是指当纹理坐标超过(0-1)这个范围,映射关系的处理模式设置,详情见(P144)典型的有GL_CLAMP_TO_EDGE。
16.MIP贴图
渲染场景中物体的大小和远近经常是发生变化的,而采用同一种纹理,这个纹理也会不停的放大或缩小,这就会带来图像模糊和闪烁的问题。Mip贴图的出现极大程度的解决了这个问题,Mip贴图是把同一个图像的从大到小(每个图像缩小一半,不一定是正方形,直到1*1)的一系列图像加载到单个纹理状态,然后OpenGL使用一组新的过滤模式根据对象的不同状态选择适当的纹理。glTexImage的level参数来控制加载那个Mip层。默认情况所有层都加载,glTexParameter的GL_TEXTURE_BASE_LEVEL和GL_TEXTURE_MAX_LEVEL控制加载的层。下表的后四项是专门针对Mip贴图的过滤模式,注意到这四项都是为缩小过滤器设置的,这是由Mip贴图的性质决定的。其中最后一种称为三线性模式,直到最近它还是纹理过滤的黄金准则。
void glGenerateMipmap(GLenum target);//用来生成所有的Mip层
各向异性过滤是为了解决观察方向和观察点不垂直时纹理贴图不逼真的问题。详见p158。
18.纹理压缩
纹理压缩是指内存中甚至图形硬件缓存中加载的都是纹理的压缩格式,这必然会带来首次加载过程的性能开销,但是在后续的纹理加载中会极大的提高纹理加载速度。
纹理压缩方法的选择极大的依赖于底层图像的本质。纹理压缩通过设置glTexImage中的internalFormat参数来实现。详见(p160)。
19.GLSL
GLSL是OpenGL的自定义着色语言,它由OpenGL内置的编译器编译并链接 。对应于顶点着色器,片段着色器,GLSL有顶点程序和片段程序。
数据类型:
基本数据类型只有4种:bool, int, uint,float
向量类型:上面基本类型组成的2-4维向量
向量的寻址:通常操作:使用xyzw寻址,v.xyz = v2(3.f, 1.f, 2.f) ,颜色操作:v.rgba = v2(3.f, 1.f, 2.f)
纹理操作:v.st = v2.st, 调换操作: v.rgba = v2.bgra
向量数据类型不仅是GLSL的本地数据类型,也是硬件的本地数据类型。所以一次性完成对分量的所有操作比单独操作要快很多。
矩阵类型:
从mat2*2到mat4*4,其中行列数相等的可以简化为mat2,mat3,mat4。GLSL的矩阵是由列向量组成的数组
存储限定符:
const 一个编译时常量,或者说是一个对函数来说为只读的参数
in 一个从以前阶段传递过来的变量
in centroid 一个从以前的阶段传递过来的变量,使用质心插值
out 传递到下一个处理阶段或者在一个函数中指定一个返回值
out centroid 传递到下一个处理阶段,使用质心插值
inout 一个读写变量,只能用于局部参数,相当于c++中的引用。
uniform 一个从客户端代码传递过来的变量,在顶点之间不做改变
centroid在对多重采样缓冲区进行渲染时才会起作用。
在默认情况下,参数将在两个着色器阶段之间一种透视正确的方法进行插补,也可以通过noperspective关键词来指定非透视插值,甚至通过flat关键词不进行插值,默认是smooth即透视正确方式。
smooth out vec3 vSmoothValue;
flat out vec3 vFlatColor;
noperspective float vLinearlySmoothed;
hVertexShader = glCreateShader(GL_VERTEX_SHADER);
hFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
在OpenGL4.0中,flat shading效果可以通过对着色器的输入和输出变量使用一个修饰符flat很方便的实现。
在顶点着色器的输出变量和片断着色器要使用作为颜色的输入变量前使用这个修饰符即可。
这个修饰符表明了这个值在传递到片断着色器的时候没有插值发生。
最后多边形的颜色可能是其顶点中最先或最后被渲染的顶点的颜色。可以通过下列函数调用来控制:
glProvokingVertex(GL_FIRST_VERTEX_CONVENTION);
或者是
glProvokingVertex(GL_LAST_VERTEX_CONVENTION);
20.着色器的编译,绑定和链接
(1)指定属性
hShader = gltLoadShaderPairWithAttributes(“vertexProg.vp”,
“fragmentProg.fp”, 2, 0, “vVertexPos”, 1, “vNormal”);
其中.vp和.fp分别是顶点程序和片段程序,2是属性个数,最多有16个,然后是可变参数对:属性值和属性位置
(2)创建着色器对象
hVertexShader = glCreateShader(GL_VERTEX_SHADER);
hFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
(3)将着色器文件传递入着色器对象
(4)编译着色器
glCompileShader(hVertexShader)
glCompilerShader(hFragmentShader)
每个OpenGL实现有个内建的GLSL编译器,这个编译器由硬件提供商提供。
(5)进行绑定
hReturn = glCreateProgram();//创建着色器程序对象
glAttachShader(hReturn, hVertexShader);//将着色器与程序对象绑定
glAttachShader(hReturn, hFragmentShader);
(6)绑定属性到指定位置
// Iterate over this argument list
char *szNextArg;
int iArgCount = va_arg(attributeList, int); // Number of attributes
for(int i = 0; i < iArgCount; i++)
{
int index = va_arg(attributeList, int);
szNextArg = va_arg(attributeList, char*);
glBindAttribLocation(hReturn, index, szNextArg);
}
va_end(attributeList);
(7)连接着色器
glLinkProgram(hReturn);
// These are no longer needed
glDeleteShader(hVertexShader);
glDeleteShader(hFragmentShader);
着色器的删除:void glDeleteProgram(GLuint program);
着色器的使用: glUseProgram(myShaderProgram);
21.着色器统一值
uniform最多被用在转换矩阵上,两个阶段的uniform不能被插值,是只读的。
寻找统一值:当在GLSL着色器程序中中声明一个统一值,如uniform float time;在c++代码中寻找的方法:
GLint locTime;
locTime = glGetUniformLocation(myShader, "time");
设置统一值:
glUniform1i(locTime, 4.f);
布尔值亦可以作为浮点值传递,0.0代表false,1.0代表true。
设置统一数组:
GLfloat vColors[2][4] = {{ 1.0f, 1.0f, 1.0f, 1.0f },
{ 1.0f, 0.0f, 0.0f, 1.0f }};
…
glUniform4fv(iColorLocation, 2, vColors);
设置同一矩阵:
glUniformMatrix4fv(GLint location, GLuint count, GLboolean transpose,
const GLfloat *m);
The Boolean flag transpose is set to GL_TRUE if the matrix is already stored in column major ordering (the way OpenGL prefers). Setting this
22.模拟光线
(1)点光源漫反射着色器
要确定一个指定顶点上光线的强度,我们需要两个向量,第一个是光源的方向,某些光源技术只提供光源的向量,我们称之为定向光(directional light),这种方式在光源距离非常远的时候是很适用的,因为在光源距离远的时候,对于所有顶点来说,指向光源的都是通一个向量。如果只提供光源的位置,那必须用光源位置减去顶点位置,来确定指向光源的向量。
有了两个向量之后,顶点上光的强度通过两个单位化向量的点乘积来计算。返回的值是-1到1之间,但光照强度不可能是负值,所以负值都设为0。得到顶点的光强之后,在顶点之间对这些颜色进行插值,这种照明方式呗成为顶点照明(vertex lighting)或者(Gouraud shading).
顶点程序:
// Simple Diffuse lighting Shader
// Vertex Shader
// Richard S. Wright Jr.
// OpenGL SuperBible
#version 130
// Incoming per vertex... position and normal
in vec4 vVertex;
in vec3 vNormal;
// Set per batch
uniform vec4 diffuseColor;
uniform vec3 vLightPosition;
uniform mat4 mvpMatrix;//模型视图投影矩阵
uniform mat4 mvMatrix;//模型视图矩阵
uniform mat3 normalMatrix;//从模型视图矩阵中抽离出来的单位化的旋转矩阵
// Color to fragment program
smooth out vec4 vVaryingColor;
void main(void)
{
// Get surface normal in eye coordinates
vec3 vEyeNormal = normalMatrix * vNormal;//因为不能讲平移矩阵应用到法向量,如平移法向量的z值时将导致法向量不再和表面垂直,所以只能将旋转矩阵抽离出来
// Get vertex position in eye coordinates
vec4 vPosition4 = mvMatrix * vVertex;//不使用mvp矩阵是因为这里是求3d场景中真实的物理模型。不用投影到平面
vec3 vPosition3 = vPosition4.xyz / vPosition4.w;//单位化
// Get vector to light source
vec3 vLightDir = normalize(vLightPosition - vPosition3);
// Dot product gives us diffuse intensity
float diff = max(0.0, dot(vEyeNormal, vLightDir));//计算光强
// Multiply intensity by diffuse color
vVaryingColor.rgb = diff * diffuseColor.rgb;
vVaryingColor.a = diffuseColor.a;
// Let's not forget to transform the geometry
gl_Position = mvpMatrix * vVertex;
}
片段程序:
// Simple Diffuse lighting Shader
// Fragment Shader
// Richard S. Wright Jr.
// OpenGL SuperBible
#version 130
out vec4 vFragColor;
smooth in vec4 vVaryingColor;
void main(void)
{
vFragColor = vVaryingColor;
}
A代表ambient,D代表diffuse,S代表specular。它遵循一个简单的原则,即物体有三种材质属性,环境光反射,漫反射和镜面反射,这些属性都是分配的颜色值,更明亮代表更高的反射量。光源也有三种相同的属性,同样也是分配的颜色值,表示光源的强度,最终的顶点颜色值就是光照和材质这3个属性互相影响的总和。
环境光:
uniform vec3 vAmbientMaterial;
uniform vec3 vAmbientLight;
vec3 vAmbientColor = vAmbientMaterial * vAmbientLight;
漫射光:
uniform vec3 vDiffuseMaterial;
uniform vec3 vDiffuseLight;
float fDotProduct = max(0.0, dot(vNormal, vLightDir));
vec3 vDiffuseColor = vDiffuseMaterial * vDiffuseLight * fDotProduct;
镜面光:
镜面光也就是光线照到物体表面反射的光,为了计算这个光强,我们首先需要计算出反射光强,然后去“镜面指数(specular power)”次幂。反光度参数也可以是统一值,固定管线中,最高的镜面指数被设置为128.
顶点程序:
// ADS Point lighting Shader
// Vertex Shader
// Richard S. Wright Jr.
// OpenGL SuperBible
#version 330
// Incoming per vertex... position and normal
in vec4 vVertex;
in vec3 vNormal;
// Set per batch
uniform vec4 ambientColor;
uniform vec4 diffuseColor;
uniform vec4 specularColor;
uniform vec3 vLightPosition;
uniform mat4 mvpMatrix;
uniform mat4 mvMatrix;
uniform mat3 normalMatrix; // Color to fragment program smooth out vec4 vVaryingColor; void main(void) { // Get surface normal in eye coordinates vec3 vEyeNormal = normalMatrix * vNormal; // Get vertex position in eye coordinates vec4 vPosition4 = mvMatrix * vVertex; vec3 vPosition3 = vPosition4.xyz / vPosition4.w; // Get vector to light source vec3 vLightDir = normalize(vLightPosition - vPosition3); // Dot product gives us diffuse intensity float diff = max(0.0, dot(vEyeNormal, vLightDir)); // Multiply intensity by diffuse color, force alpha to 1.0 vVaryingColor = diff * diffuseColor; // Add in ambient light vVaryingColor += ambientColor; // Specular Light vec3 vReflection = normalize(reflect(-vLightDir, vEyeNormal)); float spec = max(0.0, dot(vEyeNormal, vReflection)); if(diff != 0) { float fSpec = pow(spec, 128.0); vVaryingColor.rgb += vec3(fSpec, fSpec, fSpec); } // Don’t forget to transform the geometry! gl_Position = mvpMatrix * vVertex; }
片段程序和上节一样。因为这个程序同样是在顶底之间插值颜色值,所以准确说叫ADSGouraud着色.
24.Phone着色。
如上图,Gouraud有一个缺点,注意高亮部分的星光现象,而且旋转时会闪烁,这是由三角形之间不连续(不平滑)造成的,不连续导致在顶点之间颜色插值也不连续。当然细化更多的三建面片会改善这种现象。
另一种更高品质的方法叫做Phong着色,Phone着色并不在顶点之间进行颜色插值,而是在顶点之间进行表面法线插值。体现在GLSL程序上就是将顶点程序中的颜色计算搬到片段程序中。在顶点程序和片段程序见传递smooth声明的法线值。当然Phong在提高渲染质量的同时,加大了性能的开销。因为一个着色器优化的常规原则就是将尽可能多的处理过程放入顶点着色器。
25.通过着色器访问纹理
从着色器访问纹理非常简单。纹理坐标作为属性传递到顶点着色器,并通过在顶点之间平滑插值传递到片段着色器。片段着色器使用这些插值纹理坐标对纹理进行采样,经过采样和过滤的纹理颜色将作为RGBA颜色值返回,我们可以直接将它写入片段,也可以将它与其它颜色计算混合。
顶点程序:
// The TexturedIdentity Shader
// Vertex Shader
// Richard S. Wright Jr.
// OpenGL SuperBible
#version 130
in vec4 vVertex;
in vec2 vTexCoords;
smooth out vec2 vVaryingTexCoords;
void main(void)
{
vVaryingTexCoords = vTexCoords;
gl_Position = vVertex;
}
片段程序:
// The TexturedIdentity Shader
// Fragment Shader
// Richard S. Wright Jr.
// OpenGL SuperBible
#version 130
uniform sampler2D colorMap;
out vec4 vFragColor;
smooth in vec2 vVaryingTexCoords;
void main(void)
{
vFragColor = texture(colorMap, vVaryingTexCoords.st);
}
在这个着色器中,我们调用纹理贴图内建函数texture来使用插值纹理坐标对纹理进行采样,并将颜色值直接分配个片段颜色。
26.照亮纹理单元
照亮纹理单元就是将纹理颜色值乘以光线强度。有一个要注意的问题是有时候环境光和漫反射光的总和会很大,看起来就像纯白色,而纯白色在颜色空间就为1,将一个纹理单元的颜色乘以1还是原来的纹理颜色,这就意味着表现白色高光是不可能的,而事实上,光照计算时每个颜色通道的值经常稍微大于1,这就意味这着获得一个白色高光是可能的。
27.丢弃片段
丢弃片段是一种在片段的透明度接近0也即几乎不可见时丢弃这个片段的技术。片段程序中通过discard关键词来实现.使用discard声明,我们可以逐个像素的控制哪个片段会进行绘制,哪个片段不会。
if(vCloudSample.a < dissolveFactor)
discard;
28.卡通着色(Cell shading)-----将纹理单元作为光线。
卡通着色将一个一维纹理贴图作为查询表,使用纹理贴图中的纯色(使用GL_NEAREST)填充几何图形。基本的思路是,使用漫反射光强度作为纹理坐标去映射逐渐变亮的一维纹理中对应的纹理颜色值,最后作为片段颜色。
29.矩形纹理
矩形纹理是一种特殊的纹理,它不能Mip贴图,对像素寻址而不是由(0-1)映射到纹理图像,纹理坐标(1,1)即是从左边起第一个,从上面起第一个像素。纹理坐标不能重复,不支持纹理压缩。矩形纹理比通常的2d纹理贴图更简单,并且更快更高效。
30.立方体贴图
立方体贴图是作为一个单独的纹理对象来看待的,它是由组成立方体6个面的6个正方形的2d图像组成,立方体贴图应用在3D光线贴图,反射和高精度环境贴图。立方体贴图的寻址很独特,它的纹理坐标是由从立方体中心出发的有符号向量表示的,这个向量和6个面中的一个面相交,然后围绕这个点的纹理将被采样。
31.多重纹理
多重纹理就是将独立的纹理对象绑定到一些可用的纹理单元上,从而提供了将两个或更多的纹理同时应用到几何图形上的能力。默认情况下第一个纹理单元为活动的纹理单元,所有纹理绑定操作都会影响到当前活动的纹理单元,我们可以通过调用以纹理单元表示符为变量的glActiveTexture来改变当前纹理单元。
一个示例片段程序:
// Reflection Shader with multitexture
// Fragment Shader
// Richard S. Wright Jr.
// OpenGL SuperBible
#version 330
out vec4 vFragColor;
uniform samplerCube cubeMap;
uniform sampler2D tarnishMap;
smooth in vec3 vVaryingTexCoord;
smooth in vec2 vTarnishCoords;
void main(void)
{
vFragColor = texture(cubeMap, vVaryingTexCoord.stp);
vFragColor *= texture(tarnishMap, vTarnishCoords);
}
要将cubeMap和tarnishMap分别绑定到纹理单元1和纹理单元2上,我们应该这样做:
(1)设定cubeMap和tarnishMap的纹理单元
locCubeMap = glGetUniformLocation(reflectionShader, "cubeMap");
locTarnishMap = glGetUniformLocation(reflectionShader, "tarnishMap");
glUniform1i(locCubeMap, 1);
glUniform1i(locTarnishMap, 2);
(2)将纹理对象绑定到纹理单元
glActiveTexture(GL_TEXTURE0);//声明当前活动的纹理单元
glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTexture);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, tarnishTexture);
32.点精灵
点精灵又叫点块纹理,即意味着能将纹理贴图应用于整个点。点精灵和抗锯齿不能同时使用。
LISTING 7.7 Texturing a Point Sprite in the Fragment Shader
// SpaceFlight Shader
// Fragment Shader
// Richard S. Wright Jr.
// OpenGL SuperBible
#version 330
out vec4 vFragColor;
in vec4 vStarColor;
uniform sampler2D starImage;
void main(void)
{
vFragColor = texture(starImage, gl_PointCoord) * vStarColor;//gl_PointCoord是一个内建变量,
}
(1)点大小:
有两种方式可以设定点大小,第一种是glPointSize函数。这样设置会是点的大小始终不变。第二种是首先启用glEnable(GL_PROGRAM_POINT_SIZE),然后在顶点程序中内建gl_PointSize,然后基于距离或者其它更新点大小。
(2)点参数:
通过glPointParameter函数,我们可以对点精灵的几个特性进行调整,如纹理的方向和透明度。
(3)异形点
我们可以在片段着色器中使用discard关键字来丢弃位于我们想要的点形状范围之外的片段,从而创建出非正方形的点。
33.纹理数组
在纹理数组中,我们可以将整个数组的纹理图像绑定到一个纹理对象上,然后在着色器中对它们进行检索,这样就大大增加了着色器可用的纹理数据的数量。
纹理数组添加了两个新的纹理对象作为大多数纹理管理函数的有效参数,他们是GL_TEXTURE_1D_ARRAY和GL_TEXTURE_1D_ARRAY。我们以加载月亮的不同天内的30个图像为例,一秒切换一个图像。
GLuint moonTexture;
. . .
. . .
glGenTextures(1, &moonTexture);
glBindTexture(GL_TEXTURE_2D_ARRAY, moonTexture);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, 64, 64, 29, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL);//data传为NULL,设置好纹理的宽高64*64,及图片的个数29,采用//下面的glTexSubImage3D来更新纹理
//依次加载29幅图片纹理
for(int i = 0; i < 29; i++) {
char cFile[32];
sprintf(cFile, “moon%02d.tga”, i);
GLbyte *pBits;
int nWidth, nHeight, nComponents;
GLenum eFormat;
// Read the texture bits
pBits = gltReadTGABits(cFile, &nWidth, &nHeight, &nComponents, &eFormat);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, nWidth, nHeight,//i即是29幅图片的索引号
1, GL_BGRA, GL_UNSIGNED_BYTE, pBits);
free(pBits);
}
顶点程序:
// MoonShader
// Vertex Shader
// Richard S. Wright Jr.
// OpenGL SuperBible
#version 330
in vec4 vTexCoords;
uniform mat4 mvpMatrix;
uniform float fTime;
smooth out vec3 vMoonCoords;
void main(void)
{
vMoonCoords.st = vTexCoords.st;
vMoonCoords.p = fTime;//fTime从客户端程序设置,p用来索引纹理数组
gl_Position = mvpMatrix * vVertex;
}
片段程序:
// MoonShader
// Fragment Shader
// Richard S. Wright Jr.
// OpenGL SuperBible
#version 330
out vec4 vFragColor;
uniform sampler2DArray moonImage;//注意这里的采样器sampler2DArray和采样函数texture2DArray
smooth in vec3 vMoonCoords;
void main(void)
{
vFragColor = texture2DArray(moonImage, vMoonCoords.stp);
}
缓冲区对象是一个强大的概念,它允许应用程序迅速方便的将数据从一个渲染管线移动到另一个渲染管线,以及从一个对象绑定到领一个对象。我们不仅可以把数据移动到合适的位置,还可以在无需CPU介入的情况下完成这项工作。
(1)创建缓冲区
使用glGenBuffers创建新缓冲区名称。
Gluint pixBuffObjs[1];
glGenBuffers(1, pixBuffObjs);
使用glBindBuffer将名称绑定到绑定点来使用缓冲区。
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBuffObjs[0]);
其中常用的GL_PIXEL_PACK_BUFFER用来指定glReadPixels之类像素包装操作的缓冲区,GL_PIXEL_UNPACK_BUFFER用来指定glTexImageXD等纹理更新函数的源缓冲区。如glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBuffObjs[1]);
解除缓冲区的绑定,只需将名设为0,在使用完一个缓冲区后,用glDeleteBuffers删除缓冲区。
glDeleteBuffers(1, pixBuffObjs);
(2)填充缓冲区
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBuffObjs[0]);//填充之前要绑定,即说明对哪个GL_PIXEL_PACK_BUFFER缓冲区填充
glBufferData(GL_PIXEL_PACK_BUFFER, pixelDataSize, pixelData, GL_DYNAMIC_COPY);//GL_DYNAMIC_COPY是缓冲区的使用方式,
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);//解除绑定,这是一个好的编程习惯,因为缓冲区填充已经完毕,当前不需要再绑定
我们可以使用glBufferSubData对已经存在的缓冲区的一部分进行更新
void glBufferSubData(GLenum target, intptr offset, sizeiptr size, const void *data);
传统方式:
void* data = (void*)malloc(pixelDataSize);
glReadBuffer(GL_BACK_LEFT);
glReadPixels(0, 0, GetWidth(), GetHeight(), GL_RGB, GL_UNSIGNED_BYTE, pixelData);//将像素读入到pixelData,这意味着要和CPU打交道
PBO方式:
glReadBuffer(GL_BACK_LEFT)
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBuffObjs[0]);
glReadPixels(0, 0, GetWidth(), GetHeight(), GL_RGB, GL_UNSIGNED_BYTE, NULL);//最后一个参数设为NULL意味着读取的像素直接存到了GL_PIXEL_PACK_BUFFER,而
//GL_PIXEL_PACK_BUFFER在GPU中,所以不需要和CPU打交道
(4)将PBO数据读出到纹理传统方式:
// Push pixels from client memory into texture
glActiveTexture(GL_TEXTURE0+GetBlurTarget0() );
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, screenWidth, screenHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, pixelData)
PBO方式:
// Next bind the PBO as the unpack buffer, then push the pixels straight into the texture
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pixBuffObjs[0]);
glActiveTexture(GL_TEXTURE0+GetBlurTarget0() );
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, screenWidth, screenHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
35.纹理缓冲区对象
一个纹理包含两个主要组成部分:纹理采样状态和包含纹理的数据缓冲区。现在我们可以将一个缓冲区对象绑定到GL_TEXTURE_BUFFER缓冲区的一个纹理的绑定点。为什么要有纹理缓冲区呢,首先,纹理缓冲区能够直接填充来自其他渲染·的结果,其次是纹理缓冲区宽松的大小限制,OoenGL规范中其比1D纹理大64倍,某些情况要大几万倍。
纹理缓冲区作为普通的缓冲区来创建,当它被绑定到一个纹理或者GL_TEXTURE_BUFFER绑定点时才会成为真正的纹理缓冲区。
glBindBuffer(GL_TEXTURE_BUFFER, texBO[0]);
glBufferData(GL_TEXTURE_BUFFER, sizeof(float)*count, fileData,
GL_STATIC_DRAW);
但是texBo必须绑定到一个纹理单元上才能真正的使用。
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_BUFFER, texBOTexture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, texBO[0]);//将texBo绑定到texBOTexture上
在片段程序中,纹理缓冲区通过采样器:samplerBuffer来采样,采样函数为texelFetch
uniform samplerBuffer lumCurveSampler;
void main(void) {
. . .
int offset = int(vColor.r * (1024-1));
lumFactor.r = texelFetch(lumCurveSampler, offset ).r;
}
36.帧缓冲区对象
一个OpenGL窗口的表面长期以来一直被称作“帧缓冲区”。但是现在OpenGL将绘制帧缓冲区到一个对象所需要的状态进行了封装,称为帧缓冲区对象(FBO).我们可以创建多个帧缓冲区对象,并且直接渲染一个FBO而不是窗口,这就是离屏渲染。我们甚至可以将一个纹理绑定到一个FBO上,这就意味着可以直接渲染到一个纹理中。FBO其实是一种容器,它可以保存其它确实有内存存储的渲染缓冲区对象RBO,如颜色缓冲区,深度缓冲区,模板缓冲区。
(1)创建FBO
GLuint fboName;
glGenFramebuffers(1,&fboName);
(2)绑定FBO
同一时间只有一个FBO可以绑定用来进行绘制,同一时间也只有一个FBO可以绑定用来进行读取。FBO绑定点有GL_DRAW_FRAMEBUFFER和GL_READ_FRAMEBUFFER,将名称0绑定到任意一个FBO目标,读取和写入就都再次绑定到默认的窗口帧缓冲区。
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboName);
(3) 渲染缓冲区对象RBO的创建
GLuint depthBufferName;
GLuint renderBufferNames[3];
// Create depth renderbuffer
glGenRenderbuffers(1, &depthBufferName);
glBindRenderbuffer(GL_RENDERBUFFER, depthBufferName);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32, screenWidth, screenHeight);
// Create 3 color renderbuffers
glGenRenderbuffers(3, renderBufferNames);
glBindRenderbuffer(GL_RENDERBUFFER, renderBufferNames[0]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, screenWidth, screenHeight);
glBindRenderbuffer(GL_RENDERBUFFER, renderBufferNames[1]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, screenWidth, screenHeight);
glBindRenderbuffer(GL_RENDERBUFFER, renderBufferNames[2]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, screenWidth, screenHeight);
(4)RBO的绑定
// Attach all 4 renderbuffers to FBO
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboName);
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBufferName);
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBufferNames[0]);
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_RENDERBUFFER, renderBufferNames[1]);
glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_RENDERBUFFER, renderBufferNames[2]);
(5)缓冲区绘制
着色器输出:
#version 150
// multibuffer_frag_location.fs
// outputs to 3 buffers: normal color, greyscale,
// and luminance adjusted color
in vec4 vFragColor;
in vec2 vTexCoord;
uniform int bUseTexture;
uniform sampler2D textureUnit0;
uniform samplerBuffer lumCurveSampler;
out vec4 oStraightColor;
out vec4 oGreyscale;
out vec4 oLumAdjColor;
void main(void) {
vec4 vColor;
vec4 lumFactor;
if (bUseTexture != 0)
vColor = texture(textureUnit0, vTexCoord);
else
vColor = vFragColor;
// Untouched output goes to first buffer
oStraightColor = vColor;
// Black and white to second buffer
float grey = dot(vColor.rgb, vec3(0.3, 0.59, 0.11));
oGreyscale = vec4(grey, grey, grey, 1.0f);
// clamp input color to make sure it is between 0.0 and 1.0
vColor = clamp(vColor, 0.0f, 1.0f);
int offset = int(vColor.r * (1024 - 1));
oLumAdjColor.r = texelFetch(lumCurveSampler, offset ).r;
offset = int(vColor.g * (1024 - 1));
oLumAdjColor.g = texelFetch(lumCurveSampler, offset ).r;
offset = int(vColor.b * (1024 - 1));
oLumAdjColor.b = texelFetch(lumCurveSampler, offset ).r;
oLumAdjColor.a = 1.0f;
}
缓冲区映射:
glBindFragDataLocation(processProg, 0, "oStraightColor");
glBindFragDataLocation(processProg, 1, "oGreyscale");
glBindFragDataLocation(processProg, 2, "oLumAdjColor");
static const GLenum fboBuffs[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboName);
glDrawBuffers(3, fboBuffs);
缓冲区绘制:
// Direct drawing to the window
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDrawBuffers(1, windowBuff);
glViewport(0, 0, screenWidth, screenHeight);
// Source buffer reads from the framebuffer object
glBindFramebuffer(GL_READ_FRAMEBUFFER, fboName);
// Copy greyscale output to the left half of the screen
glReadBuffer(GL_COLOR_ATTACHMENT1);
glBlitFramebuffer(0, 0, screenWidth/2, screenHeight,
0, 0, screenWidth/2, screenHeight,
GL_COLOR_BUFFER_BIT, GL_NEAREST );//在帧缓冲区中复制数据,前面8个坐标给出了源和目的的左下角,右上角坐标
// Copy the luminance adjusted color to the right half of the screen
glReadBuffer(GL_COLOR_ATTACHMENT2);
glBlitFramebuffer(screenWidth/2, 0, screenWidth, screenHeight,
screenWidth/2, 0, screenWidth, screenHeight,
GL_COLOR_BUFFER_BIT, GL_NEAREST );
// Scale the unaltered image to the upper right of the screen
glReadBuffer(GL_COLOR_ATTACHMENT0);
glBlitFramebuffer(0, 0, screenWidth, screenHeight,
(int)(screenWidth *(0.8)), (int)(screenHeight*(0.8)),
screenWidth, screenHeight,
GL_COLOR_BUFFER_BIT, GL_LINEAR );
glBindTexture(GL_TEXTURE_2D, 0);
(6)缓冲区的完整性检查
GLenum fboStatus = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
(7)渲染到纹理
我们可以将一个纹理直接绑定到一个FBO绑定点上。
void glFramebufferTexture1D(GLenum target, GLenum attachment,
GLenum textarget, GLuint texture, GLint level);
void glFramebufferTexture2D(GLenum target, GLenum attachment,
GLenum textarget, GLuint texture, GLint level);
void glFramebufferTexture3D(GLenum target, GLenum attachment,
GLenum textarget, GLuint texture, GLint level,
GLint layer);