그래픽스/DX11

HLSL 개요

뽀또치즈맛 2025. 6. 3. 00:11

 

옛날 버전의 DX에서는 기본적인 쉐이더를 제공해주었지만, 이제는 그렇지 않다.

따라서 DX를 난생 처음 본다 하더라도 VertexShader와 PixelShader는 짤 줄 알아야 DX를 쓸 수 있다.

 

VertexShader와 PixelShader는 다른 프로그램이다.

VS가 쫙 실행이 되고, PS가 실행이 된다.

따라서 각각의 main이 있어야 한다.

 

HLSL은 얼핏보면 C++과 비슷하지만 분명한 차이가 있다.

근래에는 Shader Programming이 점점 복잡해지기 때문에 쉐이더를 파일로 넣어준다.

 

    AppBase::CreateVertexShaderAndInputLayout(
        L"ColorVertexShader.hlsl", inputElements, m_colorVertexShader,
        m_colorInputLayout);

    AppBase::CreatePixelShader(L"ColorPixelShader.hlsl", m_colorPixelShader);
    

void AppBase::CreateVertexShaderAndInputLayout(
    const wstring &filename,
    const vector<D3D11_INPUT_ELEMENT_DESC> &inputElements,
    ComPtr<ID3D11VertexShader> &vertexShader,
    ComPtr<ID3D11InputLayout> &inputLayout) {

    ComPtr<ID3DBlob> shaderBlob;
    ComPtr<ID3DBlob> errorBlob;
    
// ....


// 주의: 쉐이더의 시작점의 이름이 "main"인 함수로 지정
HRESULT hr = D3DCompileFromFile(filename.c_str(), 0, 0, "main", "vs_5_0",
                                compileFlags, 0, &shaderBlob, &errorBlob);

CheckResult(hr, errorBlob.Get());

m_device->CreateVertexShader(shaderBlob->GetBufferPointer(),
                             shaderBlob->GetBufferSize(), NULL,
                             &vertexShader);

m_device->CreateInputLayout(inputElements.data(),
                            UINT(inputElements.size()),
                            shaderBlob->GetBufferPointer(),
                            shaderBlob->GetBufferSize(), &inputLayout);
                            
                            
//....

}

 

 

그래픽스 파이프라인에 사용되는 쉐이더는 독립적이다.

즉 각각 다른 쉐이더이다.

 

다만, 쉐이더끼리 데이터를 주고 받긴 하지만,

양방향으로 주고받는 다는 이야기가 아니다.

 

버텍스 쉐이더에서 처리된 데이터가

D3D의 내부 처리 과정을 거쳐서 픽셀 쉐이더로 흘러들어가는 것 처럼,

버텍스 쉐이더와 픽셀 쉐이더가 데이터를 양방향으로 주고받으면서 작동하지 않는다.

 

일련 처리 과정에 의해 영향은 받지만,

각각의 독립적인 관계를 지닌다는 것을 숙지해야한다.

각각의 main문을 가진 프로그램이라고 생각하면 편하다.

 

쉐이더들은 각각 독립적이기 때문에 컴파일도 따로한다.

 

device의 인터페이스를 통해서 어떤 쉐이더를 사용할 것이다 라는 것을 지정할 수 있지만,

결국 실행이 되는 것은 GPU에서 실행한다.

이에 대하여,

어떤 함수를 통해서 일련 처리 과정을 거치는 쉐이더를 이용할 것인가에 대해서는 정해줄 수 있다.

 

    // RS: Rasterizer stage
    // OM: Output-Merger stage
    // VS: Vertex Shader
    // PS: Pixel Shader
    // IA: Input-Assembler stage

// 어떤 쉐이더를 사용할지 설정
m_context->VSSetShader(m_colorVertexShader.Get(), 0, 0);

 

 

박스의 기하정보, Vertex Position 등은 변하지 않지만

시점이 어떻게 변하고, 가상 공간을 어떻게 보며, 어떻게 그 가상 공간이 이동하는지는

ConstantBuffers를 통해 보내준다.

 

// h
	ComPtr<ID3D11Buffer> m_vertexBuffer;
    ComPtr<ID3D11Buffer> m_indexBuffer;
	// GPU ConstantBuffer에 해당하는 인터페이스 역할을 할 변수를 하나 생성해준다.
	ComPtr<ID3D11Buffer> m_constantBuffer;



// cpp
	// 실제 m_constantBuffer가 메모리를 가질 수 있도록 CreateBuffer로 할당해준다.
    // ConstantBuffer 만들기
    m_constantBufferData.model = Matrix();
    m_constantBufferData.view = Matrix();
    m_constantBufferData.projection = Matrix();

    AppBase::CreateConstantBuffer(m_constantBufferData, m_constantBuffer);
    
    //...
    
    // ConstantBuffer 활용 예시
	m_context->VSSetConstantBuffers(0, 1, m_constantBuffer.GetAddressOf());

 

 

 

	// .h

    ComPtr<ID3D11Buffer> m_vertexBuffer;
    ComPtr<ID3D11Buffer> m_indexBuffer;
    ComPtr<ID3D11Buffer> m_constantBuffer;
    UINT m_indexCount;

    // TODO: 픽셀쉐이더에서 사용할 Constant Buffer
    ComPtr<ID3D11Buffer> m_pixelShaderConstantBuffer;

    ModelViewProjectionConstantBuffer m_constantBufferData;

    // TODO: 픽셀쉐이더에서 사용할 Constant Buffer Data
    PixelShaderConstantBufferData m_pixelShaderConstantBufferData;
    
    
    // .cpp
 
 
 	// TODO: 픽셀쉐이더로 보낼 ConstantBuffer 만들기
    // 초기값 설정 (이건 CPU 측 구조체에 값 넣는 작업)
    m_pixelShaderConstantBufferData.xSplit = 0.5f;
    m_pixelShaderConstantBufferData.padding = Vector3(0, 0, 0);

	// GPU에 보낼 ConstantBuffer 생성
    AppBase::CreateConstantBuffer(m_pixelShaderConstantBufferData,
                                  m_pixelShaderConstantBuffer);

 

m_pixelShaderConstantBufferData PixelShaderConstantBufferData CPU 측 데이터 값 보관
m_pixelShaderConstantBuffer ComPtr<ID3D11Buffer> GPU 메모리 버퍼

 

// Initialize()
	// 초기값 설정 (이건 CPU 측 구조체에 값 넣는 작업)
	m_pixelShaderConstantBufferData.xSplit = 0.5f;
	m_pixelShaderConstantBufferData.padding = Vector3(0, 0, 0);

	// GPU에 보낼 ConstantBuffer 생성
	AppBase::CreateConstantBuffer(m_pixelShaderConstantBufferData, m_pixelShaderConstantBuffer);


// Update()
    // Constant를 CPU에서 GPU로 복사
    AppBase::UpdateBuffer(m_constantBufferData, m_constantBuffer);

    // TODO: 픽셀 쉐이더에서 사용할 ConstantBuffer 업데이트
    AppBase::UpdateBuffer(m_pixelShaderConstantBufferData,
                          m_pixelShaderConstantBuffer);

// Render()
    m_context->PSSetConstantBuffers(0, 1,
                                    m_pixelShaderConstantBuffer.GetAddressOf());

    m_context->PSSetShader(m_colorPixelShader.Get(), 0, 0);

    m_context->RSSetState(m_rasterizerSate.Get());

 

 

 

정점 쉐이더 코드 분해하기

 

그럼 이제 쉐이더에도 texcoord를 받아올 준비를 해야한다.

 

struct VertexShaderInput {
    float3 pos : POSITION;
    float3 color : COLOR0;
    float2 texcoord : TEXCOORD0;
};

 

pos는 정점의 위치이고, 3D 공간의 각 버텍스 위치를 가진다.

color는 정점의 색상을 가지며, 정점 단위 색 표현용이다.

texcoord는 텍스처 좌표를 가지며, 텍스처 매핑 도는 보간 조건에 사용된다.

 

“정점 단위 색”이란?

정점 단위 색? 이게 무슨 말인가 싶으실 수 있다.

 

각 정점(점)마다 색을 지정할 수 있다는 뜻이다.

예를 들어 네 개의 정점으로 사각형을 만들고,

각 꼭짓점에 다른 색을 주면 어떻게 될까?

 

정답은 GPU가 자동으로 중간 픽셀 색을 "보간"해서 부드럽게 연결한다.

 

예를 들어 다음과 같은 4개의 정점이 각각 색이 다르다고 가정해보자.

// 정점 4개, 각각 색 다름
vertices.push_back(Vertex{pos1, Vector3(1, 0, 0)}); // 빨강
vertices.push_back(Vertex{pos2, Vector3(0, 1, 0)}); // 초록
vertices.push_back(Vertex{pos3, Vector3(0, 0, 1)}); // 파랑
vertices.push_back(Vertex{pos4, Vector3(1, 1, 0)}); // 노랑

 

 

GPU는 정점 색을 이렇게 처리한다.

슬라이더 조건인 xSplit이 적용된 예제이긴 하지만,

각 정점의 색깔을 보간한다는 것을 확인할 수 있다.

(빨강)────(초록)
   │                  │
   │ 색이          │
   │ 보간됨       │
(노랑)────(파랑)

 

꼭짓점 색을 기준으로 픽셀 단위에서 부드럽게 색이 섞임
이걸 정점 색상(Vertex Color)이라고 한다.

 

 

그래서 float3 color : COLOR; 는 뭐하는 건가요?

이 줄은 정점 구조체에서 색상을 GPU로 전달하겠다는 선언이다.

  • float3 color: RGB 색상 (빨강, 초록, 파랑)
  • : COLOR: DirectX가 이게 "색상이다"라고 인식하도록 하는 의미(Semantics)

이 정보는 버텍스 쉐이더 → 픽셀 쉐이더로 전달되며,
화면에 출력할 픽셀 색상 결정에 사용된다.

 

왜 “정점 단위” 색이라 하나요?

  • 정점마다 색을 지정했기 때문
  • 텍스처를 쓰지 않고, 순수하게 정점 정보만으로 색을 표현하는 방식이기 때문

 

정점 쉐이더 코드 분해하기

 

// 픽셀 쉐이더용 ConstantBuffer 정의
cbuffer PSConstants : register(b0)
{
    float xSplit;
    float3 padding; // 16바이트 정렬 보장
}

 

 

왜 cbuffer를 사용할까?

ConstantBuffer는 C++에서 GPU로 값을 전달할 수 있는 기본적인 방법이다.

픽셀 쉐이더에서 슬라이더 값, 조명 값, UI값, 시간 등을 전달받을 때 cbuffer를 사용한다.

 

해당 예제에서는 슬라이더 값을 부여하기에 cbuffer를 통해 GPU로 값을 전달한다.

 

왜 register(b0)인가? 이게 무슨 뜻이죠?

쉐이더에 여러 개의 cbuffer가 있을 수 있으므로, "슬롯 번호"를 지정해줘야 한다.

b0은 버텍스 또는 픽셀 쉐이더에서 첫 번째 상수 버퍼란 뜻이다.

(C++에서도 PSSetConstantBuffers(0, ...)로 대응된다.)

 

왜 패딩이 필요한가?

cbuffer는 GPU 하드웨어 요구사항 때문에 무조건 19바이트(=128비트) 단위로 정렬되어야 한다.

 

  • float xSplit는 4바이트
  • 남은 12바이트를 맞추기 위해 float3 padding을 넣어 16바이트 맞춤

 

안하면 어떻게 될까?

CreateBuffer나 UpdateSubresource 등에서 정렬 오류/ 메모리 꼬임/ 버그발생할 수있다.

 

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 color : COLOR;
    float2 texcoord : TEXCOORD0; // 텍스처 좌표 추가
};

 

 

왜 float4 pos : SV_POSITION를 사용하는가?

픽셀 쉐이더가 실행되기 위해선 꼭 화면 상 위치 정보(SV_POSITION) 가 필요하다.
렌더링은 화면 픽셀 기준으로 진행되기 때문이다.

 

그럼 여기서 이제 SV_POSITION에 대한 정보의 중요성을 알았으니,

왜, MVP 3가지 행렬이 연산되어야 하는 지를 알 수 있다.

 

왜 color가 필요하지?

버텍스에서 받아온 색상을 픽셀 쉐이더에서도 쓰기 위해 전달한다.
픽셀 단위에서 이 색상을 그대로 쓰거나 수정할 수 있다.

 

해당 예제에서는 수정하여 슬라이더 조건으로 분기 처리하였다.

 

왜 float2 texcoord : TEXCOORD0를 써야 한가?

버텍스마다의 UV 좌표(=텍스처 좌표) 를 픽셀 단위에서 알고 있어야,
예시로, 텍스처에서 어떤 좌표의 색을 샘플링할지, 또는 슬라이더와 같은 조건으로 분기처리할지 결정할 수 있기 때문이다.

 

 

용어  의미
버텍스(Vertex) 도형을 이루는 점 (예: 삼각형 꼭짓점)
픽셀(Pixel) 화면에 실제로 보이는 색상의 단위 (점 하나)
텍스처(Texture) 이미지 (예: JPG, PNG 등)
UV 좌표 텍스처 이미지 위에서의 좌표 (0.0 ~ 1.0 범위의 x/y 좌표)
텍스처 좌표 = UV 좌표 같다고 보면 됩니다!

 

상황 예시

예를 들어 삼각형이 이렇게 생겼다고 해보자.

정점 A --------- 정점 B
   \                    /
     \                /
         정점 C

 

그리고 이 삼각형을 어떤 이미지(텍스처) 위에 덮어씌우고 싶다고 가정해보자

  • 정점 A에 (0.0, 0.0) → 텍스처의 왼쪽 아래
  • 정점 B에 (1.0, 0.0) → 텍스처의 오른쪽 아래
  • 정점 C에 (0.5, 1.0) → 텍스처의 위쪽 중앙

상황 정리

 

즉 버텍스마다 UV 좌표를 가진다는 것은

각 정점마다 UV 좌표를 지정했다는 것이다.

 

픽셀 단위에서 UV를 알고 있다는 것은,

GPU는 도형을 화면에 그릴 때, 도형 안에 포함된 모든 픽셀을 계산해서 화면에 보여준다.

이 과정에서 각 픽셀이 어느 위치(중간쯤)에 있는지를 기준으로,

정점들로부터 보간된 UV 좌표를 자동으로 계산해준다.

 

즉, 픽셀마다 UV 좌표를 알 수 있게 되는 것이다.

 

보간(interpolation)이란?

  • 정점 A: UV = (0, 0)
  • 정점 B: UV = (1, 0)
  • 정점 C: UV = (0.5, 1)

이 세 점으로 삼각형을 만들면, 삼각형 내부의 모든 픽셀들은 이 3개의 UV를 기반으로 자동으로 보간된 값을 갖게 된다.

 

왜 이게 중요할까?

이유 1: 픽셀 쉐이더에서 텍스처 색을 가져오기 위해

float4 color = myTexture.Sample(samplerState, input.texcoord);

 

  • input.texcoord는 그 픽셀이 삼각형 안에서 어느 위치에 있는지에 따라 자동 보간된 값이다.
  • 즉, 정점에서 준 UV → 픽셀에서도 자동으로 전달받음

 

이유 2: 조건 분기에도 사용 가능

 

if (input.texcoord.x < xSplit) {
    // 왼쪽 영역 처리
}

 

 

비유로 이해해 보기

텍스처는 지도, UV는 좌표

  • 정점마다 “나는 지도에서 여기야”라고 좌표를 알려줌
  • 삼각형 내부에 있는 픽셀은 GPU가 자동으로 "그 사이 어딘가"라고 계산해서 UV 좌표를 만들어줌
  • 픽셀 쉐이더는 이 UV 좌표를 기반으로 이미지에서 색을 따오거나, 조건을 걸 수 있음

 

 

1. 정점에서 UV 좌표 지정 각 꼭짓점이 이미지의 어느 위치에 대응되는지 알려줌
2. 픽셀에서 UV를 보간 삼각형 내부의 모든 픽셀도 "중간 UV 좌표"를 자동으로 가짐
3. 픽셀 쉐이더 사용 텍스처 색 추출 or 위치 기반 분기 처리 가능

'그래픽스 > DX11' 카테고리의 다른 글

D3D11 Phong vs Blinn-Phong  (0) 2025.06.15
D3D11 Resize Viewport  (0) 2025.06.10
MVP (Model, View, Projection)  (0) 2025.05.15
D3D11 조명 관련 노멀 벡터 변환  (2) 2025.02.03
D3D11 애파인 변환(=아핀 변환)  (1) 2024.12.16