在3D游戏中一般都会用到天空盒,在3D引擎中也通常会存在天空盒组件,让开发者能够直接使用。那么天空盒是什么?天空盒又是如何实现的呢?本篇博客主要介绍如何在Android中利用OpenGLES绘制一个天空盒,并实现VR效果。数组
天空盒、天空穹、天空球和VRide
虽然大多数人知道这些东西是啥,可是我以为我仍是有必要把他们的定义“搬”过来,万一有人不知道呢。性能
天空盒(Sky Box)是放到场景中的一个立方体,常常是由六个面组成的立方体,并常常会随着视点的移动而移动。天空盒将刻画极远处人没法达到的位置的景物。
天空穹(Sky Dome)与天空盒相似,只不过它将是天空盒除底面之外的五个面换成了一个曲面,能够理解成一个半球。和古人认为的天圆地方差很少。
天空球(Sky Sphere)就是把天空盒直接换成一个球——据说没有天空球这个说法?无所谓了,如今有了。
VR(Virtual Reality)虚的定义不说了,本篇博客所说的VR效果就是手机上显示的图像由手机的姿态来控制而已。
天空盒的实现应该是最简单的,可是效果可能会有些瑕疵,尤为是顶着两面的交点处总能看出点不一样。天空穹和天空球效果都差很少,会比天空盒好上不少,可是相对天空盒来讲,比较耗性能。this
绘制一个球orm
在以前的博客中Android OpenGLES2.0(六)——构建圆锥、圆柱和球体有介绍如何绘制一个球,只不过以前的球是没有贴图的,如今咱们绘制一个球,并为它贴上环境的贴图。就像绘制一个地球仪同样。游戏
顶点坐标和纹理坐标计算图片
首先,首先咱们须要获得球的顶点坐标和纹理坐标:ci
//计算顶点坐标和纹理坐标
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)));get
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,vCoor www.acnet.cn/ dinate);
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, http://tangrenyule11.cn/ 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 http://huaren88cai.cn/ = 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
虽然在API中定义了这么多的传感器,而后实际上绝大多书手机都不会具有全部的传感器。因此当咱们在使用某个传感器时,必定要检测这个传感器是否存在。
根据咱们的需求,咱们须要得到的是手机的姿态,因此上面的传感器中,咱们能使用的方案以下:
使用旋转矢量传感器
使用陀螺仪加上磁场传感器
使用陀螺仪加上方向传感器
使用6自由度姿态传感器
或许还有其余方案
传感器使用
咱们直接使用旋转矢量传感器来获取手机姿态。传感器的使用相对来讲比较简单:
//获取SensorManager
mSensorManager=(SensorManager)http://www.wansenpingtai22.cn/ 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);
1
1
而后获取旋转矩阵的句柄并将旋转矩阵传递进来:
mHRotateMatrix=GLES20.glGetUniformLocation(mHProgram,"uRotateMatrix");
GLES20.glUniformMatrix4fv(mHRotateMatrix,1,false,mRotateMatrix,0);
1
2
1
2
这样,球的旋转就和手机姿态同步了
这里写图片描述这里写图片描述
然而咱们须要的,并非这样的结果,仔细想一想,天空球模式的话,相机应该是在球的内部,咱们看天空看大地,左看右看的时候,应该是人相机在动,而不是球在动。而咱们如今看到的倒是球本身转动。问题出在哪儿呢?
从gl_Position=uProjMatrix*uViewMatrix*uRotateMatrix*uModelMatrix*vec4(aPosition,1);中能够看到,顶点的坐标计算中,咱们是用从传感器得到的旋转矩阵在模型矩阵前,这样咱们的旋转操做的就是球体,修改成:
gl_Position=uProjMatrix*uRotateMatrix*uViewMatrix*uModelMatrix*vec4(aPosition,www.caihonyule.com/1);
1
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,www.zzdaiy2019.cn0.0f, 0.0f, 0.0f,-1.0f, 0f,1.0f, 0.0f);
1
1
这里写图片描述这里写图片描述
源码
全部的代码所有在一个项目中,托管在Github上,欢迎Star和Fork——Android OpenGLES 2.0系列博客的Demo