Borderlands2 DX11 프로젝트를 기준으로, 그래픽스 파이프라인 각 단계가 실제 코드에서 어디에 해당되는지 정리한다.
프로젝트 코드에서 그래픽스 파이프라인이 실제로 어떻게 구성되는지에 집중한다.
프로젝트 코드는 GitHub DirectX/Project 기준으로 작성했다.
이 프로젝트는 카메라가 오브젝트를 shader domain 기준으로 분류하고, deferred pass, light pass, HDR, bloom, tone mapping, 이후 forward와 transparent pass로 이어지는 다단계 렌더링 구조를 가진다.
전체 렌더링 흐름 구현
프레임 시작은 main.cpp에서 CEngine::GetInst()->progress()를 호출하고,
마지막에 CDevice::GetInst()->Present()를 호출하는 구조다.
실제 렌더링 진입점은 CRenderMgr.cpp이다. 여기서 전역 렌더 데이터를 갱신하고,MRT(Multi Rendering Target)를 초기화하고, shadow map을 먼저 렌더링한 뒤, 마지막에 등록된 카메라들의 render()를 호출한다.
void CRenderMgr::render()
{
UpdateData();
MRT_Clear();
render_shadowmap();
(this->*m_RenderFunc)();
}
즉 전체 프레임은 아래 순서로 흘러간다.
- 전역 조명/버퍼 갱신
- MRT 초기화
- shadow map 렌더링
- 카메라별 렌더링
- 최종 SwapChain 출력
오브젝트 분류
카메라는 먼저 View matrix와 Projection matrix를 계산한다.
view matrix: CCamera.cpp
void CCamera::CalculateViewMatrix() { m_matView = XMMatrixLookAtLH(vEye, vAt, vUp); }projection matrix: CCamera.cpp
void CCamera::CalculateProjMatrix() { m_matProj = XMMatrixPerspectiveFovLH(m_fFOV, m_fAspectRatio, m_fNear, m_fFar); }
그 다음 SortObject()에서 현재 레벨의 오브젝트를 순회하면서 render component와 material을 확인하고,
material이 들고 있는 shader의 domain 기준으로 오브젝트를 분류한다.
CCamera.cpp
switch (pMtrl->GetShader()->GetDomain())
{
case SHADER_DOMAIN::DOMAIN_DEFERRED:
m_vecDeferred.push_back(pMR);
break;
case SHADER_DOMAIN::DOMAIN_OPAQUE:
case SHADER_DOMAIN::DOMAIN_MASK:
m_vecOpaque.push_back(pMR);
break;
case SHADER_DOMAIN::DOMAIN_TRANSPARENT:
m_vecTransparent.push_back(pMR);
break;
}
어떤 shader domain에 속하는가를 기준으로 렌더 패스를 결정한다.
Shader 설정
프로젝트에서 tessellation을 사용하는 shader인 LandScapeShader의 설정코드.
CResMgr.cpp
여기서 다음 단계를 연결한다.
VS_LandScapeHS_LandScapeDS_LandScapePS_LandScape
CGraphicsShader* pShader = new CGraphicsShader;
pShader->SetKey(L"LandScapeShader");
pShader->CreateVertexShader(L"shader\\landscape.fx", "VS_LandScape");
pShader->CreateHullShader(L"shader\\landscape.fx", "HS_LandScape");
pShader->CreateDomainShader(L"shader\\landscape.fx", "DS_LandScape");
pShader->CreatePixelShader(L"shader\\landscape.fx", "PS_LandScape");
pShader->SetTopology(D3D11_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST);
pShader->SetDomain(SHADER_DOMAIN::DOMAIN_DEFERRED);
LandScapeShader는 3_CONTROL_POINT_PATCHLIST topology를 사용하는 tessellation object이고, 동시에 deferred pass로 들어간다.
Material, Texture 설정
terrain component의 실제 렌더 함수는 CLandScape.cpp에 있다.
이 함수에서는 먼저 Transform()->UpdateData()를 호출해서 matWorld, matWV, matWVP를 constant buffer에 올린다. 관련 코드 CTransform.cpp ~ CTransform.cpp
그 다음 terrain이 필요한 파라미터와 texture를 material에 넣는다.
Transform()->UpdateData();
GetMaterial(0)->SetScalarParam(INT_0, &m_iFaceX);
GetMaterial(0)->SetScalarParam(INT_1, &m_iFaceZ);
GetMaterial(0)->SetTexParam(TEX_2, m_HeightMap);
GetMaterial(0)->SetTexParam(TEX_3, m_ColorMap);
GetMaterial(0)->SetTexParam(TEX_4, m_WeightMap);
GetMaterial(0)->SetTexParam(TEX_5, m_vecDiffuseTex[0]);
GetMaterial(0)->UpdateData();
GetMesh()->render(0);
terrain은 CPU에서 먼저 이번 프레임에 필요한 height map, color map, weight map, camera position을 다 세팅하고, 마지막에 draw call을 날린다.
IA 단계
Input Assembler 단계는 vertex buffer, index buffer, input layout, primitive topology를 GPU에 바인딩한다.
관련 코드 CMesh.cpp 334, CMesh.cpp 335
CONTEXT->IASetVertexBuffers(0, 1, m_VB.GetAddressOf(), &iStride, &iOffset);
CONTEXT->IASetIndexBuffer(m_vecIdxInfo[_iSubset].pIB.Get(), DXGI_FORMAT_R32_UINT, 0);
그리고 shader 쪽에서는 input layout과 topology를 같이 설정한다.
관련 코드 CGraphicsShader.cpp 146 ~ CGraphicsShader.cpp 149
CONTEXT->IASetInputLayout(m_arrLayout[_iSubset].pLayout.Get());
CONTEXT->IASetPrimitiveTopology(m_eTopology);
IA 단계에서는 어떤 정점 형식의 데이터를 어떤 primitive로 해석할 것인가가 결정된다.
VS 단계: Local → World → View → Projection
Vertex Shader에서 가장 먼저 봐야 하는 것은 좌표 변환이다. 개념은 이전 그래픽스 파이프라인 글을 기준으로 보고, 여기서는 코드에서 그 과정이 어디에 보이는지를 정리한다.
이 프로젝트에서는 CPU 쪽 CTransform.cpp에서 먼저 변환 행렬을 만든다.
g_transform.matWorld = matWorld;
g_transform.matWV = matWorld * g_matView;
g_transform.matWVP = g_transform.matWV * g_matProj;
Local → World, World → View, View → Projection에 필요한 행렬은 CPU에서 미리 준비된다.
terrain 쪽 VS는 landscape.fx에 있는 VS_LandScape다. 여기서는 local 좌표를 그대로 넘기면서, hull shader가 사용할 world 좌표도 같이 계산한다.
Local → World
output.vWorldPos = mul(float4(_in.vPos, 1.f), g_matWorld).xyz;
이 단계에서 정점은 object local space에서 world space로 옮겨진다.
terrain은 뒤에서 tessellation factor를 거리 기준으로 계산해야 하기 때문에, world position이 여기서 미리 만들어진다.
World -> View
이 프로젝트는 terrain tessellation 경로에서는 VS에서 view까지 바로 밀지 않고, CPU에서 matWV를 준비한 뒤 뒤 단계에서 사용한다. 일반 mesh 경로에서는 std3d.fx 계열 shader에서 이 값이 바로 사용된다.
float4 viewPos = mul(float4(worldPos, 1.f), g_matView);
코드상 표현은 shader마다 조금 다르지만, 의미상 이 단계가 world space를 camera 기준 좌표계로 바꾸는 과정이다.
View → Projection → Clip
이 프로젝트는 대부분 g_matWVP를 한 번에 쓰는 형태로 clip space까지 보낸다.
output.vPos = mul(float4(vLocalPos, 1.f), g_matWVP);
실제 shader 코드에서는 Local → World → View → Projection을 각 단계로 쪼개기보다는,
CPU에서 미리 만든 WVP를 곱해서 clip space까지 보내는 경우가 많다.
Hull Shader와 Tessellator
terrain tessellation의 핵심은 landscape.fx 부근의 patch constant function이다.
이 함수에서는 삼각형의 edge midpoint와 중심점 기준으로 카메라와의 거리를 구하고, 거리에 따라 tessellation factor를 다르게 계산한다.
output.Edges[0] = CalculateTessFactor(vEdgeMidPos0);
output.Edges[1] = CalculateTessFactor(vEdgeMidPos1);
output.Edges[2] = CalculateTessFactor(vEdgeMidPos2);
output.Inside = CalculateTessFactor(vCenterPos);
카메라에 가까운 patch는 더 세밀하게 쪼개고, 먼 patch는 덜 쪼개도록 LOD 성격의 tessellation이 들어간다. HS_LandScape 본체는 control point를 그대로 전달하고, 실제 분할 수준은 patch constant function이 결정한다.
Domain Shader: 실제 terrain 표면 생성
Domain Shader는 tessellator가 쪼갠 patch 내부 좌표를 받아 실제 terrain 표면을 만드는 단계다. 핵심 코드는 landscape.fx부터 시작한다.
여기서는 barycentric weight로 local position과 UV를 보간한 뒤, height map을 샘플링해서 최종 높이를 만든다.
vLocalPos = _out.Edges[0] * patch[0].vPos
+ _out.Edges[1] * patch[1].vPos
+ _out.Edges[2] * patch[2].vPos;
output.vHeightMapUV = _out.Edges[0] * patch[0].vUV
+ _out.Edges[1] * patch[1].vUV
+ _out.Edges[2] * patch[2].vUV;
vLocalPos.y = HeightMap.SampleLevel(g_sam_anti_0, output.vHeightMapUV, 0).x;
output.vPos = mul(float4(vLocalPos, 1.f), g_matWVP);
terrain의 실제 표면 형상은 VS가 아니라 DS에서 완성된다.
Geometry Shader
메인 terrain 흐름이나 일반 weapon mesh 흐름에서는 geometry shader를 거의 사용하지 않는다. 대신 geometry shader는 particle billboard 확장 쪽에 선택적으로 사용된다.
관련 코드
- shader 등록: CResMgr.cpp
- shader 파일: particle_render.fx
pShader->CreateVertexShader(L"shader\\particle_render.fx", "VS_Particle");
pShader->CreateGeometryShader(L"shader\\particle_render.fx", "GS_Particle");
pShader->CreatePixelShader(L"shader\\particle_render.fx", "PS_Particle");
geometry shader는 “프로젝트 전체에서 없는 단계”가 아니라, terrain/weapon 메인 경로가 아니라 particle 전용 확장 경로에 있다.
Rasterizer
Rasterizer 단계의 clipping, perspective divide, viewport transform 자체는 DX11 고정 기능 단계가 처리한다. 이 프로젝트 코드에서는 그 앞뒤 상태만 직접 세팅한다.
RS state는 CGraphicsShader.cpp에서 설정하고, viewport는 MRT를 바인딩할 때 같이 세팅하게 구현했다.
관련 코드 mMRT.cpp, mMRT.cpp
CONTEXT->OMSetRenderTargets((UINT)m_vecRT.size(), vecRTV.data(), pDSV);
CONTEXT->RSSetViewports(1, &m_viewport);
rasterizer는 shader 코드 안에서 다루는 것이 아니라, render target과 viewport 상태를 바꿀 때 같이 제어된다.
Pixel Shader: G-Buffer 채우기
terrain의 pixel shader는 landscape.fx에 있는 PS_LandScape다.
이 함수에서는 weight map을 읽어서 tile array의 여러 레이어를 섞고, 그 결과를 G-buffer에 기록한다.
float4 vWeight = WeightMap.Sample(g_sam_0, _in.vHeightMapUV);
vOut.vDiffuse = vColor0 * vWeight.x
+ vColor1 * vWeight.y
+ vColor2 * vWeight.z
+ vColor3 * vWeight.w;
pixel shader는 단순히 색만 칠하는 것이 아니라, deferred lighting에 필요한 diffuse, normal, position 같은 버퍼를 함께 채운다.
Output Merger
Output Merger 단계는 MRT가 담당한다. OMSet()에서 render target view와 depth stencil view를 묶어서 바인딩하고, viewport까지 같이 세팅한다.
관련 코드 mMRT.cpp 63~ mMRT.cpp 88
또한 blend state와 depth stencil state는 graphics shader가 마지막에 같이 세팅한다.
관련 코드 CGraphicsShader.cpp 159. CGraphicsShader.cpp 164
CONTEXT->OMSetBlendState(m_pBSState.Get(), BlendFactor, 0xffffffff);
CONTEXT->OMSetDepthStencilState(m_pDSState.Get(), 0);
이 프로젝트의 OM 단계는 어느 render target에 쓸 것인가와“어떤 depth/blend 규칙으로 합칠 것인가를 함께 관리한다.
Tessellation을 사용하지 않는 데이터
다른 mesh는 HS/DS를 타지 않는다. 일반 mesh는 CMeshRender를 통해 그려진다.
Transform()->UpdateData()Animator3D()->UpdateData()GetMaterial(i)->UpdateData()GetMesh()->render(subset)
Transform()->UpdateData();
if (Animator3D())
Animator3D()->UpdateData();
GetMaterial(i)->UpdateData();
GetMesh()->render(i);
일반 mesh는 IA → VS(Local → World → View → Projection → Clip) → Rasterizer → Pixel Shader → OM → 최종 화면 흐름으로 그려진다.
메인 카메라의 pass 순서
메인 카메라 렌더 순서는 CCamera.cpp의 RenderMain()에서 확인할 수 있다.
m_DeferredMRT->OMSet();
render_deferred();
m_LightMRT->OMSet();
render_lights();
merge_hdr();
render_bloom();
render_forward();
render_deferred_decal();
render_transparent();
render_postprocess();
- deferred MRT 바인딩
render_deferred()- light MRT 바인딩 후 light volume 렌더링
- HDR merge
- bloom
- forward
- decal
- transparent
- postprocess
m_DeferredMRT->OMSet();
render_deferred();
m_LightMRT->OMSet();
render_lights();
render_forward();
render_transparent();
render_postprocess();
이 프로젝트는 forward rendering 하나로 끝나는 구조가 아니라, deferred와 후처리 체인을 중심으로 최종 화면을 조립한다.
내용에 대한 질의나, 수정 요청은 저에게 큰 도움이 됩니다.
'그래픽스' 카테고리의 다른 글
| 그래픽스 파이프 라인 (0) | 2026.04.01 |
|---|