StarWars.Android 界面粉碎效果中的openGL操作解析

最近在学习openGL,就找了几个相关的开源项目,一边理解,一边记录~ 这篇文章要介绍的项目来自久负盛名的yalantis
阅读此文需要一点OpenGL基础,比如纹理坐标。

项目giuhub地址

首先简要翻译一下官方原理介绍:

<星战: 原力觉醒> 如何在安卓中粉碎视图

首先,我们面临两个挑战:View粉碎和斗转星移的背景。我有好几个有趣的方案来实现它们。

如何粉碎View

当原力击中View时,View被粉碎成了4000块。这告诉我们两点:1. 原力很强大 2. 如果用Canvas来生成这些碎片,恐怕性能上不行。

所以我选择强大的OpenGL。首先,我需要对要击碎的View截屏,将其纹理传输到openGL的内存中。然后去渲染碎片效果。下面是具体的步骤:

  1. 截屏。就是通俗的做法。
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888)
Canvas canvas = new Canvas(bitmap);
super.draw(canvas);
  1. 将纹理传输到openGL内存。
  2. 将图片转化成碎片。
    虽然OpenGL3.1的Android Extension Pack有一个tessellation shader可以轻松实现我们的需求:将一个平面转化成大量的三角形图元。而且OpenGL3.1 Android Extension Pack 允许我们在GPU上产生顶点数据,而不是仅仅在CPU上。
    但是! 考虑到OpenGL2.0的市场占有率还不低,我还是选择hard 模式吧。
    如何把一个View碎成4000块?我们可以挨个切下每一块碎片!当然,我只是开个玩笑。如果我们生成了上千个纹理,大概手机都要融化了。相反,我们将使用一个大的BitMap纹理,并且对每一个顶点设置纹理坐标(UV坐标)。
final float stepX = 1f / mStarWarsRenderer.sizeX;
final float stepY = 1f / mStarWarsRenderer.sizeY;

sizeX指的是X轴上的碎片数目
stepY指的是Y轴上的碎片数目

for (int x = 0; x < mStarWarsRenderer.sizeX; x++) {

        for (int y = 0; y < mStarWarsRenderer.sizeY; y++) {

            final float u0 = x * stepX;

            final float v0 = y * stepY;

            final float u1 = u0 + stepX;

            final float v1 = v0 + stepY;

            // push values to buffer

        }

}

我们要尽量把计算的任务交给GPU,因为GPU擅长异步计算. 所有的坐标计算我都放在顶点着色器里了. 我只需要一个变量来产生动画,这个变量通过 Android Interpolator产生:

// from 0 to plane height in OpenGL coordinates
animator = ValueAnimator.ofFloat(0, -Const.PLANE_HEIGHT * 2);
animator.setDuration(mAnimationDuration);
animator.setInterpolator(new DecelerateInterpolator(1.3f));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
 @Override
 public void onAnimationUpdate(ValueAnimator animation) {
   float value = (float) animation.getAnimatedValue();
   mDeltaPosX = value;
   mGlSurfaceView.requestRender();
 }
};
animator.start();

最后, 在顶点着色器里,我们将这个值传给碎片:

vec4 pos = a_Position;
pos.y += u_DeltaPos;
gl_Position = u_MVPMatrix * calcPos;

如何斗转星移

可以通过粒子效果库Leonids library画星星。此库用的是Canvas,上手也比较容易。然而,粒子数较多时,性能仍然是个问题,特别在旧手机上。
考虑到性能,我采用了跟碎片效果类似的方案,并且在顶点着色器中实现斗转星移的效果。
用纹理来画星星当然很容易,也可以用片段着色器附加一些使用技巧实现:使用公式来渲染星星。

// Render a star

float color = smoothstep(1.0, 0.0, length(v_TexCoordinate - vec2(0.5)) / v_Radius);

gl_FragColor = vec4(color);

后一种方法不仅能达成效果,还能增加30%的帧率。在大多数情况下,后一种方法渲染都比前一种方法快。
我采用了后一种方法,并且在我的旧手机Nexus 4上渲染100 000颗星星,仍有60 FPS(16ms)的帧率,很不错。

接下来回对OpenGL相关的点做一些解析。
分析开源项目,找准切入点很重要。我主要是想看怎么用OpenGl实现碎片效果,所以就先分析StarWarsRenderer这个类。
我写了些注释,来方便理解代码:

public class StarWarsRenderer implements 
GLSurfaceView.Renderer {
//略去成员变量声明

//构造函数 声明配置和监听器
  public StarWarsRenderer(StarWarsTilesGLSurfaceView glSurfaceView,TilesFrameLayout TilesFrameLayout, int animationDuration, int numberOfTilesX) {
        mGlSurfaceView = glSurfaceView;
        mListener = TilesFrameLayout;
        mAnimationDuration = animationDuration;
        mNumberOfTilesX = numberOfTilesX;
    }

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        // 常规的清屏操作
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);


        // Use culling to remove back faces.
        //开启剔除操作 默认剔除背面,如果要剔除正面:glCullFace(GL_FRONT)
        GLES20.glEnable(GLES20.GL_CULL_FACE);
        //顺时针表示正面 这样如果顶点数组是按顺时针排列的,就是告诉OpenGL是在绘制正面,反之就是背面。
        GLES20.glFrontFace(GLES20.GL_CW);

        // Enable depth testing
        //开启深度检测,这样后面被挡住的部分(按Z值区分前后)就不会被绘制,更多请参考glPolygonOffest函数
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);

        //从这一行一直到Matrix.setLookAtM 都是在设置照相机(观察者)的位置和视角,默认的是照相机正立放在原点,
        // 相机顶部朝Y轴,可以发现下面的代码就是默认设置,所以删掉也不影响。
        //详细介绍:http://blog.csdn.net/kkae8643150/article/details/52805738
        //http://www.cnblogs.com/kesalin/archive/2012/12/06/3D_math.html
        // Position the eye in front of the origin.
        final float eyeX =  0.0f;
        final float eyeY =  0.0f;
        final float eyeZ =  0.0f;

        // We are looking toward the distance
        final float lookX =  0.0f;
        final float lookY =  0.0f;
        final float lookZ =  1.0f;

        // Set our up vector. This is where our head would be pointing were we holding the camera.
        final float upX = 0.0f;
        final float upY = 1.0f;
        final float upZ = 0.0f;

        Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);

        // 加载着色器和项目(program)
        final String vertexShader = RawResourceReader.readTextFileFromRawResource(mGlSurfaceView.getContext(), R.raw.tiles_vert);
        final String fragmentShader = RawResourceReader.readTextFileFromRawResource(mGlSurfaceView.getContext(), R.raw.tiles_frag);

        final int vertexShaderHandle = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertexShader);
        final int fragmentShaderHandle = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);

        programHandle = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle,
                new String[]{"a_Position", "a_Normal", "a_TexCoordinate"});

        // Initialize the accumulated rotation matrix
        Matrix.setIdentityM(mAccumulatedRotation, 0);
    }

    private void genTilesData() {
        //生成碎片的顶点坐标等值, 生成算法不是我们的重点,先略过
        Executors.newSingleThreadExecutor().submit(new GenerateVerticesData(this));
    }

    @Override
    public void onSurfaceChanged(GL10 unused, int width, int height) {
        sizeX = mNumberOfTilesX;
        sizeY = height * sizeX /  width;

        // Set the OpenGL viewport to the same size as the surface.
        GLES20.glViewport(0, 0, width, height);

        // Create a new perspective projection matrix. The height will stay the same
        // while the width will vary as per aspect ratio.
        final float ratio = (float) width / height;

        final float left = -ratio;
        final float right = ratio;
        final float bottom = -1.0f;
        final float top = 1.0f;
        final float near = 1.0f;
        final float far = 10.0f;

        this.ratio = ratio;
        // 建议改成perspectiveM的方式,frustumM方法在某些情况下有bug
        Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);

        genTilesData();
    }

    @Override
    public void onDrawFrame(GL10 gl10) {
        logFrame();
        drawGl();
        if (!requestedReveal && mAndroidDataHandle > 0) {
            requestedReveal = true;
            mListener.reveal();
        }
    }

    private void drawGl() {
    //常规操作,绑定变量地址,有些代码冗余
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        if (mAndroidDataHandle > 0) {

            GLES20.glUseProgram(programHandle);

            // Set program handles
            mvpMatrixHandle = GLES20.glGetUniformLocation(programHandle, "u_MVPMatrix");
            mvMatrixHandle = GLES20.glGetUniformLocation(programHandle, "u_MVMatrix");
            textureUniformHandle = GLES20.glGetUniformLocation(programHandle, "u_Texture");
            deltaPosHandle = GLES20.glGetUniformLocation(programHandle, "u_DeltaPos");

            positionHandle = GLES20.glGetAttribLocation(programHandle, "a_Position");
            normalHandle = GLES20.glGetAttribLocation(programHandle, "a_Normal");
            textureCoordinateHandle = GLES20.glGetAttribLocation(programHandle, "a_TexCoordinate");
            tileXyHandle = GLES20.glGetAttribLocation(programHandle, "a_TileXY");

            Matrix.setIdentityM(mModelMatrix, 0);
            Matrix.translateM(mModelMatrix, 0, 0.0f, 0.0f, PLANE_HEIGHT);

            // Set a matrix that contains the current rotation.
            Matrix.setIdentityM(mCurrentRotation, 0);

            Matrix.multiplyMM(mTemporaryMatrix, 0, mCurrentRotation, 0, mAccumulatedRotation, 0);
            System.arraycopy(mTemporaryMatrix, 0, mAccumulatedRotation, 0, 16);

            Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);

            // Pass in the modelview matrix.
            GLES20.glUniformMatrix4fv(mvMatrixHandle, 1, false, mMVPMatrix, 0);

            Matrix.multiplyMM(mTemporaryMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);
            System.arraycopy(mTemporaryMatrix, 0, mMVPMatrix, 0, 16);

            // Pass in the combined matrix.
            GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mMVPMatrix, 0);

            // Pass in u_Gravity
            GLES20.glUniform1f(deltaPosHandle, deltaPosX);

            // Pass in the texture information
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

            // Bind the texture to this unit.
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mAndroidDataHandle);

            GLES20.glUniform1i(textureUniformHandle, 0);

            if (mPlane != null) {
                mPlane.render();
            }

            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
        }
    }

    public int getTilesCount() {
        return sizeX * sizeY;
    }
    
    public void logFrame() {
        frames++;
        timePassed = (System.nanoTime() - startTime) / 1_000_000;
        if(timePassed >= 1000) {
            Timber.d("%d tiles @ %d fps", getTilesCount(), frames);
            frames = 0;
            startTime = System.nanoTime();
        }
    }

    public void startAnimation() {
//动画改变的是tiles_vert.glsl里的u_DeltaPos值
//动画结束时,u_DeltaPos = deltaPosX = -10。
//而tiles_vert.glsl里gl_Position.w = 5, 所以x y的可见取值范围是-5~5
//这也是GenerateVerticesData产生的坐标的取值范围。 
//所以动画结束时,gl_Position的Y值最大为5-10 = -5,
//也就是最上方的点,被映射到屏幕底部了。
//有些人会问,为什么gl_Position.w = 5,这个是由 
//Matrix.translateM(mModelMatrix, 0, 0.0f, 0.0f, 5f);确定的。
// 因为这个5f会在后面的矩阵运算中参与gl_Position.w的生成。
//所以这个值要和PLANE_HEIGHT一致。
//作者为什么不直接用PLANE_HEIGHT代替哇,哭.jpg.
// ps:发现PLANE_HEIGHT设置为1.0 10.0都没问题,设置成20.0就有问题。
//然鹅没必要探究,设置成1,与归一化坐标范围一致,最简单。
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                animator = ValueAnimator.ofFloat(0, -PLANE_HEIGHT * 2); // plane height
                animator.setDuration(mAnimationDuration);
                animator.setInterpolator(new AccelerateDecelerateInterpolator());

                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        float value = (float) animation.getAnimatedValue();
                        deltaPosX = value;
                        mGlSurfaceView.requestRender();
                    }
                });
                animator.addListener(new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        mGlSurfaceView.requestRender();
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        mListener.onAnimationFinished();
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {

                    }

                    @Override
                    public void onAnimationRepeat(Animator animation) {

                    }
                });

                animator.start();

            }
        });
    }

    public void updateTexture(final Bitmap bitmap) {
        mGlSurfaceView.queueEvent(new Runnable() {
            @Override
            public void run() {
                requestedReveal = false;
                mAndroidDataHandle = TextureHelper.loadTexture(bitmap);
                mGlSurfaceView.requestRender();
            }
        });
    }

    public void cancelAnimation() {
        if (animator != null && animator.isRunning()) {
            animator.removeAllListeners();
            animator.cancel();
        }
    }
}

你可能感兴趣的:(StarWars.Android 界面粉碎效果中的openGL操作解析)