다음을 통해 공유


셰이더 및 셰이더 리소스 작업

이제 Windows 8용 Microsoft DirectX 게임을 개발하는 데 셰이더 및 셰이더 리소스를 사용하는 방법을 알아봅니다. 그래픽 디바이스 및 리소스를 설정하는 방법을 살펴보았으며, 파이프라인 수정을 시작했을 수도 있습니다. 이제 픽셀 및 꼭짓점 셰이더를 살펴보겠습니다.

셰이더 언어에 익숙하지 않은 경우 빠른 토론이 순서대로 진행됩니다. 셰이더는 그래픽 파이프라인의 특정 단계에서 컴파일되고 실행되는 작은 하위 수준 프로그램입니다. 그들의 전문 분야는 매우 빠른 부동 소수점 수학 연산입니다. 가장 일반적인 셰이더 프로그램은 다음과 같습니다.

  • 꼭짓점 셰이더 - 장면의 각 꼭짓점에 대해 실행됩니다. 이 셰이더는 호출 앱에서 제공하는 꼭짓점 버퍼 요소에서 작동하며, 최소한 4개 구성 요소 위치 벡터가 픽셀 위치로 래스터화됩니다.
  • 픽셀 셰이더 - 렌더링 대상의 각 픽셀에 대해 실행됩니다. 이 셰이더는 이전 셰이더 단계(가장 간단한 파이프라인에서는 꼭짓점 셰이더)에서 래스터화된 좌표를 수신하고 해당 픽셀 위치에 대한 색(또는 다른 4 구성 요소 값)을 반환한 다음 렌더링 대상에 기록됩니다.

이 예제에는 기하 도형만 그리는 매우 기본적인 꼭짓점 및 픽셀 셰이더와 기본 조명 계산을 추가하는 더 복잡한 셰이더가 포함되어 있습니다.

셰이더 프로그램은 Microsoft HLSL(High Level Shader Language)으로 작성됩니다. HLSL 구문은 C와 비슷하지만 포인터가 없는 것처럼 보입니다. 셰이더 프로그램은 매우 컴팩트하고 효율적이어야 합니다. 셰이더가 너무 많은 명령으로 컴파일되면 실행할 수 없으며 오류가 반환됩니다. (허용되는 명령의 정확한 수는 다음의 일부입니다.Direct3D 기능 수준입니다.)

Direct3D에서는 셰이더가 런타임에 컴파일되지 않습니다. 프로그램의 나머지 부분을 컴파일할 때 컴파일됩니다. Microsoft Visual Studio 2013을 사용하여 앱을 컴파일하면 HLSL 파일은 그리기 전에 앱이 로드하고 GPU 메모리에 배치해야 하는 CSO(.cso) 파일로 컴파일됩니다. 패키지할 때 앱에 이러한 CSO 파일을 포함해야 합니다. 메시와 텍스처와 같은 자산입니다.

HLSL 의미 체계 이해

계속하기 전에 잠시 HLSL 의미 체계에 대해 논의하는 것이 중요합니다. 이러한 의미 체계는 종종 새로운 Direct3D 개발자에게 혼동의 지점이기 때문입니다. HLSL 의미 체계는 앱과 셰이더 프로그램 간에 전달되는 값을 식별하는 문자열입니다. 다양한 가능한 문자열일 수 있지만 사용량을 나타내는 문자열을 POSITION COLOR 사용하는 것이 가장 좋습니다. 상수 버퍼 또는 입력 레이아웃을 생성할 때 이러한 의미 체계를 할당합니다. 비슷한 값에 별도의 레지스터를 사용하도록 의미 체계에 0에서 7 사이의 숫자를 추가할 수도 있습니다. 예: COLOR0, COLOR1, COLOR2...

"SV_" 접두사로 지정된 의미 체계는 셰이더 프로그램에서 작성한 시스템 값 의미 체계이며, 게임 자체(CPU에서 실행)는 이를 수정할 수 없습니다. 일반적으로 이러한 의미 체계는 그래픽 파이프라인의 다른 셰이더 단계에서 입력 또는 출력되거나 GPU에 의해 완전히 생성된 값을 포함합니다.

SV_ 또한 의미 체계는 셰이더 단계에서 입력 또는 출력을 지정하는 데 사용될 때 다른 동작을 갖습니다. 예를 들어 SV_POSITION (출력)에는 꼭짓점 셰이더 단계 중에 변환된 꼭짓점 데이터가 포함되며 SV_POSITION (입력) 래스터화 단계에서 GPU에 의해 보간된 픽셀 위치 값이 포함됩니다.

다음은 몇 가지 일반적인 HLSL 의미 체계입니다.

  • POSITION꼭짓점 버퍼 데이터의 경우 (n)입니다. SV_POSITION 는 픽셀 셰이더에 픽셀 위치를 제공하며 게임에서 작성할 수 없습니다.
  • NORMAL(n) 꼭짓점 버퍼에서 제공하는 일반 데이터의 경우
  • TEXCOORD셰이더에 제공된 텍스처 UV 좌표 데이터의 경우 (n)입니다.
  • COLOR셰이더에 제공된 RGBA 색 데이터의 경우 (n)입니다. 래스터화 중에 값을 보간하는 것을 포함하여 데이터를 조정하는 것과 동일하게 처리됩니다. 의미 체계는 단순히 색 데이터임을 식별하는 데 도움이 됩니다.
  • SV_Target[n] 픽셀 셰이더에서 대상 텍스처 또는 다른 픽셀 버퍼로 쓰기 위한 것입니다.

예제를 검토할 때 HLSL 의미 체계의 몇 가지 예제를 살펴보겠습니다.

상수 버퍼에서 읽기

해당 버퍼가 해당 단계에 리소스로 연결된 경우 모든 셰이더는 상수 버퍼에서 읽을 수 있습니다. 이 예제에서는 꼭짓점 셰이더에만 상수 버퍼가 할당됩니다.

상수 버퍼는 C++ 코드와 액세스하는 해당 HLSL 파일의 두 위치에서 선언됩니다.

다음은 C++ 코드에서 상수 버퍼 구조체를 선언하는 방법입니다.

typedef struct _constantBufferStruct {
    DirectX::XMFLOAT4X4 world;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
} ConstantBufferStruct;

C++ 코드에서 상수 버퍼의 구조를 선언할 때 모든 데이터가 16바이트 경계를 따라 올바르게 정렬되었는지 확인합니다. 이 작업을 수행하는 가장 쉬운 방법은 예제 코드와 같이 XMFLOAT4 또는 XMFLOAT4X4 같은 DirectXMath 형식을 사용하는 것입니다. 정적 어설션을 선언하여 잘못 정렬된 버퍼를 방지할 수도 있습니다.

// Assert that the constant buffer remains 16-byte aligned.
static_assert((sizeof(ConstantBufferStruct) % 16) == 0, "Constant Buffer size must be 16-byte aligned");

이 코드 줄은 ConstantBufferStruct가 16바이트 정렬되지 않은 경우 컴파일 시간에 오류가 발생합니다. 상수 버퍼 맞춤 및 압축에 대한 자세한 내용은 상수 변수에 대한 압축 규칙을 참조 하세요.

이제 상수 버퍼가 꼭짓점 셰이더 HLSL에서 선언되는 방법은 다음과 같습니다.

cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix mWorld;      // world matrix for object
    matrix View;        // view matrix
    matrix Projection;  // projection matrix
};

상수, 텍스처, 샘플러 또는 기타 모든 버퍼에는 GPU가 액세스할 수 있도록 레지스터가 정의되어 있어야 합니다. 각 셰이더 단계에서는 최대 15개의 상수 버퍼를 허용하며, 각 버퍼는 최대 4,096개의 상수 변수를 보유할 수 있습니다. 레지스터 사용 선언 구문은 다음과 같습니다.

  • b*#*: 상수 버퍼(cbuffer)에 대한 레지스터입니다.
  • t*#*: 텍스처 버퍼(tbuffer)에 대한 레지스터입니다.
  • s*#*: 샘플러에 대한 레지스터입니다. (샘플러가 텍스처 리소스의 텍셀에 대한 조회 동작을 정의합니다.)

예를 들어 픽셀 셰이더의 HLSL은 다음과 같은 선언을 사용하여 텍스처와 샘플러를 입력으로 사용할 수 있습니다.

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

레지스터에 상수 버퍼를 할당하는 것은 사용자에게 달려 있습니다. 파이프라인을 설정할 때 HLSL 파일에서 할당한 것과 동일한 슬롯에 상수 버퍼를 연결합니다. 예를 들어 이전 항목에서 VSSetConstantBuffers 호출은 첫 번째 매개 변수에 대해 '0'을 나타냅니다. 즉, Direct3D가 상수 버퍼 리소스를 연결하여 0을 등록하도록 지시합니다. 이 리소스는 HLSL 파일에 등록(b0)하기 위한 버퍼의 할당과 일치합니다.

꼭짓점 버퍼에서 읽기

꼭짓점 버퍼는 장면 개체의 삼각형 데이터를 꼭짓점 셰이더에 제공합니다. 상수 버퍼와 마찬가지로 꼭짓점 버퍼 구조체는 유사한 압축 규칙을 사용하여 C++ 코드에 선언됩니다.

typedef struct _vertexPositionColor
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 color;
} VertexPositionColor;

Direct3D 11에는 꼭짓점 데이터에 대한 표준 형식이 없습니다. 대신 설명자를 사용하여 자체 꼭짓점 데이터 레이아웃을 정의합니다. 데이터 필드는 D3D11_INPUT_ELEMENT_DESC 구조의 배열을 사용하여 정의됩니다. 여기서는 이전 구조체와 동일한 꼭짓점 형식을 설명하는 간단한 입력 레이아웃을 보여 줍니다.

D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayout
    );

예제 코드를 수정할 때 꼭짓점 형식에 데이터를 추가하는 경우 입력 레이아웃도 업데이트해야 합니다. 그렇지 않으면 셰이더에서 해석할 수 없습니다. 다음과 같이 꼭짓점 레이아웃을 수정할 수 있습니다.

typedef struct _vertexPositionColorTangent
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT3 tangent;
} VertexPositionColorTangent;

이 경우 다음과 같이 입력 레이아웃 정의를 수정합니다.

D3D11_INPUT_ELEMENT_DESC iaDescExtended[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayoutExtended
    );

각 입력 레이아웃 요소 정의에는 이 항목의 앞부분에서 설명한 의미 체계인 "POSITION" 또는 "NORMAL"과 같은 문자열이 접두사로 지정됩니다. 꼭짓점을 처리할 때 GPU가 해당 요소를 식별하는 데 도움이 되는 핸들과 같습니다. 꼭짓점 요소에 대해 일반적이고 의미 있는 이름을 선택합니다.

상수 버퍼와 마찬가지로 꼭짓점 셰이더에는 들어오는 꼭짓점 요소에 대한 해당 버퍼 정의가 있습니다. (따라서 입력 레이아웃을 만들 때 꼭짓점 셰이더 리소스에 대한 참조를 제공했습니다. Direct3D는 셰이더의 입력 구조체를 사용하여 꼭짓점별 데이터 레이아웃의 유효성을 검사합니다.) 입력 레이아웃 정의와 이 HLSL 버퍼 선언 간에 의미 체계가 어떻게 일치하는지 확인합니다. 그러나 COLOR "0"이 추가되었습니다. 레이아웃에 선언된 요소가 하나 COLOR 뿐인 경우 0을 추가할 필요는 없지만 나중에 색 요소를 더 추가하도록 선택하는 경우 추가하는 것이 좋습니다.

struct VS_INPUT
{
    float3 vPos   : POSITION;
    float3 vColor : COLOR0;
};

셰이더 간에 데이터 전달

셰이더는 입력 형식을 사용하고 실행 시 기본 함수에서 출력 형식을 반환합니다. 이전 섹션에서 정의한 꼭짓점 셰이더의 경우 입력 형식은 VS_INPUT 구조체이며 일치하는 입력 레이아웃과 C++ 구조체를 정의했습니다. 이 구조체의 배열은 CreateCube 메서드에서 꼭짓점 버퍼를 만드는 데 사용됩니다.

꼭짓점 셰이더는 4개 구성 요소(float4) 최종 꼭짓점 위치를 최소한으로 포함해야 하는 PS_INPUT 구조를 반환합니다. GPU에 다음 그리기 단계를 수행하는 데 필요한 데이터가 있도록 이 위치 값에는 시스템 값 의미 체계 SV_POSITION가 선언되어 있어야 합니다. 꼭짓점 셰이더 출력과 픽셀 셰이더 입력 사이에는 1:1 대응이 없습니다. 꼭짓점 셰이더는 지정된 각 꼭짓점마다 하나의 구조를 반환하지만 픽셀 셰이더는 각 픽셀에 대해 한 번씩 실행됩니다. 꼭짓점별 데이터가 먼저 래스터화 단계를 통과하기 때문입니다. 이 단계에서는 그리는 기하 도형을 "덮는" 픽셀을 결정하고, 각 픽셀에 대해 보간된 꼭짓점당 데이터를 계산한 다음, 각 픽셀에 대해 픽셀 셰이더를 한 번 호출합니다. 보간은 출력 값을 래스터화할 때의 기본 동작이며, 특히 출력 벡터 데이터(광 벡터, 꼭짓점별 법선 및 탄젠트 등)의 올바른 처리에 필수적입니다.

struct PS_INPUT
{
    float4 Position : SV_POSITION;  // interpolated vertex position (system value)
    float4 Color    : COLOR0;       // interpolated diffuse color
};

꼭짓점 셰이더 검토

꼭짓점 셰이더의 예는 매우 간단합니다. 꼭짓점(위치 및 색)을 사용하고, 모델 좌표의 위치를 원근감 있는 투영 좌표로 변환한 다음,(색과 함께) 래스터라이저로 반환합니다. 꼭짓점 셰이더가 색 값에 대한 계산을 수행하지 않았더라도 색 값이 위치 데이터와 함께 바로 보간되어 각 픽셀에 다른 값을 제공합니다.

VS_OUTPUT main(VS_INPUT input) // main is the default function name
{
    VS_OUTPUT Output;

    float4 pos = float4(input.vPos, 1.0f);

    // Transform the position from object space to homogeneous projection space
    pos = mul(pos, mWorld);
    pos = mul(pos, View);
    pos = mul(pos, Projection);
    Output.Position = pos;

    // Just pass through the color data
    Output.Color = float4(input.vColor, 1.0f);

    return Output;
}

퐁 음영에 대한 개체의 꼭짓점을 설정하는 것과 같이 더 복잡한 꼭짓점 셰이더는 다음과 같이 보일 수 있습니다. 이 경우 벡터와 노멀이 보간되어 매끄럽게 보이는 표면을 근사화한다는 사실을 활용합니다.

// A constant buffer that stores the three basic column-major matrices for composing geometry.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
};

cbuffer LightConstantBuffer : register(b1)
{
    float4 lightPos;
};

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 normal : NORMAL;
};

// Per-pixel color data passed through the pixel shader.

struct PixelShaderInput
{
    float4 position : SV_POSITION; 
    float3 outVec : POSITION0;
    float3 outNormal : NORMAL0;
    float3 outLightVec : POSITION1;
};

PixelShaderInput main(VertexShaderInput input)
{
    // Inefficient -- doing this only for instruction. Normally, you would
 // premultiply them on the CPU and place them in the cbuffer.
    matrix mvMatrix = mul(model, view);
    matrix mvpMatrix = mul(mvMatrix, projection);

    PixelShaderInput output;

    float4 pos = float4(input.pos, 1.0f);
    float4 normal = float4(input.normal, 1.0f);
    float4 light = float4(lightPos.xyz, 1.0f);

    // 
    float4 eye = float4(0.0f, 0.0f, -2.0f, 1.0f);

    // Transform the vertex position into projected space.
    output.gl_Position = mul(pos, mvpMatrix);
    output.outNormal = mul(normal, mvMatrix).xyz;
    output.outVec = -(eye - mul(pos, mvMatrix)).xyz;
    output.outLightVec = mul(light, mvMatrix).xyz;

    return output;
}

픽셀 셰이더 검토

이 예제의 이 픽셀 셰이더는 픽셀 셰이더에서 사용할 수 있는 코드의 절대 최소 크기일 수 있습니다. 래스터화 중에 생성된 보간된 픽셀 색 데이터를 가져와서 출력으로 반환하며, 여기서 렌더링 대상에 기록됩니다. 얼마나 지루!

PS_OUTPUT main(PS_INPUT In)
{
    PS_OUTPUT Output;

    Output.RGBColor = In.Color;

    return Output;
}

중요한 부분은 반환 값의 SV_TARGET 시스템 값 의미 체계입니다. 출력은 표시를 위해 스왑 체인에 제공된 텍스처 버퍼인 기본 렌더링 대상에 기록되어야 임을 나타냅니다. 픽셀 셰이더에 필요합니다. 픽셀 셰이더의 색 데이터가 없으면 Direct3D에 표시할 항목이 없습니다.

퐁 음영을 수행하는 더 복잡한 픽셀 셰이더의 예는 다음과 같습니다. 벡터와 노멀이 보간되었으므로 픽셀 단위로 계산할 필요가 없습니다. 그러나 보간이 작동하는 방식 때문에 이를 다시 정규화해야 합니다. 개념적으로, 벡터를 꼭짓점 A의 방향에서 꼭짓점 B의 방향으로 점진적으로 "회전"해야 하며, 기본 길이를 지정합니다. wheras 보간은 두 벡터 엔드포인트 사이의 직선을 가로지르게 됩니다.

cbuffer MaterialConstantBuffer : register(b2)
{
    float4 lightColor;
    float4 Ka;
    float4 Kd;
    float4 Ks;
    float4 shininess;
};

struct PixelShaderInput
{
    float4 position : SV_POSITION;
    float3 outVec : POSITION0;
    float3 normal : NORMAL0;
    float3 light : POSITION1;
};

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 L = normalize(input.light);
    float3 V = normalize(input.outVec);
    float3 R = normalize(reflect(L, input.normal));

    float4 diffuse = Ka + (lightColor * Kd * max(dot(input.normal, L), 0.0f));
    diffuse = saturate(diffuse);

    float4 specular = Ks * pow(max(dot(R, V), 0.0f), shininess.x - 50.0f);
    specular = saturate(specular);

    float4 finalColor = diffuse + specular;

    return finalColor;
}

또 다른 예에서 픽셀 셰이더는 조명 및 재질 정보를 포함하는 자체 상수 버퍼를 사용합니다. 꼭짓점 셰이더의 입력 레이아웃은 일반 데이터를 포함하도록 확장되며, 해당 꼭짓점 셰이더의 출력에는 뷰 좌표계의 꼭짓점, 조명 및 꼭짓점 법선에 대한 변환된 벡터가 포함될 것으로 예상됩니다.

할당된 레지스터가 있는 텍스처 버퍼와 샘플러(각각 ts)가 있는 경우 픽셀 셰이더에서도 액세스할 수 있습니다.

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
};

float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
    float3 lightDirection = normalize(float3(1, -1, 0));
    float4 texelColor = simpleTexture.Sample(simpleSampler, input.tex);
    float lightMagnitude = 0.8f * saturate(dot(input.norm, -lightDirection)) + 0.2f;
    return texelColor * lightMagnitude;
}

셰이더는 그림자 맵 또는 노이즈 텍스처와 같은 절차적 리소스를 생성하는 데 사용할 수 있는 매우 강력한 도구입니다. 실제로 고급 기술을 사용하려면 시각적 요소가 아니라 버퍼로 텍스처를 더 추상적으로 생각해야 합니다. 이러한 데이터(예: 높이 정보) 또는 최종 픽셀 셰이더 패스 또는 특정 프레임에서 다단계 효과 전달의 일부로 샘플링할 수 있는 기타 데이터를 보유합니다. 다중 샘플링은 강력한 도구이며 많은 최신 시각 효과의 중추입니다.

다음 단계

이 시점의 DirectX 11에 익숙해지고 프로젝트 작업을 시작할 준비가 됐으면 합니다. DirectX 및 C++를 사용한 개발에 대해 가질 수 있는 다른 질문에 답변하는 데 도움이 되는 몇 가지 링크는 다음과 같습니다.

DirectX 디바이스 리소스 작업

Direct3D 11 렌더링 파이프라인 이해