Android音视频开发,实现刮刮卡功能详解

刮刮卡实现:

实现原理

其实利用 Android Canvas 实现类似刮刮卡或者手写板功能比较方便,通过自定义 View 绘制 2 个图层,位于上层的图层在手指划过的位置将透明度置为 0 ,这样下层图层的颜色便可以显示出来。

不过话又说回来,Android Canvas 实现类似刮刮卡功能虽然方便,但是性能一言难尽,通常在复杂的应用界面不宜采用此类方法,此时就不得不考虑使用 OpenGL 进行优化。

本文尝试使用 OpenGL 来实现类似刮刮卡的功能,简而言之就是利用 OpenGL 根据手指滑动的坐标去构建一条一条的带状网格,然后基于此网格实现纹理映射。

为了使带状图形(网格)看起来平滑自然,我们还需要在起点和终点位置构建 2 个半圆,使滑动轨迹看起来平滑自然。

实现原理图:


image.png

我们基于 2 点之间滑动轨迹构建的形状如上图所示,形状由一个矩形和 2 个半圆组成。

设 P0、P1 为手指在屏幕上滑动时前后相邻的 2 个点(注意屏幕坐标需要进行归一化转换为纹理坐标),r 为圆的半径,同时也用于控制矩形的宽度。

上述原理图中,点 P1、P2 和半径 r 为已知信息,我们需要求出矩形的四个点 V0、V1、V2、V3 的坐标,便于去构建矩形网格,而两个圆的圆心和半径信息已知,只需要以圆心为顶点构建三角形即可。

这里我们选择直接绘制 2 个圆而不是 2 个半圆,因为绘制半圆的话需要去计算直线 V0V1 或 V2V3 斜率的反正切角,这个时候有几种特殊情况需要考虑,反而变得比较麻烦。

而无脑去绘制 2 个圆的话,后续可以利用模板测试来防止重复绘制,实现起来更为方便。

为求得直线 V0V1 的方程,可以利用 2 个直线 P0P1 和 V0V1 相交的关系,即向量 V0P0 和向量 P0P1 的点乘值为 0 。

求出直线 V0V1 的方程后,直线 V0V1 与以 P0 为圆心 r 为半径圆的 2 个交点,就是点 V0 和 V1 的坐标,在数学上就是求解二元二次方程。同样,使用相同的方法,也可以求出点 V2、V3 的坐标。

OpenGL 刮刮卡实现代码

OpenGL 实现刮刮卡效果的关键在于利用滑动轨迹构建网格,我们在 GLSurfaceView 类的 onTouchEvent 回调方法中获得滑动轨迹传入 Native 层用于构建网格。

public void consumeTouchEvent(MotionEvent e) {
        float touchX = -1, touchY = -1;
        switch (e.getAction()) {
            case MotionEvent.ACTION_MOVE:
                touchX = e.getX();
                touchY = e.getY();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL://取消或停止滑动时进行标记
                touchX = -1;
                touchY = -1;
                break;
        }

        //滑动、触摸
        switch (mGLRender.getSampleType()) {
            case SAMPLE_TYPE_KEY_SCRATCH_CARD:
                mGLRender.setTouchLoc(touchX, touchY);
                requestRender();
                break;
            default:
                break;
        }

    }

在 Native 层构建网格,其中点 pre 和 cur 为滑动轨迹中相邻的 2 个点。

void ScratchCardSample::CalculateMesh(vec2 pre, vec2 cur) {
    vec2 imgSize(m_RenderImage.width, m_RenderImage.height);
    vec2 p0 = pre * imgSize, p1 = cur * imgSize;
    vec2 v0, v1, v2, v3;
    float r = static_cast<float>(EFFECT_RADIUS * imgSize.x);
    float x0 = p0.x, y0 = p0.y;
    float x1 = p1.x, y1 = p1.y;
    if (p0.y == p1.y) //1. 平行于 x 轴的
    {
        v0 = vec2(p0.x, p0.y - r) / imgSize;
        v1 = vec2(p0.x, p0.y + r) / imgSize;
        v2 = vec2(p1.x, p1.y - r) / imgSize;
        v3 = vec2(p1.x, p1.y + r) / imgSize;

    } else if (p0.x == p1.x) { //2. 平行于 y 轴的
        v0 = vec2(p0.x - r, p0.y) / imgSize;
        v1 = vec2(p0.x + r, p0.y) / imgSize;
        v2 = vec2(p1.x - r, p1.y) / imgSize;
        v3 = vec2(p1.x + r, p1.y) / imgSize;

    } else { //3. 其他 case
        float A0 = (y1 - y0) * y0 + (x1 - x0) * x0;
        float A1 = (y0 - y1) * y1 + (x0 - x1) * x1;
        // y = a0 * x + c0,  y = a1 * x + c1
        float a0 = -(x1 - x0) / (y1 - y0);
        float c0 = A0 / (y1 - y0);

        float a1 = -(x0 - x1) / (y0 - y1);
        float c1 = A1 / (y0 - y1);

        float x0_i = 0;
        float y0_i = a0 * x0_i + c0;

        float x1_i = 0;
        float y1_i = a1 * x1_i + c1;

        //计算直线与圆的交点
        vec4 v0_v1 = getInsertPointBetweenCircleAndLine(x0, y0, x0_i, y0_i, x0, y0, r);

        v0 = vec2(v0_v1.x, v0_v1.y) / imgSize;
        v1 = vec2(v0_v1.z, v0_v1.w) / imgSize;

        vec4 v2_v3 = getInsertPointBetweenCircleAndLine(x1, y1, x1_i, y1_i, x1, y1, r);

        v2 = vec2(v2_v3.x, v2_v3.y) / imgSize;
        v3 = vec2(v2_v3.z, v2_v3.w) / imgSize;

    }

    // 矩形 3 个三角形(一个矩形为什么要绘制 3 个三角形?)
    m_pTexCoords[0] = v0;
    m_pTexCoords[1] = v1;
    m_pTexCoords[2] = v2;
    m_pTexCoords[3] = v0;
    m_pTexCoords[4] = v2;
    m_pTexCoords[5] = v3;
    m_pTexCoords[6] = v1;
    m_pTexCoords[7] = v2;
    m_pTexCoords[8] = v3;

    int index = 9;
    float step = MATH_PI / 10;
    // 2 个圆,一共 40 个三角形,360 度角平分 20 份 
    for (int i = 0; i < 20; ++i) {
        float x = r * cos(i * step);
        float y = r * sin(i * step);

        float x_ = r * cos((i + 1) * step);
        float y_ = r * sin((i + 1) * step);

        x += x0;
        y += y0;
        x_ += x0;
        y_ += y0;

        m_pTexCoords[index + 6 * i + 0] = vec2(x, y) / imgSize;
        m_pTexCoords[index + 6 * i + 1] = vec2(x_, y_) / imgSize;
        m_pTexCoords[index + 6 * i + 2] = vec2(x0, y0) / imgSize;

        x = r * cos(i * step);
        y = r * sin(i * step);

        x_ = r * cos((i + 1) * step);
        y_ = r * sin((i + 1) * step);

        x += x1;
        y += y1;
        x_ += x1;
        y_ += y1;

        m_pTexCoords[index + 6 * i + 3] = vec2(x, y) / imgSize;
        m_pTexCoords[index + 6 * i + 4] = vec2(x_, y_) / imgSize;
        m_pTexCoords[index + 6 * i + 5] = vec2(x1, y1) / imgSize;
    }

    for (int i = 0; i < TRIANGLE_NUM * 3; ++i) {
        m_pVtxCoords[i] = GLUtils::texCoordToVertexCoord(m_pTexCoords[i]);
    }

看到上面的代码,你或许会感到疑惑,一个矩形为什么要绘制 3 个三角形?

这是因为点 V0、V1 的相对位置(谁在左边、谁在右边)我们并不知道,为了确保能绘制完整的矩形,这里直接绘制了 3 个三角形,这个后面还有优化。

下面是绘制部分的逻辑,其中为了防止重复绘制,我们开启模板测试,下面代码设置的意思是:我们之前已经绘制过的位置,后面就不再进行重复绘制了。

   glUseProgram(m_ProgramObj);
    glEnable(GL_STENCIL_TEST);
    glStencilFunc(GL_NOTEQUAL, 1, 0xFF);//当片段的模板值不为 1 时,片段通过测试进行渲染
    glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);//若模板测试和深度测试都通过了,将片段对应的模板值替换为1
    glStencilMask(0xFF);

    glBindVertexArray(m_VaoId);
    glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_TextureId);
    glUniform1i(m_SamplerLoc, 0);

    for (int i = 0; i < m_PointVector.size(); ++i) {
        vec4 pre_cur_point = m_PointVector[i];
        CalculateMesh(vec2(pre_cur_point.x, pre_cur_point.y), vec2(pre_cur_point.z, pre_cur_point.w));
        glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(m_pVtxCoords), m_pVtxCoords);
        glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(m_pTexCoords), m_pTexCoords);
        glDrawArrays(GL_TRIANGLES, 0, TRIANGLE_NUM * 3);
    }
    glDisable(GL_STENCIL_TEST);

当我们绘制一张图的时候,滑动屏幕呈现出来的就是刮刮卡效果:


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

推荐阅读更多精彩内容