本文檔描述了在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、坑
雖然樸素實現代碼達成了目標,但它有多余可優化之處?,F在逐一介紹本人已實踐的優化辦法。
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框架實現相同的圖像處理并比較兩者性能差異。