从0打造一个GPUImage(2)
每一行代码都值得研究透彻
究竟是如何绘制一个三角形的
上一章我们讲了如何绘制一个红色的三角形。但是很多关键细节都没有讲清楚。
第二章我们的目的主要是绘制一个正方形外加详细解释代码内容。
shader的compile
在github的https://github.com/zangqilong198812/OpenGLESTutorial 这个地址,我们将demo程序下载后。
会发现这样的文件目录。
这里会详细介绍一下ZQLShaderConpiler这个类。
这个类主要的工作很简单。编译shader并且把他们装载一个叫做glProgram的东西里。
在OpenGL ES中使用一个shader不只是写完它这么简单。还要经过一下步骤,所幸的是,这些都是属于固定的套路。所以只要记住流程。就不难理解了。
首先,编译一个shader的代码如下。
- (GLuint)compileShader:(NSString *)shaderName withType:(GLenum)shaderType {
NSString *path = [[NSBundle mainBundle] pathForResource:shaderName ofType:nil];
NSError *error = nil;
NSString *shaderString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
if (!shaderString) {
NSLog(@"%@", error.localizedDescription);
}
const char * shaderUTF8 = [shaderString UTF8String];
GLint shaderLength = (GLint)[shaderString length];
GLuint shaderHandle = glCreateShader(shaderType);
glShaderSource(shaderHandle, 1, &shaderUTF8, &shaderLength);
glCompileShader(shaderHandle);
GLint compileSuccess;
glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
GLchar message[256];
glGetShaderInfoLog(shaderHandle, sizeof(message), 0, &message[0]);
NSString *messageString = [NSString stringWithUTF8String:message];
NSLog(@"%@", messageString);
exit(1);
}
return shaderHandle;
}
因为shader的source文件在iOS中是以bundle中的某个文件存在的。所以前三行代码主要是检测这个文件是否存在,如果存在的话,把文件里的字符串取出来放入shaderString的变量中。
由于OpenGL中不能识别NSString,所以我们需要把NSString转换为const char *的char型数组。
然后我们使用GLuint shaderHandle = glCreateShader(shaderType);创建了一个名为shaderHandle的shader对象。
最后使用glShaderSource(shaderHandle, 1, &shaderUTF8, &shaderLength);加载shader的内容,更新shaderHandler。
最后调用glCompileShader(shaderHandle);编译他。
剩下的代码主要是检测整个流程是否成功。
当然这里我们虽然成功编译了一个shader,但是我们还是不能直接使用它,而是应该创建一个glProgram来attach它们,最后这样,shader才会被加载到OpenGL中。
流程如下。
- (void)compileVertexShader:(NSString *)vertexShader fragmentShader:(NSString *)fragmentShader {
GLuint vertexShaderName = [self compileShader:vertexShader withType:GL_VERTEX_SHADER];
GLuint fragmenShaderName = [self compileShader:fragmentShader withType:GL_FRAGMENT_SHADER];
_programHandle = glCreateProgram();
glAttachShader(_programHandle, vertexShaderName);
glAttachShader(_programHandle, fragmenShaderName);
glLinkProgram(_programHandle);
GLint linkSuccess;
glGetProgramiv(_programHandle, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(_programHandle, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"%@", messageString);
exit(1);
}
}
首先,利用第一个方法,我们获取了vertxShader和fragmentShader编译之后的shader对象。
然后我们调用_programHandle = glCreateProgram();创建了一个glProgram。
然后利用glAttachShader链接了两个shader到我们的glProgram里。这样,shader的整个编译流程才算完成了。
我们编译了shader,但是如何把数据传递给shader呢?
我们已知,在vertexShader中,我们需要传递顶点坐标给gl_Position。顶点坐标是由一个叫做a_Position的变量确定的。
attribute vec4 a_Position;
void main(void) {
gl_Position = a_Position;
}
现在的问题在于,我们如何把CPU中的数据,传递到GPU中的shader里。
当然这里也是有套路的。
首先,你需要获取shader中a_Position这个变量的位置。
那么分为以下几种情况了。
- 如果变量类型为attribute 那么需要通过GLuint positionLocation = glGetAttribLocation(_programHandle, "a_Position");这个函数获取. _programHandle是上文提到的glProgram。 第二个参数就是这个变量在shader中的名字。
- 如果变量类型为uniform GLuint positionLocation = glGetUniformLocation(_programHandle, "a_Position");这个函数获取. _programHandle是上文提到的glProgram。 第二个参数就是这个变量在shader中的名字。
那么在拿到了shader中变量的地址之后,我们就可以赋值了。
赋值的操作有两步。
1. 启动这个变量。
假设我们获取的变量地址为_positionSlot,那么只需要通过glEnableVertexAttribArray(_positionSlot);即可启动。
2. 赋值。
假设,我们的顶点坐标放在vertices的数组中。那么,我们赋值语句如下。glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
第一个是需要赋值的变量地址,这个肯定没问题了,你赋值总要确定赋值给谁吧。
第二个参数要和shader中变量的类型相同,什么意思呢?如果你这个变量是vec3,那么这里就写3,意思是,vertices中每隔3个数值是shader中一个变量的值。但是也有特例,比如gl_Position,gl_Position是vec4,而我们实际传入的却是3,表示在数组中,3个GLfloat数值确定一个位置。因为gl_Position这个比较特殊,如果你传入的是3个GLfloat,那么第四位w会默认为1.
另举一个例子,假如你的shader中有一个vec4格式的变量a_Color,用来表示RGBA的颜色。你传入的数据为static const GLfloat colors[] = {
1, 0, 0, 1,
1, 0, 0, 1,
1, 0, 0, 1,
1, 0, 0, 1
};
然后获取了a_Color这个变量的位置_colorSlot,那么赋值语句就会如下。
glEnableVertexAttribArray(_colorSlot);
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 0, colors);
如何显示我们的绘制结果?
显示OpenGL的绘制结果,我们需要用到两个东西。
1. GL_FRAMEBUFFER
2. GL_RENDERBUFFER
- (void)setupRenderBuffers {
glGenFramebuffers(1, &_frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
glGenRenderbuffers(1, &_renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
[_eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];
GLint width = 0;
GLint height = 0;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);
//check success
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
}
}
framebuffer和renderbuffer的概念比较晦涩,这里提供了两个博客地址,大家这里不需要太过在意。后面会在使用的过程中加深理解。http://longzxr.blog.sohu.com/168909774.html http://blog.csdn.net/wl_soft50/article/details/7916955
在iOS中想要显示OpenGL的绘制结果,主要有两种方法。
简单点的可以使用GLKView,难一点的是使用CAEAGLLayer。
当然,GLKView本质上不过是基于CAEAGLLayer的封装而已。所以我们介绍一下CAEAGLLayer的用法。
- (void)setupCAEAGLLayer:(CGRect)rect {
_eaglLayer = [CAEAGLLayer layer];
_eaglLayer.frame = rect;
_eaglLayer.backgroundColor = [UIColor yellowColor].CGColor;
_eaglLayer.opaque = YES;
_eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:NO],kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat, nil];
[self.view.layer addSublayer:_eaglLayer];
}
CAEAGLLayer的作用主要有两个,首先,它为renderbuffer分配共享存储。其次,它将渲染缓冲区呈现给Core Animation,用renderbuffer中的数据替换了以前的内容。该模型的一个优点是,只有当渲染的图像更改时,Core Animation图层的内容不需要在每个帧中绘制。
和所有的layer一样,我们需要生成和配置CAEAGLLayer的各种属性,其他frame,和backgroundColor还有opaque之类的基础属性就不细说了。这里主要介绍一下这个_eaglLayer.drawableProperties属性。这个属性是一个字典,主要规定了两个属性。
第一个是kEAGLDrawablePropertyRetainedBacking,表示不想保存已经显示的内容,因此在下一次显示时,应用程序必须完全重绘一次。将该设置为 TRUE 对性能和资源影像较大,因此只有当renderbuffer需要保持其内容不变时,我们才设置 kEAGLDrawablePropertyRetainedBacking为TRUE。”
kEAGLDrawablePropertyColorFormat,主要规定显示的颜色格式。
讲到这里,整个绘制流程可以用下图表示。
讲了这么多,好像还是跟GPUImage这个库一点关系没有。但是不要紧,下一篇,我们就可以做一些和图片有关的操作了。