【转】OpenGL多线程创建纹理,附加我的测试结果

本文介绍如何在OpenGL中实现多线程纹理加载,通过创建并共享渲染上下文(RC)来避免线程间的资源冲突,提高加载效率。具体包括创建和共享RC的方法,以及在多线程环境下正确使用OpenGL函数的注意事项。

原文地址 http://www.cnblogs.com/mazhenyu/archive/2010/04/29/1724190.html

 

关于这个问题以前只知道多个线程不能同时使用一个RC,结果为了能动态加载,当初还做了一个通过拆分主线程的工作来模拟多线程加载的伪多线程程序,今天突然很想把这个问题彻底搞明白,结果从百度到google.com最后才在终于找到这么一篇讲解详细的英文文章,可怜我4级都没过的英语啊...

这是英文原文地址:http://veelck.wordpress.com/2008/11/28/multithread-texture-loading-in-opengl/

老外写东西一般废话会比较多,为了节省技术人员的宝贵时间就不逐句翻译了,提取一下要点吧,如下:

首先使用同一个DC创建两个RC,并且使用wglShareLists共享两个RC的资源。建议不要给这三个函数之间加其他向RC中添加东西的GL函数。

HGLRC hRC1 = wglCreateContext(hDC);

HGLRC hRC2 = wglCreateContext(hDC);

wglShareLists(hRC2, hRC1);

注意:wglShareLists的第一个参数中的RC是分享别人资源的,第二个参数中的RC是奉献资源供别人分享的。

//这是一个用来渲染场景的线程(也可用主线程来渲染)

renderingThread()

{

//...

wglMakeCurrent(hDC, hRC1);

//...

}

理论上来讲,可以在多创建几个RC,然后用多线程同时渲染。但是据说这样做是可行但却无益的。因为OpenGL会进行一些频繁的切换,导致产生高昂的开销。

//这是一个用来加载资源的线程,可以加载图片,并使用创建纹理,设置纹理参数等。

loadingThread()

{

//...

wglMakeCurrent(hDC, hRC2);

//...

}

注意,加载工作也可以写在多个线程中,例如使用多个线程从硬盘中读取图片文件(在多核机器上用双核读图片会比单核快一些),然后使用一个专门的线程调用GL的函数来创建纹理。但是,如果使用一些开源的图像库来读取图片的话就要注意了,有些开源的图像库不支持多线程,如DevIL。(FreeImage记得也存在这个问题)

最后别忘了在各个线程结束时调用wglMakeCurrent(NULL, NULL);取消DC与RC的关联。

还有要删除RC

wglDeleteContext(hRC2);

wglDeleteContext(hRC1);

最后要发一下牢骚。网上三维图形方面的资料相比其他计算机技术而言实在是不多,而且大多数还没有用。国内更是少的可怜,可能高人们都在忙着为买房子而奋斗导致没时间上网了。以下是该英文文章作者的感言,深有同感啊,向这种有国际主义精神的高人致敬。

It was quite a struggle to find anything useful in the web, only some minor things at few forums and mailing groups. But using that with some thinking and reading between lines I finally found the solution I’d like to share, as it can save (in my opinion) much time of searching and experimenting for others.

 

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

我的测试:

测试程序由nehe的lesson7项目修改完成

#include<process.h>

HDC   hDC=NULL;  // Private GDI Device Context
HGLRC  hRC=NULL;  // Permanent Rendering Context

HGLRC  hRCShareing=NULL;// 用于分享hRC的资源

 

if (!(hRC=wglCreateContext(hDC)))    // Are We Able To Get A Rendering Context?
 {
  KillGLWindow();        // Reset The Display
  MessageBox(NULL,"Can't Create A GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
  return FALSE;        // Return FALSE
 }

 hRCShareing = wglCreateContext(hDC);

 wglShareLists(hRCShareing, hRC);

 // 启动加载线程
 _beginthread(TextureLoadingThread, NULL, NULL);  //开启加载纹理线程

 

void TextureLoadingThread(void* p)
{

 //...

 wglMakeCurrent(hDC, hRCShareing);

 if (!LoadGLTextures())        // Jump To Texture Loading Routine
 {
  return;         // If Texture Didn't Load Return FALSE
 }

 //...

 wglMakeCurrent(NULL, NULL);

 

// 结束线程  
  _endthread();
}

 

测试结果:

通过OK,纹理显示正常

如果注释掉下面共享hrc的代码,如下所示

//wglShareLists(hRCShareing, hRC);

则纹理无法显示,看到的是个白块。

 

GLFW文档对Current context的注释:

Before you can make OpenGL or OpenGL ES calls, you need to have a current context of the correct type. A context can only be current for a single thread at a time, and a thread can only have a single context current at a time.

转载于:https://www.cnblogs.com/cack/p/4970653.html

<think>我们使用OpenGL ES 3.0将RGBA图像换为NV12格式。NV12是一种YUV格式,属于YUV420SP(半平面)格式,其中Y分量是连续的,然后UV分量(在NV12中称为VU分量)交错存储在一个平面中(先是U,然后是V,但在NV12中实际上是交错存储的:U0,V0, U1,V1, ...,但在内存布局上是先所有Y,然后是一个交错的UV平面)。 由于OpenGL ES 3.0支持计算着色器(Compute Shader),我们可以使用计算着色器来高效地进行换。但是请注意,OpenGL ES 3.0的计算着色器支持是可选的(在扩展中),并不是所有设备都支持。因此,我们假设设备支持计算着色器(例如,具有OES_shader_image_atomic扩展等,但更直接的是使用图像加载存储扩展)。 另一种方法是使用片段着色器渲染到纹理,但这里我们使用计算着色器,因为它更直接。 步骤: 1. 创建两个纹理:一个用于输入的RGBA图像,另一个用于输出的NV12图像(注意:NV12图像是一个具有特定格式的纹理,我们将其视为一个2D纹理,但高度是原高度的1.5倍,因为Y平面占满,UV平面高度为一半,但宽度相同,并且UV平面是交错的)。 但实际上,我们通常将NV12图像视为一个纹理对象,但使用GL_R8格式存储Y平面,然后使用GL_RG8格式存储UV平面(因为OpenGL ES 3.0不支持直接存储NV12纹理,所以我们需要两个纹理或者一个纹理的两个层?)。但是,更高效的方式是使用一个纹理,其中Y平面占整个纹理的2/3(因为每个像素一个Y,而UV平面是每4个Y共享一组UV,所以UV平面大小为宽度*高度/2,但注意NV12的存储要求:总大小 = width * height * 3/2)。 2. 由于OpenGL ES 3.0没有原生的NV12纹理格式,我们可以将输出NV12数据存储在一个纹理中,但该纹理的格式设置为GL_R8,宽度为原图像宽度,高度为原图像高度的1.5倍。然后,我们将Y分量写入纹理的下部(高度0到height-1),而UV分量(交错的)写入从height开始的高度为height/2的区域。注意,在UV部分,每个纹素包含两个字节:U和V(对应两个水平相邻的像素)。 3. 另一种方式(更符合OpenGL ES)是使用两个输出图像:一个用于Y平面(GL_R8格式,宽度=原宽度,高度=原高度),另一个用于UV平面(GL_RG8格式,宽度=原宽度/2,高度=原高度/2)。因为NV12的UV平面是每2x2个像素共享一组UV,但在存储上,UV平面是交错存储的,每个UV纹素包含两个值:U和V(对应相邻的两个像素的U和V?实际上,在NV12中,UV平面是连续存储的,每两个像素(水平相邻)的U和V交替存储,即U0,V0, U1,V1...,但注意这些UV值覆盖2x2的块)。然而,在UV平面中,每个纹素对应原始图像的一个2x2块,但存储时,我们存储的是连续的交错值。因此,使用GL_RG8格式,我们可以将U和V分别存储在R和G通道,每个纹素对应两个像素(水平方向)的UV值?实际上,一个2x2块在Y平面有4个Y值,而在UV平面只有两个值(一个U和一个V)?不对,在NV12中,UV平面是交错的,每个2x2块对应一个UV对(即一个U和一个V),但存储时,UV平面是连续的,每个水平位置有两个值(U和V),所以每个UV纹素实际上存储一个U和一个V,对应一个2x2块。因此,UV平面的宽度是原图像宽度的一半,高度是原图像高度的一半。 4. 因此,我们可以创建两个图像(纹理)作为输出: - 一个Y平面图像:格式为GL_R8,尺寸为width x height。 - 一个UV平面图像:格式为GL_RG8,尺寸为width/2 x height/2。 5. 在计算着色器中,我们将每个线程处理一个像素(对于Y平面)和对应的UV计算(但注意UV是每2x2块计算一次,所以UV平面每个线程处理一个2x2块)。 6. 换公式:RGBYUV Y = 0.299 * R + 0.587 * G + 0.114 * B U = -0.169 * R - 0.331 * G + 0.5 * B + 128 V = 0.5 * R - 0.419 * G - 0.081 * B + 128 注意:这些系数是近似值,并且通常使用整数运算和查表优化。另外,在移动设备上,我们可能使用更精确的公式(如BT.601或BT.709)。这里我们使用BT.601的公式(用于标准清晰度电视)。 7. 但是,由于我们输入是RGBA,所以我们需要忽略A通道(或者使用它?通常不需要,所以只取RGB)。 8. 步骤: - 创建计算着色器程序。 - 创建输入纹理(RGBA)并绑定到图像单元(使用glBindImageTexture)。 - 创建两个输出图像:Y平面和UV平面,并绑定到不同的图像单元。 - 分派计算着色器:对于Y平面,每个线程处理一个像素,所以线程组数量为(width+15)/16, (height+15)/16, 1(假设工作组大小为16x16)。 - 在计算着色器中,根据当前线程的全局ID(即gl_GlobalInvocationID)读取输入纹理的RGBA值,然后计算Y值,写入Y平面图像。 - 同时,为了计算UV,我们需要考虑2x2块。我们可以为每个2x2块启动一个线程来处理UV,或者在一个线程组中同时计算Y和UV。但为了简单,我们可以先计算每个像素的Y,然后使用另一个计算着色器来计算UV(因为UV需要2x2块的平均值?)。或者,我们在同一个着色器中同时输出Y和UV?但是注意,同一个着色器可以写入多个输出图像。 9. 然而,在同一个着色器中同时计算Y和UV会有一个问题:一个线程处理一个像素,而UV需要四个像素(2x2)才能计算。因此,我们可以这样设计: 方案一:使用两个计算着色器。 第一个着色器:计算Y平面(每个像素一个线程)。 第二个着色器:计算UV平面(每个2x2块一个线程)。在第二个着色器中,每个线程读取2x2像素的RGB值,计算平均的RGB(或者分别计算四个像素的YUV,然后取U和V的平均值?或者直接计算四个像素的RGB平均,然后计算U和V?)。注意,标准做法是:先计算每个像素的Y、U、V,然后对于2x2块,取四个像素的U的平均值作为U,V的平均值作为V。但这样需要额外的存储来保存中间U和V(每个像素的)。我们可以避免中间存储,直接计算四个像素的RGB平均值,然后计算U和V。 方案二:在一个着色器中同时输出Y和UV,但使用不同的线程组织。我们可以将线程分为两组:一组用于Y(每个像素一个线程),另一组用于UV(每个2x2块一个线程)。但这样需要启动两个不同的线程组,所以还是两个着色器更清晰。 10. 这里我们采用两个计算着色器的方式: 第一个着色器(Y平面): 输入:RGBA纹理(绑定为image2D,只读) 输出:Y平面图像(绑定为image2D,格式为r8,只写) 每个线程计算一个像素的Y值。 第二个着色器(UV平面): 输入:RGBA纹理(绑定为image2D,只读) 输出:UV平面图像(绑定为image2D,格式为rg8,只写) 每个线程处理一个2x2块(即原始图像的4个像素)。计算这4个像素的平均RGB(或者分别计算每个像素的U和V,然后取平均),然后计算一个U和一个V值(注意:我们计算的是整个块的平均U和V,或者直接计算平均RGB再计算U和V?两种方法都可以,但标准做法是计算平均U和平均V)。 11. 但是,为了节省带宽,我们可以在一个着色器中完成:每个线程处理一个2x2块,同时计算四个Y值和该块的UV值。这样只需要一个着色器,并且每个2x2块启动一个线程。这样更高效,因为只需要启动原来1/4的线程数。 因此,我们采用一个着色器,每个线程处理一个2x2块,做以下事情: - 读取2x2块的四个RGBA像素。 - 计算每个像素的Y值,并写入Y平面相应的位置。 - 计算四个像素的U和V的平均值(或者先计算四个像素的RGB平均值,再计算一个U和一个V?根据标准,通常每个像素都计算YUV,然后对UV进行平均)。 这里我们采用:先计算四个像素的RGB平均值,然后根据这个平均RGB计算U和V。 注意:这种方法与分别计算每个像素的U和V然后平均在数学上不完全等价,但实际中常使用平均RGB再计算UV,因为计算量小。 公式: R_avg = (R0 + R1 + R2 + R3) / 4 G_avg = (G0 + G1 + G2 + G3) / 4 B_avg = (B0 + B1 + B2 + B3) / 4 然后计算 U = -0.169 * R_avg - 0.331 * G_avg + 0.5 * B_avg + 128 V = 0.5 * R_avg - 0.419 * G_avg - 0.081 * B_avg + 128 或者,使用每个像素的YUV然后平均UV(但这样需要计算4次YUV公式): U_avg = (U0 + U1 + U2 + U3) / 4 V_avg = (V0 + V1 + V2 + V3) / 4 两种方法都可以,但第一种方法计算量小。 12. 我们采用第一种方法(平均RGB再计算UV)。 13. 步骤(在计算着色器中): 每个线程负责一个2x2块(即原始图像中2x2的像素区域)。 计算该2x2块中每个像素的Y值,并写入Y平面(需要四个写入操作)。 计算该2x2块的平均RGB,然后计算U和V,写入UV平面(一个位置,因为UV平面每个纹素对应一个2x2块)。 14. 注意:在写入Y平面时,我们需要知道每个像素在块内的位置。例如,块左上角像素的坐标为(2*x, 2*y),右上角为(2*x+1, 2*y),左下角为(2*x, 2*y+1),右下角为(2*x+1, 2*y+1)。 15. 工作组大小:我们可以设置工作组为16x16个线程,每个线程处理一个2x2块,那么一个工作组可以处理32x32像素的块。 16. 创建输入纹理(RGBA)和两个输出图像(Y平面和UV平面): 输入纹理:GL_RGBA8, 宽度=image_width, 高度=image_height。 Y平面图像:GL_R8, 宽度=image_width, 高度=image_height。 UV平面图像:GL_RG8, 宽度=image_width/2, 高度=image_height/2。 17. 在着色器中,我们需要将输入纹理绑定为image2D,并设置为只读(layout(rgba8) uniform image2D inputImage;)。输出Y平面绑定为image2D,格式为r8(layout(r8) writeonly uniform image2D yImage;),输出UV平面绑定为image2D,格式为rg8(layout(rg8) writeonly uniform image2D uvImage;)。 18. 计算着色器代码(GLSL ES 3.1): 注意:OpenGL ES 3.1支持计算着色器,但需要设备支持。 19. 由于OpenGL ES 3.0本身不支持计算着色器,但通过扩展支持(如OES_compute_shader)。因此,我们假设环境支持计算着色器。 20. 代码实现: 顶点着色器和片段着色器?不需要,我们使用计算着色器。 步骤: a. 创建着色器程序(计算着色器)。 b. 创建输入纹理并上传数据(RGBA)。 c. 创建两个输出图像(纹理)用于Y和UV平面。 d. 绑定输入纹理和输出图像到图像单元(使用glBindImageTexture)。 e. 使用计算着色器程序,设置工作组大小,然后分派计算(glDispatchCompute)。 f. 使用内存屏障(glMemoryBarrier)确保计算完成。 g. 然后,我们可以从输出图像中读取数据(使用glGetTexImage)或者用于其他操作。 21. 注意:输出图像需要先分配内存(使用glTexStorage2D),并且格式为GL_R8和GL_RG8。 22. 下面给出代码框架(使用C++和OpenGL ES 3.0,假设支持计算着色器扩展)。 由于代码较长,我们分步骤: 初始化: // 创建输入纹理(RGBA) GLuint inputTex; glGenTextures(1, &inputTex); glBindTexture(GL_TEXTURE_2D, inputTex); glTexParameteri(...); glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, rgba_data); // 创建Y平面纹理 GLuint yTex; glGenTextures(1, &yTex); glBindTexture(GL_TEXTURE_2D, yTex); glTexStorage2D(GL_TEXTURE_2D, 1, GL_R8, width, height); // 创建UV平面纹理 GLuint uvTex; glGenTextures(1, &uvTex); glBindTexture(GL_TEXTURE_2D, uvTex); glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, width/2, height/2); // 创建计算着色器程序 GLuint computeShader = glCreateShader(GL_COMPUTE_SHADER); // 加载着色器源码(字符串) glShaderSource(computeShader, 1, &computeShaderSource, NULL); glCompileShader(computeShader); // 检查编译错误... GLuint program = glCreateProgram(); glAttachShader(program, computeShader); glLinkProgram(program); // 检查链接错误... // 使用程序 glUseProgram(program); // 绑定图像纹理 glBindImageTexture(0, inputTex, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); // 绑定到单元0 glBindImageTexture(1, yTex, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R8); // 绑定到单元1 glBindImageTexture(2, uvTex, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RG8); // 绑定到单元2 // 设置工作组大小,假设我们使用16x16的工作组,每个工作组处理16x16个2x2块(即32x32像素) // 计算分块数量:x方向:ceil(width/2 / 16) = (width+31)/32 取整,但注意:每个线程处理一个2x2块(即原始图像2x2像素),所以原始图像宽度方向有 width/2 个块,高度方向有 height/2 个块。 // 所以分派:glDispatchCompute((width/2 + 15)/16, (height/2 + 15)/16, 1); glDispatchCompute((width + 31) / 32, (height + 31) / 32, 1); // 注意:因为每个线程处理2x2像素,所以整个图像需要 (width/2) x (height/2) 个线程。工作组大小为16x16,所以分派x方向:ceil((width/2)/16) = (width+31)/32(因为width/2除以16,相当于width除以32,向上取整就是(width+31)/32) // 内存屏障 glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); // 之后,我们可以从yTex和uvTex读取数据,合并成NV12格式(注意:NV12格式是先所有Y,然后UV平面(交错的,U和V交替))。 // 读取Y平面:glGetTexImage(GL_TEXTURE_2D, 0, GL_RED, GL_UNSIGNED_BYTE, y_data); // 读取UV平面:glGetTexImage(GL_TEXTURE_2D, 0, GL_RG, GL_UNSIGNED_BYTE, uv_data); // 然后,将Y数据复制到NV12缓冲区的前面,再将UV数据(每个纹素两个字节:U和V)复制到Y数据后面,注意UV平面的大小是width/2 * height/2 * 2 = width * height / 2,所以总大小= width*height + width*height/2 = width*height*3/2。 23. 计算着色器源码(字符串): 注意:我们使用BT.601系数的公式(标准清晰度电视),并且将结果限制在0-255。 着色器代码: ```glsl #version 310 es layout(local_size_x = 16, local_size_y = 16) in; // 工作组大小为16x16 layout(rgba8, binding=0) readonly uniform image2D inputImage; layout(r8, binding=1) writeonly uniform image2D yImage; layout(rg8, binding=2) writeonly uniform image2D uvImage; void main() { // 当前线程处理的块坐标(每个线程处理一个2x2块,块坐标从0到(width/2-1, height/2-1)) ivec2 blockCoord = ivec2(gl_GlobalInvocationID.xy); // 原始图像中,该块左上角像素的坐标 ivec2 baseCoord = blockCoord * 2; // 读取2x2块内的四个像素 vec4 pixel00 = imageLoad(inputImage, baseCoord); vec4 pixel01 = imageLoad(inputImage, baseCoord + ivec2(1,0)); vec4 pixel10 = imageLoad(inputImage, baseCoord + ivec2(0,1)); vec4 pixel11 = imageLoad(inputImage, baseCoord + ivec2(1,1)); // 计算每个像素的Y值(使用BT.601系数) // Y = 0.299*R + 0.587*G + 0.114*B float y00 = 0.299 * pixel00.r + 0.587 * pixel00.g + 0.114 * pixel00.b; float y01 = 0.299 * pixel01.r + 0.587 * pixel01.g + 0.114 * pixel01.b; float y10 = 0.299 * pixel10.r + 0.587 * pixel10.g + 0.114 * pixel10.b; float y11 = 0.299 * pixel11.r + 0.587 * pixel11.g + 0.114 * pixel11.b; // 将Y值写入Y平面(注意:Y平面是单通道,所以写入r分量) // 注意:imageStore函数需要整数坐标 imageStore(yImage, baseCoord, vec4(y00, 0.0, 0.0, 1.0)); imageStore(yImage, baseCoord + ivec2(1,0), vec4(y01, 0.0, 0.0, 1.0)); imageStore(yImage, baseCoord + ivec2(0,1), vec4(y10, 0.0, 0.0, 1.0)); imageStore(yImage, baseCoord + ivec2(1,1), vec4(y11, 0.0, 0.0, 1.0)); // 计算四个像素的平均RGB vec3 avgRGB = (pixel00.rgb + pixel01.rgb + pixel10.rgb + pixel11.rgb) * 0.25; // 使用平均RGB计算U和V(BT.601) // U = -0.169*R - 0.331*G + 0.5*B + 128.0/255.0 [注意:我们的RGB是0.0-1.0,而YUV公式中通常使用0-255,所以我们需要调整] // 但是,我们上面的计算中,RGB是0.0-1.0,所以需要将公式调整到0-1范围: // 原始公式(0-255): // U = -0.169*R -0.331*G +0.5*B +128 // 除以255,得到0-1范围的公式: // U = (-0.169*R -0.331*G +0.5*B)*255/255 + 128/255 // 但是,我们也可以将系数调整到0-1范围,然后加上128/255。不过,我们通常先计算0-255的值,然后归一化到0-1。 // 另一种方法:在0-1范围内使用调整的系数: // Y = 0.299*R + 0.587*G + 0.114*B (这个系数在0-1和0-255下是一样的,因为线性) // U = -0.169*R -0.331*G +0.5*B + 0.5; [因为128/255≈0.5]? 不对,128对应0.5,但公式中我们计算的是-0.169*R(0-1)然后加上0.5,这样得到0-1的值,然后乘以255就是0-255的值。 // 但是,我们写入UV图像的是0-1的值,然后OpenGL会自动换为0-255(因为纹理是GL_UNSIGNED_BYTE)?但是,在计算着色器中,我们写入的是浮点数,但图像格式是r8和rg8,所以会自动换为整数。 // 因此,我们使用0-1范围的公式(注意:标准公式中的128在0-1范围对应0.5): // U = -0.169*R -0.331*G +0.5*B + 0.5; // V = 0.5*R -0.419*G -0.081*B + 0.5; float u = -0.169 * avgRGB.r - 0.331 * avgRGB.g + 0.5 * avgRGB.b + 0.5; float v = 0.5 * avgRGB.r - 0.419 * avgRGB.g - 0.081 * avgRGB.b + 0.5; // 将u和v写入UV平面(每个块一个纹素) imageStore(uvImage, blockCoord, vec4(u, v, 0.0, 1.0)); } ``` 24. 注意:上面的计算中,我们假设输入纹理的像素值范围是0.0-1.0(因为imageLoad返回的是归一化的浮点数)。 25. 但是,上面的公式可能不是完全精确的。实际上,更精确的公式(来自BT.601): Y = 0.299 * R + 0.587 * G + 0.114 * B U = -0.147 * R - 0.289 * G + 0.436 * B + 0.5 [或者用 U = 0.492*(B-Y) 等] 但是,我们使用常见的公式(与上面一致): U = -0.169 * R - 0.331 * G + 0.5 * B + 0.5 V = 0.5 * R - 0.419 * G - 0.081 * B + 0.5 26. 另外,注意:上面的UV公式可能不是标准公式,标准公式通常使用: U = (B-Y) * 0.565 V = (R-Y) * 0.713 然后缩放并偏移到0-255。但我们使用直接计算的方法。 27. 由于YUV换公式有多种变体,我们使用上述公式(与常见软件一致)。 28. 最后,将Y和UV数据合并成NV12格式: NV12数据布局: [Y0, Y1, Y2, ..., Y_{width*height-1}, U0, V0, U1, V1, ...] 其中,UV数据来自uvImage纹理,每个纹素包含一个U和一个V(对应一个2x2块),按顺序排列(先U后V,然后下一个块的U、V)。 因此,我们读取Y平面数据(大小为width*height),然后读取UV平面数据(大小为width*height/2,因为每个纹素两个字节,共width/2 * height/2 * 2 = width*height/2字节)。然后,将UV数据直接附加在Y数据后面。 29. 但是,在OpenGL ES中,读取纹理数据时,Y平面是GL_RED格式,每个像素一个字节(GL_UNSIGNED_BYTE),所以读取到的大小为width*height。UV平面是GL_RG格式,每个像素两个字节(GL_UNSIGNED_BYTE),所以读取到的大小为 (width/2)*(height/2)*2 = width*height/2 字节。 30. 因此,整个NV12数据的大小为 width*height*3/2 字节。 31. 注意:上面的计算着色器代码中,我们使用了浮点运算,可能不是最高效的,但清晰。在实际应用中,可以使用整数运算或查找表优化。 32. 另外,我们假设输入纹理是RGBA8(每个通道8位)。如果输入是其他格式(如10位或浮点),则需要调整。 33. 最后,由于移动设备GPU的差异,性能可能需要优化(例如使用共享内存等)。 34. 以上代码只是一个示例,实际应用中可能需要测试和调整。 35. 如果设备不支持计算着色器,则需要使用片段着色器渲染到纹理的方式,将整个图像渲染到两个纹理(Y和UV),这需要两个渲染通道:第一个通道渲染Y平面(使用一个片段着色器,每个片段输出一个Y值),第二个通道渲染UV平面(使用一个片段着色器,每个片段处理一个2x2块,需要将顶点坐标按块划分)。但这样需要渲染一个四边形,其顶点数等于块数(即width/2 * height/2)。这比较复杂,这里不展开。 由于问题要求使用OpenGL ES 3.0,并且计算着色器在OpenGL ES 3.1中才正式支持(在3.0中是扩展),所以如果设备不支持计算着色器,则需要使用其他方法。 但是,我们这里假设支持计算着色器。 36. 代码中可能需要添加错误检查,确保纹理创建、着色器编译等成功。 37. 最后,给出完整的C++代码框架(省略了上下文创建等)和着色器代码。 注意:以上代码仅供参考,实际运行可能需要根据具体环境调整。 由于篇幅限制,这里只提供关键部分的代码。 如果有任何问题,请提问。 相关问题:</think>在 OpenGL ES 3.0 中实现 RGBA 到 NV12 的换,可以通过计算着色器高效完成。NV12 是一种 YUV 420 半平面格式,包含一个 Y 平面(亮度)和一个交错的 UV 平面(色度)。以下是完整的解决方案: ### 解决方案步骤 1. **创建输入/输出纹理**: - 输入:RGBA 纹理(`GL_RGBA8` 格式) - 输出:Y 平面纹理(`GL_R8` 格式)和 UV 平面纹理(`GL_RG8` 格式) 2. **计算着色器设计**: - 每个线程处理一个 2×2 像素块 - 计算每个像素的 Y 值(亮度) - 计算 2×2 像素块的平均 UV 值(色度) 3. **换公式**(基于 BT.601 标准): - `Y = 0.299 * R + 0.587 * G + 0.114 * B` - `U = -0.169 * R - 0.331 * G + 0.5 * B + 0.5` - `V = 0.5 * R - 0.419 * G - 0.081 * B + 0.5` 4. **执行流程**: - 绑定输入/输出纹理到图像单元 - 分派计算着色器(工作组大小 16×16) - 添加内存屏障确保数据同步 ### 完整代码实现 ```cpp // 创建纹理函数 GLuint createTexture(GLenum format, int width, int height) { GLuint tex; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexStorage2D(GL_TEXTURE_2D, 1, format, width, height); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); return tex; } // 主换函数 void RGBA_to_NV12(GLuint rgbaTex, int width, int height, GLuint& yTex, GLuint& uvTex) { // 创建输出纹理 yTex = createTexture(GL_R8, width, height); uvTex = createTexture(GL_RG8, width/2, height/2); // 创建计算着色器 const char* shaderSrc = R"( #version 310 es layout(local_size_x = 16, local_size_y = 16) in; layout(rgba8, binding=0) readonly uniform mediump image2D uInput; layout(r8, binding=1) writeonly uniform mediump image2D uYPlane; layout(rg8, binding=2) writeonly uniform mediump image2D uUVPlane; void main() { ivec2 blockCoord = ivec2(gl_GlobalInvocationID.xy); ivec2 baseCoord = blockCoord * 2; // 读取2x2像素块 vec4 p00 = imageLoad(uInput, baseCoord); vec4 p01 = imageLoad(uInput, baseCoord + ivec2(1, 0)); vec4 p10 = imageLoad(uInput, baseCoord + ivec2(0, 1)); vec4 p11 = imageLoad(uInput, baseCoord + ivec2(1, 1)); // 计算Y分量 float y00 = 0.299 * p00.r + 0.587 * p00.g + 0.114 * p00.b; float y01 = 0.299 * p01.r + 0.587 * p01.g + 0.114 * p01.b; float y10 = 0.299 * p10.r + 0.587 * p10.g + 0.114 * p10.b; float y11 = 0.299 * p11.r + 0.587 * p11.g + 0.114 * p11.b; imageStore(uYPlane, baseCoord, vec4(y00)); imageStore(uYPlane, baseCoord + ivec2(1, 0), vec4(y01)); imageStore(uYPlane, baseCoord + ivec2(0, 1), vec4(y10)); imageStore(uYPlane, baseCoord + ivec2(1, 1), vec4(y11)); // 计算平均RGB vec3 avg = 0.25 * (p00.rgb + p01.rgb + p10.rgb + p11.rgb); // 计算UV分量 float u = -0.169 * avg.r - 0.331 * avg.g + 0.5 * avg.b + 0.5; float v = 0.5 * avg.r - 0.419 * avg.g - 0.081 * avg.b + 0.5; imageStore(uUVPlane, blockCoord, vec4(u, v, 0, 0)); } )"; // 编译着色器(实际项目需添加错误检查) GLuint shader = glCreateShader(GL_COMPUTE_SHADER); glShaderSource(shader, 1, &shaderSrc, NULL); glCompileShader(shader); GLuint program = glCreateProgram(); glAttachShader(program, shader); glLinkProgram(program); glUseProgram(program); // 绑定纹理到图像单元 glBindImageTexture(0, rgbaTex, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); glBindImageTexture(1, yTex, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R8); glBindImageTexture(2, uvTex, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RG8); // 分派计算任务 int blockX = (width + 31) / 32; // 每个线程处理2像素,工作组16x16 int blockY = (height + 31) / 32; glDispatchCompute(blockX, blockY, 1); // 确保数据写入完成 glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); // 清理 glDeleteProgram(program); glDeleteShader(shader); } ``` ### 关键代码解释 1. **纹理创建**: - `createTexture` 函数创建指定格式的 2D 纹理 - Y 纹理:单通道(`GL_R8`),分辨率与输入相同 - UV 纹理:双通道(`GL_RG8`),分辨率宽高各减半 2. **计算着色器核心逻辑**: - **坐标计算**:`blockCoord` 标识当前处理的 2×2 块,`baseCoord` 是块内左上角像素坐标 - **Y 值计算**:对每个像素独立计算亮度值(公式基于 BT.601) - **UV 值计算**:取 2×2 像素块的平均 RGB,再计算色度值 - **存储操作**:`imageStore` 写入计算结果到指定图像 3. **执行配置**: - 工作组大小设为 16×16,平衡并行效率和资源占用 - `glDispatchCompute` 调用根据图像尺寸计算工作组数量 - `glMemoryBarrier` 确保着色器写入完成后再访问数据 4. **内存布局**: ```mermaid graph LR A[NV12 内存布局] --> B[Y 平面] A --> C[UV 平面] B --> D[width * height 字节] C --> E[width * height / 2 字节] E --> F[交错存储:U0, V0, U1, V1...] ``` ### 使用示例 ```cpp // 初始化 int width = 1920, height = 1080; GLuint rgbaTex = createTexture(GL_RGBA8, width, height); // (上传RGBA数据到rgbaTex...) // 执行换 GLuint yTex, uvTex; RGBA_to_NV12(rgbaTex, width, height, yTex, uvTex); // 读取结果 std::vector<uint8_t> nv12Data(width * height * 3 / 2); glBindTexture(GL_TEXTURE_2D, yTex); glGetTexImage(GL_TEXTURE_2D, 0, GL_RED, GL_UNSIGNED_BYTE, nv12Data.data()); glBindTexture(GL_TEXTURE_2D, uvTex); glGetTexImage(GL_TEXTURE_2D, 0, GL_RG, GL_UNSIGNED_BYTE, nv12Data.data() + width * height); ``` ### 性能优化建议 1. 使用 `glPixelStorei` 调整字节对齐减少内存拷贝 2. 对静态内容预编译着色器程序 3. 使用 `GL_HALF_FLOAT` 格式减少带宽(需支持扩展) 4. 添加异步 PBO 读取避免管道阻塞
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值