干货 | 移动应用中使用OpenGL生成转场特效

作者简介

jzg,携程资深前端开发工程师,专注Android开发;

zcc,携程高级前端开发工程师,专注iOS开发。

一、前言

随着移动端短视频的火热,音视频编辑工具在做内容类APP上的地位举足轻重。丰富的转场方式可以给短视频带来更多炫酷的效果,从而更好地赢得用户青睐。本议题主要包含了对OpenGL的简单介绍及相关API使用,GLSL着色器语言的基本使用,以及如何通过编写自定义的着色器程序来实现图片的转场效果。

二、为什么使用OpenGL以及使用的难点

2.1 为什么使用OpenGL

视频的转场效果离不开图形的处理,移动设备在处理3D图形相关的计算时一般都会选择使用GPU。相较于CPU,GPU在图像动画处理时具有更高效的性能。移动设备以android为例,GPU处理提供了两套不同的API,分别是VulkanOpenGL ES。其中VulKan只支持 Android 7.0 以上的设备,OpenGL ES 则支持所有的 Android 版本,而iOS并没有对vulkan的官方支持。同时 OpenGL ES 作为 OpenGL 的子集,针对手机、PDA 和游戏主机等嵌入式设备去除了 glBegin/glEnd,四边形、多边形等复杂图元等许多非绝对必要的特性,消除它的冗余功能,从而提供了更容易学习和易于在移动图形硬件中实现的库。

目前,在短视频图像处理中, OpenGL ES 凭借良好的系统支持性和功能的高度精简性,成为了最广泛的 GPU 处理 API 之一。为了方便,本文中提到的 OpenGL 即表示 OpenGL ES

2.2 使用OpenGL处理视频转场的难点

使用OpenGL处理视频转场的难点是如何编写转场效果的着色器,关于这一点,我们可以参考开源的GLTransitions网站。该网站有很多开源的转场效果,我们可以借鉴并学习,下文会有较为详细的介绍。

三、OpenGL的基本介绍和转场应用

3.1 OpenGL的基本介绍

OpenGL 是一种开放式的图形库,用于渲染2D、3D矢量图形的跨语言,跨平台的应用程序编程接口。OpenGL 可以⽤来做什么?

  • 视频,图形,图⽚处理
  • 2D/3D 游戏引擎开发
  • 科学可视化
  • 医学软件开发
  • CAD(计算机辅助技术)
  • 虚拟实境(AR,VR)
  • AI ⼈⼯智能

我们使用OpenGL来处理视频转场,就是上面提到的用OpenGL来对视频、图形、图片进行处理。

3.1.1 OpenGL渲染流程

在使用OpenGL进行绘制时,我们主要关注的是顶点着色器片元着色器。顶点着色器用来确定绘制图形的顶点位置,片元着色器负责给图形添加颜色。主要绘制流程如下图:

渲染的流程有以下几步:

1)顶点数据的输入:

顶点数据用来为后面的顶点着色器等阶段提供处理的数据。

2)顶点着色器:

顶点着色器主要功能是进行坐标变换。

3)几何着色器:

与顶点着色器不同,几何着色器的输入是完整的图元(比如,点),输出可以是一个或多个其他的图元(比如,三角面),或者不输出任何的图元,几何着色器是可选的。

4)图元组装、光栅化:

图元组装将输入的顶点组装成指定的图元,经过图元组装以及屏幕映射阶段后,我们将物体坐标变换到了窗口坐标,光栅化是个离散化的过程,将3D连续的物体转化为离散屏幕像素点的过程。

5)片元着色器(片段着色器):

片元着色器用来决定屏幕上像素的最终颜色。

6)混合测试:

渲染的最后一个阶段是测试混合阶段。测试包括裁切测试、Alpha测试、模板测试和深度测试。没有经过测试的片段会被丢弃,不需要进行混合阶段,经过测试的片段会进入混合阶段。

经过以上几个步骤,OpenGL就能将最终的图形显示到屏幕上。

OpenGL绘制流程中,我们能够编码的就是Vertex Shader(顶点着色器) 和 Fragment Shader(片元着色器)。这也是渲染过程中必备的2个着色器。

Vertex Shader处理从客户端输入的数据、应用变换、进行其他的类型的数学运算来计算光照效果、位移、颜色值等。比如为了渲染共有3个顶点的三角形,Vertex Shader将执行3次,也就是为了每个顶点执行一次。

图中的3个顶点已经组合在一起,而三角形也已经逐个片段的进行了光栅化。每个片段通过执行Fragment Shader进行填充。Fragment Shader会输出我们屏幕上看到的最终颜色值。

在绘制图形的时候,我们会使用到OpenGL的多种状态变量,例如当前的颜色,控制当前视图和投影变换、直线和多边形点画模式、多边形绘图模式、像素包装约定、光照的位置和特征以及被绘制物体的材料属性等。可以设置它的各种状态(或模式),然后让这些状态一直生效,直到再次修改它们。

以把当前颜色设置为白色、红色或其他任何颜色,在此之后绘制的所有物体都将使用这种颜色,直到再次把当前颜色设置为其他颜色。许多表示模式的状态变量可以用glEnable()glDisable()。所以我们说OpenGL是一个状态机。

因为OpenGL在渲染处理过程中会顺序执行一系列操作,就如流水线作业一样,所以我们将OpenGL绘制的流程称为渲染管线,包括固定管线和可编程管线。我们使用的是可编程管线,在可编程管线里,顶点的位置、颜色、贴图座标、贴图传进来之后,如何对数据进行改动,产生的片元如何生成结果,可以很自由地控制。

下面就简单介绍一下管线和在可变编程管线中必不可少的GLSL(着色器语言)。

3.1.2 管线

管线:渲染管线可以理解为渲染流水线。指的是输入需要渲染的3D物体的相关描述信息数据(例:顶点坐标、顶点颜色、顶点纹理等),经过渲染管线一系列的变化和渲染过程,输出一帧最终的图像。简单理解就是一堆原始图形数据经过一个输送管道,期间经过各种变化处理最终出现展示到屏幕的过程。管线又分为固定管线和可编程管线两种。

固定管线:在渲染图像的过程,我们只能通过调用GLShaderManager类的固定管线效果实现一系列的着色器处理。

可编程管线:在渲染图像的过程,我们能够使用自定义顶点着色器和片元着色器的去处理数据的过程。由于OpenGL的使用场景非常丰富,固定管线或者存储着色器无法完成的任务,这时我们可以使用可编程管线去处理。

3.1.3 GLSL(OpenGL Shading Language)

OpenGL着色语言(OpenGL Shading Language)是用来在OpenGL中着色编码的语言,也即开发人员写的短小的自定义程序,他们是在GPU(Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。它可以得到当前OpenGL 中的状态,GLSL内置变量进行传递。GLSL其使用C语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。

GLSL的着色器代码分成2个部分:VertexShader(顶点着色器) 和 Fragment Shader(片元着色器)。

着色器Shader

着色器(Shader)是用来实现图像渲染的,用来替代固定渲染管线的可编辑程序。其中Vertex Shader(顶点着色器)主要负责顶点的几何关系等的运算,Pixel Shader(像素着色器)主要负责片源颜色等的计算。

顶点着色器VertexShader

顶点着色器是一个可编程的处理单元,一般用来处理图形每个顶点变换(旋转/平移/投影等)、光照、材质的应用与计算等顶点的相关操作。顶点着色器是逐顶点运算的程序,每个顶点数据都会执行一次。替代了原有固定管线的顶点变换、光照计算,采用GLSL进行开发 。我们可以根据自己的需求采用着色语言自行开发顶点变换、光照等功能,大大增加了程序的灵活性。

顶点着色器工作过程为将原始的顶点几何信息(顶点坐标、颜色、纹理)及其他属性传送到顶点着色器中,经过自定义的顶点着色程序处理产生变化后的顶点位置信息,将变化后的顶点位置信息传递给后续图元装配阶段,对应的顶点纹理、颜色等信息则经光栅化后传递到片元着色器。

顶点着色器的输入主要为待处理顶点相应的attributeuniform采样器以及临时变量,输出主要为经过顶点着色器后生成的varying及一些内建输出变量

顶点着色器示例代码:

代码语言:javascript
复制
//顶点位置attribute vec4 Position;//纹理坐标attribute vec2 TextureCoord;//纹理坐标 用于接收和传递给片元着色器的纹理坐标varying vec2 varyTextureCoord;void main() {    gl_Position = Position;    varyTextureCoord = TextureCoord;}
片元着色器FragmentShader
片元着色器是一个可编程的处理单元,一般用来处理图形中每个像素点颜色计算和填充、纹理的采样等操作。片元着色器是逐像素运算的程序,也就说每个像素都会执行一次片元着色器。
片元着色器是替换了OpenGL固定渲染管线阶段中纹理颜色求和、雾以及Alpha测试等阶段,采用GLSL进行开发 ,我们可以根据自己的需求采用着色语言自行开发。
片元着色器示例代码:
代码语言:javascript
复制
//高精度precision highp float;//用于接收顶点着色器的纹理坐标varying vec2 varyTextureCoord;//图片纹理uniform sampler2D Texture;//图片纹理uniform sampler2D Texture2;const vec2 direction = vec2(0.0, 1.0);void main(){vec2 p = varyTextureCoord.xy/vec2(1.0).xy;vec4 color = mix(texture2D(Texture, varyTextureCoord), texture2D(Texture2, varyTextureCoord), step(1.0-p.y,progress));gl_FragColor = vec4(color);}

3.1.4 三种向OpenGL着⾊器传递数据的⽅法

上面的顶点着色器和片元着色器里出现了attribute,varying,uniform等类型定义,下面就简单介绍一下这三种类型。

attribute

attribute:attribute变量是只能在顶点着色器中使用的变量,一般用attribute变量来表示一些顶点的数据,如:顶点坐标,法线,纹理坐标,顶点颜色等。

uniform

uniform:uniform变量是外部application程序传递给着色器的变量,uniform变量就像是C语言里面的常量,也就是说着色器只能用而不能修改uniform变量。

varying

varying:从顶点着色器传递到片元着色器的量,如用于传递到片元着色器中的顶点颜色,可以使用varying(易变变量)。

注意点: Attributes不能够直接传递给Fragment Shader,如果需要传递给Fragment Shader,则需要通过Vertex Shader间接的传递过去。而 UnifromTexture Data可以直接传递给Vertex ShaderFragment Shader,具体怎么传递,依需求而定。

3.1.5 如何使用OpenGL来绘制一张图片

上面介绍了顶点着色器和片元着色器,以及如何向OpenGL程序传递数据的方法。

现在我们就利用刚刚介绍的一些知识点,通过OpenGL程序将图片绘制到屏幕上,这也是制作图片轮播转场特效的前提。图片的绘制对于OpenGL来说就是纹理的绘制,这里只为了展示效果,不使用变换矩阵来处理图片的宽高比例,直接铺满整个窗口。

首先定义一个顶点着色器:

代码语言:javascript
复制
attribute vec4 a_position;//传入的顶点坐标attribute vec2 a_texCoord;//传入的纹理坐标varying vec2 v_texCoord;//传递给片元着色器的纹理坐标void main(){    gl_Position = a_position;//将顶点坐标赋值给OpenGL的内置变量    v_texCoord = a_texCoord;//将传入的纹理坐标传递给片元着色器}
代码语言:javascript
复制
再定义一个片元着色器:
代码语言:javascript
复制
precision mediump float;//定义float精度,纹理坐标使用的是一个float类型的二维向量vec2uniform sampler2D u_texture;//纹理varying vec2 v_texCoord;//纹理坐标void main(){    gl_FragColor = texture2D(u_texture, v_texCoord);//2D纹理采样,将颜色赋值给OpenGL的内置变量gl_FragColor}

再给出Android端使用这两个着色器绘制一个图片纹理的代码:

代码语言:javascript
复制
class SimpleImageRender(private val context: Context) : GLSurfaceView.Renderer {    //顶点坐标    private val vCoordinates = floatArrayOf(        -1.0f, -1.0f,        1.0f, -1.0f,        -1.0f, 1.0f,        1.0f, 1.0f    )    //纹理坐标    private val textureCoordinates = floatArrayOf(        0.0f, 1.0f,        1.0f, 1.0f,        0.0f, 0.0f,        1.0f, 0.0f    )    //OpenGL程序id    var programId = 0    //顶点坐标句柄    var vCoordinateHandle = 0    //纹理坐标句柄    var textureCoordinateHandle = 0    //纹理id    var textureId = 0    private val vertexBuffer =        ByteBuffer.allocateDirect(vCoordinates.size * 4).order(ByteOrder.nativeOrder())            .asFloatBuffer()            .put(vCoordinates)
    private val textureBuffer =        ByteBuffer.allocateDirect(textureCoordinates.size * 4).order(ByteOrder.nativeOrder())            .asFloatBuffer()            .put(textureCoordinates)
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {        vertexBuffer.position(0)        textureBuffer.position(0)        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)        //根据顶点着色器和片元着色器编辑链接OpenGL程序        programId =            loadShaderWithResource(context, R.raw.simple_image_vs, R.raw.simple_image_fs)        //获取顶点坐标的句柄        vCoordinateHandle = GLES20.glGetAttribLocation(programId, "a_position")        //获取纹理坐标的句柄        textureCoordinateHandle = GLES20.glGetAttribLocation(programId, "a_texCoord")        //生成纹理        val textureIds = IntArray(1)        GLES20.glGenTextures(1, textureIds, 0)        if (textureIds[0] == 0) {            return        }        textureId = textureIds[0]        //绑定纹理        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)        //环绕方式        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT)        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT)        //过滤方式        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
        val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.scene1)        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)        bitmap.recycle()    }
    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {        GLES20.glViewport(0, 0, width, height)    }
    override fun onDrawFrame(gl: GL10?) {        //清屏,清理掉颜色的缓冲区        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)        //设置清屏的颜色,这里是float颜色的取值范围的[0,1]        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
        //使用program        GLES20.glUseProgram(programId)
        //设置为可用的状态        GLES20.glEnableVertexAttribArray(vCoordinateHandle)        //size 指定每个顶点属性的组件数量。必须为1、2、3或者4。初始值为4。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a))        //stride 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。        //size 2 代表(x,y),stride 8 代表跨度 (2个点为一组,2个float有8个字节)        GLES20.glVertexAttribPointer(vCoordinateHandle, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer)
        GLES20.glEnableVertexAttribArray(textureCoordinateHandle)        GLES20.glVertexAttribPointer(            textureCoordinateHandle,            2,            GLES20.GL_FLOAT,            false,            8,            textureBuffer        )
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
    }}

这样就完成了一个图片的绘制:

3.2 OpenGL的转场特效应用

3.2.1 移植开源的转场效果

什么是转场效果?一般来说,就是两个视频画面之间的过渡衔接效果。在opengl中,图片的转场,其实就是两个纹理的过渡切换。在这里推荐一个开源项目,该项目主要用来收集各种GL转场特效及其 GLSL 实现代码,开发者可以很方便地移植到自己的项目中。

GLTransitions 项目网站地址

GLTransitions 项目有接近大概70种转场特效,能够非常方便的使用在图片或者视频的转场中,很多转场特效包含了混合、边缘检测、腐蚀膨胀等常见的图像处理方法,由易到难。

对于想学习 GLSL 的同学,既能快速上手,又能学习到一些高阶图像处理方法 GLSL 实现,强烈推荐。

由于glsl代码在各个平台都是通用的,所以将GLTransitions的效果移植到移动端也是比较简单的。现在我们以该网站的第一个转场效果为例,介绍一下移植的大致流程。

首先我们来看一下转场所需的片元着色器的代码,这是实现转场的关键。其中sign函数,mix函数,fract函数,step函数是glsl的内置函数。这里只为了展示效果,不使用变换矩阵来处理图片的宽高比例,直接铺满整个窗口。

代码语言:javascript
复制
uniform vec2 direction; // = vec2(0.0, 1.0)
vec4 transition (vec2 uv) {  vec2 p = uv + progress * sign(direction);  vec2 f = fract(p);  return mix(    getToColor(f),    getFromColor(f),    step(0.0, p.y) * step(p.y, 1.0) * step(0.0, p.x) * step(p.x, 1.0)  );}

我们可以看到,从GLTransitions的片元着色器代码已经提供了转场效果,但是还需要使用者进行一些修改。以上面的代码为例,需要我们自己定义一个转场进度的变量progress(取值为0到1的浮点数)。还有转场最基本的两个要素,即图片纹理,一个转场需要两个图片纹理,从纹理1过渡到纹理2,getToColor和getFromColor就是对纹理1和纹理2取色的函数。当然还有必不可少的main函数,将我们程序计算的颜色赋值给gl_FragColor,所以我们要将上面的片元着色器代码修改一下。如下:

代码语言:javascript
复制
precision mediump float;uniform vec2 direction;// = vec2(0.0, 1.0)uniform float progress;//转场的进度uniform sampler2D u_texture0;//纹理1uniform sampler2D u_texture1;//纹理2varying vec2 v_texCoord;//纹理坐标vec4 transition (vec2 uv) {    vec2 p = uv + progress * sign(direction);    vec2 f = fract(p);    return mix(    texture2D(u_texture1, f),    texture2D(u_texture0, f),    step(0.0, p.y) * step(p.y, 1.0) * step(0.0, p.x) * step(p.x, 1.0)    );}
void main(){    gl_FragColor = transition(v_texCoord);}

这里也顺便给出顶点着色器的代码,主要就是设置顶点坐标和纹理坐标,关于这两个坐标上文已经介绍过了,这里就不赘述了。代码如下:

代码语言:javascript
复制
attribute vec4 a_position;attribute vec2 a_texCoord;varying vec2 v_texCoord;void main(){    gl_Position = a_position;    v_texCoord = a_texCoord;}

现在顶点着色器和片元着色器这两个关键的着色器程序都有了,一个基本的转场就实现了。只要在我们的程序中使用这两个着色器,在绘制的时候根据当前的帧数不停地更新两个纹理和转场的进度就可以了。

下面给出绘制时的代码逻辑,以安卓为例:

代码语言:javascript
复制
frameIndex++ //每次绘制修并记录绘制的帧数        //使用program        GLES20.glUseProgram(programId)
        //设置为可用的状态        GLES20.glEnableVertexAttribArray(vCoordinateHandle)        //size 指定每个顶点属性的组件数量。必须为1、2、3或者4。初始值为4。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a))        //stride 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。        //size 2 代表(x,y),stride 8 代表跨度 (2个点为一组,2个float有8个字节)        GLES20.glVertexAttribPointer(vCoordinateHandle, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer)
        GLES20.glEnableVertexAttribArray(textureCoordinateHandle)        GLES20.glVertexAttribPointer(            textureCoordinateHandle,            2,            GLES20.GL_FLOAT,            false,            8,            textureBuffer        )
        val uTexture0Handle = GLES20.glGetUniformLocation(programId, "u_texture0")        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)        GLES20.glBindTexture(            GLES20.GL_TEXTURE_2D,            imageTextureIds[(frameIndex / transitionFrameCount) % imageNum]        )        GLES20.glUniform1i(uTexture0Handle, 0)
        val uTexture1Handle = GLES20.glGetUniformLocation(programId, "u_texture1")        GLES20.glActiveTexture(GLES20.GL_TEXTURE1)        GLES20.glBindTexture(            GLES20.GL_TEXTURE_2D,            imageTextureIds[(frameIndex / transitionFrameCount + 1) % imageNum]        )        GLES20.glUniform1i(uTexture1Handle, 1)
        val directionHandle = GLES20.glGetUniformLocation(programId, "direction")        GLES20.glUniform2f(directionHandle, 0f, 1f)
        val uOffsetHandle = GLES20.glGetUniformLocation(programId, "u_offset")        val offset = (frameIndex % transitionFrameCount) * 1f / transitionFrameCount        GLES20.glUniform1f(uOffsetHandle, offset)        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

以上就是将一个GLTransitions网站中的转场特效移植到Android端的基本流程。iOS的也是类似的,非常方便。

3.2.2 实现复杂转场效果

通过上面的介绍,我们已经对如何使用opengl来处理图片转场有了一个简单的了解。但是刚刚的操作只能让多张图片都使用同一种转场,这样比较单调乏味。下面介绍一个思路,在用多张图片合成转场效果时,将不同的转场效果组合起来使用。

回想一下,刚刚做转场移植的时候,只是使用了一个opengl程序。现在咱们来加载多个opengl程序,然后在不同的时间段使用对应的opengl程序,这样就能比较方便地实现多个转场效果的组合使用了。

首先定义一个IDrawer接口,表示一个使用opengl程序的对象:

代码语言:javascript
复制
interface IDrawer {    //准备阶段,准备程序,资源    fun onPrepare()    //绘制    fun onDraw(frameIndex:Int){}
    fun onSurfaceChanged(p0: GL10?, width: Int, height: Int){
    }}

然后定义一个render,来控制如何使用这些IDrawer:

代码语言:javascript
复制
class ComposeRender : GLSurfaceView.Renderer {    private var frameIndex = 0//当前绘制了多少帧    private var drawersFrames = 0 //所有的drawer绘制一遍需要的帧数,目前每一个drawer占用200帧    private val framesPerDrawer = 200//每一个IDrawer绘制所需要的帧数,这里暂时固定为200
    //使用的IDrawer集合    private val drawers = mutableListOf(        HelloWorldTransitionDrawer(),        SimpleTransitionDrawer(),        PerlinTransitionDrawer(),    )
    init {        drawersFrames = drawers.size.times(framesPerDrawer)    }
    override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {        //设置清屏的颜色,这里是float颜色的取值范围的[0,1]        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)        //清屏,清理掉颜色的缓冲区        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)        drawers.forEach {            it.onPrepare()        }    }
    override fun onSurfaceChanged(p0: GL10?, p1: Int, p2: Int) {        GLES20.glViewport(0, 0, p1, p2)        drawers.forEach {            it.onSurfaceChanged(p0, p1, p2)        }    }
    override fun onDrawFrame(p0: GL10?) {        frameIndex++        //清屏,清理掉颜色的缓冲区        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)        val offset = frameIndex % drawersFrames        val logicFrame = if (offset == 0) 1 else offset        //计算当前的帧数轮到哪个IDrawer的绘制,让对应的IDrawer进行绘制        drawers.forEachIndexed { index, iDrawer ->            if (logicFrame <= (index + 1).times(framesPerDrawer) && logicFrame >= index.times(                    framesPerDrawer                )            ) {                iDrawer.onDraw(logicFrame - index.times(framesPerDrawer))            }        }    }}

这里为了方便展示流程,先将纹理和每个转场的耗时(即使用的帧数)的使用固定值写在代码里。比如现在有四张图片编号为1,2,3,4,我们就定义三个IDrawer A,B,C。A使用图片1和图片2,B使用图片2和图片3,C使用图片3和图片4,然后每个转场都耗时200帧,这样就能实现三个opengl程序的组合转场了。

下面给出其中一个IDrawer的实现类:

代码语言:javascript
复制
class HelloWorldTransitionDrawer() : IDrawer {    private val imageNum = 2//需要使用两个图片纹理
    //转场需要耗费的帧数,这里固定写200帧    private val transitionFrameCount = 200    private val vCoordinates = floatArrayOf(        -1.0f, -1.0f,        1.0f, -1.0f,        -1.0f, 1.0f,        1.0f, 1.0f    )    private val textureCoordinates = floatArrayOf(        0.0f, 1.0f,        1.0f, 1.0f,        0.0f, 0.0f,        1.0f, 0.0f    )    var programId = 0    var vCoordinateHandle = 0    var textureCoordinateHandle = 0    var imageTextureIds = IntArray(imageNum)    private val vertexBuffer =        ByteBuffer.allocateDirect(vCoordinates.size * 4).order(ByteOrder.nativeOrder())            .asFloatBuffer()            .put(vCoordinates).position(0)
    private val textureBuffer =        ByteBuffer.allocateDirect(textureCoordinates.size * 4).order(ByteOrder.nativeOrder())            .asFloatBuffer()            .put(textureCoordinates).position(0)
    override fun onPrepare() {        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)        programId =            loadShaderWithResource(                MyApplication.getApp(),                R.raw.helloworld_transition_vs,                R.raw.helloworld_transition_fs            )        vCoordinateHandle = GLES20.glGetAttribLocation(programId, "a_position")        textureCoordinateHandle = GLES20.glGetAttribLocation(programId, "a_texCoord")        //生成纹理        val textureIds = IntArray(1)        GLES20.glGenTextures(1, textureIds, 0)        if (textureIds[0] == 0) {            return        }        loadTextures(intArrayOf(R.drawable.scene1, R.drawable.scene2))    }
    override fun onDraw(frameIndex:Int) {        //使用program        GLES20.glUseProgram(programId)
        //设置为可用的状态        GLES20.glEnableVertexAttribArray(vCoordinateHandle)        //size 指定每个顶点属性的组件数量。必须为1、2、3或者4。初始值为4。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a))        //stride 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。        //size 2 代表(x,y),stride 8 代表跨度 (2个点为一组,2个float有8个字节)        GLES20.glVertexAttribPointer(vCoordinateHandle, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer)
        GLES20.glEnableVertexAttribArray(textureCoordinateHandle)        GLES20.glVertexAttribPointer(            textureCoordinateHandle,            2,            GLES20.GL_FLOAT,            false,            8,            textureBuffer        )
        val uTexture0Handle = GLES20.glGetUniformLocation(programId, "u_texture0")        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)        GLES20.glBindTexture(            GLES20.GL_TEXTURE_2D,            imageTextureIds[0]        )        GLES20.glUniform1i(uTexture0Handle, 0)
        val uTexture1Handle = GLES20.glGetUniformLocation(programId, "u_texture1")        GLES20.glActiveTexture(GLES20.GL_TEXTURE1)        GLES20.glBindTexture(            GLES20.GL_TEXTURE_2D,            imageTextureIds[1]        )        GLES20.glUniform1i(uTexture1Handle, 1)
        val directionHandle = GLES20.glGetUniformLocation(programId, "direction")        GLES20.glUniform2f(directionHandle, 0f, 1f)
        val uOffsetHandle = GLES20.glGetUniformLocation(programId, "u_offset")        val offset = (frameIndex % transitionFrameCount) * 1f / transitionFrameCount        GLES20.glUniform1f(uOffsetHandle, offset)        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)    }
    private fun loadTextures(resIds: IntArray) {        if (resIds.isEmpty()) return        //直接生成两个纹理        GLES20.glGenTextures(2, imageTextureIds, 0)        resIds.forEachIndexed { index, resId ->            if (imageTextureIds.indexOfFirst {                    it == 0
                } == 0) return            GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + index)            //绑定纹理            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, imageTextureIds[index])            //环绕方式            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT)            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT)            //过滤方式            GLES20.glTexParameteri(                GLES20.GL_TEXTURE_2D,                GLES20.GL_TEXTURE_MIN_FILTER,                GLES20.GL_LINEAR            )            GLES20.glTexParameteri(                GLES20.GL_TEXTURE_2D,                GLES20.GL_TEXTURE_MAG_FILTER,                GLES20.GL_LINEAR            )
            val bitmap = BitmapFactory.decodeResource(MyApplication.getApp().resources, resId)            GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)            bitmap.recycle()        }    }}

这样就可以达到将多个转场组合使用的目的。

四、总结

在移动端进行图形处理时,OpenGL凭借其效率高,兼容性好的优势,得到了大家的青睐。

本文对OpenGL的基本概念和绘制流程进行了简单介绍,让大家对OpenGL的绘制流程有了一个初步的认识。在绘制流程中,对我们开发者比较重要的是使用GLSL来编写顶点着色器和片元着色器。在使用OpenGL处理图片轮播转场时,关键点是编写转场所需的着色器,我们可以参考GLTransitions网站的开源转场效果。该网站提供丰富的转场效果和着色器代码,可以很方便的移植到客户端中。

对于实现复杂转场,即将多个转场效果组合使用,本文也提供了一个思路,就是组合使用多个OpenGL程序,在对应的时间点加载并使用对应的OpenGL程序。

鉴于篇幅原因,本文分享了部分我们基于OpenGL开发视频转场特效的思考与实践,希望对大家有所帮助,欢迎更多关于音视频编辑的实践和交流。

【推荐阅读】

  • Taro性能优化之复杂列表篇
  • 携程小程序生态之Taro跨端解决方案
  • 携程活动搭建平台的前端“开放性”建设探索
  • 携程基于 GraphQL 的前端 BFF 服务开发实践

 “携程技术”公众号

  分享,交流,成长