iPhone : CGBitmapContext와 CGLayer를 이용한 Fingers Drawing
최근 몇 달동안 Objective-C부터 해서 Xcode를 공부하고 있는데, 생각보다 기초적인 개념을 잘 설명한 글이 드물더군요. 아직 잘 몰라서 좋은 자료가 있는 사이트를 잘 모르는 탓이겠지요.
여튼 가장 기본적인 finger drawing조차 관련된 몇 가지 개념을 잘 모르다보니 한참 걸렸습니다. 그 내용을 정리해서 공유할까 합니다.
이 글에서 설명하고자 하는 것은 무엇보다 Quartz 2D library에서 Graphic Context와 CGLayer에 대한 것입니다. UIView와 UIViewController에 대한 것도 알고 있어야 하겠지만 실제로 그림이 어디에 어떤 과정으로 그려지는지를 살펴봅니다.
1. Graphic Context
>Quartz 2D Programming Guide를 보면 Graphic Context의 정의는 다음과 같습니다.
“A graphics context represents a drawing destination. It contains drawing parameters and all device-specific information that the drawing system needs to perform any subsequent drawing commands. A graphics context defines basic drawing attributes such as the colors to use when drawing, the clipping area, line width and style information, font information, compositing options, and several others.”
즉 Graphic Context는 현재 View가 사용하는 캔버스 같다고 볼 수 있습니다. 거기에 그림을 그리면 화면에 보여지는 것이죠. 그리고 어떤 색으로 어떤 스타일의 그림을 그릴지에 대한 정의도 담고 있습니다. 빨간 펜을 들고 그림을 그리는 것이 아니라 종이에다가 빨간 색으로 그려지라고 정해주는 방식입니다. 이 부분이 좀 낯설긴 하네요.
그런데 보통 Graphic Context라고 말을 하면 현재 View가 사용하는 CGContext를 말합니다. 즉 context는 임의로 만들수도 있고 만들어놓은 context를 Graphic Context로 만들 수도 있습니다. >UIKit Function에 이런 context와 관련된 메서드들이 있습니다. 문서를 보면 context는 stack 구조를 가진 것처럼 임의로 context를 만들어서 현재 Graphic Context위에 쌓을 수도 있고 다시 꺼낼 수도 있습니다. 즉, 가장 위에 쌓인 context가 Graphic Context가 되어서 화면에 보여지는 것입니다. UIKit Function에는 그런 일을 하는 다음과 같은 함수들을 제공합니다.
- UIGraphicsGetCurrentContext
- UIGraphicsPushContext
- UIGraphicsPopContext
- UIGraphicsBeginImageContext
- UIGraphicsBeginImageContextWithOptions
결국 context라는 것은 하나의 가상 LCD buffer memory라고 생각할 수 있습니다. Graphic Context라는 것은 최종 LCD 버퍼이고, 개발자는 필요한 만큼 여러 장의 LCD buffer를 가지고 있을 수 있습니다. 따라서 context를 만드는 과정도 필요한 메모리를 계산해서 정해주는 과정입니다.
CGContextRef newContext = CGBitmapContextCreate(NULL, bound.size.width, bound.size.height, 8, 4*bound.size.width, colorSpace, kCGImageAlphaPremultipliedFirst); |
위의 소스는 BitmapContext를 생성해주는 CGBitmapContext함수를 이용해서 새로운 context를 만든 것입니다. 이 함수는 다음과 같은 인자를 가집니다.
CGContextRef CGBitmapContextCreate ( void *data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef colorspace, CGBitmapInfo bitmapInfo ); |
먼저 data는 아직 잘 모르겠으니 일단 패스하고, 나머지 인자를 보면 가상 LCD의 크기와 Color model, 그리고 각 픽셀의 color depth를 정의해주는 것입니다. 즉, 가로*세로*픽셀당 color depth = 필요한 메모리크기 가 되는 것이죠. width, height는 쉽습니다. Context의 크기를 픽셀 단위로 지정해주는 것입니다.
순서는 뒤에 있지만 먼저 colorspace를 설명하겠습니다. Color model이 RGB, CMYK, HSB등 여러 가지가 있고, >CGColorSpace Reference를 보면 적절한 Color model을 가져오는 방법을 제공하고 있습니다. 그 중에서도 CGColorSpaceCreateDeviceRGB() 함수는 Device에 따른 일반적인 RGB입니다. 보통은 RGB를 사용하겠지만 경우에 따라 Gray나 CMYK를 지정할 수도 있겠죠. >Color Programming Topic을 보면 자세한 설명이 있지만 깊이 들어가면 어렵습니다.
다음으로 bitsPerComponent에 대한 설명입니다. 픽셀의 색상은 ARGB라는 4가지 요소를 가지고있으며 각 요소(component)에 메모리를 얼마나 지정할 지에 대한 것입니다. 위의 샘플처럼 8bit를 지정하면 8*4=32bit되어서 각 픽셀이 32bit의 색상을 표현하기 위한 메모리를 지정받게 됩니다. 다양한 Pixel Formats에 대해서는 >Quartz 2D Programming Guide 의 Table 2-1을 참고하면 됩니다.
Table 2-1 Pixel formats supported for bitmap graphics contexts
| CS | Pixel format and bitmap information constant | Availability |
| Null | 8 bpp, 8 bpc, kCGImageAlphaOnly | Mac OS X, iOS |
| Gray | 8 bpp, 8 bpc,kCGImageAlphaNone | Mac OS X, iOS |
| Gray | 8 bpp, 8 bpc,kCGImageAlphaOnly | Mac OS X, iOS |
| Gray | 16 bpp, 16 bpc, kCGImageAlphaNone | Mac OS X |
| Gray | 32 bpp, 32 bpc, kCGImageAlphaNone|kCGBitmapFloatComponents | Mac OS X |
| RGB | 16 bpp, 5 bpc, kCGImageAlphaNoneSkipFirst | Mac OS X, iOS |
| RGB | 32 bpp, 8 bpc, kCGImageAlphaNoneSkipFirst | Mac OS X, iOS |
| RGB | 32 bpp, 8 bpc, kCGImageAlphaNoneSkipLast | Mac OS X, iOS |
| RGB | 32 bpp, 8 bpc, kCGImageAlphaPremultipliedFirst | Mac OS X, iOS |
| RGB | 32 bpp, 8 bpc, kCGImageAlphaPremultipliedLast | Mac OS X, iOS |
| RGB | 64 bpp, 16 bpc, kCGImageAlphaPremultipliedLast | Mac OS X |
| RGB | 64 bpp, 16 bpc, kCGImageAlphaNoneSkipLast | Mac OS X |
| RGB | 128 bpp, 32 bpc, kCGImageAlphaNoneSkipLast |kCGBitmapFloatComponents | Mac OS X |
| RGB | 128 bpp, 32 bpc, kCGImageAlphaPremultipliedLast |kCGBitmapFloatComponents | Mac OS X |
| CMYK | 32 bpp, 8 bpc, kCGImageAlphaNone | Mac OS X |
| CMYK | 64 bpp, 16 bpc, kCGImageAlphaNone | Mac OS X |
| CMYK | 128 bpp, 32 bpc, kCGImageAlphaNone |kCGBitmapFloatComponents | Mac OS X |
그리고 bytesPerRow는 한 줄당 필요한 메모리를 지정합니다. 위의 샘플에서는 4byte*width 를 지정했는데, 1byte는 8bit고 즉 4byte는 32bit입니다. 결국 4byte*width 는 32bit*width 와 같습니다.
마지막으로 CGBitmapInfo는 위의 Table 2-1에 있는 것과 같은 값입니다. Color의 components(R, G, B, A)들의 구성을 정의합니다. 처음의 샘플 소스에서 지정한 kCGImageAlphaPremultipliedFirst값은 ARGB입니다. kCGImageAlphaPremultipliedLast는 RGBA가 되겠죠.
정리를 하면 BitmapContext를 만들어준 위의 샘플 코드는 픽셀당 32bit ARGB의 Color model을 적용하면서 그에 필요한 메모리를 확보하도록 한 것입니다.
2. CGLAyer
CGLayer에 대해서는 >CGLayer Reference를 참고하면 됩니다. CGLayer를 만들려면 context가 필요합니다. 즉 CGLayer는 context위에 생성되는 것이라고 생각할 수 있습니다. 그러나 context를 만들 때처럼 필요한 memory나 Color model을 지정할 필요는 없습니다.
하지만 CGLayer에 어떤 그림을 그려도 관련 context에 동시에 그려지는 것은 아닙니다! 주의할 점은 이것이죠.
말했듯이 CGLayer를 만들기 위해서는 기반이 되는 context를 지정해야 합니다. 그런데 CGLayer도 자체의 context를 가집니다. 이 부분이 저는 처음에 많이 헷갈렸는데, CGLayer를 생성할 때 기반 context를 지정하지만 실상 CGLayer에 그림을 그려도 기반 context에 그려지는 것이 아니라 CGLayer가 가진 context에 그려지는 것입니다.
Quartz 2D에서 어떤 그림을 그리는 것은 context 기반입니다. 선이든 이미지든 context에 그리게 되어 있고, CGLayer도 자신만의 context를 가지고 있는 것입니다.
CGContextDrawLayerInRect함수를 이용해서 CGLayer의 내용을 특정 context에 그릴 수 있습니다. 즉 CGLayer도 context와 마찬가지로 별개의 LCD buffer와 같은 용도로 사용할 수 있습니다. CGLayer에 어떤 그림을 그려놓고 어디든지 원하는 context에 copy할 수 있기 때문에 pattern을 그린다거나 재사용을 위한 이미지를 CGLayer에 그려놓으면 성능의 향상을 기대할 수 있다고 합니다. 그래서 BitmapContext와 같이 offScreen을 위한 LCD buffer로 사용하거나 재사용을 위한 이미지를 저장하는 용도로 보면 될 것 같습니다.
다양한 그리기 방법
사실 상황에 따라 Graphic context에 직접 그림을 그려도 되고 별도의 context를 만들어서 거기에 그린 다음 Graphic context에 옮길 수도 있습니다. 또는 CGLayer를 이용할 수도 있습니다. 이번 글을 통해서 설명하고자 했던 것은 Graphic Context와 직접 만든 context들에 대한 관계와 용도, 그리고 BitmapContext를 만드는데 필요한 Color와 메모리에 대한 이해를 통해 context가 어떤 것인지를 이해하는 것입니다. 그리고 CGLayer의 구성과 활용에 대한 것도 역시 설명하고자 했구요. 혼자 찾아보면서 공부한 내용이라 부족한 부분이 있고 혹시 틀린 부분이 있을 수도 있으니 감안하시기 바랍니다.
Fingers Drawing
>link를 통해 multi touch가 지원되는 fingers drawing app의 소스를 받을 수 있습니다.
이 app에서는 CGBitmapContext와 CGLayer를 활용한 방법을 적용했습니다. CGLayer는 touch가 시작되고 끝날 때까지 그려지는 새로운 line을 그리고, touch가 끝나면 새로 그려진 line을 CGBitmapContext에 옮겨서 그립니다. 그리고 Graphic Context에는 CGBitmapContext의 내용과 CGLayer의 내용을 옮겨서 그려주고 있습니다.
즉, 새로 그려지는 라인들은 CGLayer에서 가져오고 이미 그려진 라인들은 CGBitmapContext에서 가져오는 것입니다. 이렇게 해서 offScreen을 적용하고 있는 것입니다. 간단한 app이라 이렇게 복잡한 방법은 필요없었겠지만 공부를 위한 것이니까요.
그리고 샘플에서는 line의 color를 선택할 수 있고 모든 그림을 지우는 버튼도 제공합니다. 이런 control이 들어가니까 UIView외에도 UIViewController도 필요합니다. 이제 소스에서 DrawingView.m의 주요 부분을 간단히 보겠습니다.
- (id)initWithCoder:(NSCoder *)aDecoder { if ( (self = [super initWithCoder:aDecoder]) ) { CGRect bound = self.bounds; NSLog(@"initWithCoder:%f, %f", bound.size.width, bound.size.height); // device에 따른 RGB color space 생성 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); // BitmapContext 생성. 32bit color ARGB로 color system 지정 drawContext = CGBitmapContextCreate(NULL, bound.size.width, bound.size.height, 8, 4*bound.size.width, colorSpace, kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); //drawLayer를 생성하고 drawLayer의 context의 drawing property를 설정한다. drawLayer = CGLayerCreateWithContext(drawContext, bound.size, NULL); CGContextRef ctx = CGLayerGetContext(drawLayer); CGContextSetLineWidth(ctx, 2.0); } return self; } |
UIViewController의 nib파일에서 DrawingView가 호출되기 때문에 initWithCoder 메서드를 통해 초기화됩니다.
앞에서 설명한대로 CGLayer를 위해 CGBitmapContext를 만들고 CGLayer도 생성합니다. 그리고 CGLayer의 context에 그려질 line의 두께를 미리 정의해놓습니다. line의 color를 지정할 때도 CGLayer의 context에 해야 하겠죠.
- (void) clearDrawingView { CGContextClearRect(drawContext, self.bounds); NSLog(@"clearDrawingView"); [self setNeedsDisplay]; } // Only override drawRect: if you perform custom drawing. // An empty implementation adversely affects performance during animation. - (void)drawRect:(CGRect)rect { // Drawing code // Application의 graphic context를 가져온다. CGContextRef graphicContext = UIGraphicsGetCurrentContext(); // drawContext에 그려진 line의 이미지를 가져온다. CGImageRef drawnLineImage = CGBitmapContextCreateImage(drawContext); // graphic context에 bounds의 위치, 크기로 drawnLineImage를 그린다. CGContextDrawImage(graphicContext, self.bounds, drawnLineImage); CGImageRelease(drawnLineImage); // graphic context에 bounds의 위치, 크기로 drawLayer에서 그려지고 있는 line을 그린다. CGContextDrawLayerInRect(graphicContext, self.bounds, drawLayer); } #pragma mark - #pragma mark Respond to Touch Event - (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch; CGPoint lastTouch, currentTouch; CGContextRef ctx = CGLayerGetContext(drawLayer); for (touch in touches) { lastTouch = [touch previousLocationInView:self]; currentTouch = [touch locationInView:self]; // drawLayer의 context에 line을 그린다. CGContextBeginPath(ctx); CGContextMoveToPoint(ctx, lastTouch.x, lastTouch.y); CGContextAddLineToPoint(ctx, currentTouch.x, currentTouch.y); CGContextStrokePath(ctx); } [self setNeedsDisplay]; } - (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { // drawContext에 bounds의 위치, 크기로 drawLayer의 context에 그려진 라인을 출력한다. CGContextDrawLayerInRect(drawContext, self.bounds, drawLayer); // drawLayer의 context를 지우고 새로운 line을 그릴 준비를 한다. CGContextClearRect(CGLayerGetContext(drawLayer), self.bounds); [self setNeedsDisplay]; } |
소스를 보면 touch를 해서 그리는 도중에는 CGLayer의 context에 line을 그리고 있습니다. 그리고 touch가 끝나는 순간 그려진 line을 CGBitmapContext에 옮기고 있지요. 그리고 touch event가 생길 때마다 View의 drawRect메서드를 통해서 CGLayer와 CGBitmapContext의 내용을 Graphic Context에 옮겨그리고 있습니다.
이 소스에서는 항상 전체화면을 업데이트 해주고 있는데 최적화를 위해서는 dirty rect를 찾아서 업데이트해주는 것이 좋겠죠. 그 부분은 아직 공부 중입니다.
주목할 부분은 Clear버튼을 눌렀을 때 모든 그림을 지워주는 clearDrawingView 메서드입니다. Graphic Context를 지우는게 아니라 CGBitmapContext의 내용만 지운 다음 화면을 업데이트합니다. 즉 화면을 업데이트 할 때 CGBitmapContext의 내용을 복사하기 때문에 CGBitmapContext만 지우고 업데이트하는 것으로 충분합니다. 오히려 Graphic Context만 지우고 업데이트하면 그림이 사라지지 않습니다.
안녕하세요 구글링 하다가 찾아오게 됐습니다
코어그래픽을 이용해서 곡선이 많은 구름 같은 모양을 그려서 동적으로 크기를 변경하는 구조로 만들어 볼려고 하는데요. 곡선으로 그려진 그림이라 (반지름 없는) 크기를 바꾸는 부분에서 막혀서 어찌해야 될지 모르겠네요..
혹시 drawRect 함수안에서 그린 그림을 이미지로 바꿔주는 함수는 없을까요?
CGContextAddQuadCurveToPoint 를 이용해서 약 10번을 반복해서 그린 그림이라 크기를 동적으로
바꿀려면 어떤 식으로 값을 줘야할지 모르겠네요 ..