ABOUT ME

-

  • 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가 실행할 명령을 설정합니다. 이 단계에서 중요한 작업은 다음과 같습니다. 

    1. Metal 디바이스 및 뷰 초기화
    2. 정점 데이터와 같은 그래픽 자원 생성
    3. 렌더 파이프라인 상태 객체 설정
    4. 명령 버퍼 및 인코더 생성

    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()
    

    이 과정에서

    1. 명령 큐는 GPU에 보낼 명령 버퍼들을 관리
    2. 명령 버퍼는 렌더링 작업의 모든 명령을 담음
    3. 렌더 명령 인코더는 구체적인 그리기 명령을 버퍼에 기록
    4. 모든 명령이 인코딩된 후, 버퍼는 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;
    }
    

    정점 쉐이더의 주요 역할

    1. 각 정점의 3D 위치를 화면 공간으로 변환
    2. 색상, 텍스처 좌표 등의 정점 속성을 처리
    3. 조명 계산 등을 수행할 수 있음

    래스터라이저

    이 단계는 하드웨어에 의해 자동으로 수행됩니다. 

    1. 정점들을 연결하여 삼각형 형성
    2. 삼각형을 화면의 픽셀로 변환(래스터화)
    3. 정점 속성을 삼각형 내부의 각 픽셀로 보간

    프래그먼트 쉐이더

    프래그먼트 쉐이더는 각 픽셀(프래그먼트)의 최종 색상을 결정합니다.

    fragment float4 fragmentShader(VertexOut in [[stage_in]]) {
        return in.color;
    }
    

    프래그먼트 쉐이더의 주요 역할

    1. 픽셀 단위로 색상 결정
    2. 텍스처 매핑, 조명 효과, 그림자 계산 등 수행
    3. 여러 효과와 후처리 적용 가능
    더보기

    정점 쉐이더 연산 과정 자세하게

    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)

    1. vertexID = 0이므로 vertexID * 7 = 0이 됩니다.
    2. position을 계산합니다:따라서 position = float3(0.0, 0.5, 0.0)이 됩니다.
    3. position.x = vertices[0] = 0.0 position.y = vertices[1] = 0.5 position.z = vertices[2] = 0.0
    4. color를 계산합니다:따라서 color = float4(1.0, 0.0, 0.0, 1.0)이 됩니다.
    5. color.r = vertices[3] = 1.0 color.g = vertices[4] = 0.0 color.b = vertices[5] = 0.0 color.a = vertices[6] = 1.0
    6. out.position에 float4(position, 1.0)을 할당합니다:여기서 1.0이 추가된 것은 동차 좌표계(homogeneous coordinate system)를 사용하기 위한 것입니다.
    7. out.position = float4(0.0, 0.5, 0.0, 1.0)
    8. out.color에 색상 값을 할당합니다:
    9. out.color = float4(1.0, 0.0, 0.0, 1.0)
    10. 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)
    

    이 프래그먼트에 대한 프래그먼트 쉐이더 실행

    1. 쉐이더는 in.color 값으로 (0.4, 0.3, 0.3, 1.0)을 받습니다.
    2. 이 예제에서는 간단히 이 색상을 그대로 반환합니다: return (0.4, 0.3, 0.3, 1.0).
    3. 반환된 색상 값은 이 픽셀의 최종 색상으로 프레임 버퍼에 기록됩니다.

    더 복잡한 프래그먼트 쉐이더라면 텍스처 샘플링, 조명 계산, 그림자 계산 등 다양한 연산을 수행할 수 있습니다.

    4. 출력 단계

    최종 이미지는 프레임버퍼에 기록되고 화면에 표시됩니다.

    // 드로어블에 표시하도록 예약
    commandBuffer.present(drawable)
    

    이 단계에서

    1. 프래그먼트 쉐이더의 결과가 프레임버퍼에 기록됨
    2. 깊이 테스트, 투명도 블렌딩 등의 추가 연산이 수행될 수 있음
    3. 최종 이미지가 화면에 표시됨

    댓글

Designed by Tistory.