Android 利用 OpenGL ES 实现抖音“蓝线挑战”特效

4,852 阅读3分钟

前面文章中我们实现了抖音传送带特效,发现大家普遍对抖音视频特效感兴趣,那么本文将继续这个抖音特效实现系列,后面也将带来更多的关于抖音视频特效分析和实现的文章。

抖音传送带特效

本文带给大家的是抖音的经典视频特效“蓝线挑战”实现,为啥选择“蓝线挑战”?

之前这个特效没太在意,知道看到老罗直播团队用这个特效拍了一个短视频,发现挺有意思的。

老罗直播团队视频展示

本文“蓝线挑战”特效实现

抖音“蓝线挑战”特效原理

之前“蓝线挑战”特效有人实现过它的变种,但是看起来搞的有点复杂了,本文的实现思路就非常简单,抖音“蓝线挑战”和“传送带”实现原理基本相同:每次更新预览帧图像的特定区域进行渲染。

抖音“蓝线挑战”特效实现可以直接基于前文“传送带”特效的 demo 修改几行代码即可,其实就是图像区域的拷贝方式有所变化。

抖音“蓝线挑战”特效原理图

原理如上图所示,蓝线从左侧向右侧移动(每帧移动一定的像素),蓝线左侧区域像素保存不变,蓝线右侧区域像素不断刷新,每次都更新为当前预览帧相应区域的像素

抖音“蓝线挑战”特效实现

抖音“蓝线挑战”特效实现

上节原理分析时,将当前预览帧相应区域的像素不断地拷贝到蓝线右侧区域,这样需要进行很多次拷贝操作,并不高效,可能会在低端机上导致一些性能问题。

好在 Android 相机出图都是横向的(旋转了 90 或 270 度),这样图像区域只需要一次拷贝即可完成,最后渲染的时候利用 OpenGL 变换矩再将图像旋转回来

Android 相机出图是 YUV 格式的,这里为了拷贝处理方便,先使用 OpenCV 将 YUV 图像转换为 RGBA 格式,当然为了追求性能直接使用 YUV 格式的图像问题也不大。

cv::Mat mati420 = cv::Mat(pImage->height * 3 / 2, pImage->width, CV_8UC1, pImage->ppPlane[0]);
cv::Mat matRgba = cv::Mat(m_SrcImage.height, m_SrcImage.width, CV_8UC4, m_SrcImage.ppPlane[0]);
cv::cvtColor(mati420, matRgba, CV_YUV2RGBA_I420);

用到的着色器程序就是简单的贴图:

//顶点着色器
#version 300 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;
uniform mat4 u_MVPMatrix;
out vec2 v_texCoord;
void main()
{
gl_Position = u_MVPMatrix * a_position;
v_texCoord = a_texCoord;
}

//片段着色器
#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D u_texture;
uniform float u_offset;//归一化后,蓝线的位置

void main()
{

    if (v_texCoord.y >= u_offset - 0.004 && v_texCoord.y <= u_offset + 0.004) {
		//对靠近蓝线的位置进行采样时,绘制蓝线
        outColor = vec4(0.0, 0.0, 1.0, 1.0);
    }
    else {
        outColor = texture(u_texture, v_texCoord);
    }
}

将当前预览帧相应区域的像素,不断地拷贝到蓝线右侧区域实现代码:

m_frameIndex = m_frameIndex % m_bannerNum;
float offset = m_frameIndex * 1.0f / m_bannerNum; //归一化后,蓝线的位置
int pixelOffset = (m_bannerNum - m_frameIndex) * BF_BANNER_WIDTH; //根据蓝线位置计算蓝线右侧区域要拷贝的像素数量

memcpy(m_RenderImage.ppPlane[0], m_SrcImage.ppPlane[0], m_RenderImage.width * pixelOffset * 4); //拷贝到蓝线右侧区域

将图像从左向右划分很多竖条,竖条宽度为 BF_BANNER_WIDTH 像素,即蓝线每次移动 BF_BANNER_WIDTH 像素;

竖条数量为 m_bannerNum ,m_frameIndex 为绘制次数,m_SrcImage 为当前预览帧,m_RenderImage 为当前要渲染的图像。

渲染操作:

glUseProgram (m_ProgramObj);

glBindVertexArray(m_VaoId);

glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);

//将当前预览帧相应区域的像素拷贝到蓝线右侧区域
m_frameIndex = m_frameIndex % m_bannerNum;
float offset = m_frameIndex * 1.0f / m_bannerNum;
int pixelOffset = (m_bannerNum - m_frameIndex) * BF_BANNER_HEIGHT;
LOGCATE("BluelineChallengeExample::Draw[offset, pixelOffset]=[%.4f, %d]", offset, pixelOffset);
memcpy(m_RenderImage.ppPlane[0], m_SrcImage.ppPlane[0], m_RenderImage.width * pixelOffset * 4);


//更新纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]);
glBindTexture(GL_TEXTURE_2D, GL_NONE);

//更新蓝线位置
GLUtils::setFloat(m_ProgramObj, "u_offset", offset);

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
GLUtils::setInt(m_ProgramObj, "u_texture", 0);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
glBindVertexArray(GL_NONE);

详细实现代码见项目: github.com/githubhaoha…