Android OpenGLES2.0开发(十一):渲染YUV
人生如逆旅,我亦是行人
- Android OpenGLES开发:EGL环境搭建
- Android OpenGLES2.0开发(一):艰难的开始
- Android OpenGLES2.0开发(二):环境搭建
- Android OpenGLES2.0开发(三):绘制一个三角形
- Android OpenGLES2.0开发(四):矩阵变换和相机投影
- Android OpenGLES2.0开发(五):绘制正方形和圆形
- Android OpenGLES2.0开发(六):着色器语言GLSL
- Android OpenGLES2.0开发(七):纹理贴图之显示图片
- Android OpenGLES2.0开发(八):Camera预览
- Android OpenGLES2.0开发(九):图片滤镜
- Android OpenGLES2.0开发(十):FBO离屏渲染
- Android OpenGLES2.0开发(十一):渲染YUV
引言
还记的我们在Android OpenGLES2.0开发(八):Camera预览章节显示Camera预览中提到的一种方式吗,渲染NV21数据,这种方式略复杂我们没有详细讲解。但是在音视频开发中,由于YUV数据量比RGB小很多常常用于传输,而我们拿到YUV数据会要求显示。本章我们就来详细介绍如何使用OpenGL高效渲染YUV数据。
YUV格式
YUV格式是一种颜色编码方法,常用于视频处理和压缩中,特别是在电视广播、视频会议、视频播放等领域。它将颜色信息分成了三个部分:亮度(Y)和色度(U和V)。
- Y (亮度): 表示图像的亮度信息,决定了图像的明暗。
- U (蓝色差): 表示蓝色与亮度的差异,记录蓝色通道的信息。
- V (红色差): 表示红色与亮度的差异,记录红色通道的信息。
由于Y分量与U、V分量是分开存储的,YUV格式可以有效地进行颜色压缩,特别适合视频传输和存储。
常见的几中YUV格式:
YUV420P
Y、U、V分布图如下,U和V分开存储,又称为I420格式,UV交换顺序后为YV12格式
I420:
YV12:
YUV420SP
该格式,UV交错分布,根据UV的先后顺序分为NV12和NV21(Android默认)
NV12:
NV21:
YUV和RGB转换
我们知道OpenGL纹理最终渲染的都是RGBA数据,因此我们需要将YUV转换为RGB。通用的转换公式如下:
R = Y + 1.402 * (V - 128)G = Y - 0.344136 * (U - 128) - 0.714136 * (V - 128)B = Y + 1.772 * (U - 128)
上面YUV转RGB的公式我使用了BT.601标准,实际上有多种标准,每种标准系数不同
- ITU-R BT.601(SDTV 标准,适用于 Android NV21)
- ITU-R BT.709(HDTV 标准,适用于 1080p 及以上视频)
- ITU-R BT.2020(UHDTV 标准,适用于 4K、8K 视频)
NV21转换示例
应该有很多人和我有一样的疑问,Y的数据量是UV的4倍,一个Y是如何和UV映射的呢,下面我们来举例说明。
假设我们有一个 4×4 的 Y 纹理(每个像素一个 Y 值),而 UV 纹理是 2×2,示意如下:
Y 纹理(4×4)
Y00 Y01 Y02 Y03
Y10 Y11 Y12 Y13
Y20 Y21 Y22 Y23
Y30 Y31 Y32 Y33
UV 纹理(2×2)
UV0 UV1
UV2 UV3
每个 UV 采样点对应 4 个 Y 像素:
(UV0) → {Y00, Y01, Y10, Y11}
(UV1) → {Y02, Y03, Y12, Y13}
(UV2) → {Y20, Y21, Y30, Y31}
(UV3) → {Y22, Y23, Y32, Y33}
但在 OpenGL 中,每个 Y 片段着色器 都需要一个 UV 值,而 UV 纹理比 Y 纹理小 4 倍(2x2),所以 OpenGL 会对 UV 进行插值。
OpenGL转换YUV
有了上面的理论基础,我们知道只需要将YUV数据传到OpenGL中,shader程序一个像素一个像素的转换即可。YUV数据如何传入到OpenGL中,答案是通过sampler2D纹理传递。我们又知道OpenGL中sampler2D纹理中有四个值RGBA,而YUV中Y只是单通道,I420中UV是单通道,NV12和NV21中UV交错存储可以理解为双通道。
那么现在的问题就是找到创建单通道和双通道的纹理了, OpenGL为我们提供了GL_LUMINANCE 和 GL_LUMINANCE_ALPHA 格式的纹理,其中 GL_LUMINANCE 纹理用来加载 NV21 Y Plane 的数据,GL_LUMINANCE_ALPHA 纹理用来加载 UV Plane 的数据。
GL_LUMINANCE:
单通道纹理,纹理对象中RGBA值都相同
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,GLES20.GL_LUMINANCE, width, height, 0,GLES20.GL_LUMINANCE,GLES20.GL_UNSIGNED_BYTE, imageData);
GL_LUMINANCE_ALPHA:
双通道纹理,UV存储到R和A值中
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,GLES20.GL_LUMINANCE_ALPHA, width, height, 0,GLES20.GL_LUMINANCE_ALPHA,GLES20.GL_UNSIGNED_BYTE, imageData
);
顶点着色器
顶点着色器代码没有变化,和之前Image中一样
// 顶点着色器代码
private final String vertexShaderCode ="uniform mat4 uMVPMatrix;\n" +// 顶点坐标"attribute vec4 vPosition;\n" +// 纹理坐标"attribute vec2 vTexCoordinate;\n" +"varying vec2 aTexCoordinate;\n" +"void main() {\n" +" gl_Position = uMVPMatrix * vPosition;\n" +" aTexCoordinate = vTexCoordinate;\n" +"}\n";
片段着色器代码
// 片段着色器代码
private final String fragmentShaderCode ="precision mediump float;\n" +"uniform sampler2D samplerY;\n" +"uniform sampler2D samplerU;\n" +"uniform sampler2D samplerV;\n" +"uniform sampler2D samplerUV;\n" +"uniform int yuvType;\n" +"varying vec2 aTexCoordinate;\n" +"void main() {\n" +" vec3 yuv;\n" +" if (yuvType == 0) {" +" yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +" yuv.y = texture2D(samplerU, aTexCoordinate).r - 0.5;\n" +" yuv.z = texture2D(samplerV, aTexCoordinate).r - 0.5;\n" +" } else if (yuvType == 1) {" +" yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +" yuv.y = texture2D(samplerUV, aTexCoordinate).r - 0.5;\n" +" yuv.z = texture2D(samplerUV, aTexCoordinate).a - 0.5;\n" +" } else {" +" yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +" yuv.y = texture2D(samplerUV, aTexCoordinate).a - 0.5;\n" +" yuv.z = texture2D(samplerUV, aTexCoordinate).r - 0.5;\n" +" }" +" vec3 rgb = mat3(1.0, 1.0, 1.0,\n" +" 0.0, -0.344, 1.772,\n" +" 1.402, -0.714, 0.0) * yuv;\n" +" gl_FragColor = vec4(rgb, 1);\n" +"}\n";
片段着色器中代码乍一看很复杂,待我来详细解释
sampler2D
我们上面声明了4个变量samplerY、samplerU、samplerV、samplerUV
- yuvType=0:YUV格式为I420,U和V是分开存储的,我们需要把U和V分别映射到不同的纹理中,用到samplerY、samplerU、samplerV
- yuvType=1:YUV格式为NV12,UV和交错存储的,UV映射到一个纹理上,用到samplerY、samplerUV
- yuvType=2:YUV格式为NV21,UV和交错存储的,UV映射到一个纹理上,用到samplerY、samplerUV
texture2D
texture2D方法我们在前面的章节应该很熟悉了,就是获取对应纹理坐标下的RGBA的值
- yuvType=0:YUV格式为I420,YUV都是分开存储,所以只需获取r就可以得到对应的YUV的值
- yuvType=1:YUV格式为NV12,Y和上面一样,UV交错存储,通过获取r和a可得到对应的UV
- yuvType=1:YUV格式为NV21,Y和上面一样,UV交错存储,通过获取a和r可得到对应的UV
计算RGB
vec3 rgb = mat3(1.0, 1.0, 1.0,
0.0, -0.344, 1.772,
1.402, -0.714, 0.0) * yuv;gl_FragColor = vec4(rgb, 1);
上面我们使用了矩阵乘法,其实和上面的提到的公式一样,如果你不熟悉这种方式,你可以分开计算:
float r = yuv.x + 1.402 * yuv.z;
float g = yuv.x - 0.344 * yuv.y - 0.714 * yuv.z;
float b = yuv.x + 1.772 * yuv.y;gl_FragColor = vec4(r, g, b, 1.0);
这两种方式的效果是一样的,为了计算效率我们只取float的后三位小数
YUVFilter
接下来我们看下YUVFilter的完整代码,这个类也是从之前Image拷贝而来,并做了修改如下:
public class YUVFilter {/*** 绘制的流程* 1.顶点着色程序 - 用于渲染形状的顶点的 OpenGL ES 图形代码* 2.片段着色器 - 用于渲染具有特定颜色或形状的形状的 OpenGL ES 代码纹理。* 3.程序 - 包含您想要用于绘制的着色器的 OpenGL ES 对象 一个或多个形状* <p>* 您至少需要一个顶点着色器来绘制形状,以及一个 fragment 着色器来为该形状着色。* 这些着色器必须经过编译,然后添加到 OpenGL ES 程序中,该程序随后用于绘制形状。*/// 顶点着色器代码private final String vertexShaderCode ="uniform mat4 uMVPMatrix;\n" +// 顶点坐标"attribute vec4 vPosition;\n" +// 纹理坐标"attribute vec2 vTexCoordinate;\n" +"varying vec2 aTexCoordinate;\n" +"void main() {\n" +" gl_Position = uMVPMatrix * vPosition;\n" +" aTexCoordinate = vTexCoordinate;\n" +"}\n";// 片段着色器代码private final String fragmentShaderCode ="precision mediump float;\n" +"uniform sampler2D samplerY;\n" +"uniform sampler2D samplerU;\n" +"uniform sampler2D samplerV;\n" +"uniform sampler2D samplerUV;\n" +"uniform int yuvType;\n" +"varying vec2 aTexCoordinate;\n" +"void main() {\n" +" vec3 yuv;\n" +" if (yuvType == 0) {" +" yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +" yuv.y = texture2D(samplerU, aTexCoordinate).r - 0.5;\n" +" yuv.z = texture2D(samplerV, aTexCoordinate).r - 0.5;\n" +" } else if (yuvType == 1) {" +" yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +" yuv.y = texture2D(samplerUV, aTexCoordinate).r - 0.5;\n" +" yuv.z = texture2D(samplerUV, aTexCoordinate).a - 0.5;\n" +" } else {" +" yuv.x = texture2D(samplerY, aTexCoordinate).r;\n" +" yuv.y = texture2D(samplerUV, aTexCoordinate).a - 0.5;\n" +" yuv.z = texture2D(samplerUV, aTexCoordinate).r - 0.5;\n" +" }" +" vec3 rgb = mat3(1.0, 1.0, 1.0,\n" +" 0.0, -0.344, 1.772,\n" +" 1.402, -0.714, 0.0) * yuv;\n" +" gl_FragColor = vec4(rgb, 1);\n" +"}\n";private int mProgram;// 顶点坐标缓冲区private FloatBuffer vertexBuffer;// 纹理坐标缓冲区private FloatBuffer textureBuffer;// 此数组中每个顶点的坐标数static final int COORDS_PER_VERTEX = 2;/*** 顶点坐标数组* 顶点坐标系中原点(0,0)在画布中心* 向左为x轴正方向* 向上为y轴正方向* 画布四个角坐标如下:* (-1, 1),(1, 1)* (-1,-1),(1,-1)*/private float vertexCoords[] = {-1.0f, 1.0f, // 左上-1.0f, -1.0f, // 左下1.0f, 1.0f, // 右上1.0f, -1.0f, // 右下};/*** 纹理坐标数组* 这里我们需要注意纹理坐标系,原点(0,0s)在画布左下角* 向左为x轴正方向* 向上为y轴正方向* 画布四个角坐标如下:* (0,1),(1,1)* (0,0),(1,0)*/private float textureCoords[] = {0.0f, 1.0f, // 左上0.0f, 0.0f, // 左下1.0f, 1.0f, // 右上1.0f, 0.0f, // 右下};private int positionHandle;// 纹理坐标句柄private int texCoordinateHandle;// Use to access and set the view transformationprivate int vPMatrixHandle;private IntBuffer mPlanarTextureHandles = IntBuffer.wrap(new int[3]);private int[] mSampleHandle = new int[3];private int mYUVTypeHandle;private final int vertexCount = vertexCoords.length / COORDS_PER_VERTEX;private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertexprivate int mTextureWidth;private int mTextureHeight;public YUVFilter() {// 初始化形状坐标的顶点字节缓冲区vertexBuffer = ByteBuffer.allocateDirect(vertexCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(vertexCoords);vertexBuffer.position(0);// 初始化纹理坐标顶点字节缓冲区textureBuffer = ByteBuffer.allocateDirect(textureCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(textureCoords);textureBuffer.position(0);}public void setTextureSize(int width, int height) {mTextureWidth = width;mTextureHeight = height;}public void surfaceCreated() {// 加载顶点着色器程序int vertexShader = GLESUtils.loadShader(GLES20.GL_VERTEX_SHADER,vertexShaderCode);// 加载片段着色器程序int fragmentShader = GLESUtils.loadShader(GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode);// 创建空的OpenGL ES程序mProgram = GLES20.glCreateProgram();// 将顶点着色器添加到程序中GLES20.glAttachShader(mProgram, vertexShader);// 将片段着色器添加到程序中GLES20.glAttachShader(mProgram, fragmentShader);// 创建OpenGL ES程序可执行文件GLES20.glLinkProgram(mProgram);// 获取顶点着色器vPosition成员的句柄positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");// 获取顶点着色器中纹理坐标的句柄texCoordinateHandle = GLES20.glGetAttribLocation(mProgram, "vTexCoordinate");// 获取绘制矩阵句柄vPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");// 获取yuvType句柄mYUVTypeHandle = GLES20.glGetUniformLocation(mProgram, "yuvType");// 生成YUV纹理句柄GLES20.glGenTextures(3, mPlanarTextureHandles);}public void surfaceChanged(int width, int height) {GLES20.glViewport(0, 0, width, height);}public void onDraw(float[] matrix, YUVFormat yuvFormat) {// 将程序添加到OpenGL ES环境GLES20.glUseProgram(mProgram);// 重新绘制背景色为黑色GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);// 为正方形顶点启用控制句柄GLES20.glEnableVertexAttribArray(positionHandle);// 写入坐标数据GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);// 启用纹理坐标控制句柄GLES20.glEnableVertexAttribArray(texCoordinateHandle);// 写入坐标数据GLES20.glVertexAttribPointer(texCoordinateHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, textureBuffer);// 将投影和视图变换传递给着色器GLES20.glUniformMatrix4fv(vPMatrixHandle, 1, false, matrix, 0);int yuvType = 0;// 设置yuvTypeif (yuvFormat == YUVFormat.I420) {yuvType = 0;} else if (yuvFormat == YUVFormat.NV12) {yuvType = 1;} else if (yuvFormat == YUVFormat.NV21) {yuvType = 2;}GLES20.glUniform1i(mYUVTypeHandle, yuvType);// yuvType: 0是I420,1是NV12int planarCount = 0;if (yuvFormat == YUVFormat.I420) {planarCount = 3;mSampleHandle[0] = GLES20.glGetUniformLocation(mProgram, "samplerY");mSampleHandle[1] = GLES20.glGetUniformLocation(mProgram, "samplerU");mSampleHandle[2] = GLES20.glGetUniformLocation(mProgram, "samplerV");} else {//NV12、NV21有两个平面planarCount = 2;mSampleHandle[0] = GLES20.glGetUniformLocation(mProgram, "samplerY");mSampleHandle[1] = GLES20.glGetUniformLocation(mProgram, "samplerUV");}for (int i = 0; i < planarCount; i++) {GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mPlanarTextureHandles.get(i));GLES20.glUniform1i(mSampleHandle[i], i);}// 绘制GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);// 禁用顶点阵列GLES20.glDisableVertexAttribArray(positionHandle);GLES20.glDisableVertexAttribArray(texCoordinateHandle);}public void release() {GLES20.glDeleteProgram(mProgram);mProgram = -1;}/*** 将图片数据绑定到纹理目标,适用于UV分量分开存储的(I420)** @param yPlane YUV数据的Y分量* @param uPlane YUV数据的U分量* @param vPlane YUV数据的V分量* @param width YUV图片宽度* @param height YUV图片高度*/public void feedTextureWithImageData(ByteBuffer yPlane, ByteBuffer uPlane, ByteBuffer vPlane, int width, int height) {//根据YUV编码的特点,获得不同平面的基址textureYUV(yPlane, width, height, 0);textureYUV(uPlane, width / 2, height / 2, 1);textureYUV(vPlane, width / 2, height / 2, 2);}/*** 将图片数据绑定到纹理目标,适用于UV分量交叉存储的(NV12、NV21)** @param yPlane YUV数据的Y分量* @param uvPlane YUV数据的UV分量* @param width YUV图片宽度* @param height YUV图片高度*/public void feedTextureWithImageData(ByteBuffer yPlane, ByteBuffer uvPlane, int width, int height) {//根据YUV编码的特点,获得不同平面的基址textureYUV(yPlane, width, height, 0);textureNV12(uvPlane, width / 2, height / 2, 1);}/*** 将图片数据绑定到纹理目标,适用于UV分量分开存储的(I420)** @param imageData YUV数据的Y/U/V分量* @param width YUV图片宽度* @param height YUV图片高度*/private void textureYUV(ByteBuffer imageData, int width, int height, int index) {// 将纹理对象绑定到纹理目标GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mPlanarTextureHandles.get(index));// 设置放大和缩小时,纹理的过滤选项为:线性过滤GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);// 设置纹理X,Y轴的纹理环绕选项为:边缘像素延伸GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);// 加载图像数据到纹理,GL_LUMINANCE指明了图像数据的像素格式为只有亮度,虽然第三个和第七个参数都使用了GL_LUMINANCE,// 但意义是不一样的,前者指明了纹理对象的颜色分量成分,后者指明了图像数据的像素格式// 获得纹理对象后,其每个像素的r,g,b,a值都为相同,为加载图像的像素亮度,在这里就是YUV某一平面的分量值GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,GLES20.GL_LUMINANCE, width, height, 0,GLES20.GL_LUMINANCE,GLES20.GL_UNSIGNED_BYTE, imageData);}/*** 将图片数据绑定到纹理目标,适用于UV分量交叉存储的(NV12、NV21)** @param imageData YUV数据的UV分量* @param width YUV图片宽度* @param height YUV图片高度*/private void textureNV12(ByteBuffer imageData, int width, int height, int index) {GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mPlanarTextureHandles.get(index));GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,GLES20.GL_LUMINANCE_ALPHA, width, height, 0,GLES20.GL_LUMINANCE_ALPHA,GLES20.GL_UNSIGNED_BYTE, imageData);}
}
具体改动点主要就是增加了根据YUV类型生成对应的纹理,并将纹理闯入OpenGL中
DisplayYUVGLSurfaceView
新建一个GLSurfaceView,在其中使用YUVFilter,完整代码如下:
public class DisplayYUVGLSurfaceView extends GLSurfaceView {private static final String TAG = DisplayYUVGLSurfaceView.class.getSimpleName();private Context mContext;private MyRenderer mMyRenderer;public DisplayYUVGLSurfaceView(Context context) {super(context);init(context);}public DisplayYUVGLSurfaceView(Context context, AttributeSet attrs) {super(context, attrs);init(context);}private void init(Context context) {mContext = context;mMyRenderer = new MyRenderer();setEGLContextClientVersion(2);setRenderer(mMyRenderer);setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);}public void feedYUVData(byte[] yuvData, int width, int height, YUVFormat yuvFormat, int rotate) {if (yuvData == null) {return;}mMyRenderer.feedData(yuvData, width, height, yuvFormat, rotate);requestRender();}public void setCameraId(int id) {mMyRenderer.setCameraId(id);}static class MyRenderer implements Renderer {private YUVFilter mYUVFilter;private YUVFormat mYUVFormat;private int mWidth;private int mHeight;// vPMatrix is an abbreviation for "Model View Projection Matrix"private float[] mMVPMatrix = new float[16];// y分量数据private ByteBuffer y = ByteBuffer.allocate(0);// u分量数据private ByteBuffer u = ByteBuffer.allocate(0);// v分量数据private ByteBuffer v = ByteBuffer.allocate(0);// uv分量数据private ByteBuffer uv = ByteBuffer.allocate(0);// 标识GLSurfaceView是否准备好private boolean hasVisibility = false;private boolean isMirror = false;private int mRotate;private int mCameraId;public MyRenderer() {mYUVFilter = new YUVFilter();}public void setCameraId(int cameraId) {mCameraId = cameraId;}@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {mYUVFilter.surfaceCreated();}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {mYUVFilter.surfaceChanged(width, height);hasVisibility = true;}@Overridepublic void onDrawFrame(GL10 gl) {synchronized (this) {if (y.capacity() > 0) {y.position(0);if (mYUVFormat == YUVFormat.I420) {u.position(0);v.position(0);mYUVFilter.feedTextureWithImageData(y, u, v, mWidth, mHeight);} else {uv.position(0);mYUVFilter.feedTextureWithImageData(y, uv, mWidth, mHeight);}MatrixUtils.getMatrix(mMVPMatrix, MatrixUtils.TYPE_FITXY, mWidth, mHeight, mWidth, mHeight);MatrixUtils.flip(mMVPMatrix, false, true);if (mCameraId == 1) {MatrixUtils.flip(mMVPMatrix, true, false);}MatrixUtils.rotate(mMVPMatrix, mRotate);try {long start = System.currentTimeMillis();mYUVFilter.onDraw(mMVPMatrix, mYUVFormat);Log.i(TAG, "drawTexture " + mWidth + "x" + mHeight + " 耗时:" + (System.currentTimeMillis() - start) + "ms");} catch (Exception e) {Log.w(TAG, e.getMessage());}}}}/*** 设置渲染的YUV数据的宽高** @param width 宽度* @param height 高度*/public void setYuvDataSize(int width, int height) {if (width > 0 && height > 0) {// 初始化容器if (width != mWidth || height != mHeight) {this.mWidth = width;this.mHeight = height;int yarraySize = width * height;int uvarraySize = yarraySize / 4;synchronized (this) {y = ByteBuffer.allocate(yarraySize);u = ByteBuffer.allocate(uvarraySize);v = ByteBuffer.allocate(uvarraySize);uv = ByteBuffer.allocate(uvarraySize * 2);}}}}public void feedData(byte[] yuvData, int width, int height, YUVFormat yuvFormat, int rotate) {setYuvDataSize(width, height);synchronized (this) {mWidth = width;mHeight = height;mYUVFormat = yuvFormat;mRotate = rotate;if (hasVisibility) {if (yuvFormat == YUVFormat.I420) {y.clear();u.clear();v.clear();y.put(yuvData, 0, width * height);u.put(yuvData, width * height, width * height / 4);v.put(yuvData, width * height * 5 / 4, width * height / 4);} else {y.clear();uv.clear();y.put(yuvData, 0, width * height);uv.put(yuvData, width * height, width * height / 2);}}}}}
}
OpenGLES渲染YUV数据主要涉及到YUV数据的处理和渲染过程。YUV是一种颜色编码方法,其中“Y”表示明亮度(Luminance或Luma),而“U”和“V”表示色度(Chrominance或Chroma),用于描述影像色彩及饱和度。YUV格式主要用于电视系统以及模拟视频领域,它允许降低色度的带宽,同时保持图片质量,提供传输效率。在OpenGLES中渲染YUV数据,通常涉及以下几个步骤:
显示Camera的YUV数据
我们使用了Camera系列中Camera2Manager类,通过他获取YUV数据
public class DisplayYUVActivity extends AppCompatActivity implements CameraCallback {private static final String TAG = DisplayYUVActivity.class.getSimpleName();private DisplayYUVGLSurfaceView mDisplayYUVGLSurfaceView;private ICameraManager mCameraManager;private int mCameraId = 1;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_display_yuvactivity);mDisplayYUVGLSurfaceView = findViewById(R.id.displayYUVGLView);mCameraManager = new Camera2Manager(this);mCameraManager.setCameraId(mCameraId);mCameraManager.setCameraCallback(this);mCameraManager.addPreviewBufferCallback(mPreviewBufferCallback);mDisplayYUVGLSurfaceView.setCameraId(mCameraId);}@Overrideprotected void onResume() {super.onResume();mCameraManager.openCamera();}@Overrideprotected void onPause() {super.onPause();mCameraManager.releaseCamera();}@Overridepublic void onOpen() {mCameraManager.startPreview((SurfaceTexture) null);}@Overridepublic void onOpenError(int error, String msg) {}@Overridepublic void onPreview(int previewWidth, int previewHeight) {}@Overridepublic void onPreviewError(int error, String msg) {}@Overridepublic void onClose() {}private PreviewBufferCallback mPreviewBufferCallback = new PreviewBufferCallback() {@Overridepublic void onPreviewBufferFrame(byte[] data, int width, int height, YUVFormat format) {mDisplayYUVGLSurfaceView.feedYUVData(data, width, height, format, mCameraManager.getOrientation());}};
}
最后
本章我们学习了如何将YUV原数据通过OpenGL显示,该方式通过OpenGL将YUV数据转换为RGB然后显示到屏幕,性能比用CPU转换好很多。
OpenGL ES系列:https://github.com/xiaozhi003/AndroidOpenGLDemo.git,如果对你有帮助可以star下,万分感谢^_^
相关文章:

Android OpenGLES2.0开发(十一):渲染YUV
人生如逆旅,我亦是行人 Android OpenGLES开发:EGL环境搭建Android OpenGLES2.0开发(一):艰难的开始Android OpenGLES2.0开发(二):环境搭建Android OpenGLES2.0开发(三&am…...
在linux中利用conda安装blast
在 Linux 中使用 conda 安装 BLAST 非常简单。conda 是一个流行的包管理工具,可以轻松安装和管理生物信息学工具,包括 BLAST。以下是具体步骤: 1. 确保已安装 Conda 如果你还没有安装 conda,可以参考以下步骤安装 Miniconda&…...
三、多项式环
文章目录 一、多项式环的定义二、多项式环的性质1. 多项式加法2. 多项式乘法3. 满足的运算规律4. 次数5. 单位元 三、剩余多项式环(商多项式环)四、有限多项式环五、多项式环的性质与特性1. 子环与理想2. 不可约性和素性3. 有限生成性 一、多项式环的定义…...
python unzip file
要在 Python 中解压文件并显示进度,我们需要在解压过程中跟踪文件的提取进度。由于 zipfile 模块本身不直接支持进度显示,我们可以通过手动计算并使用 tqdm 库来显示进度条。 安装 tqdm 首先,确保你已经安装了 tqdm 库,用于显示…...

MySQL-增删改查
一、Create(创建) 📖 语法: INSERT INTO table_name(value_list); 当我们使用表的时候,就可以使用这个语法来向表中插入元素~ 我们这边创建一个用于示范的表(Student)~ create table student( id int, name varchar(20), chinese int, math…...
LeetCode 热题100 15. 三数之和
LeetCode 热题100 | 15. 三数之和 大家好,今天我们来解决一道经典的算法题——三数之和。这道题在 LeetCode 上被标记为中等难度,要求我们从一个整数数组中找到所有不重复的三元组,使得三元组的和为 0。下面我将详细讲解解题思路,…...

网络空间安全(1)web应用程序的发展历程
前言 Web应用程序的发展历程是一部技术创新与社会变革交织的长卷,从简单的文档共享系统到如今复杂、交互式、数据驱动的平台,经历了多个重要阶段。 一、起源与初期发展(1989-1995年) Web的诞生: 1989年,欧洲…...

ABAQUS功能梯度材料FGM模型
功能梯度材料(FGM)作为一种新型复合材料,通过材料内部成分或微观结构的梯度变化,优化特定性能适应复杂环境,被广泛应用于高温防护、结构优化、生物医学、光电设备等领域。本案例介绍在ABAQUS内建立功能梯度材料模型。 …...
自适应增强技术
1. 传统图像处理中的自适应增强(如CLAHE) 难度:⭐容易 实现方式:调用成熟的库(如OpenCV)函数即可完成。 示例代码(CLAHE增强): <PYTHON> import cv2# 输入灰度或彩…...

虚拟项目:一个好用的工具平台
在当今数字化的时代,虚拟项目如雨后春笋般涌现,为人们提供了诸多便捷且充满机遇的选择。以下将为大家详细介绍几种颇具特色的虚拟项目,包括书签、资源站、题库、虚拟商城、专栏、证件照以及分站搭建等,一起来了解它们各自的独特之…...
MySQL 和 Elasticsearch 之间的数据同步
MySQL 和 Elasticsearch 之间的数据同步是常见的需求,通常用于将结构化数据从关系型数据库同步到 Elasticsearch 以实现高效的全文搜索、聚合分析和实时查询。以下是几种常用的同步方案及其实现方法: 1. 应用层双写(双写模式) 原…...
PS裁剪工具
裁剪: 多张图同一标准裁剪:裁剪–》前面的图像–》选择其他图像–》 确定 选区–》裁剪工具–》确定:选区制作矩形裁剪 裁剪–》拉直 裁剪–》内容识别:当裁剪大于图片大小,会自动填充空白区域 (栅格化图层…...

[Web 安全] PHP 反序列化漏洞 —— PHP 序列化 反序列化
关注这个专栏的其他相关笔记:[Web 安全] 反序列化漏洞 - 学习笔记-CSDN博客 0x01:PHP 序列化 — Serialize 序列化就是将对象的状态信息转化为可以存储或传输的形式的过程,在 PHP 中,通常使用 serialize() 函数来完成序列化的操作…...

QT入门--QMainWindow
从上向下依次是菜单栏,工具栏,铆接部件(浮动窗口),状态栏,中心部件 菜单栏 创建菜单栏 QMenuBar* mybar1 menuBar(); 将菜单栏放到窗口中 setMenuBar(mybar1); 创建菜单 QMenu *myfilemenu mybar1-…...
C++ | 高级教程 | 信号处理
👻 概念 信号 —— 操作系统传给进程的中断,会提早终止程序有些信号不能被程序捕获,有些则可以被捕获,并基于信号采取适当的动作 信号描述SIGABRT程序的异常终止,如调用 abortSIGFPE错误的算术运算,比如除…...
最新前端框架选型对比与建议(React/Vue/Svelte/Angular)
前端框架选型对比与建议(React/Vue/Svelte/Angular) 一、核心框架技术特性对比(基于最新版本) 维度React 19 25Vue 3.5 12Svelte 5 25Angular 19 5核心理念函数式编程、JSX语法、虚拟DOM渐进式框架、组合式API、模板语法编译时框…...

游戏引擎学习第123天
仓库:https://gitee.com/mrxiao_com/2d_game_3 黑板:线程同步/通信 目标是从零开始编写一个完整的游戏。我们不使用引擎,也不依赖任何库,完全自己编写游戏所需的所有代码。我们做这个节目不仅是为了教育目的,同时也是因为编程本…...
计算机网络:从底层原理到前沿应用,解锁数字世界的连接密码
计算机网络:从底层原理到前沿应用,解锁数字世界的连接密码 在信息如洪流般奔涌的时代,计算机网络宛如无形的脉络,贯穿于我们生活的每一个角落。它不仅是数据传输的通道,更是连接全球、驱动创新的核心力量。从日常的网络…...
grafana K6压测
文章目录 install and runscript.jsoptions最佳实践 report 解析 https://grafana.com/docs/k6/latest/get-started install and run install # mac brew install k6当前目录下生成压测脚本 # create file script.js k6 new [filename] # create file ‘script.js’ in …...
Vue的组合式API和选项式API有什么区别
Vue3的组合式API(Composition API)和选项式API(Options API)是两种不同的组件编写方式,主要区别如下: 1. 代码组织方式 选项式API: 按照选项(如data、methods、computed等࿰…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...

【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...

从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...

为什么需要建设工程项目管理?工程项目管理有哪些亮点功能?
在建筑行业,项目管理的重要性不言而喻。随着工程规模的扩大、技术复杂度的提升,传统的管理模式已经难以满足现代工程的需求。过去,许多企业依赖手工记录、口头沟通和分散的信息管理,导致效率低下、成本失控、风险频发。例如&#…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。
1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj,再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
以下是一个完整的 Angular 微前端示例,其中使用的是 Module Federation 和 npx-build-plus 实现了主应用(Shell)与子应用(Remote)的集成。 🛠️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...

以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...