Opengl ES系列学习--用粒子增添趣味

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_22657459/article/details/89407166

     我们本节开始分析《OpenGL ES应用开发实践指南 Android卷》书中第10章中的粒子系统的实现原理,搞清楚其中的代码逻辑,代码下载请点击:Opengl ES Source Code,该Git库中的particles Module就是我们本节要分析的目标,先看下本节最终实现的结果。

粒子系统

     最终运行在真机上的效果非常炫,三个红绿蓝粒子系统不断的发射新的粒子,所有粒子由于重力上升到一定高度后开始下降,而且颜色发亮,非常漂亮,下面让我们看一下它到底是怎么实现的。

     首先,我们来看一下ParticlesActivity类中的代码,非常简单,判断当前设备是否支持Opengl ES2.0,如果支持,则调用glSurfaceView.setEGLContextClientVersion(2)将版本设置为2.0,然后构造一个Render渲染类,调用setRenderer设置为glSurfaceView的渲染器。

     接下来看一下ParticlesRenderer类,首先必须实现android.opengl.GLSurfaceView.Renderer,重写父类定义的onSurfaceCreated、onSurfaceChanged、onDrawFrame三个方法,然后在每个方法中添加实现逻辑,三个方法的回调意图也非常清晰,onSurfaceCreated就是当GLSurfaceView创建完成后的回调,到这里显示系统分配给当前View的Surface才有效,才可以执行绘图工作;onSurfaceChanged表示Surface发生变化时的回调,最明显的就是当前Activity退出再进入,Surface可见性变化,就会回调该方法;onDrawFrame表示需要绘制一帧,这里就和Vsync垂直同步信号相关了,显示器一般是60FPS帧率,大家可以看下我之前Vsync相关的博客。

    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];
    /*
    // Maximum saturation and value.
    private final float[] hsv = {0f, 1f, 1f};*/
    
    private ParticleShaderProgram particleProgram;      
    private ParticleSystem particleSystem;
    private ParticleShooter redParticleShooter;
    private ParticleShooter greenParticleShooter;
    private ParticleShooter blueParticleShooter;
    /*private ParticleFireworksExplosion particleFireworksExplosion;
    private Random random;*/
    private long globalStartTime;
    private int texture;

     以上是ParticlesRenderer类中定义的所有成员变量,首先是三个float数组,长度全部为16,这三个数组是进行Matrix矩阵运算需要的,首先调用MatrixHelper.perspectiveM(projectionMatrix, 45, (float) width / (float) height, 1f, 10f)获取到一个透视投影矩阵,45表示视角为45度,(float) width / (float) height表示缩放率,这里请一定注意,不能直接用width和height相除,因为我们如果是竖屏的话,宽度比高度小,结果得到的是0,不会有任何绘制,1和10表示z轴的可视范围从-1到-10,大家看下MatrixHelper中该方法的实现就会明白了;其次调用setIdentityM(viewMatrix, 0)得到一个4*4单位矩阵,如果大家不明白单位矩阵的话,请百度搜索搞明白。为什么是4*4呢?因为矩阵中一般运算都是四个分量,比如顶点位置属性(x、y、z、w),w分量其实非常有用,书中有很详细的解释;接着调用translateM(viewMatrix, 0, 0f, -1.5f, -5f)将viewMatrix单位矩阵进行平移,-1.5f表示Y方向1.5个单位,负值也就是向下,-5f表示Z方向5个单位,负值表示离视点(屏幕)越远,这里需要注意,因为我们创建透视投影矩阵时,指定的Z轴最近距离是一个单位,最远距离是10个单位,只有在这个范围内的顶点才会被绘制,如果Z轴的值超出这个范围,也不会有任何东西被绘制,大家可以试一下;最后调用multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0)将两个矩阵相乘,第一个参数viewProjectionMatrix是存储输出结果的,看到这里,大家就明白这三个float数组的作用了吧,其实就是得到两个矩阵,然后执行乘法运算,把结果存储在第三个矩阵中。

     接下来是ParticleShaderProgram particleProgram是自定义的一个着色器程序,ParticleSystem particleSystem是自定义的粒子系统,redParticleShooter、greenParticleShooter、blueParticleShooter是三个粒子发射器,等下分析完当前类,我们逐个分析这三个自定义的类。globalStartTime记录一个时间戳,texture是纹理ID。

     构造方法很简单,我们就不说了,继续看onSurfaceCreated,代码如下:

    @Override
    public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        
        // Enable additive blending
        glEnable(GL_BLEND);
        glBlendFunc(GL_ONE, GL_ONE);
        
        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));
        
        greenParticleShooter = new ParticleShooter(
            new Point(0f, 0f, 0f), 
            particleDirection,
            Color.rgb(25, 255, 25));
        
        blueParticleShooter = new ParticleShooter(
            new Point(1f, 0f, 0f), 
            particleDirection,
            Color.rgb(5, 50, 255));     
        */
        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); 
        /*
        particleFireworksExplosion = new ParticleFireworksExplosion();
        
        random = new Random();  */
        
        texture = TextureHelper.loadTexture(context, R.drawable.particle_texture);
    }

     glClearColor的作用就是清屏,传入的参数就是RGBA分量,四个0表示什么也没有,就是黑色,如果要用白色清屏,那RGB肯定都要设置成1了;glEnable(GL_BLEND)表示启用Blend混合,Blend混合是将源色和目标色以某种方式混合生成特效的技术。混合常用来绘制透明或半透明的物体。在混合中起关键作用的α值实际上是将源色和目标色按给定比率进行混合,以达到不同程度的透明。α值为0则完全透明,α值为1则完全不透明。混合操作只能在RGBA模式下进行,颜色索引模式下无法指定α值。物体的绘制顺序会影响到OpenGL的混合处理,说的通俗点,比如我们下落的粒子和刚生成的粒子重叠,两个粒子的颜色就会混合在一起,大家可以关闭混合就会看到明显的效果;glBlendFunc(GL_ONE, GL_ONE)就是叠加混合,Opengl还提供了其他很多的混合算法,大家可以自己去研究。接着创建着色器程序和粒子系统,粒子系统的参数10000表示最大粒子容量为10000,如果超出的话,就会从头存储,后面看到这里的代码就会明白,然后给globalStartTime赋值,也就是记录当前时间,接下来创建Vector向量,X和Z轴的值都为0,只有Y轴为正,该值会影响到粒子往上飞。然后定义角度为5度,速度为1,接着构造三个粒子发射器,第一个参数表示粒子发射器的位置,当前坐标系统,(0,0,0)为屏幕正中心,X轴正向向右,Y轴正向向上,Z轴正向向外(朝着我们的眼睛),所以三个粒子发射器的定位就是左中(-1,0,0)、中心(0,0,0)、右中(1,0,0),大家看下实际的效果粒子的Y轴位置不在中间,这是因为使用透视投影矩阵和平移的结果导致的,第二个参数方向向量都一样,然后是RGB颜色,接着是角度和速度;该方法最后一句texture = TextureHelper.loadTexture(context, R.drawable.particle_texture)表示加载纹理,我们跟进去看一下该方法的实现,源码如下:

public static int loadTexture(Context context, int resourceId) {
        final int[] textureObjectIds = new int[1];
        glGenTextures(1, textureObjectIds, 0);

        if (textureObjectIds[0] == 0) {
            if (LoggerConfig.ON) {
                Log.w(TAG, "Could not generate a new OpenGL texture object.");
            }

            return 0;
        }
        
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = false;

        // Read in the resource
        final Bitmap bitmap = BitmapFactory.decodeResource(
            context.getResources(), resourceId, options);

        if (bitmap == null) {
            if (LoggerConfig.ON) {
                Log.w(TAG, "Resource ID " + resourceId
                    + " could not be decoded.");
            }

            glDeleteTextures(1, textureObjectIds, 0);

            return 0;
        } 
        
        // Bind to the texture in OpenGL
        glBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);

        // Set filtering: a default must be set, or the texture will be
        // black.
        glTexParameteri(GL_TEXTURE_2D,
            GL_TEXTURE_MIN_FILTER,
            GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D,
            GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        // Load the bitmap into the bound texture.
        texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);

        // Note: Following code may cause an error to be reported in the
        // ADB log as follows: E/IMGSRV(20095): :0: HardwareMipGen:
        // Failed to generate texture mipmap levels (error=3)
        // No OpenGL error will be encountered (glGetError() will return
        // 0). If this happens, just squash the source image to be
        // square. It will look the same because of texture coordinates,
        // and mipmap generation will work.

        glGenerateMipmap(GL_TEXTURE_2D);

        // Recycle the bitmap, since its data has been loaded into
        // OpenGL.
        bitmap.recycle();

        // Unbind from the texture.
        glBindTexture(GL_TEXTURE_2D, 0);

        return textureObjectIds[0];        
    }

     该方法纯粹是一个功能方法,就是使用Opengl ES纹理功能的步骤,先调用系统API把纹理图片加载成Bitmap,然后绑定到我们申请好的纹理ID上,同时指定纹理渲染方式,大家基本知道这样的流程就行了。

     回到ParticlesRenderer,我们继续看onSurfaceChanged方法的实现,源码如下:

    @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);
        
        setIdentityM(viewMatrix, 0);
        translateM(viewMatrix, 0, 0f, -1.5f, -5f);
        multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0,
            viewMatrix, 0);
    }

     onSurfaceChanged方法的逻辑比较少,第一句设置视口,第二句得到透视投影矩阵,第三句得到单位矩阵,第四句将单位矩阵平移,Y方向平移1.5个单位,Z方向平移5个单位,最后将透视投影矩阵和平移的结果矩阵相乘,最终的这个结果是我们在顶点着色中需要使用的。

     接下来看onDrawFrame方法,源码如下:

    @Override
    public void onDrawFrame(GL10 glUnused) {        
        glClear(GL_COLOR_BUFFER_BIT);
        
        float currentTime = (System.nanoTime() - globalStartTime) / 1000000000f;
        
        redParticleShooter.addParticles(particleSystem, currentTime, 5);
        greenParticleShooter.addParticles(particleSystem, currentTime, 5);              
        blueParticleShooter.addParticles(particleSystem, currentTime, 5);
        /*
        if (random.nextFloat() < 0.02f) {
            hsv[0] = random.nextInt(360);
            
            particleFireworksExplosion.addExplosion(
                particleSystem, 
                new Vector(
                    -1f + random.nextFloat() * 2f, 
                     3f + random.nextFloat() / 2f,
                    -1f + random.nextFloat() * 2f), 
                Color.HSVToColor(hsv), 
                globalStartTime);                              
        }    */            
        
        particleProgram.useProgram();
        /*
        particleProgram.setUniforms(viewProjectionMatrix, currentTime);
         */
        particleProgram.setUniforms(viewProjectionMatrix, currentTime, texture);
        particleSystem.bindData(particleProgram);
        particleSystem.draw(); 
    }

     首先将当前时间和起始时间相减,再除以一万亿,得到的就是秒,所以currentTime的意思就是执行到当前帧绘制时,离开始过去了currentTime多秒,然后以相同的参数往三个粒子发射器中添加粒子,每次添加5个,我一开始看到这里,感觉很怪,想不通这些粒子是如何存在的,一直就纠着这块的代码前后理解,最终才明白了,随着时间的推移,绘制一帧,加5个粒子,再绘制一帧,再加5个粒子,所有加的这些粒子,全部是顶点数组的方式存储在VertexArray当中,只是每个粒子的属性有一些不一样,最终就表现出来不同的现象了,这里大家一定要理解,只有理解清楚,我们才能真正明白粒子系统到底是如何工作的。接下来particleProgram.useProgram()就是调用Opengl的API使用program;particleProgram.setUniforms(viewProjectionMatrix, currentTime, texture)设置属性,第一个就是我们对矩阵运算完成的结果矩阵,第二个是当前帧离起始时间点的耗时,第三个是加载进来的纹理ID;particleSystem.bindData(particleProgram)绑定数据,该方法里边的逻辑非常重要,顶点属性数据都是在这里设置的,我们分析到ParticleSystem粒子系统的代码时再解释它;最后particleSystem.draw()执行绘制。

     我们按照ParticlesRenderer类中成员变量关联的类的顺序来分析,所以接着我们来看 ParticleShaderProgram着色器程序,它是继承父类ShaderProgram,先看父类,源码如下:

abstract class ShaderProgram {
    // Uniform constants
    protected static final String U_MATRIX = "u_Matrix";
    protected static final String U_COLOR = "u_Color";
    protected static final String U_TEXTURE_UNIT = "u_TextureUnit";
    protected static final String U_TIME = "u_Time";    

    // Attribute constants
    protected static final String A_POSITION = "a_Position";
    protected static final String A_COLOR = "a_Color";
    protected static final String A_TEXTURE_COORDINATES = "a_TextureCoordinates";
    
    
    protected static final String A_DIRECTION_VECTOR = "a_DirectionVector";
    protected static final String A_PARTICLE_START_TIME = "a_ParticleStartTime";

    // Shader program
    protected final int program;

    protected ShaderProgram(Context context, int vertexShaderResourceId,
        int fragmentShaderResourceId) {
        // Compile the shaders and link the program.
        program = ShaderHelper.buildProgram(
            TextResourceReader
                .readTextFileFromResource(context, vertexShaderResourceId),
            TextResourceReader
                .readTextFileFromResource(context, fragmentShaderResourceId));
    }        

    public void useProgram() {
        // Set the current OpenGL shader program to this program.
        glUseProgram(program);
    }
}

     首先定义了一些字符串类型的常量,这些常量全部是在glsl着色器中定义的,我们可以通过名称获取并访问它们,这就是Application和Opengl交互的秘诀了,glsl的着色器全部是执行在GPU上的代码段,我们无法像Java或者C++代码那样直接控制它,但是我们可以间接控制它们,先获取到它们的引用,然后传值给它们,这样我们就能实现很多功能。定义的这些常量必须和glsl中的顶点属性一模一样,否则会获取失败,返回结果-1。成员变量program就是加载成功的Opengl程序索引,一般都是顺序增大的整数值;构造方法中非常清晰,就是先调用TextResourceReader.readTextFileFromResource获取glsl定义的着色器程序代码,然后执行ShaderHelper.buildProgram构造program,这里大家一定要明白,顶点着色器和片段着色器必须一一对应,加载完成的一个program必须同时对应一个顶点着色器和一个片段着色器,否则Opengl程序无法正常绘制;当然,一个程序中可以构造多个program,比如我们可以写五个顶点着色器和五个片段着色器,它们一一对应,那么load成功时,就会生成五个program对象;useProgram的方法非常简单,就是调用Opengl的API使用program。

     好,我们回到ParticleShaderProgram,继续看它的实现,源码如下:

public class ParticleShaderProgram extends ShaderProgram {
    // Uniform locations
    private final int uMatrixLocation;
    private final int uTimeLocation;    
    
    // Attribute locations
    private final int aPositionLocation;
    private final int aColorLocation;
    private final int aDirectionVectorLocation;
    private final int aParticleStartTimeLocation;
    private final int uTextureUnitLocation;
    public ParticleShaderProgram(Context context) {
        super(context, R.raw.particle_vertex_shader,
            R.raw.particle_fragment_shader);

        // Retrieve uniform locations for the shader program.
        uMatrixLocation = glGetUniformLocation(program, U_MATRIX);
        uTimeLocation = glGetUniformLocation(program, U_TIME);
        uTextureUnitLocation = glGetUniformLocation(program, U_TEXTURE_UNIT);
        
        // Retrieve attribute locations for the shader program.
        aPositionLocation = glGetAttribLocation(program, A_POSITION);
        aColorLocation = glGetAttribLocation(program, A_COLOR);
        aDirectionVectorLocation = glGetAttribLocation(program, A_DIRECTION_VECTOR);
        aParticleStartTimeLocation = 
            glGetAttribLocation(program, A_PARTICLE_START_TIME);
    }
	
    /*
    public void setUniforms(float[] matrix, float elapsedTime) {
     */ 
    public void setUniforms(float[] matrix, float elapsedTime, int textureId) {
        glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
        glUniform1f(uTimeLocation, elapsedTime);        
        glActiveTexture(GL_TEXTURE0);        
        glBindTexture(GL_TEXTURE_2D, textureId);
        glUniform1i(uTextureUnitLocation, 0);
    }

    public int getPositionAttributeLocation() {
        return aPositionLocation;
    }
    public int getColorAttributeLocation() {
        return aColorLocation;
    }
    public int getDirectionVectorAttributeLocation() {
        return aDirectionVectorLocation;
    }       
    public int getParticleStartTimeAttributeLocation() {
        return aParticleStartTimeLocation;
    }
}

     它当中定义的所有int型成员变量就是父类中获取顶点和片段着色器中定义的属性的索引,全部都是int型整数,可以获取到的属性只有两种,一种是uniform型,一种是attribute型,varying型在上一篇中已经讲过,是用来在顶点着色器和片段着色器之间传递数据使用的,Application无法获取它的值,要获取uniform和attribute类型属性的索引,可以分别通过调用glGetUniformLocation、glGetAttribLocation完成,第一个参数就是加载成功的program,我们刚才说过,一个APP可以有多个program,那么多个program中也是可以使用完全相同的字符串命名属性的,比如AProgram和BProgram中都定义了a_Position的属性,获取索引的时候,只要传入不同的program值,那我们就可以获取到两个program中不同的a_Position的索引。

     接着是setUniforms方法,第一句我们就看可以看到,将外部传入的matrix矩阵通过glUniformMatrix4fv  API设置给uMatrixLocation变量,这也就是我们能控制glsl中变量的原因了,第二句设置uTimeLocation变量的值,接下来激活纹理GL_TEXTURE0,设置类型为2D纹理,并和uTextureUnitLocation关联;剩下几个get方法就是获取定义好的成员变量的值了。

     接着继续来看Render中引用的ParticleSystem类的实现,源码如下:

public class ParticleSystem {
    private static final int POSITION_COMPONENT_COUNT = 3;
    private static final int COLOR_COMPONENT_COUNT = 3;
    private static final int VECTOR_COMPONENT_COUNT = 3;    
    private static final int PARTICLE_START_TIME_COMPONENT_COUNT = 1;

    private static final int TOTAL_COMPONENT_COUNT = 
        POSITION_COMPONENT_COUNT
      + COLOR_COMPONENT_COUNT 
      + VECTOR_COMPONENT_COUNT      
      + PARTICLE_START_TIME_COMPONENT_COUNT;

    private static final int STRIDE = TOTAL_COMPONENT_COUNT * BYTES_PER_FLOAT;

    private final float[] particles;
    private final VertexArray vertexArray;
    private final int maxParticleCount;

    private int currentParticleCount;
    private int nextParticle;

    public ParticleSystem(int maxParticleCount) {
        particles = new float[maxParticleCount * TOTAL_COMPONENT_COUNT];
        vertexArray = new VertexArray(particles);
        this.maxParticleCount = maxParticleCount;
    }
    
    public void addParticle(Point position, int color, Vector direction,
        float particleStartTime) {                
        final int particleOffset = nextParticle * TOTAL_COMPONENT_COUNT;
		
        int currentOffset = particleOffset;        
        nextParticle++;
        
        if (currentParticleCount < maxParticleCount) {
            currentParticleCount++;
        }
        
        if (nextParticle == maxParticleCount) {
            // Start over at the beginning, but keep currentParticleCount so
            // that all the other particles still get drawn.
            nextParticle = 0;
        }  
        
        particles[currentOffset++] = position.x;
        particles[currentOffset++] = position.y;
        particles[currentOffset++] = position.z;
        
        particles[currentOffset++] = Color.red(color) / 255f;
        particles[currentOffset++] = Color.green(color) / 255f;
        particles[currentOffset++] = Color.blue(color) / 255f;
        
        particles[currentOffset++] = direction.x;
        particles[currentOffset++] = direction.y;
        particles[currentOffset++] = direction.z;             
        
        particles[currentOffset++] = particleStartTime;
              
        vertexArray.updateBuffer(particles, particleOffset, TOTAL_COMPONENT_COUNT);
    }

    public void bindData(ParticleShaderProgram particleProgram) {
        int dataOffset = 0;
        vertexArray.setVertexAttribPointer(dataOffset,
            particleProgram.getPositionAttributeLocation(),
            POSITION_COMPONENT_COUNT, STRIDE);
        dataOffset += POSITION_COMPONENT_COUNT;
        
        vertexArray.setVertexAttribPointer(dataOffset,
            particleProgram.getColorAttributeLocation(),
            COLOR_COMPONENT_COUNT, STRIDE);        
        dataOffset += COLOR_COMPONENT_COUNT;
        
        vertexArray.setVertexAttribPointer(dataOffset,
            particleProgram.getDirectionVectorAttributeLocation(),
            VECTOR_COMPONENT_COUNT, STRIDE);
        dataOffset += VECTOR_COMPONENT_COUNT;       
        
        vertexArray.setVertexAttribPointer(dataOffset,
            particleProgram.getParticleStartTimeAttributeLocation(),
            PARTICLE_START_TIME_COMPONENT_COUNT, STRIDE);
    }

    public void draw() {
        glDrawArrays(GL_POINTS, 0, currentParticleCount);
    }
}

     开始的几个常量必须要说明清楚,POSITION_COMPONENT_COUNT表示描述一个顶点的位置属性需要3个size,分别对应X、Y、Z,如果我们加上W分量的话,那就是4,如果我们只显示二维平面,不需要Z和W的话,那么它应该被赋值为2;COLOR_COMPONENT_COUNT表示描述一个顶点颜色属性需要3个size,分别对应R、G、B,如果我们需要描述A(Alpha透明度)的话,那么它应该被赋值为4;VECTOR_COMPONENT_COUNT,表示描述一个顶点发射向量需要3个size,每个分量的含义和POSITION相同;PARTICLE_START_TIME_COMPONENT_COUNT表示描述当前帧绘制时,离开始时间的耗时有多久,它就是一个float值,所以需要1个size。TOTAL_COMPONENT_COUNT就非常清晰了,一个顶点包含了四个属性,分别是位置(POSITION)、颜色(COLOR)、方向(VECTOR)、耗时(START_TIME),总的size就是把这四个相加,也就是要正确描述一个顶点属性信息,需要3 + 3 + 3 + 1 = 10个size。STRIDE的意思就是跨距,我们在addParticle方法中就可以非常清楚的明白它的意思,particles就是存储所有顶点属性的数组,vertexArray是自定义的类,主要是把float的顶点数组存储在Buffer中,方便调用API时传递参数,它当中的逻辑比较简单,我们后面再分析;maxParticleCount表示能容纳的最多粒子数,也就是说我们每帧添加5个粒子,那么2000帧之后,数组已经满了,此时2001帧的顶点属性就会从0开始,最开始的粒子属性会被覆盖掉;currentParticleCount表示当前添加到多少个了;nextParticle就表示下一个粒子。

     构造方法中的逻辑很清晰,是对成员变量的初始化;我们继续看addParticle方法,它是在粒子发射器ParticleShooter中调用的,四个参数position、color是它的成员变量,是在构造函数时已经赋值了,position就是粒子发射器的位置,也就是我们前面讲过的左中、中心、右中;color是三个粒子发射器的颜色,也是在Render类的onSurfaceCreated方法中传入的,分别是Color.rgb(255, 50, 5)、Color.rgb(25, 255, 25)、Color.rgb(5, 50, 255),这也就是为什么我们看到的是红、绿、蓝三个粒子发射器的原因了;thisDirection是在addParticle方法中运算得出的,currentTime是方法参数,也就是在Render中我们前面讲过的那个秒数。好,分析完参数,我们继续看该方法的逻辑,particleOffset表示要添加的目标粒子的偏移量,比如我们要添加第3个粒子,那么它的顶点属性的第一个float在数组中的位置就是2 * TOTAL_COMPONENT_COUNT = 20,也就是数组中第21个元素,如果当前数组还未填满,则currentParticleCount++继续往后填充,如果满了,则从头开始,就把nextParticle赋值为0,接下来就把我们方法参数传入的值保存在顶点数组中,可以看到往顶点数组particles填充时,每次填充完,currentOffset自增,表示往后移一位,三个位置、三个颜色、三个方向、一个耗时,一共10位,这就是顶点数组真实的用途了,所有描述顶点的数组全部保存在一个数组中,然后把数组传递给着色器,让它根据我们设置好的跨距来取就可以了,这里一定要理解清楚!!!数组填充完毕后,将所有数据更新到vertexArray当中。

     再来看bindData方法,方法参数ParticleShaderProgram我们已经分析过了,它当中的逻辑非常规整,计算dataOffset,然后调用vertexArray.setVertexAttribPointer设置值,分别设置POSITION、COLOR、VECTOR、START_TIME四个属性的值,一定注意dataOffset,它一定要增加,比如POSITION有三位,那么设置完POSITION之后,下面要设置COLOR,必须从第3位开始(也就是数组中的第四个元素),明白了dataOffset的意义,大家就会意识到,如果该参数传错,就会导致我们取颜色分量时,错误的取到了位置分量上,那结果将是不可预知的!!我们先看完最后的draw方法,然后回过来再分析vertexArray.setVertexAttribPointer。

     draw方法很简单,就是调用Opengl的API进行绘制,底层会通过调用eglSwapBuffers交互前台缓冲区和后台缓冲区,来让我们绘制的像素显示在屏幕上,这里也需要注意,在Render的onDrawFrame中,是每帧都会调用的,假如我们为了节省性能,想着我是不是只需要把像素绘制出来一次,交给FrameBuffer渲染就可以了,后边就不需要了,实现就是在onDrawFrame中判断一下,加一个boolean类型的变量,只在第一次渲染,以后所有帧全部不渲染。这样的想法是不对的,原理是这样,FrameBuffer渲染时有前后缓冲区和后台缓冲区,如果我们只渲染第一帧,那么所有像素被绘制到FrontBuffer,那么第二帧需要渲染时,原来的BackBuffer没有填充任何像素,它被交换到前台,整个屏幕就会显示黑的,第三帧过来时,又进行交换,第一帧的FrontBuffer被交换到前台,我们绘制的像素又显示出来,第四帧又黑屏,这样就会造成闪屏的现象,大家可以自己修改代码试一下。glDrawArrays方法的第一个参数是指我们要如何去绘制所有顶点,GL_POINTS的意思就是把它们全部当成点来绘制,我们还可以使用GL_TRIANGLES绘制三角形。

     好,我们回过头来看一下vertexArray.setVertexAttribPointer的实现逻辑,源码如下:

public class VertexArray {
    private final FloatBuffer floatBuffer;

    public VertexArray(float[] vertexData) {
        floatBuffer = ByteBuffer
            .allocateDirect(vertexData.length * BYTES_PER_FLOAT)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
            .put(vertexData);
    }   
        
    public void setVertexAttribPointer(int dataOffset, int attributeLocation,
        int componentCount, int stride) {        
        floatBuffer.position(dataOffset);        
        glVertexAttribPointer(attributeLocation, componentCount,
            GL_FLOAT, false, stride, floatBuffer);
        glEnableVertexAttribArray(attributeLocation);
        
        floatBuffer.position(0);
    }
    
    /**
     * Updates the float buffer with the specified vertex data, assuming that
     * the vertex data and the float buffer are the same size.
     */
    public void updateBuffer(float[] vertexData, int start, int count) {
       floatBuffer.position(start);
       floatBuffer.put(vertexData, start, count);
       floatBuffer.position(0);
    }
}

     先来看构造方法,给成员变量floatBuffer赋值,也就是分配内存,因为它存储的是float,一个float需要4个字节,所以分配的长度需要乘以BYTES_PER_FLOAT(4);setVertexAttribPointer方法中先把floatBuffer移动到dataOffset,也就是我们上面举的例子,假如我们设置第三个顶点的位置属性,那么就是要从floatBuffer中的第20个位置(起始位置为0)开始取值,如果我要设置它的颜色属性,那么就移动POSITION长度三个位置,从23个位置开始取值,移动好了之后,调用glVertexAttribPointer接口API设置值,这个API非常重要,大家使用的时间一定要小心,参数必须正确,否则就会出现我们前面所说的把位置属性当成颜色属性传错的情况,如果绘制出现有异常的时候,大家也应该重点检查该API的参数。glVertexAttribPointer方法的第一个参数表示我们在glsl着色器中定义的变量索引,第二个参数componentCount表示描述该属性需要几个size,第三个参数GL_FLOAT表示描述该属性使用的是什么类型的数据,stride表示跨距,前面已经详细解释了,floatBuffer就是数据存在哪里;glEnableVertexAttribArray表示启用该顶点属性,如果要禁用的话,可以调用glDisableVertexAttribArray;最后floatBuffer.position(0)把buffer移动到起始位置,我们必须从起始位置开始取,否则肯定是会出错的。

     好,看到这里,大家应该有一个非常清晰的认识了,我们先通过给float顶点数组赋值,把我们目标数组的值存储好,然后调用glVertexAttribPointer告诉glsl着色器程序,该值从哪里取,在什么位置,这样着色器程序就能正确绘制我们的意图了。

     updateBuffer方法的逻辑很简短,就是添加新的数据,添加从哪里开始,添加的长度是多少,我们只需要按照要求把数据放在buffer中就可以了,填充完毕一定记得把buffer移动到起始位置。

     到这里,ParticleSystem粒子系统的代码我们也分析完了,大家回顾一下,是不是对粒子系统内部的动作有一些小小的理解了??非常惊喜,不过最最重点的glsl着色器程序还没开始,让我们继续。

     粒子系统分析完了,我们接着看ParticleShooter,最后就是glsl着色器程序,glsl分析完,基本我们就讲完粒子系统动作的所有逻辑了,大家如果能全部理解,就可以尝试自己从0开始写一个了。

     ParticleShooter粒子发射器的所有源码如下:

public class ParticleShooter {
    private final Point position;
    private final Vector direction;
    private final int color; 
	
    private final float angleVariance;
    private final float speedVariance;    
    
    private final Random random = new Random();
    
    private float[] rotationMatrix = new float[16];
    private float[] directionVector = new float[4];
    private float[] resultVector = new float[4];
    /*
          
    public ParticleShooter(Point position, Vector direction, int color) {
     */
    public ParticleShooter(
        Point position, Vector direction, int color, 
        float angleVarianceInDegrees, float speedVariance) {
        this.position = position;
        this.direction = direction;
        this.color = color;        
        this.angleVariance = angleVarianceInDegrees;
        this.speedVariance = speedVariance;        
        
        directionVector[0] = direction.x;
        directionVector[1] = direction.y;
        directionVector[2] = direction.z;        
    }
    
    public void addParticles(ParticleSystem particleSystem, float currentTime, 
        int count) {        
        for (int i = 0; i < count; i++) {
            setRotateEulerM(rotationMatrix, 0, 
                (random.nextFloat() - 0.5f) * angleVariance, 
                (random.nextFloat() - 0.5f) * angleVariance, 
                (random.nextFloat() - 0.5f) * angleVariance);
            
            multiplyMV(
                resultVector, 0, 
                rotationMatrix, 0, 
                directionVector, 0);
            
            float speedAdjustment = 1f + random.nextFloat() * speedVariance;
            
            Vector thisDirection = new Vector(
                resultVector[0] * speedAdjustment,
                resultVector[1] * speedAdjustment,
                resultVector[2] * speedAdjustment);        
            
            /*
            particleSystem.addParticle(position, color, direction, currentTime);
             */
            particleSystem.addParticle(position, color, thisDirection, currentTime);
        }       
    }
}

     首先还是来看成员变量,Point position表示粒子发射器的位置,也就是左中、中心、右中,Vector direction表示发射出的粒子飞的方向,前面我们已经讲过,三个粒子发射器的方向向量全部相同,这也就是我们看到的结果,三个粒子发射器中粒子飞的基本都是一样的方向的原因了;int color表示三个粒子发射器发射的粒子的颜色;angleVariance表示粒子飞出后的角度,如果我们把角度改为0,那么所有粒子都只在Y方向正方向上往上飞,不会往旁边偏移,float speedVariance表示粒子飞行的速度,这里有些奇怪,速度是什么意思呢?粒子只有位置,速度要怎么表达呢?等一下我们看glsl着色器程序的时候,大家就明白了,这个参数其他最终还是作用的位置上,使粒子的位移变的快一些,就可以达到速度的效果了,大家可以把它改大一些看一下,明显粒子会很快的飞出去;float[] rotationMatrix、float[] directionVector、float[] resultVector三个数组跟Render中我们讲的一样,还是两个换算,最后相乘,把结果存储在resultVector中。

     构造方法就是对成员变量赋值,特殊一些的就是directionVector,它把方向向量的X、Y、Z三个方向的值单独存储,后面需要对每个方向进行单独运算;addParticles的功能是添加新的粒子,currentTime表示离起始的耗时,count等于5,表示每次添加5个粒子,for循环中对5个粒子进行不同的设置,这也就是为什么产生的粒子会不一样的原因了,差异就是在这里产生的。先调用setRotateEulerM得到一个旋转矩阵,旋转矩阵的X、Y、Z三个方向都不同的值,注意random.nextFloat()得到一个大于0,小于1的浮点数,减去0.5的结果可能为正,可能为负,这也就是为什么有的粒子往左飞,有的粒子往右飞的原因了。得到旋转矩阵后,调用multiplyMV和directionVector相乘,使方向向量可以对粒子的路径作用,接着得到一个随机速度,也作用的结果矩阵上,乘以速度相当于变化的阶段值更大,视觉看到的就越快,也就是速度的意思了。最后调用particleSystem.addParticle(position, color, thisDirection, currentTime)把所有的值传到的顶点数组中,往下的逻辑我们前面已经分析过了。

     到这里,Application所有的代码我们都看过了,接下来看glsl着色器的代码,先来看顶点着色器particle_vertex_shader.glsl,源码如下:

uniform mat4 u_Matrix;
uniform float u_Time;

attribute vec3 a_Position;  
attribute vec3 a_Color;
attribute vec3 a_DirectionVector;
attribute float a_ParticleStartTime;

varying vec3 v_Color;
varying float v_ElapsedTime;

void main()                    
{                                	  	  
    v_Color = a_Color;
    v_ElapsedTime = u_Time - a_ParticleStartTime;    
    float gravityFactor = v_ElapsedTime * v_ElapsedTime / 8.0;
    vec3 currentPosition = a_Position + (a_DirectionVector * v_ElapsedTime);
    currentPosition.y -= gravityFactor;
    gl_Position = u_Matrix * vec4(currentPosition, 1.0);
    /*
    gl_PointSize = 10.0;
    */
    gl_PointSize = 25.0;
}   

     顶点着色器的代码比较短,前两句定义了两个uniform变量。uniform变量在vertex和fragment两者之间声明方式完全一样,则它可以在vertex和fragment共享使用。相当于一个被vertex和fragment shader共享的全局变量,uniform变量一般用来表示:变换矩阵,材质,光照参数和颜色等顶点属性信息。第一个u_Matrix是一个4*4的矩阵,它的值是通过Render中的onDrawFrame方法调用particleProgram.setUniforms(viewProjectionMatrix, currentTime, texture)传入的,对应的是第一个参数,也就是先透视,然后平移得到的那个矩阵;第二个u_Time的值就是particleProgram.setUniforms(viewProjectionMatrix, currentTime, texture)调用中的第二个参数,也就是耗时,这里要说一下着色器程序中变量命名规则,前一节我们已经说过,着色器中只有三种类型:uniform、attribute、varying,如果我们要定义的变量是哪一种,则前面就加一个首字母开头,这样非常容易理解,大家从上面着色器代码中所有变量的定义就可以看到非常清楚。接着是四个attribute变量,attribute变量是只能在vertex shader中使用的变量。(它不能在fragment shader中声明attribute变量,也不能被fragment shader中使用)。一般用attribute变量来表示一些顶点的数据,如:顶点坐标,法线,纹理坐标,顶点颜色等。在application中,一般用函数glBindAttribLocation()来绑定每个attribute变量的位置,然后用函数glVertexAttribPointer()为每个attribute变量赋值。a_Position表示粒子发射器的位置,它的值是从顶点数组中取出来的,也就是顶点数组最开始的3个float,表示X、Y、Z坐标,它的值是ParticleShooter粒子发射器的成员变量position,是固定的,也就是左中、中心、右中;a_Color表示粒子着色器的颜色,值也是固定的,对应ParticleShooter粒子发射器的成员变量color;a_DirectionVector表示粒子飞行的方向向量,这个值是运算得到的,粒子之间就有差异了;a_ParticleStartTime表示耗时,是通过顶点数组最后一位传入的。v_Color和v_ElapsedTime是顶点着色器要传递给片段着色器而定义的,前一节我们已经讲过了。

     分析完所有的值,我有个问题没搞明白,变量中u_Time和a_ParticleStartTime传入的值都是在Render中计算得到的局部变量currentTime,它们俩的值应该是相等的,后边相减得到的结果应该是0,但是从实现效果上并不得这样,这个问题一直没搞清楚,如果有哪位朋友弄清楚了,请帮忙解答一下,非常感谢!!

     我们继续看着色器剩下的代码,main函数表示着色器的主程序,这个大家记住就行。main函数第一行把a_Color赋值给v_Color,因为我们在片段着色器中也需要使用颜色,所以就需要传递过去;第二行计算耗时,也需要传递给片段着色器;第三行计算得到一个重力因子,后面的8.0没有特定的含义,是随意取的,大家也可以修改它的值;第四行vec3 currentPosition = a_Position + (a_DirectionVector * v_ElapsedTime)我们需要分开解读,先把方向向量a_DirectionVector和耗时v_ElapsedTime相乘,注意方向向量a_DirectionVector是一个vec3类型,也就是X、Y、Z三个方向都会有值,所以我们会看到粒子飞行时会有不同的方向,最根本的原因就是在这里,把它和耗时v_ElapsedTime相乘,然后和起始位置a_Position相加,注意,相乘的结果仍然是vec3类型,这行最终运算的意思就是说耗时越久,结果越大,粒子离起始位置的距离就越远,这就是为什么粒子会连续不断的绘制在屏幕上,而且位置不一样,奥秘就在这里了!第五行currentPosition.y -= gravityFactor把当前位置的Y分量和重力因子相减,注意,重力因子也是直接受耗时影响的,耗时越久,重力因子越大,所以当重力因子的影响越过飞行分量时,粒子就开始下降了,这就是粒子下降的根因!!非常巧妙!!!第六句gl_Position = u_Matrix * vec4(currentPosition, 1.0)就很明显了,先将粒子位置转换为4*4矩阵,因为gl_Position就是4*4的矩阵,如果不转换,着色器程序会编译出错,转换的1.0指W分量,接着将得到的粒子位置和我们运算好的矩阵相乘,这里是为了适应坐标系统,因为我们已经不是正交投影了。关于坐标系统的还没搞的太清楚,有朋友精通的话,请多多指教。最后一句gl_PointSize = 25.0表示指定点的大小,我们可以随意修改它的值就可以非常清晰的看到效果了。

     分析完顶点着色器的代码,大家一定要有这样一个认识,顶点着色中最关键的控制粒子变化的就是a_DirectionVector和v_ElapsedTime变量,而且a_DirectionVector变量是一个vec3类型的变量,就是说我在Application代码中修改它X、Y、Z三个分量的值,直接就会反馈到粒子的飞行轨迹上。大家一定要对顶点着色器中的代码有清晰的认识,如果作不到这一点,那说明我们根本还没有掌握着色器程序!

     好了,最后我们看一下片段着色器particle_fragment_shader.glsl,源码如下:

precision mediump float; 
uniform sampler2D u_TextureUnit;
varying vec3 v_Color;
varying float v_ElapsedTime;     	 							    	   								
void main()                    		
{
    /*            
    float xDistance = 0.5 - gl_PointCoord.x;
    float yDistance = 0.5 - gl_PointCoord.y;
    float distanceFromCenter = 
        sqrt(xDistance * xDistance + yDistance * yDistance);
    gl_FragColor = vec4(v_Color / v_ElapsedTime, 1.0);
    
    if (distanceFromCenter > 0.5) {
        discard;
    } else {            
        gl_FragColor = vec4(v_Color / v_ElapsedTime, 1.0);        
    }
    */     
    gl_FragColor = vec4(v_Color / v_ElapsedTime, 1.0)
                 * texture2D(u_TextureUnit, gl_PointCoord);
}

     片段着色的代码比较少,第一行设置float浮点数的精度,Opengl中有三种精度:lowp、mediump、highp,一般片段着色器中第一行都需要设置,而且大部分都设置为mediump,大家记住就可以了;接下来的定义uniform sampler2D类型的2D纹理变量u_TextureUnit,它的值是在ParticleShaderProgram类的setUniforms方法中赋值的,值是在Render中传进来的,也就是我们加载好的图片纹理;紧接着的ec3 v_Color、v_ElapsedTime都是从顶点着色器传递过来的;接下来是片段着色器的main函数,先用v_Color除以耗时v_ElapsedTime,然后转换为4*4矩阵,1.0表示Alpha值为1.0,相除的结果就是说时间越久,值越小,那么R、G、B三个分量上的值就越小,颜色也就越黑,然后调用texture2D纹理函数生成纹理,再和顶点颜色相乘,得到最终的结果。

     到这里,我们就把所有的逻辑分析完了,大家是否对着色器有了一定的理解,还是要提醒大家,着色器程序中的逻辑我们是无法修改的,我们可以定义变量,通过Application控制glsl着色器程序中变量的值来达到绘制的效果,着色器的原理就是这样的,所以当我们要实现一个功能时,必须首先搞清楚,我们要在着色器中怎么控制最终的效果,先想好着色器的逻辑,我们才能通过Application控制它,否则我们根本无法下手。

     好了,本节的内容到这里就结束了,希望能给大家带来一些帮助!!

猜你喜欢

转载自blog.csdn.net/sinat_22657459/article/details/89407166