-
Metal 렌더링 파이프라인Apple🍎/Metal 2025. 3. 29. 21:11
Metal 렌더링 파이프라인은 3D 그래픽을 2D 화면에 체계적으로 그리는 과정을 말합니다. 이 과정을 단계별로 자세히 알아보도록 합시다.
💡 글 이해를 위해 필요한 개념들
정점(Vertext) : 3D 공간상의 점으로, 3D 모델이나 그래픽을 구성하는 가장 기본적인 요소이며 다음과 같은 정보를 포합합니다.
- 위치(position): x, y, z 좌표로 3D 공간에서의 위치를 나타냅니다.
- 색상(Color) : 정점의 색상 정보입니다.
- 텍스쳐 좌표(texture coordinates): UV 좌표라고도 하며, 텍스쳐 이미지의 어느 부분이 해당 정점에 매핑될지를 결정합니다.
- 법선 벡터(normal vectors) : 정점에서의 표면 방향을 나타내느 벡터로, 조명 계산에 사용됩니다.
텍스쳐(Texture): 3D 모델의 표면에 입히는 이미지입니다. 단순한 색상, 복잡한 패턴, 울퉁불퉁함을 표현할 수 있습니다.
- 확산 맵(Diffuse Map): 표면의 기본 색상
- 법선 맵(Normal Map): 표면의 굴곡을 시뮬레이션
- 스페큘러 맵(Specular Map): 표면의 반사율
- 환경 맵(Environment Map): 주변 환경 반사 정보
명령 버퍼(Command Buffer) : 명령 버퍼는 CPU가 GPU에게 "무엇을 그릴지"에 대한 지시사항을 담는 컨테이너입니다.
- 명령 인코딩: 렌더링 명령이나 컴퓨트 명령을 인코더를 통해 버퍼에 기록합니다.
- 동기화: CPU와 GPU 간의 작업 동기화를 관리합니다.
- 배치 처리: 여러 드로잉 명령을 하나의 버퍼에 모아 효율적으로 처리합니다.
- 의존성 관리: 다른 명령 버퍼와의 실행 의존성을 설정할 수 있습니다.
인코더(Encoder) : 인코더는 CPU에서 작성한 명령들을 GPU가 이해할 수 있는 형태로 변환하여 명령버퍼에 기록하는 역할을 합니다.
쉐이더(Shader) : 쉐이더는 GPU에서 실행되는 프로그램(수행해야하는 연산을 정의)으로, 그래픽스 렌더링 파이프라인의 특정 단계에서 병렬 처리를 통해 데이터를 변환합니다.
- 정점 쉐이더(Vertex Shader): 3D 모델의 각 정점(vertext)에 대한 계산 프로그램입니다.
- 좌표 변환: 3D 공간의 정점 좌표를 2D 화면 좌표로 변환합니다(투영).
- 정점 속성 처리: 색상, 텍스처 좌표, 법선 벡터 등의 정점 데이터를 처리합니다.
- 정점별 계산: 각 정점에 대해 조명, 애니메이션 등의 계산을 수행할 수 있습니다.
- 프래그먼트 쉐이더(Fragment Shader): 래스터화 과정 후 생성된 각 픽셀(프래그먼트)의 최종 색상을 결정합니다.
- 픽셀 색상 결정: 각 픽셀의 최종 색상을 계산합니다.
- 텍스처 매핑: 텍스처를 표면에 적용합니다.
- 조명 효과: 픽셀 단위의 조명 계산을 수행합니다.
- 특수 효과: 그림자, 반사, 굴절 등의 효과를 계산합니다.
레스터화(Rasterization) : 벡터 기반의 기하학적 데이터(주로 삼각형)를 픽셀 기반의 이미지로 변환하는 과정입니다. 이 과정은 화면에 최종적으로 표시되는 픽셀들을 결정합니다.
- 3차원상의 객체를 2차원인 모니터 화면에 매핑하는 절차입니다.
쉐이더 인스턴스: 쉐이더 프로그램이 GPU에서 실행될 때, 동일한 연산 로직(쉐이더 코드)이 수많은 데이터 요소(정점, 픽셀 등)에 병렬적으로 적용됩니다. 이때 각 데이터 요소에 대한 쉐이더의 개별 실행을 '쉐이더 인스턴스'라고 합니다.
1. 앱(CPU) 단계
이 단계에서는 iOS 앱에서 Metal API를 사용하여 그래픽 렌더링을 위한 객체를 준비합니다.
// Metal 디바이스 생성 guard let device = MTLCreateSystemDefaultDevice() else { fatalError("GPU가 Metal을 지원하지 않습니다") } metalDevice = device // 커맨드 큐 생성 metalCommandQueue = metalDevice.makeCommandQueue() // 정점 버퍼 준비 vertexBuffer = metalDevice.makeBuffer(bytes: vertices, length: dataSize, options: [])
앱은 렌더링에 필요한 데이터(정점, 텍스처 등)를 준비하고 GPU가 실행할 명령을 설정합니다. 이 단계에서 중요한 작업은 다음과 같습니다.
- Metal 디바이스 및 뷰 초기화
- 정점 데이터와 같은 그래픽 자원 생성
- 렌더 파이프라인 상태 객체 설정
- 명령 버퍼 및 인코더 생성
2. 명령 큐 및 버퍼 단계
CPU가 준비한 작업은 명령 버퍼에 기록되고 GPU로 전송됩니다.
// 커맨드 버퍼 생성 guard let commandBuffer = metalCommandQueue.makeCommandBuffer() else { return } // 렌더 커맨드 인코더 생성 guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return } // 렌더링 명령 인코딩 renderEncoder.setRenderPipelineState(pipelineState) renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) // 인코딩 종료 및 커밋 renderEncoder.endEncoding() commandBuffer.present(drawable) commandBuffer.commit()
이 과정에서
- 명령 큐는 GPU에 보낼 명령 버퍼들을 관리
- 명령 버퍼는 렌더링 작업의 모든 명령을 담음
- 렌더 명령 인코더는 구체적인 그리기 명령을 버퍼에 기록
- 모든 명령이 인코딩된 후, 버퍼는 GPU로 커밋됨
3. 쉐이더 처리 단계
GPU에서 실행되는 이 단계는 파이프라인의 핵심입니다.
정점 쉐이더
정점 쉐이더는 각 정점의 위치와 속성을 처리합니다.
vertex VertexOut vertexShader(uint vertexID [[vertex_id]], constant float* vertices [[buffer(0)]]) { VertexOut out; // 정점 데이터 파싱 float3 position = float3(vertices[vertexID * 7 + 0], vertices[vertexID * 7 + 1], vertices[vertexID * 7 + 2]); float4 color = float4(vertices[vertexID * 7 + 3], vertices[vertexID * 7 + 4], vertices[vertexID * 7 + 5], vertices[vertexID * 7 + 6]); out.position = float4(position, 1.0); out.color = color; return out; }
정점 쉐이더의 주요 역할
- 각 정점의 3D 위치를 화면 공간으로 변환
- 색상, 텍스처 좌표 등의 정점 속성을 처리
- 조명 계산 등을 수행할 수 있음
래스터라이저
이 단계는 하드웨어에 의해 자동으로 수행됩니다.
- 정점들을 연결하여 삼각형 형성
- 삼각형을 화면의 픽셀로 변환(래스터화)
- 정점 속성을 삼각형 내부의 각 픽셀로 보간
프래그먼트 쉐이더
프래그먼트 쉐이더는 각 픽셀(프래그먼트)의 최종 색상을 결정합니다.
fragment float4 fragmentShader(VertexOut in [[stage_in]]) { return in.color; }
프래그먼트 쉐이더의 주요 역할
- 픽셀 단위로 색상 결정
- 텍스처 매핑, 조명 효과, 그림자 계산 등 수행
- 여러 효과와 후처리 적용 가능
더보기정점 쉐이더 연산 과정 자세하게
vertex VertexOut vertexShader(uint vertexID [[vertex_id]], constant float* vertices [[buffer(0)]]) { VertexOut out; float3 position = float3(vertices[vertexID * 7 + 0], vertices[vertexID * 7 + 1], vertices[vertexID * 7 + 2]); float4 color = float4(vertices[vertexID * 7 + 3], vertices[vertexID * 7 + 4], vertices[vertexID * 7 + 5], vertices[vertexID * 7 + 6]); out.position = float4(position, 1.0); out.color = color; return out; }
삼각형을 그리기 위한 세 개의 정점이 있다고 가정해 봅시다. 버퍼에 저장된 데이터는 다음과 같습니다.
vertices = [ // 정점 0 (위치 x,y,z + 색상 r,g,b,a) 0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0, // 정점 1 (위치 x,y,z + 색상 r,g,b,a) -0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0, // 정점 2 (위치 x,y,z + 색상 r,g,b,a) 0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 1.0 ]
GPU는 이 정점 쉐이더를 세 정점 각각에 대해 병렬로 실행합니다. 각 실행 인스턴스는 서로 다른 vertexID 값을 가집니다.
정점 0에 대한 정점 쉐이더 실행 (vertexID = 0)
- vertexID = 0이므로 vertexID * 7 = 0이 됩니다.
- position을 계산합니다:따라서 position = float3(0.0, 0.5, 0.0)이 됩니다.
- position.x = vertices[0] = 0.0 position.y = vertices[1] = 0.5 position.z = vertices[2] = 0.0
- color를 계산합니다:따라서 color = float4(1.0, 0.0, 0.0, 1.0)이 됩니다.
- color.r = vertices[3] = 1.0 color.g = vertices[4] = 0.0 color.b = vertices[5] = 0.0 color.a = vertices[6] = 1.0
- out.position에 float4(position, 1.0)을 할당합니다:여기서 1.0이 추가된 것은 동차 좌표계(homogeneous coordinate system)를 사용하기 위한 것입니다.
- out.position = float4(0.0, 0.5, 0.0, 1.0)
- out.color에 색상 값을 할당합니다:
- out.color = float4(1.0, 0.0, 0.0, 1.0)
- out 구조체를 반환합니다.
정점 1에 대한 정점 쉐이더 실행 (vertexID = 1)
유사한 방식으로 계산하면
position = float3(-0.5, -0.5, 0.0) color = float4(0.0, 1.0, 0.0, 1.0) out.position = float4(-0.5, -0.5, 0.0, 1.0) out.color = float4(0.0, 1.0, 0.0, 1.0)
정점 2에 대한 정점 쉐이더 실행 (vertexID = 2)
마찬가지로
position = float3(0.5, -0.5, 0.0) color = float4(0.0, 0.0, 1.0, 1.0) out.position = float4(0.5, -0.5, 0.0, 1.0) out.color = float4(0.0, 0.0, 1.0, 1.0)
이렇게 세 개의 정점이 처리되어 화면 좌표와 색상 정보를 가진 VertexOut 구조체들로 변환됩니다. 실제 더 복잡한 쉐이더에서는 여기서 행렬 변환, 조명 계산 등이 추가로 수행될 수 있습니다.
래스터화 과정
정점 쉐이더의 출력이 완료되면, 하드웨어 래스터라이저가 이 세 정점을 연결하여 삼각형을 형성하고, 이를 픽셀로 변환합니다. 이 과정에서 정점 속성(색상 등)이 삼각형 내부의 각 픽셀로 보간됩니다.
예를 들어, 위의 세 정점으로 형성된 삼각형 중앙에 위치한 픽셀은 대략적으로 다음과 같은 보간된 색상을 가질 수 있습니다.
실제로는 각 픽셀의 정확한 위치에 따라 세 정점의 가중치가 달라지므로, 부드러운 색상 그라데이션이 생성됩니다.
프래그먼트 쉐이더 연산 과정
래스터화 이후, 프래그먼트 쉐이더는 각 픽셀(프래그먼트)에 대해 실행됩니다.
fragment float4 fragmentShader(VertexOut in [[stage_in]]) { return in.color; }
우리 삼각형에서 생성된 프래그먼트 중 하나를 살펴봅시다.
프래그먼트 위치: (100, 150) 보간된 색상: (0.4, 0.3, 0.3, 1.0)
이 프래그먼트에 대한 프래그먼트 쉐이더 실행
- 쉐이더는 in.color 값으로 (0.4, 0.3, 0.3, 1.0)을 받습니다.
- 이 예제에서는 간단히 이 색상을 그대로 반환합니다: return (0.4, 0.3, 0.3, 1.0).
- 반환된 색상 값은 이 픽셀의 최종 색상으로 프레임 버퍼에 기록됩니다.
더 복잡한 프래그먼트 쉐이더라면 텍스처 샘플링, 조명 계산, 그림자 계산 등 다양한 연산을 수행할 수 있습니다.
4. 출력 단계
최종 이미지는 프레임버퍼에 기록되고 화면에 표시됩니다.
// 드로어블에 표시하도록 예약 commandBuffer.present(drawable)
이 단계에서
- 프래그먼트 쉐이더의 결과가 프레임버퍼에 기록됨
- 깊이 테스트, 투명도 블렌딩 등의 추가 연산이 수행될 수 있음
- 최종 이미지가 화면에 표시됨
'Apple🍎 > Metal' 카테고리의 다른 글
[Particle Simulator] Compute Shader 사용해 gpu 연산하기 (0) 2025.04.01 [Particle Simulator] cpu 연산 - gpu 랜더링 방법 (0) 2025.03.30 CPU와 GPU 비교와 GPGPU 프로그래밍의 이해 (0) 2025.03.27 cellular automata: 간단한 모래 시뮬레이터 만들기 (0) 2025.03.24 cellular automata(세포 자동자)는 뭘까? (0) 2025.03.23