OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始


目录

一、目标

1. 基础知识准备
2. 图形分析

二、编写程序

0. 工程结构与整体渲染管线
1. Depth Render Buffer
2. 数据源的编写与绑定
3. 深度测试与绘制
4. 让正方体动起来

三、参考书籍、文章


一、目标

正方体.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 类来专门做定时更新的工作,所以可以选用它进行运动更新;

OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第1张图片

二、编写程序

0. 工程结构与整体渲染管线

OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第2张图片

结构目录简述

  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 并使其工作的步骤:


OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第3张图片
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];
        
    }

步骤分解:


OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第4张图片
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 :

OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第5张图片

右下方,线框正方体的 8 个顶点坐标分布,其实 0~7 的编号是你决定的,也就是说 0 放在那里开始都是可以的,只要是 8 个点即可;
OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第6张图片
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 个顶点数据的下标;第二个因素是时钟方向;

现在看看时钟方向:


OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第7张图片

有没有发现,每一个正方形的两个小三角,都是逆时针方向的;当然你也可以换成顺时针方向,相应的下标数据就要发生改变;

EP: 如 Front 这个面,如果使用顺时针来写数据为:

    // Front  ------------- 白绿橙蓝 中间线(白橙)
    3, 2, 1, // 白绿橙
    1, 0, 2, // 橙蓝绿

你也可以从 2 或 1 开始,看你的喜好咯;

方向只有两个:


OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第8张图片

资源绑定
这里主要是 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. 深度测试与绘制

OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第9张图片
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 程序运行后是这样的:


关掉 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 .

OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第10张图片
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)]]; 的运行结果;

OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第11张图片

4. 让正方体动起来

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 类的实时更新问题;

// 
- (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 () 方法;

VFRedisplay.h 主要内容

@protocol VFRedisplayDelegate 

- (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 宏定义;

开关
数据源
OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始_第12张图片
正方体_魔方色.gif

三、参考书籍、文章

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

你可能感兴趣的:(OpenGL ES 2.0 (iOS)[05-1]:进入 3D 世界,从正方体开始)