顯示具有 Core Graphics 標籤的文章。 顯示所有文章
顯示具有 Core Graphics 標籤的文章。 顯示所有文章

2012年3月10日 星期六

Filter4Cam 學習之 Graphics 小記

since: 2012/03/02
update: 2012/03/02

reference: Developing iPad Applications for Visualization and Insight
                    => Lecture 12: Graphics

   1. Graphics Stack

   2. Blitting
        blit = Bit-Block Image Transfer (位元區塊影像轉換)

   3. CoreGraphics is a Vector graphics API
       Coordinate system:


   4. CoreGraphics is a CoreFoundation API
       a. CoreFoundation takes the ideas of Objective-C and applies them to C
           (p.s. CoreFoundation is a C API; Foundation is a Objective-C API )

       b. Memory management:
          (1). CFRetain / CFRelease
          (2). Methods with Create in the name: you need to release
          (3). Methods with Get in the name: same semantics as autorelease

          (4). Many CF types have their own free.
                 For example, CGPathRelease instead of CFRelease.
                 You can use either one. (使用 CGPathRelease, 讓程式碼更清楚)
              (唯一的差異: 將 nill 傳入 CFRelease 會 crash 掉; CGPathRelease 則不會)

       c. Toll-Free Bridging:
          (1). Any CF object type can be cast to id
          (2). NSArray* and CFArrayRef are compatible
          (3). Same with NSDictionary* and CFDictionaryRef


2012年2月5日 星期日

Filter4Cam 學習之 Core Graphics isn’t scary, honest!

since: 2012/01/30
update: 2012/02/05

reference: Core Graphics isn't scary, honest!

A. 前言
   1. 任何在 iOS 上曾經只用 UIViews 來開發應用程式的人, 可能會對這篇文章的標題
       感到有點奇特. 他們可能會說 "什麼?", "你是精神錯亂嗎? Core Graphics 不僅僅
       只是 CAPI, 並且有讓人感到困惑的函式名稱, 而且要比我用 UIView 能夠作到
       相同的事情需要更多的程式碼". 是的, 他們可能是正確的, 但是有一個
       Core Graphics 存在的理由: 它是速度快的!

       但是使用 Core Graphics 並不意謂著你的程式碼就會是讓人感到困惑的, 或者你
       必須適應去跟效能妥協. 魚與熊掌可兼得(You can  have your cake and eat it too),
       或者說你可以擁有易於閱讀且高執行力的程式碼. 請繼續閱讀來瞭解我的意思.

   2. 本篇文章以原始作者的內容為主, 來建立整個專案.

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

B. 新增專案
   1. Xcode > File > New > New Project...
       iOS > Application > Single View Application > Next
       Product Name: MyCoreGraphics
       Device Family: iPhone
       Use Storyboard: checked
       Use Automatic Reference Couting: checked
       > Next

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

C. 新增 UIView 子類別
    1. Xcode > File > New > New File...
        iOS > Cocoa Touch > Objective-C class > Next
        Class: CGView
        Subclass of: UIView
        > Next

    2. 調整 MainStoryboard.storyboard 內容如下:
        先選取 View, 接著將其 Class 由 UIView 改選為 CGView.

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

D. 使用 drawRect: 的嬰兒步驟
    1. 要在 Core Graphics 中畫圖, 你的進入點是 drawRect: 方法, 並且(大多數情況下)
        是繪製作業能夠被執行的唯一地方. 當開始在 iOS 中用低階的方式來繪圖, 這是
        新手所會有的最大問題, 因為他們習慣於在任何偶然的方法中能夠任意地增加
        subViews 或 subLayers.

        drawRect: 是在其主要框架(main frame)的描繪迴圈(rendering loop)中, 被底層的
        繪圖子系統所呼叫. 繪製下一個 frame 的時機為何, iOS 會去探究螢幕上所有的
        view 並且檢查是否它的內容需要被更新. 如果是的話, drawRect: 方法就會在需要
        被更新的適當矩形範圍被呼叫. 就這麼簡單. 只有當你需要的時候, 它就會被要求
        去繪製內容.

    2. 找到你的環境(context)
        所有在 Core Graphics 中的繪圖操作利用了一個 context 指標, 來對即將要作繪圖
        操作的地區持續地追蹤. 這是一個你會在 C 的 API 中發現的標準作法, 因為沒有了
        物件導向容易可用的 "self" 屬性, 你就必須採用某些手段: 提供 context.

        相關程式碼調整:
        開啓 CGView.m 檔案, 將 drawRect: 方法的註解取消, 並修改如下:
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
}

       說明: 從這個觀點看來, 你可以在之後所有的 Core Graphics 呼叫中使用 ctx 變數.

    3. 儲存狀態(state)
        a. 如果你想要在螢幕上畫任何東西, 甚至是像一條線一樣簡單的事情, 你將會需要
            使用數個函式呼叫來作此事. 這是因為要用各別的呼叫來設定屬性, 例如: 線條
            的顏色, 線條的寬度, 任何你在線條角落(轉彎處)想要的端蓋(end-cap)設定, 然後
            你的線條就會沿著實際的路徑行進. Core Graphics 藉由更新內部的狀態來對那些
            所有不同的屬性保持追蹤.

        b. 把繪畫的狀態想成是一支筆: 你可以改變筆尖, 顏色, 然後可以用同一支筆畫出
            可供區別的線條. 但是如果你忘了將筆改變回去, 使用後沒有自行清理, 你可能
            會在未來的繪畫中招來混亂的情況.

        c. 要避免在工作上遇到類似這樣的問題, 最好使用以下的二個函式:
             CGContextSaveGStateCGContextRestoreGState. 把它想成在你目前的
             繪畫狀態中儲存一個書籤或是檢查點. 然後你就可以改變任何你喜歡的繪畫
             屬性: 畫圖, 填滿, 畫任何的線條; 當都完成後, 便可以將繪畫狀態重設回之前
             的狀態.

        d. 然而要抓住一個重點: 你所呼叫的儲存跟回復的次數必須完美地相配合, 因為
            不然的話, 當無論是誰呼叫你的類別時, 你可能會把繪畫狀態弄混亂了. 這是
            重要的, 因為不然的話你將會在螢幕上看到一些很不可思議的垃圾剛被畫出來.
            要避免這種情況, 我使用了一個非常簡單的技巧, 將在以下的範例程式中展示.

            相關程式碼調整:
            開啓 CGView.m 檔案, 修改 drawRect: 方法如下:
- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
   
    CGContextSaveGState(ctx);
    {
        // Draw whatever you like
    }
    CGContextRestoreGState(ctx);
}

           說明: 藉由在儲存回復函式呼叫之間, 包含新的一對括弧, 你就有了一個程式的
                     縮排區塊.(可以在其中寫入任何你想要畫的圖之程式碼)

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

E. 使用 UIView 類別來簡單繪畫
   1. 藉由在一些 UIView 子類別所提供的幾個簡單的助手方法, 有一些相當簡單的操作
       讓人可以容易地完成想要做的事情. 例如, 假定你正在建構下一個最偉大的 Twitter
       應用程式, 並且你想要盡可能地在主控項目(feed items)之外榨出更多的效能.
       明顯地, 如果你想要去處理將數以千計的發文以每秒顯示60幀數(60fps)的方式呈現,
       那麼, 描繪文本, 影像, 坡度, 邊界與陰影會是相當昂貴的操作.

      幸運地, 你不需要將 UIView 類別用數百行的 Core Graphics 呼叫所取代. 許多大部份
      常見的類別, 像是: UIImage, UIBezierPath 等等, 都藉由直接使用 Core Graphics 的
      drawRect:方法, 來提供方便繪製內容的方法.

      更新: 原始投遞(post)的文章有一些 bugs, 大部份由於這篇文章是在聖誕假期時,
                只憑記憶寫出來的. 然而有一些對此篇文章非常有用的評註, 讓我必須做些
                改變. 我對第一個版本的 bugs 道歉.

   2. 畫出 UIImage
       我們可以使用 UIImage 物件直接地畫到 Core Graphics 的 context. 當你必須去
       建立一個 CGImage 物件, 色彩(color-space)空間物件等等, 對於每個冗長的
       程式碼, 都要使用原生的(raw) API 來呼叫. 取而代之, 畫出一個影像, 可以如
       以下般地容易: 

            相關程式碼調整:
            開啓 CGView.m 檔案, 修改 drawRect: 方法如下:
- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
   
    CGContextSaveGState(ctx);
    {
        // Draw whatever you like
        UIImage *img = [UIImage imageNamed:@"MyImage.png"];
        [img drawInRect:rect];
    }
    CGContextRestoreGState(ctx);
  
}

           結果:
           說明: 用這個技巧, 你可以有更多的選項, 例如: 混合(blending), 與不透明度
                     (alpha)參數. 還有其它方法可讓你在特定的點畫出圖片, 畫圖案等等.

   3. 畫出貝茲路徑(BezierPath)
       經常有時候, 你想要在一些 view 上顯示圓角, 但是你只想讓某幾個角是圓的. 例如,
       假定你想要顯示一個只有頂部二個角為圓形的 view 容器(container). 大多數最直接
       的方式是將它丟入 Core Graphics 中去作描繪. 當可允許單獨使用低階的 API 來依
       路徑構圖, 就可以相當簡單地作到以下的事.

            相關程式碼調整:
            開啓 CGView.m 檔案, 修改 drawRect: 方法如下:
- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
   
    CGContextSaveGState(ctx);
    {
        // Draw whatever you like
        //@UIImage
        //UIImage *img = [UIImage imageNamed:@"MyImage.png"];
        //[img drawInRect:rect];
        
        //@UIBezierPath
        CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
        CGContextSetLineWidth(ctx, 3);
       
        UIBezierPath *path;
       
        path = [UIBezierPath bezierPathWithRoundedRect:rect
                                     byRoundingCorners:(UIRectCornerTopLeft |
                                                        UIRectCornerTopRight)
                                           cornerRadii:CGSizeMake(10.0, 10.0)];
         [path stroke];
       
    }
    CGContextRestoreGState(ctx);
}

           結果:

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

F. 回應使用者的輸入
   1. Core Graphics 會盡可能地去試著提高效率, 所以代表你可以使用一些捷徑. 大多數
       那些正好是你想要與期待的部分, 以及作為當你從其它平台轉換過來時的驚喜.
       但是有些時候, 你可以用自己的方式來達成, 所以進階地去瞭解那些最佳化是什麼,
       以及如何去控制它們, 是有幫助的. 並且, 作戰中一半的工作是去了解情報
       (knowing is half the battle)....

   2. 了解 view 何時需要顯示(needs display)
        a. drawRect: 方法只有在當 UIKit 覺得你的內容已經變得陳舊並需要被重畫時才會
            被喚起. 大部份的情況代表你的 drawRect: 方法, 將於 view 被加到 superview 時
            被呼叫. 當 frame 被畫時, drawRect: 就會被呼叫, 明確地去呼叫 drawRect:
            並不會如你預期般地發生作用; 所以如果某些跟你互動的使用者要求重畫你的
            內容, 這裡有一個簡單的解決方式. 

        b. 所有的 UIView 物件都有一個方法: setNeedsDisplay. 呼叫這個方法將會在你的
         物件
內部繫緊(toggle)一個旗標(flag), 用來指明在下一個 frame 時, 你的 view
            需要被重新畫出. 這樣有幾個好處, 事實上包括讓你的程式碼看起來非常敏捷;
            因為無論你在相同的執行迴圈(runloop)裡呼叫了多少次, 你的 frame 將仍然
            只會在下一個 frame 時被重畫一次. 因此, 你不需擔心過度呼叫這個方法, 保證
            你可以從任何 UI 邏輯中決定需要被改變的內容, 而將其內容保持在更新狀態.

        c. 說明: 原始的 code 無法正常執行, 因此在先此不實作.

   3. 當 frame 大小改變時, 作出回應
       a. 當你的 view 改變了它的大小, 無論是明確地設定或者 superview 自動調整
           了 subviews 的大小, 你的內容在某方面來說已經無效了(invalidated). 當這
          情況發生時, 與其總是呼叫昂貴的 drawRect: 方法, UIView 物件有一個
          contentMode 屬性可以讓你用來給 UIKit 一個暗示(hint), 告知在此情況下你想要
          它作什麼.

       b. 這個屬性是一個名為 UIViewContentMode類舉型別之定義類型(typedef enum),
           並且有一串你可以用來檢查的不同項目. 預設值為
            UIViewContentModeScaleToFill, 它代表著: 假如你的 frame 大小改變了, UIKit
           將會簡單地採用最近一次 drawRect: 方法所建立的描繪結果, 並且將其結果放大
           或縮小來符合可用的空間.

          最簡單的方式來讓你的內容重畫, 是去將 view 的 contentMode 設為
          UIViewContentModeRedraw, 但是在許多實例上, 這可能會造成過度的殺傷力
          (overkill).

       c. 例如, 假定你的 drawRect: 方法畫了一個邊界(border)及沿著 frame 的底部畫
           一些線條. 如果你的 frame 改變了高度, 當大部分的內容都沒有改變時, 去執行
           完整的重畫工作是一件浪費的事情. 所以, 如果你將 contentMode 設為
     
UIViewContentModeBottomLeft 的話, UIKit 將會在 view 的左下角重新排列
          內容, 而保持其餘的內容照舊. 

   4. 避免畫過像素的邊界
       a. 當你將元件置放到螢幕上(不只是用 Core Graphics, 一般更常用 UIKit), 你需要
           去確認你的 view 是存在於整數像素座標上的. iOS 的繪圖座標系統是使用點來
           操作的, 不是絕對的像素, 這意謂著有可能告訴 UIKit 或 Core Graphics 在分數
           像素(fractional-pixel)上去繪製內容. 因為這是不可能的, iOS 裝置會試圖去將
           內容作反鋸齒(anti-alias), 所以它會在幾個像素之間變得模糊. 這幾乎不曾是你
           想要的結果, 所以當在計算位置時(特別是當有除值計算時), 使用 floorf
     
ceilf 函數來將其值往上或往下調整至最接近的整數是一件重要的事.   

       b. 當在螢幕上畫圖, 畫線或畫路徑時, 這種情況更是明顯. 看看以下的例子, 我們
           沿著 view 的底部畫一條線. 然而, 這個例子我們將使用低階的畫圖程序來畫出
           路徑, 取代使用 UIBezierPath 物件的方式.

            相關程式碼調整:
            開啓 CGView.m 檔案, 修改 drawRect: 方法如下:
- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
   
    CGContextSaveGState(ctx);
    {
        // Draw whatever you like
        //@add: button line
        CGContextSetStrokeColorWithColor(ctx, [UIColor greenColor].CGColor);
        CGContextSetLineWidth(ctx, 1);
       
        CGContextBeginPath(ctx);
        CGContextMoveToPoint(ctx, 0, CGRectGetMaxY(rect));
        CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
        CGContextStrokePath(ctx);
    }
    CGContextRestoreGState(ctx);
}

           結果:
           說明: 這段程式碼似乎相當直截了當; 先將畫圖的筆移到左下角, 接著新增一條線
                     到右下角, 然後利用先前定義好的線寬與線的顏色將結果路徑畫出來. 

                     抓住一點, 當你在 (0, 0) 建立一個點, 它代表螢幕上合理的左上角. 然而,
                     左上角的像素佔據了定義為 frame (0, 0, 1, 1) 的空間(亦即, 從 (0,0)
                     作為起始的一個點之正方形). 當你畫線時, 是以任何你給的座標為參考中心
                     的. 因此, 上面的範例中, 你的程式向 Core Graphics 要求畫一條線, 其中 0.5
                     在一個像素上, 另外的 0.5 在另外的像素上. 

                     要想避開這個問題, 你應該將要畫的 frame 作偏移, 所以你把要畫的點作
                     0.5 的位移. 底下是更新後的程式碼版本, 會畫出實心且不會模糊的線:  

            相關程式碼調整:
            開啓 CGView.m 檔案, 修改 drawRect: 方法如下:- (void)drawRect:(CGRect)rect
{
    // Drawing code
    //@add: Do something with your context
    CGContextRef ctx = UIGraphicsGetCurrentContext();
   
    CGContextSaveGState(ctx);
    {
        // Draw whatever you like
        //@add: button line
        CGContextSetStrokeColorWithColor(ctx, [UIColor greenColor].CGColor);
        CGContextSetLineWidth(ctx, 1);
       
        CGContextBeginPath(ctx);
        //CGContextMoveToPoint(ctx, 0, CGRectGetMaxY(rect));
        CGContextMoveToPoint(ctx, 0, CGRectGetMaxY(rect) - 0.5);

        //CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect));
        CGContextAddLineToPoint(ctx, CGRectGetMaxX(rect), CGRectGetMaxY(rect) - 0.5);

        CGContextStrokePath(ctx);
    }
    CGContextRestoreGState(ctx);
}

           結果:

   5. 畫彩色的容易方法
       a. Core Graphics 已隨著時間逐漸發展, 有了新的與改進過的 API. 這意謂著, 對許多
          繪畫的操作, 可能會有幾個不同的函式呼叫, 會讓你得到相同的結果. 特別是在使用
          顏色時會是如此.

       b. 舉例來說, 如果你想要在 Core Graphics 中設定線條的顏色, 分別有四個函數呼叫
           可以達成相同的結果.
           你第一個念頭可能會先採用顯示的第一個結果, 但是那代表需要做更多的工作.
           標準的 CGContextSetStrokeColor 函式呼叫, 帶有一個 CGFloats 的陣列, 在其中
           指出你想要畫的不同顏色之值.

       c. 然而, 有一個更容易的方式. CGContextSetStrokeColorWithColor 是一個稍微更
           冗長的函式名稱, 但是允許你可以提供一個 CGColorRef 的值來描述一個顏色.
    
並且方便, UIColor 類別提供一個助手(helper)的屬性叫作 CGColor, 它會回傳
          預先計算好的(pre-calculated)顏色參照(reference), 當 UIColor 物件離開時, 會
     自動釋放記憶體.
這意謂著, 你不需要手動去配置記憶體(malloc)或釋放(release)
          顏色的參照, 因此, 最後結果會使用較少的程式碼.

       d. 每當你看到一個函式有一個 "ColorWithColor" 可以選擇, 它即是使用了這個模式.

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

G. 使用適合的工具來工作
     1. 我能給的最佳忠告或許是: 使用適合的工具來工作. 例如, 如果你需要設計一些
         視覺上的介面元素, 你就不該去新增不需要的 subviews, 只要去畫東西像是邊界,
         圓角, 瓷磚圖案等等. 此外, 有些個案使用了大量的 subviews (就像是在
         UITableViewCells 裡) 是沒有效率的, 並且繪製簡單的元素像是圖像, 文字或邊界
         是更適合使用 Core Graphics 的.

     2. iOS 與 UIKit 是跟 HTML 絕然不同的環境, 並且你不能把設備的版面編排與繪圖
         看作相同的方式來處理.

     3. 最後, 確認你有做練習, 並且不要害怕試驗 Core Graphics. 從簡單的事開始, 像是
         邊界, 畫圖, 與文字或者以程式化的方式去繪製出特效(否則使用 Core Animation
         或是基於圖像的紋理(textures)的話, 速度會較慢).

2012年1月14日 星期六

Filter4Cam 學習之 Paint with Incoming Video

since: 2012/01/11
update: 2012/01/14

reference: Conway's Game of Life Painted with Incoming Video (Core Image Tut) | Indie Ambitions

A. 結合 Core Image 與 Core Graphics
   1. 有一個優秀的 app 叫作 Composite, 它可以讓你將影像的輸入當成畫布來繪畫.
      使用 core image 的濾鏡也可以作到類似的功能.

   2. 在這篇文章將使用影像的輸入當成 cell images 來實作 Conway's Game Of Life
        (康威:生命遊戲).

   3. 並且, 更進一步地. 用來將影像的輸入當成畫布來繪製的處理, 使用到 Core Image
       和 Core Graphics. 我們將會使用 core graphics 來繪製 CGImage, 該 CGImage 能
       被轉換成 Core Image, 並且採用任意的 CIFilters 濾鏡效果來達成.

   4. 這篇文章將接續建置在之前的文章上.
        說明: 記得先將 MainStoryboard.storyboard 中的 buttonslider 設為 Hidden,
                  並移至銀幕的最下方.

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

B. 繪畫功能
   1. 首先, 必需設定好繪畫(drawing)類別. 新增一個 CGContext 到 ViewController 類別,
       並新增一個方法用來設置. ViewController.h 檔案修改如下:
....
@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
    AVCaptureSession *session;
   
    CIContext *coreImageContext;
    EAGLContext *context;
    GLuint _renderBuffer;
    IBOutlet UISlider *sldr;
    CIImage *ciimg;
   
    //@add
    CGSize screenSize;
    CGContextRef cgcontext;
    CIImage *maskImage;
    float scl;
}

@property (strong, nonatomic) EAGLContext *context;

- (IBAction)snap:(id)sender;

//@add
-(void)setupCGContext;

@end

   2. 在實作檔: ViewController.m, 需要在 viewDidLoad 方法裡, 將螢幕大小存入變數中,
       並且呼叫 setupCGContext 方法.

- (void)viewDidLoad
{
    [super viewDidLoad];
       ....
    //@add
    CGSize scrn = [UIScreen mainScreen].bounds.size;

    //UIScreen mainScreen.bounds returns the device in portrait, we need to switch it to landscape
    screenSize = CGSizeMake(scrn.height, scrn.width);
   
    if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
        scl = [[UIScreen mainScreen] scale];
        screenSize = CGSizeMake(screenSize.width * scl, screenSize.height * scl);
    }
    [self setupCGContext];
}

    說明: 在這段程式碼區塊中, 檢查 devices 是否為 retina 的. 當使用 openGL 描繪到
               螢幕時是使用 pixels 的, 因此如果是在 retina 的 device 中, 就需要按比例提高
               繪製的圖形大小. 

   3. 接著, 在 ViewController.m 中, 實作 setupCGContext 方法:
//@add
-(void)setupCGContext {
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
   
    NSUInteger bytesPerPixel = 4;
    NSUInteger bytesPerRow = bytesPerPixel * screenSize.width;
    NSUInteger bitsPerComponent = 8;
   
    cgcontext = CGBitmapContextCreate(NULL, screenSize.width, screenSize.height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast);
   
    CGColorSpaceRelease(colorSpace);
}

   4. 下一步, 需要新增一個方法用來畫出 CGImage, 然後轉成 CIImage.
      a. 在 ViewController.h, 新增方法宣告如下:
....
//@add
-(void)setupCGContext;
-(CIImage *)drawGameOfLife;

@end

      b. 在 ViewController.m, 實作方法如下:
....
//@add
-(CIImage *)drawGameOfLife {
    CGContextSetRGBFillColor(cgcontext, 1, 1, 1, 1);
    CGContextFillEllipseInRect(cgcontext, CGRectMake(0, 0, screenSize.width, screenSize.height));
   
    CGImageRef cgImg = CGBitmapContextCreateImage(cgcontext);
    CIImage *ci = [CIImage imageWithCGImage:cgImg];
    CGImageRelease(cgImg);
    return ci;
}
....

    說明: 之後會再修改此方法來畫出 Game Of Life 的狀況. 目前只畫出一個大橢圓.
               這方法的最後四行: 先從 context 建立一個 CGImage, 再從  CGImage 產生
               Core Image 的影像, 最後釋放掉 CGImage 並回傳 Core Image. 依此步驟,
               再套用濾鏡.

   5. 現在, 我們已經設置好建立 CIImage 的方法了, 必需去呼叫它並且使用 CIImage 的
       濾鏡功能. 也要去調整輸入影像的大小來符合所建立的 CIImage, 修改
       ViewController.m 裡的 captureOutput 方法如下:
//@update for new
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
   
    CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
   
    CIImage *image = [CIImage imageWithCVPixelBuffer:pixelBuffer];
   
    float heightSc = screenSize.height/(float)CVPixelBufferGetHeight(pixelBuffer);
    float widthSc = screenSize.width/(float)CVPixelBufferGetWidth(pixelBuffer);
   
    CGAffineTransform transform = CGAffineTransformMakeScale(widthSc, heightSc);
   
    image = [CIFilter filterWithName:@"CIAffineTransform" keysAndValues:kCIInputImageKey, image, @"inputTransform", [NSValue valueWithCGAffineTransform:transform],nil].outputImage;
   
    maskImage = [self drawGameOfLife];
   
    image = [CIFilter filterWithName:@"CIMinimumCompositing" keysAndValues:kCIInputImageKey, image, kCIInputBackgroundImageKey, maskImage, nil].outputImage;
   
    [coreImageContext drawImage:image atPoint:CGPointZero fromRect:[image extent] ];
   
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

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

C. 利用繪製的 CGImage 對輸入影像作濾鏡處理
   1. 第一個要套用到圖像(輸入的影像)的濾鏡是 CIAffineTransform(2D 仿射變換),
       它會放大或縮小影像來符合螢幕的尺寸.

   2. 第二個濾鏡為 CIMinimumCompositing(最低合成), 它使用我們建的圖像
        (黑底橢圓白)並且對照到其大小. 從 pixel 的角度看來, 這個濾鏡使用二個值之一
        的最小顏色值. 因為黑色是絕對的最小值, 而白色是絕對的最大值, 因此我們會得到
        有著黑色橢圓圍繞的外側以及從影格(video frame)傳來的顏色.

   3. 編譯並執行: (左右相反, 上下顛倒)

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

D. 引進生命遊戲
   1. 開始來引進生命遊戲的模型. 在這邊不去涉及到實作, 基礎上採用 Alan Quatermain
      建立的版本, 並新增一些額外的屬性: 一個隨機的種子程序用來產生部分的 cell; 以及
      一個用來新增特定樣式(爬行動物)到 grid 的方法.

   2. 先下載此篇文章作者的專案原始碼 並解壓縮. 將 GOLModel.hGOLModel.m 加到
       專案裡.

   3. 在 ViewController.h 中引入 GOLModel.h 並建立一個 Game Of Life 的實體變數.
....
//@add
#import "GOLModel.h"

@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
.... 
    //@add
    GOLModel *GOL;
}

   4. 在 ViewController.mviewDidLoad 方法中作初始化.
- (void)viewDidLoad
{
    [super viewDidLoad];
    ....
    //@add
    GOL = [[GOLModel alloc] initGameWidth:30 andHeight:20];
    [GOL randomPopulate];
}

   5. 目前已經有了初始化的 GOL(Game Of Life) 物件, 需要改變 ViewController.m
       的 drawGameOfLife 方法來顯現出遊戲的狀態.

-(CIImage *)drawGameOfLife {
    //@add
    // 步驟 1: 經由將螢幕的大小除以 cells 的數目, 來取得每個 cell 的寬與高.
    int colwidth = screenSize.width / GOL.width;
    int rowheight = screenSize.height / GOL.height;

    //CGContextSetRGBFillColor(cgcontext, 1, 1, 1, 1);
    //@update
    // 步驟 2: 接著, 使用不透明度(opacity)為 0.4 來清除螢幕, 這會讓 cells 在完全消失後,
    //              還能產生幾個回合. 在視覺上較為有趣.
    CGContextSetRGBFillColor(cgcontext, 0, 0, 0, 0.4);
    CGContextFillEllipseInRect(cgcontext, CGRectMake(0, 0, screenSize.width, screenSize.height));

    //@add
    NSArray *ar = GOL.cells;
   
    // 步驟 3: 反覆地從 GOL 模型中, 計算每個 cell 的起始 x 與 y 的座標.
    for (int i = 0; i < [ar count]; i++) {
        BOOL CELLACTIVE = [[ar objectAtIndex:i] boolValue];
        int x = i % GOL.width;
        int y = (int)(i/GOL.width);

        // 步驟 4: 接著, 如果發現到一個活躍(active)的 cell, 就在適當的位置畫一個白圈. 
        //               +1 並使用寬與高; -2 來讓 cells 間留一些縫隙.
        if (CELLACTIVE) {
            CGContextSetRGBFillColor(cgcontext, 1, 1, 1, 1);
            CGContextFillEllipseInRect(cgcontext, CGRectMake(x * colwidth + 1, y * rowheight + 1, colwidth - 2, rowheight - 2));
        }
    }

        // 步驟 5: 如同之前的, 將 CGImage 轉成 CIImage.
        CGImageRef cgImg = CGBitmapContextCreateImage(cgcontext);
        CIImage *ci = [CIImage imageWithCGImage:cgImg];
        CGImageRelease(cgImg);
        return ci;
}

   6. 編譯並執行: (左右相反, 上下顛倒)
       產生隨機的一群小圓圈, 取代之前的一個大圓圈. 並在小圓圈中可以看到影像.

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

E. 進行生命遊戲
   1. 現在開始讓遊戲進行. 新增一個 buttonMainStoryboard.storyboard 用來啟動
       與關閉遊戲. 給此 button 一個 IBOutlet: playGOLButton, 以及方法: playGOL.
       並做好相關的聯結. 我們也需要一個計時器: 新增一個實體變數:
       NSTimer *refreshTimer.

   2. 在 ViewController.h 中修改如下:
....
@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
....
    //@add
    GOLModel *GOL;
   
    //@add
    IBOutlet UIButton *playGOLButton;
    NSTimer *refreshTimer;
}

....
//@add
-(void)setupCGContext;
-(CIImage *)drawGameOfLife;

//@add
- (IBAction)playGOL:(id)sender;
-(void)updateGOL:(NSTimer *)timer;

@end

   3. 在 ViewController.m 中新增方法如下:

//@add
// 更改 buttons 的標籤, 接著新增計時器給 updateGOL 方法使用或關閉計時器.
- (IBAction)playGOL:(id)sender {
    if (refreshTimer) {
        [playGOLButton setTitle:@"Play" forState:UIControlStateNormal];
        [playGOLButton setTitle:@"Play" forState:UIControlStateHighlighted];
        [refreshTimer invalidate];
        refreshTimer = nil;
       
    } else {
        [playGOLButton setTitle:@"Stop" forState:UIControlStateNormal];
        [playGOLButton setTitle:@"Stop" forState:UIControlStateHighlighted];
       
        // 效能提升: 每 0.5 秒執行一次 drawGameOfLife, 取代每個 frame 執行.
        refreshTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateGOL:) userInfo:nil repeats:YES];
    }
}

//@add
// 對 GOL 物件呼叫 update 方法, 將會使遊戲進行一個步驟. 然後在更新的 GOL
// 模型上重新繪製 maskImage.
-(void)updateGOL:(NSTimer *)timer {
    [GOL update];
    maskImage = [self drawGameOfLife];
}

   4. 在  ViewController.m 裡的 captureOutput: 方法中, 修改如下: 
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    ....
    //@update
    // 這邊呼叫了一次相同的 code, 可以將它移除(作註解)
    //maskImage = [self drawGameOfLife];
    ....
}

   5. 在 ViewController.m 裡的 viewDidLoad 方法最下方, 加入以下的 code, 不然會
       得到黑色的銀幕直到按下 Play button.
- (void)viewDidLoad
{
    [super viewDidLoad];
    ....

    //@add
    maskImage = [self drawGameOfLife];
}

   6. 編譯並執行:


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

F. 更改生命遊戲裡 Cell 的值
   1. 下一件我們想作的事是: 在生命遊戲模型中增加多點觸控的互動機制.
       當我們觸控銀幕時, 對於每個觸控, 我們都要去計算 cell 在 GOL 中的位置
       並且開啓或關閉.

   2. 首先, 在 ViewController.m 裡的  viewDidLoad 方法中, 開啓 view 的多點觸控的功能.
- (void)viewDidLoad
{
    [super viewDidLoad];
    //@add
    [self.view setMultipleTouchEnabled:YES];
    ....
}

   3. 接著, 在 ViewController.m 中, 新增 touchesBegantouchesMoved 方法.
        (它們是非常相似的)
//@add
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *t in touches) {
        CGPoint loc = [t locationInView:self.view];
        loc = CGPointMake(loc.x * scl, loc.y * scl);
        loc = CGPointMake(loc.x, screenSize.height - loc.y);
       
        int colWidth = (int)screenSize.width / GOL.width;
        int rowHeight = (int)screenSize.height / GOL.height;
       
        int x = floor(loc.x / (float)colWidth);
        int y = floor(loc.y / (float)rowHeight);
       
        if (refreshTimer) {
            [GOL spawnWalkerAtCellX:x andY:y];
        } else {
            [GOL toggleCellX:x andY:y];
        }
    }
    maskImage = [self drawGameOfLife];
}

//@add
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *t in touches) {
       
        CGPoint loc = [t locationInView:self.view];
        loc = CGPointMake(loc.x * scl, loc.y * scl);
        loc = CGPointMake(loc.x, screenSize.height - loc.y);
       
        int colWidth = screenSize.width / GOL.width;
        int rowHeight = screenSize.height / GOL.height;
       
        int x = floor(loc.x / colWidth);
        int y = floor(loc.y / rowHeight);
       
        [GOL toggleCellX:x andY:y];
    }
    maskImage = [self drawGameOfLife];
}

    說明: 首先, 反覆地從 touches set 中, 取得每一個 touch.  對於每一個 touch, 我們先
               轉換觸控點的位置, 因為 view 使用一種座標系統, 而 Core Graphics 使用垂直
               轉換的系統.   

               一旦完成後, 使用銀幕的位置以及對於 cells 尺寸的瞭解來計算:
               在 GOL 模型中, 被觸控的 cell 之座標. 接著呼叫 GOL 的 -toggleCellX:andY:
               方法來開啓或關閉.

               最後, 當我們完成了對所有 touch 各自 cell 的轉換, 用 Core Graphics 來更新
               GOL 的繪製, 並回傳 CIImage 給連串的 CIFilter 使用.

               唯一的不同點是: 在 touchesBegan 方法中, 假如遊戲目前是啓動的, 採用呼叫
                spawnWalker 方法, 以取代切換個別的 cell 方式. 呼叫 spawnWalker 方法會
               在觸控點的位置建立特定的生命遊戲圖案. 這個持續性的圖案會移動越過銀幕
               直到碰見另一組啓用的 cell.

   4. 編譯並執行:
        你可以經由觸控來操縱生命遊戲, 不論遊戲是啓動的或停止的.

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

G. 一些非必要的調整
   1. 為這個 app 加上一些非必要的視覺強化. 首先, 使用 perlin noise generator
       (由 Ken Perlin 發明的自然噪聲生成算法) 來決定 cell 的顏色.

   2. 先將原作者 專案原始碼 裡的 CZGPerlinGenerator.hCZGPerlinGenerator.m
       加到專案裡, 並調整 ViewController.h 如下:

//@add
#import "CZGPerlinGenerator.h"

@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
....
    //@add
    CZGPerlinGenerator *perlin;
}

.....

   3. 接著, 調整 ViewController.m 裡的 viewDidLoad 方法如下:

- (void)viewDidLoad
{
    [super viewDidLoad];
    ....
    [self setupCGContext];
   
    //@add
    perlin = [[CZGPerlinGenerator alloc] init];
    perlin.octaves = 1;
    perlin.zoom = 50;
    perlin.persistence = 0.5;  
   
    //@add
    GOL = [[GOLModel alloc] initGameWidth:30 andHeight:20];
    [GOL randomPopulate];
   
    //@add
    maskImage = [self drawGameOfLife];
}

   4. 最後, 調整 ViewController.m 裡的 drawGameOfLife 方法如下:

-(CIImage *)drawGameOfLife {
....
    for (int i = 0; i < [ar count]; i++) {
        BOOL CELLACTIVE = [[ar objectAtIndex:i] boolValue];
        int x = i % GOL.width;
        int y = (int)(i/GOL.width);
        // step 4: We next draw a white circle at the appropriate position if we’ve found an active cell. We add +1 and use a width and height – 2 to leave a littel padding between cells.
        if (CELLACTIVE) {
            //@add
            float r = [perlin perlinNoiseX:x * 5 y:y * 5 z:100 t:0] + .5;
            float g = [perlin perlinNoiseX:x * 5 y:y * 5 z:0 t:100] + .5;
            float b = [perlin perlinNoiseX:x * 5 y:y * 5 z:0 t:0] + .5;
           
            //CGContextSetRGBFillColor(cgcontext, 1, 1, 1, 1);
            CGContextSetRGBFillColor(cgcontext, r, g, b, 1);
            CGContextFillEllipseInRect(cgcontext, CGRectMake(x * colwidth + 1, y * rowheight + 1, colwidth - 2, rowheight - 2));
        }
    }
....
}

   5. Perlin noise 是一個隨機的數字產生器, 創造出逼真的外觀紋理.  如果你在 Photoshop
       中使用 Clouds 濾鏡, 會得到 Perlin noise 的二維紋理.

   6. 編譯並執行:
      你的 cells 會讓你覺得: 你是經由不同顏色的手機看出去的.

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

H. 效能
   1. 最後, 討論一下效能的問題. 這個 app 在 iPhone 4S 跟 iPad 2 上執行得相當平順.
       如果是在 3GS 上執行的話, 應該會蠻不順的.

   2. 有一些調整可以來增進效能. 你可以將 CGContext 從 RGB 改成 greyscale(灰階)
       (不過, 就無法使用 perlin noise 的 code), 這有一點幫助. 你也可以降低 GOL 中
       cells 的數目.

   3. 降低影像的解析度也有幫助. 將輸入的影像從 720p 改成 640×480, 並且改變
        cgcontext 跟 core graphics 的 code 以使它畫出 640×480 的影像. 然後移動
        魚鱗狀的濾鏡來產生效果. 根據 WWDC 的 Core Image 影片, 效能會隨著影像
        解析度的比例而改變. 大約可降低 pixels 的數目 2.5 個係數.