momodudu.zip

Metal 스터디(2) - Vertex Descriptions / Metal Coordinates System 본문

ios/Metal

Metal 스터디(2) - Vertex Descriptions / Metal Coordinates System

ally10 2021. 11. 24. 14:03

 

cpu에서 만든 데이터를 gpu로 로드하기 위해서, 메탈에서는 MTLBuffer를 쓴다. 

 

처음에 나온 예제에서는 vertex(x,y,z) 이렇게 position float3 포맷으로 MTLBuffer를 만들었다. 그치만 좀 더 복잡한 렌더링으로 들어가면 이 정점 하나에 position외에도 노말이나 텍스쳐 좌표까지 하나의 정점에 매칭된다.

예를들면 하나의 v1 = [px, py, pz, nx, ny, u, v] 이렇게 여러개의 attribute들이 매칭된다. 그래서 gpu에서 이 버퍼를 해석할 수 있도록 도움을 줘야 하는데, 그게 바로 vertex description이다.

 

// vertex descriptor 설정
let vertexDescriptor = MTLVertexDescriptor()
vertexDescriptor.attributes[0].format = .float3
vertexDescriptor.attributes[0].offset = 0
vertexDescriptor.attributes[0].bufferIndex = 0

vertex descriptor는 파이프라인 디스크립터 생성 단계에서 해줄 수 있다. 즉, 이 쉐이더를 쓰는 정점들은 이렇게 구성된다! 라고 알려주는것이다.

 

vertexDescriptor를 생성하고, 첫번째 어트리뷰트를 설정해준다. 정점 3개 포지션으로만 썼으니, float3(x,y,z)로 지정한다. Metal에서는 이 어트리뷰트를 31개까지 넣을 수 있다고 한다.(왜 32개가 아니지..?) 

offset은 데이터가 버퍼 내에서 어디서 시작하는지를 명시해준다. bufferIndex는 offset이랑 뭐가 다른거지..?하고 봤는데, MTLBuffer안에 넣고 GPU로 보낼 때, buffer를 index로 확인한다고 한다. 즉, vertext layout이라고 보면 된다. Metal에서는 31개(이것도 왜 32개가 아니지..?)의 버퍼를 buffer argument table에서 관리해준다고 한다. 

 

vertexDescriptor.layouts[0].stride = MemoryLayout<SIMD3<Float>>.stride

let meshDescriptor = MTKModelIOVertexDescriptorFromMetal(vertexDescriptor)
(meshDescriptor.attributes[0] as! MDLVertexAttribute).name = MDLVertexAttributePosition

 

이어서.. 0번째 attribute의 stride를 지정해준다. position x,y,z를 쓰므로 float3의 형태로 스트라이드를 지정해준다. 그리고 예제를 따라서 train obj 파일을 임포트해서 MDLMesh를 사용할것이므로... Model I/O 사용을 위해 descriptor를 지정해줘야 하는데, 이게 vertex descriptor랑 조금 다르기때문에 생성한 Metal Descriptor를 바탕으로 Model I/O descriptor를 생성한다.

 

 

let meshDescriptor = MTKModelIOVertexDescriptorFromMetal(vertexDescriptor)
(meshDescriptor.attributes[0] as! MDLVertexAttribute).name = MDLVertexAttributePosition
        
let allocator = MTKMeshBufferAllocator(device: _device)
guard let assetURL = Bundle.main.url(forResource: "models.bundle/train",
                                     withExtension: "obj") else {
	fatalError()
}
        
let asset = MDLAsset(url: assetURL,
                     vertexDescriptor: meshDescriptor,
                     bufferAllocator: allocator)
let mdlMesh = asset.childObjects(of: MDLMesh.self).first as! MDLMesh
_mesh = try! MTKMesh(mesh: mdlMesh, device: _device)

그리고 이렇게 .obj 에셋을 불러와서, MDLAsset을 생성하고 MTKMesh로 바꿔준다.

 

나머지를 그대로 두고 실행하면 빨간색의 두개의 바퀴만 보인다. 

guard let submesh = _mesh.submeshes.first else {
	fatalError()
}

왜냐하면.. train.obj파일은 여러개의 메쉬로 구성되어 있는데, 렌더링 루프가 위와 같이 MDLMesh의 첫번째 매쉬만을 그리도록 되어있기 때문이다.

 

 

for submesh in _mesh.submeshes {
	// GPU에게 vertexBuffer에 0번째부터 3개의 버텍스를 가지고 삼각형을 그리라고 order를 내림.
	renderEncoder.drawIndexedPrimitives(type: .line,
                                      	indexCount: submesh.indexCount,
                                      	indexType: submesh.indexType,
                                     	indexBuffer: submesh.indexBuffer.buffer,
                                    	indexBufferOffset: submesh.indexBuffer.offset)
}

 

그래서 이렇게 포문을 돌면서 각 mesh를 모두 그려라 라고 지정해주면 되는데.. buffer내에 index count나 정점에 따른 index buffer, index buffer의 시작 offset은 obj파일에서 잘 만들어주기 때문에 그냥 그걸 갖다쓰면 된다. 이대로 그리고 실행시켜보면, train이 화면의 윗쪽에 그려지는것을 볼 수 있다. 

 

그 이유는.. Metal의 NDC space는 x = [ -1, 1] / y = [-1, 1] / z = [0, 1]으로 구성된 원점이 (0, 0, 0.5)인 system인데, train이 원점 (0,0,0)을 기준으로 위쪽으로 구성된 obj이기 때문이다.

 

그래서 조금 윗쪽으로 그려진다.