OpenGL.ES在Android上的简单实践:14-全景(惯性滑动球体)

OpenGL.ES在Android上的简单实践:

14-全景(惯性滑动球体)

 

 

 

1、整理封装全景球

现在,我们的地球已经能正确的显示出来,我们来增加必要的交互,使得我们左右滑动屏幕的时候,地球能旋转起来,而且是像一个地球仪一样,手指离开屏幕后,能随着惯性的操作延后旋转。

第一步,我们现在测试页面PanoramaActivity添加对GLSurfaceView的触摸事件监听,并设置可以点击状态。

public class PanoramaActivity extends Activity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        glSurfaceView = new GLSurfaceView(this);
        renderer = new PanoramaRenderer(PanoramaActivity.this);
        ... ...
        glSurfaceView.setClickable(true);
        glSurfaceView.setOnTouchListener(new GLViewTouchListener());
        setContentView(glSurfaceView);
    }

    private class GLViewTouchListener implements View.OnTouchListener {
        @Override
        public boolean onTouch(View view, MotionEvent event) { 
            if(event.getAction() == MotionEvent.ACTION_DOWN){
            ... ...
            }else if(event.getAction() == MotionEvent.ACTION_MOVE){
            ... ...
            }else if(event.getAction() == MotionEvent.ACTION_UP){
            ... ...
            }else {
                return false;
            }
            return true;
        }
    }
}


 

第二步,我们让渲染器PanoramaRenderer响应手指“滑动”这系列操作。这系列操作分别经过按下,滑动,抬起三个事件。所以我们这三个响应的事件中添加renderer的接口:

           if(event.getAction() == MotionEvent.ACTION_DOWN){
                final float x = event.getX();
                final float y = event.getY();
                glSurfaceView.queueEvent(new Runnable() {
                    @Override
                    public void run() {
                        renderer.handleTouchUp(x, y);
                    }
                });
            }else if(event.getAction() ==MotionEvent.ACTION_MOVE){
                final float x = event.getX();
                final float y = event.getY();
                    glSurfaceView.queueEvent(new Runnable() {
                        @Override
                        public void run() {
                            renderer.handleTouchDrag(x,y);
                        }
                    });
            }else if(event.getAction() == MotionEvent.ACTION_UP){
                final float x = event.getX();
                final float y = event.getY();
                glSurfaceView.queueEvent(new Runnable() {
                    @Override
                    public void run() {
                        renderer.handleTouchUp(x,y);
                    }
                });
            }

进入PanoramaRenderer 我们把响应请求传递給ball,让ball执行对应的操作。对于渲染器renderer,我们不好做太多逻辑的操作,把逻辑操作交给对应的模型内部维护,方便模型对象的管理。

// PanoramaRenderer.java
    ... ...
    public void handleTouchUp(float x, float y) {
        if(ball!=null){
            ball.handleTouchUp( x, y);
        }
    }

    public void handleTouchDrag(float x, float y) {
        if(ball!=null){
            ball.handleTouchDrag( x, y);
        }
    }

    public void handleTouchDown(float x, float y) {
        if(ball!=null){
            ball.handleTouchDown( x, y);
        }
    }

到这里,其实Renderer的三大glsurfaceview回调,我们也应该封装在模型对象中响应。下面我们更新PanoramaRenderer和Ball->PanoramaRenderer2和PanoramaBall。

public class PanoramaRenderer2 implements GLSurfaceView.Renderer{
    private final Context context;
    private PanoramaBall ball;

    public PanoramaRenderer(Context context) {
        this.context = context
        ball = new PanoramaBall(context);
    }

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        if(ball!=null) ball.onSurfaceCreated(eglConfig);
    }

    @Override
    public void onSurfaceChanged(GL10 gl10, int width, int height) {
        if(ball!=null) ball.onSurfaceChanged(width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl10) {
        if(ball!=null) ball.onDrawFrame();
    }

    public void handleTouchUp(float x, float y) {
        if(ball!=null) ball.handleTouchUp( x, y);
    }
    public void handleTouchDrag(float x, float y) {
        if(ball!=null) ball.handleTouchDrag( x, y);
    }
    public void handleTouchDown(float x, float y) {
        if(ball!=null) ball.handleTouchDown( x, y);
    }
}

我们从渲染器入手,更新后的渲染器相当简单。只起到一个渲染管理者的作用,实际的操作我们交给模型对象PanoramaBall。还有一点注意,我们是在PanoramaRenderer构造函数中调用PanoramaBall的构造,还记得之前我们说过,所以GL的接口方法必须在glsurfaceview的三大回调中调用吗?记住这点,下面我正式改造PanoramaBall:

public class PanoramaBall {   
    ... ...
    private float[] mProjectionMatrix = new float[16];// 投影矩阵
    private float[] mViewMatrix = new float[16]; // 摄像机位置朝向9参数矩阵
    private float[] mModelMatrix = new float[16];// 模型变换矩阵
    private float[] mMVPMatrix = new float[16];// 获取具体物体对象的MVP矩阵
    private float[] getFinalMatrix() {
        Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);
        Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);
        return mMVPMatrix;
    }

    public PanoramaBall(Context context){
        this.context = context;
        Matrix.setIdentityM(mProjectionMatrix, 0);
        Matrix.setIdentityM(mViewMatrix, 0);
        Matrix.setIdentityM(mModelMatrix, 0);
        Matrix.setIdentityM(mMVPMatrix, 0);
    }
    ... ...
}

第一步,我们抽取MVP矩阵的方法,记住三大矩阵是左乘的,具体原理分析请参考这里的第4节。接下来继续查看新增的扩展函数接口onSurfaceCreated / onSurfaceChanged / onDrawFrame。

    // 紧接着上面部分 ...
    public void onSurfaceCreated(EGLConfig eglConfig) {
        initVertexData();
        initTexture();
        buildProgram();
        setAttributeStatus();
        // 这些方法跟之前的一样,直接copy就ok
    }

    public void onSurfaceChanged(int width, int height) {
        GLES20.glViewport(0,0,width,height);
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);
        MatrixHelper.perspectiveM(mProjectionMatrix, 45, (float)width/(float)height, 1f, 100f);
        Matrix.setLookAtM(mViewMatrix, 0,
                0f, 0f, 4f,
                0f, 0f, 0f,
                0f, 1f, 0f);
    }

    public void onDrawFrame() {
        GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        ballShaderProgram.userProgram();
        ballShaderProgram.setUniforms(getFinalMatrix(),textureId);
        setAttributeStatus();
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer.getIndexBufferId());
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, numElements, GLES20.GL_UNSIGNED_SHORT, 0);
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
    }

其实就是当初我们在renderer写的代码,只是我们封装在PanoramaBall中。一切都按照正确的方向前进中,现在我们可以测试页面PanoramaActivity替换渲染器看看是否和之前的一样效果?

 

2、滑动全景球

当我们用手指滑动屏幕的时候,全景球需要知道是向左还是向右滑动,以此作出正确的旋转。所以当全景球接受按压事件的时候,先记录当前的屏幕按压位置;之后在滑动的事件求出正确的移动步伐并旋转等量的角度;抬起事件则重置位置。

    private float mLastX;
    private float mLastY;

    public void handleTouchUp(float x, float y) {
        this.mLastX = 0;
        this.mLastY = 0;
    }

    public void handleTouchDown(float x, float y) {
        this.mLastX = x;
        this.mLastY = y;
    }

    public void handleTouchMove(float x, float y) {
        float offsetX = this.mLastX - x;
        float offsetY = this.mLastY - y;
        .. .. ..
    }

up和down都so easy。那么move的响应事件,我们该怎样令球旋转起来呢?我们自然想到android.opengl.Matrix的rotateM(float[] m, int mOffset, float a, float x, float y, float z)函数,调用这个函数我们需要什么?我们需要偏转角度a。得到偏转角度后,我们调用rotateM接口,得出偏转角度的旋转矩阵,然后把旋转的矩阵和模型矩阵运算求值,就得出了新的模型矩阵了,最后模型矩阵更新到MVP。搞定!

    private float mLastX;
    private float mLastY;
    private float rotationX = 0;
    private float rotationY = 0;
    private float[] mMatrixRotationX = new float[16];
    private float[] mMatrixRotationY = new float[16];

    public void handleTouchUp(float x, float y) {
        this.mLastX = 0;
        this.mLastY = 0;
    }

    public void handleTouchDown(float x, float y) {
        this.mLastX = x;
        this.mLastY = y;
    }

    public void handleTouchMove(float x, float y) {
        float offsetX = this.mLastX - x;
        float offsetY = this.mLastY - y;
        this.rotationY -= offsetX/10 ; // 注意! 屏幕横坐标的步伐,球应该是绕着Y轴旋转
        this.rotationX -= offsetY/10 ; // 注意! 屏幕纵坐标的步伐,球应该是绕着X轴旋转

        // 这部分代码是对球模型矩阵的操作,可以抽取成方法 updateBallMatrix,在onDrawFrame调用
        Matrix.setIdentityM(this.mModelMatrix, 0);
        Matrix.setIdentityM(mMatrixRotationX, 0);
        Matrix.setIdentityM(mMatrixRotationY, 0);
        Matrix.rotateM(mMatrixRotationY, 0, this.rotationY, 0, 1, 0);
        Matrix.rotateM(mMatrixRotationX, 0, this.rotationX, 1, 0, 0);
        Matrix.multiplyMM(this.mModelMatrix,0, mMatrixRotationX,0, mMatrixRotationY,0 );

        this.mLastX = x;
        this.mLastY = y;
    }

运行工程项目,是不是如下图所示能够动起来了?!         

但是大家有没发现,我们可以直接绕过顶部和底部...尴尬  要不大家加个角度的限制?

        

 

3、添加手指操作的惯性效果

现在我们是能够滑动球体了,接下来我们来实现惯性滑动,想象我们以前小时候玩地球仪那样大力滑动产生惯性的流畅自转。所以第一件事就是增加滑动事件的速度。这时我们用到Android的API——VelocityTracker。我们在测试页面PanoramaActivity增加VelocityTracker的使用:

public class PanoramaActivity extends Activity{
    private VelocityTracker mVelocityTracker = null;
    ... ...
    protected void onResume() { 
        ... ...
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        } else {
            mVelocityTracker.clear();
        }
    }

    protected void onPause() {
        if(null != mVelocityTracker) {
            mVelocityTracker.clear();
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }


    private class GLViewTouchListener implements View.OnTouchListener {
        
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            if(event.getAction() == MotionEvent.ACTION_DOWN){
                // 增加速度
                if (mVelocityTracker == null) {
                    mVelocityTracker = VelocityTracker.obtain();
                } else {
                    mVelocityTracker.clear();
                }
                mVelocityTracker.addMovement(event);
                ... ...
            }else if(event.getAction() ==MotionEvent.ACTION_MOVE){
                mVelocityTracker.addMovement(event);
                mVelocityTracker.computeCurrentVelocity(1000);
                // 在获取速度之前总要进行以上两步
                ... ...
            }else if(event.getAction() == MotionEvent.ACTION_UP){
                final float x = event.getX();
                final float y = event.getY();
                final float xVelocity = mVelocityTracker.getXVelocity();
                final float yVelocity = mVelocityTracker.getYVelocity();
                glSurfaceView.queueEvent(new Runnable() {
                    @Override
                    public void run() {
                        renderer.handleTouchUp(x, y, xVelocity, yVelocity);
                        // 把速度当成参数传递
                    }
                });
            }else {
                return false;
            }
            return true;
        }
    }
}

当我们手抬起的时候,速度变量传递到渲染器,触发全景球的惯性操作。那么怎样实现惯性这一操作?其实就是使用这个速度变量通过一个线程逐渐递减,并逐渐增加球体的旋转分量,是不是很简单?下面我们继续更新代码:

    public void handleTouchUp(final float x, final float y,
                              final float xVelocity, final float yVelocity) {
        this.mLastX = 0;
        this.mLastY = 0;

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    handleGestureInertia(x, y, xVelocity, yVelocity);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
    private void handleGestureInertia(float upX, float upY, float xVelocity, float yVelocity)
            throws InterruptedException {
        boolean gestureInertia_isStop = false;
        float mXVelocity = xVelocity;
        float mYVelocity = yVelocity;
        while(!this.gestureInertia_isStop){
//--------------------------------------------------------------------------------
            float offsetY = mYVelocity / 2000;
            this.rotationX = this.rotationX + offsetY;

            float offsetX = mXVelocity / 2000;
            this.rotationY = this.rotationY + offsetX;

            if(rotationX%360 > 90 ){
                this.rotationX = 90;
            }
            if(rotationX%360 < -90 ){
                this.rotationX = -90;
            }
//--------------------------------------------------------------------------------
            if(Math.abs(mYVelocity - 0.975f*mYVelocity) < 0.00001f
                    || Math.abs(mXVelocity - 0.975f*mXVelocity) < 0.00001f){
                this.gestureInertia_isStop = true;
            }
            mYVelocity = 0.975f*mYVelocity;
            mXVelocity = 0.975f*mXVelocity;
            Thread.sleep(5);
        }
    }

我们分析一下这个惯性操作方法,首先我们拿到速度变量,把它们先缩放指定倍数当做步伐(2000倍的缩放),然后把步伐增量添加到旋转角度上;我们还限制绕x轴的旋转角度不能超过顶部和底部。随后我们衰减速度变量,并加一个判断条件,衰减前后的差值小于0.00001f就可以视为惯性已经可以停止了。不要忘记惯性线程每一步伐要睡眠5~10(大家可以试下不睡眠的效果)这个和硬件显示的帧率是相关的,但不会相差很大。

由于篇幅关系,以上方法的具体数值都是我经过调试之后总结出来的,大家可以随便修改成适合自己的大小,加上自己的日志。

接下来,运行工程项目,看看以下效果是否如意?

     

工程代码:https://github.com/MrZhaozhirong/BlogApp

 

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值