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 中的 button 與 slider 設為 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.h 與 GOLModel.m 加到
專案裡.
3. 在 ViewController.h 中引入 GOLModel.h 並建立一個 Game Of Life 的實體變數.
....
//@add
#import "GOLModel.h"
@interface ViewController : GLKViewController <AVCaptureVideoDataOutputSampleBufferDelegate>
{
....
//@add
GOLModel *GOL;
}
4. 在 ViewController.m 的 viewDidLoad 方法中作初始化.
- (void)viewDidLoad
{
[super viewDidLoad];
....
//@add{
[super viewDidLoad];
....
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. 現在開始讓遊戲進行. 新增一個 button 到 MainStoryboard.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 中, 新增 touchesBegan 與 touchesMoved 方法.
(它們是非常相似的)
//@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.h 與 CZGPerlinGenerator.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 個係數.
沒有留言:
張貼留言
注意:只有此網誌的成員可以留言。