OpenGL ES之GLSL自定義着色器程式設計實現粒子效果

2020-08-09 23:52:13

效果展示

在这里插入图片描述

實現流程

一、自定義着色器

  • 頂點着色器:CCPointParticleShader.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(頂點屬性陣列緩衝區)
// 此方法在當前的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狀態被改變,允許系結緩衝區和設定指針。
- (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開始
- (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];
    
}