本文档描述了在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进行浮点计算并读取结果 详细描述了iOS上使用Transform Feedback的完整流程。
- NDK OpenGL ES 3 编译C/C++可执行文件(无需JNI调用)描述了NDK了配置OpenGL ES 3.0及以上版本开发环境并C++可执行文件,Android上进行通用GPU计算可参考此文档。
- iOS GPGPU 编程:解决苹果开发者论坛的求助帖(Transform Feedback doesn't write, crashes) 解决提问者代码中存在的内存映射空间大小错误导致映射失败。
- iOS GPGPU 编程:Transform Feedback实现图像对比度调整 介绍读取顶点着色器处理后的图像数据并生成UIImage的具体实现。
基于前面所写的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);
}
简单分析上述代码:
- 指定输出变量out_color为flat表示不对结果进行插值,从而保持main函数的处理结果。
- 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);
- mix函数实现了对比度调整。mix函数的作用对contrast和rgb两个参数,根据u_contrast_adjustment的值(表示为百分比)进行线性插值,最终将contrast与rgb所表示的两个颜色混合到一起。如果mix函数的第三个参数为第二个参数的alpha值,此时,计算结果相当于调用glBlendFunc函数。
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
- 将计算结果映射回[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框架实现相同的图像处理并比较两者性能差异。