Direct3D 11 렌더링 파이프라인 이해
이전에는 DirectX 디바이스 리소스 작업에서 그리기에 사용할 수 있는 창을 만드는 방법을 살펴보았습니다. 이제 그래픽 파이프라인을 빌드하는 방법과 그래픽 파이프라인에 연결할 수 있는 위치에 대해 알아봅니다.
그래픽 파이프라인을 정의하는 두 개의 Direct3D 인터페이스인 ID3D11Device는 GPU 및 해당 리소스의 가상 표현을 제공합니다. 및 ID3D11DeviceContext는 파이프라인에 대한 그래픽 처리를 나타냅니다. 일반적으로 ID3D11Device의 instance 사용하여 장면에서 그래픽 처리를 시작하는 데 필요한 GPU 리소스를 구성하고 가져오며, ID3D11DeviceContext를 사용하여 그래픽 파이프라인의 각 적절한 셰이더 단계에서 해당 리소스를 처리합니다. 일반적으로 장면을 설정하거나 디바이스가 변경되는 경우에만 ID3D11Device 메서드를 자주 호출합니다. 반면에 디스플레이를 위해 프레임을 처리할 때마다 ID3D11DeviceContext 를 호출합니다.
이 예제에서는 간단한 회전 꼭짓점 음영 큐브를 표시하는 데 적합한 최소 그래픽 파이프라인을 만들고 구성합니다. 표시에 필요한 리소스의 약 작은 집합을 보여 줍니다. 여기에서 정보를 읽으면서 렌더링하려는 장면을 지원하도록 확장해야 할 수 있는 지정된 예제의 제한 사항에 유의하세요.
이 예제에서는 그래픽에 대한 두 가지 C++ 클래스인 디바이스 리소스 관리자 클래스와 3D 장면 렌더러 클래스를 다룹니다. 이 항목에서는 특히 3D 장면 렌더러에 중점을 둡니다.
큐브 렌더러는 무엇을 합니까?
그래픽 파이프라인은 3D 장면 렌더러 클래스에 의해 정의됩니다. 장면 렌더러는 다음을 수행할 수 있습니다.
- 균일한 데이터를 저장할 상수 버퍼를 정의합니다.
- 개체 꼭짓점 데이터를 저장할 꼭짓점 버퍼와 해당 인덱스 버퍼를 정의하여 꼭짓점 셰이더가 삼각형을 올바르게 걸을 수 있도록 합니다.
- 텍스처 리소스 및 리소스 뷰를 만듭니다.
- 셰이더 개체를 로드합니다.
- 각 프레임을 표시하도록 그래픽 데이터를 업데이트합니다.
- 그래픽을 스왑 체인에 렌더링(그리기)합니다.
처음 네 프로세스는 일반적으로 그래픽 리소스를 초기화하고 관리하기 위해 ID3D11Device 인터페이스 메서드를 사용하고, 마지막 두 프로세스는 ID3D11DeviceContext 인터페이스 메서드를 사용하여 그래픽 파이프라인을 관리하고 실행합니다.
렌더러 클래스의 instance 생성되고 기본 프로젝트 클래스에서 멤버 변수로 관리됩니다. DeviceResources instance 기본 프로젝트 클래스, 앱 뷰 공급자 클래스 및 렌더러를 비롯한 여러 클래스에서 공유 포인터로 관리됩니다. Renderer를 사용자 고유의 클래스로 바꾸는 경우 DeviceResources instance 선언하고 공유 포인터 멤버로 할당하는 것이 좋습니다.
std::shared_ptr<DX::DeviceResources> m_deviceResources;
App 클래스의 Initialize 메서드에서 DeviceResources instance 만든 후 클래스 생성자(또는 기타 초기화 메서드)에 포인터를 전달하기만 하면 됩니다. 대신 기본 클래스가 DeviceResources instance 완전히 소유하도록 하려면 weak_ptr 참조를 전달할 수도 있습니다.
큐브 렌더러 만들기
이 예제에서는 다음 메서드를 사용하여 장면 렌더러 클래스를 구성합니다.
- CreateDeviceDependentResources: 장면을 초기화하거나 다시 시작해야 할 때마다 호출됩니다. 이 메서드는 초기 꼭짓점 데이터, 텍스처, 셰이더 및 기타 리소스를 로드하고 초기 상수 및 꼭짓점 버퍼를 생성합니다. 일반적으로 대부분의 작업은 ID3D11DeviceContext 메서드가 아닌 ID3D11Device 메서드로 수행됩니다.
- CreateWindowSizeDependentResources: 크기 조정이 발생하는 경우 또는 방향이 변경되는 경우와 같이 창 상태가 변경 될 때마다 호출됩니다. 이 메서드는 카메라와 같은 변환 매트릭스를 다시 빌드합니다.
- 업데이트: 일반적으로 즉각적인 게임 상태를 관리하는 프로그램의 일부에서 호출됩니다. 이 예제에서는 Main 클래스에서 호출하기만 하면 됩니다. 개체 위치 또는 애니메이션 프레임 업데이트와 같은 렌더링에 영향을 주는 게임 상태 정보와 조명 수준 또는 게임 물리학 변경과 같은 전역 게임 데이터에서 이 메서드를 읽도록 합니다. 이러한 입력은 프레임별 상수 버퍼 및 개체 데이터를 업데이트하는 데 사용됩니다.
- 렌더링: 일반적으로 게임 루프를 관리하는 프로그램의 일부에서 호출됩니다. 이 경우 Main 클래스에서 호출합니다. 이 메서드는 셰이더를 바인딩하고, 버퍼와 리소스를 셰이더 스테이지에 바인딩하고, 현재 프레임에 대한 그리기를 호출하는 그래픽 파이프라인을 생성합니다.
이러한 메서드는 자산을 사용하여 Direct3D로 장면을 렌더링하기 위한 동작 본문을 구성합니다. 새 렌더링 클래스를 사용하여 이 예제를 확장하는 경우 기본 프로젝트 클래스에서 선언합니다. 이렇게 하려면 다음을 수행합니다.
std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;
다음과 같이 바꿉니다.
std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;
이 예제에서는 메서드가 구현에서 동일한 서명을 가지고 있다고 가정합니다. 서명이 변경된 경우 Main 루프를 검토하고 그에 따라 변경합니다.
장면 렌더링 메서드를 좀 더 자세히 살펴보겠습니다.
디바이스 종속 리소스 만들기
CreateDeviceDependentResources 는 ID3D11Device 호출을 사용하여 장면 및 해당 리소스를 초기화하기 위한 모든 작업을 통합합니다. 이 메서드는 Direct3D 디바이스가 장면에 대해 방금 초기화되었거나 다시 만들어진 것으로 가정합니다. 꼭짓점 및 픽셀 셰이더, 개체의 꼭짓점 및 인덱스 버퍼, 기타 리소스(예: 텍스처 및 해당 뷰)와 같은 모든 장면별 그래픽 리소스를 다시 만들거나 다시 로드합니다.
CreateDeviceDependentResources에 대한 예제 코드는 다음과 같습니다.
void Renderer::CreateDeviceDependentResources()
{
// Compile shaders using the Effects library.
auto CreateShadersTask = Concurrency::create_task(
[this]( )
{
CreateShaders();
}
);
// Load the geometry for the spinning cube.
auto CreateCubeTask = CreateShadersTask.then(
[this]()
{
CreateCube();
}
);
}
void Renderer::CreateWindowSizeDependentResources()
{
// Create the view matrix and the perspective matrix.
CreateViewAndPerspective();
}
컴파일된 셰이더 개체(CSO 또는 .cso) 파일 또는 텍스처와 같은 리소스를 디스크에서 로드할 때마다 비동기적으로 로드합니다. 이렇게 하면 다른 작업(예: 다른 설정 작업)을 동시에 계속할 수 있으며, 기본 루프가 차단되지 않으므로 사용자에게 시각적으로 흥미로운 항목(예: 게임의 로드 애니메이션)을 계속 표시할 수 있습니다. 이 예제에서는 Windows 8 시작하는 동시성::Tasks API를 사용합니다. 비동기 로드 작업을 캡슐화하는 데 사용되는 람다 구문을 확인합니다. 이러한 람다는 오프 스레드라는 함수를 나타내므로 현재 클래스 개체(이)에 대한 포인터가 명시적으로 캡처됩니다.
다음은 셰이더 바이트코드를 로드하는 방법의 예입니다.
HRESULT hr = S_OK;
// Use the Direct3D device to load resources into graphics memory.
ID3D11Device* device = m_deviceResources->GetDevice();
// You'll need to use a file loader to load the shader bytecode. In this
// example, we just use the standard library.
FILE* vShader, * pShader;
BYTE* bytes;
size_t destSize = 4096;
size_t bytesRead = 0;
bytes = new BYTE[destSize];
fopen_s(&vShader, "CubeVertexShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, vShader);
hr = device->CreateVertexShader(
bytes,
bytesRead,
nullptr,
&m_pVertexShader
);
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
);
delete bytes;
bytes = new BYTE[destSize];
bytesRead = 0;
fopen_s(&pShader, "CubePixelShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, pShader);
hr = device->CreatePixelShader(
bytes,
bytesRead,
nullptr,
m_pPixelShader.GetAddressOf()
);
delete bytes;
CD3D11_BUFFER_DESC cbDesc(
sizeof(ConstantBufferStruct),
D3D11_BIND_CONSTANT_BUFFER
);
hr = device->CreateBuffer(
&cbDesc,
nullptr,
m_pConstantBuffer.GetAddressOf()
);
fclose(vShader);
fclose(pShader);
꼭짓점 및 인덱스 버퍼를 만드는 방법의 예는 다음과 같습니다.
HRESULT Renderer::CreateCube()
{
HRESULT hr = S_OK;
// Use the Direct3D device to load resources into graphics memory.
ID3D11Device* device = m_deviceResources->GetDevice();
// Create cube geometry.
VertexPositionColor CubeVertices[] =
{
{DirectX::XMFLOAT3(-0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3( 0, 0, 0),},
{DirectX::XMFLOAT3(-0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3( 0, 0, 1),},
{DirectX::XMFLOAT3(-0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3( 0, 1, 0),},
{DirectX::XMFLOAT3(-0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3( 0, 1, 1),},
{DirectX::XMFLOAT3( 0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3( 1, 0, 0),},
{DirectX::XMFLOAT3( 0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3( 1, 0, 1),},
{DirectX::XMFLOAT3( 0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3( 1, 1, 0),},
{DirectX::XMFLOAT3( 0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3( 1, 1, 1),},
};
// Create vertex buffer:
CD3D11_BUFFER_DESC vDesc(
sizeof(CubeVertices),
D3D11_BIND_VERTEX_BUFFER
);
D3D11_SUBRESOURCE_DATA vData;
ZeroMemory(&vData, sizeof(D3D11_SUBRESOURCE_DATA));
vData.pSysMem = CubeVertices;
vData.SysMemPitch = 0;
vData.SysMemSlicePitch = 0;
hr = device->CreateBuffer(
&vDesc,
&vData,
&m_pVertexBuffer
);
// Create index buffer:
unsigned short CubeIndices [] =
{
0,2,1, // -x
1,2,3,
4,5,6, // +x
5,7,6,
0,1,5, // -y
0,5,4,
2,6,7, // +y
2,7,3,
0,4,6, // -z
0,6,2,
1,3,7, // +z
1,7,5,
};
m_indexCount = ARRAYSIZE(CubeIndices);
CD3D11_BUFFER_DESC iDesc(
sizeof(CubeIndices),
D3D11_BIND_INDEX_BUFFER
);
D3D11_SUBRESOURCE_DATA iData;
ZeroMemory(&iData, sizeof(D3D11_SUBRESOURCE_DATA));
iData.pSysMem = CubeIndices;
iData.SysMemPitch = 0;
iData.SysMemSlicePitch = 0;
hr = device->CreateBuffer(
&iDesc,
&iData,
&m_pIndexBuffer
);
return hr;
}
이 예제에서는 메시 또는 텍스처를 로드하지 않습니다. 게임과 관련된 메시 및 텍스처 형식을 로드하는 메서드를 만들고 비동기적으로 호출해야 합니다.
여기에서 장면별 상수 버퍼에 대한 초기 값도 채웁니다. 장면별 상수 버퍼의 예로는 고정 조명 또는 기타 정적 장면 요소 및 데이터가 있습니다.
CreateWindowSizeDependentResources 메서드 구현
CreateWindowSizeDependentResources 메서드는 창 크기, 방향 또는 해상도가 변경 될 때마다 호출됩니다.
창 크기 리소스는 다음과 같이 업데이트됩니다. 정적 메시지 절차는 창 상태 변경을 나타내는 몇 가지 가능한 이벤트 중 하나를 가져옵니다. 그런 다음 기본 루프는 이벤트에 대해 알리고 기본 클래스 instance CreateWindowSizeDependentResources를 호출한 다음, 장면 렌더러 클래스에서 CreateWindowSizeDependentResources 구현을 호출합니다.
이 메서드의 주요 작업은 창 속성의 변경으로 인해 시각 효과가 혼동되거나 잘못되지 않는지 확인하는 것입니다. 이 예제에서는 크기가 조정되거나 방향이 변경된 창에 대한 새 FOV(보기 필드)로 프로젝트 매트릭스를 업데이트합니다.
DeviceResources에서 창 리소스를 만들기 위한 코드가 이미 확인되었습니다. 즉, 스왑 체인(백 버퍼 포함) 및 렌더링 대상 뷰였습니다. 렌더러가 가로 세로 비율 종속 변환을 만드는 방법은 다음과 같습니다.
void Renderer::CreateViewAndPerspective()
{
// Use DirectXMath to create view and perspective matrices.
DirectX::XMVECTOR eye = DirectX::XMVectorSet(0.0f, 0.7f, 1.5f, 0.f);
DirectX::XMVECTOR at = DirectX::XMVectorSet(0.0f,-0.1f, 0.0f, 0.f);
DirectX::XMVECTOR up = DirectX::XMVectorSet(0.0f, 1.0f, 0.0f, 0.f);
DirectX::XMStoreFloat4x4(
&m_constantBufferData.view,
DirectX::XMMatrixTranspose(
DirectX::XMMatrixLookAtRH(
eye,
at,
up
)
)
);
float aspectRatioX = m_deviceResources->GetAspectRatio();
float aspectRatioY = aspectRatioX < (16.0f / 9.0f) ? aspectRatioX / (16.0f / 9.0f) : 1.0f;
DirectX::XMStoreFloat4x4(
&m_constantBufferData.projection,
DirectX::XMMatrixTranspose(
DirectX::XMMatrixPerspectiveFovRH(
2.0f * std::atan(std::tan(DirectX::XMConvertToRadians(70) * 0.5f) / aspectRatioY),
aspectRatioX,
0.01f,
100.0f
)
)
);
}
장면에 가로 세로 비율에 따라 달라지는 구성 요소의 특정 레이아웃이 있는 경우 가로 세로 비율에 맞게 다시 정렬할 수 있습니다. 여기에서도 사후 처리 동작의 구성을 변경할 수 있습니다.
Update 메서드 구현
Update 메서드는 게임 루프당 한 번 호출됩니다. 이 예제에서는 동일한 이름의 기본 클래스 메서드에 의해 호출됩니다. 이전 프레임 이후 경과된 시간(또는 경과된 시간 단계)의 양에 따라 장면 기하 도형 및 게임 상태를 업데이트하는 간단한 목적이 있습니다. 이 예제에서는 단순히 프레임당 큐브를 한 번 회전합니다. 실제 게임 장면에서 이 메서드는 게임 상태를 확인하고, 프레임당(또는 기타 동적) 상수 버퍼, 기하 도형 버퍼 및 기타 메모리 내 자산을 적절하게 업데이트하기 위한 더 많은 코드를 포함합니다. CPU와 GPU 간의 통신에는 오버헤드가 발생하므로 마지막 프레임 이후 실제로 변경된 버퍼만 업데이트해야 합니다. 이를 보다 효율적으로 만들기 위해 필요에 따라 상수 버퍼를 그룹화하거나 분할할 수 있습니다.
void Renderer::Update()
{
// Rotate the cube 1 degree per frame.
DirectX::XMStoreFloat4x4(
&m_constantBufferData.world,
DirectX::XMMatrixTranspose(
DirectX::XMMatrixRotationY(
DirectX::XMConvertToRadians(
(float) m_frameCount++
)
)
)
);
if (m_frameCount == MAXUINT) m_frameCount = 0;
}
이 경우 회전은 큐브에 대한 새 변환 매트릭스로 상수 버퍼를 업데이트합니다. 행렬은 꼭짓점 셰이더 단계에서 꼭짓점당 곱합니다. 이 메서드는 모든 프레임에서 호출되므로 동적 상수 및 꼭짓점 버퍼를 업데이트하는 메서드를 집계하거나 그래픽 파이프라인에 의한 변환을 위해 장면의 개체를 준비하는 다른 작업을 수행하는 것이 좋습니다.
Render 메서드 구현
이 메서드는 Update를 호출한 후 게임 루프당 한 번 호출 됩니다. Update와 마찬가지로 Render 메서드는 기본 클래스에서도 호출됩니다. ID3D11DeviceContext instance 메서드를 사용하여 프레임에 대해 그래픽 파이프라인이 생성되고 처리되는 메서드입니다. 이는 ID3D11DeviceContext::D rawIndexed에 대한 최종 호출에서 절정에 달합니다. 이 호출(또는 ID3D11DeviceContext에 정의된 다른 유사한 Draw* 호출)이 실제로 파이프라인을 실행한다는 것을 이해하는 것이 중요합니다. 특히 Direct3D가 GPU와 통신하여 그리기 상태를 설정하고, 각 파이프라인 단계를 실행하고, 스왑 체인에서 표시할 렌더링 대상 버퍼 리소스에 픽셀 결과를 쓰는 경우입니다. CPU와 GPU 간의 통신에는 오버헤드가 발생하므로, 특히 장면에 렌더링된 개체가 많은 경우 여러 그리기 호출을 단일 호출로 결합합니다.
void Renderer::Render()
{
// Use the Direct3D device context to draw.
ID3D11DeviceContext* context = m_deviceResources->GetDeviceContext();
ID3D11RenderTargetView* renderTarget = m_deviceResources->GetRenderTarget();
ID3D11DepthStencilView* depthStencil = m_deviceResources->GetDepthStencil();
context->UpdateSubresource(
m_pConstantBuffer.Get(),
0,
nullptr,
&m_constantBufferData,
0,
0
);
// Clear the render target and the z-buffer.
const float teal [] = { 0.098f, 0.439f, 0.439f, 1.000f };
context->ClearRenderTargetView(
renderTarget,
teal
);
context->ClearDepthStencilView(
depthStencil,
D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL,
1.0f,
0);
// Set the render target.
context->OMSetRenderTargets(
1,
&renderTarget,
depthStencil
);
// Set up the IA stage by setting the input topology and layout.
UINT stride = sizeof(VertexPositionColor);
UINT offset = 0;
context->IASetVertexBuffers(
0,
1,
m_pVertexBuffer.GetAddressOf(),
&stride,
&offset
);
context->IASetIndexBuffer(
m_pIndexBuffer.Get(),
DXGI_FORMAT_R16_UINT,
0
);
context->IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
);
context->IASetInputLayout(m_pInputLayout.Get());
// Set up the vertex shader stage.
context->VSSetShader(
m_pVertexShader.Get(),
nullptr,
0
);
context->VSSetConstantBuffers(
0,
1,
m_pConstantBuffer.GetAddressOf()
);
// Set up the pixel shader stage.
context->PSSetShader(
m_pPixelShader.Get(),
nullptr,
0
);
// Calling Draw tells Direct3D to start sending commands to the graphics device.
context->DrawIndexed(
m_indexCount,
0,
0
);
}
컨텍스트에서 다양한 그래픽 파이프라인 단계를 순서대로 설정하는 것이 좋습니다. 일반적으로 순서는 다음과 같습니다.
- 필요에 따라 새 데이터를 사용하여 상수 버퍼 리소스를 새로 고칩니다( 업데이트의 데이터 사용).
- IA(입력 어셈블리): 장면 기하 도형을 정의하는 꼭짓점 및 인덱스 버퍼를 연결하는 위치입니다. 장면의 각 개체에 대해 각 꼭짓점 및 인덱스 버퍼를 연결해야 합니다. 이 예제에는 큐브만 있으므로 매우 간단합니다.
- VS(꼭짓점 셰이더): 꼭짓점 버퍼의 데이터를 변환하는 꼭짓점 셰이더를 연결하고 꼭짓점 셰이더에 대한 상수 버퍼를 연결합니다.
- PS(픽셀 셰이더): 래스터화된 장면에서 픽셀당 작업을 수행할 픽셀 셰이더를 연결하고 픽셀 셰이더(상수 버퍼, 텍스처 등)에 대한 디바이스 리소스를 연결합니다.
- OM(출력 병합기): 셰이더가 완료된 후 픽셀이 혼합되는 단계입니다. 다른 단계를 설정하기 전에 깊이 스텐실을 연결하고 대상을 렌더링하기 때문에 규칙에 대한 예외입니다. 그림자 맵, 높이 맵 또는 기타 샘플링 기술과 같은 텍스처를 생성하는 추가 꼭짓점 및 픽셀 셰이더가 있는 경우 여러 스텐실 및 대상이 있을 수 있습니다. 이 경우 그리기 함수를 호출하기 전에 각 그리기 패스에 적절한 대상 집합이 필요합니다.
다음으로, 마지막 섹션(셰이더 및 셰이더 리소스 작업)에서 셰이더를 살펴보고 Direct3D에서 셰이더를 실행하는 방법을 설명합니다.
관련 항목