본문 바로가기
공부/Graphics

[Graphics] D3D11 렌더링 파이프 라인(Rendering Pipeline)

by MY블로그 2023. 8. 10.

 

렌더링 파이프라인 ?

DirectX 3D Graphics에서의 렌더링 파이프라인은 그래픽스 카드에서 3D 모델을 렌더링하는 과정을 각각의 단계별로 나타낸 것입니다.

렌더링 파이프라인은 크게 두가지 버전으로 나뉘게 됩니다.

 

1. 고정 함수 파이프라인

2. 프로그래머블 파이프라인(DirectX 10 이후부터 지원 현재 배우는 기준은 DirectX11)

 

두가지의 버전중에서 프로그래머블 파이프라인이 더 우연하며 강력한 기능을 제공합니다.

때문에 프로그래머블 파이프라인을 기준으로 각 단계에 대하여 정리하도록 합니다.


렌더링 파이프 라인의 단계(생략가능, 필수)

1. 입력 어셈블러(IA - Input Assembler)

정점(버텍스)데이터를 가져와 점, 선, 삼각형 등의 프리미티브를 조합 생성합니다.

버텍스 버퍼, 인덱스 버퍼 등의 데이터를 이용하여 버텍스의 정보를 준비하는 단계입니다.

// Input Assembler 단계의 예제 코드
// IA 단계에서는 버텍스 데이터를 정의하고 이를 바탕으로 기하 프리미티브를 생성하는 설정 작업을 합니다.
#include <Windows.h>
#include <d3d11.h>

#pragma comment(lib, "d3d11.lib")

// 이하 코드는 윈도우 프로시저, 초기화, 렌더링 루프 등의 설정을 포함할 수 있음

int main() {
    // ... DirectX 디바이스 및 스왑 체인 생성 코드 ...

    // 버텍스 데이터 정의
    struct Vertex 
    {
        float x, y, z;
    };
    
    // 정점 x, y, z 의 위치 설정
    Vertex vertices[] = 
    {
        { -1.0f, -1.0f, 0.0f }, // 왼쪽 아래
        { 1.0f, -1.0f, 0.0f },  // 오른쪽 아래
        { 0.0f, 1.0f, 0.0f }    // 위
    };

    // 버텍스 버퍼 생성
    D3D11_BUFFER_DESC vertexBufferDesc = {};
    vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT; // gpu 읽기/쓰기허용 cpu 접근 불가
    vertexBufferDesc.ByteWidth = sizeof(vertices);
    vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

    D3D11_SUBRESOURCE_DATA vertexBufferData = {};
    vertexBufferData.pSysMem = vertices;

    ID3D11Buffer* vertexBuffer;
    device->CreateBuffer(&vertexBufferDesc, &vertexBufferData, &vertexBuffer);

    // Input Assembler 설정
    UINT stride = sizeof(Vertex);
    UINT offset = 0;

    context->IASetInputLayout(inputLayout);
    /*
    인풋 레이아웃을 설정합니다.
    쉐이더에게 어떤 종류의 버텍스 데이터가 사용되는지 알려줍니다.
    */
    
    context->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);
    /*
    사용할 버텍스 버퍼를 지정하고, 각 버텍스의 크기와 오프셋을 설정합니다.
    */
    
    context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    /*
    기하 프리미티브의 종류를 설정합니다.
    예제는 삼각형을 사용하므로 삼각형을 그리는 방식에 대하여 정해져 있습니다.
    */

    // ... 렌더링 루프 및 Clean-up ...

    return 0;
}

 


2. 버텍스 쉐이더(VS - Vertex Shader)

버텍스에 대해 연산을 수행합니다.

공간 변환, 조명 계산, 스키닝 등의 작업을 처리하여 버텍스의 최종 위치 및 특성을 계산하는 단계입니다.

// Vertex Shader 단계의 예제 코드
/*
Vertex Shader 단계는 정점단위로 실행되며, 각 버텍스의 위치와 속성을 변환하거나 계산하여 출력을 만들어낼 수 있습니다.
아래의 예제는 단순히 입력위치를 그대로 반환하였지만 실제로는 버텍스의 변환, 빛의 계산, 텍스처 매핑등 다양한 연산이 이루어집니다.

개인메모
현재 프레임워크에서 GameObjec / Member / Shader 클래스는
hlsl 쉐이더 파일을 불러와서 실행되도록 설계가 되어져 있다.
*/

#include <Windows.h>
#include <d3d11.h>
#include <d3dcompiler.h>

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "d3dcompiler.lib")

// 이하 코드는 윈도우 프로시저, 초기화, 렌더링 루프 등의 설정을 포함할 수 있음

int main() 
{
    // ... DirectX 디바이스 및 스왑 체인 생성 코드 ...

    // Vertex Shader 코드 정의
    /*
    문자열에 Vertex Shader 코드를 정의 합니다.
    현재 예제는 단순히 입력으로 받은 위치를 그대로 반환 하는 코드입니다.
    */
    const char* vsCode = R"(
        float4 main(float4 position : POSITION) : SV_POSITION {
            return position;
        }
    )";

    // Vertex Shader 컴파일 및 생성
    /*
    D3DColpile 함수를 사용하여 Vertex Shader 코드를 컴파일합니다.
    ID3D11VertexShader 인터페이스를 생성합니다.
    */
    ID3D11VertexShader* vertexShader;
    D3DCompile(vsCode, strlen(vsCode), nullptr, nullptr, nullptr, "main", "vs_5_0", 0, 0, &vertexShader, nullptr);

    // Vertex Shader 설정
    /*
    VSSetShaer 함수를 사용하여 생성한 Vertex Shader를 설정합니다.
    이렇게 한다면 해당 Vertex Shader가 그래픽스 파이프라인의 Vertex Shader 단계에서 실행됩니다.
    */
    context->VSSetShader(vertexShader, nullptr, 0);

    // ... 렌더링 루프 및 Clean-up ...

    return 0;
}

 

https://rito15.github.io/posts/rendering-pipeline/

공간 변환 내용 정리

M(Model)

로컬공간에서 > 월드공간으로 변환

각 오브젝트마다 자신의 중심(Pivot)위치를 Vector3(0,0,0)으로 하는 좌표 공간(1.LOCAL SPACE)을 가지고 있습니다.

3D 공간내의 월드공간(2.WORLD SPACE)은 단하나의 위치를 원잠으로 하는 좌표 공간을 가지고 있습니다.

모델 변환은 각 오브젝트의 좌표 공간을 변환시켜 월드 공간에 통합 시키는 과정 입니다.

이 과정에서 이동(T), 회전(R), 크기(S)의 변환이 이루어 집니다.

이동, 회전, 크기의 세가지 변환은 각각의 행렬들을 통하여 수행되고, 이를 하나의 행렬로 만드는 것을 TRS행렬 이라고 합니다.

TRS행렬이 곱해지는 순서는 S * R * T 순서로 곱해지게 됩니다.(벡터와 곱할때 벡터는 가장 우측 즉, 제일 나중에)

따라서 만일 각각 따로 곱해야 할때에는 행렬을 좌측에 두고 T * R * S 의 역순으로 곱해줍니다.

V(View)

월드공간 > 카메라공간으로 변환

카메라또한 카메라의 위치가 원점 Vector3(0,0,0)으로 하는 좌표를 가지고 있습니다.

카메라가 바라보는 방향이 +Z 축인 공간 입니다. (깊이)

뷰 변환은 모든 오브젝트를 화면에 그려내기 쉽도록 카메라 기준으로 공간을 변환하는 과정입니다.

P(Projection)

카메라공간 > 클립공간으로 변환

카메라를 기준으로 정점 위치를  화면에 보이기 위한 정점의 위치로 변환해줍니다.

화면에 렌더링될 수 있는 영역을 나타내는 절두체(Frustum)가 정의 됩니다.(절두체 상세내용은 접은글 참고)

더보기

절두체(Frustum)는 그래픽스에서 시야에 보이는 부분을 나타내는 공간의 형태를 말합니다. 주로 3D 렌더링에서 카메라의 시야를 표현하고, 렌더링할 객체가 시야 내에 있는지를 결정하는 데 사용됩니다.

 

절두체는 원뿔 모양의 공간을 의미하며, 카메라 위치에서 시야를 나타내는 평면들로 구성됩니다. 이 평면들은 보통 6개의 면으로 구성되는데, 이것들은 "Near", "Far", "Left", "Right", "Top", "Bottom"이라 불리는 면입니다. 이러한 면들은 카메라의 시야를 정의하고, 절두체 내에 위치한 물체만이 렌더링되는 대상이 됩니다.

 

렌더링 파이프라인에서 Vertex Shader 단계에서는 주로 3D 객체의 버텍스들을 카메라 공간으로 변환한 후, 이 공간에서 절두체 테스트를 수행합니다. 이 과정을 통해 객체의 일부 또는 전체가 절두체 내에 있는지 여부를 판단하고, 시야에 보이는 객체들만을 선택적으로 렌더링할 수 있습니다. 이렇게 함으로써 시야 밖의 객체들은 렌더링 과정에서 생략되어 효율적인 렌더링이 가능해집니다.

 

절두체는 가상 공간의 일부를 나타내며, 시야를 기준으로 하는 다양한 렌더링 및 클리핑 작업에서 중요한 역할을 합니다.

절두체 참고 이미지

절두체는 Near Clipping Plane, Far Clipping Plane, Field of Wiew 를 통해 정의 합니다.(Camera 클래스 참고)

절두체를 완전히 벗어나는 폴리곤들은 모두 버려지며 경계에 걸쳐 있는 폴리곤들은 유지 합니다.

원근감과 관련된 직교, 원근 투영이 행해집니다.

클립 공간의 좌표계는 사실 3D가 아닌 4D 입니다.(3D 좌표계와 4D 좌표계 설명글은 아래 접은글 참고)

더보기

3D 좌표계와 4D 좌표계는 그래픽스와 3D 컴퓨터 그래픽스에서 사용되는 좌표 시스템을 나타내는 데 사용되는 개념입니다.

 

3D 좌표계 (3D Coordinate System): 3D 좌표계는 3차원 공간에서 객체의 위치를 표현하는 데 사용되는 시스템입니다. 보통 3개의 축인 X, Y, Z축을 사용하여 3D 공간을 나타냅니다. 각 축은 서로 수직이며, 원점(0, 0, 0)을 기준으로 좌표가 지정됩니다. 3D 좌표계에서 객체의 위치는 (X, Y, Z) 형태로 표현됩니다.

 

4D 좌표계 (4D Coordinate System): 4D 좌표계는 4차원 공간을 다루는 시스템으로, 주로 컴퓨터 그래픽스와 물리 시뮬레이션에서 사용됩니다. 4D 좌표계는 3D 좌표에 시간(time)을 추가하여 표현되며, 이를 "시간-공간(time-space)"이라고도 합니다. 이러한 4D 좌표계는 특수 상대성 이론과 같은 물리적 현상을 모델링하는 데 사용됩니다.

4D 좌표계는 각 축이 X, Y, Z 및 T(시간)으로 표현됩니다. 각 좌표는 (X, Y, Z, T) 형태로 표현되며, 이를 통해 객체의 위치와 시간을 같이 나타낼 수 있습니다.

 

4D 좌표계는 시간의 변화와 공간의 위치를 한 번에 다룰 수 있게 해주므로, 3D 그래픽스 뿐만 아니라 물리 시뮬레이션, 컴퓨터 애니메이션, 과학적 모델링 등 다양한 분야에서 활용됩니다.

w값은 카메라에서 멀어질수록 커지며, 추후 NDC(Normalized Device Coordinates)로의 변환에서 사용됩니다.

*NDC 변환은 3D 그래픽스 파이프라인에서 처리되는 중요한 단계중 하나입니다. 월드 좌표나 카메라 좌표로부터 화면 좌표로 변환된 객체를 최종적으로 디스플레이되는 화면에 표시하기 위해 필요한 과정입니다. 투영 변환을 거쳐 3D 공간의 좌표를 정규화된 디바이스 좌표로 변환하는 과정입니다.

 

클립 공간의 4D 좌표계를 동차 좌표계(Homogeneous Coordinates)라고 합니다.

버텍스 쉐이더의 최종 출력은 클립 공간의 정점 데이터 입니다.


3. 헐 쉐이더(HS - Hull Shader) & 도메인 쉐이더(DS - Domain Shader) & 테셀레이션(Tessellation)

테셀레이션을 사용하여 복잡하고 기하학적 표현을 생성하거나 세부 레벨을 조절합니다.

이 단계는 필수가 아닌 선택적인 단계이며 고급 그래픽스 기술에서 사용되기도 합니다.


4. 기하 쉐이더(GS - Geometry Shader)

기하 프리미티브들을 이용하여 새로운 프리미티브를 생성하거나 변형시킵니다.

3번 단계와 마찬가지로 선택적인 단계이며 특정 효과를 위하여 필요한 단계입니다.


5. 레스터 라이저(Rasterization)

정점 정보를 완전히 결정한 다음 3D 도형을 실제 픽셀 데이터로 변환해 주는 단계입니다.

이때 가지고 있는 정점 데이터는 말그대로 정점의 데이터이며 정점 사이의 공간들은 보간(interpolation)을 하여 채워주어야 합니다.

레스터 라이저는 다양한 작업을 합니다.

 

1. 클리핑

버텍스 쉐이더의 마지막 단계에서 절두체를 완전히 벗어나는 폴리곤은 버렸습니다.

하지만 절두체의 경계에 있는 폴리곤들은 버려지지 않았는데 이때 레스터 라이저에서 이렇게 걸처져 있는 폴리곤들을 잘라내어 절두체 내부와 외부 영역을 분리하여 절두체 외부 영역은 버리는 추가 작업을 합니다.

 

2. 원근 분할

클립 스페이스(동차 좌표계, 4D)좌표의 모든 요소를 W 값으로 나누게 되는데 이를 통해 모든 원근법 구현이 완료되며 이를 원근 분할 이라고 하며 원근 불할을 끝마친 좌표계를 NDC 라고 합니다.

NDC(Normalized Device Coordinates)

클립 스페이스의 좌표(X,Y,Z)를 모두 W로 나눈 좌표 입니다.

X,Y 좌표는 모두 -1 ~ 1, Z좌표는 0 ~ 1에 위치하는 좌표계 입니다.

스크린 좌표로 손쉽게 변환할 수 있도록 하기위한 3D 공간 변환 상의 마지막 좌표계 입니다.

 

3. 후면 컬링(Back Face Culling)

View 벡터와 Normal 벡터의 관계를 통하여 뒷변을 찾아내고 렌더되지 않도록 합니다.

 

4. 뷰포트 변환(Viewport Transformation)

3D NDC 공간 상의 좌표를 2D 스크린 좌표로 변환 합니다.

-1 ~ 1 범위로 정규화 되어있는 좌표를 화면 해상도의 범위로 변환하여 줍니다.

2D 공간으로 변환할때 Z값을 깊이값으로 사용하기 위하여  Z값은 그대로 유지 합니다.

 

5. 스캔 변환(Scan Transformation)

프리미티브(기본 도형, 삼각형)를 통하여 프래그먼트를 생성하고 프래그먼트를 채워주는 픽셀들을 찾아냅니다. 각 픽셀마다 정점 데이터(위치, 색상, 노멀, UV)들을 보간하여 할당합니다.

// Resterization 단계의 예제 코드
/*
레스터화 단계는 점, 선, 삼각형등 기하 프리미티브를 실제 픽셀로 변환합니다.
정점의 위치와 속성을 픽셀 위치에 매핑하고, 쉐이더 처리 후의 결과를 실제 화면에 표시하도록
2D 형태의 이미지로 변환합니다.

개인메모 2DFrameWork / GameObject / State / RasterState 클래스 참고
*/

#include <Windows.h>
#include <d3d11.h>

#pragma comment(lib, "d3d11.lib")

// 이하 코드는 윈도우 프로시저, 초기화, 렌더링 루프 등의 설정을 포함할 수 있음

int main() {
    // ... DirectX 디바이스 및 스왑 체인 생성 코드 ...

    // ... 버텍스 쉐이더 및 픽셀 쉐이더 설정 ...
    /*
    이전 단계에서 설정한 VS & PS를 이용하여 처리를 수행합니다.
    */

    // 레스터화 단계 설정
    context->RSSetState(nullptr); // 일반적으로 기본 상태로 사용
    /*
    RSSetState 함수를 사용하여 레스터화 상태를 설정합니다.
    일반적으로 기본상태를 사용하며, 본 예제는 nullptr 을 전달합니다.
    */

    // ... 렌더링 루프 및 Clean-up ...

    return 0;
}

6. 필셀 쉐이더(PS - Pixel Shader) 

버텍스 쉐이더에서 계산한 정보를 바탕으로 각 픽셀의 색상을 계산하는 단계입니다.

조명, 그림자, 텍스처(이미지) 맵핑 등의 작업을 수행합니다.

DirectX에서는 픽셀 쉐이더라고하며, 유니티 OpenGL에서는 프래그먼트 쉐이더 라고합니다.

화면에서 차지하는 픽셀의 갯수만큼 작업이 실행됩니다.

쉐이더를 통해 색상을 변화시키는 것은 모두 픽셀 쉐이더의 작업 입니다.

투명도를 결정하는 것 또한 픽셀 쉐이더이며 라이팅, 그림자, 텍스처 색상을 메시에 입히는 작업 등도 픽셀 쉐이더에서 진행합니다.

깊이 값은 Z-Buffer에 저장됩니다.

색상 값은 Color Buffer에 저장됩니다.

이러한 버퍼들을 통틀어 스크린 버퍼(Screen Buffer)라고 합니다.

// Pixel Shader 단계의 코드 예제

#include <Windows.h>
#include <d3d11.h>
#include <d3dcompiler.h>

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "d3dcompiler.lib")

// 이하 코드는 윈도우 프로시저, 초기화, 렌더링 루프 등의 설정을 포함할 수 있음

int main() {
    // ... DirectX 디바이스 및 스왑 체인 생성 코드 ...

    // ... 버텍스 쉐이더 설정 ...
    /*
    이전 단계에서 설정한 버텍스 쉐이더를 이용하여 버텍스 처리를 수행합니다.
    */

    // Pixel Shader 코드 정의
    /*
    psCode 문자열에 PixelShader 코드를 정의합니다.
    본 예제는 단순히 빨간색으로 출력하는 코드를 사용합니다.
    */
    const char* psCode = R"(
        float4 main() : SV_TARGET {
            return float4(1.0, 0.0, 0.0, 1.0); // 빨간색으로 출력
        }
    )";

    // Pixel Shader 컴파일 및 생성
    /*
    D3DCompile 함수를 사용하여 PixelShader 코드를 컴파일하고
    ID3D11PixelShader 인터페이스를 생성합니다.
    개인메모 - 현재 사용중인 프레임워크는 쉐이더 클래스에서 파일을 불러와 컴파일
    */
    ID3D11PixelShader* pixelShader;
    D3DCompile(psCode, strlen(psCode), nullptr, nullptr, nullptr, "main", "ps_5_0", 0, 0, &pixelShader, nullptr);

    // Pixel Shader 설정
    /*
    PSSetShader 함수를 사용하여 생성한 PixelShader를 설정합니다.
    이렇게하면 해당 PixelShader가 그래픽스 파이프라인의 PixelShader 단게에서 실행됩니다.
    */
    context->PSSetShader(pixelShader, nullptr, 0);

    // ... 렌더링 루프 및 Clean-up ...

    return 0;
}

7. 출력 병합(OM - Output Merger)

픽셀 쉐이더에서 계산된 픽셀의 색상을 프레임 버퍼에 결합하거나 병합합니다.

깊이 테스트, 스텐실 테스트 등의 후처리 작업을 수행하여 최종 이미지를 생성합니다.

픽셀들을 화면에 출력하기 위한 마지막 연산들을 수행합니다.

Z-Test / Stencil Test / Alpha Blending

// Output Merger 단계의 예제 코드
/*
Output Merger 단계에서는 픽셀의 최종 색상, 깊이, 스텐실(Stencil) 값을 기반으로 렌더 타겟에 픽셀을 렌더링 합니다.
또한 블렌딩, 깊이 테스트, 스텐실 테스트 등의 작업을 수행하여 최종적으로 보여지는 영상을 결정합니다.
이 단계는 화면에 렌더링 결과를 표시하고 색상, 깊이정보를 기반으로 다양한 후처리 작업을 수행하여 최종 출력을 만들어내는 중요한 과정입니다.
*/

#include <Windows.h>
#include <d3d11.h>
#include <d3dcompiler.h>

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "d3dcompiler.lib")

// 이하 코드는 윈도우 프로시저, 초기화, 렌더링 루프 등의 설정을 포함할 수 있음

int main() {
    // ... DirectX 디바이스 및 스왑 체인 생성 코드 ...

    // ... 버텍스 쉐이더 및 픽셀 쉐이더 설정 ...
    /*
    이전 단계에서 설정한 버텍스 쉐이더 픽셀 쉐이더를 이용하여 처리 수행을 합니다.
    */

    // Output Merger 설정
    /*
    OMSetRenderTargets 함수를 사용하여 렌더 타겟(프레임 버퍼)를 설정합니다.
    첫번째 매개변수는 렌더 타겟의 갯수를 나타냅니다.
    두번째 매개변수는 렌더 타겟 뷰의 배열을 전달합니다.
    이 단계에서는 보통 프레임 버퍼에 픽셀을 렌더하기 위해 사용합니다.
    */
    context->OMSetRenderTargets(1, &renderTargetView, nullptr);

    // ... 렌더링 루프 및 Clean-up ...

    return 0;
}

8. 렌더링 타겟 출력(Render Target Output)

파이프라인의 최종 단계로 프레임 버퍼에 렌더링된 이미지를 보여줍니다.

보여지는 이미지는 스크린에 표시되거나, 텍스처로 저장되어 다른 계산에 사용될 수 있습니다.

댓글