SWIFT개발

Metal - swift

2료일 2023. 4. 22. 20:31

 

3D작업에는 변화를 주는 것도 많어 수학적인 계산이 요구되는데 소프트웨어로는 한계가 있어서 하드웨어 가속기능이 필수적이다.

이러한 복잡한 연산 + 하드웨어 제어를 도와주는 것이 3D 그래픽라이브러인데 OpenGL이 여기포함.

OpenGL ES라는 것이 있는데 3차원 그래픽스 API인데 휴대폰같은 임베디드 시스템을 위한 API.
원래는 위에거를 썻는데 2018년도에 사라지면서 메탈이 본격적으로 활성화되었다. 차이점은 크로스플랫폼이 아닌 애플하드웨어에 맞춰 설계되어 빠른 속도와 낮은 오버헤드를 제공한다는 것이다.

SpriteKit, SceneKit, Unity등 상위 프레임워크는 메탈또는 오픈지엘위에 있어 boilerplateCode(최소한의 변경으로 반복적인 일을 줄이고 코드도 개발시간도 단축시켜주는 코드 ex)creat-react-app)가 있어 지금당장 게임을 만드려면 상위프레임워크를 쓰는것이 당연하다.

하지만!! 그럼에도 불구하고 내가 왜??힘들고 힘든 메탈을 배우려 할까?

1. 메탈이 로우레벨이기 때문에 하드웨어를 한계까지 사용하며 내가 원하는데로 모든방향 커스텀이 가능해진다.

2. 어차피 거의 모든킷의 기본이기때문에 작동방식에 대해 더 이해할수 있을것 같다

----------------------------------

1. MTLDevice 생성. 

GPU에 직접적인 연결하는 인터페이스. 다른 모든 METAL 개체(명령대기열, 버퍼 및 텍스처 등)을 만든다. (프로토콜임)

이후에 앱과 상호작용하는 모든 메탈객체는 런타임에 획득한 MTLDevice 인스턴스에서 가져온다.처음에만 초기화되고 그 후 재사용되도록 설계

 

2. CAMetalLayer 생성.  

화면에 무언가 그리려면 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

2. 레이어를 사용할 device 생성

3. 픽셀형식을 Blue,Green,Red,Alpha에 대해 8바이트 순서로 0과 1사이 정규화된 값으로 설정한다.

4. 생성된 텍스쳐에대해 샘플링해야 하거나 layer drawbla texture에서 컴퓨팅 커널 활성화하는경우아니라면 일반적으로 true

5. 뷰의 프레임에 맞춰 metalLayer 프레임 설정

6, 뷰의 기본레이어의 하위레이어로 추가.

3. Vertex Buffer생성

"메탈의 모든것은 삼각형"

이게 매우 중요하다. 만약 좌표를 구한 데이터가 있다면 이데이터를 MTLBuffer라는 것을 통해 GPU로 보내야한다.

var vertexBuffer: MTLBuffer!
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 기본구성을 위해 빈 배열 전달.

4. 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키워드 필요. 반환값은 꼭짓점의 최종위치를 float4로 반환. 

4. vertex array 받아서 거기 vid꺼 리턴.

5. fragment shader 
각 fragment의 최종 색상 반환
fragment half4 basic_fragment() { // 1
  return half4(1.0);              // 2
}

keyword is  fragment!! 

6. Render pipeline
var pipelineState: MTLRenderPipelineState!
// 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)

shader가 precompile되고 renderpipeline이 처음 설정 된 후 컴파일이 된다.

1번째를 통해 프로젝트에 포함된 precompile된 shader 접근할수 있다.

7. Creating a command queue

GPU에서 생성할 명령 버퍼 구성하는 대기열!

var commandQueue: MTLCommandQueue!
commandQueue = device.makeCommandQueue()

 

놀랍게도 여기까지가 세팅이라는 것이다. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ아직 아무것도 안한거임;;

 역시 하이레벨 사랑....아니!다시 돌아가자


var timer: CADisplayLink!
timer = CADisplayLink(target: self, selector: #selector(gameloop))
timer.add(to: RunLoop.main, forMode: .default)
func render() {
  // TODO
}

@objc func gameloop() {
  autoreleasepool {
    self.render()
  }
}

gameloop는 각각프레임마다 이제 render()함수를 호출한다.

2. Creating  a render pass descriptor
guard let drawable = metalLayer?.nextDrawable() else { return }
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(
  red: 0.0, 
  green: 104.0/255.0, 
  blue: 55.0/255.0, 
  alpha: 1.0)

렌더링할 texture, clearcolor, 기타 configuration구성하는ㄱ객체.

3. creating command buffer
let commandBuffer = commandQueue.makeCommandBuffer()!

render command 목록. 신기한것은  command buffer commit할때까지 아무일도 안일어남.

so? 내가 하고 싶어하는 작업이 발생하는 시점을 제어가능해진다.

4. Creating a Render Command Encoder
let renderEncoder = commandBuffer
  .makeRenderCommandEncoder(descriptor: renderPassDescriptor)!
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder
  .drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
renderEncoder.endEncoding()

 

 

마지막 명령줄이 삼각형을 그려주는 것. vertex buffer기준으로GPU에 삼각형 set 그리도록 지시. 하나만 그려서 InstanceCOunt = 1

5. commit
commandBuffer.present(drawable)
commandBuffer.commit()

 

커밋을 해야 무슨일이 이러난다.

 

링크 : https://www.kodeco.com/7475-metal-tutorial-getting-started#toc-anchor-008