OpenGL.Shader:志哥教你写一个滤镜直播客户端(可能是结束篇)

OpenGL.Shader基本的图像处理知识已经学习的7788了,所以这篇应该是滤镜直播客户端的最后一篇了,之后会出基于FFmpeg4.3的实战文章,可能还会穿插OpenCV系列的学习文章。

今天介绍几个抖音上比较流行的滤镜实现,网上可能已经存在很多相关的帖子,就是没有几个能提供demo,这不是耍流氓吗,所以还是自己动手来吧。不过u1s1,自己现在的实现效果可能也不太完美,因为之前写这框架没有考虑输入输出帧率的控制,很多好玩的滤镜其实需要掌握好帧率的控制,这是以前的我做得不足,有不足就要改进,所谓的框架迭代升级不就是这样吗?

0、动态帧率

在介绍滤镜实现之前,先说说如何识别现有框架下Android摄像头输出的动态帧率统计。有经验的同学都知道,Android系统的摄像头帧率输出一直都是动态变化着。在运行图像是静止的情况下,输出帧率可以稳定在25~30fps;但是一旦图像产生变化,及时帧内压缩有运动补偿,输出帧率就会降低至15~20fps。对于需要稳定获取Android摄像头输出帧的应用可说是“哑巴吃黄连——有口难言”,需要一种可靠的方法知道当前系统的输出帧率是多少,以便我们写的程序能在恰当的时机做出需要的调整。

实现思路很简单,视频帧率就是每秒内有多少帧图像显示的统计。我们只需要实现一个定时任务,每一秒回调一次次数统计并清空就可以了。具体代码如下:

头文件CELLTimer.h的接口定义

#ifndef CELLTIMER_H
#define CELLTIMER_H
#include <sys/types.h>namespace CELL {class CELLTimerHandler {public:virtual void handlerCallback()=0;virtual ~CELLTimerHandler() {};};class CELLTimer {private:long m_second, m_microsecond;pthread_t thread_timer;static void *OnTimer_stub(void *p) {(static_cast<CELLTimer *>(p))->thread_proc();return NULL;}void thread_proc();CELLTimerHandler *m_handler;bool isStop;public:CELLTimer();CELLTimer(long second, long microsecond);virtual ~CELLTimer();void setTimer(long second, long microsecond, CELLTimerHandler *handler);void startTimer();void stopTimer();};
}
#endif // CELLTIMER_H

接口实现CELLTimer.cpp;实现的方法很简单,用POSIX标准的select函数实现。关于select和poll/epoll的多路复用内容是面试的常考知识点,大家还是要牢牢掌握好(这里推荐两篇收藏的好文,cpp角度https://www.cnblogs.com/aspirant/p/9166944.html,Java角度https://blog.csdn.net/qq_27529917/article/details/82945450)一些小巧妙(超时唤起当定时器)也可以略为了解。

namespace CELL {
//public methods//CELLTimer::CELLTimer() :m_second(0), m_microsecond(0) {thread_timer = -1;}CELLTimer::CELLTimer(long second, long microsecond) :m_second(second), m_microsecond(microsecond) {thread_timer = -1;}CELLTimer::~CELLTimer() {}void CELLTimer::setTimer(long second, long microsecond,CELLTimerHandler *handler) {m_second = second;m_microsecond = microsecond;m_handler = handler;}void CELLTimer::startTimer() {if (thread_timer == -1)pthread_create(&thread_timer, NULL, OnTimer_stub, this);}void CELLTimer::stopTimer() {isStop = true;//pthread_join(thread_timer, NULL); //wait the thread stoppedthread_timer = -1;}
//private methods//void CELLTimer::thread_proc() {while (!isStop) {if (m_handler != NULL)m_handler->handlerCallback();if (isStop) break;struct timeval tempval;tempval.tv_sec = m_second;tempval.tv_usec = m_microsecond;select(0, NULL, NULL, NULL, &tempval);}//LOGD("CELLTimer--thread_proc exit ... ");}}

使用方法,在GpuFilterRender中定义一个CELL::CELLTimer mFpsTimer,并实现CELLTimer的回调接口CELLTimerHandler;一个静态变量mInputFps,一个私有变量mCurrentInputFps。在接收摄像头帧图输入的方法内统计计算mCurrentInputFps++,在CELLTimerHandler的回调就能及时统计出当前帧率。

// 头文件定义
int static      mInputFps;
int             mCurrentInputFps;
CELL::CELLTimer mFpsTimer;
// CELLTimer定时器Callback
virtual void handlerCallback()
{mInputFps = mCurrentInputFps;LOGI("InputFps : %d", mInputFps);mCurrentInputFps = 0;
}
// /
void GpuFilterRender::surfaceCreated(ANativeWindow *window)
{... ...mFpsTimer.setTimer(1, 0, this);mFpsTimer.startTimer();
}
void GpuFilterRender::surfaceDestroyed()
{... ...mFpsTimer.stopTimer();
}
void GpuFilterRender::feedVideoData(int8_t *data, int data_len, int previewWidth, int previewHeight)
{// 在帧图输入接口统计帧率mCurrentInputFps++;
}

1、4镜像滤镜

动态帧率有了,接下来先来一个简单的滤镜效果——4镜像,将屏幕它分割为四部分,每部分画一个相同的视频帧,因为屏幕被分割为4部分,我们的物体坐标在渲染时就不能设定为全屏的。在OpenGL中物体坐标,左下角为(-1,-1),右上角为(1,1),这样我们就可以分别计算出4部分的物体坐标。

确认好物体坐标后,我们接下来就要确认画什么?也就是将视频帧以什么样的方式画在物体坐标上,这时就需要控制纹理坐标,先前已介绍过OpenGL定义的纹理坐标:从左下角(0,0)到右上角(1,1),但实际上,系统视频帧图是一张以左上角为(0,0)到右下角(1,1)倒立过的图像。而且要想高效实现4镜像的渲染,需要借助fbo离屏渲染视频帧图,然后把视频帧当作一张纹理(此时的纹理就是按照OpenGL定义的纹理坐标),根据不同位置进行图像处理后再渲染,这样就可以实现简单的镜像效果。

实现代码如下,详情请参考https://github.com/MrZhaozhirong/NativeCppApp   /src/main/cpp/gpufilter/filter/DouYin4ImageFilter.hpp

#ifndef DOUYIN_4IMAGE_FILTER_HPP
#define DOUYIN_4IMAGE_FILTER_HPP
#include "GpuBaseFilter.hpp"
#include "../render/GpuFilterRender.h"class DouYin4ImageFilter : public GpuBaseFilter {
public:int getTypeId() { return FILTER_TYPE_DOUYIN_4IMAGE; }DouYin4ImageFilter(){   // 不需要特殊的shader,沿用GpuBaseFilter就可以了。LOGI("---DouYin4ImageFilter构造, %p", this);}void init() {GpuBaseFilter::init(NO_FILTER_VERTEX_SHADER.c_str(), NO_FILTER_FRAGMENT_SHADER.c_str());}void onOutputSizeChanged(int width, int height) {GpuBaseFilter::onOutputSizeChanged(width, height);if (mFboTextureId != 9999) {glDeleteTextures(1, &mFboTextureId);mFboTextureId = 9999;}if (mFboId != 9999) {glDeleteFramebuffers(1, &mFboId);mFboId = 9999;}// 创建fbo,用于保存视频帧图作为纹理对象glGenFramebuffers(1, &mFboId);glGenTextures(1, &mFboTextureId);glBindTexture(GL_TEXTURE_2D, mFboTextureId);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // GL_REPEATglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // GL_REPEATglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, 0);glBindFramebuffer(GL_FRAMEBUFFER, mFboId);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFboTextureId, 0);glBindTexture(GL_TEXTURE_2D, 0);glBindFramebuffer(GL_FRAMEBUFFER, 0);}void onDraw(GLuint SamplerY_texId, GLuint SamplerU_texId, GLuint SamplerV_texId,void* positionCords, void* textureCords){glBindFramebuffer(GL_FRAMEBUFFER, mFboId);GpuBaseFilter::onDraw(SamplerY_texId, SamplerU_texId, SamplerV_texId, positionCords, textureCords );glBindFramebuffer(GL_FRAMEBUFFER, 0);// 把视频帧的纹理对象当做二次输入,根据不同位置的顶点坐标和纹理坐标实现4镜像GpuBaseFilter::onDrawRGB(mFboTextureId, positionCords_left_top, texCoordinates_left_top);GpuBaseFilter::onDrawRGB(mFboTextureId, positionCords_right_top, texCoordinates_right_top);GpuBaseFilter::onDrawRGB(mFboTextureId, positionCords_left_bottom, texCoordinates_left_bottom);GpuBaseFilter::onDrawRGB(mFboTextureId, positionCords_right_bottom, texCoordinates_right_bottom);}private:GLuint mFboId = 9999;GLuint mFboTextureId = 9999;// fbo纹理,遵循opengl的正立原则float texCoordinates_left_top[8] = {0.0f, 0.0f,     //左下1.0f, 0.0f,     //右下0.0f, 1.0f,     //左上1.0f, 1.0f,     //右上};float texCoordinates_right_top[8] = {1.0f, 0.0f,     //左下0.0f, 0.0f,     //右下1.0f, 1.0f,     //左上0.0f, 1.0f,     //右上};float texCoordinates_left_bottom[8] = {0.0f, 1.0f,     //左下1.0f, 1.0f,     //右下0.0f, 0.0f,     //左上1.0f, 0.0f,     //右上};float texCoordinates_right_bottom[8] = {1.0f, 1.0f,     //左下0.0f, 1.0f,     //右下1.0f, 0.0f,     //左上0.0f, 0.0f,     //右上};// 顶点坐标float positionCords_left_top[8] = {//x, y          //position-1.0f, -0.0f,   //左下0.0f, -0.0f,    //右下-1.0f, 1.0f,    //左上0.0f, 1.0f,     //右上};float positionCords_right_top[8] = {//x, y          //position-0.0f, -0.0f,   //左下1.0f, -0.0f,    //右下-0.0f, 1.0f,    //左上1.0f, 1.0f,     //右上};float positionCords_left_bottom[8] = {//x, y          //position-1.0f, -1.0f,   //左下0.0f, -1.0f,    //右下-1.0f, 0.0f,    //左上0.0f, 0.0f,     //右上};float positionCords_right_bottom[8] = {//x, y          //position-0.0f, -1.0f,   //左下1.0f, -1.0f,    //右下-0.0f, 0.0f,    //左上1.0f, 0.0f,     //右上};
};
#endif // DOUYIN_4IMAGE_FILTER_HPP

2、 电击效果

接下来一起来看看复杂一丢丢的特效实现方法,首先是电击效果,实际上它的实现就是反选的处理,只需要使用下面代码就可以:

gl_FragColor  = vec4((1.0 - texture.rgb), texture.w);

但想要达到一个很好的效果,其中还是有一些小技巧,也就是需要把握好节奏。假如我们现在有250ms运动的视频帧,再排上180ms静止的反选视频帧就可以实现了,如下方动图演示:假设50ms为一帧,那么对于10帧总时间为500ms的视频帧来说,前5帧都不变,依旧是正常的效果,从第6帧开始我们做反选并且保证画面是静止的,也就是说第7、8、9帧同样放第6帧,而第10帧时我们渲染正常的第10帧,这样周而复始就可以实现电击效果。这下就需要我们用到上方的动态帧率了,根据帧率我们要恰当的调整其电击效果的渲染。

原理实现的相关代码如下,详情请参考:https://github.com/MrZhaozhirong/NativeCppApp   /src/main/cpp/gpufilter/filter/DouYinElectricShockFilter.hpp

#ifndef DOUYIN_ELECTRIC_SHOCK_FILTER_HPP
#define DOUYIN_ELECTRIC_SHOCK_FILTER_HPP
#include "GpuBaseFilter.hpp"class DouYinElectricShockFilter : public GpuBaseFilter {
public:int getTypeId() { return FILTER_TYPE_DOUYIN_SHOCK; }DouYinElectricShockFilter(){SHOCK_FRAGMENT_SHADER     ="precision mediump float;\n\varying highp vec2 textureCoordinate;\n\uniform sampler2D SamplerRGB;\n\uniform sampler2D SamplerY;\n\uniform sampler2D SamplerU;\n\uniform sampler2D SamplerV;\n\mat3 colorConversionMatrix = mat3(\n\1.0, 1.0, 1.0,\n\0.0, -0.39465, 2.03211,\n\1.13983, -0.58060, 0.0);\n\vec3 yuv2rgb(vec2 pos)\n\{\n\vec3 yuv;\n\yuv.x = texture2D(SamplerY, pos).r;\n\yuv.y = texture2D(SamplerU, pos).r - 0.5;\n\yuv.z = texture2D(SamplerV, pos).r - 0.5;\n\return colorConversionMatrix * yuv;\n\}\n\uniform int is_shock;\n\void main()\n\{\n\vec4 textureColor = vec4(yuv2rgb(textureCoordinate), 1.0);\n\if (is_shock==0) \n\{\n\gl_FragColor = textureColor;\n\}\n\else\n\{\n\gl_FragColor = vec4( (1.0-textureColor.rgb), textureColor.w);\n\}\n\}";mInputFps = 0;mCurrentFps = 0;}~DouYinElectricShockFilter() {if(!SHOCK_FRAGMENT_SHADER.empty()) SHOCK_FRAGMENT_SHADER.clear();}void init() {GpuBaseFilter::init(NO_FILTER_VERTEX_SHADER.c_str(), SHOCK_FRAGMENT_SHADER.c_str());mShockLocation = glGetUniformLocation(mGLProgId, "is_shock");}// 接收统计的静态帧率  和  当前帧率void setShockFps(int inputFps, int currentFps) {mInputFps = inputFps;mCurrentFps = currentFps;}void onDraw(GLuint SamplerY_texId, GLuint SamplerU_texId, GLuint SamplerV_texId,void* positionCords, void* textureCords){if (!mIsInitialized)return;glUseProgram(mGLProgId);glUniform1i(mDrawModeLocation, 0);glVertexAttribPointer(mGLAttribPosition, 2, GL_FLOAT, GL_FALSE, 0, positionCords);glEnableVertexAttribArray(mGLAttribPosition);glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GL_FLOAT, GL_FALSE, 0, textureCords);glEnableVertexAttribArray(mGLAttribTextureCoordinate);// runPendingOnDrawTasks();if (mInputFps==0&&mCurrentFps==0) {mShock = 0;} else {int halfInputFps = mInputFps/2;if (mCurrentFps > halfInputFps){mShock = 1;}else{mShock = 0;}}// 为了突出效果,我把一秒内的一半帧图都当作电击效果处理了。glUniform1i(mShockLocation, mShock);if (mShock==0) {glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, SamplerY_texId);glUniform1i(mGLUniformSampleY, 0);glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, SamplerU_texId);glUniform1i(mGLUniformSampleU, 1);glActiveTexture(GL_TEXTURE2);glBindTexture(GL_TEXTURE_2D, SamplerV_texId);glUniform1i(mGLUniformSampleV, 2);} else {//glActiveTexture(GL_TEXTURE0);//glActiveTexture(GL_TEXTURE1);//glActiveTexture(GL_TEXTURE2);}// onDrawArraysPre();glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);glDisableVertexAttribArray(mGLAttribPosition);glDisableVertexAttribArray(mGLAttribTextureCoordinate);glBindTexture(GL_TEXTURE_2D, 0);}
private:std::string SHOCK_FRAGMENT_SHADER;GLint mShockLocation;int mShock;int mInputFps;int mCurrentFps;
};
#endif // DOUYIN_ELECTRIC_SHOCK_FILTER_HPP

3、灵魂出窍

最后来个更好玩的滤镜效果——灵魂出窍。这个特效就是人影有一个向外扩散的效果,同样它的节奏也是非常重要的,尤其是能与音乐的配合才能达到一个完美的效果。在得到放大后的“灵魂”(拷贝帧),我们就需要考虑把“灵魂”和“肉体”(原本视频帧)混合起来,这里需要用到GLES的一个内嵌Mix函数将两个纹理进行mix即可。那么同理,我们还可以实现眩晕、影随的效果:眩晕是将每一帧向两侧做位移再与本帧进行mix,而影随则是将之前的帧缓存下来,以一定的间隔和当前帧做mix。

原理实现的相关代码如下,详情请参考:https://github.com/MrZhaozhirong/NativeCppApp   /src/main/cpp/gpufilter/filter/DouYinSouloutFilter.hpp

我个人更喜欢影随这个效果,所以尝试实现影随的效果。因为从原理上看,影随比灵魂出窍处理效果要好啊。实现方式大同小异,利用fbo在合适的时机保存“灵魂图像”,然后利用纹理坐标的归一化特性实现“视频帧”与“灵魂帧”的差异变化,最后就是利用GLSL的内置函数mix实现两帧图的混合。

#ifndef DOUYIN_SOULOUT_FILTER_HPP
#define DOUYIN_SOULOUT_FILTER_HPP
#include "GpuBaseFilter.hpp"class DouYinSouloutFilter : public GpuBaseFilter {
public:int getTypeId() { return FILTER_TYPE_DOUYIN_SOULOUT; }DouYinSouloutFilter(){SOULOUT_VERTEX_SHADER   = "attribute vec4 position;\n\attribute vec4 inputTextureCoordinate;\n\attribute vec4 soulTextureCoordinate;\n\varying vec2 textureCoordinate;\n\varying vec2 soulCoordinate;\n\void main()\n\{\n\textureCoordinate = inputTextureCoordinate.xy;\n\soulCoordinate = soulTextureCoordinate.xy;\n\gl_Position = position;\n\}";SOULOUT_FRAGMENT_SHADER = "precision mediump float;\n\varying vec2 textureCoordinate;\n\varying vec2 soulCoordinate;\n\uniform sampler2D SamplerY;\n\uniform sampler2D SamplerU;\n\uniform sampler2D SamplerV;\n\uniform int mixFlag;\n\uniform int drawMode;\n\uniform sampler2D textureSoul;\n\mat3 colorConversionMatrix = mat3(\n\1.0, 1.0, 1.0,\n\0.0, -0.39465, 2.03211,\n\1.13983, -0.58060, 0.0);\n\vec3 yuv2rgb(vec2 pos)\n\{\n\vec3 yuv;\n\yuv.x = texture2D(SamplerY, pos).r;\n\yuv.y = texture2D(SamplerU, pos).r - 0.5;\n\yuv.z = texture2D(SamplerV, pos).r - 0.5;\n\return colorConversionMatrix * yuv;\n\}\n\void main()\n\{\n\if (mixFlag==1) \n\{\n\gl_FragColor = vec4(yuv2rgb(textureCoordinate), 1.0);\n\}\n\else\n\{\n\vec4 normalColor = vec4(yuv2rgb(textureCoordinate), 1.0);\n\vec4 soulColor = texture2D(textureSoul, soulCoordinate);\n\gl_FragColor = mix(normalColor, soulColor, 0.35);\n\}\n\}";}~DouYinSouloutFilter(){if(!SOULOUT_VERTEX_SHADER.empty()) SOULOUT_VERTEX_SHADER.clear();if(!SOULOUT_FRAGMENT_SHADER.empty()) SOULOUT_FRAGMENT_SHADER.clear();}void onOutputSizeChanged(int width, int height) {GpuBaseFilter::onOutputSizeChanged(width, height);if (mSoulTextureId != 9999) {glDeleteTextures(1, &mSoulTextureId);mSoulTextureId = 9999;}if (mFboId != 9999) {glDeleteFramebuffers(1, &mFboId);mFboId = 9999;}glGenFramebuffers(1, &mFboId);glGenTextures(1, &mSoulTextureId);glBindTexture(GL_TEXTURE_2D, mSoulTextureId);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // GL_REPEATglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // GL_REPEATglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, 0);glBindFramebuffer(GL_FRAMEBUFFER, mFboId);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mSoulTextureId, 0);glBindFramebuffer(GL_FRAMEBUFFER, 0);}void init() {GpuBaseFilter::init(SOULOUT_VERTEX_SHADER.c_str(), SOULOUT_FRAGMENT_SHADER.c_str());mGLAttribSoulCoordinate = static_cast<GLuint>(glGetAttribLocation(mGLProgId, "soulTextureCoordinate"));mMixFlagLocation = static_cast<GLuint>(glGetUniformLocation(mGLProgId, "mixFlag"));mGLUniformSampleSoul = static_cast<GLuint>(glGetUniformLocation(mGLProgId, "textureSoul"));}void setInputFps(int fps) {mInputFps = fps;}void setCurrentFps(int fps) {mCurrentFps = fps;}void onDraw(GLuint SamplerY_texId, GLuint SamplerU_texId, GLuint SamplerV_texId,void* positionCords, void* textureCords){if (mInputFps==0&&mCurrentFps==0) {mMixFlag = 0;} else {int halfInputFps = mInputFps/2;if (mCurrentFps>halfInputFps+0)//    || mCurrentFps==halfInputFps+1//    || mCurrentFps==halfInputFps+2//    || mCurrentFps==halfInputFps+3//    || mCurrentFps==halfInputFps+4//    || mCurrentFps==halfInputFps+5){mMixFlag = 1;}else{mMixFlag = 0;}}if (mMixFlag == 1) //mMixFlag == 1{ // 拷贝当前帧当灵魂glBindFramebuffer(GL_FRAMEBUFFER, mFboId);GpuBaseFilter::onDraw(SamplerY_texId, SamplerU_texId, SamplerV_texId, positionCords, textureCords );glBindFramebuffer(GL_FRAMEBUFFER, 0);}glUseProgram(mGLProgId);// 把相关参数传到shader当中。glVertexAttribPointer(mGLAttribPosition, 2, GL_FLOAT, GL_FALSE, 0, positionCords); // 当前帧顶点坐标glEnableVertexAttribArray(mGLAttribPosition);glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GL_FLOAT, GL_FALSE, 0, textureCords);// 当前帧纹理坐标glEnableVertexAttribArray(mGLAttribTextureCoordinate);glVertexAttribPointer(mGLAttribSoulCoordinate, 2, GL_FLOAT, GL_FALSE, 0, soul_texCoordinates);// 灵魂帧纹理坐标glEnableVertexAttribArray(mGLAttribSoulCoordinate);glUniform1i(mMixFlagLocation, mMixFlag);glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, SamplerY_texId);glUniform1i(mGLUniformSampleY, 0);glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, SamplerU_texId);glUniform1i(mGLUniformSampleU, 1);glActiveTexture(GL_TEXTURE2);glBindTexture(GL_TEXTURE_2D, SamplerV_texId);glUniform1i(mGLUniformSampleV, 2);glActiveTexture(GL_TEXTURE3);glBindTexture(GL_TEXTURE_2D, mSoulTextureId);glUniform1i(mGLUniformSampleSoul, 3);// onDrawArraysPre();glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);glDisableVertexAttribArray(mGLAttribPosition);glDisableVertexAttribArray(mGLAttribTextureCoordinate);glDisableVertexAttribArray(mGLAttribSoulCoordinate);glBindTexture(GL_TEXTURE_2D, 0);}
private:std::string SOULOUT_VERTEX_SHADER;std::string SOULOUT_FRAGMENT_SHADER;GLuint mFboId = 9999;GLuint mSoulTextureId = 9999;GLuint mMixFlagLocation;GLuint mGLAttribSoulCoordinate;GLuint mGLUniformSampleSoul;int mMixFlag;int mInputFps;int mCurrentFps;//灵魂顶点坐标,实现位置相对偏移。float soul_positionCords[8] = {//x, y          //position-1.0f, -1.0f,   //左下1.0f, -1.0f,    //右下-1.0f, 1.0f,    //左上1.0f, 1.0f,     //右上};//灵魂纹理坐标,相对原图取局内部分,然后填充到屏幕,实现相对放大。float soul_texCoordinates[8] = {0.1f, 0.1f,     //左下0.9f, 0.1f,     //右下0.1f, 0.9f,     //左上0.9f, 0.9f,     //右上};
};
#endif // DOUYIN_SOULOUT_FILTER_HPP

滤镜客户端系列文章到此告一段落。理论学习实质比较大,离实战商用还有一定的距离。正所谓人无完人,代码不可能完美没有bug。写博客的初衷就是为了温故而知新,谁不是笑看当年自己的代码?庆幸自己永远在学习总结的路上,也希望大家一直在路上。The end.

OpenGL.Shader:志哥教你写一个滤镜直播客户端:仿3个抖音滤镜效果(4镜像/电击/灵魂出窍)相关推荐

  1. OpenGL.Shader:志哥教你写一个滤镜直播客户端(5)视觉滤镜:对比度、曝光、马赛克

    OpenGL.Shader:志哥教你写一个滤镜直播客户端(5) 上一章介绍了如何在渲染nv21流的时候进行滤镜的无缝切换,这章内容紧接上一章,介绍三种滤镜特效:对比度.曝光.马赛克,并介绍如何动态调节 ...

  2. 猫哥教你写爬虫 046--协程-实践-吃什么不会胖

    吃什么不会胖? 低热量食物 食物的数量有千千万,如果我们要爬取食物热量的话,这个数据量必然很大. 使用多协程来爬取大量的数据是非常合理且明智的选择 如果我们要爬取的话,那就得选定一个有存储食物热量信息 ...

  3. 猫哥教你写爬虫 006--条件判断和条件嵌套

    流程控制 复仇者联盟3-无限战争(搜集宝石) python里面, 不需要使用;来结尾, 因为python是使用换行来结束一行代码的 if判断, 没有{}, python使用缩进来表示层级关系 if.. ...

  4. 猫哥教你写爬虫 037--爬虫-宝宝要听歌

    戴上耳机, 这个世界与我无关... 让我们用音乐洗涤心灵吧... 我们从哪个网站爬取资源呢? 专治各种不服... 打开酷狗官网, 可以看到搜索框,我们要爬取的数据就是搜索歌曲后, 酷狗后台返回的歌曲列 ...

  5. 手把手教你写一个生成对抗网络

    成对抗网络代码全解析, 详细代码解析(TensorFlow, numpy, matplotlib, scipy) 那么,什么是 GANs? 用 Ian Goodfellow 自己的话来说: " ...

  6. python k线合成_手把手教你写一个Python版的K线合成函数

    手把手教你写一个Python版的K线合成函数 在编写.使用策略时,经常会使用一些不常用的K线周期数据.然而交易所.数据源又没有提供这些周期的数据.只能通过使用已有周期的数据进行合成.合成算法已经有一个 ...

  7. 猫哥教你写爬虫 002--作业-打印皮卡丘

    作业 请你使用print()函数将下面的皮卡丘打印出来, 使用三种方式 へ /|/\7 ∠_// │ / /│ Z _,< / /`ヽ│ ヽ / 〉Y ` / /イ● 、 ● ⊂⊃〈 /() へ ...

  8. cmd管道无法接收特定程序返回值_渗透不会反弹shell?来教你写一个cmd的shell

    渗透不会反弹shell?来教你写一个cmd的shell 包含的库: #include #include #include #include #include #pragma comment(lib, ...

  9. 猫哥教你写爬虫 005--数据类型转换-小作业

    小作业 程序员的一人饮酒醉 请运用所给变量,使用**str()**函数打印两句话. 第一句话:1人我编程累, 碎掉的节操满地堆 第二句话:2眼是bug相随, 我只求今日能早归 number1 = 1 ...

最新文章

  1. vmware nat模式原理探究,实现虚拟机跨网段管理
  2. Nearest Opposite Parity(反向建边+spfa)
  3. SSM整合简单登录案例
  4. 一个控制器怎么转发到另外一个控制器_楼宇自动化系统(BAS),DDC,一个最核心的控制器...
  5. 学好Java的八个条件
  6. 敲地鼠java_Java实现的打地鼠小游戏完整示例【附源码下载】
  7. js接收php 回调,JS callback回调函数的使用(附代码)
  8. 《城市轨道交通——产业关联理论与应用》读书笔记
  9. python中plot不能显示标签_解决python中使用plot画图,图不显示的问题
  10. SQL:postgresql中COALESCE函数
  11. 另辟蹊径 直取通州的“墨迹天气”APP应用的成功故事
  12. 7-1 大師と仙人との奇遇 (20 分)
  13. 在GridControl表格控件中实现多层级主从表数据的展示
  14. 嵌入式 Linux平台 C程序 交叉编译技术
  15. 京东“竖亥小车”秒测商品尺寸重量
  16. Unix/Linux编程:客户应用程序------DAYTIME、TIME、ECHO
  17. csm和uefi_[整理]BIOS设置UEFI和安全引导
  18. 论职务犯罪案件侦查 z
  19. CocosCreator高斯模糊深度优化版
  20. 全球及中国铁路行业十四五规划目标与投资建设状况分析报告2021版

热门文章

  1. Python搭建博客网站小结
  2. 更新win10后连接WIFI时,提示“无法连接到这个网络”
  3. 证明完全立方数模9同余_牡丹江2立方玻璃钢蓄水池报价
  4. F4/F7飞控betaflight固件烧写,地面站 BF无法读取、不识别、未发现等飞控疑难杂症问题解决与驱动安装
  5. 微信小程序 小程序生命周期、页面导航/事件、WXS脚本(笔记)
  6. SQL -- 游标(详细)
  7. 企业微信之——扫码登录
  8. 富文本转化为普通文本
  9. 九校联考-长沙市一中NOIP模拟Day1T1 矩阵游戏(game)
  10. Setting up Basic Access Control