网上关于如何使用OpenGL ES直接渲染NV21格式画面的文章寥寥无几,大多数都是直接用SurfaceTexture提供的纹理来渲染,本文处理的情况是Camera1的回调给了NV21的数组,编写一个滤镜来渲染出来。
理论部分
因为OpenGL要求的是rgb格式,所以需要将yuv转换成rgb格式,转换公式如下:
需要注意的是OpenGL ES的内置矩阵实际是一列一列构建的,比如YUV和RGB的转换矩阵构建是:
c
mat3 convertMat = mat3(1.0, 1.0, 1.0, //第一列
0.0,-0.338,1.732, //第二列
1.371,-0.698, 0.0);//第三列
在滤镜中需要创建两个纹理,textureY和textureUV,激活纹理时Y的format使用GL_LUMINANCE,UV使用GL_LUMINANCE_ALPHA。
滤镜编写
顶点着色器,常规写法即可。
c
#version 300 es
layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec2 aTexture;
out vec2 vTexture;
void main()
{
gl_Position = a_Position;
vTexture = aTexture;
}
片元着色器,主要做yuv到rgb的转换。
c
#version 300 es
precision mediump float;
out vec4 FragColor;
in vec2 vTexture;
uniform sampler2D textureY;
uniform sampler2D textureUV;
void main() {
//yuv转化得到的rgb向量数据
vec3 rgb;
vec3 yuv;
//分别取yuv各个分量的采样纹理
yuv.x = texture(textureY, vTexture).r;
yuv.y = texture(textureUV, vTexture).a - 0.5;
yuv.z = texture(textureUV, vTexture).r - 0.5;
//yuv转化为rgb
rgb = mat3(1.0, 1.0, 1.0,
0.0, -0.338, 1.732,
1.4075, -0.7169, 0.0)*yuv;
FragColor = vec4(rgb, 1.0);
}
滤镜类编写:
kotlin
class YuvOESFilter(context: Context): GLImageFilter(context) {
// 预览的宽高, camera一般宽高反过来,如 720*1280
private var mVideoWidth: Int = -1
private var mVideoHeight: Int = -1
// 纹理接收者
private var mTextureYHandler: Int = -1
private var mTextureUVHandler: Int = -1
// 两个纹理
private val textures = IntArray(2)
// 两个buffer
private var bufferY: ByteBuffer? = null
private var bufferUV: ByteBuffer? = null
}
在渲染之前必须要确定画面的宽高,为了确定buffer的大小。
kotlin
fun setVideoSize(videoW: Int, videoH: Int) {
mVideoWidth = videoW
mVideoHeight = videoH
}
每一帧回调时,设置buffer的数据。
kotlin
fun setNV21Data(data: ByteArray) {
try {
// 初始化
if (bufferY == null) {
bufferY = ByteBuffer.allocate(mVideoWidth * mVideoHeight)
bufferY!!.order(ByteOrder.nativeOrder())
}
if (bufferUV == null) {
bufferUV = ByteBuffer.allocate(mVideoWidth * mVideoHeight / 2)
bufferUV!!.order(ByteOrder.nativeOrder())
}
// Y的数据是宽*高
bufferY!!.put(data, 0, mVideoWidth * mVideoHeight)
bufferY!!.position(0)
// UV的数据是Y的一半
bufferUV!!.put(data, mVideoWidth * mVideoHeight, mVideoWidth * mVideoHeight / 2)
bufferUV!!.position(0)
} catch (e: Exception) {
e.printStackTrace()
}
}
接下来是渲染,设置顶点坐标、纹理坐标部分省略了,重点在于两个纹理的激活与绑定。
kotlin
override fun onDrawTexture(
textureId: Int,
vertexBuffer: FloatBuffer?,
textureBuffer: FloatBuffer?
) {
...
if (textures[0] == 0) {
// 普通的创建纹理逻辑
setTexture()
}
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
//激活指定纹理单元
GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
//绑定纹理ID到纹理单元
//y
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[0])
GLES30.glUniform1i(mTextureYHandler, 0)
GLES30.glTexImage2D(
GLES30.GL_TEXTURE_2D,
0,
GLES30.GL_LUMINANCE,
mVideoHeight,
mVideoWidth,
0,
GLES30.GL_LUMINANCE,
GLES30.GL_UNSIGNED_BYTE,
bufferY
)
GLES30.glActiveTexture(GLES30.GL_TEXTURE1)
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[1])
GLES30.glUniform1i(mTextureUVHandler, 1)
// 注意使用GLES30.GL_LUMINANCE_ALPHA
GLES30.glTexImage2D(
GLES30.GL_TEXTURE_2D,
0,
GLES30.GL_LUMINANCE_ALPHA,
mVideoHeight / 2,
mVideoWidth / 2,
0,
GLES30.GL_LUMINANCE_ALPHA,
GLES30.GL_UNSIGNED_BYTE,
bufferUV
)
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, mVertexCount)
// 解绑
GLES30.glDisableVertexAttribArray(mPositionHandle)
GLES30.glDisableVertexAttribArray(mTextureCoordinateHandle)
GLES30.glBindTexture(textureType, 0)
}
override fun release() {
super.release()
// GLES20.glDeleteProgram(mProgramHandle);
bufferY?.clear()
bufferUV?.clear()
}
这样就完成了渲染NV21的核心逻辑,每帧回调时触发一次滤镜的draw即可实现预览。