아반떼오우너의 개발블로그 ㅋㅋ

iOS Metal을 이용한 이미지 YUV 렌더링 본문

iOS

iOS Metal을 이용한 이미지 YUV 렌더링

Avante 2021. 12. 10. 15:32

https://avane.tistory.com/7 의 포스팅에 이어서 작성됩니다.

 

이번에는 Metal을 이용해서 직접 이미지를 한번 그려보도록 하겠습니다.

우선 Texture(텍스쳐)라는 개념을 먼저 알아야 합니다.

 

1. 텍스쳐?

텍스쳐란 보통 그래픽스에서 사용하는 일종의 오브젝트 입니다.

Shader의 입력데이터로 사용되거나 화면에 그려지는 타겟으로 사용됩니다. 좀 단순화 시켜서 화면에 그려질 이미지데이터, shader 처리를 거칠 이미지 데이터 정도로 생각하고 넘어갑니다.

 

그렇다면 이전 포스팅에서 우리는 FragmentShader에서 컬러 상수값을 반환하여 모든 MetalLayer에 동일한 색으로 칠해지는것을 보았습니다.

이번에는 FragmentShader에서 위의 텍스쳐를 이용하여 실제 이미지를 출력해보도록 하겠습니다.

기본적인 베이스 코드는 저번에 사용하던 코드를 그대로 사용해보겠습니다.

 

저는 구현에 쓸 이미지를 아래와같이 티몬의 메인화면을 이용해보도록 하겠습니다.

 

전체적인 절차는 아래와 같습니다.

(대부분의 코드를 저번에 작성했기 때문에 더 할만한게 없습니다 ㅋㅋ)

[1] 출력하고자 하는 이미지의 MTLTexture 생성

[2] FragmentShader에 Texture 전달

[3] FragmentShader에서 렌더링

 

그럼 위의 절차를 코드로 한번 확인해보겠습니다.

 

[1] 출력하고자 하는 이미지의 MTLTexture 생성

우선 이미지의 텍스쳐를 생성해야합니다. 위에서 말했듯이 텍스쳐는 쉐이더가 그릴 타겟입니다.

일반 UIImage와는 다른것임을 유의해주세요.

MetalKit을 Import한뒤 MTKTextureLoader를 이용해 손쉽게 Texture를 생성할수있습니다.

import MetalKit

let targetImage = UIImage.init(named: "IMG_5437.PNG")
self.imageTextureLoader = MTKTextureLoader.init(device: device)
let targetTexture = try! self.imageTextureLoader.newTexture(cgImage: targetImage!.cgImage!, options: nil)

 

[2] FragmentShader에 Texture전달

이전 코드에서 renderEncoder에 VertexBuffer만 설정했었는데

이제 FragmentTexture도 같이 설정해줍니다.

여기서 유의할건 index가 0이라는것인데 이때 쉐이더 쪽에서도 파라미터의 인덱스값을 0으로 해주어야합니다.

renderEncoder?.setFragmentTexture(targetTexture, index: 0)

 

[3] FragmentShader에서 렌더링

이번 포스팅에서 가장많이 변경되는 부분일것같습니다.

기존 FragmentShader는 파라미터도 없었고 안에 구현내용도 상수 컬러값을 반환하는게 전부였다면

이번에는 Texture를 파라미터도 받고 이를 rgb값으로 반환해보도록 하겠습니다.

절차[2]에서 말했듯이 inImageTexture라는 파라미터의 인덱스는 0번이고 이것은 renderEncoder에 등록한 인덱스번호입니다.

그리고 해당 텍스쳐에서 rgb값을 추출하여 반환해주도록 합니다.

fragment float4 TextureFragmentShader(RasterizerData in [[stage_in]],
                                      texture2d<half, access::sample>  inImageTexture  [[texture(0)]]) {
  constexpr sampler textureSampler (mag_filter::linear,
                                    min_filter::linear,
                                    s_address::clamp_to_edge,
                                    t_address::clamp_to_edge);

  
  float3 rgbColors = float3(inImageTexture.sample(textureSampler, in.textureCoordinate).rgb);
  return float4(rgbColors, 1.0);
}

추가로 VertexShader도 약간 수정해주도록 하겠습니다.

이유는 모르겠으나 Metal의 Y축이 OpenGL을 사용하던때와 다르게 반전되는것같습니다.

아직까지 원인은 못찾았으나 VertexShader의 Y값을 반전시키도록 하였습니다.

out.clipSpacePosition.y = -1.0 * vertexArray[vertexID].y;

이렇게까지 작성하면 GPU는 VertexShader가 지정하는 영역에 해당 텍스쳐를 렌더링 합니다.

결과는 아래와 같이 기기에서 티몬의 메인화면이 그려집니다.

 

 

전체코드는 아래와 같습니다.

 

ViewController.swift

//
//  ViewController.swift

import UIKit
import Metal
import MetalKit

class ViewController: UIViewController {
  private var device: MTLDevice!
  private var metalLayer: CAMetalLayer!
  
  let vertexInfo: [Float] = [
    -1.0, -1.0,
    1.0, -1.0,
    -1.0, 1.0,
    1.0, 1.0
  ]
  let coordinateInfo: [Float] = [
    0.0, 0.0,
    1.0, 0.0,
    0.0, 1.0,
    1.0, 1.0
  ]
  let indexInfo: [ushort] = [
    0, 2, 1, 2, 1, 3
  ]
  
  private var vertexBuffer: MTLBuffer!
  private var coordinateBuffer: MTLBuffer!
  private var indexBuffer: MTLBuffer!
  
  private var pipelineState: MTLRenderPipelineState!
  var commandQueue: MTLCommandQueue!
  
  var imageTextureLoader: MTKTextureLoader!

  override func viewDidLoad() {
    super.viewDidLoad()
    
    self.device = MTLCreateSystemDefaultDevice()
    
    self.metalLayer = CAMetalLayer()
    self.metalLayer.device = self.device
    self.metalLayer.pixelFormat = .bgra8Unorm
    self.metalLayer.frame = CGRect.init(x: 45.0, y: 100.0, width: 300, height: 600)
    self.view.layer.addSublayer(self.metalLayer)
    
    let vertexSize = vertexInfo.count * MemoryLayout<Float>.size
    let coordinateSize = coordinateInfo.count * MemoryLayout<Float>.size
    let indexSize = indexInfo.count * MemoryLayout<ushort>.size
    self.vertexBuffer = self.device.makeBuffer(bytes: vertexInfo, length: vertexSize, options: [])!
    self.coordinateBuffer = self.device.makeBuffer(bytes: coordinateInfo, length: coordinateSize, options: [])!
    self.indexBuffer = self.device.makeBuffer(bytes: indexInfo, length: indexSize, options: [])!
    
    let defaultLibrary = self.device.makeDefaultLibrary()!
    let vertexFunc = defaultLibrary.makeFunction(name: "TextureVertexShader")
    let fragmentFuc = defaultLibrary.makeFunction(name: "TextureFragmentShader")
    
    let renderPipeline = MTLRenderPipelineDescriptor()
    renderPipeline.vertexFunction = vertexFunc
    renderPipeline.fragmentFunction = fragmentFuc
    renderPipeline.colorAttachments[0].pixelFormat = .bgra8Unorm
    
    let targetImage = UIImage.init(named: "IMG_5437.PNG")
    self.imageTextureLoader = MTKTextureLoader.init(device: device)
    let targetTexture = try! self.imageTextureLoader.newTexture(cgImage: targetImage!.cgImage!, options: nil)
    
    self.pipelineState = try! self.device.makeRenderPipelineState(descriptor: renderPipeline)
    
    self.commandQueue = self.device.makeCommandQueue()
    
    render(targetTexture: targetTexture)
  }
  
  func render(targetTexture: MTLTexture) {
    guard let drawable = self.metalLayer.nextDrawable() else { return }
    let renderpassDescriptor = MTLRenderPassDescriptor()
    renderpassDescriptor.colorAttachments[0].texture = drawable.texture
    renderpassDescriptor.colorAttachments[0].loadAction = .clear
    
    let commandBuffer = self.commandQueue.makeCommandBuffer()!
    let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderpassDescriptor)
    renderEncoder?.setRenderPipelineState(self.pipelineState)
    renderEncoder?.setVertexBuffer(self.vertexBuffer, offset: 0, index: 0)
    renderEncoder?.setVertexBuffer(self.coordinateBuffer, offset: 0, index: 1)
    renderEncoder?.setFragmentTexture(targetTexture, index: 0)
    renderEncoder?.drawIndexedPrimitives(type: .triangle, indexCount: 6, indexType: .uint16, indexBuffer: indexBuffer, indexBufferOffset: 0)
    renderEncoder?.endEncoding()
    
    commandBuffer.present(drawable)
    commandBuffer.commit()
  }
  
  
}

MetalShader.metal

//
//  MetalShaders.metal

#include <metal_stdlib>
using namespace metal;

typedef struct {
  float4 clipSpacePosition [[position]];
  float2 textureCoordinate;
} RasterizerData;

vertex RasterizerData
TextureVertexShader(uint vertexID [[ vertex_id ]],
                    constant float2 *vertexArray [[ buffer(0) ]],
                    constant float2 *coordinateArray [[ buffer(1) ]])
{
  RasterizerData out;

  out.clipSpacePosition.x = vertexArray[vertexID].x;
  out.clipSpacePosition.y = -1.0 * vertexArray[vertexID].y;
  out.clipSpacePosition.z = 0.0;
  out.clipSpacePosition.w = 1.0;
  out.textureCoordinate = coordinateArray[vertexID];

  return out;
}

fragment float4 TextureFragmentShader(RasterizerData in [[stage_in]],
                                      texture2d<half, access::sample>  inImageTexture  [[texture(0)]]) {
  constexpr sampler textureSampler (mag_filter::linear,
                                    min_filter::linear,
                                    s_address::clamp_to_edge,
                                    t_address::clamp_to_edge);
  
  float3 rgbColors = float3(inImageTexture.sample(textureSampler, in.textureCoordinate).rgb);
  return float4(rgbColors, 1.0);
}