十四、OpenGL渲染管线
OpenGL渲染管线包含一系列有序地处理数据的阶段。两种图形数据即基于顶点的数据和基于像素的数据,在渲染管线中处理并统一输出到帧缓冲区。注意,OpenGL可以将处理后的数据重新发送到你的程序中(注意下图中的灰色线条)。
显示列表(Display List)
显示列表是一组已经被存储或编译过的的命令。所有的数据,几何数据和像素数据都可以被存储在显示列表中。它可以提升性能是由于所有的命令和数据都缓存在显示列表中。当OpenGL程序需要通过网络执行时,通过显示列表你可以减少数据在网络之间传输的次数。由于显示列表是服务器端的状态并且驻留在服务器端,客户端只需要将命令和数据发送一次到服务器端的显示列表中。
顶点操作(Vertex Operation)
每个顶点和法向量坐标需要经过GL_MODELVIEW(模型视图矩阵,从物体坐标系到人眼坐标系)矩阵变换。同样地,如果光照状态开启,作用在每个顶点上的光照计算是使用变换后的顶点和法向量数据。光照计算会更新每个顶点的颜色。
图元装配(Primitive Assembly)
顶点操作之后,图元(点,线,三角形)再次经过投影变换,然后经过裁剪平面裁剪,从人眼坐标系变换到裁剪坐标系。经过上述操作之后,再次经过透视除法和视口变换从而将三维的场景映射到窗口的区域坐标中。图元装配中最后要做的一件事就是如果裁剪状态开启则进行裁剪。
像素转换操作(Pixel Transfer Operation)
将像素数据从客户端内存中读取进来以后,数据会经过缩放,映射等一系列操作。这些操作叫做像素转换操作。转换后的数据被存储在纹理中或直接光栅化到片元中。
纹理内存(Texture Memory)
纹理图片被加载到纹理内存中以便应用到几何对象上。
光栅化(Raterization)
光栅化将几何数据和像素数据转换到片元中。片元是一种可以包含颜色,深度值,线宽,点大小和抗锯齿计算(GL_POINT_SMOOTH, GL_LINE_SMOOTH, GL_POLYGON_SMOOTH)的矩形数组。如果明暗模式是GL_FILL,那么多边形内部的像素将会在这个阶段被填充。每一个片元对于帧缓冲区中的一个像素。
片元操作(Fragment Operation)
这是将片元转换成要传输到帧缓冲区中的像素的最后一步。首先进行纹素生成;纹理元素通过纹理内存生成并且被应用到每一个片元。然后进行雾计算。接下来是一系列有序的片元测试操作:裁剪测试->alpha测试->模板测试->深度测试。
最后,经过混合,抖动,逻辑操作,遮挡这些操作实际的纹理数据被存储到帧缓冲区中。
反馈(Feedback)
OpenGL能通过glGet*()和glIsEnabled()命令返回大多数当前的状态和信息。除此之外,可以使用glReadPixels()函数从帧缓冲区中读取一个长方形区域的像素数据,使用glRenderMode(GL_FEEDBACK)函数得到变换后的顶点数据。glCopyPixels不会返回像素数据到指定的系统内存中,而是将他们复制到另一个缓冲区中,如将前台缓冲区中的像素数据复制到后台的缓冲区中。
十五、CAEAGLLayer渲染背景颜色
CAEAGLLayer渲染实现流程如下:
1.设置图层
2.设置上下文
3.清空缓冲区
4.设置renderBuffer;
5.设置frameBuffer
6.绘制
前五个步骤都是固定写法,重点在于第六个步骤,绘制。
下面通过一个简单的渲染背景颜色来理解整个渲染实现流程:
0.准备工作:
导入opengl es头文件,并在自定义的View中添加以下属性。
#import <OpenGLES/ES2/gl.h>
@property(nonatomic,strong)CAEAGLLayer *myEagLayer; //专门用来做opengl渲染的目标
@property(nonatomic,strong)EAGLContext *myContext; //上下文
@property(nonatomic,assign)GLuint myColorRenderBuffer;
@property(nonatomic,assign)GLuint myColorFrameBuffer;
@property(nonatomic,assign)GLuint myProgram; //把shader语言转化出来
@property(nonatomic,assign)GLuint myVertices; //顶点数据
整个opengl es渲染都在myEagLayer层上进行,CAEAGLLayer继承自CALayer。myContext是openGL ES是渲染上下文,因为openGL ES没有窗口的概念,所以的绘制都是在上下文上进行,类似CoreGraphics。后面的代码会建立上下文与layer的绑定关系。
myColorRenderBuffer和myColorFrameBuffer是渲染缓冲区与帧缓冲区的标记。
myProgram是最终的渲染程序标记。
myVertices是定点数据标记。
1. 设置图层
//1.设置图层
-(void)setupLayer
{
self.myEagLayer = (CAEAGLLayer *)self.layer;
[self setContentScaleFactor:[[UIScreen mainScreen]scale]];//设置屏幕的缩放
//CALayer默认是透明的,必须将它设置为不透明才能其可见
self.myEagLayer.opaque = YES;
self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];//kEAGLDrawablePropertyRetainedBacking表示绘图表面显示后,是否保留其内容。
}
//将View的默认layer改成CAEAGLLayer
+ (Class)layerClass {
return [CAEAGLLayer class];
}
2.设置上下文
//2.设置上下文
-(void)setupContext
{
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;//指定2的版本
EAGLContext *context = [[EAGLContext alloc]initWithAPI:api];
if (!context) {
NSLog(@"Create Context Failed");
return;
}
//设置为当前上下文
if (![EAGLContext setCurrentContext:context]) {
NSLog(@"Set Current Context Failed");
return;
}
self.myContext = context;
}
3.清空缓冲区
buffer分为frame buffer 和 render buffer2个大类。其中frame buffer 相当于render buffer的管理者。frame buffer object即称FBO,常用于离屏渲染缓存等。render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。
//3.清空缓存区
-(void)deletBuffer
{
glDeleteBuffers(1, &_myColorRenderBuffer);
_myColorRenderBuffer = 0;
glDeleteBuffers(1, &_myColorFrameBuffer);
_myColorFrameBuffer = 0;
}
4.设置渲染缓冲区
//4.设置renderBuffer
-(void)setupRenderBuffer
{
//1.定义一个缓存区
GLuint buffer;
//2.申请一个缓存区标志
glGenRenderbuffers(1, &buffer);
//3.
self.myColorRenderBuffer = buffer;
//4.将标识符绑定到GL_RENDERBUFFER
glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
//frame buffer仅仅是管理者,不需要分配空间;render buffer的存储空间的分配,对于不同的render buffer,使用不同的API进行分配,而只有分配空间的时候,render buffer句柄才确定其类型
//为color renderBuffer 分配空间
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
5.设置帧缓冲区
//5.设置frameBuffer
-(void)setupFrameBuffer
{
//1.定义一个缓存区标记
GLuint buffer;
//2.申请一个缓存区标志
glGenFramebuffers(1, &buffer);
//3.
self.myColorFrameBuffer = buffer;
//4.设置当前的framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
//5.将_myColorRenderBuffer 装配到GL_COLOR_ATTACHMENT0 附着点上
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
//接下来,可以调用OpenGL ES进行绘制处理,最后则需要在EGALContext的OC方法进行最终的渲染绘制。这里渲染的color buffer,这个方法会将buffer渲染到CALayer上。- (BOOL)presentRenderbuffer:(NSUInteger)target;
}
6.渲染
//6.绘制
-(void)render
{
//清掉屏幕上的颜色
glClearColor(0.5, 0, 0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
CGFloat scale = [UIScreen mainScreen].scale;
//视口
glViewport(self.frame.origin.x*scale, self.frame.origin.y*scale, self.frame.size.width*scale, self.frame.size.height*scale);
//获取顶点着色器和片元着色器
NSString* vertFile = [[NSBundle mainBundle]pathForResource:@"shaderv" ofType:@"glsl"];
NSString* fragFile = [[NSBundle mainBundle]pathForResource:@"shaderf" ofType:@"glsl"];
if (self.myProgram) {
glDeleteProgram(self.myProgram);
self.myProgram = 0;
}
//加载程序到myProgram中
self.myProgram = [self loadShader:vertFile frag:fragFile];
//链接
glLinkProgram(self.myProgram);
GLint linkSuccess;
//获取链接状态
glGetProgramiv(self.myProgram, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLchar message[256];
glGetProgramInfoLog(self.myProgram, sizeof(message), 0, &message[0]);
NSString* messageString = [NSString stringWithUTF8String:message];
NSLog(@"glLinkProgram error:%@",messageString);
return;
} else {
//使用程序对象program作为当前渲染状态的一部分
glUseProgram(self.myProgram);
}
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
}
#pragma mark -- Shader的导入
-(GLuint)loadShader:(NSString *)vert frag:(NSString *)frag
{
GLuint vertShader,fragShader;
GLuint program = glCreateProgram(); //创建一个临时的program
[self compileShader:&vertShader type:GL_VERTEX_SHADER file:vert];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
//创建程序
glAttachShader(program, vertShader);
glAttachShader(program, fragShader);
//释放shader
glDeleteShader(vertShader);
glDeleteShader(fragShader);
return program;
}
//编译shader
-(void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
{
NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar* source = (GLchar*)[content UTF8String];
*shader = glCreateShader(type); //创建一个shader
glShaderSource(*shader, 1, &source, NULL); //设置源头
glCompileShader(*shader); //编译shader
}
在可编程管线中,定点着色器和片元着色器必须由开发者实现,否者无法正常工作,而曲面着色器几何着色器则是可选的。因此,这个绘制背景色的简单例子中也需要实现两个着色器。
着色器的实现是一段使用GLSL语言写的代码函数,因此需要经过以下流程(着色器类似静态库):
1.创建程序
2.编译着色器
3.将创建好的着色器加入到程序中
4.链接程序
5.运行程序
6.开始渲染(显示渲染缓冲区)
十六、GLKit渲染背景颜色
GLKit渲染实现流程如下:
1.创建GLKView
GLKView * view = [[GLKView alloc]initWithFrame:self.view.bounds];
view.delegate = self;
[self.view addSubview:view];
view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
2.设置上下文
context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
view.context = context;
[EAGLContext setCurrentContext:context];
//开启深度测试 让离得近的物体可以遮挡远的物体
glEnable(GL_DEPTH_TEST);
3.在GLKit代理方法中实现绘制
-(void)glkView:(GLKView *)view drawInRect:(CGRect)rect{
glClearColor(0.5, 0, 0, 1);
//清除缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
十七、openGL顶点变换
在OpenGL渲染管线中几何数据在光栅化处理之前会先经过顶点操作和图元装配。
物体坐标系(Object Coordinates)
这是物体的局部坐标系,并且表示物体未经过任何变换前的初始位置和朝向。为了变换物体,使用glRotatef(),glTranslatef(),glScalef()等函数。
局部坐标系是物体内部的坐标系,他描述的是这个物体内部各个组成部分之间的相互关系,每个物体都会有一个他自己的局部坐标系。例如:一个正方形我们可以使用四个顶点来定义,我们约定,该正方形的局部坐标系的原点在正方向中心,那么正方形的四个顶点分别为[0.5, 0.5],[-0.5, 0.5],[-0.5, -0.5],[0.5, -0.5](逆时针方向)。如果有第二个相同的但是在不同位置的正方形,那么四个顶点的局部坐标也完全相同。不同的在于世界坐标不同。
世界坐标系(World coordinates)
描述所有物体的顶点之间的相对位置。
模型矩阵:将物体的坐标从局部坐标系转换到世界坐标系。两个正方形的四个顶点的局部坐标完全一样,但是模型矩阵不一样,导致在世界坐标系中的位置不一样。如果Cube0在原点,那么Cube0的模型矩阵为E,Cube1的中心在X轴的1.0位置,那么就相当于将Cube0往X轴方向平移1.0距离,那个这个平移变换就是Cube1的模型矩阵,同理旋转缩放。
人眼/摄像机坐标系(Eye/Camera Coordinates)
以摄像机为原点,描述世界中所有物体的相对位置。
这是将GL_MODELVIEW(模型视图矩阵)与物体坐标相乘产生的。在OpenGL中物体从物体自身的坐标变换到人眼坐标使用GL_MODELVIEW矩阵。GL_MODELVIEW矩阵是将模型矩阵和视图矩阵相乘的结果。模型变换是将物体坐标变换到世界坐标,视图变换是将世界坐标变换到人眼坐标。
观察矩阵:将世界中的物体坐标转换到摄像机坐标系,用以模拟摄像机在世界的不同位置、以不同角度、不同视角观察世界。这里需要注意的是,摄像机坐标系的Z轴正方向对应的是摄像机屁股对应的方向,而不是镜头对应的方向。这样定义的好处是:Opengl屏幕坐标系的Z轴向屏幕外,当摄像器的Z轴和屏幕坐标系的Z轴方向也一样时便于计算,否则的话,转换过程都需要加上Z轴翻转,但是摄像机又需要向着屏幕内的物体,这样就导致了摄像机镜头向着屏幕里(Z轴负方向),摄像机屁股向着屏幕外(Z轴正方向)。
裁剪坐标(Clip Coordinates)
将摄像机坐标系中的物体投影到裁剪空间,也即将3D投影到2D的过程。
将GL_PROJECTTION与人眼坐标相乘就得到了裁剪坐标。GL_PROJECTION(投影矩阵)定义了视景体;几何数据怎么被投影到屏幕中(透视投影或正投影)。它被称为裁剪坐标是因为变换后的顶点坐标(x,y,z)经过了裁剪(通过与-w和+w的比较)。
裁剪坐标系:描述最终显示到屏幕上的物体的相对位置,但不是距离。该坐标系的XY轴与屏幕坐标系的XY轴在同一个平面上,不同的地方在于,当我们在OpenGL中设置了ViewPort后,ViewPort空间即可认为是屏幕空间,它的原点在ViewPort的左下角。这时我们需要经过平移、旋转、缩放等转换(Z轴不变)得到屏幕坐标系内的顶点坐标。
归一化的设备坐标(Normalized Device Coordinates(NDC))
将裁剪坐标与w相除就得到归一化的设备坐标。它被称为透视除法。它更像窗口坐标,但是还没有被平移和缩放到匹配窗口。现在3个坐标轴上变量的取值被归一化到-1到1之间。
窗口坐标(Window Coordinates(Screen Coordinates))
这是将归一化的设备坐标应用到视口(viewPort)变换而产生的。归一化的设备坐标经过缩放了平移以匹配窗口。窗口坐标最后被传递给OpenGL渲染管线中光栅化程序生成片元。glViewport()命令被用来定义最后渲染出来的图像将要映射到的长方形区域。glDepthRange()命令被用来定义窗口坐标的z值的范围。
十八、Render Buffer(渲染缓冲区)和Frame Buffer(帧缓冲区)
Framebuffer对象是渲染命令的目的(指渲染指令最终会转换到framebuffer中)。 当您创建一个framebuffer对象时,您可以对其存储的颜色,深度和模板数据进行精确的控制。 您可以通过将图像附加到framebuffer来提供此存储空间。 最常见的图像附件是一个renderbuffer对象。 您还可以将OpenGL ES纹理附加到帧缓冲区的颜色附加点,这意味着任何绘图命令都将呈现到纹理中。 之后,纹理可以作为未来渲染命令的输入。 您还可以在单个渲染上下文中创建多个帧缓冲区对象。以便在多个帧缓冲区之间共享相同的渲染管道和OpenGL ES资源。
所有这些方法都需要手动创建framebuffer和renderbuffer对象来存储来自OpenGL ES上下文的渲染结果,以及编写其他代码以将其内容呈现给屏幕。
所有的OpenGL程序都要使用帧缓冲,我们目前所做的所有操作都是在默认帧缓冲的渲染缓冲上进行的。
在OpenGL渲染管线中,几何数据和纹理经过了几次变换和测试最终作为二维像素被渲染到屏幕中。OpenGL渲染管线中的目的地是帧缓冲区。帧缓冲区是被OpenGL使用的二维数组的集合,包括颜色缓冲区,深度缓冲区,模板缓冲区和累积缓冲区。默认情况下OpenGL使用完全被窗口创建和管理的帧缓冲区作为渲染的目的地。默认的帧缓冲区被叫做”窗口系统提供的“帧缓冲区。
OpenGL的扩展, GL_ARB_framebuffer_object提供了接口用来创建不渲染的帧缓冲区对象(FBO)。为了和”窗口系统提供的“帧缓冲区相区分开来这个帧缓冲区叫做”应用程序创建的“帧缓冲区。通过使用帧缓冲区对象(FBO),一个应用程序可以重定向渲染结果到”应用程序创建的“帧缓冲区对象(FBO)而不是传统的”窗口系统提供的“帧缓冲区中。这个过程是完全被OpenGL控制的。
和”窗口系统提供的“帧缓冲区类似,一个FBO也包含颜色缓冲区,深度缓冲区,模板缓冲区。(注意FBO中没有累积缓冲区)。FBO中的这些逻辑上的缓冲区被叫做“附加到帧缓冲区中镜像“(framebuffer-attachable images),这些二维的像素数据可以被附加到帧缓冲区对象中。
有两种“附加到帧缓冲区中”镜像:纹理镜像和渲染缓冲区镜像。如果一个纹理对象的镜像被附加到帧缓冲区,OpenGL执行”渲染到纹理“的操作。如果一个渲染缓冲区对象的镜像被附加到帧缓冲区中,那么OpenGL执行”离屏渲染“。
顺便说一下,渲染缓冲区对象是在GL_ARB_framebuffer_object扩展中定义的一种新的存储对象类型。它被用来作为在渲染过程中简单二维图片的渲染目的地。
下面这幅图显示了帧缓冲区对象(FBO),纹理对象(Texture Object),渲染缓冲区对象(Renderbuffer Object)之间的连接关系。多个纹理对象和渲染缓冲区对象可以通过附加点被附加到帧缓冲区对象中。
一个FBO中有多个颜色附加点(GL_COLOR_ATTACHMENT0,..., GL_COLOR_ATTACHMENTn),一个深度附加点(GL_DEPTH_ATTACHMENT),一个模板附加点(DL_STENCIL_ATTACHMENT)。颜色附加点的数目是依赖于显卡的,但是每个FBO对象中至少含有一个颜色附加点。你可以使用GL_MAX_COLOR_ATTACHMENTS查询显卡支持的颜色顶点的最大数。一个FBO中有多个颜色附加点的原因是为了同时将颜色缓冲区渲染到多个目的地。
帧缓冲区对象(FBO)提供了一种高效的切换机制:从帧缓冲区对象中分离之前附加的对象,或者附加一个新对象到帧缓冲区对象中。切换帧缓冲区对象中的附加对象比切换FBO快很多。FBO提供了glFramebufferTexture2D()来切换纹理对象,提供glFramebufferRenderbuffer()来切换渲染缓冲区对象。
创建一个帧缓冲
和OpenGL中的其它对象一样,我们会使用一个叫做glGenFramebuffers的函数来创建一个帧缓冲对象(Framebuffer Object, FBO):
unsigned int fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glGenFramebuffers()需要两个参数:第一个参数标示要创建的帧缓冲区的数目,第二个参数表示存放生成的帧缓冲区ID的GLuint类型的变量或数组的地址。它返回为使用的帧缓冲区对象的ID。ID为0标示默认的帧缓冲区,就是”窗口系统提供的“帧缓冲区。
当FBO创建以后,在使用它之前需要先绑定它。
glBindFramebuffer第一个参数target需要是GL_FRAMEBUFFER,第二个参数是帧缓冲区对象的ID。当一个FBO绑定以后,所有的OpenGL操作将会作用在这个绑定的帧缓冲区对象上。ID为0的帧缓冲区对象是”窗口系统提供的“帧缓冲区。因此为了解除当前正在使用的帧缓冲区(FBO),使用glBindFramebuffer()绑定到0。
与其他缓冲区不同的是,我们现在还不能使用我们的帧缓冲,因为它还不完整(Complete),一个完整的帧缓冲需要满足以下的条件:
1.附加至少一个缓冲(颜色、深度或模板缓冲)。
2.至少有一个颜色附件(Attachment)。
3.所有的附件都必须是完整的(保留了内存)。
4.每个缓冲都应该有相同的样本数。
之后所有的渲染操作将会渲染到当前绑定帧缓冲的附件中。由于我们的帧缓冲不是默认帧缓冲,渲染指令将不会对窗口的视觉输出有任何影响。出于这个原因,渲染到一个不同的帧缓冲被叫做离屏渲染(Off-screen Rendering)。要保证所有的渲染操作在主窗口中有视觉效果,我们需要再次激活默认帧缓冲,将它绑定到0:
glBindFramebuffer(GL_FRAMEBUFFER, 0);
在完成所有的帧缓冲操作之后,不要忘记删除这个帧缓冲对象:
glDeleteFramebuffers(1, &fbo);
在完整性检查执行之前,我们需要给帧缓冲附加一个附件。附件是一个内存位置,它能够作为帧缓冲的一个缓冲,可以将它想象为一个图像。当创建一个附件的时候我们有两个选项:纹理或渲染缓冲对象(Renderbuffer Object)。
渲染缓冲区对象
另外,渲染缓冲区对象(renderbuffer object)是在离屏渲染(offscreen rendering)中新介绍到的。它允许将一个场景直接渲染到渲染缓冲区对象而不是纹理对象中。渲染缓冲区对象是一个包含可渲染的内部格式镜像的数据存储区。它被用来存储OpenGL中没有相关联的的纹理格式的逻辑缓冲区,如模板缓冲区和深度缓冲区。
void glGenRenderbuffers(GLsizei n, GLuint* ids)
void glDeleteRenderbuffers(GLsizei n, const Gluint* ids)
当一个渲染缓冲区创建以后,它会返回一个非零的正数。0为OpenGL保存。
void glRenderbufferStorage(GLenum target,
GLenum internalFormat,
GLsizei width,
GLsizei height)
当一个渲染缓冲区对象被创建以后,它不包含任何的数据存储区,因此我们必须为它分配空间。这可以使用glRenderbufferStorage()来实现。第一个参数必须是GL_RENDERBUFFER。第二个参数可以是渲染颜色(GL_RGB,GL_RGBA,等),渲染深度(GL_DEPTH_COMPONENT),或者渲染模板格式(GL_STENCIL_INDEX)。width和height是渲染缓冲区镜像的尺寸(以像素为单位)。
附加镜像到FBO
FBO自身并没有缓冲区,而是由我们将”可以附加到帧缓冲区“的镜像(framebuffer-attachable iamges,纹理对象或渲染缓冲区对象)附加到FBO中。这种机制允许FBO快速切换(detach and attach,分离和附加)在FBO中的”可以附加到帧缓冲区“的镜像。切换这种绑定的镜像比在FBO中更加快速。并且,减少了没必要的数据拷贝和内存消耗。例如,一个纹理对象可以附加到多个帧缓冲区对象中,它的缓冲区可以被多个FBO共享。
附加二维纹理镜像到FOB中
glFramebufferTexture2D(GLenum target,
GLenum attachmentPoint,
GLenum textureTarget,
GLuint textureId,
GLint level)
glFramebufferTexture2D()将一个二维纹理镜像附加到FBO中。第一个参数必须是GL_FRAMEBUFFER。第二个参数是连接纹理镜像的附加点,一个FBO中有多个颜色附加点(GL_COLOR_ATTACHMENT0, ...,GL_COLOR_ATTACHMENTn),一个深度附加点(GL_DEPTH_ATTACHMENT),和一个模板附加点(GL_STENCIL_ATTACHMENT)。第三个参数,"texture Target"大多数情况下为GL_TEXTURE_2D。第四个参数是纹理对象的标示符。最后一个参数附加纹理的映射级别。
如果textureId的值为0,那么纹理镜像会从PBO中分离。如果一个纹理对象在它还附加到FBO中时被删除了,那么这个纹理对象会自动从当前绑定的FBO中分离。然而,如果它被附加到多个FBO并且被删除了,那么将只会从当前被绑定的FBO中删除,而不会从未绑定的FBO中删除。
附加渲染缓冲区镜像到FBO中
void glFramebufferRenderbuffer(GLenum target,
GLenum attachmentPoint,
GLenum renderbufferTarget,
GLuint renderbufferId)
可以使用glFramebufferRenderbuffer()将一个渲染缓冲区镜像附加到FBO中。第一个参数和第二个参数和glFramebufferTexture2D()一样。第三个参数必须是GL_RENDERBUFFER。最后一个参数是渲染缓冲区对象的标示。
如果renderbufferId参数被设置成0,那么渲染缓冲区对象将会从FBO中的附加点分离。如果一个渲染缓冲被附加到FBO时被删除了,它会自动从当前被绑定的FBO中分离开来。然而,它不会从其他未被绑定的FBO中分离出来。
检查FBO的状态
当一个可附加的镜像(纹理对象或渲染缓冲区对象)被附加到FBO中以后并且在FBO执行操作之前,你必须使用glCheckFramebufferStatus()来检查FBO的状态是否完整。如果FOB不是完整的,那么任何绘制或读取的命令(glBegin,glCopyTexImage2D()等)都会失败。
GLenum glCheckFramebufferStatus(GLenum target)
glCheckFramebufferStatus()检查附加到当前被绑定的FBO的镜像和帧缓冲的参数。并且这个函数不能在glBegin()和glEnd()之间。target参数必须是GL_FRAMEBUFFER。检查完FBO后会返回一个非零的值。如果所有的条件和规则都满足,会返回GL_FRAMEBUFFER_COMPLETE。否则,它返回一个有关的错误值,这个错误值会告诉违反了那条规则。
十九、openGL顶点数组
不像在立即模式(在glBegin()和glEnd()对之间)中指定单独的顶点数据 ,你可以存储顶点数据(顶点坐标,法向量,纹理坐标和颜色信息)在一系列数组中。你也可以利用数组的索引只取数组中的部分元素来绘制几何图元。
看一下下面的代码中使用立即模式绘制一个立方体的例子。
glBegin(GL_TRIANGLES); // 使用12个三角形绘制立方体
// front face =================
glVertex3fv(v0); // v0-v1-v2
glVertex3fv(v1);
glVertex3fv(v2);
glVertex3fv(v2); // v2-v3-v0
glVertex3fv(v3);
glVertex3fv(v0);
// right face =================
glVertex3fv(v0); // v0-v3-v4
glVertex3fv(v3);
glVertex3fv(v4);
glVertex3fv(v4); // v4-v5-v0
glVertex3fv(v5);
glVertex3fv(v0);
// top face ===================
glVertex3fv(v0); // v0-v5-v6
glVertex3fv(v5);
glVertex3fv(v6);
glVertex3fv(v6); // v6-v1-v0
glVertex3fv(v1);
glVertex3fv(v0);
... // 绘制另外3个面
glEnd();
使用顶点数组减少了函数调用的次数和共享顶点的冗余使用。因此,你可以提高渲染的性能。现在将解释使用顶点数组的3个不同的OpenGL函数:glDrawArrays(),glDrawElements()和glDrawRangeElements()。虽然更好的方式是使用顶点缓冲区对象(VBO)或显示列表。
初始化
OpenGL提供了glEnableClientState()和glDisableClientState()来激活或禁用6个不同类型的数组,还有6个函来操作这6个数组,因此,OpenGL可以在你的应用程序中访问这6个数组。
glVertexPointer():指向顶点坐标数组
glNormalPointer():指向法向量数组
glColorPointer():指向RGB颜色数组
glIndexPointer():指向索引颜色数组
glTexCoordPointer():指向纹理坐标数组
glEdgeFlagPointer():指向临界标示数组
每一个指向数组的函数需要不同的参数。具体需要哪些参数可以参考OpenGL API文档。临界标示数组用来标记顶点是否在边界上。所以唯一一中临界标示为真并且边界可见的情况是glPolygonMode()的值被设置成GL_LINE。
glVertexPointer(GLint size, GLenum type, GLsizei stride, const GLvoid* pointer)
1.size:顶点的维数,2表示2维的点,3表示3维的点。
2.type:GL_FLOAT, GL_SHORT, GL_INT 或 GL_DOUBLE。
3.stride:下一个顶点的偏移量。
4.pointer:指向顶点数组的指针。
glNormalPointer(GLenum type, GLsizei stride, const GLvoid* pointer)
1.type:GL_FLOAT, GL_SHORT, GL_INT 或 GL_DOUBLE。
2.stride:下一个法向量的偏移量。
3.pointer:指向顶点数组的指针。
注意顶点数组位于你的应用程序之中(系统内存),它在客户机这端。位于服务器端的OpenGL访问它们。这是为什么对于顶点数组会有与众不同的访问方式:使用glEnableClientState()和glDisableClientState()而不是使用glEnable()和glDisable()。
在OpenGL系统中有两个部分,客户端负责发送OpenGL命令,服务端负责接收OpenGL命令并执行相应操作。一般来说客户端是cpu、内存等硬件,以及用户编写的OpenGL程序;服务端是OpenGL驱动程序,显示设备。服务端的状态用glEnable / glDisable,客户端的状态则用glEnableClientState / glDisableClientState。服务端不需要知道顶点数组是否开启,只需要接收顶点数据,所以顶点数组的状态放在客户端比较合理。
glDrawArrays()
glDrawArrays()以连续地无跳跃的方式从顶点数组中读取数据。因为glDrawArrays不允许跳跃地访问顶点数组,你依然需要重复定义顶点。
glDrawArrays()需要三个参数。第一个参数是图元类型,第二个参数是第一个元素在数组中的偏移量,第三个参数是传递给OpenGL渲染的顶点数目。对于上面绘制立方体的例子而言,第一个参数是GL_TRIANGLES,第二个参数是0,表示是从数组中的首个元素开始的,最后一个参数是36:一个立方体有6个面,每个面需要6个顶点来绘制两个三角形,6*6=36。
GLfloat vertices[] = {...}; // 36 个顶点的坐标
...
// 激活顶点数组
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, vertices);
// 绘制一个立方体
glDrawArrays(GL_TRIANGLES, 0, 36);
// 绘制完成后禁用顶点数组
glDisableClientState(GL_VERTEX_ARRAY);
现在顶点数组的大小是8,和立方体中的顶点数目相同。
注意索引数组的类型是GLubyte而不是GLuint或glushort。它应该是能容纳最多顶点数目的占用空间最小的数据类型,否则,由于索引数组的容量将会导致性能下降。由于顶点数组包含8个顶点,GLubyte能容纳所有的索引。
glDrawRangeElements()
和glDrawElements()比较类似,glDrawRangeElements()也可以在数组中跳跃访问。然而,glDrawRangeElements()还有两个额外的参数(开始和结束位置的索引)来指定顶点的范围。通过添加这个范围限制,OpenGL可以只获得访问有限范围的顶点的来优先渲染,这样可能提高性能。
二十、显示列表
显示列表是一组被存储或编译的用来以后执行的OpenGL命令的集合。当一个显示列表被创建以后,所有的顶点数据和像素数据被复制到位于服务器端的显示列表内存中。这个过程只进行一次。当显示列表准备好(被编译完成)后,你可以重复使用它而不需要在每帧中重复地传输这些数据。显示列表是最快的一种绘制静态数据的方式,因为顶点数据和OpenGL命令被缓冲在服务器端的显示列表中,这样减少了从客户端到服务器段的数据传输。这意味着减少了执行实际的数据传输的CPU周期。
显示列表另一个重要的功能是显示列表可以被多个客户端所共享,因为它是服务器端的状态。
为了最佳的性能,将矩阵变换,光照,材质计算尽可能地放在显示列表中,这样OpenGL将只会在显示列表创建时执行一次这些高昂开销的计算,并将最终的结果存储在显示列表中。
然而,显示列表有一个缺点。当一个显示列表被编译后,它不能被改变。如果你需要频繁地改变数据或需要动态的数据,使用顶点数组或顶点缓冲区对象。顶点缓冲区对象可以同时处理静态和动态的数据。
注意并不是所有的OpenGL命令都可以存储在显示列表中。由于显示列表是服务器端的状态,所有与客户端状态相关的命令不能被放置在显示列表中。例如:glFlush(),glFinish(),glRenderMode(),glEnableClientState(),glVertexPointer()等这些函数不能放在显示列表中。并且有返回值的OpenGL命令也不能放在显示列表中,因为这些返回值需要被返回到客户端,而不是显示列表中,例如:glIsEnabled(),geGet*(),glReadPixels(),glFeedbackBuffer()等。如果这些命令存储在显示列表中,它们将会被立即执行。
使用显示列表非常简单。首先是使用glGenLists()函数创建一个或多个显示列表对象,它的参数是需要创建的显示列表的数目,它返回连续的显示列表块中的第一个元素的索引。例如,glGenLists()函数如果返回1并且你创建的显示列表的数目为3的话,那么索引1,2,3是可用的。如果OpenGL创建显示列表失败,它将会返回0。如果你不再使用显示列表,可以使用glDeleteLists()删除它们。
第二步,你需要在glNewList()和glEndList()块之间存储OpenGL命令到显示列表中来优先渲染。这个过程叫做编译。glNewList()需要两个参数,第一个参数是glGenLists()函数返回的索引值,第二个参数是指定模式:是只编译还是编译并执行,GL_COMPLILE,GL_COMPLILE_AND_EXECUTE。
到现在为止,准备工作已经做好了。只需要在每帧中使用glCallList()函数或glCallLists()函数来执行显示列表就可以了。
看一下下面这个简单的显示列表的例子:
// 创建一个显示列表对象
GLuint index = glGenLists(1);
// 编译显示列表, 存储一个三角形
glNewList(index, GL_COMPILE);
glBegin(GL_TRIANGLES);
glVertex3fv(v0);
glVertex3fv(v1);
glVertex3fv(v2);
glEnd();
glEndList();
...
// 绘制显示列表
glCallList(index);
...
// 如果不再需要这个显示列表,删除它
glDeleteLists(index, 1);
注意只有在glNewLists()和glEndLists()之间的OpenGL命令才会被记录到显示列表中一次,并在每次调用glCallList()时缓存一次。
为了使用glCallLists()执行多个显示列表,你需要做一个额外的工作。你需要使用一个数组来存放那些需要被渲染的显示列表的索引。换句话说,你可以选择渲染所有显示列表中的部分显示列表。glCallLists()需要3个参数:需要绘制的显示列表的个数,索引数组的数据类型,指向索引数组的指针。
注意你可以不需要指定显示列表的准确索引到数组中,你可以指定它们的偏移量,然后使用glListBase()设置基准的显示列表的位置。当glCallLists()被调用时,实际的索引值会通过将基准的位置与偏移量想加得到。下面的例子显示了如何使用glCallLists()以及一些说明:
GLuint index = glGenLists(10); // 创建10个显示列表
GLubyte lists[10]; // 最多允许渲染10个显示列表
glNewList(index, GL_COMPILE); // 编译第一个显示列表
...
glEndList();
...// 编译中间的显示列表
glNewList(index+9, GL_COMPILE); // 编译最后一个显示列表
...
glEndList();
...
// 只绘制部分显示列表 (第1个,第3个,第5个,第7个,第9个)
lists[0]=0; lists[1]=2; lists[2]=4; lists[3]=6; lists[4]=8;
glListBase(index); // 设置基准的位置
glCallLists(5, GL_UNSIGNED_BYTE, lists);
二十一、OpenGL顶点缓冲区对象(VBO)
GL_ARB_vertex_buffer_object扩展通过提供顶点数组和显示列表的优点并且避免它们的不足提高了OpenGL的性能。顶点缓冲区对象(vertex buffer object,VBO)允许顶点数组存储在位于服务器端的高性能的图形内存中,并且提升了数据传输的效率。如果缓冲区对象用来存储像素数据,那么它叫做像素缓冲区对象(PBO)。
使用顶点数据可以减少函数调用的次数和共享顶点的冗余使用,但是,顶点数组的缺点是顶点数组中的函数位于客户端并且数组中的数据在它每次被引用时都需要发送到服务器端一次。
相反,显示列表位于服务器端,因此它不需要忍受数据传输的开销。但是,当一个显示列表编译后,显示列表里的数据就不能够再修改了。
顶点缓冲区对象(VBO)在服务器端高性能的内存中为顶点属性创造了一个“缓冲区对象”,并且提供了一些函数来引用在顶点数组中使用的数组,如glVertexPointer(),glNormalPointer(),glTexCoordPointer()等。
顶点缓冲区对象(VBO)中的内存管理根据用户定义的"目标(target)"和“用法(usage)”模式,将缓冲区对象放置在内存中最合适的地方。因此,内存管理通过在3中不同的内存(系统内存,AGP,显卡内存)中协调能优化缓冲区。
1.显存:显卡内存;
2.系统内存:即常说的内存条;
3.AGP内存:显卡向系统内存借的内存,其中AGP(Accelebrate Graphic Port)为加速图形端口;
特点:
1.对于CPU来说,读写速度最快,当然是使用系统内存;
2.显卡使用显存,读写速度大于显卡使用AGP内存(速度一般,略小于显卡使用显存)和系统内存;
3.CPU读取AGP内存速度较慢,但写的速度不慢;
不像显示列表,通过将缓冲区映射到客户端的内存区域中顶点缓冲区中的数据可以被读取或者更新。
顶点缓冲区对象(VBO)的另外一个优点是缓冲区对象可以被多个客户端共享,像显示列表和纹理。由于顶点缓冲区对象(VBO)位于服务器端,多个客户端使用相关联的标示符可以访问相同的缓冲区。
创建VBO
创建VBO需要3步:
- 使用glGenBuffersARB()生成一个缓冲区对象;
- 使用glBindBufferARB()绑定一个缓冲区对象;
- 使用glBufferDataARB()将顶点数据复制到缓冲区对象中。
glGenBuffersARB()
glGenBuffersARB()创建一个缓冲区对象并且返回这个缓冲区对象的标示。它需要两个参数:第一个参数指示要创建的缓冲区对象的个数,第二个参数指示存放返回一个或多个缓冲区标示的GLuint类型变量或数组的地址。
void glGenBuffersARB(GLsizei n, GLuint* ids)
glBindBufferARB()
当缓冲区对象创建以后,在使用缓冲区对象之前我们需要将缓冲区对象的标示绑定。glBindBufferARB()需要两个参数:target和ID。
void glBindBufferARB(GLenum target, GLuint id)
target是告诉VBO这个缓冲区对象是用来存储顶点数组数据还是用来存储索引数组数据的:GL_ARRAY_BUFFER_ARB或 GL_ELEMENT_ARRAY_BUFFER_ARB 。任何顶点属性,如顶点坐标,纹理坐标,法向量,和颜色信息需要使用GL_ARRAY_BUFFER_ARB作为target。而像glDraw[Range]Elements()函数使用的索引数组则需要与GL_ELEMENT_ARRAY_BUFFER_ARB绑定。注意这个target标示帮助VBO决定顶点缓冲区的最佳位置,例如,有些系统将会将索引数组放置在AGP或系统内存中,而将顶点数组放置在显卡内存中。
当glBindBufferARB()被首次调用的时候,VBO使用大小为0的内存区初始化这个缓冲区并且设置了这个VBO的初始状态,例如usage和访问属性。
glBufferDataARB()
当缓冲区初始化以后你可以使用glBufferDataARB()将数组复制到缓冲区对象中。
void glBufferDataARB(GLenum target, GLsizei size, const void* data, GLenum usage)
第一个参数target可能是GL_ARRAY_BUFFER_ARB或GL_ELEMENT_ARRAY_BUFFER_ARB。size是将要传输的数据的字节数。第3个参数是指向数据源的指针,如果这个指针是NULL,那么VBO将会只保留指定大小的存储空间。最后一个参数usage是另一个VBO中的性能参数,指示这个缓冲区会被怎么使用,static(静态的),dynamic(动态的),stream(流),read(读),copy(复制),draw(绘制)。
GL_STATIC_DRAW_ARB
GL_STATIC_READ_ARB
GL_STATIC_COPY_ARB
GL_DYNAMIC_DRAW_ARB
GL_DYNAMIC_READ_ARB
GL_DYNAMIC_COPY_ARB
GL_STREAM_DRAW_ARB
GL_STREAM_READ_ARB
GL_STREAM_COPY_ARB
“static”意味着VBO中的数据不能被改变(指定一次使用多次),“dynamic”意味着数据将会频繁地改变(指定多次使用多次),“stream”意味着数据在每一帧中都会被改变(指定一次使用一次)。“draw”意味着数据被传输至GPU渲染(从应用程序到OpenGL),“read”意味着数据被客户端的应用程序所读取(从OpenGL到应用程序),“copy0”意味着既可以用来“draw”也可以用来“read”。
注意只有"draw"标示可以被VBO使用,“copy”和“read”标示对像素缓冲区对象(PBO)和帧缓冲区对象(FBO)才有意义。
VBO内存管理会根据usage的值为缓冲区对象选择最合适的内存位置,例如,GL_STATIC_DRAW_ARB 和GL_STREAM_DRAW_ARB将会选择显卡内存,GL_DYNAMIC_DRAW_ARB将会选择AGP内存。任何与READ相关的缓冲区既可以使用系统内存也可以是用AGP内存,因为数据应该很容易被访问到。
glDeleteBuffersARB()
void glDeleteBuffersARB(GLsizei n, const GLuint* ids)
你可以使用glDeleteBuffersARB()函数来删除一个或多个不再使用的VBO。缓冲区对象被删除掉后,它的内容会丢失掉。
下面的代码是一个为顶点坐标创建一个简单的顶点缓冲区对象(VBO)的例子。注意当你把所有的数据复制到VBO中之后你可以删除在你的应用程序中为顶点数组分配的内存。
GLuint vboId; // VBO的ID
GLfloat* vertices = new GLfloat[vCount*3]; // 创建顶点数组
...
// 生成一个新的顶点缓冲区对象并得到相关联的ID
glGenBuffersARB(1, &vboId);
// 绑定顶点缓冲区对象
glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId);
// 将数据复制到顶点缓冲区对象中
glBufferDataARB(GL_ARRAY_BUFFER_ARB, dataSize, vertices, GL_STATIC_DRAW_ARB);
// 将数据复制到顶点缓冲区对象中后就可以删除顶点数组了
delete [] vertices;
...
// 程序终止时删除顶点缓冲区对象
glDeleteBuffersARB(1, &vboId);
绘制VBO
因为VBO是基于顶点数组实现的,渲染VBO和使用顶点数组比较类似。唯一的不同点是指向顶点数据的指针现在变成了指向现在绑定的缓冲区对象的偏移量。因此,除了glBindBufferARB()之外绘制VBO不需要额外的API。
// 为顶点数组和索引数组绑定VBO
glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId1); // 为顶点数组
glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, vboId2); // 为索引数组
// 除了指针不同外其他和顶点数组操作一样
glEnableClientState(GL_VERTEX_ARRAY); // 激活顶点数组
glVertexPointer(3, GL_FLOAT, 0, 0); // 最后一个参数是偏移
// 使用索引数组的偏移值绘制6个面
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, 0);
glDisableClientState(GL_VERTEX_ARRAY); // 禁用顶点数组
// 绑定0,这样将返回到正常的指针操作
glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0);
glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
最后一行将缓冲区对象绑定到0将关闭VBO操作。这是一个使用VBO之外关闭VBO的好方法,这样正常的顶点数组操作将会被再次激活。
更新VBO
VBO相比于显示列表的优点是客户端可以读取和更改缓冲区对象中的数据,而显示列表不能。最简单的更新VBO中的数据的方法是使用glBufferDataARB()或glBufferSubDataARB()函数将数据一遍又一遍地复制到你所绑定的VBO中。对这种情况,你的应用程序需要一直有一个有效的顶点数组。这意味着你需要有顶点数组的两份拷贝:一份位于你的应用程序中,另一份位于VBO中。
另一个修改缓冲区对象的方式是将缓冲区对象映射到客户端内存中,然后客户端可以使用指向映射到缓冲区的指针更新数据。下面显示了如何将一个VBO映射到客户端内存中并怎样访问被映射的数据。
glMapBufferARB()
VBO使用glMapBufferARB()来将缓冲区对象映射到客户端内存中。
void* glMapBufferARB(GLenum target, GLenum access)
如果OpenGL能将缓冲区映射到客户端的内存中,glMapBufferARB()将返回指向缓冲区的指针,否则返回NULL。
第一个参数,target和上面的glBindBufferARB()一样,第二个参数access指定了对那些映射的数据的操作:读,写或者读写都可以。
GL_READ_ONLY_ARB
GL_WRITE_ONLY_ARB
GL_READ_WRITE_ARB
注意glMapBufferARB()将会导致一个同步的问题。如果GPU依然在顶点缓冲区中工作,那么glMapBufferARB()函数将会在GPU结束在指定的缓冲区的工作之后才返回。
为了避免等待,你可以首先使用一个空指针调用glBufferDataARB()函数,然后调用glMapBufferARB()。在这种情况下,之前的数据将会被舍弃,glMapBufferARB()将立即返回一个新分配区域的指针,即使GPU依然在之前的数据上工作。
然而,这种方法只有在你需要更新整个数据集的时候才有效,因为这样将会舍弃之前的数据。如果你只是希望改变部分数据或者只是希望读取数据,你最好不要舍弃之前的数据。
glUnmapBufferARB()
GLboolean glUnmapBufferARB(GLenum target)
修改完VBO的数据之后,必须解除掉缓冲区对象和客户端内存的映射。glUnmapBufferARB()成功时会返回GL_TRUE。当它返回GL_FALSE时VBO的内容会被破坏。破坏的原因是窗口分辨率的改变或者系统时间发生了。在这种情况下,数据需要被再次提交。
下面是一个使用映射的方法修改VBO的例子:
// 绑定并映射VBO
glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboId);
float* ptr = (float*)glMapBufferARB(GL_ARRAY_BUFFER_ARB, GL_WRITE_ONLY_ARB);
// 如果映射成功,则更新VBO
if(ptr)
{
updateMyVBO(ptr, ...); // 修改缓冲区数据
glUnmapBufferARB(GL_ARRAY_BUFFER_ARB); // 解除映射
}
// 使用新的VBO绘图
...
二十二、OpenGL像素缓冲区对象(PBO)
OpenGL中的ARB_pixel_buffer_object扩展和ARB_vertex_buffer_object扩展非常类似。这样非常简单地扩充 ARB_vertex_buffer_object是为了不仅能在缓冲区对象中存储顶点数据也能存储像素数据。这个存储像素数据的缓冲区对象叫做像素缓冲区(PBO)。ARB_pixel_buffer_object扩展借用了所有VBO的框架和API,并且添加了两个额外的"target"值。这个“target”值方便PBO的内存管理机制决定缓冲区对象在内存中的最佳位置,是将缓冲区对象放置在系统内存中,还是共享内存中,还是显卡内存中。这个“target”值也清晰地指明了PBO的使用范围: GL_PIXEL_PACK_BUFFER_ARB表示将像素数据“传入”到PBO,GL_PIXEL_UNPACK_BUFFER_ARB表示数据从PBO中“传出”。
PBO最大的优点是使用DMA(Direct Memory Access,直接内存访问)的方式快速地将数据传入到显卡或从显卡传出,它不需要等待CPU的周期。PBO的另外一个优点是可以使用异步地DMA方式传输数据。让我们来比较一下传统的纹理传输的方式和使用PBO的区别。如下图所示,第一幅图显示传统的方式从数据源(图片文件或视频流)中加载纹理数据。数据源首先被加载到系统内存中,然后使用glTexImage2D()冲系统内存中复制到OpenGL中的纹理对象中。这两个步骤(加载和复制)都是在CPU中进行的。
第二幅图片则正好相反,数据源能直接被加载到PBO中,而PBO是被OpenGL控制的。CPU也参与了加载数据源到PBO,但是不会将PBO中的纹理数据传输到纹理对象中。反而由GPU(OpenGL驱动器)管理将数据从PBO复制到纹理对象中。这意味着OpenGL进行了DMA方式的传输操作而不需要等待CPU的周期。除此之外,OpenGL甚至还可以使用异步地传输方式。因此glTexImage2D()会立即返回,CPU可以执行其他的事情而不需要等待像素数据传输完。
创建PBO
像之间提到过的那样,像素缓冲区对象(PBO)借用了顶点缓冲区对象(VBO)中的所有API。唯一的区别是为”target“表示增加了两个值:GL_PIXEL_PACK_BUFFER_ARB和GL_PIXEL_UNPACK_BUFFER_ARB。GL_PIXEL_PACK_BUFFER_ARB用来表示将数据从OpenGL中传输到你的应用程序中,GL_PIXEL_UNPACK_BUFFER_ARB表示将数据从应用程序中传输到OpenGL中。OpenGL使用这些标示来决定将PBO放置在最合适的地方,例如,显卡内存用来上传(unpack)纹理,系统内存用来冲帧缓冲区中读取(pack)数据。然而,这些标记都是单独使用的,OpenGL驱动将会为你指定合适的位置。
创建PBO需要3步:
- 使用glGenBuffersARB()创建一个缓冲区对象
- 使用glBindBufferARB()绑定一个缓冲区对象
- 使用glBufferDataARB()将像素数据复制到缓冲区对象中
如果在glBufferDataARB()中为数据源数组传入了NULL,那么PBO只会分配一个指定大小的空间。glBufferDataARB()函数的最后一个参数是另个用来指定PBO如何使用的性能指标,GL_STREAM_DRAW_ARB表示通过流的方式上传纹理,GL_STREAM_READ_ARB表示异步地从帧缓冲区中进行回读。