Opengl ES之YUV数据渲染

Posted FlyerGo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Opengl ES之YUV数据渲染相关的知识,希望对你有一定的参考价值。

YUV回顾

记得在音视频基础知识介绍中,笔者专门介绍过YUV的相关知识,可以参考:
《音视频基础知识-YUV图像》

YUV数据量相比RGB较小,因此YUV适用于传输,但是YUV图不能直接用于显示,需要转换为RGB格式才能显示,因而YUV数据渲染实际上就是使用Opengl ES将YUV数据转换程RGB数据,然后显示出来的过程。

也就是说Opengl ES之所以能渲染YUV数据其实就是使用了Opengl强大的并行计算能力,快速地将YUV数据转换程了RGB数据。

本文首发于微信公总号号:思想觉悟

更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟

YUV的格式比较多,我们今天就以YUV420SP为例,而YUV420SP又分为NV12NV21两种,因此今天我们的主题就是如何使用Opengl ES对NV12NV21数据进行渲染显示。

在着色器中使用texture2D对YUV数据进行归一化处理后Y数据的映射范围是0到1,而U和V的数据映射范围是-0.5到0.5。

因为YUV格式图像 UV 分量的默认值分别是 127 ,Y 分量默认值是 0 ,8 个 bit 位的取值范围是 0 ~ 255,由于在 shader 中纹理采样值需要进行归一化,所以 UV 分量的采样值需要分别减去 0.5 ,确保 YUV 到 RGB 正确转换。

YUV数据准备

首先我们可以使用ffmpeg命令行将一张png图片转换成YUV格式的图片:

ffmpeg -i 图片名称.png -s 图片宽x图片高 -pix_fmt nv12或者nv21 输出名称.yuv)

通过上面这个命令行我们就可以将一张图片转换成yuv格式的图片,此时我们可以使用软件YUVViewer看下你转换的图片对不对,如果本身转换出来的图片就是错的,那么后面的程序就白搭了…

注意:转换图片的宽高最好是2的幂次方,笔者测试了下发现如果宽高不是2的幂次方的话虽然能正常转换,但是查看的时候要么有色差,要么有缺陷,也有可能正常。

又或者你可以极客一点,直接使用ffmpeg代码解码视频的方式获得YUV数据并保存,这个可以参考笔者之前的文章:

《FFmpeg连载3-视频解码》

同时在上面的文章中笔者也介绍了通过ffplay命令行的方式查看YUV数据的方法。

YUV数据渲染

YUV 渲染步骤:

  • 生成 2 个纹理,分别用于承载Y数据和UV数据,编译链接着色器程序;

NV21和NV12格式的YUV数据是只有两个平面的,它们的排列顺序是YYYY UVUV或者YYYY VUVU因此我们的片元着色器需要两个纹理采样。

  • 确定纹理坐标及对应的顶点坐标;
  • 分别加载 NV21 的两个 Plane 数据到 2 个纹理,加载纹理坐标和顶点坐标数据到着色器程序;
  • 绘制。

YUV与RGB的转换格式图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cpiIVQPw-1669172740519)(https://flyer-blog.oss-cn-shenzhen.aliyuncs.com/YUV%E8%BD%ACRGB%E5%85%AC%E5%BC%8F.png)]

在OpenGLES的内置矩阵实际上是一列一列地构建的,比如YUV和RGB的转换矩阵的构建是:

// 标准转换,舍弃了部分小数精度
mat3 convertMat = mat3(1.0, 1.0, 1.0,      //第一列
                       0.0,-0.338,1.732, //第二列
                       1.371,-0.698, 0.0);//第三列

OpenGLES 实现 YUV 渲染需要用到 GL_LUMINANCE 和 GL_LUMINANCE_ALPHA 格式的纹理,其中 GL_LUMINANCE 纹理用来加载 NV21 Y Plane 的数据,GL_LUMINANCE_ALPHA 纹理用来加载 UV Plane 的数据。

废话少说,show me the code

YUVRenderOpengl.h

#ifndef NDK_OPENGLES_LEARN_YUVRENDEROPENGL_H
#define NDK_OPENGLES_LEARN_YUVRENDEROPENGL_H
#include "BaseOpengl.h"

class YUVRenderOpengl: public BaseOpengl

public:
    YUVRenderOpengl();

    virtual ~YUVRenderOpengl();

    virtual void onDraw() override;

    // 设置yuv数据
    virtual void setYUVData(void *y_data,void *uv_data, int width, int height, int yuvType);

private:
    GLint positionHandle-1;
    GLint textureHandle-1;
    GLint y_textureSampler-1;
    GLint uv_textureSampler-1;
    GLuint y_textureId0;
    GLuint uv_textureId0;
;

#endif //NDK_OPENGLES_LEARN_YUVRENDEROPENGL_H

YUVRenderOpengl.cpp


#include "YUVRenderOpengl.h"

#include "../utils/Log.h"

// 顶点着色器
static const char *ver = "#version 300 es\\n"
                         "in vec4 aPosition;\\n"
                         "in vec2 aTexCoord;\\n"
                         "out vec2 TexCoord;\\n"
                         "void main() \\n"
                         "  TexCoord = aTexCoord;\\n"
                         "  gl_Position = aPosition;\\n"
                         "";

// 片元着色器 nv12
//static const char *fragment = "#version 300 es\\n"
//                              "precision mediump float;\\n"
//                              "out vec4 FragColor;\\n"
//                              "in vec2 TexCoord;\\n"
//                              "uniform sampler2D y_texture; \\n"
//                              "uniform sampler2D uv_texture;\\n"
//                              "void main()\\n"
//                              "\\n"
//                              "vec3 yuv;\\n"
//                              "yuv.x = texture(y_texture, TexCoord).r;\\n"
//                              "yuv.y = texture(uv_texture, TexCoord).r-0.5;\\n"
//                              "yuv.z = texture(uv_texture, TexCoord).a-0.5;\\n"
//                              "vec3 rgb =mat3( 1.0,1.0,1.0,\\n"
//                              "0.0,-0.344,1.770,1.403,-0.714,0.0) * yuv;\\n"
//                              "FragColor = vec4(rgb, 1);\\n"
//                              "";

/**
 *  仅仅是以下两句不同而已
 *  "yuv.y = texture(uv_texture, TexCoord).r-0.5;\\n"
 *  "yuv.z = texture(uv_texture, TexCoord).a-0.5;\\n"
 */
// 片元着色器nv21 仅仅是
static const char *fragment = "#version 300 es\\n"
                              "precision mediump float;\\n"
                              "out vec4 FragColor;\\n"
                              "in vec2 TexCoord;\\n"
                              "uniform sampler2D y_texture; \\n"
                              "uniform sampler2D uv_texture;\\n"
                              "void main()\\n"
                              "\\n"
                              "vec3 yuv;\\n"
                              "yuv.x = texture(y_texture, TexCoord).r;\\n"
                              "yuv.y = texture(uv_texture, TexCoord).a-0.5;\\n"
                              "yuv.z = texture(uv_texture, TexCoord).r-0.5;\\n"
                              "vec3 rgb =mat3( 1.0,1.0,1.0,\\n"
                              "0.0,-0.344,1.770,1.403,-0.714,0.0) * yuv;\\n"
                              "FragColor = vec4(rgb, 1);\\n"
                              "";

// 使用绘制两个三角形组成一个矩形的形式(三角形带)
// 第一第二第三个点组成一个三角形,第二第三第四个点组成一个三角形
const static GLfloat VERTICES[] = 
        0.5f,-0.5f, // 右下
        0.5f,0.5f, // 右上
        -0.5f,-0.5f, // 左下
        -0.5f,0.5f // 左上
;

// 贴图纹理坐标(参考手机屏幕坐标系统,原点在左上角)
//由于对一个OpenGL纹理来说,它没有内在的方向性,因此我们可以使用不同的坐标把它定向到任何我们喜欢的方向上,然而大多数计算机图像都有一个默认的方向,它们通常被规定为y轴向下,X轴向右
const static GLfloat TEXTURE_COORD[] = 
        1.0f,1.0f, // 右下
        1.0f,0.0f, // 右上
        0.0f,1.0f, // 左下
        0.0f,0.0f // 左上
;

YUVRenderOpengl::YUVRenderOpengl() 
    initGlProgram(ver,fragment);
    positionHandle = glGetAttribLocation(program,"aPosition");
    textureHandle = glGetAttribLocation(program,"aTexCoord");
    y_textureSampler = glGetUniformLocation(program,"y_texture");
    uv_textureSampler = glGetUniformLocation(program,"uv_texture");
    LOGD("program:%d",program);
    LOGD("positionHandle:%d",positionHandle);
    LOGD("textureHandle:%d",textureHandle);
    LOGD("y_textureSampler:%d",y_textureSampler);
    LOGD("uv_textureSampler:%d",uv_textureSampler);


YUVRenderOpengl::~YUVRenderOpengl() 



void YUVRenderOpengl::setYUVData(void *y_data, void *uv_data, int width, int height, int yuvType) 

    // 准备y数据纹理
    glGenTextures(1, &y_textureId);
    glActiveTexture(GL_TEXTURE2);
    glUniform1i(y_textureSampler, 2);

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, y_textureId);
    // 为当前绑定的纹理对象设置环绕、过滤方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, y_data);
    // 生成mip贴图
    glGenerateMipmap(GL_TEXTURE_2D);

    glBindTexture(GL_TEXTURE_2D, y_textureId);
    // 解绑定
    glBindTexture(GL_TEXTURE_2D, 0);

    // 准备uv数据纹理
    glGenTextures(1, &uv_textureId);
    glActiveTexture(GL_TEXTURE3);
    glUniform1i(uv_textureSampler, 3);

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, uv_textureId);
    // 为当前绑定的纹理对象设置环绕、过滤方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // 注意宽高
    // 注意要使用 GL_LUMINANCE_ALPHA
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, width/2, height/2, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, uv_data);
    // 生成mip贴图
    glGenerateMipmap(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, uv_textureId);
    // 解绑定
    glBindTexture(GL_TEXTURE_2D, 0);


void YUVRenderOpengl::onDraw() 

    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);

    // 激活纹理
    glActiveTexture(GL_TEXTURE2);
    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, y_textureId);
    glUniform1i(y_textureSampler, 2);

    // 激活纹理
    glActiveTexture(GL_TEXTURE3);
    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, uv_textureId);
    glUniform1i(uv_textureSampler, 3);

    /**
     * size 几个数字表示一个点,显示是两个数字表示一个点
     * normalized 是否需要归一化,不用,这里已经归一化了
     * stride 步长,连续顶点之间的间隔,如果顶点直接是连续的,也可填0
     */
    // 启用顶点数据
    glEnableVertexAttribArray(positionHandle);
    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,0,VERTICES);

    // 纹理坐标
    glEnableVertexAttribArray(textureHandle);
    glVertexAttribPointer(textureHandle,2,GL_FLOAT,GL_FALSE,0,TEXTURE_COORD);

    // 4个顶点绘制两个三角形组成矩形
    glDrawArrays(GL_TRIANGLE_STRIP,0,4);

    glUseProgram(0);

    // 禁用顶点
    glDisableVertexAttribArray(positionHandle);
    if(nullptr != eglHelper)
        eglHelper->swapBuffers();
    

    glBindTexture(GL_TEXTURE_2D, 0);

注意看着色器代码的注释,NV12和NV21的渲染仅仅是着色器代码有细小差别而已。

YUVRenderActivity.java

public class YUVRenderActivity extends BaseGlActivity 

    // 注意改成你自己图片的宽高
    private int yuvWidth = 640;
    private int yuvHeight = 428;

    private String nv21Path;
    private String nv12Path;
    private Handler handler = new Handler(Looper.getMainLooper());

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        // 注意申请磁盘写权限
        // 拷贝资源
        nv21Path = getFilesDir().getAbsolutePath() + "/nv21.yuv";
        FileUtils.copyAssertToDest(this,"nv21.yuv",nv21Path);

        nv12Path = getFilesDir().getAbsolutePath() + "/nv12.yuv";
        FileUtils.copyAssertToDest(this,"nv12.yuv",nv12Path);
    

    @Override
    public BaseOpengl createOpengl() 
        YUVRenderOpengl yuvRenderOpengl = new YUVRenderOpengl();
        return yuvRenderOpengl;
    

    @Override
    protected void onResume() 
        super.onResume();
        handler.postDelayed(new Runnable() 
            @Override
            public void run() 
                // 注意nv12和nv21的偏远着色器有点不一样的,需要手动改下调试  YUVRenderOpengl.cpp
//                if(!TextUtils.isEmpty(nv12Path))
//                    loadYuv(nv12Path,BaseOpengl.YUV_DATA_TYPE_NV12);
//                

                if(!TextUtils.isEmpty(nv21Path))
                    loadYuv(nv21Path,BaseOpengl.YUV_DATA_TYPE_NV21);
                
            
        ,200);
    

    @Override
    protected void onStop() 
        handler.removeCallbacksAndMessages(null);
        super.onStop();
    

    private void loadYuv(String path,int yuvType)
        try 
            InputStream inputStream = new FileInputStream(new File(path));
            Log.v("fly_learn_opengl","---length:" + inputStream.available());
            byte[] yData = new byte[yuvWidth * yuvHeight];
            inputStream.read(yData,0,yData.length);
            byte[] uvData = new byte[yuvWidth * yuvHeight / 2];
            inputStream.read(uvData,0,uvData.length);
            Log.v("fly_learn_opengl","---read:" + (yData.length + uvData.length) + "available:" + inputStream.available());
            myGLSurfaceView.setYuvData(yData,uvData,yuvWidth,yuvHeight);
         catch (Exception e) 
            e.printStackTrace();
        
    

这个主要看懂loadYuv方法,对于YUV数据的读取即可。

思考

都说YUV的格式较多,本文我们介绍了如何使用Opengl ES渲染YUV420SP数据,那么对于YUV420P数据,使用Opengl ES如何渲染呢?欢迎关注评论解答交流。

专栏系列

Opengl ES之EGL环境搭建
Opengl ES之着色器
Opengl ES之三角形绘制
Opengl ES之四边形绘制
Opengl ES之纹理贴图
Opengl ES之VBO和VAO
Opengl ES之EBO
Opengl ES之FBO
Opengl ES之PBO

关注我,一起进步,人生不止coding!!!

Android OpenGL ES 学习 –渲染YUV视频以及视频抖音特效

OpenGL 学习教程
Android OpenGL ES 学习(一) – 基本概念
Android OpenGL ES 学习(二) – 图形渲染管线和GLSL
Android OpenGL ES 学习(三) – 绘制平面图形
Android OpenGL ES 学习(四) – 正交投影
Android OpenGL ES 学习(五) – 渐变色
Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序
Android OpenGL ES 学习(七) – 纹理
Android OpenGL ES 学习(八) –矩阵变换
Android OpenGL ES 学习(九) – 坐标系统和。实现3D效果
Android OpenGL ES 学习(十) – GLSurfaceView 源码解析GL线程以及自定义 EGL
Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效

代码工程地址: https://github.com/LillteZheng/OpenGLDemo.git

之前学习到的图片,这一章,我们使用OpenGL 来解析 yuv 视频,并实现一些效果,废话不多说,先上效果:


当然,在进入主题之前,先学习一些基础知识。

一. 什么是 YUV

在说 yuv 之前,就不得不说 RGB 图像空间,顾名思义,RGB 是值图像的每一个像素都有 R、G,B 三个值,且三个值一次排列存储;但不一定说一定是按照 R,G,B 顺序排列,也可以是 B,G,R 这样的顺序。其中 R,G,B 的位深为 8 bit。

我们常见的图片处理,都是用 R,G,B 的图像格式,比如bitmap,比如图像的存储,基本使用 R,G,B

1.1 那为什么还有 yuv 呢?

我们知道,视频是由一张张图片组成,假设有一个 1920 * 1080 分辨率、帧率为60帧的视频,如果不进行压缩处理,并且使用RGB进行存储的话,仅仅一分钟的视频就能达到 ( 1920 * 1080 * 8 * 60 * 60 )bit (约等于56G),这显然是很夸张的。
但R,G,B这三个颜色是彼此是由相关性的,不利于编码压缩,所以,我们需要另外一种图像格式,来解决图像压缩问题,这个时候,yuv 就被提升来了。

yuv 图像格式将亮度信息 Y 和 色彩信息 UV 分离开来,Y 表示亮度,是图像的总体轮廓,即我们常说的灰度值,UV 表示色度,主要描绘图像的色彩信息,即颜色饱和度。如下图(图片来源wiki百科):

yuv 最早用于电视系统和模拟视频领域,它兼容了黑白电视和彩色电视,如果你家有vcd,dvd 这种设备,就会发现有 YCbCr(YUV) 这种接口,如果是黑白电视,值需要接入Y分量即可。

从很早的时候,人们就发现,人类对亮度信息比较敏感,而对色彩信息不那么敏感,比如我们降低一些颜色值,并不影响人对这张图像感官。因此,yuv 的编码压缩,又可以分为 YUV 4:4:4、YUV 4:2:2、YUV 4:2:0 这几种常用的类型

1.2 YUV 格式

YUV 4:4:4、YUV 4:2:2、YUV 4:2:0,指的是U,V 分量像素点的个数和采集方式,其中又以 YUV 4:2:0 最为常用。


可以这样简单理解:

  • YUV 4:4:4:每一个 Y 就对应一个 U 和一个 V分量
  • YUV 4:2:2:每两个 Y 共用一个 U、一个 V 分量
  • YUV 4:2:0:每四个 Y 共用一个 U、V分量

如下图(图片来源极客时间):


其中,YUV 又有不同的存储方式:

  • packed :packed格式是先连续存储所有的Y分量,然后依次交叉储存U、V分量;
  • planar:planar格式也会先连续存储所有的Y分量,但planar会先连续存储U分量的数据,再连续存储V分量的数据,或者先连续存储V分量的数据,再连续存储U分量的数据:

更多内容和YUV转RGB,可以参考:https://time.geekbang.org/column/article/449795

二. 视频解析

从之前OpenGL 的纹理教程中,我们是把一张图片,通过纹理的方式,传递给片段着色器,最终通过纹理采样,复制给片段颜色值,呈现出来的。
现在使用 YUV ,该如何处理呢?我们知道,视频最终的呈现还是RGB格式的数据,因此,我们需要把 YUV 的数据,所以需要在片段着色器赋值之前,把YUV转换成 RGB。

2.1 GL_LUMINANCE

在OpenGL 的api 中,可以发现有个 GL_LUMINANCE 格式,它表示只取一个颜色通道,这样的话,就可以把 YUV 拆分成3个通道来读取,然后我们设置 3个纹理,把 YUV 数据传入其中,并最终把这三个通道合并在一起。

2.2 获取 YUV 视频

为了方便演示,我们使用 YUV420P 的视频,即4个Y共用一个U,V 分量,且存储是先存储Y,然后是U,最后再存储V分量。
这里我们可以用 ffmepg 的命令,轻松把一个 MP4 的视频转换成 YUV,由于 YUV 比较大,记得修改分辨率,这样小一些:

ffmpeg -i input.mp4 -s 288x512 -r 30 -pix_fmt yuv420p out.yuv

2.3 读取 yuv 文件数据

之后,就可以通过不断读取这个yuv文件,拿到y,u,v的数据,假设视频大小为 wxh ,则先读取 wh 个y,再读取 wh/4 个u,再读取 w*h/4 个 v;一帧读取完后,就进行渲染,然后再重复操作,直到文件被读取完毕。
我们把文件放在 assert 文件夹下:

 /**
  * 读取yuv数据,注意 w,h 为视频宽高
  */
 private fun readYuvData(w: Int, h: Int) 
     val input = context.resources.assets.open(YUV_FILE)
     //视频时 yuv420p ,4 个 y 共用一个 uv,先存储y,再u,和v
     val y = ByteArray(w * h)
     val u = ByteArray(w * h / 4)
     val v = ByteArray(w * h / 4)

     while (true) 
         if (isExit) 
             Log.d(TAG, "readYuvData,手动退出")
             return
         
         val readY = input.read(y)
         val readU = input.read(u)
         val readV = input.read(v)
         //都读到分量
         if (readY > 0 && readU > 0 && readV > 0) 
             //从这里触发刷新
             bufferY = ByteBuffer.wrap(y)
             bufferU = ByteBuffer.wrap(u)
             bufferV = ByteBuffer.wrap(v)

             val glView = view as GLSurfaceView
             //主动触发刷新
             glView.requestRender()
             //延时30ms,控制速度
             Thread.sleep(30)

          else 
             Log.d(TAG, "readYuvData,文件末尾,退出")
             return
         
     
 

2.4 着色器编写

顶点着色器,沿用上一章,不需要改变,但是我们把位置改一下,让它填充整个屏幕:

private val POINT_RECT_DATA2 = floatArrayOf(
    // positions           // texture coords
    1f,  1f, 0.0f, 1.0f, 0.0f, // top right
    1f, -1f, 0.0f, 1.0f, 1.0f, // bottom right
   -1f, -1f, 0.0f, 0.0f, 1.0f, // bottom left
   -1f,  1f, 0.0f, 0.0f, 0.0f  // top left
)

片段着色中,设置三个纹理,用来读取 yuv分量的数据:

private const val FRAGMENT_SHADER = """#version 300 es
    precision mediump float;
    out vec4 FragColor;
    in vec2 vTexture;
    uniform sampler2D textureY;
    uniform sampler2D textureU;
    uniform sampler2D textureV;
    void main() 
        //采样到的yuv向量数据  
         float y,u,v;
        //yuv转化得到的rgb向量数据
        vec3 rgb;
        //分别取yuv各个分量的采样纹理
        y = texture(textureY, vTexture).r;
        u = texture(textureU, vTexture).g - 0.5;
        v = texture(textureV, vTexture).b - 0.5;
        //yuv转化为rgb, https://en.wikipedia.org/wiki/YUV
        rgb.r = y + 1.540*v;
        rgb.g = y - 0.183*u - 0.459*v;
        rgb.b = y + 1.818*u;
        FragColor = vec4(rgb, 1.0);
    
    
"""

可以看到,我们使用了三个纹理textureY,textureU,textureV,然后用了三个变量 y,u,v 用来接收纹理数据。
前面说到,OpenGL 的分量,除了包含位置信息x,y,z,w,还有颜色(r,g,b,a)和纹理信息(s,t,r,q):

  • x,y,z,w: 与位置相关的分量
  • r,g,b,a: 与颜色相关的分量
  • s,t,p,q: 与纹理坐标相关的分量

当我们设置 sampler2D 的类型为 GL_LUMINANCE,所以 texture().r 拿到的是yuv 的第一个颜色向量的第一个分量信息,就是y;

那这个 0.5 是什么?为啥要减去它?
先看到YUV与RGB 的转换公司,这里用高清模式(BT709),颜色空间为 Limited Range 的转换公式:(图片来源)

可以看到,有个转换偏差值,而 U,V 默认是127 ,Y 的偏移量为0。8 个 bit 位的取值范围是 0 ~ 255,由于在 shader 中纹理采样值需要进行归一化(注意,纹理的范围是[0,1]),所以 UV 分量的采样值需要分别减去 0.5 ,确保 YUV 到 RGB 正确转换。

2.5 纹理加载

编写完着色器,就可以编写纹理对象了。首先,设置纹理的下标:

private val textures = IntArray(3)
 //三个纹理,需要设置纹理的下标
GLES30.glUniform1i(GLES30.glGetUniformLocation(programId, "textureY"), 0)
GLES30.glUniform1i(GLES30.glGetUniformLocation(programId, "textureU"), 1)
GLES30.glUniform1i(GLES30.glGetUniformLocation(programId, "textureV"), 2)

设置纹理的对象:


GLES30.glGenTextures(3, textures, 0)
for (i in 0..2) 
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[i])

    //纹理环绕
    GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_REPEAT)
    GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_REPEAT)

    //纹理过滤
    GLES30.glTexParameteri(
        GLES30.GL_TEXTURE_2D,
        GLES30.GL_TEXTURE_MIN_FILTER,
        GLES30.GL_NEAREST
    )
    GLES30.glTexParameteri(
        GLES30.GL_TEXTURE_2D,
        GLES30.GL_TEXTURE_MAG_FILTER,
        GLES30.GL_LINEAR
    )

    //解绑纹理对象
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0)

前面2.3章节,已经拿到了 yuv 的数据,这里,我们使用 glTexImage2D 把数据设置给纹理:

    override fun onDrawFrame(gl: GL10?) 
        //步骤1:使用glClearColor设置的颜色,刷新Surface
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)

        //使用 y 数据
        GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[0])
        GLES30.glTexImage2D(
            GLES30.GL_TEXTURE_2D,
            0,
            GLES30.GL_LUMINANCE,
            w,
            h,
            0,
            GLES30.GL_LUMINANCE,
            GLES30.GL_UNSIGNED_BYTE,
            bufferY
            )
        //使用 u 数据
        GLES30.glActiveTexture(GLES30.GL_TEXTURE1)
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[1])
        GLES30.glTexImage2D(
            GLES30.GL_TEXTURE_2D,
            0,
            GLES30.GL_LUMINANCE,
            w / 2,
            h / 2,
            0,
            GLES30.GL_LUMINANCE,
            GLES30.GL_UNSIGNED_BYTE,
            bufferU
        )
        //使用 v 数据
        GLES30.glActiveTexture(GLES30.GL_TEXTURE2)
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[2])
        GLES30.glTexImage2D(
            GLES30.GL_TEXTURE_2D,
            0,
            GLES30.GL_LUMINANCE,
            w / 2,
            h / 2,
            0,
            GLES30.GL_LUMINANCE,
            GLES30.GL_UNSIGNED_BYTE,
            bufferV
        )
        GLES30.glBindVertexArray(vao[0])
        GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP, 6, GLES30.GL_UNSIGNED_INT, 0)
        bufferY?.clear()
        bufferU?.clear()
        bufferV?.clear()
    

效果如下:

三.加抖音特效

从纹理那张可知
Android OpenGL ES 学习(七) – 纹理
我们可以通过:

FragColor = mix(texture1,texture2,0.5);

的方式去添加纹理的效果。因此,我们也可以修改片段着色器中的 RGB 数据,实现不同的效果。

3.1 灰度

比如灰度,只需要修改rgb的颜色,即可,你可以把 u,v 分量去掉:

// u = texture(textureU, vTexture).g - 0.5;
// v = texture(textureV, vTexture).b - 0.5;
 u = 0.0;
 v = 0.0;

也可以使用算法:

float gray = rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722;
FragColor = vec4(gray,gray,gray, 1.0);

得到灰度视频:

3.2 颜色反转

让每个颜色值反转,我们只需要1 - 颜色值即可:

rgb.r = 1.0 - (y + 1.540*v);
rgb.g = 1.0 - (y - 0.183*u - 0.459*v);
rgb.b = 1.0 - (y + 1.818*u);
FragColor = vec4(rgb, 1.0);

3.3 对称不同颜色值

什么意思呢?就是让左上角颜色反转,右下角灰色,其他区域正常显示,如下视频:

思路是对y进行分割,取中间(0.5,0.5)作为分割点:

if(vTexture.x <= 0.5 && vTexture.y <= 0.5)
    //左上角,使用反色
    float r = 1.0 - rgb.r;
    float g = 1.0 - rgb.g;
    float b = 1.0 - rgb.b;
    FragColor = vec4(r,g,b, 1.0);
else if(vTexture.x > 0.5 && vTexture.y > 0.5)
   
     //右下角,使用灰度
    float gray = rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722;
    FragColor = vec4(gray,gray,gray, 1.0);
else
    FragColor = vec4(rgb, 1.0);

3.4 二/三分屏

分屏这个原理呢,需要抽象一下,拿二分屏来说,其实最终操作的是纹理坐标的值。
如下图:

比如,因为要二分屏,实际上显示的,肯定不是全部内容,如显示区域为 0.25 到0.75 范围,实际就是把这个范围,填充到上下两个区域,分割线为0.5.

这样,上半部分(0,0)到(0,0.5) 要显示时,实际是从(0,0.25),(0,0.75)的内容,同理下半部分,也是(0,0.5)到(0,1.0),实际也是 (0,0.25),(0,0.75)。
因此,我们修改 y 分量的大小即可:

//输入是不能被修改的,所以使用一个vec2 分量
vec2 uv = vTexture.xy;
if(uv.y >= 0.0 && uv.y <= 0.5)
    uv.y = uv.y + 0.25;
else
    uv.y = uv.y - 0.25;


//分别取yuv各个分量的采样纹理
y = texture(textureY, uv).r;
u = texture(textureU, uv).g - 0.5;
v = texture(textureV, uv).b - 0.5;

就可以得到二分屏:

同理,我们可以得到三分屏的效果:

if(uv.y >= 0.0 && uv.y <= 0.2)
    uv.y = uv.y + 0.3;
else if(uv.y > 0.8)
    uv.y = uv.y - 0.5;


参考:
https://juejin.cn/post/7160304816877469733
https://juejin.cn/post/7168042219163779108
https://time.geekbang.org/column/article/449795
https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/05%20Framebuffers/

以上是关于Opengl ES之YUV数据渲染的主要内容,如果未能解决你的问题,请参考以下文章

Android OpenGL ES 学习 –渲染YUV视频以及视频抖音特效

视频学习笔记:Android OpenGL渲染YUV420P图像

OpenGL渲染YUV420P

用于绘制视频帧的 OpenGL ES 着色器

基于FFmpeg的视频播放器之五:使用SDL2渲染yuv420p

Android音视频(六) 使用OpenGL ES 3.0预览Camera