OpenGL.ES在Android上的简单实践:15-全景(视野变换 上)

OpenGL.ES在Android上的简单实践:15-全景(视野变换)

 

本篇文章只有一个内容,就是模仿Insta360的视野变换效果,实现我们全景球的视野变换。首先来直观的感受一下Insta360的绚丽的变换效果。然后加以分析一下,视野变换的关键变量有哪些?

(insta360视野变换效果)(左下角标注了视野的名称)

从动图中看到,其实这几个视野(特别是小行星)都是基于一个球体的表面所进行变换的,只是所观察的视野范围不仅相同。说到观察视野,那就是和透视投影矩阵有关了。我们之前的一篇文章中介绍到视椎体这一个概念。我们从之前的文章中摘取重点知识,结合实例分析分析:

 

视椎体(frustum)这个观看空间是由一个透视投影矩阵和投影除法创建的。简单来说,视椎体只是一个立方体,其远端比近端大,从而使其变成一个被截断的金字塔。两端的大小差别越大,观察的范围越宽,我们能看到的也越多。

一个视椎体有一个焦点(focal point)这个焦点可以这样得到,顺着从视椎体较大端向较小端扩展处理的那些直线,一直向前通过较小端知道它们汇聚到一起。当你用透视投影观察一个场景的时候,那个场景看上去就像你站在焦点处观察一样。焦点和视椎体小端的距离被称为焦距(focal length),它影响视椎体小端和大端的比例,以及其对应的视野。

 

焦距,它是影响视野的关键!回到项目工程,现在的透视投影矩阵是如下设置:

MatrixHelper.perspectiveM(mProjectionMatrix, 45, (float)width/(float)height, 1f, 100f);



可以看到现在焦距是45。我们不妨调试修改 焦距 看看出来的效果是怎样的?

当我们增大角度a的值,视顶角会不断增大,视野会越来越窄,在同等观察距离下视野中物体会拉近,超出(屏幕)可视范围;

当我们减小角度a的值,视顶角会不断减少,视野会越来越宽,在同等观察距离下视野中物体会远离,在可视范围中缩小;

还有一点需要注意,要达到各种视野的效果,还需要结合全景球的半径进行考虑。怎么理解这句话?譬如小行星这样的视野,我们必须把摄像头的观察距离锁定在球的半径内(或者与半径等同),在扩大视野范围。如果超出了半径的范围外,再大的视野也没有内容填充!不明白的话,再想想我们现实世界,你在近地面的直升机上观察 地球 和 月球,差别是什么?同等的视野,不同的观察距离差别是巨大的!

 

经过以上的分析后,我们已经基本知道了视野变换的基本变量,观察距离和焦距。经过大量调试得到了以下视野的变量参数:

// 全景球
MatrixHelper.perspectiveM(mProjectionMatrix, 70f, (float)width/(float)height, 0.01f, 1000f);
Matrix.setLookAtM(mViewMatrix, 0,
                0f, 0f, 2.8f,
                0f, 0f, 0f,
                0f, 1f, 0f);
// 透视
MatrixHelper.perspectiveM(mProjectionMatrix, 70f, (float)width/(float)height, 0.01f, 1000f);
Matrix.setLookAtM(mViewMatrix, 0,
                0f, 0f, 1.0f,
                0f, 0f, 0f,
                0f, 1f, 0f);		
// 小行星
MatrixHelper.perspectiveM(mProjectionMatrix, 150f, (float)width/(float)height, 0.01f, 1000f);
Matrix.setLookAtM(mViewMatrix, 0,
                0f, 0f, 1.0f,
                0f, 0f, 0f,
                0f, 1f, 0f);

两个注意点希望大家能观察到:我把近平面参数变成0.01了,比之前1拉得更低,如果按照原来的1,那么观察矩阵的距离小于1(全景球半径)的话就会失去观察视野的内容;第二点,就是初始全景球模型的参数,焦距是70,观察距离2.8。仔细观察,我们三个模型下的变量都是只有一项差别,这样方便视野之间的动态变换。

好了,既然三个视野的参数都决定好了。我们正式编码增加视野的动态切换效果。首先我们在测试页面PanoramaActivity增加双击屏幕的事件,来响应视野切换的请求。

    private class GLViewTouchListener implements View.OnTouchListener {
        private long lastClickTime;
        @Override
        public boolean onTouch(View view, MotionEvent event) {
            if(event.getAction() == MotionEvent.ACTION_DOWN){
                ... ...
                if (System.currentTimeMillis() - lastClickTime < 500) {
                    Log.w(Constants.TAG, "SurfaceView-GL double click in thread."+Thread.currentThread().getName());
                    lastClickTime = 0;
                    glSurfaceView.queueEvent(new Runnable() {
                        @Override
                        public void run() {
                            renderer.handleDoubleClick();
                        }
                    });
                } else {
                    lastClickTime = System.currentTimeMillis();
                }
            }else if(event.getAction() ==MotionEvent.ACTION_MOVE){
                ... ...
            }else if(event.getAction() == MotionEvent.ACTION_UP){
                ... ...
            }else {
                return false;
            }
            return true;
        }
    }
// PanoramaRenderer2.java 渲染器把双击事件->模型的视野切换请求    
    public void handleDoubleClick() {
        if(ball!=null){
            ball.nextControlMode();
        }
    }

我们继续分析视野切换的动画效果,既然是一个动画过程,就必须有开始和结束两个状态,然后还要方便管理操作对象。所以我们把 观察矩阵+焦距 封装成一个类CameraViewport(观察视口),在Constants增加三个视野状态的标志位:

public class Constants {
    public static final int BYTES_PER_BYTE = 1;
    public static final int BYTES_PER_SHORT = 2;
    public static final int BYTES_PER_FLOAT = 4;
    public static final int BYTES_PER_INT = 4;

    public static final String TAG = "BlogApp-TAG";

    public static final int RENDER_MODE_CRYSTAL = 360;  //全景球
    public static final int RENDER_MODE_PERSPECTIVE = 360+1; //普通透视
    public static final int RENDER_MODE_PLANET = 360+2; //小行星
}
public class CameraViewport {
    // 各种目标焦距
    private static final float CRYSTAL_OVERLOOK = 70f;
    private static final float PERSPECTIVE_OVERLOOK = 70f;
    private static final float PLANET_OVERLOOK = 140f;
    
    public float overlook;// 焦距
    public float cx; // 摄像机位置X
    public float cy; // 摄像机位置Y
    public float cz; // 摄像机位置Z
    public float tx; // 摄像机目标点X
    public float ty; // 摄像机目标点Y
    public float tz; // 摄像机目标点Z
    public float upx;// 摄像机UP向量X
    public float upy;// 摄像机UP向量Y
    public float upz;// 摄像机UP向量Z

    public CameraViewport setCameraVector(float cx,float cy,float cz){
        this.cx = cx;
        this.cy = cy;
        this.cz = cz;
        return this;
    }
    public CameraViewport setTargetViewVector(float tx,float ty,float tz){
        this.tx = tx;
        this.ty = ty;
        this.tz = tz;
        return this;
    }

    public CameraViewport setCameraUpVector(float upx,float upy,float upz){
        this.upx = upx;
        this.upy = upy;
        this.upz = upz;
        return this;
    }

    @Override
    public boolean equals(Object o) {
        CameraViewport targetEye = (CameraViewport) o;
        if(
            beEqualTo(targetEye.cx , this.cx) &&
            beEqualTo(targetEye.cy , this.cy) &&
            beEqualTo(targetEye.cz , this.cz) &&
            beEqualTo(targetEye.tx , this.tx) &&
            beEqualTo(targetEye.ty , this.ty) &&
            beEqualTo(targetEye.tz , this.tz) &&
            beEqualTo(targetEye.upx , this.upx) &&
            beEqualTo(targetEye.upy , this.upy) &&
            beEqualTo(targetEye.upz , this.upz)
        ) {
            return true;
        }else{
            return false;
        }
    }

    private static boolean beEqualTo(float a, float b){
        if(Math.abs(a-b) < 0.001f || Math.abs(a-b)==0f){
            return true;
        }
        return false;
    }

    public void copyTo(CameraViewport targetEye) {
        if(targetEye != null){
            targetEye.cx = this.cx;
            targetEye.cy = this.cy;
            targetEye.cz = this.cz;

            targetEye.tx = this.tx;
            targetEye.ty = this.ty;
            targetEye.tz = this.tz;

            targetEye.upx = this.upx;
            targetEye.upy = this.upy;
            targetEye.upz = this.upz;
        }
    }
}

基础类都准备好了,我们开始编写代码吧。我们定义一个当前的CameraViewport 和 目标CameraViewport,并在原来初始化透视投影矩阵和观察矩阵的地方onSurfaceChanged,进行CameraViewport的初始化操作。

// org.zzrblog.blogapp.objects.PanoramaBall.java

private CameraViewport currentViewport;
private CameraViewport targetViewport;
private int mSurfaceWidth;
private int mSurfaceHeight;
public void onSurfaceChanged(int width, int height) {
        GLES20.glViewport(0,0,width,height);
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);
        mSurfaceWidth = width;
        mSurfaceHeight = height;

        if(currentViewport==null)
            currentViewport = new CameraViewport();
        if(targetViewport==null)
            targetViewport = new CameraViewport();
        
        currentViewport.overlook = CameraViewport.CRYSTAL_OVERLOOK;
        currentViewport.setCameraVector(0, 0, 2.8f);
        currentViewport.setTargetViewVector(0f, 0f, 0.0f);
        currentViewport.setCameraUpVector(0f, 1.0f, 0.0f);
        currentViewport.copyTo(targetViewport);

        MatrixHelper.perspectiveM(mProjectionMatrix, currentViewport.overlook, 
                (float)width/(float)height, 0.01f, 1000f);
        Matrix.setLookAtM(this.mViewMatrix,0,
                currentViewport.cx,  currentViewport.cy,  currentViewport.cz, 
                currentViewport.tx,  currentViewport.ty,  currentViewport.tz, 
                currentViewport.upx, currentViewport.upy, currentViewport.upz);
    }

随即我们定义场景视野变换的顺序:全景球->透视->小行星。所以当我们双击屏幕的时候,调用nextControlMode我们就要判断当前的场景,和指引下一个目标场景是啥。基本思路梳理好了,代码如下:

// org.zzrblog.blogapp.objects.PanoramaBall.java    

    private int currentControlMode = Constants.RENDER_MODE_CRYSTAL; //默认状态是全景球
    private int targetControlMode  = currentControlMode; 
    //** 视野变换 每次调用nextControlMode就判断当前场景,根据当前场景确定目标场景
    public int nextControlMode() {
        if(currentControlMode == Constants.RENDER_MODE_CRYSTAL){
            targetViewport.overlook = CameraViewport.PERSPECTIVE_OVERLOOK;
            // 其实这些场景数据可用直接放在CameraViewport
            targetViewport.setCameraVector(0, 0, 1.0f);
            targetViewport.setTargetViewVector(0f, 0f, 0.0f);
            targetViewport.setCameraUpVector(0f, 1.0f, 0.0f);
            targetControlMode = Constants.RENDER_MODE_PERSPECTIVE;
        }

        if(currentControlMode == Constants.RENDER_MODE_PERSPECTIVE){
            targetViewport.overlook = CameraViewport.PLANET_OVERLOOK;
            targetViewport.setCameraVector(0, 0, 1.0f);
            tartgetEye.setTargetViewVector(0f, 0f, 0.0f);
            tartgetEye.setCameraUpVector(0f, 1.0f, 0.0f);
            targetControlMode = Constants.RENDER_MODE_PLANET;
        }

        if(currentControlMode == Constants.RENDER_MODE_PLANET){
            targetViewport.overlook = CameraViewport.CRYSTAL_OVERLOOK;
            targetViewport.setCameraVector(0, 0, 2.8f);
            tartgetEye.setTargetViewVector(0f, 0f, 0.0f);
            tartgetEye.setCameraUpVector(0f, 1.0f, 0.0f);
            targetControlMode = Constants.RENDER_MODE_CRYSTAL;
        }
        return targetControlMode; // 返回目标场景视野
    }

 

!!!由于篇幅关系,就先到这里。其实离动态效果的完整实现已经近在咫尺了!大家想想,我们现在缺什么?场景视野的起点和终点状态就设定好了,就差跑的过程了,一个动画就是每帧每帧的改变对象的状态!想想哪个接口是渲染帧图的?onDrawFrame啊!

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr_Zzr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值