iOS高级进阶|OpenGL ES之GLSL自定义着色器编程实现粒子效果

效果展示 iOS高级进阶|OpenGL ES之GLSL自定义着色器编程实现粒子效果
文章图片

实现流程 一、自定义着色器

  • 顶点着色器:YDWPointParticleShader.vsh
// 位置 attribute vec3 a_emissionPosition; // 速度 attribute vec3 a_emissionVelocity; // 受力 attribute vec3 a_emissionForce; // 大小和Fade持续时间size = GLKVector2Make(aSize, aDuration); attribute vec2 a_size; // 发射时间和消失时间 attribute vec2 a_emissionAndDeathTimes; // UNIFORMS uniform highp mat4u_mvpMatrix; // 变换矩阵 uniform sampler2Du_samplers2D[1]; // 纹理 uniform highp vec3u_gravity; // 重力 uniform highp floatu_elapsedSeconds; // 当前时间// Varyings:粒子透明度 varying lowp floatv_particleOpacity; void main() {// 流逝时间 highp float elapsedTime = u_elapsedSeconds - a_emissionAndDeathTimes.x; // 质量假设是1.0 加速度 = 力 (a = f/m) // v = v0 + at : v 是当前速度; v0 是初速度; //a 是加速度; t 是时间 // a_emissionForce 受力,u_gravity 重力// 求速度velocity highp vec3 velocity = a_emissionVelocity + ((a_emissionForce + u_gravity) * elapsedTime); // s = s0 + 0.5 * (v0 + v) * t // s 当前位置 // s0 初始位置 // v0 初始速度 // v 当前速度 // t 是时间// 运算是对向量运算,相当于分别求出x、y、z的位置,再综合 // 求粒子的受力后的位置 = a_emissionPosition(原始位置) + 0.5 * (速度+加速度) * 流逝时间highp vec3 untransformedPosition = a_emissionPosition + 0.5 * (a_emissionVelocity + velocity) * elapsedTime; //得出点的位置 gl_Position = u_mvpMatrix * vec4(untransformedPosition, 1.0); gl_PointSize = a_size.x / gl_Position.w; // 消失时间减去当前时间,得到当前的寿命; 除以Fade持续时间,当剩余时间小于Fade时间后,得到一个从1到0变化的值 // 如果这个值小于0,则取0 float remainTime = a_emissionAndDeathTimes.y - u_elapsedSeconds; float keepTime = max(a_size.y, 0.00001); v_particleOpacity = max(0.0, min(1.0,remainTime /keepTime)); }

  • 自定义片元着色器
// UNIFORMS uniform highp mat4u_mvpMatrix; uniform sampler2Du_samplers2D[1]; uniform highp vec3u_gravity; uniform highp floatu_elapsedSeconds; // Varyings varying lowp floatv_particleOpacity; void main() { // 通过texture2D函数我们可以得到一个纹素(texel),这是一个纹理图片中的像素。函数参数分别为simpler2D以及纹理坐标: // gl_PointCoord是片元着色器的内建只读变量,它的值是当前片元所在点图元的二维坐标。点的范围是0.0到1.0 lowp vec4 textureColor = texture2D(u_samplers2D[0], gl_PointCoord); // 粒子透明度 与 v_particleOpacity 值相关 textureColor.a = textureColor.a * v_particleOpacity; // 设置片元颜色值 gl_FragColor = textureColor; }

二、封装着色器工具 该工具主要是将OpenGL ES 的部分操作封装成了自定义的方法,增加了代码的复用性:
① initWithAttribStride函数 初始化,用于在OpenGL ES 中创建一个VBO(顶点属性数组缓冲区)创建VBO的3个步骤:
  • 生成新缓存对象glGenBuffers
  • 绑定缓存对象glBindBuffer : 告诉VBO缓存对象时保存顶点数组数据还是索引数组数据 :GL_ARRAY_BUFFER/GL_ELEMENT_ARRAY_BUFFER,任何顶点属性,如顶点坐标、纹理坐标、法线与颜色分量数组都使用GL_ARRAY_BUFFER。用于glDraw[Range]Elements()的索引数据需要使用GL_ELEMENT_ARRAY绑定。
    注意,target标志帮助VBO确定缓存对象最有效的位置,如有些系统将索引保存AGP或系统内存中,将顶点保存在显卡内存中。
  • 将顶点数据拷贝到缓存对象中glBufferData
// 此方法在当前的OpenGL ES上下文中创建一个顶点属性数组缓冲区 - (id)initWithAttribStride:(GLsizeiptr)aStride numberOfVertices:(GLsizei)count bytes:(const GLvoid *)dataPtr usage:(GLenum)usage { self = [super init]; if(self != nil) { _stride = aStride; _bufferSizeBytes = _stride * count; // 初始化缓存区 // 创建VBO的3个步骤 // 1.生成新缓存对象glGenBuffers // 2.绑定缓存对象glBindBuffer // 3.将顶点数据拷贝到缓存对象中glBufferData// STEP 1 创建缓存对象并返回缓存对象的标识符 glGenBuffers(1,&_name); // STEP 2 将缓存对象对应到相应的缓存上 /* glBindBuffer (GLenum target, GLuint buffer); target:告诉VBO缓存对象时保存顶点数组数据还是索引数组数据 :GL_ARRAY_BUFFER\GL_ELEMENT_ARRAY_BUFFER 任何顶点属性,如顶点坐标、纹理坐标、法线与颜色分量数组都使用GL_ARRAY_BUFFER。用于glDraw[Range]Elements()的索引数据需要使用GL_ELEMENT_ARRAY绑定。注意,target标志帮助VBO确定缓存对象最有效的位置,如有些系统将索引保存AGP或系统内存中,将顶点保存在显卡内存中。 buffer: 缓存区对象 */glBindBuffer(GL_ARRAY_BUFFER,self.name); /* 数据拷贝到缓存对象 void glBufferData(GLenum target,GLsizeiptr size, const GLvoid*data, GLenum usage); target:可以为GL_ARRAY_BUFFER或GL_ELEMENT_ARRAY size:待传递数据字节数量 data:源数据数组指针 usage: GL_STATIC_DRAW GL_STATIC_READ GL_STATIC_COPY GL_DYNAMIC_DRAW GL_DYNAMIC_READ GL_DYNAMIC_COPY GL_STREAM_DRAW GL_STREAM_READ GL_STREAM_COPY”static“表示VBO中的数据将不会被改动(一次指定多次使用) ”dynamic“表示数据将会被频繁改动(反复指定与使用) ”stream“表示每帧数据都要改变(一次指定一次使用) ”draw“表示数据将被发送到GPU以待绘制(应用程序到GL) ”read“表示数据将被客户端程序读取(GL到应用程序) */ // STEP 3 数据拷贝到缓存对象 glBufferData( GL_ARRAY_BUFFER,// Initialize buffer contents _bufferSizeBytes,// Number of bytes to copy dataPtr,// Address of bytes to copy usage); // Hint: cache in GPU memory}return self; }

② reinitWithAttribStride函数: 更新数据,用于在已有VBO的情况下,更新传入着色器中的数据:
  • 将缓存对象对应到相应的缓存上
  • 数据拷贝到缓存对象
// 此方法加载由接收存储的数据 - (void)reinitWithAttribStride:(GLsizeiptr)aStride numberOfVertices:(GLsizei)count bytes:(const GLvoid *)dataPtr { _stride = aStride; _bufferSizeBytes = aStride * count; // STEP 1 将缓存对象对应到相应的缓存上 glBindBuffer(GL_ARRAY_BUFFER, self.name); // STEP 2 数据拷贝到缓存对象 glBufferData( GL_ARRAY_BUFFER, _bufferSizeBytes, dataPtr, GL_DYNAMIC_DRAW); }

③ prepareToDrawWithAttrib函数:
  • 准备绘制,用于打开attribute通道 & 设置数据的读取方式;
  • 当应用程序希望使用缓冲区呈现任何几何图形时,必须准备一个顶点属性数组缓冲区。当你的应用程序准备一个缓冲区时,一些OpenGL ES状态被改变,允许绑定缓冲区和配置指针。
  • 将缓存对象对应到相应的缓存上。
  • 出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU,由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的属性数据。
  • VBO只是建立CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU。但是,数据在GPU端是否可见,即着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。
  • 顶点数据传入GPU之后,还需要通知OpenGL如何解释这些顶点数据,这个工作由函数glVertexAttribPointer完成。
// 当应用程序希望使用缓冲区呈现任何几何图形时,必须准备一个顶点属性数组缓冲区。当你的应用程序准备一个缓冲区时,一些OpenGL ES状态被改变,允许绑定缓冲区和配置指针。 - (void)prepareToDrawWithAttrib:(GLuint)index numberOfCoordinates:(GLint)count attribOffset:(GLsizeiptr)offset shouldEnable:(BOOL)shouldEnable { if (count < 0 || count > 4) { NSLog(@"Error:Count Error"); return ; }if (_stride < offset) { NSLog(@"Error:_stride < Offset"); return; }if (_name == 0) { NSLog(@"Error:name == Null"); }// STEP 1 将缓存对象对应到相应的缓存上 glBindBuffer(GL_ARRAY_BUFFER,self.name); // 判断是否使用 if(shouldEnable) { // Step 2 // 出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU,由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的属性数据. // VBO只是建立CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU。但是,数据在GPU端是否可见,即着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。 // 顶点数据传入GPU之后,还需要通知OpenGL如何解释这些顶点数据,这个工作由函数glVertexAttribPointer完成 glEnableVertexAttribArray(index); }// Step 3 // 顶点数据传入GPU之后,还需要通知OpenGL如何解释这些顶点数据,这个工作由函数glVertexAttribPointer完成 /* glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr) indx:参数指定顶点属性位置 size:指定顶点属性大小 type:指定数据类型 normalized:数据被标准化 stride:步长 ptr:偏移量 NULL+offset */ glVertexAttribPointer( index, count, GL_FLOAT, GL_FALSE, (int)self.stride, NULL + offset); }

④ drawPreparedArraysWithMode函数:
  • 绘制,调用OpenGL ES 的数组绘制方式进行图形的绘制;
  • 将绘图命令模式和instructsopengl ES确定使用缓冲区从顶点索引的第一个数的顶点,顶点索引从0开始;
  • 当采用顶点数组方式绘制图形时,使用该函数glDrawArrays (GLenum mode, GLint first, GLsizei count)。该函数根据顶点数组中的坐标数据和指定的模式,进行绘制。
// 将绘图命令模式和instructsopengl ES确定使用缓冲区从顶点索引的第一个数的顶点,顶点索引从0开始 - (void)drawArrayWithMode:(GLenum)mode startVertexIndex:(GLint)first numberOfVertices:(GLsizei)count { if (self.bufferSizeBytes < (first + count) * self.stride) { NSLog(@"Vertex Error!"); }// 绘制 /* glDrawArrays (GLenum mode, GLint first, GLsizei count); 提供绘制功能。当采用顶点数组方式绘制图形时,使用该函数。该函数根据顶点数组中的坐标数据和指定的模式,进行绘制。 参数列表: mode,绘制方式,OpenGL2.0以后提供以下参数:GL_POINTS、GL_LINES、GL_LINE_LOOP、GL_LINE_STRIP、GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN。 first,从数组缓存中的哪一位开始绘制,一般为0。 count,数组中顶点的数量。 */ glDrawArrays(mode, first, count); }

三、粒子类:管理并绘制粒子 【iOS高级进阶|OpenGL ES之GLSL自定义着色器编程实现粒子效果】主要是管理并绘制所有的粒子,相当GLKit中的GLKBaseEffect类
① 定义变量
  • 定义需要用到的枚举和结构体
// 用于定义粒子属性的类型 typedef struct { GLKVector3 emissionPosition; // 发射位置 GLKVector3 emissionVelocity; // 发射速度 GLKVector3 emissionForce; // 发射重力 GLKVector2 size; // 发射大小 GLKVector2 emissionTimeAndLife; // 发射时间和寿命[出生时间,死亡时间] } CCParticleAttributes; // GLSL程序Uniform参数 enum { CCMVPMatrix,// MVP矩阵 CCSamplers2D,// Samplers2D纹理 CCElapsedSeconds,// 耗时 CCGravity,// 重力 CCNumUniforms// Uniforms个数 }; // 属性标识符 typedef enum { CCParticleEmissionPosition = 0,// 粒子发射位置 CCParticleEmissionVelocity,// 粒子发射速度 CCParticleEmissionForce,// 粒子发射重力 CCParticleSize,// 粒子发射大小 CCParticleEmissionTimeAndLife, // 粒子发射时间和寿命 } CCParticleAttrib;

  • 定义属性
// 重力 @property(nonatomic,assign)GLKVector3 gravity; // 耗时 @property(nonatomic,assign)GLfloat elapsedSeconds; // 纹理 @property (strong, nonatomic, readonly)GLKEffectPropertyTexture *texture2d0; // 变换 @property (strong, nonatomic, readonly) GLKEffectPropertyTransform *transform;

@interface YDWPointParticleEffect() { GLfloat elapsedSeconds; // 耗时 GLuint program; // 程序 GLint uniforms[CCNumUniforms]; // Uniforms数组 }// 顶点属性数组缓冲区 @property (strong, nonatomic, readwrite) YDWVertexAttribArrayBuffer*particleAttributeBuffer; // 粒子个数 @property (nonatomic, assign, readonly) NSUInteger numberOfParticles; // 粒子属性数据 @property (nonatomic, strong, readonly) NSMutableData *particleAttributesData; // 是否更新粒子数据 @property (nonatomic, assign, readwrite) BOOL particleDataWasUpdated;

  • 定义方法名
// 加载shaders - (BOOL)loadShaders; // 编译shaders - (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file; // 链接Program - (BOOL)linkProgram:(GLuint)prog; // 验证Program - (BOOL)validateProgram:(GLuint)prog;

② addParticleAtPosition函数:用于添加粒子,每次调用仅添加一个粒子,在视图控制器中创建粒子对象,通过粒子对象来调用该方法创建粒子
  • 初始化纹理属性
// 初始化 - (id)init { self = [super init]; if (self != nil) { // 初始化纹理属性 texture2d0 = [[GLKEffectPropertyTexture alloc] init]; // 是否可用 texture2d0.enabled = YES; /* 命名纹理对象 * 等价于:void glGenTextures (GLsizei n, GLuint *textures); * 在数组textures中返回n个当期未使用的值,表示纹理对象的名称 * 零作为一个保留的纹理对象名,它不会被此函数当做纹理对象名称而返回 */ texture2d0.name = 0; // 纹理类型 默认值是glktexturetarget2d texture2d0.target = GLKTextureTarget2D; /* 纹理用于计算其输出片段颜色的模式。看到GLKTextureEnvMode * GLKTextureEnvModeReplace,输出颜色设置为从纹理获取的颜色,忽略输入颜色 * GLKTextureEnvModeModulate, 默认!输出颜色是通过将纹理的颜色乘以输入颜色来计算的 * GLKTextureEnvModeDecal,输出颜色是通过使用纹理的alpha组件来混合纹理颜色和输入颜色来计算的 */ texture2d0.envMode = GLKTextureEnvModeReplace; // 坐标变换的信息用于GLKit渲染效果。GLKEffectPropertyTransform类定义的属性进行渲染时的效果提供的坐标变换 transform = [[GLKEffectPropertyTransform alloc] init]; // 重力.默认地球重力 gravity = CCDefaultGravity; // 耗时 elapsedSeconds = 0.0f; // 粒子属性数据 particleAttributesData = https://www.it610.com/article/[NSMutableData data]; }return self; }

  • 创建粒子和设置粒子属性
// 添加一个粒子 - (void)addParticleAtPosition:(GLKVector3)aPosition velocity:(GLKVector3)aVelocity force:(GLKVector3)aForce size:(float)aSize lifeSpanSeconds:(NSTimeInterval)aSpan fadeDurationSeconds:(NSTimeInterval)aDuration { // 创建新的粒子 CCParticleAttributes newParticle; // 设置相关参数(位置\速度\抛物线\大小\耗时) newParticle.emissionPosition = aPosition; newParticle.emissionVelocity = aVelocity; newParticle.emissionForce = aForce; newParticle.size = GLKVector2Make(aSize, aDuration); // 向量(耗时,发射时长) newParticle.emissionTimeAndLife = GLKVector2Make(elapsedSeconds, elapsedSeconds + aSpan); BOOL foundSlot = NO; // 粒子个数 const long count = self.numberOfParticles; // 循环设置粒子到数组中 for(int i = 0; i < count && !foundSlot; i++) {// 获取当前旧粒子 CCParticleAttributes oldParticle = [self particleAtIndex:i]; // 如果旧的粒子的死亡时间小于当前时间:emissionTimeAndLife.y = elapsedSeconds + aspan if(oldParticle.emissionTimeAndLife.y < self.elapsedSeconds) { // 更新粒子的属性 [self setParticle:newParticle atIndex:i]; // 是否替换 foundSlot = YES; } }// 如果不替换 if(!foundSlot) { // 在particleAttributesData 拼接新的数据 [self.particleAttributesData appendBytes:&newParticle length:sizeof(newParticle)]; // 粒子数据是否更新 self.particleDataWasUpdated = YES; } }

// 设置粒子的属性 - (void)setParticle:(CCParticleAttributes)aParticle atIndex:(NSUInteger)anIndex { // mutableBytes:指向可变数据对象所包含数据的指针 // 获取粒子属性结构体内容 CCParticleAttributes *particlesPtr = (CCParticleAttributes *)[self.particleAttributesData mutableBytes]; // 将粒子结构体对应的属性修改为新值 particlesPtr[anIndex] = aParticle; // 更改粒子状态! 是否更新 self.particleDataWasUpdated = YES; }

  • 获取粒子个数
// 获取粒子个数 - (NSUInteger)numberOfParticles { static long last; // 总数据/粒子结构体大小 long ret = [self.particleAttributesData length] / sizeof(CCParticleAttributes); // 如果last != ret 表示粒子个数更新 if (last != ret) { // 则修改last数量 last = ret; NSLog(@"count %ld", ret); }return ret; }

③ prepareToDraw函数
  • 加载 & 使用shader
- (BOOL)loadShaders { GLuint vertShader, fragShader; NSString *vertShaderPathname, *fragShaderPathname; // 创建program program = glCreateProgram(); // 创建并编译 vertex shader. vertShaderPathname = [[NSBundle mainBundle] pathForResource: @"CCPointParticleShader" ofType:@"vsh"]; if (![self compileShader:&vertShader type:GL_VERTEX_SHADER file:vertShaderPathname]) { NSLog(@"Failed to compile vertex shader"); return NO; }// 创建并编译 fragment shader. fragShaderPathname = [[NSBundle mainBundle] pathForResource: @"CCPointParticleShader" ofType:@"fsh"]; if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragShaderPathname]) { NSLog(@"Failed to compile fragment shader"); return NO; }// 将vertex shader 附加到程序. glAttachShader(program, vertShader); // 将fragment shader 附加到程序. glAttachShader(program, fragShader); // 绑定属性位置 // 这需要在链接之前完成. /* 应用程序通过glBindAttribLocation把“顶点属性索引”绑定到“顶点属性名”,glBindAttribLocation在program被link之前执行。 void glBindAttribLocation(GLuint program, GLuint index,const GLchar *name) program:对应的程序 index:顶点属性索引 name:属性名称 *///位置 glBindAttribLocation(program, CCParticleEmissionPosition, "a_emissionPosition"); //速度 glBindAttribLocation(program, CCParticleEmissionVelocity, "a_emissionVelocity"); //重力 glBindAttribLocation(program, CCParticleEmissionForce, "a_emissionForce"); //大小 glBindAttribLocation(program, CCParticleSize, "a_size"); //持续时间、渐隐时间 glBindAttribLocation(program, CCParticleEmissionTimeAndLife, "a_emissionAndDeathTimes"); // Link program 失败 if (![self linkProgram:program]) { NSLog(@"Failed to link program: %d", program); //link识别,删除vertex shader\fragment shader\program if (vertShader) { glDeleteShader(vertShader); vertShader = 0; } if (fragShader) { glDeleteShader(fragShader); fragShader = 0; } if (program) { glDeleteProgram(program); program = 0; }return NO; }// 获取uniform变量的位置. // MVP变换矩阵 uniforms[CCMVPMatrix] = glGetUniformLocation(program,"u_mvpMatrix"); // 纹理 uniforms[CCSamplers2D] = glGetUniformLocation(program,"u_samplers2D"); // 重力 uniforms[CCGravity] = glGetUniformLocation(program,"u_gravity"); // 持续时间、渐隐时间 uniforms[CCElapsedSeconds] = glGetUniformLocation(program,"u_elapsedSeconds"); // 使用完 // 删除 vertex and fragment shaders. if (vertShader) { glDetachShader(program, vertShader); glDeleteShader(vertShader); } if (fragShader) { glDetachShader(program, fragShader); glDeleteShader(fragShader); } return YES; }

  • 编译shader
// 编译shader - (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file { //状态 //GLint status; //路径-C语言 const GLchar *source; //从OC字符串中获取C语言字符串 //获取路径 source = (GLchar *)[[NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil] UTF8String]; //判断路径 if (!source) { NSLog(@"Failed to load vertex shader"); return NO; }//创建shader-顶点\片元 *shader = glCreateShader(type); //绑定shader glShaderSource(*shader, 1, &source, NULL); //编译Shader glCompileShader(*shader); //获取加载Shader的日志信息 //日志信息长度 GLint logLength; /* 在OpenGL中有方法能够获取到 shader错误 参数1:对象,从哪个Shader 参数2:获取信息类别, GL_COMPILE_STATUS//编译状态 GL_INFO_LOG_LENGTH//日志长度 GL_SHADER_SOURCE_LENGTH //着色器源文件长度 GL_SHADER_COMPILER//着色器编译器 参数3:获取长度 */ glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength); // 判断日志长度 > 0 if (logLength > 0) { // 创建日志字符串 GLchar *log = (GLchar *)malloc(logLength); /* 获取日志信息 参数1:着色器 参数2:日志信息长度 参数3:日志信息长度地址 参数4:日志存储的位置 */ glGetShaderInfoLog(*shader, logLength, &logLength, log); //打印日志信息 NSLog(@"Shader compile log:\n%s", log); //释放日志字符串 free(log); return NO; } return YES; }

  • 通过Uniform传递数据
// 使用program glUseProgram(program); // 计算MVP矩阵变化 // 投影矩阵 与 模式视图矩阵 相乘结果 GLKMatrix4 modelViewProjectionMatrix = GLKMatrix4Multiply(self.transform.projectionMatrix,self.transform.modelviewMatrix); /* 将结果矩阵,通过unifrom传递 * glUniformMatrix4fv (GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) * 参数1:location,要更改的uniforms变量的位置 * 参数2:cout ,更改矩阵的个数 * 参数3:transpose,指是否要转置矩阵,并将它作为uniform变量的值,必须为GL_FALSE * 参数4:value ,指向count个数的元素指针.用来更新uniform变量的值. */ glUniformMatrix4fv(uniforms[CCMVPMatrix], 1, 0,modelViewProjectionMatrix.m); /* 一个纹理采样均匀变量 * glUniform1f(GLint location,GLfloat v0); * location:指明要更改的uniform变量的位置 * v0:指明在指定的uniform变量中要使用的新值 */ glUniform1i(uniforms[CCSamplers2D], 0); /* 粒子物理值:重力 * void glUniform3fv(GLint location,GLsizei count,const GLfloat *value); * 参数列表: * location:指明要更改的uniform变量的位置 * count:指明要更改的向量个数 * value:指明一个指向count个元素的指针,用来更新指定的uniform变量 */ glUniform3fv(uniforms[CCGravity], 1, self.gravity.v); // 耗时 glUniform1fv(uniforms[CCElapsedSeconds], 1, &elapsedSeconds);

  • 更新粒子数据,需要调用工具类中的初始化/更新数据方法,将数据从CPU拷贝至GPU
// 粒子数据更新 if(self.particleDataWasUpdated) { // 缓存区为空,且粒子数据大小>0 if(self.particleAttributeBuffer == nil && [self.particleAttributesData length] > 0) { /* 顶点属性没有送到GPU:初始化缓存区 * 1.数据大小sizeof(CCParticleAttributes) * 2.数据个数 (int)[self.particleAttributesData length] / sizeof(CCParticleAttributes) * 3.数据源[self.particleAttributesData bytes] * 4.用途 GL_DYNAMIC_DRAW */// 数据大小 GLsizeiptr size = sizeof(CCParticleAttributes); // 个数 int count = (int)[self.particleAttributesData length] / sizeof(CCParticleAttributes); self.particleAttributeBuffer = [[YDWVertexAttribArrayBuffer alloc] initWithAttribStride:size numberOfVertices:count bytes:[self.particleAttributesData bytes] usage:GL_DYNAMIC_DRAW]; } else { /* 如果已经开辟空间,则接收新的数据 * 1.数据大小 sizeof(CCParticleAttributes) * 2.数据个数(int)[self.particleAttributesData length] / sizeof(CCParticleAttributes) * 3.数据源 [self.particleAttributesData bytes] */// 数据大小 GLsizeiptr size = sizeof(CCParticleAttributes); // 个数 int count = (int)[self.particleAttributesData length] / sizeof(CCParticleAttributes); [self.particleAttributeBuffer reinitWithAttribStride:size numberOfVertices:count bytes:[self.particleAttributesData bytes]]; }// 恢复更新状态为NO self.particleDataWasUpdated = NO; }

  • 准备相关数据,需要调用工具类的封装的prepareToDrawWithAttrib方法,打开attribute通道,并设置数据的读取方式
// 准备顶点数据 [self.particleAttributeBuffer prepareToDrawWithAttrib:CCParticleEmissionPosition numberOfCoordinates:3 attribOffset: offsetof(CCParticleAttributes, emissionPosition) shouldEnable:YES]; // 准备粒子发射速度数据 [self.particleAttributeBuffer prepareToDrawWithAttrib:CCParticleEmissionVelocity numberOfCoordinates:3 attribOffset: offsetof(CCParticleAttributes, emissionVelocity) shouldEnable:YES]; // 准备重力数据 [self.particleAttributeBuffer prepareToDrawWithAttrib:CCParticleEmissionForce numberOfCoordinates:3 attribOffset: offsetof(CCParticleAttributes, emissionForce) shouldEnable:YES]; // 准备粒子size数据 [self.particleAttributeBuffer prepareToDrawWithAttrib:CCParticleSize numberOfCoordinates:2 attribOffset: offsetof(CCParticleAttributes, size) shouldEnable:YES]; // 准备粒子的持续时间和渐隐时间数据 [self.particleAttributeBuffer prepareToDrawWithAttrib:CCParticleEmissionTimeAndLife numberOfCoordinates:2 attribOffset: offsetof(CCParticleAttributes, emissionTimeAndLife) shouldEnable:YES];

  • 绑定纹理
// 将所有纹理绑定到各自的单位 /* void glActiveTexture(GLenum texUnit); 该函数选择一个纹理单元,线面的纹理函数将作用于该纹理单元上,参数为符号常量GL_TEXTUREi ,i的取值范围为0~K-1,K是OpenGL实现支持的最大纹理单元数,可以使用GL_MAX_TEXTURE_UNITS来调用函数glGetIntegerv()获取该值 可以这样简单的理解为:显卡中有N个纹理单元(具体数目依赖你的显卡能力),每个纹理单元(GL_TEXTURE0、GL_TEXTURE1等)都有GL_TEXTURE_1D、GL_TEXTURE_2D等 */ glActiveTexture(GL_TEXTURE0); //判断纹理标记是否为空,以及纹理是否可用 if(0 != self.texture2d0.name && self.texture2d0.enabled) { //绑定纹理到纹理标记上 //参数1:纹理类型 //参数2:纹理名称 glBindTexture(GL_TEXTURE_2D, self.texture2d0.name); } else { //绑定一个空的 glBindTexture(GL_TEXTURE_2D, 0); }

④ draw函数:用于绘制粒子,底层需要调用工具类的drawArrayWithMode方法,使用OpenGL ES数组绘制的方式进行绘制
- (void)draw { // 禁用深度缓冲区写入 glDepthMask(GL_FALSE); //绘制 /* 1.模式 2.开始的位置 3.粒子个数 */ [self.particleAttributeBuffer drawArrayWithMode:GL_POINTS startVertexIndex:0 numberOfVertices:(int)self.numberOfParticles]; // 启用深度缓冲区写入 glDepthMask(GL_TRUE); }

四、视图控制器 ②③④bai⑤⑥⑦⑧⑨du⑩??? 通过粒子类YDWPointParticleEffect创建粒子对象,并实现4种粒子效果,然后系统调用GLKView & GLKViewDelegate代理方法进行绘制和更新
  • viewDidLoad:
- (void)viewDidLoad { [super viewDidLoad]; // 新建OpenGLES 上下文 self.mContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; GLKView* view = (GLKView *)self.view; view.context = self.mContext; view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888; view.drawableDepthFormat = GLKViewDrawableDepthFormat24; [EAGLContext setCurrentContext:self.mContext]; // 纹理路径 NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"ball" ofType:@"png"]; if (path == nil) { NSLog(@"ball texture image not found"); return; }// 加载纹理对象 NSError *error = nil; self.ballParticleTexture = [GLKTextureLoader textureWithContentsOfFile:path options:nil error:&error]; // 粒子对象 self.particleEffect = [[YDWPointParticleEffect alloc]init]; self.particleEffect.texture2d0.name = self.ballParticleTexture.name; self.particleEffect.texture2d0.target = self.ballParticleTexture.target; // 开启深度测试 glEnable(GL_DEPTH_TEST); // 开启混合 glEnable(GL_BLEND); // 设置混合因子 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // 执行代码块.4种不同效果 void(^blockA)(void) = ^{ self.autoSpawnDelta = 0.5f; // 重力 self.particleEffect.gravity = CCDefaultGravity; // X轴上随机速度 float randomXVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX; /* Position:出发位置 velocity:速度 force:抛物线 size:大小 lifeSpanSeconds:耗时 fadeDurationSeconds:渐逝时间 */ [self.particleEffect addParticleAtPosition:GLKVector3Make(0.0f, 0.0f, 0.9f) velocity:GLKVector3Make(randomXVelocity, 1.0f, -1.0f) force:GLKVector3Make(0.0f, 9.0f, 0.0f) size:8.0f lifeSpanSeconds:3.2f fadeDurationSeconds:0.5f]; }; void(^blockB)(void) = ^{ self.autoSpawnDelta = 0.05f; // 重力 self.particleEffect.gravity = GLKVector3Make(0.0f,0.5f, 0.0f); // 一次创建多少个粒子 int n = 50; for(int i = 0; i < n; i++) { // X轴速度 float randomXVelocity = -0.1f + 0.2f *(float)random() / (float)RAND_MAX; // Y轴速度 float randomZVelocity = 0.1f + 0.2f * (float)random() / (float)RAND_MAX; [self.particleEffect addParticleAtPosition:GLKVector3Make(0.0f, -0.5f, 0.0f) velocity:GLKVector3Make( randomXVelocity, 0.0, randomZVelocity) force:GLKVector3Make(0.0f, 0.0f, 0.0f) size:16.0f lifeSpanSeconds:2.2f fadeDurationSeconds:3.0f]; }}; void(^blockC)(void) = ^{ self.autoSpawnDelta = 0.5f; //重力 self.particleEffect.gravity = GLKVector3Make(0.0f, 0.0f, 0.0f); int n = 100; for(int i = 0; i < n; i++) { //X,Y,Z速度 float randomXVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX; float randomYVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX; float randomZVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX; //创建粒子 [self.particleEffect addParticleAtPosition:GLKVector3Make(0.0f, 0.0f, 0.0f) velocity:GLKVector3Make( randomXVelocity, randomYVelocity, randomZVelocity) force:GLKVector3Make(0.0f, 0.0f, 0.0f) size:4.0f lifeSpanSeconds:3.2f fadeDurationSeconds:0.5f]; }}; void(^blockD)(void) = ^{ self.autoSpawnDelta = 3.2f; // 重力 self.particleEffect.gravity = GLKVector3Make(0.0f, 0.0f, 0.0f); int n = 100; for(int i = 0; i < n; i++) { // X,Y速度 float randomXVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX; float randomYVelocity = -0.5f + 1.0f * (float)random() / (float)RAND_MAX; // GLKVector3Normalize 计算法向量 // 计算速度与方向 GLKVector3 velocity = GLKVector3Normalize( GLKVector3Make( randomXVelocity, randomYVelocity, 0.0f)); [self.particleEffect addParticleAtPosition:GLKVector3Make(0.0f, 0.0f, 0.0f) velocity:velocity force:GLKVector3MultiplyScalar(velocity, -1.5f) size:4.0f lifeSpanSeconds:3.2f fadeDurationSeconds:0.1f]; }}; // 将4种不同效果的BLOCK块存储到数组中 self.emitterBlocks = @[[blockA copy],[blockB copy],[blockC copy],[blockD copy]]; // 纵横比 float aspect = CGRectGetWidth(self.view.bounds) / CGRectGetHeight(self.view.bounds); // 设置投影方式\模型视图变换矩阵 [self preparePointOfViewWithAspectRatio:aspect]; }

  • MVP矩阵
// MVP矩阵 - (void)preparePointOfViewWithAspectRatio:(GLfloat)aspectRatio { // 设置透视投影方式 self.particleEffect.transform.projectionMatrix = GLKMatrix4MakePerspective( GLKMathDegreesToRadians(85.0f), aspectRatio, 0.1f, 20.0f); // 模型视图变换矩阵 // 获取世界坐标系去模型矩阵中. /* LKMatrix4 GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ, float centerX, float centerY, float centerZ, float upX, float upY, float upZ) 等价于 OpenGL 中 void gluLookAt(GLdouble eyex,GLdouble eyey,GLdouble eyez,GLdouble centerx,GLdouble centery,GLdouble centerz,GLdouble upx,GLdouble upy,GLdouble upz); 目的:根据你的设置返回一个4x4矩阵变换的世界坐标系坐标。 参数1:眼睛位置的x坐标 参数2:眼睛位置的y坐标 参数3:眼睛位置的z坐标 第一组:就是脑袋的位置参数4:正在观察的点的X坐标 参数5:正在观察的点的Y坐标 参数6:正在观察的点的Z坐标 第二组:就是眼睛所看物体的位置参数7:摄像机上向量的x坐标 参数8:摄像机上向量的y坐标 参数9:摄像机上向量的z坐标 第三组:就是头顶朝向的方向(因为你可以头歪着的状态看物体) */self.particleEffect.transform.modelviewMatrix = GLKMatrix4MakeLookAt( 0.0, 0.0, 1.0,// Eye position 0.0, 0.0, 0.0,// Look-at position 0.0, 1.0, 0.0); // Up direction }

  • update更新:
// 更新 - (void)update { // 时间间隔 NSTimeInterval timeElapsed = self.timeSinceFirstResume; /* // 上一次更新时间 NSLog(@"timeSinceLastUpdate: %f", self.timeSinceLastUpdate); // 上一次绘制的时间 NSLog(@"timeSinceLastDraw: %f", self.timeSinceLastDraw); // 第一次恢复时间 NSLog(@"timeSinceFirstResume: %f", self.timeSinceFirstResume); // 上一次恢复时间 NSLog(@"timeSinceLastResume: %f", self.timeSinceLastResume); */ // 消耗时间 self.particleEffect.elapsedSeconds = timeElapsed; // 动画时间 < 当前时间与上一次更新时间 if(self.autoSpawnDelta < (timeElapsed - self.lastSpawnTime)) { // 更新上一次更新时间 self.lastSpawnTime = timeElapsed; // 获取当前选择的block void(^emitterBlock)() = [self.emitterBlocks objectAtIndex: self.currentEmitterIndex]; // 执行block emitterBlock(); } }

  • glkView:(GLKView *)view drawInRect:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(0.3, 0.3, 0.3, 1); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); // 准备绘制 [self.particleEffect prepareToDraw]; // 绘制 [self.particleEffect draw]; }

完整示例传送门 OpenGL_ES之GLSL自定义粒子效果

    推荐阅读