iOS音视频(直播 音频 视频)IosiOS高性能编程

OpenGL ES 3.0 Transform Feedback

2016-08-22  本文已影响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框架实现相同的图像处理并比较两者性能差异。

上一篇下一篇

猜你喜欢

热点阅读