本节我们在上一节的基础上来分析一下天空盒的实现。我们要分析的目标就是《OpenGL ES应用开发实践指南 Android卷》书中第11章实现的最终的结果,代码下载请点击:Opengl ES Source Code,该Git库中的skybox Module就是我们本节要分析的目标,先看下本节最终实现的结果,CSDN对GIF动画文件的大小有限制,所以只能录制三个文件来看效果。
需要说明,我们本节的大部分内容和上一节的内容完全相同,只是在上一节的基础上增加新的功能而已,所以如果对一节的内容有任何疑问,请大家先回过头去搞清楚上一节的原理:Opengl ES系列学习--用粒子增添趣味。
我们本节就分析和上一节代码不同的地方,因为该Module涉及到文件也不多,差异的地方也不多,我们就直接逐个分析所有的文件,文件结构图如下。
VertexArray、ParticleShooter、ParticleSystem类和上一节完全相同,我们就直接跳过了;接着看Skybox,该类就是为了绘制天空盒定义的,源码如下:
public class Skybox {
private static final int POSITION_COMPONENT_COUNT = 3;
private final VertexArray vertexArray;
private final ByteBuffer indexArray;
public Skybox() {
// Create a unit cube.
vertexArray = new VertexArray(new float[] {
-1, 1, 1, // (0) Top-left near
1, 1, 1, // (1) Top-right near
-1, -1, 1, // (2) Bottom-left near
1, -1, 1, // (3) Bottom-right near
-1, 1, -1, // (4) Top-left far
1, 1, -1, // (5) Top-right far
-1, -1, -1, // (6) Bottom-left far
1, -1, -1 // (7) Bottom-right far
});
// 6 indices per cube side
indexArray = ByteBuffer.allocateDirect(6 * 6)
.put(new byte[] {
// Front
1, 3, 0,
3, 2, 0,
// Back
4, 6, 5,
5, 6, 7,
// Left
0, 2, 4,
4, 2, 6,
// Right
5, 7, 1,
1, 7, 3,
// Top
5, 1, 4,
4, 1, 0,
// Bottom
6, 2, 7,
7, 2, 3
});
indexArray.position(0);
}
public void bindData(SkyboxShaderProgram skyboxProgram) {
vertexArray.setVertexAttribPointer(0,
skyboxProgram.getPositionAttributeLocation(),
POSITION_COMPONENT_COUNT, 0);
}
public void draw() {
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, indexArray);
}
}
成员变量POSITION_COMPONENT_COUNT和粒子发射系统中的含义相同,也表示描述一个顶点需要3个size;接着定义两个buffer来存储数据,继续看构造方法,我们把要绘制的天空盒看成立方体,一个立方体有6个面,8个顶点,每个顶点的位置注释中也写的非常清楚了,它们的构成如下图。
好,知道了每个顶点的位置,我们要绘制一个面,就需要两个三角形就可以了,这里直接使用索引数组来指定每个顶点的位置,比如我们要绘制前面的这个面,它的四个顶点分别是0、1、2、3,那么我们定义(1,3,0)、(3,2,0)两个索引数组就可以正确的描述出两个三角形了,而每个索引值是一个float,绘制所有面的三角形需要36个float(一个面6个,一共6个面,所以是6 * 6),按照索引值定义好所有的三角形索引值,存储到indexArray当中,需要绘制时就从它当中取值,就可以绘制出来了;bindData方法很简单,就是指定数组的取的方式,跨距stride为0,因为我们的顶点数组中,存储的全部是位置属性,没有其他属性,所以stride为0;draw方法中指定使用GL_TRIANGLES绘制三角形的方式绘制我们定义的所有顶点,总共是36个顶点。
接下来看SkyboxShaderProgram,源码如下:
public class SkyboxShaderProgram extends ShaderProgram {
private final int uMatrixLocation;
private final int uTextureUnitLocation;
private final int aPositionLocation;
public SkyboxShaderProgram(Context context) {
super(context, R.raw.skybox_vertex_shader,
R.raw.skybox_fragment_shader);
uMatrixLocation = glGetUniformLocation(program, U_MATRIX);
uTextureUnitLocation = glGetUniformLocation(program, U_TEXTURE_UNIT);
aPositionLocation = glGetAttribLocation(program, A_POSITION);
}
public void setUniforms(float[] matrix, int textureId) {
glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureId);
glUniform1i(uTextureUnitLocation, 0);
}
public int getPositionAttributeLocation() {
return aPositionLocation;
}
}
该类是为了实现天空盒着色器定义的,三个成员变量定义属性,构造方法加载着色器程序并完成属性查找,setUniforms方法给属性赋值,和ParticleShaderProgram都是大同小异,我们就不细讲了,如果还有疑问,前回到前一篇弄清楚。
util包下的工具类都相同,我们就跳过了。
接着看ParticlesActivity类,和上一节不同的,就是给glSurfaceView设置了OnTouchListener,并计算手指每次和上一次相比的移动距离,再把结果作为参数调用handleTouchDrag方法。我们继续看ParticlesRenderer类,源码如下:
public class ParticlesRenderer implements Renderer {
private final Context context;
private final float[] projectionMatrix = new float[16];
private final float[] viewMatrix = new float[16];
private final float[] viewProjectionMatrix = new float[16];
private SkyboxShaderProgram skyboxProgram;
private Skybox skybox;
private ParticleShaderProgram particleProgram;
private ParticleSystem particleSystem;
private ParticleShooter redParticleShooter;
private ParticleShooter greenParticleShooter;
private ParticleShooter blueParticleShooter;
private long globalStartTime;
private int particleTexture;
private int skyboxTexture;
private float xRotation, yRotation;
public ParticlesRenderer(Context context) {
this.context = context;
}
public void handleTouchDrag(float deltaX, float deltaY) {
xRotation += deltaX / 16f;
yRotation += deltaY / 16f;
if (yRotation < -90) {
yRotation = -90;
} else if (yRotation > 90) {
yRotation = 90;
}
}
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
skyboxProgram = new SkyboxShaderProgram(context);
skybox = new Skybox();
particleProgram = new ParticleShaderProgram(context);
particleSystem = new ParticleSystem(10000);
globalStartTime = System.nanoTime();
final Vector particleDirection = new Vector(0f, 0.5f, 0f);
final float angleVarianceInDegrees = 5f;
final float speedVariance = 1f;
redParticleShooter = new ParticleShooter(
new Point(-1f, 0f, 0f),
particleDirection,
Color.rgb(255, 50, 5),
angleVarianceInDegrees,
speedVariance);
greenParticleShooter = new ParticleShooter(
new Point(0f, 0f, 0f),
particleDirection,
Color.rgb(25, 255, 25),
angleVarianceInDegrees,
speedVariance);
blueParticleShooter = new ParticleShooter(
new Point(1f, 0f, 0f),
particleDirection,
Color.rgb(5, 50, 255),
angleVarianceInDegrees,
speedVariance);
particleTexture = TextureHelper.loadTexture(context, R.drawable.particle_texture);
skyboxTexture = TextureHelper.loadCubeMap(context,
new int[] { R.drawable.left, R.drawable.right,
R.drawable.bottom, R.drawable.top,
R.drawable.front, R.drawable.back});
}
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
glViewport(0, 0, width, height);
MatrixHelper.perspectiveM(projectionMatrix, 45, (float) width
/ (float) height, 1f, 10f);
}
@Override
public void onDrawFrame(GL10 glUnused) {
glClear(GL_COLOR_BUFFER_BIT);
drawSkybox();
drawParticles();
}
private void drawSkybox() {
setIdentityM(viewMatrix, 0);
rotateM(viewMatrix, 0, -yRotation, 1f, 0f, 0f);
rotateM(viewMatrix, 0, -xRotation, 0f, 1f, 0f);
multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
skyboxProgram.useProgram();
skyboxProgram.setUniforms(viewProjectionMatrix, skyboxTexture);
skybox.bindData(skyboxProgram);
skybox.draw();
}
private void drawParticles() {
float currentTime = (System.nanoTime() - globalStartTime) / 1000000000f;
redParticleShooter.addParticles(particleSystem, currentTime, 1);
greenParticleShooter.addParticles(particleSystem, currentTime, 1);
blueParticleShooter.addParticles(particleSystem, currentTime, 1);
setIdentityM(viewMatrix, 0);
rotateM(viewMatrix, 0, -yRotation, 1f, 0f, 0f);
rotateM(viewMatrix, 0, -xRotation, 0f, 1f, 0f);
translateM(viewMatrix, 0, 0f, -1.5f, -5f);
multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE);
particleProgram.useProgram();
particleProgram.setUniforms(viewProjectionMatrix, currentTime, particleTexture);
particleSystem.bindData(particleProgram);
particleSystem.draw();
glDisable(GL_BLEND);
}
}
该类和上一节相比,只是增加了天空盒部分的逻辑,其他的完全相同。首先实现handleTouchDrag方法,就是把触摸距离除以16并赋值给成员变量作为角度,Y轴作了-90到90度的范围限定,我们可以试一下,我们横方向可以一直滑动,而且会回到初始状态,假如我们初始是粒子发射器在中间,然后我们沿X轴也就是横方向滑动,一直滑,粒子发射器的位置先消失在屏幕中,过一会又回来了,但是我们沿Y轴,竖方向滑动,则不会,竖方向不管向上或者向下,都会有界限,到界面处就不会再有反应了,这就是因为这里对Y轴的角度限定在-90到90范围之内决定的,我们可以试着把这个限定去掉,那么Y方向的滑动也可以再次回来;onSurfaceCreated方法中需要创建我们的天空盒对象,然后调用TextureHelper.loadCubeMap加载天空盒纹理,这里需要注意,六个方向的顺序是left、right、bottom、top、front、back,千万不能搞反!!!加载成功,则我们会得到纹理ID;onSurfaceChanged方法当中把原来对矩阵的操作去掉了,放在了drawParticles方法内部,因为怕影响天空盒的逻辑,所以单独只把矩阵操作作用在粒子发射器上,天使盒有自己的矩阵操作;最后分别通过调用drawSkybox()、drawParticles()来完成粒子系统和天空盒的绘制。
我们来看一下drawSkybox方法,首先得到单位矩阵,然后调用rotateM对它进行旋转,关于矩阵旋转有问题的,请回头看Opengl ES系列学习--序,其中对旋转的原理已经作了详细的讲解。我们观察一下两行旋转的代码,为什么第一行沿X轴旋转时,传入的是yRotation,而第二行沿Y轴旋转时传入的又是xRotation呢?可以想象一下,当我们手指沿屏幕竖向滑动时(对应的delta是Y方向有偏移),我们想要图像上下旋转,这种效果是不是就是沿X轴旋转呢?而我们横向滑动时(对应的delta是X方向有偏移),图像在屏幕内左右旋转,也就是绕Y轴旋转,所以就是这个意思了。加上负号的意思也很清晰了,比如我们向下滑动,deltaY为正值,而此时图像是向着我们手指的反方向运动的,所以就需要传负值了;drawParticles方法当中传入负值也是这个意思。
粒子着色器particle_vertex_shader.glsl和particle_fragment_shader.glsl和上一节完全相同,我们来看一下天空盒着色器skybox_vertex_shader.glsl和skybox_fragment_shader.glsl。
先来看skybox_vertex_shader.glsl,源码如下:
uniform mat4 u_Matrix;
attribute vec3 a_Position;
varying vec3 v_Position;
void main()
{
v_Position = a_Position;
// Make sure to convert from the right-handed coordinate system of the
// world to the left-handed coordinate system of the cube map, otherwise,
// our cube map will still work but everything will be flipped.
v_Position.z = -v_Position.z;
gl_Position = u_Matrix * vec4(a_Position, 1.0);
gl_Position = gl_Position.xyww;
}
首先还是定义透视投影矩阵,它的值也是在setUniforms方法中传入的;然后是位置属性a_Position、传递参数v_Position;v_Position.z = -v_Position.z的意思是将Z轴方向反转,我们的视点是处于立方体内部的,而立方体贴图的惯例是在立方体内部使用左手坐标系统,而在立方体外部使用右手坐标系统,所以我们需要将Z轴反转,我们也可以把这行代码注释掉,立方体贴图仍然可以正常工作,但是贴图的方向看着就是反的。
gl_Position = gl_Position.xyww这句代码看起来逻辑很简单,就是把W分量的值赋值给Z分量,这样透视除法作用时,Z除以W的结果为1,相当于把物体置在Z轴为1的远平面的位置上了!我也可以使用如下两种方法来处理Z分量的值,也可以显示正确的效果。
gl_Position = gl_Position.xyww;
// gl_Position.z = gl_Position.w;
// gl_Position.z = gl_Position.w / 10.0;
第一种就是直接把W分量的值赋值给Z分量,第二种是把W分量除以10.0后赋值给Z分量,尤其第二种,相除之后,效果没有任何变化,所以对这句代码的内层含义还是没有理解,如果有哪位朋友清楚的话,请指点一下。第二种相除时,一定注意,不能写除以10,这样着色器会编译出错,日志如下:
gl_Position = u_Matrix * vec4(a_Position, 1.0);
// gl_Position = gl_Position.xyww;
// gl_Position.z = gl_Position.w;
gl_Position.z = gl_Position.w / 10;
}
:ERROR: 0:16: '/' : wrong operand types no operation '/' exists that takes a left-hand operand of type 'float' and a right operand of type 'const int' (or there is no acceptable conversion)
ERROR: 1 compilation errors. No code generated.
错误日志的意思也非常明白,我们需要的左值是float类型,而右值是const int类型,不符合,我们只要把它修改为除以10.0就可以了。对Z分量的处理逻辑一定不能少,如果去掉对Z轴的处理,实现就会变成如下模样:
最后我们来看一下片段着色器,源码如下:
precision mediump float;
uniform samplerCube u_TextureUnit;
varying vec3 v_Position;
void main()
{
gl_FragColor = textureCube(u_TextureUnit, v_Position);
}
片段着色器的代码比较少,第一行还是定义精度,第二行定义天空盒纹理,第三行是顶点着色器传递过来的顶点位置,main函数中直接调用textureCube内建函数来构建纹理并赋值给内建变量gl_FragColor就完成了。
从本节的学习中,我们可以了解到,天空盒是完全独立的另外一个着色器程序,我们可以在Render中把和天空盒相关的代码全部注释掉,或者把所有粒子发射器的代码全部注释掉,整个程序仍然可以正常工作,说明它们两个完全独立,都是一个完整的Opengl ES着色器程序。学习完本节,我们和上一节相比,需要掌握的就是实现Cube贴图纹理的流程,我们必须要学到这点才算掌握了本节的重点。
到这里,本节的内容就结束了,大家如果掌握了的话,就可以自己动手从零开始写一个Cube程序了!!继续加油!!