오늘의 TIL 목차 (22. 12. 14)
- 렌더링 파이프라인
렌더링 파이프라인
렌더링 파이프라인이란?
3D 세계를 가상 카메라를 이용해 2D 이미지(우리가 보는 화면)를 만들어내는 역할을 담당

렌더링 파이프라인 특징
- 렌더링 파이프라인은 행렬의 곱으로 변환이 이뤄진다.
- 게임 오브젝트마다 각각 변환을 적용시킬 수 있다.
- 렌더링 파이프라인을 끝마쳐야지만 출력이 되기 때문에, 우리가 변환을 해주지 않은 경우 자동으로 항등행렬이 곱해진다.
- 렌더링 파이프라인은 게임 오브젝트 출력 순서, 변환(SetTransform) 선언 순서와 상관없이 디바이스 자체에서 보관하고 있다가 가장 마지막에 렌더링 파이프라인 순서대로 자동 정렬해서 각 오브젝트마다 변환을 실행
- 변환을 적용하지 않을 경우 자동으로 항등행렬이 곱해지지만, 다른 곳에서 해당 변환의 행렬이 존재한다면 항등 행렬 대신 해당 행렬이 곱해진다. ( 밑 예제 참고 )
- 뷰 포트까지 우리가 관여할 수 있다.
[ Jusin 5개월차 6일차 수업 기준 ]
CLogo : public Engine::Scene ( 스테이지 1, 2 같은 씬임 )
< CLogo::Ready_Scene() >
Ready_Proto(); // 프로톤 타입 패턴을 위해 복제해서 사용할 기능(CComponent)들을 Create하여 ProtoMgr의 map에 넣어줌
Ready_Layer_Environment(); // 사용할 게임 오브젝트(Player, Monster 등)을 CLayer의 map에 넣어줌
=> 오브젝트들을 Create()하는 과정에서 Ready_GameObject() -> Add_Component()가 실행됨
< C오브젝트::Add_Component() >
pComponent = m_pBufferCom = dynamic_cast<CRcTex*>(Engine::Clone_ProtoType(L"Proto_RcTex"));
NULL_CHECK_RETURN(pComponent, E_FAIL);
m_uMapComponent[ID_STATIC].insert({ L"Proto_RcTex", pComponent });
// 프로토타입에 넣어둔 기능들을 복사하여 갖고와서 컴포넌트 맵에 insert
* 씬에서 오브젝트들을 넣고, 오브젝트에서 컴포넌트들 넣는다.
< CPlayer::Render_GameObject() > // 마동석 텍스처 출력
m_pGraphicDev->SetTransform(D3DTS_WORLD, m_pTransformCom->Get_WorldMatrixPointer());
< CBackGround::Render_GameObjcet() > // 아이유 텍스처 출력
월드 변환 생략 ( + Update()에서 Key_Input()도 생략 )
=> 플레이어(마동석)를 움직이면 가만히 있어야 할 백그라운드(아이유)도 움직이는 문제 발생
[ 원인 ]
원인 : 디바이스는 각 행렬들을 보관해뒀다가 마지막 시점에 렌더링 파이프라인을 수행.
또한, 변환을 직접 명시해주지 않은 경우 자동으로 항등행렬을 곱해줌
=> 플레이어에 월드 변환을 명시하지 않았기 때문에 항등행렬을 자동으로 곱해주려 했지만
맨 마지막 다같이 렌더링 파이프라인을 적용하는 시점에서 CBackGround에 적용되어 있는
월드 행렬을 보고 자동으로 항등 행렬이 아닌 CBackGround의 월드 행렬을 적용 시킴
=> 맨 마지막 시점에서 곱하는 것이기 때문에 CBackGround를 먼저 Create해서 Add_GameObject했든
CPlayer를 먼저 map에 넣었든 상관없이 다른 오브젝트의 월드 행렬을 디폴트로 적용시켜 버림
[ 해결 ]
< CBackGround::Render_GameObject() >
_matrix matWorld;
D3DXMatrixIdentity(&matWorld); // 항등행렬 만드는 함수
m_pGraphicDev->SetTransform(D3DTS_WORLD, &matWorld);
=> 직접 항등행렬 곱해주어 CBackGround 고유의 월드 행렬이 있다고 명시해줘야함.
즉, 원래는 명시 안 하면 자동으로 항등 행렬을 곱해줘야 하는데 다른 오브젝트의 월드 행렬을
자동으로 곱해주는 문제를 없애기 위해 우리가 직접 항등행렬을 곱해버리는 것.
공간 변환을 위해선 행렬 전달하는 함수
ID3D11DevcieContext::SetTrasnform(D3DTS_WORLD, &월드 행렬);
렌더링 파이프라인 과정
아래는 요약된 렌더링 파이프라인 과정을 설명한다.
1. 로컬 스페이스 (모델링 스페이스)
물체의 삼각형 리스트(Triangle List)를 정의하는 데 이용하는 좌표 시스템
로컬 스페이스를 이용하면 위치나 크기, 월드 내의 다른 물체와의 관계를 고려하지 않고도 모델을 구성할 수 있다.

2. 월드 스페이스
여러 개의 3D 물체들을 하나의 전역(월드) 좌표 시스템으로 옮겨 하나의 장면을 구성
로컬 스페이스의 물체들은 이동, 회전, 크기 변형 등을 포함하는 월드 변환 과정을 거쳐 옮겨진다.

2.1. 월드 변환 행렬 (로컬 → 월드)
월드 변환 행렬은 크(기) * 자(전) * 이(동) * 공(전) * 부(모) 순으로 행렬을 곱해준다.
행렬 곱은 교환 법칙이 성립되지 않기 때문에 순서가 중요하다.
크기 행렬

회전 행렬
각도는 회전축을 따라 원점을 내려다 볼 때 시계 방향으로 측정한다.
회전 행렬의 반시계 방향($-\theta$)은 전치 행렬과 동일하다.



회전 행렬을 구성하는 함수
/** (DirectX9) Y-축 회전 행렬을 구성하는 D3DX 함수 */
D3DXMATRIX* D3DXMatrixRotationY(
_Inout_ D3DXMATRIX *pOut, // 결과
_In_ FLOAT Angle // 라디안으로 측정한 회전각
);
/** (DirectX11) directxmath 함수 */
XMMATRIX XM_CALLCONV XMMatrixRotationX(
[in] float Angle
) noexcept;
예제: 정사각형(min(-1, 0, 1), max(1, 0, 1))을 시계방향으로 $-30$도 회전
이는 반시계 방향으로 30도 회전과 동일한 의미이다.



이동 행렬

3. 뷰 스페이스
카메라를 기점으로 월드를 바라보는 공간
카메라가 월드 내 임의의 위치나 방위를 가지면 이후 작업이 어렵거나 덜 효율적이기 때문에
카메라를 월드 시스템의 원점으로 변환하고, 양의 z-축을 바라보도록 월드 내 모든 기하물체를 카메라 기준에 맞추어 변환한다.
월드 내 모든 기하물체에 카메라 역행렬을 곱한 것과 같다.

3.1. 뷰 스페이스 변환 행렬 (월드 → 뷰스페이스)
D3DXMATRIX *D3DXMatrixLookAtLH(
D3DXMATRIX* pOut , // 카메라 변환 행렬 (Out)
CONST D3DXVECTOR3* pEye , // 월드 내의 카메라 위치
CONST D3DXVECTOR3* pAt , // 월드 내의 카메라가 보는 지점
CONST D3DXVECTOR3* pUp // 월드의 엽 벡터 - (0, 1, 0)
}
Device->SetTransform(D3DTS VIEW , &V);
4. 후면 추려내기 (뷰 스페이스)
일반적으로 카메라가 폴리곤의 후면을 보지 못하는 점을 고려하여 폴리곤의 전면, 후면 중 후면을 추려내는 것
폴리곤의 후면을 볼 수 있는 경우, 후면 추려내기가 작동하지 않는다.


4.1. 폴리곤의 전면 / 후면을 구분하는 법
1) 버텍스의 두르기 순서에 따라 결정된다.(디폴트 Direct3D 설정)
전면 폴리곤은 뷰 스페이스에서 시계 방향, 후면 폴리곤은 반시계 방향으로 지정된 버텍스를 가진다.
※ 뷰 스페이스 시점의 두르기 방향으로 정의함에 주의
삼각형이 180도 회전할 경우, 두르기 순서가 뒤집힌다.
로컬 스페이스에서 시계 방향의 두르기 순서를 가지고 있던 폴리곤이어도
뷰 스페이스 변환 과정에서 회전할 경우 시계 방향의 두르기 순서가 아니게 될 수 있다.
2) D3DRS_CULLMODE 렌더 상태를 수정하여 추려내기 기준을 변경할 수 있다.
SetRenderState로 렌더 상태 수정 시 모든 오브젝트에 적용되므로,
특정 오브젝트에 다른 렌더 상태 설정 시 다시 되돌려줘야 한다.
이 껐다켰다 하는 낭비를 줄이기 위해서 렌더 상태가 같은 오브젝트끼리 묶어서 출력을 수행한다.
m_pGraphicDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); // 출력상태 설정
[ D3DRS_CULLMODE ] : 후면 추려내기 모드를 설정하겠다
D3DCULL_NONE : 후면 추려내기 X
D3DCULL_CW : 인덱스 시계 방향인 경우 추려내기
D3DCULL_CCW : 인덱스 반시계 방향인 경우 추려내기 (디폴트)
/** 특정 오브젝트에 D3DCULL_NONE을 적용 후, 다시 되돌려놓기 */
m_pGraphicDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); // 후면 추려내기 X
m_pTextureCom->Set_Texture(); // 텍스처 설정 (디폴트 매개변수 = 0)
m_pBufferCom->Render_Buffer(); // CRcTex - CVIBuffer의 Render 수행
m_pGraphicDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); // 반시계 방향 추려내기
5. 조명 (뷰 스페이스)
광원은 월드 스페이스 내에 정의되며, 뷰 스페이스 변환에 의해 뷰 스페이스로 변환된다.
광원은 물체에 명암을 추가하여 사실감을 더해준다.
조명은 고정 기능 파이프라인(5장) 또는 프로그래머블 파이프라인(IV)에서 다룬다.
6. 클리핑 (투영 공간)
절두체(시야 볼륨) 외부의 기하 물체를 추려낸다.
- 완전한 내부 - 보존되어 다음 단계로 진행
- 완전한 외부 - 추려냄.
- 부분적 내부(외부) - 삼각형을 두 개의 부분으로 분리하여, 절두체 내부의 부분만 보존한다.

7. 투영 공간
n 차원에서 n - 1차원을 얻는 과정, 현재 3D 장면의 2D 표현을 얻는 과정
투영은 직교 투영과 원근 투영이 있으나, 일반적으로 사용하는 원근 투영에 대해 설명한다.
원근 투영은 원근법을 사용하여 물체를 투영 시켜 멀리 떨어진 물체는 비교적 작게 나타낸다.
투영 변환은 절두체(시야 볼륨)을 정의하고, 절두체 내의 물체를 투영 평면에 투영한다.


7.1. 원근 투영 변환 행렬
// #include "directxmath.h"
XMMATRIX XM_CALLCONV XMMatrixPerspectiveFovLH(
[in] float FovAngleY, // 수직 시야곽 (라디안)
[in] float AspectRatio, // 종횡비 (너비/높이)
[in] float NearZ, // 가까운 클리핑 평면 거리 (0 < NearZ)
[in] float FarZ // 원거리 클리핑 평면 거리 (0 < FarZ)
) noexcept;
XMMATRIX matProj;
D3DXMatrixPerspectiveFovLH(&matProj, D3DXToRadian(60.f), (float)WINCX / (float)WINCY, 1.f, 1000.f);
// 수직 시야각 60, 근평면 1, 원평면 1000인 원근 투영 행렬 생성
※ 줌 아웃 효과: 시야각이 클수록 담겨지는 절두체 범위가 넓어지므로 화면 상의 오브젝트들이 작게 보임
7.2. 직교 투영 변환 행렬
// #include "directxmath.h"
XMMATRIX XM_CALLCONV XMMatrixOrthographicLH(
[in] float ViewWidth, // 가까운 클리핑 평면의 절두체 너비
[in] float ViewHeight, // 가까운 클리핑 평면의 절두체 높이
[in] float NearZ, // 가까운 클리핑 평면 거리 (0 < NearZ)
[in] float FarZ // 원거리 클리핑 평면 거리 (0 < FarZ)
) noexcept;
※ 뷰 스페이스 공간 → 투영 변환 행렬 → 클립 공간(클리핑) → z 나누기 → 투영 공간
8. 뷰포트
정사각형 NDC 공간에서 뷰포트라 불리는 직사각형 화면으로 변환되는 과정
뷰포트는 일반적으로 전체 화면이 되지만, 화면의 일부가 될 수도 있다.
뷰포트 변환은 DirectX에서 D3DVIEWPORT9 구조체를 넘기면 자동으로 수행해준다.
마우스 픽킹 등 직접 뷰포트 상의 좌표를 이전 공간으로 역변환해야될 때, 변환 공식을 직접 사용하곤 한다.

8.1. 뷰포트 변환 행렬

뷰포트 변환 (투영 → 뷰포트 )
스크린 포인트 x: ((투영 x + 1.f) * (Swidth * 0.5f)); // 분배법칙 이용, Swidth: 1280.f
스크린 포인트 y: ((투영 y - 1.f)) * -(Sheight * 0.5f); // Sheight: 720.f
뷰포트 역변환 (뷰포트 → 투영)
x: 구해야할 X / (1280.f * 0.5f) - 1.f;
y: 구해야할 Y / -(720.f * 0.5f) + 1.f;
z: 0.f;
w: 1.f;
9. 래스터라이즈
정점을 스크린 좌표로 변환 시, 2D 삼각형들의 리스트를 가진다. 각각의 삼각형을 그리는 데 필요한 픽셀 컬러들을 계산한다.
레스터라이즈의 결과물은 모니터에 바로 디스플레이할 수 있는 2D 이미지가 된다.

World-View-Projection 변환 파이프라인
월드-뷰-투영 변환 파이프라인이란?
3D 모델의 정점을 2D 화면 좌표로 변환하는 과정
1. 월드 변환
로컬 좌표계를 가진 각 모델을 게임의 한 장면에 담기 위해 전역 좌표계인 월드 좌표계로 변환합니다.
각 모델은 이동, 회전, 크기를 가진 월드 변환 행렬을 곱하여 월드 공간으로 이동됩니다.
2. 뷰 변환
월드 공간 내 물체를 월드 내 카메라 기준으로 보기 위해 변환하는 과정입니다.
연산을 간편하게 하기 위해 카메라를 원점으로 이동 시키고, 카메라의 방향을 양의 z-축 방향으로 회전시키는 과정입니다.
월드 내 모든 물체는 카메라의 역행렬인 뷰 변환 행렬을 곱하여 카메라 기준의 뷰 공간으로 변환합니다.
3. 투영 변환
투영 변환은 뷰 스페이스 공간의 정점을 클립 공간으로 변환하고, 추후 z 나누기를 통해서 2차원 투영 평면에 투영합니다.
투영 변환 행렬은 근평면과 원평면의 z값을 정의하여 절두체 공간을 만들고, 이를 통해 정점이 클립 공간으로 이동합니다.
이때, 동차좌표계를 이용해서 깊이값과 뷰스페이스 z값을 보관합니다.
투영 변환 행렬을 통해 뷰 스페이스 정점이 클립 공간으로 이동하면,
뷰 스페이스 z값을 보관하고 있는 동차 좌표계의 마지막 원소로 나누어 정규화된 투영 평면으로 투영됩니다.
행렬 곱
월드-뷰-투영 변환 과정을 통해 3D 모델이 2D 화면으로 올바르게 표현됩니다.
각 단계는 행렬 곱을 통해서 효율적으로 계산하여 각 정점은 모든 변환을 한번씩만 곱하여 적용할 수 있습니다.
(각 정점마다 5개의 행렬을 곱하지 않고, 결합법칙을 이용해서 한번에 행렬곱 만들어 각 정점에 대해 한 번씩만 행렬을 곱하면 됨.)

실제 게임에서는 변환 행렬 셋팅 순서와 상관없이 마지막에 렌더링 파이프라인 순서에 맞게 곱하여 진행함.
참고 문헌
[1] Frank D. Luna, 최현호 역, DirectX 9를 이용한 3D 게임 프로그래밍 입문, 정보문화사, 2008, pp.107-119.
'DirectX 기반 그래픽스 > 그래픽스 이론' 카테고리의 다른 글
| [DirectX] 그래픽 파이프라인 - 입력 어셈블러(IA) 단계 (0) | 2023.02.13 |
|---|---|
| [3D 6일차] 쓰레드(feat. 크리티컬 섹션), CLoader (0) | 2023.02.09 |
| [그래픽스] 계층 구조 (0) | 2023.01.12 |
| [그래픽스] 충돌 검사 - 점과 점/선/평면과의 충돌 (0) | 2023.01.10 |
| [DirectX] 픽킹, 교차 판정 - 경계 구체와 반직선(광선) (0) | 2022.12.19 |