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 mat4      u_mvpMatrix;      // 变换矩阵
uniform sampler2D       u_samplers2D[1];  // 纹理
uniform highp vec3      u_gravity;        // 重力
uniform highp float     u_elapsedSeconds; // 当前时间

// Varyings:粒子透明度
varying lowp float      v_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 mat4      u_mvpMatrix;
uniform sampler2D       u_samplers2D[1];
uniform highp vec3      u_gravity;
uniform highp float     u_elapsedSeconds;

// Varyings
varying lowp float      v_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);
}

三、粒子类:管理并绘制粒子

主要是管理并绘制所有的粒子,相当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 = [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自定义粒子效果

猜你喜欢

转载自blog.csdn.net/Forever_wj/article/details/107902672
今日推荐