OpenGL ES 3.0 Transform Feedback

作者: 熊皮皮 | 来源:发表于2016-08-22 19:27 被阅读1462次

    本文档描述了在iOS上使用OpenGL ES 3.0新增的Transform Feedback功能只在顶点着色器中实现图像处理等通用GPU计算功能。区别于OpenGL ES 2.0将图像处理算法写在片段着色器,最终输出到离线纹理、渲染缓冲区或屏幕(默认帧缓冲区)中,Transform Feedback(变换反馈)可以只用顶点着色器实现所需的算法,故有时也被称为顶点变换。

    目录:
    |- (顶点着色器)实现图像对比度调整
    |- 读取GPU处理的结果图像
    |-- 映射GPU内存
    |-- RGBA原始数据创建UIImage
    |- 生成纹理坐标
    |- 坑
    |-- 使用整数采样器isampler2D容易出现的精度问题
    |-- 使用整数采样器isampler2D容易出现的纹理坐标问题
    |- 性能比较

    本人已编写的Transform Feedback相关文档:

    基于前面所写的iOS GPGPU 编程:GPU进行浮点计算并读取结果,现在探索调整图像对比度的简单实现及读取处理结果至主存并生成UIImage实例。下面是本文档对应程序的运行结果示例。

    原图 改变对比度

    1、(顶点着色器)实现图像对比度调整

    朴素实现如下所示。

    #version 300 es
    
    layout(location = 0) in vec2 in_texcoord;
    
    uniform sampler2D u_sampler;
    uniform float u_image_width;
    uniform float u_image_height;
    
    uniform float u_contrast_adjustment; // 默认为0.5
    
    flat out uint out_color;
    
    void main()
    {
        vec2 normalized_texcoord = vec2(
            in_texcoord.x / u_image_width, 
            in_texcoord.y / u_image_height);
        vec3 rgb = texture(u_sampler, normalized_texcoord).rgb;
        vec3 contrast = vec3(0.0, 0.0, 0.0);
        vec3 normalized_rgb = vec3(mix(contrast, rgb, u_contrast_adjustment));
    
        out_color = (255u << 24) + 
            (uint(normalized_rgb.b * 255.0) << 16) + 
            (uint(normalized_rgb.g * 255.0) << 8) + 
            (uint(normalized_rgb.r * 255.0) << 0);
    }
    

    简单分析上述代码:

    1. 指定输出变量out_color为flat表示不对结果进行插值,从而保持main函数的处理结果。
    2. u_image_width、u_image_height由客户端指定需要处理的图像维度,由于后面上传的纹理坐标是[0, 图像宽高],而OpenGL ES定义的纹理坐标范围为[0, 1.0],因此进行归一化处理。
    vec2 normalized_texcoord = vec2(
            in_texcoord.x / u_image_width, 
            in_texcoord.y / u_image_height);
    
    1. mix函数实现了对比度调整。mix函数的作用对contrast和rgb两个参数,根据u_contrast_adjustment的值(表示为百分比)进行线性插值,最终将contrast与rgb所表示的两个颜色混合到一起。如果mix函数的第三个参数为第二个参数的alpha值,此时,计算结果相当于调用glBlendFunc函数。
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    1. 将计算结果映射回[0, 255]范围,后续在CPU上创建UIImage。不像Fragment Shader那样使用vec4的原因是,CGImage需要32位(4分量、每分量一字节)的数据格式,而vec4是4个浮点数据,还得做数据截断,多出了工作量。补充:根据对图像数据的进一步了解,图像的RGB值也可定义为浮点数,具体操作办法随后添加。

    注意,OpenGL ES 3.0顶点着色器中不允许指定统一变量和输出变量的布局修饰符,下面的写法将导致编译失败。

    layout(location = 0) uniform sampler2D u_sampler;
    layout(location = 0) out vec4 out_color;
    

    然而,在片段着色器中,指定输出变量的布局修饰符是合法的。

    2、读取GPU处理的结果图像

    读取顶点着色器输出的图像数据的过程略为曲折,由于图像RGB(A)数据一般是大端存储,而iOS是小端,故最终输出时得作些额外操作。

    2.1、映射GPU内存

    图像操作的结果数据在GPU内存中,而生成UIImage得在CPU上运行,因此不得不进行内存映射。根据老外的说法,iOS设备使用统一内存模型(Uniform Memory Model),那么数据不像PC一样在主存和显存中拷贝,而是全部放置于主存中。

    GLuint *mappedBuffer = glMapBufferRange(GL_ARRAY_BUFFER, 
        0, 
        imagePixels * sizeof(GLuint), 
        GL_MAP_READ_BIT);
    

    这里映射的数据类型和着色器代码中输出的数据类型保持一致,避免读写越界错误。

    2.2、RGBA原始数据创建UIImage

    这里参考我另一个文档iOS OpenGL ES 3.0 数据可视化 4:纹理映射实现2维图像与视频渲染简介描述的RGBA祼数据创建UIImage的方法,区别是前面绘制时没使用顶点数据,所以纹理是完全按原图像进行采样,不存在结果图像倒转问题,因此删除了翻转代码。

    CGContextTranslateCTM(context, 0.0, renderTargetHeight);
    CGContextScaleCTM(context, 1.0, -1.0);
    

    完整实现如下所示。

    int renderTargetSize = imagePixels * 4;
    int renderTargetWidth = imageWidth;
    int renderTargetHeight = imageHeight;
    int rowSize = renderTargetWidth * 4;
    CGDataProviderRef ref = CGDataProviderCreateWithData(NULL, 
        mappedBuffer, 
        renderTargetSize, 
        NULL);
    CGImageRef iref = CGImageCreate(renderTargetWidth,
        renderTargetHeight, 8, 32, rowSize,
        CGColorSpaceCreateDeviceRGB(),
        kCGImageAlphaLast | kCGBitmapByteOrderDefault, ref,
        NULL, true, kCGRenderingIntentDefault);
    
    uint8_t* contextBuffer = (uint8_t*)malloc(renderTargetSize);
    memset(contextBuffer, 0, renderTargetSize);
    CGContextRef context = CGBitmapContextCreate(contextBuffer,
        renderTargetWidth, renderTargetHeight, 
        8, 
        rowSize,
        CGImageGetColorSpace(iref),
        kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Big);
    CGContextDrawImage(context, 
        CGRectMake(0.0, 0.0, renderTargetWidth, renderTargetHeight), 
        iref);
    CGImageRef outputRef = CGBitmapContextCreateImage(context);
    UIImage* image = [[UIImage alloc] initWithCGImage:outputRef];
    
    CGImageRelease(outputRef);
    CGContextRelease(context);
    CGImageRelease(iref);
    CGDataProviderRelease(ref);
    free(contextBuffer);
    

    3、生成纹理坐标

    出于编程方便起见,定义纹理坐标结构体。

    typedef struct {
        GLushort s, t;
    } TextureCoodinate;
    

    根据图像维度信息生成纹理坐标。

    int imagePixels = (int) (image.size.width * image.size.height);
    TextureCoodinate *texcoods = calloc(imagePixels, sizeof(TextureCoodinate));
    int index = 0;
    for (int line = 0; line < image.size.height; ++line) {
        for (int col = 0; col < image.size.width; ++col) {
            TextureCoodinate *t = &texcoods[index];
            t->t = (GLushort)line;
            t->s = (GLushort)col;
            ++index;
        }
    }
    

    生成坐标时,需注意纹理坐标系的方向。

    4、坑

    虽然朴素实现代码达成了目标,但它有多余可优化之处。现在逐一介绍本人已实践的优化办法。

    4.1、使用整数采样器isampler2D容易出现的精度问题

    在朴素实现中,使用了浮点类型的采样器sampler2D,它采得的是浮点数、范围在[0, 1]内。然而,多数情况下,我们加载和创建UIImage时使用的数据源往往是[0, 255]的整数,最终输出变量不得不乘以255.0作逆映射。为优化这种多余的乘法,现尝试使用整型采样器isampler2D。

    按之前的编程经验,自然写出如下代码。

    // Vertex Shader
    uniform isampler2D u_sampler;
    

    但是,得到一个编译错误:declaration must include a precision qualifier for type。

    片段着色器需要声明浮点数的精度,这在OpenGL ES 3.0的开发过程中大家熟知的步骤,然而,整型采样器需要添加什么精度修饰符呢?语法类似于单个变量的浮点数精度声明,直接添加精度修饰符在变量类别关键字之后、类型之前,示例如下。

    // Vertex Shader
    uniform lowp isampler2D u_sampler;
    

    现在,通过u_sampler使用texture采样,我们得到了[0, 255]之间的颜色值。

    4.2、使用整数采样器isampler2D容易出现的纹理坐标问题

    虽然,前面的修改让我们得到了整数颜色值,但是,对于纹理坐标归一化,还得每次都计算一次,还是多了一次额外的操作。那么,是否可以使用ivec2替换当前的vec2浮点纹理坐标呢?经尝试,不可行。可能需要额外的设置步骤,基于本人有限的OpenGL ES了解,暂时放弃此方案。不过,前面的实现可进一步优化为:

    vec2 normalized_texcoord = in_texcoord / 
        vec2(u_image_width, u_image_height);
    

    4.3、纹理坐标的数据类型

    朴素实现代码采用了逐点绘制方式进行每个像素点的操作,这要求生成的纹理坐标与glVertexAttribPointer函数指定数据解析格式相符,比如:

    // Using Vertex Buffer Object
    glVertexAttribPointer(0, 
        2,
        GL_UNSIGNED_SHORT, 
        GL_FALSE,
        0,
        NULL);
    

    若生成的纹理坐标为浮点类型,则glVertexAttribPointer的参数需同步为GL_FLOAT,避免错误的数据格式读取,导致坐标值错误,最终输出错误的计算结果。

    5、性能比较

    目前,因工作任务较多,暂未用Accelerate框架实现相同的图像处理并比较两者性能差异。

    相关文章

      网友评论

      本文标题:OpenGL ES 3.0 Transform Feedback

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