본문 바로가기

프로그래밍/iOS,macOS

[Metal] 이미지 렌더링~ 텍스처에 렌더링

이번엔 텍스처에 렌더링하고, 해당 텍스처를 화면에 렌더링하는 예제이다.
예제에서는 일반 사이즈보다 훨씬 작은 64x64 크기의 렌더타겟 텍스처를 생성해 렌더링을 진행했다.


여전히 기존 코드들은 동일.

버텍스

let kImagePlaneVertexData:[Float] = [
    -1.0, -1.0, 0.0, 1.0,
    1.0, -1.0, 1.0, 1.0,
    -1.0, 1.0, 0.0, 0.0,
    1.0, 1.0, 1.0, 0.0,
]

 

기본 프로퍼티들 선언
텍스처로 렌더링하기 위해 MTLRenderPipelineState와 MTLRenderPassDescriptor 가 추가되고, 
렌더타겟인 MTLTexture, 프래그먼트 쉐이더 MTLFunction 이 추가되었다.

    var device:MTLDevice!
    
    var commandQueue: MTLCommandQueue!
    
    var imageVertexBuffer: MTLBuffer!
    var imagePipelineState: MTLRenderPipelineState!
    
    var renderTargetTexture: MTLTexture?
    var renderPassDescriptor: MTLRenderPassDescriptor!
    var renderPipelineState: MTLRenderPipelineState!
    
    var imageTexture: MTLTexture?
    var imageDepthState:MTLDepthStencilState!
    
    var imageVertexFunction: MTLFunction!
    var renderScreenFunction: MTLFunction!
    var renderTextureFragmentFunction: MTLFunction!

 

초기화
초기화의 경우 렌더타겟과 화면렌더링을 위해 구분해서 진행.
쉐이더 함수들을 읽어들이고, 버텍스 버퍼를 설정한다.

func initMetal() {
  guard let defaultLibrary = try? self.device.makeDefaultLibrary(bundle: Bundle(for: Renderer.self)) else {
  print("[Renderer.initMetal] init error")
    return
  }

  imageVertexFunction = defaultLibrary.makeFunction(name: "imageVertexFunction")
  renderScreenFragmentFunction = defaultLibrary.makeFunction(name: "swapFragmentFunction")
  renderTextureFragmentFunction = defaultLibrary.makeFunction(name: "imageFragmentFunction")

  self.commandQueue = self.device.makeCommandQueue()

  let size = kImagePlaneVertexData.count * MemoryLayout<Float>.size
  imageVertexBuffer = self.device.makeBuffer(bytes: kImagePlaneVertexData, length: size)
  imageVertexBuffer.label = "ImageVertexBuffer"


  initRederTarget()
  initSwapRender()

  self.imageTexture = loadTexture(name:"sample", ext:"png")
}

 

텍스처로딩

지난 예제와 동일한 path에서 텍스처를 읽어들이는 메쏘드

func loadTexture(name:String, ext:String) -> MTLTexture? {
  let textureLoader = MTKTextureLoader(device: device)
  if let url = Bundle(for: Renderer.self).url(forResource: name, withExtension: ext) {
    let texture = try? textureLoader.newTexture(URL: url , options: nil)
    return texture
  }
  return nil
}

 

 

렌더타겟 초기화
화면렌더링과 동일하게 텍스처에 렌더링하기 위한 버텍스 정보와 파이브라인 정보를 기술한다. 만약 깊이버퍼를 사용할 일이 없는 경우(평면 이미지 렌더링 등..)  깊이버퍼는 invalid로 설정한다. (만약 깊이 버퍼가 필요한 경우 깊어버퍼용 텍스처를 별도 구성해 주어야 함)
예제에서는 64x64 크기의 텍스처를 생성하고, 해당 텍스처에 대한 렌더패스를 구성했다.

func initRederTarget() {
  let imageVertexDescriptor = MTLVertexDescriptor()
  imageVertexDescriptor.attributes[0].format = .float2
  imageVertexDescriptor.attributes[0].offset = 0
  imageVertexDescriptor.attributes[0].bufferIndex = 0
  imageVertexDescriptor.attributes[1].format = .float2
  imageVertexDescriptor.attributes[1].offset = 8
  imageVertexDescriptor.attributes[1].bufferIndex = 0
  imageVertexDescriptor.layouts[0].stride = 16
  imageVertexDescriptor.layouts[0].stepRate = 1
  imageVertexDescriptor.layouts[0].stepFunction = .perVertex


  let imagePipelineDescriptor = MTLRenderPipelineDescriptor()
  imagePipelineDescriptor.label = "ImageRenderPipeline"
  imagePipelineDescriptor.sampleCount = 1
  imagePipelineDescriptor.vertexFunction = imageVertexFunction
  imagePipelineDescriptor.fragmentFunction = renderTextureFragmentFunction
  imagePipelineDescriptor.vertexDescriptor = imageVertexDescriptor
  imagePipelineDescriptor.depthAttachmentPixelFormat = .invalid
  imagePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm

  do {
    try self.renderPipelineState = self.device.makeRenderPipelineState(descriptor: imagePipelineDescriptor)
  } catch let error {
    print("error=\(error.localizedDescription)")
  }

  let texDescriptor = MTLTextureDescriptor()
  texDescriptor.textureType = MTLTextureType.type2D
  texDescriptor.width = 64
  texDescriptor.height = 64
  texDescriptor.pixelFormat = .bgra8Unorm
  texDescriptor.storageMode = .shared
  texDescriptor.usage = [.renderTarget, .shaderRead]

  self.renderTargetTexture = self.device.makeTexture(descriptor: texDescriptor)

  let clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)

  self.renderPassDescriptor = MTLRenderPassDescriptor()
  self.renderPassDescriptor.colorAttachments[0].texture = self.renderTargetTexture
  self.renderPassDescriptor.colorAttachments[0].loadAction = .clear
  self.renderPassDescriptor.colorAttachments[0].clearColor = clearColor
  self.renderPassDescriptor.colorAttachments[0].storeAction = .store

}

 

화면 렌더링 설정

기존 화면 렌더링과 동일한 내용. 버텍스는 동일하게 사용하므로, descriptor 들은 위 텍스처 렌더링항목과 동일하게 사용해도 된다.

func initSwapRender() {
  let imageVertexDescriptor = MTLVertexDescriptor()
  imageVertexDescriptor.attributes[0].format = .float2
  imageVertexDescriptor.attributes[0].offset = 0
  imageVertexDescriptor.attributes[0].bufferIndex = 0
  imageVertexDescriptor.attributes[1].format = .float2
  imageVertexDescriptor.attributes[1].offset = 8
  imageVertexDescriptor.attributes[1].bufferIndex = 0
  imageVertexDescriptor.layouts[0].stride = 16
  imageVertexDescriptor.layouts[0].stepRate = 1
  imageVertexDescriptor.layouts[0].stepFunction = .perVertex


  let imagePipelineDescriptor = MTLRenderPipelineDescriptor()
  imagePipelineDescriptor.label = "ImageRenderPipeline"
  imagePipelineDescriptor.sampleCount = 1
  imagePipelineDescriptor.vertexFunction = imageVertexFunction
  imagePipelineDescriptor.fragmentFunction = renderScreenFragmentFunction
  imagePipelineDescriptor.vertexDescriptor = imageVertexDescriptor
  imagePipelineDescriptor.depthAttachmentPixelFormat = .depth32Float
  imagePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm

  do {
    try self.imagePipelineState = self.device.makeRenderPipelineState(descriptor: imagePipelineDescriptor)
  } catch let error {
    print("error=\(error.localizedDescription)")
  }

  // depth state
  let depthDescriptor = MTLDepthStencilDescriptor()
  depthDescriptor.depthCompareFunction = .lessEqual
  depthDescriptor.isDepthWriteEnabled = true
  self.imageDepthState = self.device.makeDepthStencilState(descriptor: depthDescriptor)
}

 

 

렌더링

렌더링은 렌더타겟 텍스처 렌더링 encoder 를 생성해 렌더링하고, 화면 렌더링 encoder 를 생성해 렌더링하는 순서로 진행한다.
각 렌더링에 맞는 render pipeline state 지정하고, 각 쉐이더 함수를 지정하는 부분은 동일.

func render(view:MTKView) {
  print("render")

  guard let renderPass = view.currentRenderPassDescriptor else { return }
  guard let drawable = view.currentDrawable else { return }
  guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { return }
  commandBuffer.label = "RenderCommand"

  if let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: self.renderPassDescriptor) {
    encoder.label = "RenderEncoder"
    encoder.setCullMode(.front)
    encoder.setRenderPipelineState(self.renderPipelineState)
    encoder.setVertexBuffer(self.imageVertexBuffer, offset: 0, index: 0)
    encoder.setFragmentTexture(self.imageTexture!, index: 0)
    encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
    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(self.renderTargetTexture, index: 0)
    encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
    encoder.endEncoding()
  }


  commandBuffer.present(drawable)
  commandBuffer.commit()
}

 

쉐이더

이 예제에서는 동일한 내용이지만 하나의 버텍스 쉐이더와 두개의 프래그먼트 쉐이더로 나누어 구성했다.

#include <metal_stdlib>
#include <simd/simd.h>

using namespace metal;


typedef struct {
    float3 position [[attribute(0)]];
    float2 texCoord [[attribute(1)]];
} ImageVertex;

typedef struct {
    float4 position [[position]];
    float2 texCoord;
} ImageOut;


vertex ImageOut imageVertexFunction( ImageVertex in [[stage_in]]) {
    ImageOut out;
    
    
    float4 position = float4(in.position, 1.0);
    out.position = position;
    out.texCoord = in.texCoord;
    return out;
}

fragment float4 imageFragmentFunction(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)]]) {
    
    constexpr sampler colorSampler;
    float4 color = texture1.sample(colorSampler, in.texCoord);
    return color;
}