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

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

相关推荐
龙之叶4 小时前
Android13源码下载和编译过程详解
android·linux·ubuntu
闲暇部落6 小时前
kotlin内联函数——runCatching
android·开发语言·kotlin
大渔歌_6 小时前
软键盘显示/交互问题
android
LuiChun14 小时前
webview_flutter_android 4.3.0使用
android·flutter
Tanecious.14 小时前
C语言--分支循环实践:猜数字游戏
android·c语言·游戏
闲暇部落16 小时前
kotlin内联函数——takeIf和takeUnless
android·kotlin
Android西红柿1 天前
flutter-android混合编译,原生接入
android·flutter
大叔编程奋斗记1 天前
【Salesforce】审批流程,代理登录 tips
android
程序员江同学1 天前
Kotlin 技术月报 | 2025 年 1 月
android·kotlin
爱踢球的程序员-11 天前
Android:View的滑动
android·kotlin·android studio