转场
转场特效网址: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程序链接起来
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
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);
}
效果
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
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
根据转场效果,实践了下其他效果