本文主要是介绍Opengl ES系列学习--用粒子增添趣味,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
我们本节开始分析《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,代码如下:
@Overridepublic void onSurfaceCreated(GL10 glUnused, EGLConfig config) {glClearColor(0.0f, 0.0f, 0.0f, 0.0f);// Enable additive blendingglEnable(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 resourcefinal 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 OpenGLglBindTexture(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方法的实现,源码如下:
@Overridepublic 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方法,源码如下:
@Overridepublic 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 constantsprotected 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 constantsprotected 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 programprotected 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 locationsprivate final int uMatrixLocation;private final int uTimeLocation; // Attribute locationsprivate 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控制它,否则我们根本无法下手。
好了,本节的内容到这里就结束了,希望能给大家带来一些帮助!!
这篇关于Opengl ES系列学习--用粒子增添趣味的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!