OpenGL ES 實現刮刮卡和手寫板功能

該原創文章首發於微信公衆號:字節流動

刮刮卡效果

刮刮卡效果實現原理

其實利用 Android Canvas 實現類似刮刮卡或者手寫板功能比較方便,通過自定義 View 繪製 2 個圖層,位於上層的圖層在手指劃過的位置將透明度置爲 0 ,這樣下層圖層的顏色便可以顯示出來。

不過話又說回來,Android Canvas 實現類似刮刮卡功能雖然方便,但是性能一言難盡,通常在複雜的應用界面不宜採用此類方法,此時就不得不考慮使用 OpenGL 進行優化。

本文嘗試使用 OpenGL 來實現類似刮刮卡的功能,簡而言之就是**利用 OpenGL 根據手指滑動的座標去構建一條一條的帶狀網格,然後基於此網格實現紋理映射。**爲了使帶狀圖形(網格)看起來平滑自然,我們還需要在起點和終點位置構建 2 個半圓,使滑動軌跡看起來平滑自然。

實現原理圖

我們基於 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);

當我們繪製一張圖的時候,滑動屏幕呈現出來的就是刮刮卡效果:
刮刮卡效果

當我們繪製單一的某種顏色(純色),滑動屏幕呈現出來的就是手寫板效果:
手寫板效果

實現代碼路徑:
Android_OpenGLES_3_0

聯繫與交流

我的公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章