美文网首页
StarWars.Android 界面粉碎效果中的openGL操

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

作者: hjm1fb | 来源:发表于2017-09-07 18:19 被阅读51次

    最近在学习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操

          本文链接:https://www.haomeiwen.com/subject/mxtwjxtx.html