Android OpenGLES2.0(十七)——球形天空盒VR效果實(shí)現(xiàn)

在3D游戲中通常都會(huì)用到天空盒,在3D引擎中也一般會(huì)存在天空盒組件,讓開(kāi)發(fā)者可以直接使用。那么天空盒是什么?天空盒又是如何實(shí)現(xiàn)的呢?本篇博客主要介紹如何在Android中利用OpenGLES繪制一個(gè)天空盒,并實(shí)現(xiàn)VR效果。

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

雖然大多數(shù)人知道這些東西是啥,但是我覺(jué)得我還是有必要把他們的定義“搬”過(guò)來(lái),萬(wàn)一有人不知道呢。

  • 天空盒(Sky Box)是放到場(chǎng)景中的一個(gè)立方體,經(jīng)常是由六個(gè)面組成的立方體,并經(jīng)常會(huì)隨著視點(diǎn)的移動(dòng)而移動(dòng)。天空盒將刻畫(huà)極遠(yuǎn)處人無(wú)法達(dá)到的位置的景物。
  • 天空穹(Sky Dome)與天空盒類似,只不過(guò)它將是天空盒除底面以外的五個(gè)面換成了一個(gè)曲面,可以理解成一個(gè)半球。和古人認(rèn)為的天圓地方差不多。
  • 天空球(Sky Sphere)就是把天空盒直接換成一個(gè)球——聽(tīng)說(shuō)沒(méi)有天空球這個(gè)說(shuō)法?無(wú)所謂了,現(xiàn)在有了。
  • VR(Virtual Reality)虛的定義不說(shuō)了,本篇博客所說(shuō)的VR效果就是手機(jī)上顯示的圖像由手機(jī)的姿態(tài)來(lái)控制而已。

天空盒的實(shí)現(xiàn)應(yīng)該是最簡(jiǎn)單的,但是效果可能會(huì)有些瑕疵,尤其是頂著兩面的交點(diǎn)處總能看出點(diǎn)不同。天空穹和天空球效果都差不多,會(huì)比天空盒好上很多,但是相對(duì)天空盒來(lái)說(shuō),比較耗性能。

繪制一個(gè)球

在之前的博客中Android OpenGLES2.0(六)——構(gòu)建圓錐、圓柱和球體有介紹如何繪制一個(gè)球,只不過(guò)之前的球是沒(méi)有貼圖的,現(xiàn)在我們繪制一個(gè)球,并為它貼上環(huán)境的貼圖。就像繪制一個(gè)地球儀一樣。

頂點(diǎn)坐標(biāo)和紋理坐標(biāo)計(jì)算

首先,首先我們需要得到球的頂點(diǎn)坐標(biāo)和紋理坐標(biāo):

//計(jì)算頂點(diǎn)坐標(biāo)和紋理坐標(biāo)
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對(duì)應(yīng)紋理坐標(biāo)
            textureVertix.add(t0);
            textureVertix.add(s0);// x0 y0對(duì)應(yīng)紋理坐標(biāo)
            textureVertix.add(t0);
            textureVertix.add(s0);// x3 y3對(duì)應(yīng)紋理坐標(biāo)
            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對(duì)應(yīng)紋理坐標(biāo)
            textureVertix.add(t0);
            textureVertix.add(s0);// x3 y3對(duì)應(yīng)紋理坐標(biāo)
            textureVertix.add(t1);
            textureVertix.add(s1);// x2 y3對(duì)應(yīng)紋理坐標(biāo)
            textureVertix.add(t1);
        }
    }
    vCount = alVertix.size() / 3;
    posBuffer = convertToFloatBuffer(alVertix);
    cooBuffer=convertToFloatBuffer(textureVertix);
}

//動(dòng)態(tài)數(shù)組轉(zhuǎn)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;
}

著色器

相應(yīng)的頂點(diǎn)著色器和片元著色器分別為:

//頂點(diǎn)著色器
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;
}

獲取矩陣

準(zhǔn)備好了頂點(diǎn)坐標(biāo)、紋理坐標(biāo)和著色器,從頂點(diǎn)著色器中可以看出,我們還需要幾個(gè)變換矩陣,變換矩陣求取:

 public void setSize(int width,int height){
        //計(jì)算寬高比
        float ratio=(float)width/height;
        //透視投影矩陣/視錐
        MatrixHelper.perspectiveM(mProjectMatrix,0,45,ratio,1f,300);
        //設(shè)置相機(jī)位置
        Matrix.setLookAtM(mViewMatrix, 0, 0f, 0.0f,5.0f, 0.0f, 0.0f,-1.0f, 0f,1.0f, 0.0f);
        //模型矩陣
        Matrix.setIdentityM(mModelMatrix,0);
    }

渲染

這樣,萬(wàn)事俱備,我們就可以編譯glprogram,并進(jìn)行球體的渲染了:

//編譯glprogram并獲取控制句柄(onSurfaceCreated時(shí)調(diào)用)
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進(jìn)行渲染(onDrawFrame中調(diào)用)
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);

其中紋理圖片如下:


渲染的結(jié)果如下:


讓球與手機(jī)姿態(tài)同步

繪制出球體之后,我們需要讓球與手機(jī)的姿態(tài)進(jìn)行同步,也就是當(dāng)手機(jī)背面超下時(shí),我們看到的應(yīng)該是地面,手機(jī)背面朝上是,我們看到的應(yīng)該是天空。很明顯,這就需要用到手機(jī)中的傳感器了。

傳感器

Android中的傳感器定義如下:

//加速度傳感器
public static final int TYPE_ACCELEROMETER = 1;
//磁場(chǎng)傳感器
public static final int TYPE_MAGNETIC_FIELD = 2;
//方向傳感器,已廢棄
public static final int TYPE_ORIENTATION = 3;
//陀螺儀
public static final int TYPE_GYROSCOPE = 4;
//光線傳感器,接聽(tīng)電話黑屏
public static final int TYPE_LIGHT = 5;
//壓力傳感器
public static final int TYPE_PRESSURE = 6;
//溫度傳感器,已廢棄
public static final int TYPE_TEMPERATURE = 7;
//近程傳感器(接聽(tīng)電話黑屏)
public static final int TYPE_PROXIMITY = 8;
//重力傳感器
public static final int TYPE_GRAVITY = 9;
//線性加速度傳感器
public static final int TYPE_LINEAR_ACCELERATION = 10;
//旋轉(zhuǎn)矢量傳感器
public static final int TYPE_ROTATION_VECTOR = 11;
//濕度傳感器
public static final int TYPE_RELATIVE_HUMIDITY = 12;
//環(huán)境溫度傳感器
public static final int TYPE_AMBIENT_TEMPERATURE = 13;
//未校準(zhǔn)磁力傳感器
public static final int TYPE_MAGNETIC_FIELD_UNCALIBRATED = 14;
//旋轉(zhuǎn)矢量傳感器,用來(lái)探測(cè)運(yùn)動(dòng)而不必受到電磁干擾的影響,因?yàn)樗⒉灰蕾囉诖疟睒O
public static final int TYPE_GAME_ROTATION_VECTOR = 15;
//未校準(zhǔn)陀螺儀傳感器
public static final int TYPE_GYROSCOPE_UNCALIBRATED = 16;
//特殊動(dòng)作觸發(fā)傳感器
public static final int TYPE_SIGNIFICANT_MOTION = 17;
//步行探測(cè)器
public static final int TYPE_STEP_DETECTOR = 18;
//計(jì)步器
public static final int TYPE_STEP_COUNTER = 19;
//地磁旋轉(zhuǎn)矢量傳感器
public static final int TYPE_GEOMAGNETIC_ROTATION_VECTOR = 20;
//心率傳感器
public static final int TYPE_HEART_RATE = 21;
//傾斜探測(cè)器,隱藏的systemApi
public static final int TYPE_TILT_DETECTOR = 22;
//喚醒手勢(shì)傳感器,隱藏的systemApi
public static final int TYPE_WAKE_GESTURE = 23;
//快速手勢(shì),隱藏的systemApi
public static final int TYPE_GLANCE_GESTURE = 24;
//設(shè)備抬起手勢(shì),隱藏的systemApi
public static final int TYPE_PICK_UP_GESTURE = 25;
//腕關(guān)節(jié)抬起手勢(shì),隱藏的systemApi
public static final int TYPE_WRIST_TILT_GESTURE = 26;
//設(shè)備方向傳感器,隱藏的systemApi
public static final int TYPE_DEVICE_ORIENTATION = 27;
//6自由度姿態(tài)傳感器
public static final int TYPE_POSE_6DOF = 28;
//靜止探測(cè)器
public static final int TYPE_STATIONARY_DETECT = 29;
//手勢(shì)傳感器
public static final int TYPE_MOTION_DETECT = 30;
//心跳傳感器
public static final int TYPE_HEART_BEAT = 31;
//傳感器動(dòng)態(tài)添加和刪除,隱藏的systemApi
public static final int TYPE_DYNAMIC_SENSOR_META = 32;

雖然在API中定義了這么多的傳感器,然后實(shí)際上絕大多書(shū)手機(jī)都不會(huì)具備所有的傳感器。所以當(dāng)我們?cè)谑褂媚硞€(gè)傳感器時(shí),一定要檢測(cè)這個(gè)傳感器是否存在。
根據(jù)我們的需求,我們需要獲得的是手機(jī)的姿態(tài),所以上面的傳感器中,我們能使用的方案如下:

  1. 使用旋轉(zhuǎn)矢量傳感器
  2. 使用陀螺儀加上磁場(chǎng)傳感器
  3. 使用陀螺儀加上方向傳感器
  4. 使用6自由度姿態(tài)傳感器
  5. 或許還有其他方案

傳感器使用

我們直接使用旋轉(zhuǎn)矢量傳感器來(lái)獲取手機(jī)姿態(tài)。傳感器的使用相對(duì)來(lái)說(shuō)比較簡(jiǎn)單:

//獲取SensorManager
mSensorManager=(SensorManager)getSystemService(Context.SENSOR_SERVICE);
List<Sensor> sensors=mSensorManager.getSensorList(Sensor.TYPE_ALL);
//todo 判斷是否存在rotation vector sensor
//獲取旋轉(zhuǎn)矢量傳感器
mRotation=mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
//注冊(cè)傳感器  監(jiān)聽(tīng)器     mSensorManager.registerListener(this,mRotation,SensorManager.SENSOR_DELAY_GAME);

然后再監(jiān)聽(tīng)器中處理數(shù)據(jù)就可以了:

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

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

}

傳感器數(shù)據(jù)與渲染結(jié)合

利用旋轉(zhuǎn)矢量傳感器我們很方便的獲得了一個(gè)旋轉(zhuǎn)矩陣,將這個(gè)矩陣傳遞到頂點(diǎn)著色器我們就可以讓球體隨著手機(jī)的姿態(tài)變化而變化了。
修改頂點(diǎn)著色器中頂點(diǎn)的計(jì)算為:

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

然后獲取旋轉(zhuǎn)矩陣的句柄并將旋轉(zhuǎn)矩陣傳遞進(jìn)來(lái):

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

這樣,球的旋轉(zhuǎn)就和手機(jī)姿態(tài)同步了



然而我們需要的,并不是這樣的結(jié)果,仔細(xì)想想,天空球模式的話,相機(jī)應(yīng)該是在球的內(nèi)部,我們看天空看大地,左看右看的時(shí)候,應(yīng)該是人相機(jī)在動(dòng),而不是球在動(dòng)。而我們現(xiàn)在看到的卻是球自己轉(zhuǎn)動(dòng)。問(wèn)題出在哪兒呢?
gl_Position=uProjMatrix*uViewMatrix*uRotateMatrix*uModelMatrix*vec4(aPosition,1);中可以看到,頂點(diǎn)的坐標(biāo)計(jì)算中,我們是用從傳感器獲得的旋轉(zhuǎn)矩陣在模型矩陣前,這樣我們的旋轉(zhuǎn)操作的就是球體,修改為:

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

這樣,我們操作的就是相機(jī)了,得到的渲染結(jié)果如下,當(dāng)攝像頭對(duì)的方向變?cè)挘蛟谄聊簧系奈恢靡矔?huì)發(fā)生變換,就像我們頭轉(zhuǎn)動(dòng)時(shí),看到的東西在我們眼睛中成像的位置也會(huì)發(fā)生變化。



進(jìn)入天空球內(nèi)部

完成上述操作,我們里成功就剩下一步之遙了。上面的操作,我們始終在球的外面看球,就如同我們?cè)谕馓湛吹厍蛞粯印,F(xiàn)在我們需要回到球的內(nèi)部來(lái)看球。在獲取矩陣時(shí),我們的視圖矩陣求法如下:

 //設(shè)置相機(jī)位置
 //第一個(gè)參數(shù)為最終的矩陣存儲(chǔ)數(shù)組,第二個(gè)參數(shù)為數(shù)組的偏移
 //第3-5個(gè)參數(shù)為相機(jī)位置,第6-8個(gè)參數(shù)為相機(jī)視線方向,第9-11個(gè)參數(shù)為相機(jī)的up方向
 Matrix.setLookAtM(mViewMatrix, 0, 0f, 0.0f,5.0f, 0.0f, 0.0f,-1.0f, 0f,1.0f, 0.0f);

根據(jù)上面矩陣可以看到,很簡(jiǎn)單,我們只需要將相機(jī)位置改為球的圓心就可以了,當(dāng)然也可以是球內(nèi)的其他位置,但是效果上肯定是不如讓相機(jī)和球心重合。

Matrix.setLookAtM(mViewMatrix, 0, 0f, 0.0f,0.0f, 0.0f, 0.0f,-1.0f, 0f,1.0f, 0.0f);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容