Android OpenGLES2.0(十七)——球形天空盒VR效果实现

在3D游戏中通常都会用到天空盒,在3D引擎中也一般会存在天空盒组件,让开发者可以直接使用。那么天空盒是什么?天空盒又是如何实现的呢?本篇博客主要介绍如何在Android中利用OpenGLES绘制一个天空盒,并实现VR效果。

天空盒、天空穹、天空球和VR

虽然大多数人知道这些东西是啥,但是我觉得我还是有必要把他们的定义“搬”过来,万一有人不知道呢。

  • 天空盒(Sky Box)是放到场景中的一个立方体,经常是由六个面组成的立方体,并经常会随着视点的移动而移动。天空盒将刻画极远处人无法达到的位置的景物。
  • 天空穹(Sky Dome)与天空盒类似,只不过它将是天空盒除底面以外的五个面换成了一个曲面,可以理解成一个半球。和古人认为的天圆地方差不多。
  • 天空球(Sky Sphere)就是把天空盒直接换成一个球——听说没有天空球这个说法?无所谓了,现在有了。
  • VR(Virtual Reality)虚的定义不说了,本篇博客所说的VR效果就是手机上显示的图像由手机的姿态来控制而已。

天空盒的实现应该是最简单的,但是效果可能会有些瑕疵,尤其是顶着两面的交点处总能看出点不同。天空穹和天空球效果都差不多,会比天空盒好上很多,但是相对天空盒来说,比较耗性能。

绘制一个球

在之前的博客中Android OpenGLES2.0(六)——构建圆锥、圆柱和球体有介绍如何绘制一个球,只不过之前的球是没有贴图的,现在我们绘制一个球,并为它贴上环境的贴图。就像绘制一个地球仪一样。

顶点坐标和纹理坐标计算

首先,首先我们需要得到球的顶点坐标和纹理坐标:

//计算顶点坐标和纹理坐标
private void calculateAttribute(){
    ArrayList<Float> alVertix = new ArrayList<>();
    ArrayList<Float> textureVertix = new ArrayList<>();
    for (double vAngle = 0; vAngle < Math.PI; vAngle = vAngle + angleSpan){

        for (double hAngle = 0; hAngle < 2*Math.PI; hAngle = hAngle + angleSpan){
            float x0 = (float) (radius* Math.sin(vAngle) * Math.cos(hAngle));
            float y0 = (float) (radius* Math.sin(vAngle) * Math.sin(hAngle));
            float z0 = (float) (radius * Math.cos((vAngle)));

            float x1 = (float) (radius* Math.sin(vAngle) * Math.cos(hAngle + angleSpan));
            float y1 = (float) (radius* Math.sin(vAngle) * Math.sin(hAngle + angleSpan));
            float z1 = (float) (radius * Math.cos(vAngle));

            float x2 = (float) (radius* Math.sin(vAngle + angleSpan) * Math.cos(hAngle + angleSpan));
            float y2 = (float) (radius* Math.sin(vAngle + angleSpan) * Math.sin(hAngle + angleSpan));
            float z2 = (float) (radius * Math.cos(vAngle + angleSpan));

            float x3 = (float) (radius* Math.sin(vAngle + angleSpan) * Math.cos(hAngle));
            float y3 = (float) (radius* Math.sin(vAngle + angleSpan) * Math.sin(hAngle));
            float z3 = (float) (radius * Math.cos(vAngle + angleSpan));

            float s0 = (float) (hAngle / Math.PI/2);
            float s1 = (float) ((hAngle + angleSpan)/Math.PI/2);
            float t0 = (float) (vAngle / Math.PI);
            float t1 = (float) ((vAngle + angleSpan) / Math.PI);

            alVertix.add(x1);
            alVertix.add(y1);
            alVertix.add(z1);
            alVertix.add(x0);
            alVertix.add(y0);
            alVertix.add(z0);
            alVertix.add(x3);
            alVertix.add(y3);
            alVertix.add(z3);

            textureVertix.add(s1);// x1 y1对应纹理坐标
            textureVertix.add(t0);
            textureVertix.add(s0);// x0 y0对应纹理坐标
            textureVertix.add(t0);
            textureVertix.add(s0);// x3 y3对应纹理坐标
            textureVertix.add(t1);

            alVertix.add(x1);
            alVertix.add(y1);
            alVertix.add(z1);
            alVertix.add(x3);
            alVertix.add(y3);
            alVertix.add(z3);
            alVertix.add(x2);
            alVertix.add(y2);
            alVertix.add(z2);

            textureVertix.add(s1);// x1 y1对应纹理坐标
            textureVertix.add(t0);
            textureVertix.add(s0);// x3 y3对应纹理坐标
            textureVertix.add(t1);
            textureVertix.add(s1);// x2 y3对应纹理坐标
            textureVertix.add(t1);
        }
    }
    vCount = alVertix.size() / 3;
    posBuffer = convertToFloatBuffer(alVertix);
    cooBuffer=convertToFloatBuffer(textureVertix);
}

//动态数组转FloatBuffer
private FloatBuffer convertToFloatBuffer(ArrayList<Float> data){
    float[] d=new float[data.size()];
    for (int i=0;i<d.length;i++){
        d[i]=data.get(i);
    }

    ByteBuffer buffer=ByteBuffer.allocateDirect(data.size()*4);
    buffer.order(ByteOrder.nativeOrder());
    FloatBuffer ret=buffer.asFloatBuffer();
    ret.put(d);
    ret.position(0);
    return ret;
}

着色器

相应的顶点着色器和片元着色器分别为:

//顶点着色器
uniform mat4 uProjMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uModelMatrix;
uniform mat4 uRotateMatrix;

attribute vec3 aPosition;
attribute vec2 aCoordinate;

varying vec2 vCoordinate;

void main(){
    gl_Position=uProjMatrix*uViewMatrix*uModelMatrix*vec4(aPosition,1);
    vCoordinate=aCoordinate;
}
//片元着色器
precision highp float;

uniform sampler2D uTexture;
varying vec2 vCoordinate;

void main(){
   vec4 color=texture2D(uTexture,vCoordinate);
   gl_FragColor=color;
}

获取矩阵

准备好了顶点坐标、纹理坐标和着色器,从顶点着色器中可以看出,我们还需要几个变换矩阵,变换矩阵求取:

 public void setSize(int width,int height){
        //计算宽高比
        float ratio=(float)width/height;
        //透视投影矩阵/视锥
        MatrixHelper.perspectiveM(mProjectMatrix,0,45,ratio,1f,300);
        //设置相机位置
        Matrix.setLookAtM(mViewMatrix, 0, 0f, 0.0f,5.0f, 0.0f, 0.0f,-1.0f, 0f,1.0f, 0.0f);
        //模型矩阵
        Matrix.setIdentityM(mModelMatrix,0);
    }

渲染

这样,万事俱备,我们就可以编译glprogram,并进行球体的渲染了:

//编译glprogram并获取控制句柄(onSurfaceCreated时调用)
mHProgram=Gl2Utils.createGlProgramByRes(res,"vr/skysphere.vert","vr/skysphere.frag");
mHProjMatrix=GLES20.glGetUniformLocation(mHProgram,"uProjMatrix");
mHViewMatrix=GLES20.glGetUniformLocation(mHProgram,"uViewMatrix");
mHModelMatrix=GLES20.glGetUniformLocation(mHProgram,"uModelMatrix");
mHUTexture=GLES20.glGetUniformLocation(mHProgram,"uTexture");
mHPosition=GLES20.glGetAttribLocation(mHProgram,"aPosition");
mHCoordinate=GLES20.glGetAttribLocation(mHProgram,"aCoordinate");

//使用Program进行渲染(onDrawFrame中调用)
GLES20.glUseProgram(mHProgram);
GLES20.glUniformMatrix4fv(mHProjMatrix,1,false,mProjectMatrix,0);
GLES20.glUniformMatrix4fv(mHViewMatrix,1,false,mViewMatrix,0);
GLES20.glUniformMatrix4fv(mHModelMatrix,1,false,mModelMatrix,0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureId);

GLES20.glEnableVertexAttribArray(mHPosition);
GLES20.glVertexAttribPointer(mHPosition,3,GLES20.GL_FLOAT,false,0,posBuffer);
GLES20.glEnableVertexAttribArray(mHCoordinate);
GLES20.glVertexAttribPointer(mHCoordinate,2,GLES20.GL_FLOAT,false,0,cooBuffer);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount);

GLES20.glDisableVertexAttribArray(mHCoordinate);
GLES20.glDisableVertexAttribArray(mHPosition);

其中纹理图片如下:


渲染的结果如下:


让球与手机姿态同步

绘制出球体之后,我们需要让球与手机的姿态进行同步,也就是当手机背面超下时,我们看到的应该是地面,手机背面朝上是,我们看到的应该是天空。很明显,这就需要用到手机中的传感器了。

传感器

Android中的传感器定义如下:

//加速度传感器
public static final int TYPE_ACCELEROMETER = 1;
//磁场传感器
public static final int TYPE_MAGNETIC_FIELD = 2;
//方向传感器,已废弃
public static final int TYPE_ORIENTATION = 3;
//陀螺仪
public static final int TYPE_GYROSCOPE = 4;
//光线传感器,接听电话黑屏
public static final int TYPE_LIGHT = 5;
//压力传感器
public static final int TYPE_PRESSURE = 6;
//温度传感器,已废弃
public static final int TYPE_TEMPERATURE = 7;
//近程传感器(接听电话黑屏)
public static final int TYPE_PROXIMITY = 8;
//重力传感器
public static final int TYPE_GRAVITY = 9;
//线性加速度传感器
public static final int TYPE_LINEAR_ACCELERATION = 10;
//旋转矢量传感器
public static final int TYPE_ROTATION_VECTOR = 11;
//湿度传感器
public static final int TYPE_RELATIVE_HUMIDITY = 12;
//环境温度传感器
public static final int TYPE_AMBIENT_TEMPERATURE = 13;
//未校准磁力传感器
public static final int TYPE_MAGNETIC_FIELD_UNCALIBRATED = 14;
//旋转矢量传感器,用来探测运动而不必受到电磁干扰的影响,因为它并不依赖于磁北极
public static final int TYPE_GAME_ROTATION_VECTOR = 15;
//未校准陀螺仪传感器
public static final int TYPE_GYROSCOPE_UNCALIBRATED = 16;
//特殊动作触发传感器
public static final int TYPE_SIGNIFICANT_MOTION = 17;
//步行探测器
public static final int TYPE_STEP_DETECTOR = 18;
//计步器
public static final int TYPE_STEP_COUNTER = 19;
//地磁旋转矢量传感器
public static final int TYPE_GEOMAGNETIC_ROTATION_VECTOR = 20;
//心率传感器
public static final int TYPE_HEART_RATE = 21;
//倾斜探测器,隐藏的systemApi
public static final int TYPE_TILT_DETECTOR = 22;
//唤醒手势传感器,隐藏的systemApi
public static final int TYPE_WAKE_GESTURE = 23;
//快速手势,隐藏的systemApi
public static final int TYPE_GLANCE_GESTURE = 24;
//设备抬起手势,隐藏的systemApi
public static final int TYPE_PICK_UP_GESTURE = 25;
//腕关节抬起手势,隐藏的systemApi
public static final int TYPE_WRIST_TILT_GESTURE = 26;
//设备方向传感器,隐藏的systemApi
public static final int TYPE_DEVICE_ORIENTATION = 27;
//6自由度姿态传感器
public static final int TYPE_POSE_6DOF = 28;
//静止探测器
public static final int TYPE_STATIONARY_DETECT = 29;
//手势传感器
public static final int TYPE_MOTION_DETECT = 30;
//心跳传感器
public static final int TYPE_HEART_BEAT = 31;
//传感器动态添加和删除,隐藏的systemApi
public static final int TYPE_DYNAMIC_SENSOR_META = 32;

虽然在API中定义了这么多的传感器,然后实际上绝大多书手机都不会具备所有的传感器。所以当我们在使用某个传感器时,一定要检测这个传感器是否存在。
根据我们的需求,我们需要获得的是手机的姿态,所以上面的传感器中,我们能使用的方案如下:

  1. 使用旋转矢量传感器
  2. 使用陀螺仪加上磁场传感器
  3. 使用陀螺仪加上方向传感器
  4. 使用6自由度姿态传感器
  5. 或许还有其他方案

传感器使用

我们直接使用旋转矢量传感器来获取手机姿态。传感器的使用相对来说比较简单:

//获取SensorManager
mSensorManager=(SensorManager)getSystemService(Context.SENSOR_SERVICE);
List<Sensor> sensors=mSensorManager.getSensorList(Sensor.TYPE_ALL);
//todo 判断是否存在rotation vector sensor
//获取旋转矢量传感器
mRotation=mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
//注册传感器  监听器     mSensorManager.registerListener(this,mRotation,SensorManager.SENSOR_DELAY_GAME);

然后再监听器中处理数据就可以了:

@Override
public void onSensorChanged(SensorEvent event) {
    SensorManager.getRotationMatrixFromVector(matrix,event.values);
    mSkySphere.setMatrix(matrix);
}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {

}

传感器数据与渲染结合

利用旋转矢量传感器我们很方便的获得了一个旋转矩阵,将这个矩阵传递到顶点着色器我们就可以让球体随着手机的姿态变化而变化了。
修改顶点着色器中顶点的计算为:

gl_Position=uProjMatrix*uViewMatrix*uRotateMatrix*uModelMatrix*vec4(aPosition,1);

然后获取旋转矩阵的句柄并将旋转矩阵传递进来:

mHRotateMatrix=GLES20.glGetUniformLocation(mHProgram,"uRotateMatrix");
GLES20.glUniformMatrix4fv(mHRotateMatrix,1,false,mRotateMatrix,0);

这样,球的旋转就和手机姿态同步了



然而我们需要的,并不是这样的结果,仔细想想,天空球模式的话,相机应该是在球的内部,我们看天空看大地,左看右看的时候,应该是人相机在动,而不是球在动。而我们现在看到的却是球自己转动。问题出在哪儿呢?
gl_Position=uProjMatrix*uViewMatrix*uRotateMatrix*uModelMatrix*vec4(aPosition,1);中可以看到,顶点的坐标计算中,我们是用从传感器获得的旋转矩阵在模型矩阵前,这样我们的旋转操作的就是球体,修改为:

gl_Position=uProjMatrix*uRotateMatrix*uViewMatrix*uModelMatrix*vec4(aPosition,1);

这样,我们操作的就是相机了,得到的渲染结果如下,当摄像头对的方向变话,球在屏幕上的位置也会发生变换,就像我们头转动时,看到的东西在我们眼睛中成像的位置也会发生变化。



进入天空球内部

完成上述操作,我们里成功就剩下一步之遥了。上面的操作,我们始终在球的外面看球,就如同我们在外太空看地球一样。现在我们需要回到球的内部来看球。在获取矩阵时,我们的视图矩阵求法如下:

 //设置相机位置
 //第一个参数为最终的矩阵存储数组,第二个参数为数组的偏移
 //第3-5个参数为相机位置,第6-8个参数为相机视线方向,第9-11个参数为相机的up方向
 Matrix.setLookAtM(mViewMatrix, 0, 0f, 0.0f,5.0f, 0.0f, 0.0f,-1.0f, 0f,1.0f, 0.0f);

根据上面矩阵可以看到,很简单,我们只需要将相机位置改为球的圆心就可以了,当然也可以是球内的其他位置,但是效果上肯定是不如让相机和球心重合。

Matrix.setLookAtM(mViewMatrix, 0, 0f, 0.0f,0.0f, 0.0f, 0.0f,-1.0f, 0f,1.0f, 0.0f);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容