从0打造一个GPUImage(2)

488 阅读6分钟
原文链接: zhuanlan.zhihu.com

从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这个变量的位置。

那么分为以下几种情况了。

  1. 如果变量类型为attribute 那么需要通过GLuint positionLocation = glGetAttribLocation(_programHandle, "a_Position");这个函数获取. _programHandle是上文提到的glProgram。 第二个参数就是这个变量在shader中的名字。
  2. 如果变量类型为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这个库一点关系没有。但是不要紧,下一篇,我们就可以做一些和图片有关的操作了。