6 minutes
❎ Direct3D 11 Graphics Pipeline - 7. 그래픽스 프로그램 구조와 주요 Direct3D 11 함수들
1. 그래픽스 프로그램의 기본 구조
우선 알아두어야 하는 것은, CPU 프로그램과 GPU 프로그램은 역할이 다름
CPU(C++ 실행 파일): 일반적인 실행 파일로,
main()함수를 포함하며, D3D 11 API를 호출함- 렌더링 파이프라인 구성 및 제어 담당
GPU(HLSL 등의 Shader 프로그램): GPU에서 실행되는 작은 프로그램으로, 각 단계(VS, PS 등)마다 진입 지점(
VS_Main등)이 하나씩 있음
프로젝트의 최소 구조
/Project ├── main.cpp ├── pixel_shader.hlsl ├── rederer.cpp └── vertex_shader.hlslmain.cpp: 프로그램의 시작점으로, 그래픽스 렌더러를 사용하는 쪽Win32 API 등을 통해 윈도우 생성
메시지 / 프레임 루프
렌더러 객체 생성
rederer.cpp: 그래픽스 시스템 구현부로, CPU에서 하위 제어하는 쪽파이프라인 구성
(D3D 11의 경우)
ID3D11Device,ID3D11DeviceContext생성스왑체인 생성
Render Target / Depth Buffer 생성
Shader 컴파일 및 로딩
Vertex Buffer / Input Layout 설정
Draw호출
vertex_shader.hlsl: 정점 처리 프로그램으로, 정점 하나당 한 번 실행좌표 변환(Object Space -> Clip Space)
정점 속성 전달
pixel_shader.hlsl: 픽셀 색상 계산 프로그램으로, 픽셀(정확히는 fragment) 하나당 한 번 실행픽셀의 최종 색상 결정
텍스처 샘플링
조명 계산
2. Stage별 D3D 11 주요 함수들
d3d11.h내 함수들
1. Input-Assembler Stage
ID3D11Device::CreateInputLayout()정점 버퍼의 메모리 레이아웃과 Vertex Shader 입력 시그니처를 연결하는 Input Layout 객체를 생성
초기화 단계에 VS가 준비된 이후 한 번 실행
예시
VS 입력 형태 예시
struct VSInput { float3 pos: POSITION; float4 color: COLOR; };C++ 정점 구조체 및 Input Layout 정의
// 정점 구조체 struct Vertex { float position[3]; float color[4]; // RGBA } // Input Layout 정의 D3D11_INPUT_ELEMENT_DESC layout[] = { { "POSITION", // SemanticName, VS에서 POSITION sementic과 매칭됨 0, // SemanticIndex, POSITION0으로 매핑되며, POSITION1, POSITION2인 경우 1, 2로 씀 DXGI_FORMAT_R32G32B32_FLOAT, // Format, float(단정밀도 32bit) 3개이므로 RGB로 간주 0, // InputSlot, 0번 Vertex Buffer로 사용함을 의미하며, 여러 Input Buffer를 사용하고 싶은 경우 나누어 사용 0, // AlignedByteOffset, 버퍼 시작으로부터 몇 Byte 떨어져 있는지 결정. POSITION은 버퍼 첫 부분이니 지금은 0 D3D11_INPUT_PER_VERTEX_DATA, // InputSlotClass, 인스턴싱을 사용한다면 다른 클래스를 사용하며, 지금 클래스는 정점 하나 당 데이터를 하나씩 사용한다는 의미 0 // InstanceDataStepRate, 인스턴싱 사용 시 몇 개의 데이터를 쓰는지 결정하며, D3D11_INPUT_PER_VERTEX_DATA를 사용하는 경우 이 값은 반드시 0이여야 함 }, { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, // RGBA 4개의 float(단정밀도 32bit) 0, 12, // POSITION이 4Byte 씩 3개의 값을 사용하므로 12Byte 오프셋. Byte 단위임을 반드시 기억! D3D11_INPUT_PER_VERTEX_DATA, 0 } } // Input Layout 생성 device->CreateInputLayout( layout, // pInputElementDescs, 정점 데이터 해석 규칙 배열. POSITION, COLOR 등이 어디에 있는지 설명 ARRAYSIZE(layout), // NumElements, layout 배열의 원소 개수(입력 요소의 개수) vsBlob->GetBufferPointer(), pShaderBytecodeWithInputSignature, 컴파일된 VS의 바이트코드, VS의 입력 시그니처와 Input Layout이 호환되는지 확인하기 위해 여기서 필요 vsBlob->GetBufferSize(), // BytecodeLength, VS 바이트코드의 크기 &inputLayout // ppInputLayout, 최종 생성된 ID3D11InputLayout 객체를 가리킬 출력 포인터의 위치(더블 포인터!) );
ID3D11DeviceContext::IASetInputLayout()현재 파이프라인에서 사용할 Input Layout 지정
그냥 간단한 상태 설정 함수라서 프레임 렌더링 루프 내에서 반복 호출해도 문제 없음
다른 메시나 셰이더를 그릴 때 레이아웃을 바꿀 수도 있음
예시
context->IASetInputLayout(inputLayout);
ID3D11DeviceContext::IASetPrimitiveTopology()정점들을 어떤 도형 단위로 묶을지 지정
예시
context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
2. Vertex Shader Stage
ID3D11Device::CreateVertexShader()컴파일된 VS 바이트코드로부터 GPU가 실행 가능한 VS 객체를 생성하여 등록
초기화 단계에 한 번만 실행
예시
device->CreateVertexShader( vsBlob->GetBufferPointer(), // pShaderBytecode, 컴파일된 VS의 바이트코드(시작 주소) vsBlob->GetBufferSize(), // BytecodeLength, VS 바이트코드의 크기 nullptr, // pClassLinkage, 셰이더 동적 링크 시 사용하며, 보통은 nullptr &vertexShader // ppVertexShader, 생성된 VS 객체를 가리킬 출력 포인터의 위치(더블 포인터) );
ID3D11DeviceContext::VSSetShader()현재 파이프라인에서 사용할 VS 지정
프레임 렌더링 루프 내에서 사용
VS는 한 번에 하나만 활성화해야 함
예시
context->VSSetShader( vertexShader, // pVertexShader, 사용할 VS 객체 nullptr, // ppClassInstances, HLSL 클래스 인스턴스 설정하는 부분인데, 거의 사용 안 함 0 // NumClassInstances, HLSL 클래스 인스턴스 개수 );
ID3D11DeviceContext::VSSetConstantBuffers()VS에서 사용하는 Constant Buffer 바인딩
좌표 변환을 위한 행렬 등은 상수 버퍼로 사용
예시
VS 상수 버퍼 예시
cbuffer Transform: register(b0) { float4x4 MVP; };Constant Buffer 설정
context->VSSetConstantBuffers( 0, // StartSlot, b0 슬롯부터 바인딩한다는 의미 1, // NumBuffers, 바인딩할 Constant Buffer 개수 &constantBuffer // ppConstantBuffers, ID3D11Buffer 포인터 배열의 주소(더블 포인터) );
ID3D11DeviceContext::VSSetShaderResources()VS에서 사용할 Shader Resource View(SRV) 바인딩
텍스처같은 것을 VS에 바인딩하는 과정으로, VS에서도 텍스처 샘플링이 가능하기는 해서 있는 기능이지만 PS보다 사용 빈도는 낮음
예시
VS 텍스처 리소스 예시
Texture2D heightMap: register(t0);SRV 설정
context->VSSetShaderResources( 0, // StartSlot, t0 슬롯부터 읽기 1, // NumViews, SRV 개수 &srv // ppShaderResourceView, 바인딩할 SRV의 포인터 배열의 주소(더블 포인터) );
ID3D11DeviceContext::VSSetSampler()VS에서 사용할 Sampler State 설정
마찬가지로 VS에서 텍스처 샘플링을 수행할 때 사용하며, VS에서도 가능은 하지만 주 용도는 PS
예시
VS Sampler State 예시
SamplerState linearSampler: register(s0);Sampler State 설정
context->VSSetSampler( 0, // StartSlot, s0 슬롯부터 읽기 1, // NumSamplers &samplerState // ppSamplers, Sampler State 배열(더블 포인터) );
3. Rasterizer State
ID3D11Device::CreateRasterizerState()Rasterizer State 객체 생성
보통 초기화 시 한 번 만들어 두고 재사용
예시
D3D11_RASTERIZER_DESC rsDesc{}; rsDesc.FillMode = D3D11_FILL_SOLID; // 폴리곤 채우기 방식(SOLID / WIREFRAME) rsDesc.CullMode = D3D11_CULL_BACK; // 컬링 방식(BACK / FRONT / NONE) rsDesc.FrontCounterClockwise = FALSE; // 정면 판정 기준(시계 / 반시계) rsDesc.DepthClipEnable = TRUE; // Z 클리핑 활성화 여부 device->CreateRasterizerState( &rsDesc, // Rasterizer 설정 구조체 &rasterizerState // 생성된 Rasterizer State 출력 );
ID3D11DeviceContext::RSSetState()현재 파이프라인에서 사용할 Rasterizer State 지정
예시
context->RSSetState(rasterizerState);
ID3D11DeviceContext::RSSetViewports()NDC(-1 ~ 1) 좌표를 실제 화면 좌표로 변환하는 영역 지정
여러 Viewport를 통해 화면 분할 렌더링도 가능
좌표 변환의 마지막 단계
예시
D3D11_VIEWPORT viewport{}; viewport.TopLeftX = 0.0f; // Viewport 좌측 상단 X viewport.TopLeftY = 0.0f; // Viewport 좌측 상단 Y viewport.Width = 1280.0f; // Viewport 너비 viewport.Height = 720.0f; // Viewport 높이 viewport.MinDepth = 0.0f; // 깊이 최소값(보통은 고정) viewport.MaxDepth = 1.0f; // 깊이 최대값(보통은 고정) context->RSSetViewports( 1, // 설정할 Viewport 개수 &viewport // Viewport 배열(더블 포인터) );
ID3D11DeviceContext::RSSetScissorRects()그릴 수 있는 픽셀 영역을 사각형으로 제한
UI 렌더링에 주로 사용하며 Viewport와는 전혀 다른 개념
예시
D3D11_RECT scissor{}; scissor.left = 0; scissor.top = 0; scissor.right = 1280; scissor.bottom = 720; context->RSSetScissorRects( 1, // 설정할 Scissor 개수 &scissor // scissor 배열(더블 포인터) );
4. Pixel Shader Stage
셰이더 코드가 아니라 API 수준이라서 VS와 코드 자체는 거의 똑같음
- VS로 된 것을 PS로 바꾸기만 하면 동일
ID3D11DeviceContext::PSSetConstantBuffers()API 자체는 VS와 동일하지만, 하나 알아두면 좋은 것은 각 셰이더가 같은
register(b0)를 쓰더라도 그b0가 VS / PS 별로 별개의 공간을 사용한다는 것VS와는 완전히 독립된 Constant Buffer 슬롯을 사용
5. Output-Merger Stage
ID3D11DeviceContext::OMSetRenderTargets()픽셀 출력이 기록될 Render Target들과 Depth-Stencil Buffer를 파이프라인에 바인딩
예시
ID3D11RenderTargetView* rtvs[] = { backBufferRTV }; // 실제 화면에 그려질 RTV. 여기에 여러 RTV가 포함되면 그 RTV들이 모두 동시에 그려짐. MRT의 개념이지 더블 버퍼링이 아님! context->OMSetRenderTargets( 1, // NumValues, 바인딩할 RTV 개수. 대부분 1개(화면) rtvs, // ppRenderTargetViews, RTV 포인터 배열(더블 포인터) dsv // pDepthStencilView, Depth-Stencil View 객체. Depth Test에 반드시 필요하며, 사용하지 않을 경우 nullptr도 가능 )PS는 그 결과를
SV_TARGET(또는 MRT에서SV_TARGET0,SV_TARGET1, …)에 쓰며, 이것이rtvs[0]에 기록되는 것
ID3D11DeviceContext::ClearDepthStencilView()Depth-Stencil Buffer를 프레임 시작에 초기화
보통 깊이를 1.0으로 채우기를 매 프레임 수행
깊이를 클리어하지 않으면 이전 프레임 깊이값 때문에 현재 프레임이 가려지는 현상 발생
초기 깊이가 1.0(가장 먼 값)이어야 이번 프레임에서 새로 그리는 픽셀이 정상적으로 그려짐(보통 Depth 비교 시
LESS또는LESS_EQUAL을 쓰기 때문에)
예시
context->ClearDepthStencilView( dsv, // pDepthStencilView, 초기화할 DSV D3D11_CLEAR_DEPTH, // ClearFlags, Stencil도 함께 초기화하려면 D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL 1.0f, // Depth, 클리어할 깊이 값. D3D 기본 깊이 범위가 0 ~ 1이라 보통 가장 먼 1로 초기화 0 // Stencil, Stencil 클리어 값. 안 쓰면 보통 0 )
ID3D11DeviceContext::OMSetDepthStencilState()Depth-Stencil State를 파이프라인에 바인딩
예시
context->OMSetDepthStencilState( depthStencilState, // pDepthStencilState, Depth-Stencil State 객체(DepthEnable, DepthFunc 등) 0 // StencilRef, Stencil 비교에 사용하는 기준 값. 안 쓰면 보통 0 )