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.hlsl
    
    • main.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

  1. 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 객체를 가리킬 출력 포인터의 위치(더블 포인터!)
        );
        
  2. ID3D11DeviceContext::IASetInputLayout()

    • 현재 파이프라인에서 사용할 Input Layout 지정

    • 그냥 간단한 상태 설정 함수라서 프레임 렌더링 루프 내에서 반복 호출해도 문제 없음

    • 다른 메시나 셰이더를 그릴 때 레이아웃을 바꿀 수도 있음

    • 예시

      context->IASetInputLayout(inputLayout);
      
  3. ID3D11DeviceContext::IASetPrimitiveTopology()

    • 정점들을 어떤 도형 단위로 묶을지 지정

    • 예시

      context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
      

2. Vertex Shader Stage

  1. ID3D11Device::CreateVertexShader()

    • 컴파일된 VS 바이트코드로부터 GPU가 실행 가능한 VS 객체를 생성하여 등록

    • 초기화 단계에 한 번만 실행

    • 예시

      device->CreateVertexShader(
          vsBlob->GetBufferPointer(), // pShaderBytecode, 컴파일된 VS의 바이트코드(시작 주소)
          vsBlob->GetBufferSize(), // BytecodeLength, VS 바이트코드의 크기
          nullptr, // pClassLinkage, 셰이더 동적 링크 시 사용하며, 보통은 nullptr
          &vertexShader // ppVertexShader, 생성된 VS 객체를 가리킬 출력 포인터의 위치(더블 포인터)
      );
      
  2. ID3D11DeviceContext::VSSetShader()

    • 현재 파이프라인에서 사용할 VS 지정

    • 프레임 렌더링 루프 내에서 사용

    • VS는 한 번에 하나만 활성화해야 함

    • 예시

      context->VSSetShader(
          vertexShader, // pVertexShader, 사용할 VS 객체
          nullptr, // ppClassInstances, HLSL 클래스 인스턴스 설정하는 부분인데, 거의 사용 안 함
          0 // NumClassInstances, HLSL 클래스 인스턴스 개수
      );
      
  3. 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 포인터 배열의 주소(더블 포인터)
        );
        
  4. 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의 포인터 배열의 주소(더블 포인터)
        );
        
  5. 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

  1. 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 출력
      );
      
  2. ID3D11DeviceContext::RSSetState()

    • 현재 파이프라인에서 사용할 Rasterizer State 지정

    • 예시

      context->RSSetState(rasterizerState);
      
  3. 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 배열(더블 포인터)
      );
      
  4. 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

  1. 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]에 기록되는 것

  2. 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
      )
      
  3. ID3D11DeviceContext::OMSetDepthStencilState()

    • Depth-Stencil State를 파이프라인에 바인딩

    • 예시

      context->OMSetDepthStencilState(
          depthStencilState, // pDepthStencilState, Depth-Stencil State 객체(DepthEnable, DepthFunc 등)
          0 // StencilRef, Stencil 비교에 사용하는 기준 값. 안 쓰면 보통 0
      )