在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,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

Android OpenGLES2.0(十七)——球形天空盒VR效果实现的更多相关文章

  1. (转)使用OpenGL显示图像(七)Android OpenGLES2.0——纹理贴图之显示图片

    转:http://blog.csdn.net/junzia/article/details/52842816 前面几篇博客,我们将了Android中利用OpenGL ES 2.0绘制各种形体,并在上一 ...

  2. Android 7.0 新增功能和api

    Android 7.0 Nougat 为用户和开发者引入多种新功能.本文重点介绍面向开发者的新功能. 请务必查阅 Android 7.0 行为变更以了解平台变更可能影响您的应用的领域. 要详细了解 A ...

  3. Android上使用OpenglES2.0遇到的一点问题

    按照教程开发OpenglES2.0应用,遇到Logcat报错“Called unimplemented OpenGL ES API” 在论坛和stackoverflow上找到了答案. 1.manife ...

  4. 深度了解Android 7.0 ,你准备好了吗?

    作者:Redyan, 腾讯移动客户端开发工程师 商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. 原文链接:http://wetest.qq.com/lab/view/288.html ...

  5. Android 7.0 Nougat牛轧糖 发布啦

    Android 7.0 Nougat牛轧糖 发布啦 Android 7.0 Nougat 牛轧糖于本月发布了. 从官方blog里可以了解到这个版本的新特性. Android 7.0 从2016年8月正 ...

  6. win7下搭建opengles2.0编程环境

    原帖地址:http://sixgod.org/archives/72   1.下载AMD的OpenGL ES2.0的模拟器,地址: http://www.opengles-book.com/ESEmu ...

  7. Android 7.0(牛轧糖)新特性

    Android 7.0(牛轧糖)新特性 谷歌正式在I/O大会现场详细介绍了有关Android 7.0的大量信息.目前,我们已经知道,新一代Android操作系统将支持无缝升级,能够通过Vulkan A ...

  8. Android 7.0新特性“Nougat”(牛轧糖)。

    1.Unicode 9支持和全新的emoji表情符号 Android Nougat将会支持Unicode 9,并且会新增大约70种emoji表情符号.这些表情符号大多数都是人形的,并且提供不同的肤色, ...

  9. Android 9.0 默认输入法的设置流程分析

    Android 输入法设置文章 Android 9.0 默认输入法的设置流程分析 Android 9.0 添加预置第三方输入法/设置默认输入法(软键盘) 前言 在上一篇文章  Android 9.0 ...

随机推荐

  1. Linux vi/vim使用方法

    vi/vim 基本使用方法 vi编辑器是所有Unix及Linux系统下标准的编辑器,它的强大不逊色于任何最新的文本编辑器,这里只是简单地介绍一下它的用法和一小部分指令. 1.vi的基本概念 基本上vi ...

  2. Redis命令参考之复制(Replication)

    Redis 支持简单且易用的主从复制(master-slave replication)功能, 该功能可以让从服务器(slave server)成为主服务器(master server)的精确复制品. ...

  3. BEC listen and translation exercise 45

    So the Counselling Services we offer deal with any problems arising from your studies or in your lif ...

  4. Android简单数据存储SharedPreferences

    SharedPreferences是Android中存储简单数据的一个工具类.可以想象它是一个小小的Cookie,它通过用键值对的方式把简单数据类型(boolean.int.float.long和St ...

  5. ACM学习历程—HDU 5072 Coprime(容斥原理)

    Description There are n people standing in a line. Each of them has a unique id number. Now the Ragn ...

  6. INT 21H 指令说明及使用方法

    很多初学汇编语言的同学可能会对INT 21H这条指令感到困惑,不知道是什么意思,下面就以一段简单的程序为大家讲解: 例如:需要键盘输入,并且回显. AH的值需要查表取得,表在下面 指令:      M ...

  7. gulp安装简介

    1 全局安装gulp:npm install -g gulp 2 在项目根目录中,安装项目的开发依赖:npm install --save-dev gulp 2.1 根据gulpfile.js中的依赖 ...

  8. bzoj 3157 & bzoj 3516 国王奇遇记 —— 推式子

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=3157 https://www.lydsy.com/JudgeOnline/problem.p ...

  9. 【转】 Pro Android学习笔记(四四):Dialog(1):触发Dialog

    目录(?)[-] 创建dialog fragment Activity显示对话框 Android提供alert.prompt.pick-list,单选.多选,progress.time-picker和 ...

  10. js右击事件

    先贴代码: <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF ...