본문 바로가기

프로그래밍/iOS,macOS

[Metal] 이미지렌더링~ 카메라 입력과 가우시안 블러~

이전 kernel shader 를 사용하는 샘플에 입력 텍스처를 이미지가 아닌 카메라 픽셀버퍼로 변경해보자~
대부분 소스는 동일한데, 카메라 입력을 처리하기 위한 부분들만 차이가 있다. 
샘플이므로 비율처리 없이 카메라 입력을 그대로 받아 256x256 사이즈로 줄이고, 가우시안블러, 최종 렌더링시에 마스킹 이미지를 추가해 실제 이미지와 블러된 이미지를 혼합해 픽셀을 결정하게 된다.

먼지가 잔뜩인 맥북~ 이러면 블러를 먹이나 안먹이나...

ViewController

렌더러에 있던 metal view delegate 를 뷰컨트롤러로 이동하고, 렌더링 루틴을 2단계로 분리했다.
첫번째 카메라 이미지가 입력되었을때 첫번째 리사이징 렌더링을 수행하고, 해당 작업 이후에 블러와 화면 렌더링을 진행한다.
첫번째 렌더링은 별도의 렌더타겟인 텍스처에 렌더링하므로, 카메라에서 프레임 데이터가 전달되면 수행하고, 이 후에 화면 렌더링을 진행.
사실 동기화를 위해 DispatchQueue 나 세마포어등을 사용해야 하는데, AVCaptureSession 에서 작업큐를 지정할 수가 있어 별도 동기화 루틴은 작성하지 않았다.

MTKView 의 isPaused=true, enableSetNeedsDisplay=false 로 설정해 직접 draw를 호출하도록 수정.

class ViewController: UIViewController {

    @IBOutlet weak var mtkView: MTKView!
    
    let camera = Camera()
    var renderer:Renderer = .init()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        self.mtkView.isPaused = true
        self.mtkView.enableSetNeedsDisplay = false
        self.mtkView.device = self.renderer.device
        self.mtkView.delegate = self
    }
    
    override func viewDidAppear(_ animated: Bool) {
        self.camera.setSampleBufferDelegate(self)
        self.camera.start()
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        self.camera.stop()
    }
}

카메라 캡처러에서 CMSampleBuffer가 전달되면, pixel buffer로 변환하고, 1차 변환을 위한 렌더링을 수행한다.
렌더링이 정상적으로 완료되면, MTKView 의 draw를 호출해 블러와 화면 렌더링을 진행한다.

extension ViewController:MTKViewDelegate {
    func draw(in view: MTKView) {

        self.renderer.renderToDraw(view: view)
    }
    
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        
    }
}

extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        connection.videoOrientation = .portrait
        //let scale:Float64 = 1_000_000
        //let time = Int64( CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * scale)
        
        if let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
            if self.renderer.renderToTransform(pixelBuffer) == true {
                self.mtkView.clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0)
                self.mtkView.draw()
            }
        }
    }
}

 

카메라

class Camera: NSObject {
    lazy var session: AVCaptureSession = .init()
    lazy var input: AVCaptureDeviceInput = try! AVCaptureDeviceInput(device: device)
    lazy var device: AVCaptureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)!
    lazy var output: AVCaptureVideoDataOutput = .init()
    
    override init() {
        super.init()
        output.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_32BGRA]
        
        session.addInput(input)
        session.addOutput(output)
    }
    
    func setSampleBufferDelegate(_ delegate: AVCaptureVideoDataOutputSampleBufferDelegate) {
        output.setSampleBufferDelegate(delegate, queue: .main)
    }
    
    func start() {
        session.startRunning()
    }
    
    func stop() {
        session.stopRunning()
    }
}

 

렌더러

입력 텍스처만 매번 갱신되어야 하므로, 텍스처 캐시를 사용한다.
CVMetalTextureCache 프로퍼티를 하나 선언하고, 기존 MTLTexture 인 imageTexture를 CVMetalTexture 타입인 cameraTexture로 이름을 변경했다.

class Renderer:NSObject {
   .
   .
   .
  var textureCache: CVMetalTextureCache?
  var cameraTexture: CVMetalTexture?
   .
   .
   
  func initRenderTarget() {
    .
    .
    CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
    .
    .
  }
}

 

렌더링

처음 이미지 사이즈를 변환하는 루틴에선 카메라 입력에 대한 소스 텍스처를 매번 생성해 주어야 한다. 해당 텍스처는 화면렌더링 이후에 제거해주어야 하며, 제거하지 않으면 몇번 루프를 돌다가 동작이 멈춰 버린다.
카메라 출력에 설정한 픽셀포맷에 맞춰 CVMetalTexture를 생성한다.
렌더링할때 MTLTexture 가 필요하므로 해당 텍스처 형식으로 변환해서 프레그먼트 쉐이더로 전달

func renderToTransform(_ pixelBuffer:CVPixelBuffer ) -> Bool {
  let startTime = Int64((Date().timeIntervalSince1970 * 1000.0).rounded())
  guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { return false }
  commandBuffer.label = "RenderCommand1"

  let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0)
  let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0)
  let status = CVMetalTextureCacheCreateTextureFromImage(
                      nil,
                      self.textureCache!,
                      pixelBuffer,
                      nil,
                      .bgra8Unorm,
                      width,
                      height,
                      0,
                      &self.cameraTexture)
  if status != kCVReturnSuccess {
    return false
  }


  if let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: self.renderPassDescriptor)  {
    encoder.label = "RenderResizeEncoder"
    encoder.setCullMode(.front)
    encoder.setRenderPipelineState(self.renderPipelineState)
    encoder.setVertexBuffer(self.imageVertexBuffer, offset: 0, index: 0)
    encoder.setFragmentTexture(CVMetalTextureGetTexture(self.cameraTexture!), index: 0)
    encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
    encoder.endEncoding()
  }


  commandBuffer.commit()
  commandBuffer.waitUntilCompleted()

  let endTime = Int64((Date().timeIntervalSince1970 * 1000.0).rounded())
  print("complete: \(endTime - startTime)ms)")
  return true
}

 

블러 및 화면 렌더링

블러는 기존 샘플과 동일하게 kernel 쉐이더를 5회 적용.
화면 렌더링시에 위의 카메라 텍스처를 다시한번 넣어 주고, 예전 샘플에서 사용했던 마스크 이미지도 전달~
각 샘플에서 사용했던 코드들이라 딱히 설명할 내용이 없다...

func renderToDraw(view:MTKView) {
  let startTime = Int64((Date().timeIntervalSince1970 * 1000.0).rounded())
  guard let renderPass = view.currentRenderPassDescriptor else { return }
  guard let drawable = view.currentDrawable else { return }
  guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { return }
  commandBuffer.label = "RenderCommand2"


  // 블러
  let threadGroupCount = MTLSizeMake(16, 16, 1)
  let threadCountPerGroup = MTLSizeMake( self.workHTargetTexture!.width / threadGroupCount.width,
  	                                     self.workHTargetTexture!.height / threadGroupCount.height,
  	                                     1)

  for i in 0...5 {
    var inputTexture:MTLTexture = self.workVTargetTexture!
    if i == 0 {
      inputTexture = self.imageResizeTexture!
    }

    if let encoder = commandBuffer.makeComputeCommandEncoder() {
      encoder.setComputePipelineState(self.computeHPipelineState)
      encoder.setTexture(inputTexture, index: 0)
      encoder.setTexture(self.workHTargetTexture, index: 1)
      encoder.setBuffer(self.sharedDataBuffer, offset: 0, index: 0)
      encoder.dispatchThreadgroups(threadCountPerGroup, threadsPerThreadgroup: threadGroupCount)
      encoder.endEncoding()
    }

    if let encoder = commandBuffer.makeComputeCommandEncoder() {
      encoder.setComputePipelineState(self.computeVPipelineState)
      encoder.setTexture(self.workHTargetTexture, index: 0)
      encoder.setTexture(self.workVTargetTexture, index: 1)
      encoder.setBuffer(self.sharedDataBuffer, offset: 0, index: 0)
      encoder.dispatchThreadgroups(threadCountPerGroup, threadsPerThreadgroup: threadGroupCount)
      encoder.endEncoding()
    }
  }


  if let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPass) {
    encoder.label = "SwapEncoder"
    encoder.setCullMode(.front)
    encoder.setRenderPipelineState(self.imagePipelineState)
    encoder.setDepthStencilState(self.imageDepthState)
    encoder.setVertexBuffer(self.imageVertexBuffer, offset: 0, index: 0)
    encoder.setFragmentTexture(CVMetalTextureGetTexture(self.cameraTexture!), index: 0)
    encoder.setFragmentTexture(self.workVTargetTexture, index: 1)
    encoder.setFragmentTexture(self.maskingTexture!, index: 2)
    encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
    encoder.endEncoding()
  }


  commandBuffer.present(drawable)
  commandBuffer.commit()
  commandBuffer.waitUntilCompleted()

  self.cameraTexture = nil
  let endTime = Int64((Date().timeIntervalSince1970 * 1000.0).rounded())
  print("complete: \(endTime - startTime)ms)")
}

 

쉐이더

쉐이더는 특별히 변경할건 없고, 마지막 화면 렌더링을 위한 프레그먼트 쉐이더에 마스킹 이미지를 적용했다.

kernel void gaussianBlurHFunction(
                                 texture2d<float, access::read> input [[texture(0)]],
                                 texture2d<float, access::write> output [[texture(1)]],
                                 constant SharedData &sharedData [[buffer(0)]],
                                 uint2 gid[[thread_position_in_grid]]) {
    
    float3 sum = float3(0.0, 0.0, 0.0);
    for (int i=0;i<sharedData.tapCount;i++) {
        int index = i - (sharedData.tapCount - 1) / 2;
        uint2 id = uint2(gid.x + index, gid.y);
        sum += input.read(id).rgb * sharedData.gaussian[i];
    }
    
    float4 color = float4(sum, 1.0);
    output.write( color, gid);

}

kernel void gaussianBlurVFunction(
                                 texture2d<float, access::read> input [[texture(0)]],
                                 texture2d<float, access::write> output [[texture(1)]],
                                 constant SharedData &sharedData [[buffer(0)]],
                                 uint2 gid[[thread_position_in_grid]]) {
    
    float3 sum = float3(0.0, 0.0, 0.0);
    for (int i=0;i<sharedData.tapCount;i++) {
        int index = i - (sharedData.tapCount - 1) / 2;
        uint2 id = uint2(gid.x, gid.y + index);
        sum += input.read(id).rgb * sharedData.gaussian[i];
    }
    
    float4 color = float4(sum, 1.0);
    output.write( color, gid);

}

 

fragment float4 imageResizeFragmentFunction(ImageOut in [[stage_in]],
                                       texture2d<float> texture1 [[texture(0)]] ) {
    constexpr sampler colorSampler;
    float4 color = texture1.sample(colorSampler, in.texCoord);
    return color;
}


fragment float4 swapFragmentFunction(ImageOut in [[stage_in]],
                                     texture2d<float> texture1 [[texture(0)]],
                                     texture2d<float> texture2 [[texture(1)]],
                                     texture2d<float> texture3 [[texture(2)]]) {
    
    constexpr sampler colorSampler;
    float4 color = texture1.sample(colorSampler, in.texCoord);
    float4 bgColor = texture2.sample(colorSampler, in.texCoord);
    float4 masking = texture3.sample(colorSampler, in.texCoord);
    
    color = float4((bgColor.rgb * masking.r ) + (color.rgb * (1 - masking.r)), 1.0);
    return color;
}