GPUImage源码阅读(一)

作者: 秦明Qinmin | 来源:发表于2017-04-15 12:38 被阅读1488次

    概述

    GPUImage是一个著名的图像处理开源库,它让你能够在图片、视频、相机上使用GPU加速的滤镜和其它特效。与CoreImage框架相比,可以根据GPUImage提供的接口,使用自定义的滤镜。项目地址:https://github.com/BradLarson/GPUImage
    这篇文章主要是阅读GPUImage框架中的 GLProgram、GPUImageContext 两个重要类的源码。这两个类是 GPUImage 框架的基础,里面涉及的知识也有 OpenGL ES 基础 和 多线程 基础。以下是源码内容:
    GLProgram
    GPUImageContext

    基础

    阅读GPUImage源码需要一定的知识储备,在这里我列出了需要的一些基础知识:OpenGL ES 2.0AVFoundationCoreGraphics。如果对以上框架不熟悉,可以学习一下相关的知识。在之前我写了一些 OpenGL专题 http://www.jianshu.com/c/30e2e76bc140 相关的专题系列,大家也可以去学习下。

    GLProgram

    之前在学习OpenGL的时候,我们首先要需要做的就是初始化OpenGL ES环境,编译、链接顶点着色器和片元着色器。在GPUImage中封装了GLProgram专门处理OpenGL ES程序的创建等相关工作。
    GLProgram的方法不是很多,这里简单介绍:

    • 初始化方法,可以根据需要传入顶点着色器的路径或字符串以及片源着色器的路径及字符串进行初始化。
    - (id)initWithVertexShaderString:(NSString *)vShaderString 
                fragmentShaderString:(NSString *)fShaderString;
    - (id)initWithVertexShaderString:(NSString *)vShaderString 
              fragmentShaderFilename:(NSString *)fShaderFilename;
    - (id)initWithVertexShaderFilename:(NSString *)vShaderFilename 
                fragmentShaderFilename:(NSString *)fShaderFilename;
    

    初始化的过程包含了顶点片源着色器的创建、编译,着色器程序的创建,顶点片源着色器附着到着色器程序等过程。

    - (id)initWithVertexShaderString:(NSString *)vShaderString 
                fragmentShaderString:(NSString *)fShaderString;
    {
        if ((self = [super init])) 
        {
            _initialized = NO;
            
            // 初始化属性数组
            attributes = [[NSMutableArray alloc] init];
            // 初始化uniform属性数组
            uniforms = [[NSMutableArray alloc] init];
            
            // 创建着色器程序
            program = glCreateProgram();
            
            // 编译顶点着色器
            if (![self compileShader:&vertShader 
                                type:GL_VERTEX_SHADER 
                              string:vShaderString])
            {
                NSLog(@"Failed to compile vertex shader");
            }
            
            // 编译片源着色器
            // Create and compile fragment shader
            if (![self compileShader:&fragShader 
                                type:GL_FRAGMENT_SHADER 
                              string:fShaderString])
            {
                NSLog(@"Failed to compile fragment shader");
            }
            
            // 将顶点片源着色器附着到着色器程序
            glAttachShader(program, vertShader);
            glAttachShader(program, fragShader);
        }
        
        return self;
    }
    
    • 链接程序,和编译型语言一样,OpenGL程序也需要链接。
    - (BOOL)link
    {
    //    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    
        GLint status;
        
        // 链接着色器程序
        glLinkProgram(program);
        
        // 获取链接状态
        glGetProgramiv(program, GL_LINK_STATUS, &status);
        // 链接失败则返回
        if (status == GL_FALSE)
            return NO;
        
        // 链接成功,就可以删掉相关的shader,释放资源
        if (vertShader)
        {
            glDeleteShader(vertShader);
            vertShader = 0;
        }
        if (fragShader)
        {
            glDeleteShader(fragShader);
            fragShader = 0;
        }
        
        // 设置初始化成功标识
        self.initialized = YES;
    
    //    CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime);
    //    NSLog(@"Linked in %f ms", linkTime * 1000.0);
    
        return YES;
    }
    
    • 使用着色器程序
    - (void)use
    {
        glUseProgram(program);
    }
    
    • 在给着色器传值的时候,我们需要获取相关属性的位置。GLProgram也提供了相关接口,方便我们获取变量的位置。
    - (void)addAttribute:(NSString *)attributeName
    {
        // 先判断当前的属性是否已存在
        if (![attributes containsObject:attributeName])
        {
            // 如果不存在先加入属性数组,然后绑定该属性的位置为在属性数组中的位置
            [attributes addObject:attributeName];
            glBindAttribLocation(program, 
                                 (GLuint)[attributes indexOfObject:attributeName],
                                 [attributeName UTF8String]);
        }
    }
    // END:addattribute
    // START:indexmethods
    - (GLuint)attributeIndex:(NSString *)attributeName
    {
        // 获取着色器属性变量的位置,即在数组的位置(根据之前的绑定关系)
        return (GLuint)[attributes indexOfObject:attributeName];
    }
    - (GLuint)uniformIndex:(NSString *)uniformName
    {
        // 获取Uniform变量的位置
        return glGetUniformLocation(program, [uniformName UTF8String]);
    }
    
    • 释放资源。在析构的时候,将着色器等相关资源清理。
    - (void)dealloc
    {
        if (vertShader)
            glDeleteShader(vertShader);
            
        if (fragShader)
            glDeleteShader(fragShader);
        
        if (program)
            glDeleteProgram(program);
           
    }
    

    GPUImageContext

    GPUImageContext类,提供OpenGL ES基本上下文,GPUImage相关处理线程,GLProgram缓存、帧缓存。由于是上下文对象,因此该模块提供的更多是存取、设置相关的方法。

    • 属性列表
    // GPUImage处理OpenGL绘制的相关队列,串行队列
    @property(readonly, nonatomic) dispatch_queue_t contextQueue;
    // 当前使用的着色器程序
    @property(readwrite, retain, nonatomic) GLProgram *currentShaderProgram;
    // OpenGLES上下文对象
    @property(readonly, retain, nonatomic) EAGLContext *context;
    // CoreVideo中的纹理缓存
    @property(readonly) CVOpenGLESTextureCacheRef coreVideoTextureCache;
    // 帧缓存
    @property(readonly) GPUImageFramebufferCache *framebufferCache;
    
    • 初始化过程。在初始化的过程中通过dispatch_queue_set_specific设置队列标识,这样做的原因在Effective Objective-C 2.0 中有提到,或者参见我之前的博客 重拾Effective Objective-C 2.0热点问题
    - (id)init;
    {
        if (!(self = [super init]))
        {
            return nil;
        }
        
        // 创建OpenGL渲染队列
        openGLESContextQueueKey = &openGLESContextQueueKey;
        _contextQueue = dispatch_queue_create("com.sunsetlakesoftware.GPUImage.openGLESContextQueue", GPUImageDefaultQueueAttribute());
        
    #if OS_OBJECT_USE_OBJC
        // 设置队列标识
        dispatch_queue_set_specific(_contextQueue, openGLESContextQueueKey, (__bridge void *)self, NULL);
    #endif
        // 初始化着色器缓存相关数组
        shaderProgramCache = [[NSMutableDictionary alloc] init];
        shaderProgramUsageHistory = [[NSMutableArray alloc] init];
        
        return self;
    }
    
    • 方法列表
    // 获取队列标识
    + (void *)contextKey;
    // 单例对象
    + (GPUImageContext *)sharedImageProcessingContext;
    // 获取处理队列
    + (dispatch_queue_t)sharedContextQueue;
    // 帧缓存
    + (GPUImageFramebufferCache *)sharedFramebufferCache;
    // 设置当前上下文
    + (void)useImageProcessingContext;
    - (void)useAsCurrentContext;
    // 设置当前的GL程序
    + (void)setActiveShaderProgram:(GLProgram *)shaderProgram;
    - (void)setContextShaderProgram:(GLProgram *)shaderProgram;
    // 获取设备OpenGLES相关特性的支持情况
    + (GLint)maximumTextureSizeForThisDevice;
    + (GLint)maximumTextureUnitsForThisDevice;
    + (GLint)maximumVaryingVectorsForThisDevice;
    + (BOOL)deviceSupportsOpenGLESExtension:(NSString *)extension;
    + (BOOL)deviceSupportsRedTextures;
    + (BOOL)deviceSupportsFramebufferReads;
    // 纹理大小调整,保证纹理不超过OpenGLES支持最大的尺寸
    + (CGSize)sizeThatFitsWithinATextureForSize:(CGSize)inputSize;
    // 将渲染缓存呈现在设备上
    - (void)presentBufferForDisplay;
    // 创建GLProgram,首先在缓存中查找,如果没有则创建
    - (GLProgram *)programForVertexShaderString:(NSString *)vertexShaderString fragmentShaderString:(NSString *)fragmentShaderString;
    // 创建Sharegroup
    - (void)useSharegroup:(EAGLSharegroup *)sharegroup;
    // Manage fast texture upload
    + (BOOL)supportsFastTextureUpload;
    @end
    

    由于GPUImageContext相当于一个上下文对象,主要是管理其它的对象,因此该类没有太多复杂的业务,这里主要看一下几个方法:

    • 调整纹理大小,保证纹理不超过OpenGLES支持最大的尺寸:
    + (CGSize)sizeThatFitsWithinATextureForSize:(CGSize)inputSize;
    {
        GLint maxTextureSize = [self maximumTextureSizeForThisDevice]; 
        if ( (inputSize.width < maxTextureSize) && (inputSize.height < maxTextureSize) )
        {
            return inputSize;
        }
        
        CGSize adjustedSize;
        if (inputSize.width > inputSize.height)
        {
            adjustedSize.width = (CGFloat)maxTextureSize;
            adjustedSize.height = ((CGFloat)maxTextureSize / inputSize.width) * inputSize.height;
        }
        else
        {
            adjustedSize.height = (CGFloat)maxTextureSize;
            adjustedSize.width = ((CGFloat)maxTextureSize / inputSize.height) * inputSize.width;
        }
    
        return adjustedSize;
    }
    
    • 获取OpenGLES支持的最大纹理尺寸。
    + (GLint)maximumTextureSizeForThisDevice;
    {
        static dispatch_once_t pred;
        static GLint maxTextureSize = 0;
        
        dispatch_once(&pred, ^{
            [self useImageProcessingContext];
            glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);
        });
    
        return maxTextureSize;
    }
    
    • 创建GLProgram,首先在缓存中查找,如果没有则创建
    - (GLProgram *)programForVertexShaderString:(NSString *)vertexShaderString fragmentShaderString:(NSString *)fragmentShaderString;
    {
        NSString *lookupKeyForShaderProgram = [NSString stringWithFormat:@"V: %@ - F: %@", vertexShaderString, fragmentShaderString];
        GLProgram *programFromCache = [shaderProgramCache objectForKey:lookupKeyForShaderProgram];
    
        if (programFromCache == nil)
        {
            programFromCache = [[GLProgram alloc] initWithVertexShaderString:vertexShaderString fragmentShaderString:fragmentShaderString];
            [shaderProgramCache setObject:programFromCache forKey:lookupKeyForShaderProgram];
    //        [shaderProgramUsageHistory addObject:lookupKeyForShaderProgram];
    //        if ([shaderProgramUsageHistory count] >= MAXSHADERPROGRAMSALLOWEDINCACHE)
    //        {
    //            for (NSUInteger currentShaderProgramRemovedFromCache = 0; currentShaderProgramRemovedFromCache < 10; currentShaderProgramRemovedFromCache++)
    //            {
    //                NSString *shaderProgramToRemoveFromCache = [shaderProgramUsageHistory objectAtIndex:0];
    //                [shaderProgramUsageHistory removeObjectAtIndex:0];
    //                [shaderProgramCache removeObjectForKey:shaderProgramToRemoveFromCache];
    //            }
    //        }
        }
        
        return programFromCache;
    }
    
    • 创建EAGLContext上下文对象,使用的是kEAGLRenderingAPIOpenGLES2的API也就是OpenGL ES 2.0。
    - (EAGLContext *)createContext;
    {
        EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2 sharegroup:_sharegroup];
        NSAssert(context != nil, @"Unable to create an OpenGL ES 2.0 context. The GPUImage framework requires OpenGL ES 2.0 support to work.");
        return context;
    }
    
    • 设置当前的上下文对象,以及当前的着色器程序。
    - (void)setContextShaderProgram:(GLProgram *)shaderProgram;
    {
        EAGLContext *imageProcessingContext = [self context];
        if ([EAGLContext currentContext] != imageProcessingContext)
        {
            [EAGLContext setCurrentContext:imageProcessingContext];
        }
        
        if (self.currentShaderProgram != shaderProgram)
        {
            self.currentShaderProgram = shaderProgram;
            [shaderProgram use];
        }
    }
    

    总结

    GLProgram 与着色器程序创建息息相关,其中包括创建、编译、链接、使用等过程。
    GPUImageContext 是GPUImage的上下文对象,管理着OpenGLES上下文对象,管理着GL程序,管理着帧缓存等GPUImage中的基本组件。

    源码地址:GPUImage源码阅读系列 https://github.com/QinminiOS/GPUImage
    系列文章地址:GPUImage源码阅读 http://www.jianshu.com/nb/11749791

    相关文章

      网友评论

      • Link913:加油,关注了
      • 狼牙破军:是否可以不用GPUImage的runAsynchronouslyOnVideoProcessingQueue方法来做处理呢,如果可以的话是不是可以自行创建队列组来操作GPUImage的数据?
      • 9a5f9a19e9d3:学习了

      本文标题:GPUImage源码阅读(一)

      本文链接:https://www.haomeiwen.com/subject/ohmwattx.html