[Swift] METAL

호랭이·2022년 10월 13일
2

🍎 Swift

목록 보기
12/13

Programming Metal in iOS

WWDC 2014에서 소개
iOS8에서 출시

GPU 가속 3D그래픽용 자체 API

3D 그래픽 하드웨어와 상호 작용하기 위한 저수준 API

OpenGL과의 차이점은 크로스 플랫폼이 아니라는 것
*크로스플랫폼: 여러 종류의 장치 플랫폼에서 동작할 수 있음
Apple 하드웨어에서 매우 효율적으로 설계되어 OpenGL에 비해 빠른 속도와 낮은 오버헤드를 제공한다.

SpriteKit, SceneKit 또는 Unity와 같은 상위 수준 프레임워크와의 차이점은?

Metal은 OpenGL과 유사한 저수준 3D그래픽 API지만 오버헤드가 낮을수록 성능이 향상된다.
*오버헤드: 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간, 메모리 등
이것은 GPU 위의 매우 얇은 레이어이다.

스프라이트, 3D 모델을 화면에 렌더링하는 것과 같은 거의 모든 작업을 수행하려면 모든 코드를 작성해야한다. 대신 개발자가 완전한 권한과 통제력을 가지고 있다.
반대로 SpriteKit, SceneKit 및 Unity와 같은 상위 수준 게임 프레임워크는 Metal 또는 OpenGL ES와 같은 하위 수준 3D 그래픽 API 위에 구축된다.

게임을 만드는 것 뿐이라면 상위 수준 게임 프레임워크를 사용하는 것이 더 용이하다.
하지만 Metal을 배워야하는 이유도 있다.
1. Metal은 low-level이기 때문에 하드웨어를 한계까지 사용하고 게임 작동 방식을 완전히 제어할 수 있다.
2. 3D 그래픽, 게임 엔진 구현, 게임 프레임워크 작동 방식에 대해 많은 것을 배울 수 있다.

Metal VS OpenGL

OpenGL은 크로스플랫폼으로 설계되어 C++로 작성할 수 있고 약간의 수정만 거치면 안드로이드와 같은 다른 플랫폼에서 실행할 수 있다.
하지만 Apple이 제품을 설계하는 방식에 있어 OS와 HW, SW를 완전한 패키지로 통합하기 위해 개발된 Apple HW용 그래픽 API가 설계되었다.
최신 기능을 지원하는 동시에 매우 낮은 오버헤드, 높은 성능을 목표로 설계되었다.
그 결과 OpenGL ES와 비교하여 앱에 최대 10✕개의 드로우 콜을 제공할 수 있는 Metal이 탄생했다.
*드로우콜: CPU는 현재 프레임에 어떤 것을 그려야할지 정하고, GPU는 오브젝트를 그리라는 명령을 호출하는데, 이 명령이 드로우콜이다.

시작하기

렌더링을 시작할 수 있도록 Metal을 설정하는데 필요한 7단계
1. MTLDevice
2. CAMetalLayer
3. Vertex Buffer
4. Vertex Shader
5. Fragment Shader
6. Render Pipeline
7. Command Queue

MTLDevice 생성

GPU에 대한 직접적인 연결. 이것을 사용하여 다른 모든 Metal개체(명령 대기열, 버퍼 및 텍스처 등)를 만든다.

CAMetalLayer 생성

iOS에서 화면에 보이는 모든 것은 CALayers의 하위 클래스인 gradient layers, shape layers, replicator layers와 같은 것들로 이루어진다.
Metal을 이용해서 화면에 무엇인가 그리기 위해서는 CAMetalLayer를 호출하여 하위 클래스인 CALayer가 필요하다.

metalLayer = CAMetalLayer() //1
metalLayer.device = device //2
metalLayer.pixelFormat = .bgra8Unorm //3
metalLayer.framebufferOnly = true //4
metalLayer.frame = view.layer.frame //5
view.layer.addSublayer(metalLayer) //6
  1. 새로운 CAMetalLayer 생성
  2. 레이어를 사용할 device 설정
  3. 픽셀 형식을 Blue, Green, Red, Alpha에 대해 8바이트 순서로, 0과 1 사이의 정규화된 값으로 설정
    *알파값: 픽셀 또는 이미지의 투명도를 나타냄. 0이면 투명하고 255이면 불투명하다.
  4. Apple은 이 레이어에 대해 생성된 텍스처에서 샘플링해야 하거나 레이어 드로어블 텍스처에서 컴퓨팅 커널을 활성화해야 하는 경우가 아니면 성능상의 이유로 true로 설정하기를 권장한다.
  5. 뷰의 프레임과 일치하도록 레이어의 프레임 설정
  6. 뷰의 기본 레이어의 하위 레이어로 추가

Vertex Buffer 생성

Metal의 모든 것은 삼각형이 기본이다. 복잡한 3D 모양도 일련의 삼각형으로 분해할 수 있다.
Metal의 기본 좌표계는 정규화된 좌표계이다. 기본적으로 (0, 0, 0.5)에 중심을 둔 2x2x1 큐브로 되어있다.

  let vertexData: [Float] = [
      0.0, 1.0, 0.0,
     -1.0, -1.0, 0.0,
      1.0, -1.0, 0.0
  ]
  
let dataSize = vertexData.count *  MemoryLayout .size(ofValue: vertexData[0]) // 1 
vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: []) // 2
  1. vertexData의 사이즈를 바이트 단위로 가져온다.
  2. CPU의 저장된 vertexData를 전달하여 GPU에 새 버퍼를 생성하기 위해 makeBuffer를 호출한다. MTLDevice 기본 구성을 위해 빈 배열을 전달한다.
    *버퍼: 데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리의 영역

Vertex Shader 만들기

이전 섹션에서 만든 정점은 Vertex Shader라고 하는 작은 프로그램에 대한 입력이 된다. Vertex Shader는 단순히 GPU에서 실행되는 작은 프로그램으로, Metal Shading Language라고 하는 C++과 유사한 언어로 작성되었다.
Vertex Shader는 정점당 한 번 호출되며 정점의 색상, 좌표와 같은 정보를 가져와 잠재적으로 수정된 위치 및 기타 데이터를 반환한다.

vertex float4 basic_vertex(                            // 1
  const device packed_float3 * vertex_array [[ buffer( 0 ) ]], // 2
  unsigned int vid [[ vertex_id ]]) {                  // 3
  return float4(vertex_array[vid], 1.0 );              // 4
}
  1. 모든 Vertex Shader는 vertex키워드로 시작된다. 함수는 꼭짓점의 최종 위치를 반환한다. float4를 표시하여 이 작업을 수행하고, 이름을 선언한다.
  2. 포인터. 각 정점의 위치(packed_float3)를 가르킨다. [[...]]와 같은 구문을 사용하여 추가 정보를 지정하는 속성을 선언한다. 여기서는 Metal코드에서 보내는 데이터의 첫번째가 이 매개변수가 된다.
  3. Vertex Shader는 속성과 함께 특수 매개변수도 사용한다. vertex_id의 인덱스로 이를 채운다.
  4. 3번의 vid를 기반으로 배열 내부의 위치를 찾아 반환한다. 또한 벡터를 float4로 변환한다.

Fragment Shader 생성

Vertex Shader가 완료된 후 Metal은 각 화면의 Fragment에 대해 Fragment Shader라는 또 다른 셰이더를 호출한다.
Fragment Shader는 Vertex Shader의 출력값을 보간하여 입력 값을 가져온다.
Fragment Shader의 역할은 각 Fragment에dml 최종 색상을 반환하는 것이다.

Render Pipeline

Vertex, Fragment Shader를 만들었으므로 이제 Render Pipeline이라는 특수 개체로 결합해야한다.
Metal은 셰이더가 미리 컴파일되고 Render Pipeline 구성이 처음 설정한 후에 컴파일된다.

// 1
let defaultLibrary = device.makeDefaultLibrary()!
let fragmentProgram = defaultLibrary.makeFunction(name: "basic_fragment")
let vertexProgram = defaultLibrary.makeFunction(name: "basic_vertex")
        
// 2
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.vertexFunction = vertexProgram
pipelineStateDescriptor.fragmentFunction = fragmentProgram
pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        
// 3
pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
  1. device.makeDefaultLibrary()를 호출하여 얻은 객체를 통해 프로젝트에서 미리 컴파일된 세이더에 접근할 수 있다. 그리고 각 셰이더를 이름으로 접근할 수 있다.
  2. 렌더 파이프라인 구성을 설정한다. 여기서는 사용하려는 셰이더와 픽셀 형식을 설정한다.
  3. pipelineState를 여기서 지정한 구성으로 설정한다.

Command Queue

GPU에 한 번에 하나씩 실행하도록 지시하는 명령들의 정렬된 목록이다.
MTLCommandQueue 객체를 생성하고 아래 코드를 실행한다.

commandQueue = device.makeCommandQueue()

렌더링

아래 5가지 단계로 실행된다.
1. Display Link 생성
2. Render Pass Descriptor 생성
3. Command Buffer 생성
4. Render Command Encoder 생성
5. Command Buffer Commit

profile
삐약

0개의 댓글