Metal이 뭐야

Heedon Ham·2023년 3월 24일
0

filmNoise 개발 회고

목록 보기
4/4

Custom Camera로 real-time으로 들어오는 비디오 스트림을 화면에 보이는 것까지 완성했다.
이젠 필터 데이터를 적용해서 real-time으로 보이도록 만들어야 한다.

Apple에서 이미지 처리 관련해서 제공하는 것 중 Core Image가 있다.
이 중, CIFilter를 활용하면 이미지 데이터에 필터 데이터를 적용하여 원하는 결과를 얻을 수 있다.

[Apple] Processing an Image Using Built-in Filters
(기존에 저장된 이미지를 불러와서 CIFilter를 적용하는 과정이다.)

기본으로 제공하는 CIFilter를 활용해도 되지만, 수치 조정을 디테일하게 할 수 없다는 단점이 있다.
당시, 앱 개발 기간 목표가 1개월이었고 마감까지 2주 정도 남아있었다.

그래픽 렌더링을 위해 Apple에서 제공하는 Metal을 풀로 활용해도 되지만 그래픽 개념을 처음 접한 나로서는 2주 안에 Metal을 마스터 하는 것이 불가능했다.
(WWDC에 Metal 소개 Session들이 있었지만 OpenGL에서 Metal로 전환하면서 어떻게 쉽게 전환할 수 있는지 설명하는 내용이 주를 이뤘다.)


방법을 찾다보니 WWDC Session 중에 이런 것들을 발견했다.

[WWDC] Build Metal-based Core Image kernels with Xcode

[WWDC] Explore Core Image kernel improvements

예시 코드들을 보면 custom CIFilter를 만들어서 Kernel에 등록한 후, 기본 CIFilter처럼 똑같이 적용할 수 있었다.


custom CIFilter를 제작하기 위해선 Metal Shading Language로 작성해야 하며, 실시간 화면에서도 적용된 Metal 결과물을 스크린에 "그려야" 한다.

AVCaptureVideoPreviewLayer는 다른 데이터가 적용된 화면을 보여주지 않는다.
오직 카메라로 들어오는 비디오 스트림 자체만 보여준다.

CIFilter가 적용된 화면을 실시간으로 보여야 하기 때문에 AVCaptureVideoPreviewLayer 대신 다른 View를 활용해야 한다. 이를 위해 MTKView를 활용한다.

Use CIFilter and Metal to apply filters

Apple은 MTKView를 다음으로 설명한다.

The MTKView class provides a default implementation of a Metal-aware view that you can use to render graphics using Metal and display them onscreen.

AVCaptureVideoPreviewLayer 대신 MTKView를 활용해서 실시간 화면 및 사진 저장의 큰 플로우는 다음과 같이 나타낼 수 있다.


저번 POST의 카메라 촬영 흐름에 Metal을 적용하면 다음과 같이 수정할 수 있다.

1) 전체적인 큰 흐름을 AVCaptureSession이라고 한다.
2) AVCaptureDevice와 AVCaptureInput 설정 뒤, AVCaptureConnection에 연결한다.
3) Session의 결과물을 AVCaptureOutput라고 한다.
4) AVCapturePhotoOutput과 AVCaptureVideoOutput으로 나뉠 수 있다.
5) PhotoOutput은 한 프레임을 저장하는데 활용되며, VideoOutput은 스크린에 나타내는데 활용된다.
6) AVCaptureVideoPreviewLayer가 제공되는데, UIView 위에 더해져서 CaptureSession을 실시간으로 확인할 수 있도록 해준다.
6) VideoOutput에서 CIImage를 추출, CIFilter를 추출한 CIImage에 적용
7) 적용한 CIImage를 Metal Rendering Process에 input으로 넣어서, MTKView에 "그리기"

따라서 Metal 관련 설정 코드들이 추가가 된다.

func setUpMetal() {
	//fetch the default gpu of the device (only one on iOS devices)
    metalDevice = MTLCreateSystemDefaultDevice()
        
    //tell MTKView which gpu to use
    mtkView.device = metalDevice
        
    //update MTKView every time there’s a new frame to display
    //tell MTKView to use explicit drawing (call .draw() on it)
    mtkView.isPaused = true
    mtkView.enableSetNeedsDisplay = false
        
    mtkView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        
    //create a command queue to be able to send down instructions to the GPU
    metalCommandQueue = metalDevice.makeCommandQueue()
        
    //extension for mtkView: MTKViewDelegate for responding view's drawing events
    mtkView.delegate = self
        
    //let its drawable texture be writen to
    mtkView.framebufferOnly = false        
}
func setUpCoreImage() {
	//initialize a CIContext instance using Metal
    //what GPU device to use for its built-in processing and evaluation functions
    ciContext = CIContext(mtlDevice: metalDevice)
}

VideoOutput에서 CIImage를 추출하기 위해선 captureOutput() 메서드에서 실행해야 한다.
captureOutput 메서드는 AVCaptureVideoDataOutputSampleBufferDelegate 프로토콜을 채택해야 호출할 수 있다.

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        //try and get a CVImageBuffer out of the sample buffer
        guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            debugPrint("unable to get image from sampleBuffer")
            return
        }
        
        //get a CIImage out of the CVImageBuffer
        let ciImage = CIImage(cvImageBuffer: imageBuffer)
        
        //apply Sepia filter
        let sepiaFilter = CIFilter(name:"CISepiaTone")
    	sepiaFilter?.setValue(input, forKey: kCIInputImageKey)
    	sepiaFilter?.setValue(intensity, forKey: kCIInputIntensityKey)
        guard let resultImage = sepiaFilter?.outputImage else { return }
        
        //save it for Metal rendering
        self.currentCIImage = resultImage
        
        //draw on MTKView
        mtkView.draw()
    }
extension ViewController: MTKViewDelegate {
    
    //tells us the drawable's size has changed
    //MTLDrawable: use drawable objects when you want to render images using Metal and present them onscreen
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
    }
    
    //render CIImage to the screen
    func draw(in view: MTKView) {        
    	//store a reference to each frame in our class
        //when calling draw() on the metalView, it knows what frame to use
        guard let commandBuffer = metalCommandQueue.makeCommandBuffer() else { return }
        
        //make sure we actually have a ciImage to work with
        guard let ciImage = currentCIImage else { return }
        
        //make sure the current drawable object for metalView is available
        //it's not in use by the previous draw cycle
        guard let currentDrawable = view.currentDrawable else { return }

        let widthOfDrawable = view.drawableSize.width
        let heightOfDrawable = view.drawableSize.height
        
        //make sure frame is centered on screen
        let widthOfciImage = ciImage.extent.width
        let xOffsetFromBottom = (widthOfDrawable - widthOfciImage)/2
        
        let heightOfciImage = ciImage.extent.height
        let yOffsetFromBottom = (heightOfDrawable - heightOfciImage)/2
            
        //render a CIImage into our metal texture
        //image: CIImage we create for each frame
        //texture: rendering it to the screen through metal view ~ texture of drawable that the metal view is housing ~ an image that's used to map onto an object
        //commandBuffer: instructions sent through commandQueue to GPU
        //bounds: GCRect to draw the image on the texture
        //colorSpace: tells CIContext how to interpret color info from CIImage        
        
        self.ciContext.render(ciImage, to: currentDrawable.texture, commandBuffer: commandBuffer, bounds: CGRect(origin: CGPoint(x: -xOffsetFromBottom, y: -yOffsetFromBottom), size: view.drawableSize), colorSpace: CGColorSpaceCreateDeviceRGB())
        
        //register where to draw the instructions in the command buffer once it executes
        commandBuffer.present(currentDrawable)
        
        //commit the command to the queue so it executes
        commandBuffer.commit()
    }
}

사진 저장의 경우, photoOutput의 Data를 그대로 활용하면 비디오 스트림의 원본 그대로 저장이 되었다.
따라서 captureOutput에서 저장한 currentCIImage를 Data로 변환하여 적용하는 방법을 강구했다.

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        //captureOutput에서 필터 적용된 CIImage 활용하기
        guard let ciImage = currentCIImage else {
            print("There is nothing in currentCIImage in photoOutput")
            return
        }
    
        let filteredImage = UIImage(ciImage: ciImage)
        
        //compressionQuality: 0.0 ~ 1.0 (1.0: 원본 그대로)
        let data = filteredImage.jpegData(compressionQuality: 0.99)

        PHPhotoLibrary.shared().performChanges({
        	// Add the captured photo's file data as the main resource for the Photos asset.
            let creationRequest = PHAssetCreationRequest.forAsset()
            creationRequest.addResource(with: .photo, data: data, options: nil)
        }) 
 }       

C.F.) Metal 관련 기본 개념 정리

[WWDC] Working with Metal: Overview

[WWDC] Working with Metal: Fundamentals

[WWDC] Core Image: Performance, Prototyping, and Python

profile
 iOS 개발

1개의 댓글

comment-user-thumbnail
2023년 12월 1일

Metal, a versatile alloy, has shaped industries and daily life. From the construction of towering skyscrapers to intricate machinery, its strength and malleability make it indispensable. Beyond structural applications, metal extends its influence into unexpected realms like music. Amid discussions about iconic bands and legendary performances, enthusiasts might find a surprising connection to metal's utilitarian side Carport construction. This blend of artistry and practicality showcases the diverse impact of metal in our lives. Heavy metal, a genre characterized by powerful guitar riffs and intense vocals, has a fascinating trivia.

답글 달기