HLSL 입문 - 1장: 구조 이해하기

Ashred·2025년 4월 10일

처음 HLSL을 공부하기 시작하면서 가장 헷갈렸던 건 이게 대체 어떻게 구성되어 있는가였다. 특히 Maya의 DX11 셰이더처럼 커스텀으로 셰이더를 짜야 할 때는 '일단 돌아가게' 만들기까지도 생각보다 벽이 높다. 그래서 이번 첫 번째 글에서는 HLSL 코드의 전체 구조를 간단한 조명 표현과 함께 정리해본다.

1. 상수 버퍼 (cbuffer)

cbuffer UpdatePerFrame : register(b0) { ... }
cbuffer UpdatePerObject : register(b1) { ... }

UpdatePerFrame에는 프레임마다 바뀌는 카메라 관련 정보들이 들어간다.
예: 뷰 행렬, 뷰 방향, 카메라 위치 등
UpdatePerObject는 각 오브젝트마다 바뀌는 정보들이 들어간다.
예: 월드 변환 행렬
이 값들은 CPU에서 셰이더로 넘겨주는 상수 데이터다. HLSL에서는 register(b0)처럼 슬롯을 명시해서 어디에 넣을지 지정해줘야 한다.

2. 구조체 정의 (입력과 출력)

struct appdata {
    float3 position : POSITION;
    float3 normal   : NORMAL;
    ...
};

struct vertexOutput {
    float4 position      : SV_POSITION;
    float3 WorldPosition : TEXCOORD4;
    float3 worldNormal   : NORMAL;
};

버텍스 셰이더는 GPU에서 받아온 appdata를 바탕으로 처리하고, 픽셀 셰이더에 vertexOutput 구조체를 넘겨준다. 각각의 필드는 정해진 시맨틱(Semantic)을 통해 HLSL 파이프라인과 연결된다. 예: POSITION, NORMAL, SV_POSITION 등.

3. 버텍스 셰이더 (VSMain)

vertexOutput VSMain(appdata IN) {
    vertexOutput OUT = (vertexOutput)0;

    OUT.position = mul(float4(IN.position, 1.0), _WorldViewPrj);
    OUT.WorldPosition = mul(float4(IN.position, 1.0), _World);
    OUT.worldNormal = mul(float4(IN.normal, 1.0), _WorldInvT).xyz;

    return OUT;
}

오브젝트의 로컬 좌표를 뷰-프로젝션 행렬로 변환해 OUT.position에 넣는다.
같은 로컬 좌표를 월드 좌표로 바꿔 WorldPosition으로 넘긴다.
노말도 _WorldInvT를 통해 월드 공간으로 변환해준다 (이게 중요!).

4. 픽셀 셰이더 (PSMain)

float4 PSMain(vertexOutput IN) : COLOR {
    float3 normal = normalize(IN.worldNormal);
    float3 lightDir = normalize(float3(0.0, 0.0, -1.0));

    float nDotL = saturate(dot(normal, lightDir));
    float3 finalColor = myColor.rgb * nDotL;

    return float4(finalColor, 1.0);
}

월드 노말과 라이트 방향을 내적해서 밝기(nDotL)를 구한다.
이 밝기를 myColor.rgb에 곱해 최종 색을 만든다.
이건 딱 램버트 셰이딩 한 줄 요약이다.

5. 테크닉과 패스

technique11 MainTech {
    pass P0 {
        SetVertexShader( CompileShader( vs_4_0, VSMain() ) );
        SetPixelShader( CompileShader( ps_4_0, PSMain() ) );
    }
}

이 부분이 Maya에서 FX 파일을 읽고 "MainTech"를 찾아주는 핵심이다.
버텍스와 픽셀 셰이더를 등록하고, 컴파일 타겟(vs_4_0, ps_4_0)도 명시해준다.

마무리

1장에서 다룬 내용은 전체 셰이더의 뼈대를 구성하는 방법이다. 실제로 뭔가 그리려면 훨씬 많은 요소가 필요하지만, 구조를 먼저 이해하고 나면 그 다음은 기능을 하나씩 붙여나가면 된다.
다음 장에서는 카메라 방향 기반의 rim light, 혹은 스페큘러 등을 간단하게 붙여보며 조금 더 입체적인 셰이더를 구현해볼 예정이다.

0개의 댓글