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