Opengl ES系列学习--增加天空盒

     本节我们在上一节的基础上来分析一下天空盒的实现。我们要分析的目标就是《OpenGL ES应用开发实践指南 Android卷》书中第11章实现的最终的结果,代码下载请点击:Opengl ES Source Code,该Git库中的skybox Module就是我们本节要分析的目标,先看下本节最终实现的结果,CSDN对GIF动画文件的大小有限制,所以只能录制三个文件来看效果。

     需要说明,我们本节的大部分内容和上一节的内容完全相同,只是在上一节的基础上增加新的功能而已,所以如果对一节的内容有任何疑问,请大家先回过头去搞清楚上一节的原理:Opengl ES系列学习--用粒子增添趣味。

     我们本节就分析和上一节代码不同的地方,因为该Module涉及到文件也不多,差异的地方也不多,我们就直接逐个分析所有的文件,文件结构图如下。

Opengl ES系列学习--增加天空盒_第1张图片

     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个顶点,每个顶点的位置注释中也写的非常清楚了,它们的构成如下图。

Opengl ES系列学习--增加天空盒_第2张图片

     好,知道了每个顶点的位置,我们要绘制一个面,就需要两个三角形就可以了,这里直接使用索引数组来指定每个顶点的位置,比如我们要绘制前面的这个面,它的四个顶点分别是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轴的处理,实现就会变成如下模样:

Opengl ES系列学习--增加天空盒_第3张图片

     最后我们来看一下片段着色器,源码如下:

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程序了!!继续加油!!

你可能感兴趣的:(android,framework,Opengl,ES,数据结构和算法)