momodudu.zip

Metal 스터디(1) - 시작하기 본문

ios/Metal

Metal 스터디(1) - 시작하기

ally10 2021. 11. 23. 15:01

 

끄적끄적 메탈 공부하기. Metal by tutorials란 책을 참고하고 차근차근 따라가보면서 포스팅해보기~

 

모든 그래픽스 API가 그렇듯 삼각형 하나 출력하는데 설정할게 매우 많다... 과정을 일단 요약해보면,

 

1. Metal Rendering을 위한 초반 셋업 과정

1) MTLDevice 생성

- GPU랑 direct connection을 해주는 역할. GPU object를 이걸 통해서 만든다.

2) MTLBuffer 생성 

- CPU에서 생성한 데이터를 GPU로 보내기 위한 버퍼 역할.

3) MTLRenderPipeline 생성

- shaders, color attachment 포맷 등 여러가지 렌더링 config를 지정한 파이프라인을 여기서 생성한다.

 

2. 프레임마다 실행될 렌더링 루프 작성

1) Display link 생성

- os에서 렌더링이 필요할때마다 호출되는 display link를 생성해준다.

2) RenderPass Descriptor 생성

- 그려질 텍스쳐, 클리어 컬러는 뭘로 초기화할지등의 config를 전달하는 역할을 한다. 

3) Command Buffer 생성

- Command Buffer는 하나 이상의 Render command를 담고 있다.

4) Render Command Encoder 생성

- 전달한 버퍼의 내용중 n번째부터 k번째까지를 버텍스로 쓰고, primitive를 뭘로 쓸지..등 렌더링 커맨드 Encoder를 생성한다.

5) Commit Command Buffer

- 생성한 커맨드버퍼를 commit해야 렌더링 커맨드가 실행된다.

 

 

매우 생소한 단어들이 줄줄이 나오는데.. 하나씩 살펴보자.

 

 

1. MTLDevice

MTLDevice를 제일 먼저 생성해줘야한다. 이걸 만들어야만 커맨드 큐, MTLBuffer등을 만들 수 있다. 만드는건 아쥬 쉽다~ MTLDevice는 나와 GPU를 바로 연결해주는 객체로, GPU한테 이거만들어줘, 저거만들어줘 할 때 쓸 수 있다. 

_device = MTLCreateSystemDefaultDevice()

 

2. Command Queue, Buffer, Encoder

메탈에 대한 문서를 읽다보면 커맨드큐, 커맨드버퍼, 커맨드인코더 등 커맨드어쩌구에 대한 내용이 엄청 많이 나온다. 메탈에서 이 "커맨드"라 함은, GPU에 내리는 커맨드다. 즉, 그냥 하나하나가 API call이라고 생각하면 된다. 이 API콜을 하나하나 다 날리는게 아니고, 어떤 개념들로 묶어서 정의한다.

 

먼저, command encoder는 위에서 간단하게 설명했듯, 렌더 커맨드 그 자체라고 보면 된다. 어떤 버퍼를 쓸지 setting하고, drawTriangle()같은.. 그리고 이 렌더 커맨드에 있는 작업들은 매 프레임마다 실행된다. 

Command Buffer는 여러개의 Command Encoder를 묶은것이라고 보면 된다. 이것도 매 프레임마다 생성해줘야한다.

Command Queue는 여러개의 Command Buffer를 묶은것이다. 이건 한번만 만들면 된다.

 

즉, 크기로 봤을때 개념은 Command Queue > Command Buffer > Command Encoder 이런느낌?

 

커맨드큐는 최초 셋업당시 한번만 만들어주면 되고, 버퍼와 인코더는 렌더링 루프마다 생성한다.

_commandQueue = _device.makeCommandQueue()

커맨드 큐는 위처럼 MTLDevice를 통해서 생성할 수 있다.

 

3.. MTLBuffer

MTLBuffer는 CPU에서 생성한 데이터.. 정점이나 인덱스, 텍스쳐 좌표 등등.. 그릴때 필요한 모든 CPU에서 계산한 데이터들을 GPU로 올리는 역할을 담당한다. 

let dataSize = _vertexData.count * MemoryLayout.size(ofValue: _vertexData[0])
_vertexBuffer = _device.makeBuffer(bytes: _vertexData, length: dataSize, options: [])

 

MTLBuffer도 MTLDevice를 통해서 생성한다. _vertexData 요게 CPU에서 계산한 정점들이다. 해당 사이즈만큼의 MTLBuffer를 만들면서 _vertexData의 정점들을 카피한다.

 

4. Shader

metal의 쉐이더 랭귀지 MSL은 shader가 특이하게도 vertex shader랑 fragment shader가 하나의 파일 *.metal로 묶여있다. 그리고 이 쉐이더 function들은 pre-compiled 된다고 한다. 그래서 각 쉐이더 함수 이름을 바탕으로 좀 특이하게 쉐이더를 지정한다. 예를들어서, vertex shader 함수 이름은 vertex_main 이고 frag shader 이름은 frag_main이라고 가정하면,

let defaultLibrary = _device.makeDefaultLibrary()!      // 미리 컴파일한 쉐이더에 접근가능하게 만들어줌.
let fragmentProgram = defaultLibrary.makeFunction(name: "frag_main")   // frag shader. shader function name설정
let vertexProgram = defaultLibrary.makeFunction(name: "vertex_main")       // vert shader. shader function name설정

 

이렇게 함수 이름을 지정해서, 프로그램을 객체처럼 뽑아(?)올 수 있다.

 

그럼 이 뽑아온걸 어따쓰느냐?  그거슨 밑에서..

 

 

 

5. RenderingPipelineState

렌더링 루프를 돌 때, 작성한 command encoder가 어떤 pipeline state를 쓸 지 지정을 해준다. 즉, 내가 만든 렌더링 커맨드들은 이러이러한 파이프라인에서 돌아라! 라고 지정해줄 수 있다는것이다. 이런 렌더링 파이프라인은 매 프레임 생성하는게 아니고, 미리 만들어두고 RenderingPipelineState를 생성해놓고, 그 state를 command encoder마다 지정해주면 된다. 

 

일단 렌더링 파이프라인부터 만들어보자. 렌더링 파이프라인을 생성할 때, 파이프라인을 이러이러하게~~~ 만들어주세요. 라고 하는 RenderingPipelineDescriptor라는 타입이 있다. 이 디스크립터를 통해서 위에서 뽑아둔 쉐이더 프로그램 객체를 지정할 수 있다. 즉, 이 파이프라인에서는 이 프로그램을 써라! 라고 명시해줄 수 있다. 

let pipelineStateDescriptor = MTLRenderPipelineDescriptor() // 렌더링 파이프라인의 config를 설정해줌.
pipelineStateDescriptor.vertexFunction = vertexProgram      // 파이프라인에서 사용하는 각각의 쉐이더를 지정해주고,
pipelineStateDescriptor.fragmentFunction = fragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm   //  color attachment의 포맷을 설정해줌.

 

pipeline을 생성할 때 쓰는 디스크립터에서, 이 파이프라인에서는 이 vertex function과 fragment function을 쓰겠습니다. 라고 지정해주는것이다. 

 

// 이제 설정한 config를 바탕으로 렌더링 파이프라인을 생성한다.
_pipelineState = try! _device.makeRenderPipelineState(descriptor:  pipelineStateDescriptor)

만들어준 디스크립터를 바탕으로 파이프라인 스테이트를 생성해주세요~ 하고 만든다. 이 스테이트 객체를 계속 갖고 다니면서 command encoder에서 이 command는 이걸로 실행해! 라고 지정하게 된다. 오.. 오히려 gl보다 더 직관적인것 같다.

 

 

6. Render Passes

조명이나 그림자 같은 효과들은 매 프레임마다 계산이 이루어지는데, 이 계산 오버헤드가 엄청 크다. 그래서 이건 별도의 "렌더패스"에서 이루어진다. 예를 들어서, shadow를 그리는 렌더 패스는 그리고자 하는 model의 전체적인 부분을 그리고, 그 model의 gray scale 정보만을 가져온다. 또 다른 렌더패스에서는, 이 model을 원래 그리고자 하는 컬러로 그리는 식으로...

즉, shadow를 그린 렌더패스에서는 gray sclae만 존재하는 model의 그림자 텍스쳐 아웃풋이 나올거고, 두번째 렌더패스에서는 model을 그린 아웃풋이 나올텐데 이 두개를 결합해서 최종 결과를 화면에 띄우게 된다. 각 Command Encoder는 렌더패스와 1:1 매칭이 된다. 즉, shadow를 그리고자하는 command encoder 하나, 모델을 그리고자하는 command encoder 두번째, 이렇게 두개가 각각의 렌더패스를 거쳐 최종 결과가 렌더링된다.

 

일단 이 초장 예제에서는 하나의 렌더패스만 사용한다.

 

guard let drawable = _layer?.nextDrawable() else { return } // 메탈레이어에서 그릴 다음 drawable 판이 있는지 pool 체크해서 가져옴.
let renderDescriptor = MTLRenderPassDescriptor()
renderDescriptor.colorAttachments[0].texture = drawable.texture
renderDescriptor.colorAttachments[0].loadAction = .clear        // 매 렌더패스마다 컬러 어태치먼트에 요걸 따라서 초기화해줌.
renderDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 104.0/255.0, blue: 55.0/255.0, alpha: 1.0)
        
// 3. Command Buffer 생성
//  커맨드버퍼는 하나 이상의 렌더 커맨드를 담고 있어야 한다.
let commandBuffer = _commandQueue.makeCommandBuffer()!
        
// 4. RenderCommandEncoder 생성
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderDescriptor)!
renderEncoder.setRenderPipelineState(_pipelineState)
renderEncoder.setVertexBuffer(_vertexBuffer, offset: 0, index: 0)
        
// GPU에게 vertexBuffer에 0번째부터 3개의 버텍스를 가지고 삼각형을 그리라고 order를 내림.
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
renderEncoder.endEncoding()
        
// 5. commit
commandBuffer.present(drawable) // GPU가 현재 그리는게 끝나자마자 새로운 텍스쳐를 보여주도록 확인.
commandBuffer.commit()

 

위에가 렌더링 루프 전문이다. 메탈 레이어에서 drawable을 얻어오고, RenderPassDescriptor를 생성한다. 커맨드 버퍼를 생성하고, 렌더 커맨드 인코더를 생성한다. 이제 인코더에다가 실제 draw call을 쌓아둔다고 생각하면 된다. 아까전에 생성해두었던 파이프라인 state를 지정하고, 어떤 데이터를 쓸지 지정해준다. MTLBuffer로 전달이 가능하며, drawPrimitives call을 통해 어떤 프리미티브를 그릴지, 몇개로 그릴지 등등.. 을 지정해서 command Buffer를 commit해주면 렌더링 루프가 끝난다.

 

 

 

 

약간 순서가 얼레벌레지만.. 대충 정리를 해보자면,

1. Metal 초기화 - MTLDevice, MTLCommandQueue 생성

2. MTLBuffer 생성 - 그리고자 하는 모델 생성.

3. Pipeline생성 - 쉐이더 프로그램 지정

4. 렌더링 루프 - 렌더패스 생성 및 커맨드버퍼 & 커맨드버퍼 인코더 생성

 

의 순서이고, 1~3은 초기 한번만 셋업해주면 된다.