2012年6月30日 星期六

Filter4Cam 實作: 17. 功能按鈕 UI 介面

since: 2012/06/30
update: 2012/06/30


A. 說明:
     1. 先將 "濾鏡" 以外的功能按鈕 UI 排好, 之後再一一實作, 先不考慮 "美觀"
         與 "程式碼架構(重整)".

     2. UI 上的功能, 目前有四個: "拍照", "鏡頭切換", "閃光燈", 與 "觀察者模式".
       
-----------------------------------------------------------------------------------

B. 開啟 ViewController.h 檔案, 修改如下:
// step 1:
....
@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate, UITableViewDelegate, UITableViewDataSource>
{
....
    //@add for Camera Overlay UI
    UIImageView *filterLensImageView;   
    UITableView *filterListTableView;
    UIButton *saveButton;   // "拍照" 按鈕
    UIButton *switchButton; // "鏡頭切換" 按鈕
    UIButton *torchButton;  // "閃光燈" 按鈕
    UIButton *observerButton; // "觀察者模式" 按鈕
....
}

// step 2:
....
//@add for Camera Overlay UI
@property (nonatomic, strong) UIImageView *filterLensImageView;
@property (nonatomic, strong) UITableView *filterListTableView;
@property (nonatomic, strong) UIButton *saveButton;
@property (nonatomic, strong) UIButton *switchButton;
@property (nonatomic, strong) UIButton *torchButton;
@property (nonatomic, strong) UIButton *observerButton;
....

// step 3:
....
//@add: functional buttons 
- (void)savePhoto; // "拍照"
- (void)switchCameras; // "鏡頭切換"
- (void)switchLight; // "閃光燈切換"
- (void)switchObserver; // "觀察者模式"

@end

-----------------------------------------------------------------------------------

C. 開啟 ViewController.m 檔案, 修改如下:
// step 1:
....
//@add for Camera Overlay UI
@synthesize filterLensImageView = _filterLensImageView;
@synthesize filterListTableView = _filterListTableView;
@synthesize saveButton = _saveButton;
@synthesize switchButton = _switchButton;
@synthesize torchButton = _torchButton;
@synthesize observerButton = _observerButton;

// step2:
....
//@add: "拍照" 按鈕 
- (UIButton *)saveButton
{
    if (_saveButton == nil) {
        _saveButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        [_saveButton setTitle:@"Save" forState:UIControlStateNormal];
        [_saveButton addTarget:self action:@selector(savePhoto) forControlEvents:UIControlEventTouchUpInside];

        [_saveButton setOpaque:NO];
        [_saveButton setAlpha:0.7];
    }
   
    return _saveButton;
}

//@add: "鏡頭切換" 按鈕
- (UIButton *)switchButton
{
    if (_switchButton == nil) {
        _switchButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        [_switchButton setTitle:@"Switch" forState:UIControlStateNormal];
        [_switchButton addTarget:self action:@selector(switchCameras) forControlEvents:UIControlEventTouchUpInside];

        [_switchButton setOpaque:NO];
        [_switchButton setAlpha:0.7];
    }

    return _switchButton;
}

//@add: "閃光燈切換" 按鈕
- (UIButton *)torchButton
{
    if (_torchButton == nil) {
        _torchButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        [_torchButton setTitle:@"Torch" forState:UIControlStateNormal];
        [_torchButton addTarget:self action:@selector(switchLight) forControlEvents:UIControlEventTouchUpInside];

        [_torchButton setOpaque:NO];
        [_torchButton setAlpha:0.7];
    }

    return _torchButton;
}

//@add: "觀察者模式" 按鈕
- (UIButton *)observerButton
{       
    if (_observerButton == nil) {
        _observerButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        [_observerButton setTitle:@"Observe" forState:UIControlStateNormal];
        [_observerButton addTarget:self action:@selector(switchObserver) forControlEvents:UIControlEventTouchUpInside];

        [_observerButton setOpaque:NO];
        [_observerButton setAlpha:0.7];
    }

    return _observerButton;
}

// step 03:
....
- (void)cameraOverlay
{  
    [self.view addSubview:self.filterLensImageView];
    [self.view addSubview:self.saveButton];
    [self.view addSubview:self.switchButton];
    [self.view addSubview:self.torchButton];
    [self.view addSubview:self.observerButton];
....
}

// step 04:
....
//@add: "拍照"
- (void)savePhoto
{
    //@TODO
    NSLog(@"savePhoto");
}

//@add: "鏡頭切換"
- (void)switchCameras
{
    //@TODO
    NSLog(@"switchCameras");
}

//@add: "閃光燈切換"
- (void)switchLight
{
    //@TODO
    NSLog(@"switchLight");
}

//@add: "觀察者模式"
- (void)switchObserver
{
    //@TODO
    NSLog(@"switchObserver");
}

....
// step 05:
....
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
....
    //@update
    // 設備垂直擺放
    if (interfaceOrientation == UIDeviceOrientationPortrait)
    {
        [self.filterLensImageView setFrame:CGRectMake(20, 120, 280, 280)];
       
        //@add for Button
        [self.saveButton setFrame:CGRectMake(10, 420, 66, 40)];
        [self.switchButton setFrame:CGRectMake(87, 420, 66, 40)];
        [self.torchButton setFrame:CGRectMake(164, 420, 66, 40)];
        [self.observerButton setFrame:CGRectMake(241, 420, 66, 40)];
....
    }
    // 設備垂直 180 度擺放
    else if (interfaceOrientation == UIDeviceOrientationPortraitUpsideDown) {       
        [self.filterLensImageView setFrame:CGRectMake(20, 120, 280, 280)];
       
        //@add for Button
        [self.saveButton setFrame:CGRectMake(10, 420, 66, 40)];
        [self.switchButton setFrame:CGRectMake(87, 420, 66, 40)];
        [self.torchButton setFrame:CGRectMake(164, 420, 66, 40)];
        [self.observerButton setFrame:CGRectMake(241, 420, 66, 40)];
....
    }
    // 設備水平向右擺放
    else if (interfaceOrientation == UIDeviceOrientationLandscapeRight) {       
        [self.filterLensImageView setFrame:CGRectMake(120, 20, 280, 280)];
       
        //@add for Button
        [self.saveButton setFrame:CGRectMake(406, 32, 66, 40)];
        [self.switchButton setFrame:CGRectMake(406, 104, 66, 40)];
        [self.torchButton setFrame:CGRectMake(406, 176, 66, 40)];
        [self.observerButton setFrame:CGRectMake(406, 248, 66, 40)];
....
    }
    // 設備水平向左擺放
    else if (interfaceOrientation == UIDeviceOrientationLandscapeLeft) {       
        [self.filterLensImageView setFrame:CGRectMake(120, 20, 280, 280)];
       
        //@add for Button
        [self.saveButton setFrame:CGRectMake(406, 32, 66, 40)];
        [self.switchButton setFrame:CGRectMake(406, 104, 66, 40)];
        [self.torchButton setFrame:CGRectMake(406, 176, 66, 40)];
        [self.observerButton setFrame:CGRectMake(406, 248, 66, 40)];
....
    }
    // 其它 (UIDeviceOrientationFaceUp, UIDeviceOrientationFaceDown,
    // UIDeviceOrientationUnknown)

    else {
        [self.filterLensImageView setFrame:CGRectMake(120, 20, 280, 280)];
       
        //@add for Button
        [self.saveButton setFrame:CGRectMake(406, 32, 66, 40)];
        [self.switchButton setFrame:CGRectMake(406, 104, 66, 40)];
        [self.torchButton setFrame:CGRectMake(406, 176, 66, 40)];
        [self.observerButton setFrame:CGRectMake(406, 248, 66, 40)];
....
    }
....
}

....

-----------------------------------------------------------------------------------

D. 編譯並執行

2012年6月25日 星期一

OpenGL ES 2.0 with GLKit Part 2 - 2


since: 2012/06/25
update: 2012/06/25


reference:
1. Beginning OpenGL ES 2.0 with GLKit Part 2 | Ray Wenderlich
2. I touchs: OpenGL ES 2.0 with GLKit Part 2 - 1

GLKBaseEffect 實作

A. 建立一個 GLKBaseEffect
     開啟 HelloGLKitViewController.m 檔案, 修改如下:
....
// step 1:
@interface HelloGLKitViewController ()
{
    //@add
    float _curRed;
    BOOL _increasing;
    GLuint _vertexBuffer;
    GLuint _indexBuffer;
    GLKBaseEffect *effect;
    float _rotation;

}

//@add
@property (strong, nonatomic) EAGLContext *context;
@property (strong, nonatomic) GLKBaseEffect *effect;

@end

....
// step 2:
- (void)setupGL
{
    // Set the current OpenGL context to the current context.
    [EAGLContext setCurrentContext:self.context];
   
    //@add for initialize GLKBaseEffect
    self.effect = [[GLKBaseEffect alloc] init]; 
....
}

....
// step 3:
- (void)tearDownGL
{
    [EAGLContext setCurrentContext:self.context];
   
    glDeleteBuffers(1, &_vertexBuffer);
    glDeleteBuffers(1, &_indexBuffer);
   
    //@add
    self.effect = nil;
}

....

----------------------------------------------------------------------------------

B. 設定 GLKBaseEffect 屬性
     1. 說明:
          a. 第一個需要設定的屬性為: 投影矩陣. 投影矩陣是用來告訴 CPU 如何在 2D
              的平面上繪製出 3D 的幾何圖形.

          b. GLKit 提供了一些便利的函式來設定投影矩陣. 我們將使用其中一個來設定:
              (1). 沿著 Y 軸的視野角度 (fovy: field of view along the y-axis),
                     即上下二個平面的.
              (2). 寬與高的比例 (aspect ratio: w / h)
              (3). 近端與遠端平面 (near and far planes)

          c. Field of view 就像是相機的鏡頭. 較小的 field of view (例如: 10), 類似攝遠
              鏡頭 - 藉由將圖片向你拉近而使圖片放大. 較大的 field of view (例如: 100),
              類似廣角鏡頭 - 它讓所有物體看起來較遠. 一般設定 Field of view 的值介於:
              65 - 75 之間.

          d. Aspect ratio 是你想要繪製的方向比例(亦即, view 的方向比例). 它與 field
              of view 結合使用, 來決定沿著 X 軸的視野大小.

          e. 近端與遠端平面, 是場景中用來框住可視空間的容量邊界. 所以, 如果某個
              物體與眼睛(相機)的距離過近於近端平面或過遠於遠端平面, 它就不會被
              繪製出來. 這是一個普遍可能會遇到的問題 - 你在繪製一個物體, 但是它沒有
              在螢幕上顯示出來. 記得要去檢查: 這個物體是否位於 near plane 與 far plane
              之間.

********************************************************

     2. 開啟 HelloGLKitViewController.m 檔案, 修改如下:
       // 設定投影矩陣
....
- (void)update
{
    ....

    //@add: set the projection matrix
    //
    // 1. get the aspect ratio of the GLKView.
    float aspect = fabsf(self.view.bounds.size.width / self.view.bounds.size.height);
   
    // 2. Create a perspective matrix: (with GLKit math library)
    //
    // We set the near plane to 4 units away from the eye, and the far plane
    // to 10 units away.

    GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0f), aspect, 4.0f, 10.0f);
   
    // 3. Set the projection matrix on the effect's transform property
    self.effect.transform.projectionMatrix = projectionMatrix;
}
....

********************************************************

     3. 設定 modelView 矩陣
         a. 說明:
             ModelViewMatrix 是要繪製的幾何物體效果都需要套用的轉換.
             在此, 又藉由 GLKit 數學函式庫的協助, 來執行位移, 旋轉縮放的功能.

         b. 開啟 HelloGLKitViewController.m 檔案, 修改如下: 
....
- (void)update
{
    ....
    //@add: set the projection matrix
    ....
   
    //@add: set the modelViewMatrix
    //
    // 1. Move z-coordinate backwards.
    //    Create a matrix for us that translates 6 units(Z) backwards.
    //
    // Because when we set up the vertices for the square, the z-coordinate
    // for each vertex was 0.

    GLKMatrix4 modelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, -6.0f); 
   
    // 2. Increment rotate variable
    _rotation += 90 * self.timeSinceLastUpdate;
   
    // 3. Rotating it
    modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, GLKMathDegreesToRadians(_rotation), 0, 0, 1);
   
    // 4. Set the model view matrix on the effect's transform property.
    self.effect.transform.modelviewMatrix = modelViewMatrix;
}
....

----------------------------------------------------------------------------------

C. 在 GLKBaseEffect 上呼叫 prepareToDraw
     1. 說明:
         在將物體繪製到螢幕之前, 任何時候你在 GLKBaseEffect 上更改了屬性設定,
         都需要先呼叫此方法.

     2. 開啟 HelloGLKitViewController.m 檔案, 修改如下: 
....
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    glClearColor(_curRed, 0.0, 0.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
   
    //@add
    [self.effect prepareToDraw];
}
....

----------------------------------------------------------------------------------

D. 啟用 "預先定義好的" 的屬性 
     1. 開啟 HelloGLKitViewController.m 檔案, 修改如下: 
....
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    glClearColor(_curRed, 0.0, 0.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
   
    //@add
    [self.effect prepareToDraw];
   
    //@add: Enable pre-defined attributes
    //
    // 1. bind the vertex and index buffers
    //
    // Every time before you draw, you have to tell OpenGL which vertex buffer objects
    // you should use.
So here we bind the vertex and index buffers we created earlier.
    //
    // Strictly, we didn't have to do this for this app (because they're already still bound
    // from before)
but usually you have to do this because in most games you use
    // many different vertex buffer objects.

    glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer);
   
    // 2. Enable the pre-defined vertex attributes (position)
    //    and feed the correct values to input variables for the vertex shader.
    glEnableVertexAttribArray(GLKVertexAttribPosition);       

    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid *) offsetof(Vertex, Position));
   
    // 3. Enable the pre-defined vertex attributes (color)
    //    and feed the correct values to input variables for the vertex shader.
    glEnableVertexAttribArray(GLKVertexAttribColor);

    glVertexAttribPointer(GLKVertexAttribColor, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid *) offsetof(Vertex, Color));
}
....

********************************************************

     2. glVertexAttribPointer 參數說明:

        void glVertexAttribPointer(GLuint  index
                                   GLint  size
                                   GLenum  type
                                   GLboolean  normalized
                                   GLsizei  stride
                                   const GLvoid *pointer);

       a. index:
           即將被設定的 vertex shaderattribute 之索引位置,
           在此, 我們使用 GLKit 提供的 "預先定義好的" 常數值.
            (若不使用 GLKit 的話, 需呼叫 glGetAttribLocation 來取得)

       b. size:
           每個頂點屬性元素數量, 在此為:位置 3 個(x,y,z); 顏色 4 個(r,g,b,a).

       c. type:
           每個元素的資料型別, 在此, 位置顏色皆為 GL_FLOAT.

       d. normalized:
           是否要對頂點進行正規化處理, 在此為否: GL_FALSE

       e. stride:
           跨度: 二個連續頂點屬性之間的位元偏移量
           亦即, 包含個別頂點資料的資料結構之大小, 在此皆為: sizeof(Vertex)

       f. pointer:
           在儲存該屬性的資料結構中, 此屬性資料的起始偏移量
           在此, 可以使用 offsetof 運算子, 來找出在一個結構體中, 特定資料欄位
           起始偏移量.

           備註: 不然的話, 就要依照 struct 的內容來計算:  
            typedef struct {
            float Position[3];
            float Color[4];
            } Vertex;

            => Position 資料是位於 struct 的起始處, 因此偏移量為: 0.
            => Color 資料是位於 Position 資料之後, 因此偏移量為: sizeof(float) * 3.

----------------------------------------------------------------------------------

E. 繪製幾何圖形
     1. 開啟 HelloGLKitViewController.m 檔案, 修改如下: 
....
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    glClearColor(_curRed, 0.0, 0.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
   
    //@add
    [self.effect prepareToDraw];
   
    //@add: Enable pre-defined attributes
    ....
   
    //@add: Draw geometry
    glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]), GL_UNSIGNED_BYTE, 0);
}
....

     2. glDrawElements 參數說明:

        void glDrawElements(GLenum  mode
                           GLsizei  count
                           GLenum  type
                           const GLvoid *indices);

        a. mode:
            要繪製的基本圖形種類.

        b. count:
            要繪製的頂點數量.

        c. type:
            索引陣列中, 每個個別索引資料型別(GLubyte)

        d. indices:
            指向索引陣列的指標.

            備註:
            在此為特例, 由於使用了 Vertex Buffer Objects, 因此它會使用
            在 glkView:drawInRect: 函式中與 GL_ELEMENT_ARRAY_BUFFER 綁定,
            儲存在 Index Buffer object 裡的 Indices 陣列.

----------------------------------------------------------------------------------

F. 編譯並執行:
     可以在螢幕上看到一個漂亮的旋轉方塊.
     (背景仍然是: 脈動式的 "緊急警報" 效果: 黑 -> 漸紅 -> 紅)

OpenGL ES 2.0 with GLKit Part 2 - 1

since: 2012/06/25
update: 2012/06/25


reference:
1. Beginning OpenGL ES 2.0 with GLKit Part 2 | Ray Wenderlich
2. I touchs: OpenGL ES 2.0 with GLKit Part 1 - 2

GLKBaseEffect 介紹

A. 建立簡單的正方形頂點資料
     1. 說明:
         a. 正方形的頂點將被設置如下:
             V0: 座標(1, -1), 顏色:
             V1: 座標(1, 1), 顏色:
             V2: 座標(-1, 1), 顏色:
             V3: 座標(-1, -1), 顏色:


         b. 在 OpenGL ES 中, 只能使用三角形來繪製幾何圖形, 所以我們可以利用
              二個三角形來產生正方形, 如上圖所示: 一個三角形使用頂點 V0, V1, V2;
              另一個三角形使用頂點 V2, V3, V0.
              (說明: 你可以將頂點資料組成任何產生三角形的方式)

     2. 開啟 HelloGLKitViewController.m 檔案, 修改如下:
#import "HelloGLKitViewController.h"


///////////////////////////////////////////////////////
//@start: add for vertex data */

// 用來儲存每個頂點的資訊: 位置(x,y,z), 顏色(r,g,b,a)
typedef struct {
    float Position[3];
    float Color[4];
} Vertex;

// 所有個別頂點的資訊:
// {位置1, 顏色1}, {位置2, 顏色2}, {位置3, 顏色3}, {位置4, 顏色4}
const Vertex Vertices[] = {
    {{1, -1, 0}, {1, 0, 0, 1}},
    {{1, 1, 0}, {0, 1, 0, 1}},
    {{-1, 1, 0}, {0, 0, 1, 1}},
    {{-1, -1, 0}, {0, 0, 0, 1}}
};

// 產生三角形的頂點索引:
//
// 每三個頂點組成一個三角形:
// 第一個三角形: 由頂點索引為 0, 1 與 2 所組成
// 第二個三角形: 由頂點索引為 2, 3 與 0 所組成
const GLubyte Indices[] = {
    0, 1, 2,
    2, 3, 0
};

//@ end: add for vertex data
///////////////////////////////////////////////////////
....

----------------------------------------------------------------------------------

B. 建立 Vertex Buffer Objects

     1. 說明:
         a. 要將頂點資料送至 OpenGL, 最佳方式是經由頂點緩衝區物件
              (Vertex Buffer Objects).

         b. 根本上來說, Vertex Buffer Objects 是一種 OpenGL 物件, 用來將頂點資料
              儲存到緩衝區裡.

         c. 有二種類型的 vertex buffer objects, 一種用來保存(追蹤)每個頂點的資料
             (像之前的 Vertices 陣列); 另一種用來保存(追蹤)建立三角形的頂點索引
             (像之前的 Indices 陣列).

     2. 開啟 HelloGLKitViewController.h 檔案, 修改如下:
#import <GLKit/GLKit.h>

@interface HelloGLKitViewController : GLKViewController
{
}

//@add
- (void)setupGL;
- (void)tearDownGL;

@end

     3. 開啟 HelloGLKitViewController.m 檔案, 修改如下:
....
// step 1:
@interface HelloGLKitViewController ()
{
    //@add
    float _curRed;
    BOOL _increasing;
    GLuint _vertexBuffer;
    GLuint _indexBuffer;
}

....
// step 2:
//@add
- (void)setupGL
{
    // Set the current OpenGL context to the current context.
    [EAGLContext setCurrentContext:self.context];
   
    // Create a new Vertex Buffer object
    glGenBuffers(1, &_vertexBuffer);
   
    // Bind vertexBuffer to GL_ARRAY_BUFFER  
    glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
   
    // send the data over to OpenGL-land.
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
   
    glGenBuffers(1, &_indexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
}

....
// step 3:
//@add: delete the vertex and index buffers:
- (void)tearDownGL
{
    [EAGLContext setCurrentContext:self.context];
   
    glDeleteBuffers(1, &_vertexBuffer);
    glDeleteBuffers(1, &_indexBuffer);
}

....
// step 4:
- (void)viewDidLoad
{
....
    //@add
    [self setupGL];
}

....
// step 5:
- (void)viewDidUnload
{
    [super viewDidUnload];
    //add
    [self tearDownGL];
    ....
}

....

----------------------------------------------------------------------------------

C. GLKBaseEffect 介紹

      1. 說明:
          a. 在 OpenGL ES 2.0 中, 要在場景繪製出任何的幾何圖形, 必須要建立二個
              稱為 shader 的微小程式.
              註: shader 常翻譯為著色器, 對 Fragment(像素/片段) shader 而言還算吻合;
                    但是對於主要處理頂點位置的 vertex(頂點) shader 就不貼切了.

          b. Shaders 以類似 C 語言的語法被寫成, 稱作 GLSL(OpenGL Shading Language),
              有二種基本類型的 shaders: Vertex shaderFragment shader.

          c. Vertex shader:(頂點著色器)
              在場景中的每個頂點都會呼叫一次 vertex shader 這個小程式. Vertex shader
              的工作是去執行某些計算, 例如: 光源位置, 幾何轉換(位移, 旋轉, 縮放)等,
              最後會算出每個頂點最終的位置, 並且傳遞一些資料給 fragment shader.

          d. Fragment shader:(像素著色器)
              同樣地, 在場景中的每個像素都會呼叫一次 fragment shader 這個小程式.
              Fragment shader 最主要的工作是設定每個像素最終的顏色.


          e. GLKBaseEffect 是一個輔助類別, 它為你實作了一些常用的 shaders.
              GLKBaseEffect 的目的是: 提供大部份在 OpenGL ES 1.0 中可用的功能,
              使 App 較容易從 OpenGL ES 1.0 轉成 OpenGL ES 2.0.

----------------------------------------------------------------------------------

D. GLKBaseEffect 的使用步驟
      1. 建立一個 GLKBaseEffect
          通常, 當你建立 OpenGL context 時, 就會建立其中一個 GLKBaseEffect. 你能夠
          (並且應該) 對不同的幾何圖形再使用相同的 GLKBaseEffect, 並且只需重新設定
          屬性. 在場景的背後, GLKBaseEffect 只會散佈對 shaders 有所改變的那些屬性.

      2. 設定 GLKBaseEffect 屬性
          你可以設定: 光源, 轉換(位移, 旋轉, 縮放), 以及其它 GLKBaseEffect 的 shaders
          會用來繪製幾何圖形的屬性.

      3. 在 GLKBaseEffect 上呼叫 prepareToDraw
          任何時候, 你在 GLKBaseEffect 上更改一個屬性, 你需要先呼叫 prepareToDraw
          來讓 shaders 能正確地設定. 這也會賦予 GKBaseEffect 的 shaders 成為目前的
          shader program.

      4. 啟用 "預先定義好的" 的屬性
          通常, 當你建好你自己的 shaders, 它們帶有稱為 attributes參數, 並且你可以
          在 App 中寫程式碼來取得 attributes 參數的 IDs. 對 GLKBaseEffect 內建
          shaders 而言, 那些 attributes 參數是已經 "定義好的" 常數, 如: 
          GLKVertexAttribPositionGLKVertexAttribColor. 所以你需要啟用任何想要
          傳遞到 shaders 裡的參數, 並且將這些參數參照到資料.          

      5. 繪製你的幾何圖形
          一旦你都設定好之後, 就可以使用標準的 OpenGL 繪圖指令, 如: glDrawArrays
          或 glDrawElements, 它就會依照你設定的效果來繪製.  

----------------------------------------------------------------------------------

E. 備註:
      1. GLKBaseEffect 最棒的一件事就是: 如果你使用它們, 你就不需要寫任何的
          shaders. 當然, 如果你想要也可以用混搭的方式: 用 GLKBaseEffect 來繪製
          某些圖形; 再用你自己寫的 shaders 繪製另外的圖形. 如果你看看 OpenGL
          的樣板專案, 就會看到這樣的例子.

     2. 在這篇教學文章中, 會將焦點放在只使用 GLKBaseEffect, 因為主要的目的是
          讓你能夠儘快的使用新的 GLKit 功能, 它是相當容易的.

2012年6月24日 星期日

OpenGL ES 2.0 with GLKit Part 1 - 2

since: 2012/06/24
update: 2012/06/24


reference:
1.
Beginning OpenGL ES 2.0 with GLKit Part 1 | Ray Wenderlich
2. I touchs: OpenGL ES 2.0 with GLKit Part 1 - 1

GLKViewController

A. GLKViewController 介紹
      1. 說明:
          我們可以使用更容易的方式來完成之前所作的事, 使用: GLKViewController.
          先前, 之所以使用 GLKView, 是讓我們可以了解使用 GLKViewController
          背後的重點, 並節省了撰寫那些程式碼, 還可進一步自行撰寫你想要的
          額外功能.

      2. 開啟 AppDelegate.h 檔案, 修改如下:
....
//@interface AppDelegate : UIResponder <UIApplicationDelegate, GLKViewDelegate>
//@update for set the GLKViewController's delegate to the current class
@interface AppDelegate : UIResponder <UIApplicationDelegate, GLKViewDelegate, GLKViewControllerDelegate>
{
    //@add
    float _curRed;
    BOOL _increasing;
}
....
//@add

//@update: comment it
//- (void)render:(CADisplayLink *)displayLink;
....

      3. 開啟 AppDelegate.m 檔案, 修改如下:
....
// step 1
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    //@add
    _increasing = YES;
    _curRed = 0.0;
       
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    //@add for GLKView
    //
    // step 1: Create a OpenGL context
    //
    // 1-1: An EAGLContext manages all of the information iOS needs to draw with OpenGL.
    // 1-2: And specify what version of the API you want to use.(Here, OpenGL ES 2.0)
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
   
    // step 2: Create a GLKView
    //
    // This creates a new instance of a GLKView, and makes it as large as the entire window.
    GLKView *view = [[GLKView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
   
    // step 3: Set the GLKView's context
    //
    // Tell GLKView to use the OpenGL context.
    view.context = context;
   
    // step 4: Set the GLKView's delegate
    //
    // 4-1: This sets the current class (AppDelegate) as the GLKView's delegate.
    // 4-2: This means whenever the view needs to be redrawn, it will call
    //      a method named glkView:drawInRect on whatever class you specify here.
    //      We will implement this inside the App Delegate shortly to contain
    //      some basic OpenGL commands to paint the screen red.
    view.delegate = self;
   
    //@add: for synchronize
    //@update: comment it
    /*
    view.enableSetNeedsDisplay = NO;
    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render:)];
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    */
   
    // step 5: Add the GLKView as a subview
    //@update: comment it
    //[self.window addSubview:view];
   
   
    // step 6: for GLKViewController
    //
    // 6-1: Create a GLKViewController
    //
    // This creates a new instance of a GLKViewController programatically.
    //
    // In this case, it has no XIB associated.
    GLKViewController *viewController = [[GLKViewController alloc] initWithNibName:nil bundle:nil];
   
    // 6-2: Set the GLKViewController's view
    //
    // The root view of a GLKViewController should be a GLKView,
    // so we set it to the one we already created.
    viewController.view = view;
   
    // 6-3: Set the GLKViewController's delegate
    //
    // We set the current class (AppDelegate) as the delegate of the GLKViewController.
    //
    // This means that the GLKViewController will notify us each frame
    // so we can run game logic, or when the game pauses.
    viewController.delegate = self;
   
    // 6-4: Set the preferred FPS
    //
    // The GLKViewController will call your draw method a certain number of times
    // per second.

    //
    // The default value is 30 FPS. Apple's guidelines are to set this to whatever your app
    // can reliably support to the frame rate is consistent and doesn't seem to stutter.
    //
    // This app is very simple so can easily run at 60 FPS, so we set it to that.
    //
    // If you want to see the actual number of times the OS will attempt to call your
    // update/draw methods, check the read-only framesPerSecond property.
    viewController.preferredFramesPerSecond = 60;
   
    // 6-5: Set the rootViewController
    //
    // We want this view controller to be the first thing that shows up,
    // so we add it as the rootViewController of the window.
    //
    // We no longer need to add the view as a subview of the window manually,
    // because it's the root view of the GLKViewController.
    //
    // We no longer need the code to run the render loop and tell the GLView
    // to refresh each frame –
GLKViewController does that for us in the background!
    // So go ahead and comment out the render method as well.
    self.window.rootViewController = viewController;
   
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

....
// step 2
//@update: comment it

/*
- (void)render:(CADisplayLink *)displayLink
{
    GLKView *view = [self.window.subviews objectAtIndex:0];
    [view display];
}
*/

....
// step 3
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    //@add
    //@update: comment it
    /*
    if (_increasing) {
        _curRed += 0.01;
    } else {
        _curRed -= 0.01;
    }
    if (_curRed >= 1.0) {
        _curRed = 1.0;
        _increasing = NO;
    }
    if (_curRed <= 0.0) {
        _curRed = 0.0;
        _increasing = YES;
    }
    */
   
    // specify the RGB and alpha (transparency) values to use when clearing the screen.
    //glClearColor(1.0, 0.0, 0.0, 1.0);
    //@update
    glClearColor(_curRed, 0.0, 0.0, 1.0);
   
    // actually perform the clearing.
    glClear(GL_COLOR_BUFFER_BIT);
}

....
// step 4
#pragma mark - GLKViewControllerDelegate
//@add for implement delegate method
- (void)glkViewControllerUpdate:(GLKViewController *)controller
{
    // property: timeSinceLastUpdate
    //
    // it guarantees the animation will always proceed at the same speed,
    // regardless of the frame rate.
    if (_increasing) {
        _curRed += 1.0 * controller.timeSinceLastUpdate;
    } else {
        _curRed -= 1.0 * controller.timeSinceLastUpdate;
    }
    if (_curRed >= 1.0) {
        _curRed = 1.0;
        _increasing = NO;
    }
    if (_curRed <= 0.0) {
        _curRed = 0.0;
        _increasing = YES;
    }
}
....

      4. 編譯並執行:
          結果與之前相同

--------------------------------------------------------------------------------

B. GLKViewController 與 Storyboards
      1. 新增 GLKViewController 子類別檔案:
          Xcode > File > New > New File...
          iOS > Cocoa Touch > Objective-C class > Next

          Class: HelloGLKitViewController
          Subclass of: GLKViewController (如果無法選取到, 就自行輸入)
          > Next > Create

****************************************************

      2. 開啟 HelloGLKitViewController.m 檔案, 修改如下:
....
// step 1:
@interface HelloGLKitViewController ()
{
    //@add
    float _curRed;
    BOOL _increasing;
}

//@add
@property (strong, nonatomic) EAGLContext *context;

@end

@implementation HelloGLKitViewController

//@add
@synthesize context = _context;

....
// step 2:
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //@add   
    // 1. create an OpenGL ES 2.0 context
    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
   
    if (!self.context) {
        NSLog(@"Failed to create ES context");
    }
   
    // 2. Our root view is a GLKView, so we cast it as one.
    //
    // We then set its context to the OpenGL context we just created.
    GLKView *view = (GLKView *)self.view;
    view.context = self.context;
   
    // 3. Note
    //
    // We don't have to set the view controller as the view's delegate
    // – GLKViewController does this automatically behind the scenes.
}

....
// step 3:
- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    //@add
    //
    // we check to see if the current context is our context
    if ([EAGLContext currentContext] == self.context) {
        // set it to nil if so
        [EAGLContext setCurrentContext:nil];
    }
   
    // clear out your reference to it.
    self.context = nil;
}

....
// step 4:
#pragma mark - GLKViewDelegate
//@add
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    glClearColor(_curRed, 0.0, 0.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
}

....
// step 5:
#pragma mark - GLKViewControllerDelegate
//@add
- (void)update
{
    if (_increasing) {
        _curRed += 1.0 * self.timeSinceLastUpdate;
    } else {
        _curRed -= 1.0 * self.timeSinceLastUpdate;
    }
    if (_curRed >= 1.0) {
        _curRed = 1.0;
        _increasing = NO;
    }
    if (_curRed <= 0.0) {
        _curRed = 0.0;
        _increasing = YES;
    }
}
....

****************************************************

      3. 新增 Storyboard 檔案:
          Xcode > File > New > New File...
          iOS > User Interface > Storyboard > Next
          Device Family: iPhone
          Save As: MainStoryboard.storyboard
          > Create

****************************************************

      4. 開啟 MainStoryboard.storyboard 檔案, 修改如下:
          a. 從右方的 Objects panel 拖拉一個 View Controller 進來:

          b. 先選取 UI 上的 View Controller, 再點選右上方的 Identity Inspector
              將 Class 由 UIViewController 改選為: HelloGLKitViewController.

          c. 同樣地, 選取 UI 上的 View Controller 裡的 View, 再點選右上方的
              Identity Inspector, 將 Class 由 UIView 改選為: GLKView.

      5. 開啟 HelloGLKit-Info.plist 檔案, 修改如下:
          a. 在下方空白的地方按下: control + 滑鼠左鍵 > Add Row

          b. 選擇並設定:
              Key: Main storyboard file base name
              Type: String
              Value: MainStoryboard

****************************************************

       6. 移除不再使用的程式碼:
           a. 開啟 AppDelegate.h 檔案, 修改如下:  
#import <UIKit/UIKit.h>
//@add
//#import <GLKit/GLKit.h>
//#import <QuartzCore/QuartzCore.h> // used for CADisplayLink

@interface AppDelegate : UIResponder <UIApplicationDelegate>
//@update for set as GLKView's delegate
//@interface AppDelegate : UIResponder <UIApplicationDelegate, GLKViewDelegate>
//@update for set the GLKViewController's delegate to the current class
//@interface AppDelegate : UIResponder <UIApplicationDelegate, GLKViewDelegate, GLKViewControllerDelegate>
{
    //@add
    /*
    float _curRed;
    BOOL _increasing;
    */
}

@property (strong, nonatomic) UIWindow *window;

//@add
//@update: comment it
//- (void)render:(CADisplayLink *)displayLink;

@end

           b. 開啟 AppDelegate.m 檔案, 修改如下:
....
// step 1:
#pragma mark - GLKViewDelegate
/*

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
....
}
*/

....
// step 2:
#pragma mark - GLKViewControllerDelegate
/*

- (void)glkViewControllerUpdate:(GLKViewController *)controller
{
....
}
*/

....

// step 3:
/*
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
....
}
*/
//@update
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    return YES;
}

****************************************************

       7. 編譯並執行:
           結果與之前相同.
           備註: 到目前為止的專案架構, 已經與你選擇用 OpenGL Game 樣板來新增
                     專案非常類似了(除了有一大堆其它你可以刪除的程式碼), 以後就可以
                     選擇 OpenGL Game 樣板來建立新專案了, 這可以省下一點時間. 

--------------------------------------------------------------------------------

C. GLKViewController 與暫停
      1. 開 HelloGLKitViewController.m 檔案, 修改如下:
....
//@add
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.paused = !self.paused;
}
....

      2. 編譯並執行:
          任何時間, 輕觸螢幕, 便會停止動畫或繼續執行動畫. 在場景後面的機制,
          是 GLKViewController 停止 / 繼續 呼叫 glkView:drawInRect:update 方法.

      3. 除此之外, GLKViewController 有一個 pauseOnWillResignActive 屬性,
          預設值為 YES, 代表, 當使用者按下 home 按鈕或者接受到一個中止的訊號,
          例如有某人來電, 你的遊戲會自動地暫停. 類似地, 它也有一個
          resumeOnDidBecomeActive 屬性, 預設值為 YES, 代表當使用者回到你的 App ,
          遊戲就會自動繼續, 真是方便.    

      4. GLKViewController 的其它 "時間資訊" 屬性:
           a. timeSinceLastDraw
               從最近一次呼叫 draw 方法, 到目前的時間. 這可能會與 timeSinceLastUpdate
               不同, 因為執行 update 方法需要花一些時間.  

           b. timeSinceFirstResume 
               從 GLKViewController 第一次重新開始發送更新訊息, 到目前的時間.
               如果你的 GLKViewController 是第一個會展現的東西, 這通常代表從你的
               App 啟動(launched) 到目前的時間.

           c. timeSinceLastResume
               最近一次 GLKViewController 重新開始發送更新訊息, 到目前的時間.
               這通常代表從你的 App 最近一次從暫停恢復執行(unpaused) 到目前的時間.

      5. 開啟 HelloGLKitViewController.m 檔案, 修改如下:
....
//@add
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //@add for time info
    NSLog(@"timeSinceLastUpdate: %f", self.timeSinceLastUpdate);
    NSLog(@"timeSinceLastDraw: %f", self.timeSinceLastDraw);
    NSLog(@"timeSinceFirstResume: %f", self.timeSinceFirstResume);
    NSLog(@"timeSinceLastResume: %f", self.timeSinceLastResume);
   
    self.paused = !self.paused;
}
....

      6. 編譯並執行:
          按 home 鍵 > 回復 > 輕觸螢幕: 

HelloGLKit[1421:fb03] timeSinceLastUpdate: 0.033285
HelloGLKit[1421:fb03] timeSinceLastDraw: 0.033285
HelloGLKit[1421:fb03] timeSinceFirstResume: 7.780801
HelloGLKit[1421:fb03] timeSinceLastResume: 0.991113