Android OpenGL ES 11.1 相机

课程介绍

本节课主要讲解如何通过OpenGL将相机获取到的内容渲染到画面上,而拍摄、录像、相机API则不是本节课的重点。

相机画面的渲染和视频的渲染都比较接近,OpenGL部分基本一致,主要是流程上需要对应处理下,所以,直接看本章节的朋友,可以先看下上一章Android OpenGL ES 10.1 视频播放器

一. 视频播放器搭建

1. 视图容器

界面视图容器依旧使用GLSurfaceView,绘制方式是RENDERMODE_CONTINUOUSLY持续绘制的模式(课程演示,减少框架部分,相应的有不必要的性能损耗)。

2. 必要框架

因为本节涉及相机API、相机权限,该部分并非课程重点,因此详情见GLStudio工程代码。本课程中相机API参考自相机API

①. 绑定纹理ID

//  1. 创建纹理ID
    val textureIds = ......
//  2. 创建SurfaceTexture、Surface,并绑定到Camera上,接收画面驱动回调
    surfaceTexture = SurfaceTexture(textureIds[0])
    surfaceTexture!!.setOnFrameAvailableListener(this)
    camera.setPreviewTexture(surfaceTexture)

这里分为以下几步:

  1. 创建纹理ID,用于GL渲染
  2. 通过纹理ID创建一个SurfaceTexture对象
  3. 将SurfaceTexture对象传入Camera中

通过以上3步,将纹理ID和Camera进行关联。

②. 接收画面解析完毕的回调

当Camera捕捉到新的画面时,Camera会通过层层的接口调用到SurfaceTexture的onFrameAvailable接口,这时候我们可以标志下画面已经解析完毕。

/**
 * Camera有新的画面帧刷新时,通过SurfaceTexture的onFrameAvailable接口进行回调
 */
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
    updateSurface = true
}

③. 驱动画面更新纹理ID

override fun onDrawFrame(glUnused: GL10) {
    if (updateSurface) {
        // 当有画面帧解析完毕时,驱动SurfaceTexture更新纹理ID到最近一帧解析完的画面,并且驱动底层去解析下一帧画面
        surfaceTexture!!.updateTexImage()
        updateSurface = false
    }
    // ...之后通过纹理ID绘制画面
}

在这里必须将updateTexImage放在onDrawFrame中进行调用,而不能放在第二步的onFrameAvailable方法中,因为这个方法内部介绍了,必须是在GL线程中进行调用,而不能在主线程中。而GL线程的生命周期方法有onSurfaceCreated、onSurfaceChanged、onDrawFrame,想要不停地更新画面,自然是放在onDrawFrame中最为合适。

到这里,可以发现整个流程和视频播放器基本一致。区别在Camera渲染容器传入的是SurfaceTexture,而MediaPlayer传入的是Surface,但二者根本原理是一致的,只是API封装了而已。

二. GL渲染相机画面

Shader代码部分,与视频部分完全一致。

之前课程中,我们渲染的纹理都是2D纹理,而在纹理绘制的课程中,有介绍到纹理单元是包括多种类型的纹理目标,包括了GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP、GL_TEXTURE_OES,而本节课用到了GL_TEXTURE_OES这个类型的纹理目标。

要启用GL_TEXTURE_OES这种拓展类型的纹理目标,需要对之前课程中的2D纹理进行以下修改。

1. 修改纹理类型声明

private const val FRAGMENT_SHADER = """
        #extension GL_OES_EGL_image_external : require
        precision mediump float;
        varying vec2 v_TexCoord;
        uniform samplerExternalOES u_TextureUnit;
        void main() {
            gl_FragColor = texture2D(u_TextureUnit, v_TexCoord);
        }
        """

在编写Shader时,需要声明当前使用的是拓展纹理GL_OES_EGL_image_external,然后采样器类型必须是samplerExternalOES。

另外经过测试,在Fragment Shader中,不能同时启用2D纹理和OES纹理的绘制,即使只是声明而不执行也是不可以的。

2. 创建纹理、绘制纹理

创建纹理、绘制纹理时,我们绑定的纹理目标类型必须改为是OES类型的。

GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId)

在完成这一步时,应该就可以进行相机画面的播放了,不过还会有些小问题,比如比例问题,角度问题,前置摄像头翻转问题,下面会为大家解决这几个问题。

三. 解决相机角度问题

相机问题和上节课视频的一样,前置和后置摄像头都会自带一个方向角度(大部分都是后置90°,前置270°),导致预览到的画面和预期的不一样,这时候需要我们对画面方向进行处理。

问题效果图如下:

Camera_Error_Rotation.png

想要解决角度问题,可以通过旋转顶点坐标或旋转纹理坐标的方式去实现,这里我采用的是旋转顶点坐标的方式,工具类如下:

/**
 * 顶点旋转工具类
 *
 * @author Benhero
 * @date   2019/2/11
 */
object VertexRotationUtil {
    enum class Rotation {
        NORMAL, ROTATION_90, ROTATION_180, ROTATION_270;
    }

    fun getRotation(rotation: Int): Rotation {
        return when (rotation) {
            0 -> VertexRotationUtil.Rotation.NORMAL
            90 -> VertexRotationUtil.Rotation.ROTATION_90
            180 -> VertexRotationUtil.Rotation.ROTATION_180
            270 -> VertexRotationUtil.Rotation.ROTATION_270
            else -> VertexRotationUtil.Rotation.NORMAL
        }
    }

    fun rotate(srcArray: FloatArray, rotation: Int): FloatArray {
        return VertexRotationUtil.rotate(getRotation(rotation), srcArray)
    }

    fun rotate(rotation: VertexRotationUtil.Rotation, srcArray: FloatArray): FloatArray {
        return when (rotation) {
            VertexRotationUtil.Rotation.ROTATION_90 -> floatArrayOf(
                    srcArray[2], srcArray[3],
                    srcArray[4], srcArray[5],
                    srcArray[6], srcArray[7],
                    srcArray[0], srcArray[1])
            VertexRotationUtil.Rotation.ROTATION_180 -> floatArrayOf(
                    srcArray[4], srcArray[5],
                    srcArray[6], srcArray[7],
                    srcArray[0], srcArray[1],
                    srcArray[2], srcArray[3])
            VertexRotationUtil.Rotation.ROTATION_270 -> floatArrayOf(
                    srcArray[6], srcArray[7],
                    srcArray[0], srcArray[1],
                    srcArray[2], srcArray[3],
                    srcArray[4], srcArray[5])
            else -> srcArray
        }
    }

    fun flip(srcArray: FloatArray, isVertical: Boolean = false, isHorizontal: Boolean = false): FloatArray {
        var temp = floatArrayOf(
                srcArray[0], srcArray[1],
                srcArray[2], srcArray[3],
                srcArray[4], srcArray[5],
                srcArray[6], srcArray[7])
        temp = if (isVertical) floatArrayOf(
                temp[2], temp[3],
                temp[0], temp[1],
                temp[6], temp[7],
                temp[4], temp[5]) else temp
        temp = if (isHorizontal) floatArrayOf(
                temp[6], temp[7],
                temp[4], temp[5],
                temp[2], temp[3],
                temp[0], temp[1]) else temp
        return temp
    }
}

问题解决后效果如下图:


Camera_Right_Rotation.png

三. 解决前期相机垂直翻转问题

在使用前置相机预览的时候,会发现画面有垂直翻转的问题,画面随着Y轴中间左右调换了,导致画面左边对应到现实的右边。对此,只要将顶点坐标左右互换即可,使用到的是上面工具类的flip方法进行处理。

四. 摄像头切换的处理

切换摄像头的时候,由于输入源Camera对象变了,已经不是同个对象,所以这时候Renderer渲染类需要重新创建纹理ID,并且更新顶点坐标。
具体是在onDrawFrame方法中,去处理一次。

override fun onDrawFrame(glUnused: GL10) {
    if (isSwitchCamera) {
        isSwitchCamera = false
        createOESTextureId()
        updateVertex()
    }
}

/**
 * 根据相机方向更新顶点坐标
 */
private fun updateVertex() {
    var array = VertexRotationUtil.rotate(POINT_DATA, camera.rotation)
    array = VertexRotationUtil.flip(array, camera.isFront)
    mVertexData = BufferUtil.createFloatBuffer(array)
    mVertexData.position(0)
    GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT,
            GLES20.GL_FLOAT, false, 0, mVertexData)
    GLES20.glEnableVertexAttribArray(aPositionLocation)
}

其他

GitHub工程

本系列课程所有相关代码请参考我的GitHub项目⭐GLStudio⭐,喜欢的请给个小星星。😃

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269