Android OpenGL(八)转场特效

转场

转场特效网址:https://gl-transitions.com/gallery

介绍

什么是转场特效:从一个镜头转到另一个镜头,让两个不相关的场景可以平滑过渡。

为什么使用转场特效:镜头转场对普通人难度太大,特效转场更容易实现。特效转场应用场景丰富。

原理

  • 每个转场特效就是一个很短的视频
  • 因此它是由多个视频帧组成的
  • 每秒钟播放的帧越多,转场效果越丝滑
  • Android手机一般60帧/s

关键帧

  • 转场特效很多视频帧组成
  • 但是关键帧数量并不多
  • 制作专场特效时,至少要有2个关键帧
  • 其它帧都是由关键帧生成的
    制作专场还有一个progress的概念:插帧的数量由progress决定,假设你想转场特效有120帧,则每次progress增加1/120,很多特效都需要以progress为参数,假设转场特效有120帧,帧率是60帧/s,则转场耗时2s。

视频帧与shader的关系

  • 每次调用onDrawFrame就绘制一帧
  • 绘制帧时,每个像素调用一次fragment shader

重要API

  • smoothstep(e0,e1,x):两个边界:上边界e0,下边界e1,当x小于等于上边界,就是0,大于等于下边界就是1,中间时为平滑值
  • mix(f,b,alhpa):根据alpha值确定,采用f还是b,如果alpha为0,就用f,alpha为1则用b,二者之间就混合到一起,计算一个值
  • step(e,x):与smoothstep类似,但是只有一个边界,如果x小于边界就为0,如果x大于边界就为1
  • distance(p1,p2):计算两点之间的距离

擦除特效

擦除特效算法

  • 通过progress控制进度
  • 如果当前坐标小于progress
  • 显示第二张图的像素颜色,否则显示第一张图的像素颜色

步骤

  • 加载图片到项目,并在Render中进行加载
  • 创建assets目录,用于存放glsl文件
  • 创建ShaderUtil,用于读取glsl文件
  • 在Render中使用ShaderUtil中的方法
  • 在.glsl中编写shader

ShaderUtil

  • readFileFromAssets,从assets中读glsl文件
  • loadShader ,加载并编译Shader程序
  • createGLProgream,将编译好的shader程序链接起来
kotlin 复制代码
import android.content.Context
import android.graphics.Bitmap
import android.opengl.GLES30
import android.opengl.GLUtils
import android.util.Log

class ShaderUtil {
    companion object {

        fun readFileFromAssets(fileName: String?, context: Context): String? {
            fileName ?: return null
            context ?: return null
            val result = StringBuilder()
            try {
                val myIs = context.assets.open(fileName)
                val buffer = ByteArray(1024)
                var count = 0
                while ((myIs.read(buffer).also { count = it })!= -1) {
                    result.append(String(buffer, 0, count))
                }
                myIs.close()
            } catch (e: Exception) {
                e.printStackTrace()
            }
            return result.toString()
        }

        /**
         * 创建shader,加载shader程序
         */
        private fun loadShader(type: Int, shaderCode: String): Int {
            var shader = GLES30.glCreateShader(type)
            if (shader != 0) {
                GLES30.glShaderSource(shader, shaderCode)
                GLES30.glCompileShader(shader)
                val compiled = IntArray(1)
                GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0)
                if (compiled[0] == 0) {
                    Log.e("wdf", "编译失败=" + GLES30.glGetShaderInfoLog(shader))
                    GLES30.glDeleteShader(shader)
                    shader = 0
                }
            }
            return shader
        }

        /**
         * 加载纹理
         */
         fun loadTexture(bitmap: Bitmap, textureIdArray: IntArray) {
            GLES30.glGenTextures(1, textureIdArray, 0)
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIdArray[0])


            GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR)
            GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR)
            // 加载bitmap到纹理中
            GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, bitmap, 0)
            GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D)
            bitmap.recycle()
            // 取消绑定纹理
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0)
        }

        fun createGlProgram(vertexShader: String?, fragmentShader: String?): Int {
            if (vertexShader == null || fragmentShader == null) {
                return 0
            }
            val vertexShaderId = loadShader(GLES30.GL_VERTEX_SHADER, vertexShader)
            val fragmentShaderId = loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentShader)
            if (vertexShaderId == 0 || fragmentShaderId == 0) {
                return 0
            }
            var program = GLES30.glCreateProgram()
            if (program != 0) {
                program.let {
                    // 将顶点着色器加入程序
                    GLES30.glAttachShader(it, vertexShaderId)
                    // 将片元着色器加入程序
                    GLES30.glAttachShader(it, fragmentShaderId)
                    // 链接到着色器程序
                    GLES30.glLinkProgram(it)
                    val linkStatus = IntArray(1)
                    GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkStatus, 0)
                    if (linkStatus[0] != GLES30.GL_TRUE) {
                        val info = GLES30.glGetProgramInfoLog(it)
                        GLES30.glDeleteProgram(program)
                        // 打印链接程序日志
                        Log.e("wdf", "link失败==" + info)
                        program = 0
                    }
                }
            }

            return program
        }
    }


}

擦除效果的glsl

kotlin 复制代码
uniform sampler2D uSmapler;
uniform sampler2D uDstSampler;
varying vec2 vTexCoord;
uniform float progress;

vec4 getFromColor(vec2 uv){
    return texture2D(uSmapler, uv);
}
vec4 getDstColor(vec2 uv){
    return texture2D(uDstSampler, uv);
}

vec4 transition(vec2 uv){
    vec4 a=getFromColor(uv);
    vec4 b=getDstColor(uv);
    // x < progress 使用a,否则使用b颜色
    return mix(a, b, step(uv.x, progress));
}
void main(){
    gl_FragColor=transition(vTexCoord);
}

效果

kotlin 复制代码
import android.content.Context
import android.graphics.Bitmap
import android.opengl.GLES30
import android.opengl.GLSurfaceView
import android.opengl.GLUtils
import android.opengl.Matrix
import android.util.Log
import com.df.openglstudydemo.util.ShaderUtil
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10

class GLTransitionRender(val context: Context, val bitmap: Bitmap, val dstBitmap: Bitmap) : GLSurfaceView.Renderer {
    /**
     * 三角形顶点位置
     */
    private val coodData = floatArrayOf(
        // 顶点坐标        纹理坐标
        -1f, 1f, 0.0f, 0f, 0f,   // 左上角
        -1f, -1f, 0.0f, 0f, 1f,  //左下角
        1f, 1.0f, 0.0f, 1f, 0f,   //右上角
        1f, -1f, 0.0f, 1f, 1f  //右下角
    )

    private var translateMatrix = FloatArray(16)
    private var program: Int = 0
    private var positionHandle: Int = -1
    private var texCoordHandle: Int = -1
    private var samplerHandle: Int = -1
    private var dstSamplerHandle: Int = -1
    private var progressHandle: Int = -1
    private var uMatrixHandle: Int = -1


    // vbo
    private var vboId = IntArray(1)

    // 纹理id array
    private var textureIds = IntArray(1)
    private var lutTextureIds = IntArray(1)
    private val FRAME_SIZE = 120
    private var frameIndex = 0


    private lateinit var coordBuffer: FloatBuffer
    private lateinit var byteBuffer: ByteBuffer

    override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
        // 清理缓存
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
        // 顶点坐标内存申请
        createFloatBuffer()
        // 创建定点着色程序
        val vertexShader = ShaderUtil.readFileFromAssets("vertex.glsl", context)
        val fragmentShader = ShaderUtil.readFileFromAssets("fragment_wipe.glsl", context)
        program = ShaderUtil.createGlProgram(vertexShader, fragmentShader)
        // 生成VBO
        GLES30.glGenBuffers(1, vboId, 0)
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vboId[0])
        GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, byteBuffer.capacity(), byteBuffer, GLES30.GL_STATIC_DRAW)
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0)

        //
        Matrix.setIdentityM(translateMatrix, 0)

        // 将数据传递shader
        positionHandle = GLES30.glGetAttribLocation(program, "aPosition")
        GLES30.glEnableVertexAttribArray(positionHandle)

        texCoordHandle = GLES30.glGetAttribLocation(program, "aTexCoord")
        GLES30.glEnableVertexAttribArray(texCoordHandle)

        uMatrixHandle = GLES30.glGetUniformLocation(program, "uTMatrix")
        samplerHandle = GLES30.glGetUniformLocation(program, "uSampler")
        dstSamplerHandle = GLES30.glGetUniformLocation(program, "uDstSampler")
        progressHandle = GLES30.glGetUniformLocation(program, "progress")


        ShaderUtil.loadTexture(bitmap, textureIds)
        ShaderUtil.loadTexture(dstBitmap, lutTextureIds)
    }

    private fun createFloatBuffer() {
        // 申请物理层空间
        byteBuffer = ByteBuffer.allocateDirect(coodData.size * 4).apply {
            this.order(ByteOrder.nativeOrder())
        }
        // 坐标数据转换
        coordBuffer = byteBuffer.asFloatBuffer()
        coordBuffer.put(coodData, 0, coodData.size)
        coordBuffer.position(0)
    }

    override fun onSurfaceChanged(p0: GL10?, width: Int, height: Int) {
        // 计算并设置窗口大小
        val imgRatio = bitmap.width.toFloat().div(bitmap.height)
        val windowRatio = width.toFloat().div(height)
        GLES30.glViewport(0, 0, width, height)
        // 1. 矩阵数组
        // 2. 结果矩阵起始的偏移量
        // 3. left:x的最小值
        // 4. right:x的最大值
        // 5. bottom:y的最小值
        // 6. top:y的最大值
        // 7. near:z的最小值
        // 8. far:z的最大值
        // 由于是正交矩阵,所以偏移量为0,near 和 far 也不起作用
        if (imgRatio > windowRatio) {
            Matrix.orthoM(translateMatrix, 0, -1f, 1f, -imgRatio * 2f, imgRatio * 2f, 0f, 10f);
        } else {
            Matrix.orthoM(translateMatrix, 0, -(1 / imgRatio) * 2f, (1 / imgRatio) * 2f, -1f, 1f, 0f, 10f);
        }
    }

    override fun onDrawFrame(p0: GL10?) {
        if (program <= 0) {
            return
        }
        GLES30.glUseProgram(program)
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vboId[0])
        program?.let {
            GLES30.glVertexAttribPointer(positionHandle, 3, GLES30.GL_FLOAT, false, 5 * Float.SIZE_BYTES, 0)
            GLES30.glVertexAttribPointer(texCoordHandle,
                2,
                GLES30.GL_FLOAT,
                false,
                5 * Float.SIZE_BYTES,
                3 * Float.SIZE_BYTES)

            GLES30.glUniformMatrix4fv(uMatrixHandle, 1, false, translateMatrix, 0)

            // 激活纹理单元
            GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
            // 绑定纹理单元
            GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0])
            // 第二个参数传递激活的纹理单元,因为激活的是GLES30.GL_TEXTURE0,因此传递0
            GLES30.glUniform1i(samplerHandle, 0)
            if (dstSamplerHandle > 0) {
                // 激活纹理单元
                GLES30.glActiveTexture(GLES30.GL_TEXTURE1)
                // 绑定纹理单元
                GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, lutTextureIds[0])
                // 第二个参数传递激活的纹理单元,因为激活的是GLES30.GL_TEXTURE1,因此传递1
                GLES30.glUniform1i(dstSamplerHandle, 1)
            }
            val progress = (frameIndex++ % FRAME_SIZE) * 1.0 / FRAME_SIZE
            if (progressHandle > 0) {
                GLES30.glUniform1f(progressHandle, progress.toFloat())
            }
            GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4)
            GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0)
        }

    }
}

activity

kotlin 复制代码
class OpenGlTransitionActivity : AppCompatActivity() {
    private var glSurfaceView: GLSurfaceView? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_opengl_img_demo)
        glSurfaceView = findViewById(R.id.glSurfaceView_img)
        glSurfaceView?.setEGLContextClientVersion(3)
        glSurfaceView?.setRenderer(GLTransitionRender(this,
            BitmapFactory.decodeResource(resources, R.drawable.flower),
            BitmapFactory.decodeResource(resources, R.drawable.maomi)))
        glSurfaceView?.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
    }
}

其他专场效果实践

https://gl-transitions.com/gallery\](https://gl-transitions.com/gallery

根据转场效果,实践了下其他效果

相关推荐
androidwork12 小时前
Android LinearLayout、FrameLayout、RelativeLayout、ConstraintLayout大混战
android·java·kotlin·androidx
每次的天空12 小时前
Android第十三次面试总结基础
android·面试·职场和发展
wu_android12 小时前
Android 相对布局管理器(RelativeLayout)
android
李斯维14 小时前
循序渐进 Android Binder(二):传递自定义对象和 AIDL 回调
android·java·android studio
androidwork14 小时前
OkHttp 3.0源码解析:从设计理念到核心实现
android·java·okhttp·kotlin
像风一样自由15 小时前
【001】frida API分类 总览
android·frida
casual_clover15 小时前
Android 之 kotlin 语言学习笔记四(Android KTX)
android·学习·kotlin
移动开发者1号16 小时前
Android 大文件分块上传实战:突破表单数据限制的完整方案
android·java·kotlin
移动开发者1号16 小时前
单线程模型中消息机制解析
android·kotlin
每次的天空19 小时前
Android第十五次面试总结(第三方组件和adb命令)
android