阅读 GpuImage 有感


简介

GPUImage 毫无疑问是音视频项目里面必读工程了,它的侧重点在于渲染方面。有些公司的招聘要求上可能都会写明熟悉GPUImage ,重要性可见一斑。通过阅读 GPUImage 的源码,能够让你掌握 OpenGL 的渲染以及渲染链的搭建,同时工程里面很多特效 Shader 代码,通过阅读和实践这些 Shader 代码,能够让你掌握初步的 Shader 编写能力。比如常见的滤镜效果,在 GPUImage 就有现成的代码例子,掌握常见滤镜效果的代码编写。本篇文章简单记录下个人的理解,项目的地址为GpuImage Github地址

准备

首先将源码下载下来,需要将 library模块中 build.gradle文件中的

apply from: 'https://gist.githubusercontent.com/wasabeef/cf14805bee509baf7461974582f17d26/raw/bintray-v1.gradle'
apply from: 'https://gist.githubusercontent.com/wasabeef/cf14805bee509baf7461974582f17d26/raw/install-v1.gradle'

注释掉,目前这文件找不到,由于主要是影响打包,去掉也没有任何问题,具体的详细的源码分析可以参考下面的文章 OpenGL 之 GPUImage 源码分析 本文主要记录一下个人对此的理解

疑点

在GPUImageFilterGroup 中处理多个效果的时候,在onDraw 中有这样的写法
public class GPUImageFilterGroup extends GPUImageFilter {
    public GPUImageFilterGroup(List<GPUImageFilter> filters) {
        ...
        //创建纹理缓冲
        glTextureBuffer = ByteBuffer.allocateDirect(TEXTURE_NO_ROTATION.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        glTextureBuffer.put(TEXTURE_NO_ROTATION).position(0);

        //获取到 执行 镜像处理的纹理 坐标 ,创建 执行 水平镜像的缓冲,这里首先获取到 TEXTURE_NO_ROTATION 对应的纹理,然后执行 镜像的变换操作
        //就是改变y轴坐标
        float[] flipTexture = TextureRotationUtil.getRotation(Rotation.NORMAL, false, true);
        glTextureFlipBuffer = ByteBuffer.allocateDirect(flipTexture.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        glTextureFlipBuffer.put(flipTexture).position(0);
    }
    ...
    public void onDraw(final int textureId, final FloatBuffer cubeBuffer, final FloatBuffer textureBuffer) {
        //先执行还未执行的方法
        runPendingOnDrawTasks();

        //如果还未初始化,直接返回
        if (!isInitialized() || frameBuffers == null || frameBufferTextures == null) {
            return;
        }

        if (mergedFilters != null) {
            int size = mergedFilters.size();
            //相机原始图像转换的纹理ID
            int previousTexture = textureId;
            //一个一个执行绘制操作
            for (int i = 0; i < size; i++) {
                GPUImageFilter filter = mergedFilters.get(i);
                //绑定对应的帧缓冲
                boolean isNotLast = i < size - 1;
                //如果不是最后一个滤镜,绘制到FrameBuffer上,如果是最后一个,就绘制到了屏幕上
                if (isNotLast) {
                    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);
                    GLES20.glClearColor(0, 0, 0, 0);
                }
                //滤镜绘制代码
                if (i == 0) {
                    //第一个滤镜绘制使用相机的原始图像纹理Id和参数传递过来的顶点以及纹理坐标,这个处理之后会得到正确的图片
                    filter.onDraw(previousTexture, cubeBuffer, textureBuffer);
                } else if (i == size - 1) {
                    //如果是最后一个,并且 size大小可以被2整除,则纹理坐标设置为 glTextureFlipBuffer ,这是做了水平镜像处理的
                    filter.onDraw(previousTexture, glCubeBuffer, (size % 2 == 0) ? glTextureFlipBuffer : glTextureBuffer);
                } else {
                    //中间的滤镜绘制在之前纹理基础上继续绘制,使用 mGLTextureBuffer纹理坐标
                    filter.onDraw(previousTexture, glCubeBuffer, glTextureBuffer);
                }
                if (isNotLast) {
                    //如果是最后一个绑定到屏幕上
                    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
                    previousTexture = frameBufferTextures[i];
                }
            }
        }
    }
    ...
}
其中  filter.onDraw(previousTexture, glCubeBuffer, (size % 2 == 0) ? glTextureFlipBuffer : glTextureBuffer); 这里根据size是否能被2整除来设置对应的纹理坐标
其中 glTextureBuffer 的纹理坐标为 TEXTURE_NO_ROTATION

//纹理坐标,这是以左上角为原点的坐标,并不是传统的以 左下角为原点的坐标
public static final float TEXTURE_NO_ROTATION[] = {
    0.0f, 1.0f,
    1.0f, 1.0f,
    0.0f, 0.0f,
    1.0f, 0.0f,
};

glTextureFlipBuffer 刚开始值为 TEXTURE_NO_ROTATION 后面执行了 flipVertical 变换(就是将纹理Y的坐标将1变成0,0变成1),最终得到的纹理坐标为
public static final float TEXTURE_COORD_NO_ROTATION[] = {
    0.0f,0.0f, //图像的左下角
    1.0f,0.0f, //图像的右下角
    0.0f,1.0f, //图像的左下角
    1.0f,1.0f  //图像的右上角
};

而我们知道OpenGL中纹理坐标为 01之间,而且正常的纹理坐标的原点在左下角,类似下面的这张图

结果显示

而我们的计算机坐标原点是在左上角的,所以我们就需要做变换,也要以左上角为原点得到正确的纹理坐标,要不然以 默认的纹理坐标TEXTURE_COORD_NO_ROTATION 来显示的话,
会跟正确的图像倒置过来的效果为了更好的理解,写个简单的demo来验证下

//通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,等于是将这个头文件变为一个 .cpp 文件了。现在只需要在你的程序中包含stb_image.h并编译就可以了。
#define STB_IMAGE_IMPLEMENTATION
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>
#include <iostream>
#include <Shader.h>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    Shader ourShader("4.1.texture.vs", "4.1.texture.fs");

    // ------------------------------------------------------------------  传统的以纹理坐标得到的坐标系,是以左下角为下标远点的
    float vertices[] = {
        // positions           // texture coords
        -1.0f, -1.0f, 0.0f,    0.0f, 0.0f, //图像的左下角
        1.0f,  -1.0f, 0.0f,    1.0f, 0.0f, //图像的右下角
        -1.0f,  1.0f, 0.0f,    0.0f, 1.0f, //图像的左下角
        1.0f,   1.0f, 0.0f,    1.0f, 1.0f  //图像的右上角
    };

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // color attribute
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    unsigned int texture2;
    int width, height, nrChannels;

    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture2);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);    // set texture wrapping to GL_REPEAT (default wrapping method)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // load image, create texture and generate mipmaps
    unsigned char* data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
    if (data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);

    // 别忘记在激活着色器前先设置uniform!
    ourShader.use(); // don't forget to activate/use the shader before setting uniforms!
    glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);

   while (!glfwWindowShouldClose(window))
   {
        processInput(window);
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture2);

        ourShader.use();
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

    glfwTerminate();
    return 0;
}
void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

其中 4.1.texture.vs 顶点着色器的内容为  

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}

4.1.texture.fs 片段着色器的内容为

#version 330 core
out vec4 FragColor;
in vec2 TexCoord;

uniform sampler2D texture1;

void main()
{
    FragColor = texture(texture1, TexCoord);
}

我们刚开始使用的纹理坐标为 ,也就是纹理默认的左下角为原点的取值
float vertices[] = {
    // positions           // texture coords
    -1.0f, -1.0f,  0.0f,   0.0f, 0.0f, //图像的左下角
    1.0f,  -1.0f,  0.0f,   1.0f, 0.0f, //图像的右下角
    -1.0f,  1.0f,  0.0f,   0.0f, 1.0f, //图像的左下角
    1.0f,   1.0f,  0.0f,   1.0f, 1.0f  //图像的右上角
};

awesomeface.png 原图显示的样子
结果显示
接下来运行,我们看下结果
结果显示
可以看出是跟原图是倒置过来的,接下来试一下 以左上角为原点的纹理坐标

//由于 计算机的坐标系跟 纹理的坐标系是相反的,所以我们可以用 按照左上角为下标原点,这俩者的效果就是翻转过来的样子,接下来我们使用以左上角为原点得到的纹理坐标
float vertices1[] = {
    // positions          // texture coords
    -1.0f, -1.0f,  0.0f,  0.0f, 1.0f,
    1.0f,  -1.0f,  0.0f,  1.0f, 1.0f,
    -1.0f,  1.0f,  0.0f,  0.0f, 0.0f,
    1.0f,   1.0f,  0.0f,  1.0f, 0.0f
};

结果显示
可以看出我们的原图正确的显示过来了,

//水平镜像的效果  , 就是将 x 坐标的值由1变为0,0变为1 ,就跟镜子一样,因为改变x轴的坐标,这个变换是我们以上一个坐标(vertices1)变换过来的
float vertices2[] = {
    // positions          // texture coords
    -1.0f, -1.0f, 0.0f,   1.0f, 1.0f,
    1.0f,  -1.0f, 0.0f,   0.0f, 1.0f,
    -1.0f,  1.0f, 0.0f,   1.0f, 0.0f,
    1.0f,   1.0f, 0.0f,   0.0f, 0.0f
};

结果显示
可以看出我们的图片跟上一个图片做了镜像的处理,所以执行水平的变换,改变的是x坐标,能得到镜像的效果,

//垂直镜像的效果 ,就是倒置过来了,因为改变了y轴的坐标,这变换也是在上一个坐标((vertices1) 变换过来的
float vertices3[] = {
    // positions          // texture coords
    -1.0f, -1.0f, 0.0f,   0.0f, 0.0f,
    1.0f,  -1.0f, 0.0f,   1.0f, 0.0f,
    -1.0f,  1.0f, 0.0f,   0.0f, 1.0f,
    1.0f,   1.0f, 0.0f,   1.0f, 1.0f
};

结果显示
可以看出我们的图片倒置过来了,通过改变y的坐标实现的效果

继续回到项目中 GPUImage中,在GPUImage中默认的纹理坐标是以左上角为远点的,下面是默认的纹理坐标
public static final float TEXTURE_NO_ROTATION[] = {
    0.0f, 1.0f,
    1.0f, 1.0f,
    0.0f, 0.0f,
    1.0f, 0.0f,
};
所以就有点类似原本就帮我们做了一次倒置的效果,而这也是原本就要的,所以为了方便,就直接在这个基础上做修改了,继续回到
filter.onDraw(previousTexture, glCubeBuffer, (size % 2 == 0) ? glTextureFlipBuffer : glTextureBuffer); 这里根据size是否能被2整除来设置对应的纹理坐标

//纹理坐标,这是以左上角为原点的坐标,并不是传统的以 左下角为原点的坐标
public static final float TEXTURE_NO_ROTATION[] = {
    0.0f, 1.0f,
    1.0f, 1.0f,
    0.0f, 0.0f,
    1.0f, 0.0f,
};

glTextureFlipBuffer 刚开始值为 TEXTURE_NO_ROTATION 后面执行了 flipVertical 变换(就是将纹理Y的坐标将1变成0,0变成1),最终得到的纹理坐标为
public static final float TEXTURE_COORD_NO_ROTATION[] = {
    0.0f,0.0f, //图像的左下角
    1.0f,0.0f, //图像的右下角
    0.0f,1.0f, //图像的左下角
    1.0f,1.0f  //图像的右上角
};

所以使用 TEXTURE_NO_ROTATION 的纹理坐标实质上是将图像进行了上下翻转,两次调用TEXTURE_NO_ROTATION纹理坐标时,又将图像复原了,所以对于奇数次的话,
就继续使用 glTextureBuffer这个默认就是再倒过来,如果是偶数的话,因为前面一次已经将图片倒置过来了,所以我们可以不用再倒置了,
我们就可以使用 TEXTURE_COORD_NO_ROTATION,这是默认的纹理坐标,所以就能保持上次的效果了

性能问题

在使用GPUImage的时候,发现开启摄像机过了一会手机就会发烫,下面分析下性能问题

首先在 Camera2Loader中在获取到预览数据的时候,这里首先要执行一次数据的变换 image.generateNV21Data(),这里加上打印的时间
setOnImageAvailableListener({ reader ->
    //如果为空,从这个 setOnImageAvailableListener lambda中局部返回
    val image = reader?.acquireNextImage() ?: return@setOnImageAvailableListener
    //不为空,回调通知回去
    val start: Long = System.currentTimeMillis()
    Log.d("Camera2Loader", "generateNV21Data before$start")
    val result = image.generateNV21Data()
    Log.d("Camera2Loader", "generateNV21Data after${System.currentTimeMillis() - start}")
    onPreviewFrame?.invoke(result, image.width, image.height)
    image.close()
}, null)

还有一次是在将预览的数据YUV转成 RGB的时候
public void onPreviewFrame(final byte[] data, final int width, final int height) {
    //创建预览界面内存大小
    if (glRgbBuffer == null) {
        glRgbBuffer = IntBuffer.allocate(width * height);
    }
    //为空的时候才能添加
    if (runOnDraw.isEmpty()) {
        //通过任务的方式来进行
        runOnDraw(new Runnable() {
            @Override
            public void run() {
                long startTime  = System.currentTimeMillis();
                //Log.d("GPUImageRenderer","YUVtoRBGA before" + System.currentTimeMillis());
                //首先将yuv转成 rgba格式,输出的格式到 glRgbBuffer
                GPUImageNativeLibrary.YUVtoRBGA(data, width, height, glRgbBuffer.array());
                Log.d("GPUImageRenderer","YUVtoRBGA after" + (System.currentTimeMillis() - startTime));

                //获得预览画面的纹理,将相机的纹理画面保存到 glTextureId 变量中
                glTextureId = OpenGlUtils.loadTexture(glRgbBuffer, width, height, glTextureId);

                //判断宽度是否有改变,有的话,要重新调整缩放的比率
                if (imageWidth != width) {
                    imageWidth = width;
                    imageHeight = height;
                    adjustImageScaling();
                }
            }
        });
    }
}
这里也加上打印的时间,下面看看允许的结果

结果显示
结果显示
可以看出俩者的时间大致在10-11毫秒,而我们的相机正常是30帧的话,每一帧的时间只有33毫秒,假设在数据的转换上就耗费了11毫秒,那么剩下的时间根本就不多了,这还是只是转换,绘制根本还没执行而且由于这个数据的转换是在CPU上进行的,转换操作都是计算,所以导致CPU繁忙,所以会发烫,改进的话,我们在C++一层创建一个 GL_TEXTURE_EXTERNAL_OES Android特有的纹理对象,这个可以直接将YUV的数据转成RGB的纹理接受过来,而且是在GPU上

在 GPUImageFilterGroup 中,在处理多个滤镜效果的时候,内部的做法是通过创建多个帧缓冲对象 FBO,每个特效对应一个帧缓冲对象,
    public void onOutputSizeChanged(final int width, final int height) {
        super.onOutputSizeChanged(width, height);
        if (frameBuffers != null) {
            destroyFramebuffers();
        }

        //将界面大小传递下去
        int size = filters.size();
        for (int i = 0; i < size; i++) {
            filters.get(i).onOutputSizeChanged(width, height);
        }

        if (mergedFilters != null && mergedFilters.size() > 0) {
            //创建跟 mergedFilters 对应的纹理缓冲对象
            size = mergedFilters.size();
            frameBuffers = new int[size - 1];
            frameBufferTextures = new int[size - 1];

            //注意这里生成了 多个帧缓冲对象
            for (int i = 0; i < size - 1; i++) {
                //创建帧缓冲对象,帧缓冲对象通过挂载纹理可以将内容输出到对应的纹理上
                GLES20.glGenFramebuffers(1, frameBuffers, i);
                //创建挂载的纹理
                GLES20.glGenTextures(1, frameBufferTextures, i);
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameBufferTextures[i]);
                //设置纹理的环绕方式和过滤方式
                GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
                GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
                GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
                GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
                GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

                //绑定纹理到 当前生成的帧缓冲
                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);
                GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, frameBufferTextures[i], 0);

                //解绑操作
                GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
                GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
            }
        }
    }
    在多个特效之间做处理的时候,频繁的绑定帧缓冲和解绑
    public void onDraw(final int textureId, final FloatBuffer cubeBuffer, final FloatBuffer textureBuffer) {
        //先执行还未执行的方法
        runPendingOnDrawTasks();

        //如果还未初始化,直接返回
        if (!isInitialized() || frameBuffers == null || frameBufferTextures == null) {
            return;
        }

        if (mergedFilters != null) {
            int size = mergedFilters.size();
            //相机原始图像转换的纹理ID
            int previousTexture = textureId;
            //一个一个执行绘制操作
            for (int i = 0; i < size; i++) {
                GPUImageFilter filter = mergedFilters.get(i);
                //绑定对应的帧缓冲
                boolean isNotLast = i < size - 1;
                //如果不是最后一个滤镜,绘制到FrameBuffer上,如果是最后一个,就绘制到了屏幕上
                if (isNotLast) {
                    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);
                    GLES20.glClearColor(0, 0, 0, 0);
                }
                //滤镜绘制代码
                if (i == 0) {
                    //第一个滤镜绘制使用相机的原始图像纹理Id和参数传递过来的顶点以及纹理坐标,这个处理之后会得到正确的图片
                    filter.onDraw(previousTexture, cubeBuffer, textureBuffer);
                } else if (i == size - 1) {
                    //如果是最后一个,并且 size大小可以被2整除,则纹理坐标设置为 glTextureFlipBuffer ,这是做了水平镜像处理的
                    filter.onDraw(previousTexture, glCubeBuffer, (size % 2 == 0) ? glTextureFlipBuffer : glTextureBuffer);
                } else {
                    //中间的滤镜绘制在之前纹理基础上继续绘制,使用 mGLTextureBuffer纹理坐标
                    filter.onDraw(previousTexture, glCubeBuffer, glTextureBuffer);
                }
                if (isNotLast) {
                    //如果是最后一个绑定到屏幕上
                    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
                    previousTexture = frameBufferTextures[i];
                }
            }
        }
    }
    ...
}
频繁的绑定帧缓冲和解绑效率会有影响,官方建议是通过创建一个帧缓冲对象,通过频繁的 attach纹理,detach纹理来做到多个效果之间的切换处理

体验问题

1.GPUImage在切换前置摄像头的时候,会倒置过来,所以还要做垂直的变换操作
2.在切换的过程中会看到原摄像机的内容,就是还没有执行正常处理,比如旋转的角度,倒置处理之后的画面,这是由于 GPUImageRenderer 中 glTextureId 保存的是摄像机原本的画面内容并不是处理完之后,比如旋转,倒置后的结果

    public void onPreviewFrame(final byte[] data, final int width, final int height) {
        //创建预览界面内存大小
        if (glRgbBuffer == null) {
            glRgbBuffer = IntBuffer.allocate(width * height);
        }
        //为空的时候才能添加
        if (runOnDraw.isEmpty()) {
            //通过任务的方式来进行
            runOnDraw(new Runnable() {
                @Override
                public void run() {
                    long startTime  = System.currentTimeMillis();
                    //Log.d("GPUImageRenderer","YUVtoRBGA before" + System.currentTimeMillis());
                    //首先将yuv转成 rgba格式,输出的格式到 glRgbBuffer
                    GPUImageNativeLibrary.YUVtoRBGA(data, width, height, glRgbBuffer.array());
                    Log.d("GPUImageRenderer","YUVtoRBGA after" + (System.currentTimeMillis() - startTime));

                    //获得预览画面的纹理,将相机的纹理画面保存到 glTextureId 变量中
                    glTextureId = OpenGlUtils.loadTexture(glRgbBuffer, width, height, glTextureId);

                    //判断宽度是否有改变,有的话,要重新调整缩放的比率
                    if (imageWidth != width) {
                        imageWidth = width;
                        imageHeight = height;
                        adjustImageScaling();
                    }
                }
            });
        }
    }
    public void onDrawFrame(final GL10 gl) {
        //首先清除颜色
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        //运行运行中的Runnable队列
        //GLSurfaceView中,对于GL环境的操作,出queueEvent是将事件放入队列中,到GL线程中执行外,其他方法基本都是在主线程
        //(也可以是其他线程,非当前GLSurfaceView实例的GL线程)中修改某个状态值,然后取消GL线程的等待,在GL线程中根据状态值作相应的操作,
        // 并在操作后反馈给调用方法的那个线程,当然有的方法也不需要反馈。
        runAll(runOnDraw);
        //执行效果器的绘制,由于前面一步已经将相机的画面已经绘制到了 glTextureId 纹理上面了,接下来我们就可以出来效果了
        filter.onDraw(glTextureId, glCubeBuffer, glTextureBuffer);
        //运行 绘制后 Runnable 队列
        runAll(runOnDrawEnd);
        //调用updateTexImage 来完成数据到 纹理上面
        if (surfaceTexture != null) {
            surfaceTexture.updateTexImage();
        }
    }

总结

总体而言,这个项目还是非常不错的,非常有参考,学习的意义,虽然有点小瑕疵,但不足以掩盖他的牛逼,尤其是他支持的各种各样的特效处理,总之,牛逼!

参考链接

  1. android-gpuimage
  2. android GLSurfaceView渲染模式
  3. Android Camera2 简介
  4. GLSL 内建函数
  5. OpenGL 之 GPUImage 源码分析
  6. LearnOpenGL

文章作者: AheadSnail
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AheadSnail !
评论
  目录