일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- 백준
- 네이버지도
- clustering
- 프로퍼티
- vm
- 먹튜브
- IOS
- NaverMap
- Firebase
- MeTal
- JJGram
- property
- 섯다족보
- 섯다족보앱
- cluster
- acmicpc
- xcframework
- 1002
- Oracle
- 클러스터링
- 음식리뷰앱
- 음식지도
- 먹튜브로드
- quadtree
- Swift
- 먹방지도
- SwiftUI
- StaticLib
- MVVM
- 프로퍼티 종류
- Today
- Total
아반떼오우너의 개발블로그 ㅋㅋ
iOS Metal을 이용한 렌더링 동작 구현 - [2] 본문
지난 포스팅에 이어서 씁니다.
이번 포스팅에서는 렌더링을 하기위해 Shader작성 및 GPU에 렌더작업을 요청하여 그림이 그려지는것까지 해보겠습니다.
저번 포스팅에선 Metal 렌더링을 위해 CAMetalLayer를 부모뷰에 올리고 2차원 평면의 VertexBuffer들까지 정의하였습니다.
이제 남은 절차를 정리하면 아래와 같습니다.
- VertexShader, FragmentShader 정의.
- Render Pipeline 정의.
- CommandQueue, RenderEncoder 정의.
- RenderEncoder에 명령어 설정 및 렌더링 진행.
그럼 위 절차대로 하나씩 보도록 하겠습니다.
[1] VertexShader, FragmentShader 정의
Shader(쉐이더)라는것은 간단하게 말하면 그림을 그리기위한 붓 혹은 연필 정도로 이해하시면 됩니다.
Shader 내부에는 어떻게 그릴지 혹은 어디다 그릴지 등의 정보가 담겨있어서 GPU는 이 Shader를 통하여 렌더링을 진행하게 됩니다.
쉐이더를 한번 정의해보도록 하겠습니다.
우선 쉐이더 언어는 Swift가 아닌 C++로 작성됩니다.
Metal 파일을 하나 생성해봅니다. 아래와같이 Metal File이라고 별도로 나뉘어져 있습니다.
생성 후 아래와같이 코드를 정의해봅니다.
아래의 코드를 보통 VertexShader라고 부르며 하는 동작은 CAMetalLayer의 어느위치에 Rendering을 할지 정하는 역할을 합니다.
Vertex정보와 Coordinate정보를 같이 활용하기 때문에 RasterizerData라는 Struct를 생성하여 하나의 값으로 활용합니다.
clipSpacePosition에는 이전 포스팅에서 설정했던 Vertex의 x,y값이 들어가있고
textureCoordiate에는 동일하게 이전의 Coordinate값이 들어있습니다.
Shader의 파라미터값에 대한 설명은 아래에 렌더작업때에 다시 설명하겠습니다.
따라서 이 VertexShader는 이전에 우리가 정의했던 Vertex정보들에 의해 '어느 위치에 그릴것인지' 알게됩니다.
다시 한번 정리하면 VertexShader는 무엇인가를 그리는 역할이 아닌, 어느 위치에 그릴지 정하는 쉐이더로 이해하시면 됩니다.
#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 = vertexArray[vertexID].y;
out.clipSpacePosition.z = 0.0;
out.clipSpacePosition.w = 1.0;
out.textureCoordinate = coordinateArray[vertexID];
return out;
}
그렇다면 이제 실제 색을 칠하는 FragmentShader를 정의해보도록 하겠습니다.
여기에서 사용할 FragmentShader는 굉장히 단순한 형태라 코드를 볼것도 없습니다.
float4의 자료형을 반환하는데 순서대로 r,g,b,a값을 가집니다.
지금은 Metal을 이용한 렌더링에 초점을 맞추기 때문에 적당한색을 렌더링하지만
추후에 포스팅할 내용인 이미지를 렌더링할때에는 Texture단위로 넘겨서 처리하기 때문에 FragmentShader가 다소 복잡해지게 됩니다.
지금은 FragmentShader가 이런역할이구나~ 라고 정도로 넘어가주시면 됩니다.
VertexShader가 정의한 모든 영역에 (0.1, 0.78, 0.32, 1.0)의 rgba값이 렌더링 됩니다.
fragment float4 TextureFragmentShader() {
return float4(0.1, 0.78, 0.32, 1.0);
}
[2] Render Pipeline 정의
이제 Shader까지 정의 했으니 본격적으로 렌더링 준비를 해봅시다.
준비를 많이한거같은데 아직도 그림하나 못그리고 있는게 좀 아이러니하긴 합니다..ㅋㅋ
RenderPipeLineState을 생성해야하는데 간단하게 정의하면 GPU에 전달할 명령에 사용할 쉐이더 함수 및 여러 구성 상태를 말합니다.
이를 정의하기위해 RenderPipeLine객체를 생성하는데 이때 우리가 위에서 정의한 Shader함수를 PipeLine에 설정해줘야합니다.
자세한 내용은 코드에 적혀있습니다.
최종적으로 Shader정보와 렌더픽셀정보가 담겨있는 RenderPipeLineState가 생성됩니다.
private var pipelineState: MTLRenderPipelineState!
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
self.pipelineState = try! self.device.makeRenderPipelineState(descriptor: renderPipeline)
[3] CommandQueue, RenderEncoder 정의
CommandQueue라는것은 말 그대로 명령어들의 큐입니다.
여기서 말하는 명령어들은 내가 어떤 VertexFunc를 쓸거고, 이 VertexFunc의 파라미터는 뭘넣을것이며, Render의 타입은 어떻게 할것인지 등 렌더에 필요한 모든 정보들을 담은것이라고 보면 됩니다.
최초에 생성했던 MTLDevice로부터 생성할수있으며 우리는 이것을 통해 CommandEncoder라는것을 만들수있습니다.
그럼 CommandQueue를 생성하고 이것으로부터 CommandEncoder를 만들어내는것까지 해보겠습니다.
var commandQueue: MTLCommandQueue!
// CommandQueue 생성
self.commandQueue = self.device.makeCommandQueue()
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)
우선 commandQueue를 하나 만들고
CAMetalLayer에게 물어봅니다. 혹시 Render할 준비가 되었느냐고. 이를 코드로 나타내면 nextDrawable()인데 만약 GPU가 Render준비가 된상태면 CAMetalDrawable객체를 반환해줍니다.
이 함수는 블로킹함수로 정할수도 있고 논블로킹함수로 정할수도있는데 이는 다음에 다뤄보겠습니다. 디폴트는 블로킹함수입니다.
CAMetalDrawable로부터 texture를 받아오고 이를 Descriptor에 넣어준뒤 RenderEncoder를 생성해줍니다.
RenderEncoder는 위에서 말한 명령어들의 집합이라고 보시면되며 여기에 여러가지 필요한 정보들을 세팅하게 됩니다.
[4] RenderEncoder에 명령어 설정 및 렌더링 진행.
이제 정말 마지막으로 Rendering하는 작업만 남았습니다.
우리는 위에서 RenderEncoder에 필요한 각종 정보를 넣는다고 하였습니다.
아래를 보시면 RenderEncoder에 pipelineState부터 시작해서 VertexBuffer, RenderType등 여러가지 정보를 넣어주고 있습니다.
이제 위에서 VertexShader의 파라미터들이 무엇을 뜻하는지 설명하겠다고 한부분에 대해서 적어보려합니다.
VertexShader의 파라미터들을 보면 vertexArray, coordinateArray 이렇게 2가지가 buffer(0), buffer(1)의 인덱스에 매핑되어있는것을 볼수있는데 여기서 0번째 1번째가 아래의 setVertexBuffer의 index입니다.
저는 0번 인덱스에 vertexBuffer를 설정했고, 1번째 인덱스에 coordinateBuffer를 설정했습니다.
쉐이더는 이정보들을 보고 0번째와 1번째에 각각 어떤값이 들어있는지 알수있습니다.
필요한 정보들의 설정이 끝나면 endEncoding을 통해 하나의 Encode 단위를 정리하고
CommandBuffer에 Commit을 진행합니다.
renderEncoder?.setRenderPipelineState(self.pipelineState)
renderEncoder?.setVertexBuffer(self.vertexBuffer, offset: 0, index: 0)
renderEncoder?.setVertexBuffer(self.coordinateBuffer, offset: 0, index: 1)
renderEncoder?.drawIndexedPrimitives(type: .triangle, indexCount: 6, indexType: .uint16, indexBuffer: indexBuffer, indexBufferOffset: 0)
renderEncoder?.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
여기까지 하면 우리가 설정한 값들이 GPU에 전달되며 렌더링 작업이 진행됩니다.
결과는 아래와 같습니다.
그림만 보면 그냥 UIView를 띄워놓은것 같지만 우리는 Metal이라는 Graphics API를 이용하여 초록색의 네모박스를 그린것입니다.
다음 포스팅에서는 위의 코드를 이용하여 실제 사용해볼만한 예제인 이미지를 Rendering하는 코드를 작성해보도록 하겠습니다.
전체코드는 아래와 같습니다.
ViewController
import UIKit
import Metal
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!
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
self.pipelineState = try! self.device.makeRenderPipelineState(descriptor: renderPipeline)
self.commandQueue = self.device.makeCommandQueue()
render()
}
func render() {
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?.drawIndexedPrimitives(type: .triangle, indexCount: 6, indexType: .uint16, indexBuffer: indexBuffer, indexBufferOffset: 0)
renderEncoder?.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
}
Shader
#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 = vertexArray[vertexID].y;
out.clipSpacePosition.z = 0.0;
out.clipSpacePosition.w = 1.0;
out.textureCoordinate = coordinateArray[vertexID];
return out;
}
fragment float4 TextureFragmentShader() {
return float4(0.1, 0.78, 0.32, 1.0);
}
'iOS' 카테고리의 다른 글
RxSwift Bind에 대한 고찰 (0) | 2022.01.25 |
---|---|
[iOS] Custom Font 사용에 따른 Text 위치 보정 (0) | 2021.12.24 |
iOS Metal을 이용한 이미지 YUV 렌더링 (0) | 2021.12.10 |
iOS Metal을 이용한 렌더링 동작 구현 - [1] (0) | 2021.12.09 |
JJGram - SwiftUI로 전환해보기 [1] (0) | 2021.12.08 |