Android OpenGL开发学习(二)手把手教你实现抖音分屏相机


前言

上面文章介绍了一下OpenGL基本使用,由于接触OpenGL时间不长,理解不够深入,讲得不是很清楚,接下来用这篇文章,通过一个实际的开发例子,重新介绍一下OpenGL。


提示:话不多说,正文来了。

一、常规操作

上面文章已经介绍了OpenGL如何使用,下面直接上代码了。

 glSurfaceView = findViewById(R.id.camera_glsurface_view)
 glSurfaceView.setEGLContextClientVersion(2)
 myRender = MyRender()
 glSurfaceView.setRenderer(myRender)
 glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY

简单介绍一下上面代码,首先是设置OpenGL版本号,创建了一个自定义Renderer,把Renderer设置给当前GLSurfaceView,最后一行代码强调一下,是指GLSurfaceView刷新方式,一般选择这个模式,就是有变化的主动刷新一下,如果不设置的话,每隔一段时间,自动刷新,挺消耗资源的,一般选择上面这个。

二、使用步骤

1.创建SurfaceTexture

定义一个SurfaceTexture来显示处理后的数据,并实现OnFrameAvailableListener接口回调来通知GlSurfaceview渲染新的帧数据:

 private lateinit var mSurfaceTexture: SurfaceTexture

 override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
        glSurfaceView.requestRender()

    }

在说创建SurfaceTexture之前,我们先回顾一下,Renderer几个方法作用:

onSurfaceCreated():系统会在创建 GLSurfaceView
时调用一次此方法。使用此方法可执行仅需发生一次的操作,例如设置 OpenGL 环境参数或初始化 OpenGL 图形对象。
onDrawFrame():系统会在每次重新绘制 GLSurfaceView
时调用此方法。请将此方法作为绘制(和重新绘制)图形对象的主要执行点。
onSurfaceChanged():系统会在GLSurfaceView 几何图形发生变化(包括
GLSurfaceView大小发生变化或设备屏幕方向发生变化)时调用此方法。例如,系统会在设备屏幕方向由纵向变为横向时调用此方法。使用此方法可响应GLSurfaceView
容器中的更改。

如上所述,创建方法写在:

 override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
 GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
 mSurfaceTexture = SurfaceTexture(createOESTextureObject())
            ...

再看createOESTextureObject如何实现的:

fun createOESTextureObject(): Int {
       val tex: IntArray = IntArray(1)
        // 生成一个纹理
        GLES20.glGenBuffers(1, tex, 0)
        // 将此纹理绑定到外部纹理上
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, tex[0])
        // 设置纹理过滤参数
        GLES20.glTexParameterf(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
            GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST.toFloat()
        );
        GLES20.glTexParameterf(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
            GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR.toFloat()
        );
        GLES20.glTexParameterf(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
            GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE.toFloat()
        );
        GLES20.glTexParameterf(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
            GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE.toFloat()
        );
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
        return tex[0]
    }

主要是生成了一个纹理id,设置了一系列参数,最后返回。

2.自定义Renderer

重头戏,可以说OpenGL最重要的就是Renderer类,而绘制形状最重要的是两个着色器:

  • 顶点着色程序 - 用于渲染形状的顶点的 OpenGL ES 图形代码。
  • 片段着色程序-用于使用颜色或纹理渲染形状面的 OpenGL
    ES 代码。
    这是官方定义,其实简单来说,顶点着色器就是画形状,片段着色器就是上颜色,贴图
    再来张图片,看的更明白一些:

着色器
我们的形状都是由一个个三角形绘制而成,最后由片段着色器上色、贴图。
原理介绍完了,咋们看下代码:

 private val mPosCoordinate = floatArrayOf(
            -1f, -1f,
            -1f, 1f,
            1f, -1f,
            1f, 1f)
            
private val mTexCoordinateBackRight = floatArrayOf(
            1f, 1f,
            0f, 1f,
            1f, 0f,
            0f, 0f)

上面定义了顶点着色器和片段着色器的坐标,最后将我们定义好的坐标传入我们的着色程序代码里面。前面文章,我们是直接写在String里面,直接引用,实际编写代码是不会这么做的,我们写好的着色程序,是放在asserts文件里面的,然后读取的:

 val vertexSource = AssetsUtils.read(instance, "camera_vertexShader.glsl")
 val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource!!)
 val fragmentSource = AssetsUtils.read(instance, "camera_fragmentShader.glsl");
 val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource!!)
...

再看看我们着色器代码如何写的(建议大家在AS安装一个GLSL Support插件,有惊喜)
顶点着色器:

uniform mat4 textureTransform;
attribute vec2 inputTextureCoordinate;
attribute vec4 position;//NDK坐标点
varying   vec2 textureCoordinate;//纹理坐标点变换后输出

void main() {
    gl_Position = textureTransform * position;
    textureCoordinate = inputTextureCoordinate;
}

上面代码,简单说下,vec2、vec4分别代表两位float和四位float,其中gl_Position自带变量,textureTransform变换矩阵。
片段着色器:

#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES videoTex;// 图片 采样器
varying vec2 textureCoordinate;

void main() {
        vec4 tc = texture2D(videoTex, textureCoordinate);
        float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11; // 这里进行的颜色变换处理,传说中的黑白滤镜
        gl_FragColor = vec4(color,color,color,1.0);
}

gl_FragColor自带变量,上面代码实现了一个黑白滤镜。


3.坐标系

除了上面的着色器代码以外,OpenGL的坐标系尤其重要,因为我们了解它的坐标系,我们才知道形状如画绘制出来的。

OpenGl坐标系
我们看上面图片,主要有两种坐标,一种是绘制形状的,一种是贴图的纹理坐标,我们知道我们画图是有一个一个点连接起来的,OpenGL也是一样的,既然涉及到绘图,肯定有绘制顺序的,在OpenGL有专门的方法设置:

   GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mPosCoordinate1.size / 2)

OpenGL通常是GLES20.GL_TRIANGLE_STRIP这种绘制顺序,对应的就是左下角,右下角,左上角,右上角,敲黑板,这个是重点,因为我们的形状就是按这个顺序绘制出来的,你可以试着按逆时针顺序,连起来,你会发现我们画出来的形状不是矩形,会少一块。
同理,我们形状绘制出来了,贴图也要照着这个顺序绘出来。


4.OpenGL和Camera相结合

前面说了很多OpenGL相关的东西,最后我们的效果要结合Camera预览显示的。
我们知道Android相机目前有3种API,Camera1、Camera2、CameraX,这里图方便,就使用Camera1。

try {
                camera = Camera.open(0)
                camera.setPreviewTexture(mSurfaceTexture)
                camera.startPreview()
                camera.autoFocus(object : Camera.AutoFocusCallback {
                    override fun onAutoFocus(success: Boolean, camera: Camera?) {
                        if (success) {
//                            camera?.cancelAutoFocus();
                        }
                    }

                })
            } catch (e: Exception) {
                e.printStackTrace()
            }

这里我们将OpenGL处理过的mSurfaceTexture,直接设置给camera显示。
最后我们再看下OpenGL处理的代码:

uPosHandle = GLES20.glGetAttribLocation(mProgram, "position");
aTexHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "textureTransform");

mPosBuffer = convertToFloatBuffer(mPosCoordinate1)
mTexBuffer = convertToFloatBuffer(mTexCoordinateBackRight1);

GLES20.glVertexAttribPointer(uPosHandle, 2, GLES20.GL_FLOAT, false, 0, mPosBuffer)
GLES20.glVertexAttribPointer(aTexHandle, 2, GLES20.GL_FLOAT, false, 0, mTexBuffer)
// 启用顶点位置的句柄
GLES20.glEnableVertexAttribArray(uPosHandle)
GLES20.glEnableVertexAttribArray(aTexHandle)

主要是把之前传入的参数,配置OpenGL ES环境中。
最后在onDrawFrame更新画面:

   GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)
   mSurfaceTexture.updateTexImage()
   GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0)
   GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mPosCoordinate1.size / 2)


5.实际运行效果

看下代码运行的实际效果:
未处理前的图片
可以看到画面是颠倒的,前面我们说过OpenGL坐标系,我们试着将纹理坐标旋转90度试下,对应的坐标修改如下:

 private val mTexCoordinateBackRight1 = floatArrayOf(
         // 旋转90度
         1f,0f,
         1f,1f,
         0f,0f,
         0f,1f
)

这个坐标怎么来的呢,可以试着将纹理坐标图片旋转90度,然后按照之前的顺序,重新填入就可以了。
然后我们再看下效果:
旋转90度后的图片

我们可以看到字是对称的,根据前面的经验,我们把旋转系90度后图片,两边坐标换下不就可以了:

 private val mTexCoordinateBackRight1 = floatArrayOf(
         // 对称翻转
         1f,1f,
         1f,0f,
         0f,1f,
         0f,0f
)

修改后看下效果:
旋转90后再对称的图片
现在看是不是正常了,其实还有一种方法,不修改坐标,直接使用矩阵的方式(顶点着色器代码里面修改),大家可以试下。

6.分屏效果

现在画面正常了,还差最后一步分屏,分屏的效果其实很简单,我们知道分屏其实就是显示两个一模一样的画面,其实主要就是显示中间那部分画面,不属于中间的那部分,我们用中间的部分重复就行。
说的可能有点绕,看代码你们就明白了,由于显示画面,我们直接修改片段着色器代码:

    vec4 tc = texture2D(videoTex, textureCoordinate);
    float color = tc.r * 0.3 + tc.g * 0.59 + tc.b * 0.11; // 这里进行的颜色变换处理,传说中的黑白滤镜
    float x = textureCoordinate.x;
        if(x < 0.5) {
            x+=0.25;
        }else {
            x-=0.25;
        }
    gl_FragColor = texture2D(videoTex, vec2(x, textureCoordinate.y));
}

代码很简单啊,就是把x<0.5和x>0.5坐标进行变换显示中间的画面,看下效果:
二分屏图片
同理,三分屏呢:

   if (x < 1.0/3.0) {
       x+=1.0/3.0;
   } else if (x > 2.0/3.0){
       x-=1.0/3.0;
  }

替换之前x坐标代码就可以了,看下效果:
三分屏图片
我们上面实现的是横屏的分屏啊,竖屏的分屏,照葫芦画瓢就行,这里我就不写了。


7.项目地址

参考例子地址:GitHub地址,觉得不错的给个🌟。


总结

至此,我们把使用OpenGL实现抖音分屏效果全部讲完了,最后做个简单的总结吧。

  • 我们实现这样的效果,我们首先要知道OpenGL的基础知识,了解一些基本概念,感兴趣的可以看下上面文章,AndroidOpenGL开发学习(一)绘制简单图形,最重要的还是两个着色器。
  • 其次就是要了解OpenGL坐标系,这对我们绘制形状至关重要。
  • 最后,就是要了解一下编写着色器代码规则,我们甚至可以把别人的着色器代码反编译过来,自己使用。

Thanks:
Android openGl开发详解(二)——通过SurfaceView,TextureView,GlSurfaceView显示相机预览
OpenGL ES官方API

创作不易,觉得不错的话,请点赞、评论鼓励,谢谢。

下面文章预告一下,前面我们写了一些OpenGL单一效果的例子,复合效果怎么实现呢,如果期待下篇文章,敬请点赞啊。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值