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

在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

相關文章
相關標籤/搜索