OpenGL ES 2.0 (iOS)[05-1]:進入 3D 世界,從正方體開始

weixin_34402408發表於2016-12-27

目錄

一、目標

1. 基礎知識準備
2. 圖形分析

二、編寫程式

0. 工程結構與整體渲染管線
1. Depth Render Buffer
2. 資料來源的編寫與繫結
3. 深度測試與繪製
4. 讓正方體動起來

三、參考書籍、文章


一、目標

1411747-66067447aaaf1a92.gif
正方體.gif
1. 基礎知識準備

a. 渲染管線的基礎知識
《OpenGL ES 2.0 (iOS)[01]: 一步從一個小三角開始》

b. 3D 變換
《OpenGL ES 2.0 (iOS)[04]:座標空間 與 OpenGL ES 2 3D空間》

2. 圖形分析

a. 它是一個正方體,由六個正方形面組成,有 8 個頂點;

b. 正方體並不是二維圖形,而是三維圖形,即頂點座標應為{x, y, z},而且 z 不可能一直為 0;

c. 若由 OpenGL ES 繪製,z 座標表示深度(depth)資訊;

d. 六個面均有不一樣的顏色,即 8 個頂點都帶有顏色資訊,即渲染的頂點要提供相應的顏色資訊;

e. 六個正方形面,若由 OpenGL ES 繪製,需要由兩個三角面組合而成,即繪製模式為 GL_TRIANGLE*;

f. 正方體的每一個頂點都包含在三個面中,即一個頂點都會被使用多次,即繪製的時候應該使用 glDrawElements 方法而不是 glDrawArrays 方法,所以除 8 個頂點的資料外還需增加下標資料才有可能高效地繪製出正方體;

g. 正方體在不斷地旋轉運動,即可能要實時改變頂點的資訊並進行重新繪製以達到運動的效果(思路:動圖就是靜態圖的快速連續變化,只要變化的速度大於人眼可以辨別的速度,就會產生自然流暢的動圖)

分析可程式化:

  1. 結合 a、b、c、d 四點可以知道,頂點的資料格式可以為:
#define PositionCoordinateCount         (3)
#define ColorCoordinateCount            (4)
typedef struct {
    GLfloat position[PositionCoordinateCount];
    GLfloat color[ColorCoordinateCount];
} VFVertex;
static const VFVertex vertices[] = {
    {{...}, {...}}
    ......
};

當然你也可以把 position 和 color 分開來,只不過我認為放在一起更好管理罷了。

  1. 從 e、f 兩點可以知道,增加的資料及繪製的方式:

因為使用 element 方式,所以增加下標資訊;

static const GLubyte indices[] = {
    ......
};
    glDrawElements(GL_TRIANGLES,
                   sizeof(indices) / sizeof(indices[0]),
                   GL_UNSIGNED_BYTE,
                   indices);
  1. 從 g 點可以知道:

圖形的運動,表明圖形在一定時間內不斷地進行更新(重新繪製並渲染),即只要使用具有定時功能的方法即可處理圖形的運動,NSTimer 就可以勝任這個工作,不過 iOS 提供了一個 CADisplayLink 類來專門做定時更新的工作,所以可以選用它進行運動更新;

1411747-d167196d6c5ebf05.png

二、編寫程式

0. 工程結構與整體渲染管線

1411747-ac5a3391462cd15e.png

結構目錄簡述

  1. 藍框是包含 CADisplayLink 子類的類,用於更新渲染,就是讓圖形動起來;

  2. 紅框就是整體的渲染管線,所有的繪製渲染工作均在此處;

渲染管線 + Depth
Render Buffer 有三種快取,Color 、Depth 、Stencil 三種;而單純繪製 2D 圖形的時候因為沒有引入 z 座標(z != 0)而只使用了 Render Buffer 的 Color Render Buffer ;
而如今要進行渲染的正方體,是帶有 z 座標,即深度資訊,所以自然要引入 Depth Render Buffer 了;

引入 Depth Render Buffer 並使其工作的步驟:


1411747-3c76146802b7fdfd.png
Depth Render Buffer

ViewController 的程式排程

#import "ViewController.h"

#import "VFGLCubeView.h"

@interface ViewController ()
@property (strong, nonatomic) VFGLCubeView *cubeView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    CGRect rect = CGRectOffset(self.view.frame, 0, 0);
    self.cubeView = [[VFGLCubeView alloc] initWithFrame:rect];
    
    [_cubeView prepareDisplay];
    [_cubeView drawAndRender];
    
    [self.view addSubview:_cubeView];
    
}

- (void)viewDidAppear:(BOOL)animated {

    [super viewDidAppear:animated];
    
    [self.cubeView update];
    
}

- (void)viewWillDisappear:(BOOL)animated {
    
    [super viewWillDisappear:animated];
    
    [self.cubeView pauseUpdate];
    
}

@end

內容並不複雜,所以此處不進行贅述;

渲染管線
prepareDisplay + drawAndRender

prepareDisplay 渲染管線的準備部分


- (void)prepareDisplay {
    
    // 1. Context
    [self settingContext];
    
    // 2 要在 Render Context setCurrent 後, 再進行 OpenGL ES 的操作
    // [UIColor colorWithRed:0.423 green:0.046 blue:0.875 alpha:1.000]
    // [UIColor colorWithRed:0.423 green:0.431 blue:0.875 alpha:1.000]
    [self setRenderBackgroundColor:RGBAColorMake(0.423, 0.431, 0.875, 1.000)];
    
    // 2.? Vertex Buffer Object
    self.vboBufferID = [self createVBO];
    [self bindVertexDatasWithVertexBufferID:_vboBufferID
                               bufferTarget:GL_ARRAY_BUFFER
                                   dataSize:sizeof(vertices)
                                       data:vertices
                                   elements:NO];
    
    [self bindVertexDatasWithVertexBufferID:kInvaildBufferID
                               bufferTarget:GL_ELEMENT_ARRAY_BUFFER
                                   dataSize:sizeof(indices)
                                       data:indices
                                   elements:YES];
    
    // 3. Shader
    GLuint vertexShaderID = [self createShaderWithType:GL_VERTEX_SHADER];
    [self compileVertexShaderWithShaderID:vertexShaderID type:GL_VERTEX_SHADER];
    
    GLuint fragmentShaderID = [self createShaderWithType:GL_FRAGMENT_SHADER];
    [self compileVertexShaderWithShaderID:fragmentShaderID type:GL_FRAGMENT_SHADER];
    
    self.programID = [self createShaderProgram];
    [self attachShaderToProgram:_programID
                  vertextShader:vertexShaderID
                 fragmentShader:fragmentShaderID];
    
    [self linkProgramWithProgramID:_programID];
    
    [self updateUniformsLocationsWithProgramID:_programID];
    
    // 4. Attach VBOs
    [self attachCubeVertexArrays];
    
}

基於這部分,本文的工作在以下兩處進行:

    // 1. Context
    [self settingContext];

它負責確定渲染上下文,以及 Render Buffer 與 Frame Buffer 的資源繫結處理;
[self settingContext]; 詳見 本章 1.Depth Render Buffer 一節

    // 2.? Vertex Buffer Object
    self.vboBufferID = [self createVBO];
    [self bindVertexDatasWithVertexBufferID:_vboBufferID
                               bufferTarget:GL_ARRAY_BUFFER
                                   dataSize:sizeof(vertices)
                                       data:vertices
                                   elements:NO];
    
    [self bindVertexDatasWithVertexBufferID:kInvaildBufferID
                               bufferTarget:GL_ELEMENT_ARRAY_BUFFER
                                   dataSize:sizeof(indices)
                                       data:indices
                                   elements:YES];

它是處理頂點快取資料的;
VBO 與 資料來源 詳見 本章 2. 資料來源的編寫與繫結

drawAndRender 渲染管線的餘下部分

- (void)drawAndRender {
    
    // 5. Draw Cube
    // 5.0 使用 Shader
    [self userShaderWithProgramID:_programID];
    
    // 5.1 應用 3D 變換
    self.modelPosition = GLKVector3Make(0, -0.5, -5);
    [self transforms];
    
    // 5.2 清除舊渲染快取
    [self clearColorRenderBuffer:YES depth:YES stencil:NO];
    
    // 5.3 開啟深度測試
    [self enableDepthTesting];
    
    // 5.4 繪製圖形
    [self drawCube];
    
    // 5.5 渲染圖形
    [self render];
    
}

基於這部分,本文的工作在此處進行:

    // 5.2 清除舊渲染快取
    [self clearColorRenderBuffer:YES depth:YES stencil:NO];
    
    // 5.3 開啟深度測試
    [self enableDepthTesting];
    
    // 5.4 繪製圖形
    [self drawCube];

詳見 本章 3. 深度測試與繪製 一節

關於實時更新的內容

    [self.cubeView update];
    [self.cubeView pauseUpdate];

詳見 本章 4. 讓正方體動起來

1. Depth Render Buffer

[self settingContext];
它的內容為:

- (void)setContext:(EAGLContext *)context {
    
    if (_context != context) {
     
        [EAGLContext setCurrentContext:_context];
        
        [self deleteFrameBuffer:@[@(self.frameBufferID)]];
        self.frameBufferID = kInvaildBufferID;
        
        [self deleteRenderBuffer:@[@(self.colorRenderBufferID), @(self.depthRenderBufferID)]];
        self.colorRenderBufferID = self.depthRenderBufferID = kInvaildBufferID;
        
        _context = context;
        
        if (context != nil) {
            
            _context = context;
            [EAGLContext setCurrentContext:_context];
            
            // 2. Render / Frame Buffer
            
            // 2.0 建立 Frame Buffer
            [self deleteFrameBuffer:@[@(self.frameBufferID)]];
            
            self.frameBufferID = [self createFrameBuffer];
            
            // 2.1 Color & Depth Render Buffer
            [self deleteRenderBuffer:@[@(self.colorRenderBufferID)]];
            
            self.colorRenderBufferID = [self createRenderBuffer];
            
            [self renderBufferStrogeWithRenderID:self.colorRenderBufferID];
            
            [self attachRenderBufferToFrameBufferWithRenderBufferID:self.colorRenderBufferID
                                                         attachment:GL_COLOR_ATTACHMENT0];
            
            // 2.2 檢查 Frame 裝載 Render Buffer 的問題
            [self checkFrameBufferStatus];
            
            // 2.3 Add Depth Render Buffer
            [self enableDepthRenderBuffer];
            
            [self deleteRenderBuffer:@[@(self.depthRenderBufferID)]];
            
            if ( ! CGSizeEqualToSize(self.renderBufferSize, CGSizeZero) &&
                self.depthMode != VFDrawableDepthMode_None) {
                
                self.depthRenderBufferID = [self createRenderBuffer];
                
                if (self.depthRenderBufferID == kInvaildBufferID) {
                    return;
                }
                
                [self renderBufferStrogeWithRenderID:self.depthRenderBufferID];
                
                [self attachRenderBufferToFrameBufferWithRenderBufferID:self.depthRenderBufferID
                                                             attachment:GL_DEPTH_ATTACHMENT];
                
            }
            
            // 2.4 檢查 Frame 裝載 Render Buffer 的問題
            [self checkFrameBufferStatus];
            
        }
        
    }
    
}

- (void)settingContext {

    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    
}

這裡重寫了 setContext: 方法,核心內容是
// 2.3 Add Depth Render Buffer

    // 2.3 Add Depth Render Buffer
    [self enableDepthRenderBuffer];
    
    [self deleteRenderBuffer:@[@(self.depthRenderBufferID)]];
    
    if ( ! CGSizeEqualToSize(self.renderBufferSize, CGSizeZero) &&
        self.depthMode != VFDrawableDepthMode_None) {
        
        self.depthRenderBufferID = [self createRenderBuffer];
        
        if (self.depthRenderBufferID == kInvaildBufferID) {
            return;
        }
        
        [self renderBufferStrogeWithRenderID:self.depthRenderBufferID];
        
        [self attachRenderBufferToFrameBufferWithRenderBufferID:self.depthRenderBufferID
                                                     attachment:GL_DEPTH_ATTACHMENT];
        
    }

步驟分解:


1411747-0ce6f0fc391daaa2.png
Step One

第一步,建立並繫結深度渲染快取,對應程式程式碼為:

self.depthRenderBufferID = [self createRenderBuffer];
- (GLuint)createRenderBuffer {
    
    GLuint ID = kInvaildBufferID;
    glGenRenderbuffers(RenderMemoryBlock, &ID);  // 申請 Render Buffer
    glBindRenderbuffer(GL_RENDERBUFFER, ID); // 建立 Render Buffer
    
    return ID;
    
}

第二步,儲存新建立的渲染快取,對應程式程式碼為:

[self renderBufferStrogeWithRenderID:self.depthRenderBufferID];
- (void)renderBufferStrogeWithRenderID:(GLuint)renderBufferID {
    
    if (renderBufferID == self.colorRenderBufferID) {
        
        // 必須要在 glbindRenderBuffer 之後 (就是使用 Render Buffer 之後), 再繫結渲染的圖層
        [self bindDrawableObjectToRenderBuffer];
        
        self.renderBufferSize = [self getRenderBufferSize];
        
    }
    
    if (renderBufferID == self.depthRenderBufferID) {
        
        glRenderbufferStorage(GL_RENDERBUFFER,
                              GL_DEPTH_COMPONENT16,
                              self.renderBufferSize.width,
                              self.renderBufferSize.height);
        
    }
    
}

核心函式:儲存渲染資訊

glRenderbufferStorage
void glRenderbufferStorage(GLenum target,GLenum internalformat,GLsizei width, GLsizei height)
target 只能是 GL_RENDERBUFFER
internalformat 可用選項見下表
width 渲染快取的寬度(畫素單位)
height 渲染快取的高度(畫素單位)
internalformat 儲存格式(位 = bit)
顏色方面 GL_RGB565(5 + 6 + 5 = 16位)、GL_RGBA4(4 x 4 = 16)、GL_RGB5_A1(5 + 5 + 5 + 1 = 16)、GL_RGB8_OES(3 x 8 = 24 )、GL_RGBA8_OES(4 x 8 = 32)
深度方面 GL_DEPTH_COMPONENT16(16位)、GL_DEPTH_COMPONENT24_OES(24位)、GL_DEPTH_COMPONENT32_OES(32位)
模板方面 GL_STENCIL_INDEX8、GL_STENCIL_INDEX1_OES、GL_STENCIL_INDEX4_OES
深度與模板 GL_DEPTH24_STENCIL8_OES

第三步,裝載渲染快取到幀快取中,對應程式程式碼為:

[self attachRenderBufferToFrameBufferWithRenderBufferID:self.depthRenderBufferID
                                             attachment:GL_DEPTH_ATTACHMENT];
- (void)attachRenderBufferToFrameBufferWithRenderBufferID:(GLuint)renderBufferID attachment:(GLenum)attachment {

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, attachment, GL_RENDERBUFFER, renderBufferID);
    
}
2. 資料來源的編寫與繫結

資料來源的書寫
從 2D 到 3D :

1411747-e7db92add5b4a719.png

右下方,線框正方體的 8 個頂點座標分佈,其實 0~7 的編號是你決定的,也就是說 0 放在那裡開始都是可以的,只要是 8 個點即可;
1411747-70845f80f5af0c8e.png
Cube

static const VFVertex vertices[] = {
    // Front
    // 0 [UIColor colorWithRed:0.438 green:0.786 blue:1.000 alpha:1.000]
    {{ 1.0, -1.0,  1.0}, {0.438, 0.786, 1.000, 1.000}}, // 淡(藍) -- 0
    
    // 1 [UIColor colorWithRed:1.000 green:0.557 blue:0.246 alpha:1.000]
    {{ 1.0,  1.0,  1.0}, {1.000, 0.557, 0.246, 1.000}}, // 淡(橙) -- 1
    
    // 2 [UIColor colorWithRed:0.357 green:0.927 blue:0.690 alpha:1.000]
    {{-1.0,  1.0,  1.0}, {0.357, 0.927, 0.690, 1.000}}, // 藍(綠) -- 2
    
    // 3 [UIColor colorWithRed:0.860 green:0.890 blue:0.897 alpha:1.000]
    {{-1.0, -1.0,  1.0}, {0.860, 0.890, 0.897, 1.000}}, // 超淡藍 偏(白) -- 3
    
    // Back
    // 4 [UIColor colorWithRed:0.860 green:0.890 blue:0.897 alpha:1.000]
    {{-1.0, -1.0, -1.0}, {0.860, 0.890, 0.897, 1.000}}, // 超淡藍 偏(白) -- 4
    
    // 5 [UIColor colorWithRed:0.357 green:0.927 blue:0.690 alpha:1.000]
    {{-1.0,  1.0, -1.0}, {0.357, 0.927, 0.690, 1.000}}, // 藍(綠) -- 5
    
    // 6 [UIColor colorWithRed:1.000 green:0.557 blue:0.246 alpha:1.000]
    {{ 1.0,  1.0, -1.0}, {1.000, 0.557, 0.246, 1.000}}, // 淡(橙) -- 6
    
    // 7 [UIColor colorWithRed:0.438 green:0.786 blue:1.000 alpha:1.000]
    {{ 1.0, -1.0, -1.0}, {0.438, 0.786, 1.000, 1.000}}, // 淡(藍) -- 7
};

只要你空間想像不是特別差,估計能看出每個點的座標吧!你可以把這樣的點 { 1.0, -1.0, -1.0} 改成你喜歡的數值亦可,只要最終是正方體即可;

真正重要的資料其實是下標資料:

static const GLubyte indices[] = {
    // Front  ------------- 藍橙綠白 中間線(藍綠)
    0, 1, 2, // 藍橙綠
    2, 3, 0, // 綠白藍
    // Back   ------------- 藍橙綠白 中間線(白橙)
    4, 5, 6, // 白綠橙
    6, 7, 4, // 橙藍白
    // Left   ------------- 白綠
    3, 2, 5, // 白綠綠
    5, 4, 3, // 綠白白
    // Right  ------------- 藍橙
    7, 6, 1, // 藍橙橙
    1, 0, 7, // 橙藍藍
    // Top    ------------- 橙綠
    1, 6, 5, // 橙橙綠
    5, 2, 1, // 綠綠橙
    // Bottom ------------- 白藍
    3, 4, 7, // 白白藍
    7, 0, 3  // 藍藍白
};

這些下標的值由兩個因素決定,第一個因素是上面 8 個頂點資料的下標;第二個因素是時鐘方向;

現在看看時鐘方向:


1411747-0ee3b5b0916f7ca6.png

有沒有發現,每一個正方形的兩個小三角,都是逆時針方向的;當然你也可以換成順時針方向,相應的下標資料就要發生改變;

EP: 如 Front 這個面,如果使用順時針來寫資料為:

    // Front  ------------- 白綠橙藍 中間線(白橙)
    3, 2, 1, // 白綠橙
    1, 0, 2, // 橙藍綠

你也可以從 2 或 1 開始,看你的喜好咯;

方向只有兩個:


1411747-3d51b34375fd1583.png

資源繫結
這裡主要是 VBO 的資料繫結,增加 Element 的支援而已;

    [self bindVertexDatasWithVertexBufferID:kInvaildBufferID
                               bufferTarget:GL_ELEMENT_ARRAY_BUFFER
                                   dataSize:sizeof(indices)
                                       data:indices
                                   elements:YES];
- (void)bindVertexDatasWithVertexBufferID:(GLuint)vertexBufferID bufferTarget:(GLenum)target dataSize:(GLsizeiptr)size data:(const GLvoid *)data elements:(BOOL)isElement {
    
    if ( ! isElement) {
        glBindBuffer(target, vertexBufferID);
    }
    
    // 建立 資源 ( context )
    glBufferData(target,            // 快取塊 型別
                 size,              // 建立的 快取塊 尺寸
                 data,              // 要繫結的頂點資料
                 GL_STATIC_DRAW);   // 快取塊 用途
    
}

此處不再贅述;
如果實在不懂,請移步至
《OpenGL ES 2.0 (iOS)[03]:熟練圖元繪製,玩轉二維圖形》練習練習;

3. 深度測試與繪製

1411747-de0578e53623ab46.png
Step Two

清除舊的深度快取資訊

[self clearColorRenderBuffer:YES depth:YES stencil:NO];
- (void)clearColorRenderBuffer:(BOOL)color depth:(BOOL)depth stencil:(BOOL)stencil {
    
    GLbitfield colorBit     = 0;
    GLbitfield depthBit     = 0;
    GLbitfield stencilBit   = 0;
    
    if (color)      { colorBit      = GL_COLOR_BUFFER_BIT;     }
    if (depth)      { depthBit      = GL_DEPTH_BUFFER_BIT;     }
    if (stencil)    { stencilBit    = GL_STENCIL_BUFFER_BIT;   }
    
    glClear(colorBit | depthBit | stencilBit);
    
}

啟用深度測試

[self enableDepthTesting];
- (void)enableDepthTesting {
    
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);
    
}

這裡多了一個 GL_CULL_FACE 的啟用,它的意思就是,把看不見的畫素資訊剔除掉,只保留能看見的資訊(留前去後);

如果沒有啟用 GL_DEPTH_TEST 程式執行後是這樣的:


1411747-ad66bc49ff5c949b.gif
關掉 GL_DEPTH_TEST.gif

很明顯圖形是有穿透性的,如果去掉 GL_DEPTH_TEST 就不是實體的正方體了;當然如果你喜歡這種效果,也可以關掉 GL_DEPTH_TEST (反正我個人覺得關掉也蠻好看的);

重新繫結 Color Render Buffer
原因,因為當繫結 Depth Render Buffer 之後,渲染管線從原來的繫結(啟用)的 Color Render Buffer 切換成了,繫結(啟用)Depth Render Buffer ,從而導致渲染出來的結果,不是期望中的那樣;所以在繪製前要重新繫結(啟用)Color Render Buffer .

1411747-c92a3b1b705f06e7.png
Step Three
- (void)drawCube {
    
    // 失敗的核心原因
    // 因為 depth buffer 是最後一個繫結的,所以當前渲染的 buffer 變成了 depth 而不是 color
    // 所以 渲染的圖形沒有任何變化,無法產生深度效果
    // Make the Color Render Buffer the current buffer for display
    [self rebindRenderBuffer:@[@(self.colorRenderBufferID)]];
    
    [self rebindVertexBuffer:@[@(self.vboBufferID)]];
    
    glDrawElements(GL_TRIANGLES,
                   sizeof(indices) / sizeof(indices[0]),
                   GL_UNSIGNED_BYTE,
                   indices);
    
}

這是註釋了程式碼中,[self rebindRenderBuffer:@[@(self.colorRenderBufferID)]]; 的執行結果;

1411747-e79f74dca7cb5505.png

4. 讓正方體動起來
1411747-c79c277f52e71e77.png

ViewController 的排程
其實就是,view 顯示的時候更新,不顯示的時候停止更新;

- (void)viewDidAppear:(BOOL)animated {

    [super viewDidAppear:animated];
    
    [self.cubeView update];
    
}

- (void)viewWillDisappear:(BOOL)animated {
    
    [super viewWillDisappear:animated];
    
    [self.cubeView pauseUpdate];
    
}

CubeView 的應用

#pragma mark - DisplayLink Update

- (void)preferTransformsWithTimes:(NSTimeInterval)time {
    
    GLfloat rotateX = self.modelRotate.x;
//    rotateX += M_PI_4 * time;
    
    GLfloat rotateY = self.modelRotate.y;
    rotateY += M_PI_2 * time;
    
    GLfloat rotateZ = self.modelRotate.z;
    rotateZ += M_PI * time;
    
    self.modelRotate = GLKVector3Make(rotateX, rotateY, rotateZ);
    
}

本類提供的改變引數有:

@property (assign, nonatomic) GLKVector3 modelPosition, modelRotate, modelScale;
@property (assign, nonatomic) GLKVector3 viewPosition , viewRotate , viewScale ;
@property (assign, nonatomic) GLfloat projectionFov, projectionScaleFix, projectionNearZ, projectionFarZ;

已經包含了所有的變換操作;

以下的幾個方法均是處理 VFRedisplay 類的實時更新問題;

// <VFRedisplayDelegate>
- (void)updateContentsWithTimes:(NSTimeInterval)times {
    
    [self preferTransformsWithTimes:times];
    [self drawAndRender];
    
}

#pragma mark - Update

- (void)update {
    
    self.displayUpdate = [[VFRedisplay alloc] init];
    self.displayUpdate.delegate = self;
    self.displayUpdate.preferredFramesPerSecond = 25;
    self.displayUpdate.updateContentTimes = arc4random_uniform(650) / 10000.0;
    [self.displayUpdate startUpdate];
    
}

- (void)pauseUpdate {
    
    [self.displayUpdate pauseUpdate];
    
}

#pragma mark - Dealloc

- (void)dealloc {
    
    [self.displayUpdate endUpdate];

}
    self.displayUpdate.preferredFramesPerSecond = 25; //更新頻率
    self.displayUpdate.updateContentTimes = arc4random_uniform(650) / 10000.0; // 控制變化率(快慢)

核心是 - (void)updateContentsWithTimes:(NSTimeInterval)times 方法,這個方法是用於更新時,實時呼叫的方法;由VFRedisplay 類提供的協議 @interface VFGLCubeView ()<VFRedisplayDelegate> 方法;

VFRedisplay.h 主要內容

@protocol VFRedisplayDelegate <NSObject>

- (void)updateContentsWithTimes:(NSTimeInterval)times;

@end

......

- (void)startUpdate;
- (void)pauseUpdate;
- (void)endUpdate;

VFRedisplay.m 主要內容
開始更新的方法:

- (void)startUpdate {
    
    if ( ! self.delegate ) {
        return;
    }
    
    self.displayLink = [CADisplayLink displayLinkWithTarget:self
                                                   selector:@selector(displayContents:)];
    
    self.displayLink.frameInterval = (NSUInteger)MAX(kLeastSeconds,
                                                     (kTotalSeconds / self.preferredFramesPerSecond));
    
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop]
                           forMode:NSDefaultRunLoopMode];
    
    self.displayPause = kDefaultDisplayPause;
    
}

- (void)displayContents:(CADisplayLink *)sender {
    
    if ([self.delegate respondsToSelector:@selector(updateContentsWithTimes:)]) {
        
        [self.delegate updateContentsWithTimes:self.updateContentTimes];
        
    }
    
}

四步走:

第一步,建立相應的更新排程方法- (void)displayContents:(CADisplayLink *)sender,這個方法必須是- (void)selector:(CADisplayLink *)sender這種型別的;

第二步,指定一個更新頻率(就是一秒更新多少次)frameInterval 一般是 24、25、30,預設是 30 的;

第三步,把 CADisplayLink 的子類新增到當前的 RunLoop [NSRunLoop currentRunLoop] 上,不然程式是無法排程指定的方法的;

第四步,啟動更新 static const BOOL kDefaultDisplayPause = NO;

displayPause 屬性

@property (assign, nonatomic) BOOL displayPause;
@dynamic displayPause;
- (void)setDisplayPause:(BOOL)displayPause {
   self.displayLink.paused = displayPause;
}
- (BOOL)displayPause {
   return self.displayLink.paused;
}

停止更新的方法:

- (void)pauseUpdate {
    
    self.displayPause = YES;
    
}

結束更新的方法:

- (void)endUpdate {
    
    self.displayPause = YES;
    [self.displayLink invalidate];
    [self.displayLink removeFromRunLoop:[NSRunLoop currentRunLoop]
                                forMode:NSDefaultRunLoopMode];
    
    
}

不用的時候,當然要先停止更新,再關掉時鐘(CADisplayLink 就是一個時鐘類),最後要從當前 RunLoop 中移除;

5. 工程檔案

Github: DrawCube

Github:DrawCube_Onestep

增加魔方色開關,RubikCubeColor 巨集定義;

1411747-9e404b7772177908.png
開關
1411747-cb59bbab7e3e1f8b.png
資料來源
1411747-c58111ca96daf0eb.gif
正方體_魔方色.gif

三、參考書籍、文章

《OpenGL ES 2 Programming Guide》
《OpenGL Programming Guide》8th
《Learning OpenGL ES For iOS》
RW.OpenGLES2.0

相關文章