그래픽스/DX11

MVP (Model, View, Projection)

뽀또치즈맛 2025. 5. 15. 16:13

MVP (Model, View, Projection)

 

 
 
 
 
매 프레임 당 한 번씩 호출되는 Update 함수 안에서 
 
모델 변환을 의미하는 모델 행렬, 
어떤 각도에서 어떻게 보는지 의미하는 시점 변환을 결정하는 뷰 행렬
그것을 화면에 어떻게 프로젝션 시켜줄지 결정하는 프로젝션 행렬
 
이 행렬들을 합쳐서 MVP 라고 부른다.
 
이 세가지 행렬이 어떤 의미가 있는지 직관적으로 이해하고,
관련 행렬을 통해서 DX에서 MVP를 구현해보자.
 

 
앞서 시점 변환에서 말했듯 어떤 방향에서 어떻게 보이는지를 알려면
위치와 방향을 알아야한다.
따라서 m_viewEyePos, m_viewEyeDir을 사용하는 것이다.
viewUp도 내가 보고 있는 위 방향이 어디냐? 라는 것이다.
 
 
물체가 왼쪽으로 이동하던 보고있는 카메라(나)가 오른쪽으로 이동하던 똑같다.
물체를 왼쪽으로 움직이는 것과 eyePos를 오른쪽으로 움직이는 것은 같은 변환이다.
 
특히 가상 공간에 박스 하나만 있으면 구분이 안된다.
 
따라서 뷰 행렬을 만드는 내부 원리는 모델 행렬을 만드는 것과 동일하다.
플레이어가 점프한다 => 물체가 아래에서 위로 왔다갔다 하는 것과 같다
즉, 상대적인 거리가 중요한 것이지, 물체가 아래로 갔나 플레이어가 위로 갔냐가 중요하지는 않다.
 

 
 

1. 투영 설정 토글

// true 면 원근투영(perspective), false 면 직교투영(orthographic)을 사용하겠다는 의미
bool m_usePerspectiveProjection = true;
  • true 면 원근투영(perspective), false 면 직교투영(orthographic)을 사용하겠다는 의미이다.

 

2. 모델 변환(Model Transform)

Vector3 m_modelTranslation = Vector3(0.0f);  // 모델 위치 이동
Vector3 m_modelRotation    = Vector3(0.0f);  // 모델 회전(축당 라디안)
Vector3 m_modelScaling     = Vector3(0.5f);  // 모델 스케일(0.5배)

 

  • Translation : 월드 좌표계에서 모델을 얼마만큼 이동시킬지
  • Rotation : 각 축(X,Y,Z) 기준으로 얼마만큼 회전시킬지
  • Scaling : 모델 크기를 얼마나 확대, 축소할지

이 세가지를 조합해 최종 월드 행렬 (세계 변환 행렬, World Matrix)을 만든다.


3. 뷰 변환(View Transform)

Vector3 m_viewEyePos = { 0.0f, 0.0f, -2.0f };  // 카메라(시점) 위치
Vector3 m_viewEyeDir = { 0.0f, 0.0f,  1.0f };  // 카메라가 바라보는 방향(또는 초점 위치)
Vector3 m_viewUp     = { 0.0f, 1.0f,  0.0f };  // 위쪽 벡터(카메라의 '머리' 방향)

 

  • XMMatrixLookAtLH(eye, focus, up) 함수를 호출할 때 이 값들이 들어가서
    • eye = 카메라 위치
    • focus = 카메라가 바라보는 지점 (혹은 eye + eyeDir)
    • up = 월드의 위쪽 방향
  • 결과로  view matrix 가 생성되어, 월드 좌표를 카메라 좌표로 변환한다.


4. 투영(Projection) 파라미터

float m_projFovAngleY = 70.0f;  // 수직 시야각(도 단위)
float m_nearZ         =  0.01f; // near 클리핑 평면
float m_farZ          = 100.0f; // far  클리핑 평면

 

  • FOV(Field Of View) : 수직 방향으로 카메라가 볼 수 있는 시야각
  • nearZ / farZ : 카메라 시야의 앞,뒤 절투체(클리핑 영역) 경계

원근 투영을 쓸 경우 XMMatrixPerspectiveFovLH 같은 함수에,
직교 투영을 쓸 경우 XMMatrixOrthographicLH 같은 함수에 이 값들을 넣어
투영 행렬(Projection Matrix) 을 만든다.
 
 
예시 코드

// 1) World Matrix (모델 변환)
XMMATRIX S = XMMatrixScaling   (m_modelScaling.x, m_modelScaling.y, m_modelScaling.z);
XMMATRIX R = XMMatrixRotationRollPitchYaw(m_modelRotation.x, m_modelRotation.y, m_modelRotation.z);
XMMATRIX T = XMMatrixTranslation( m_modelTranslation.x, m_modelTranslation.y, m_modelTranslation.z);
m_constantBufferData.World = S * R * T;

// 2) View Matrix (카메라)
XMVECTOR eye    = XMLoadFloat3(&m_viewEyePos);
XMVECTOR focus  = XMLoadFloat3(&m_viewEyePos) + XMLoadFloat3(&m_viewEyeDir); // 또는 m_viewEyeDir을 직접 focus로
XMVECTOR up     = XMLoadFloat3(&m_viewUp);
m_constantBufferData.View = XMMatrixLookAtLH(eye, focus, up);

// 3) Projection Matrix (투영)
if (m_usePerspectiveProjection)
    m_constantBufferData.Proj = XMMatrixPerspectiveFovLH(
        XMConvertToRadians(m_projFovAngleY),
        aspectRatio, m_nearZ, m_farZ );
else
    m_constantBufferData.Proj = XMMatrixOrthographicLH(
        viewWidth, viewHeight, m_nearZ, m_farZ );

 
 
기존 강의 코드

 m_constantBufferData.model =
     Matrix::CreateScale(m_modelScaling) * Matrix::CreateRotationY(m_modelRotation.y) *
     Matrix::CreateRotationX(m_modelRotation.x) * Matrix::CreateRotationZ(m_modelRotation.z) *
     Matrix::CreateTranslation(m_modelTranslation);
 m_constantBufferData.model = m_constantBufferData.model.Transpose();

 // 시점 변환
 // m_constantBufferData.view = XMMatrixLookAtLH(m_viewEye, m_viewFocus, m_viewUp);
 m_constantBufferData.view = XMMatrixLookToLH(m_viewEyePos, m_viewEyeDir, m_viewUp);
 m_constantBufferData.view = m_constantBufferData.view.Transpose();

 // 프로젝션
 // m_aspect = AppBase::GetAspectRatio(); // <- GUI에서 조절
 if (m_usePerspectiveProjection) {
     m_constantBufferData.projection = XMMatrixPerspectiveFovLH(
         XMConvertToRadians(m_projFovAngleY), m_aspect, m_nearZ, m_farZ);
 } else {
     m_constantBufferData.projection =
         XMMatrixOrthographicOffCenterLH(-m_aspect, m_aspect, -1.0f, 1.0f, m_nearZ, m_farZ);
 }
 m_constantBufferData.projection = m_constantBufferData.projection.Transpose();

 // Constant를 CPU에서 GPU로 복사
 AppBase::UpdateBuffer(m_constantBufferData, m_constantBuffer);

 
 

1. 월드(모델) 행렬 생성 방식

강의 코드

model =
  Matrix::CreateScale(m_modelScaling) *
  Matrix::CreateRotationY(m_modelRotation.y) *
  Matrix::CreateRotationX(m_modelRotation.x) *
  Matrix::CreateRotationZ(m_modelRotation.z) *
  Matrix::CreateTranslation(m_modelTranslation);

 
 

  • DirectXTK 의 Matrix::Create… 메서드를 사용하고,
  • Y → X → Z 회전을 순차적으로 곱해서 회전 순서를 명시적으로 제어

 
강의 기반 수정된 예시 코드

XMMATRIX S = XMMatrixScaling   (sx, sy, sz);
XMMATRIX R = XMMatrixRotationRollPitchYaw(pitch, yaw, roll);
XMMATRIX T = XMMatrixTranslation(tx, ty, tz);
World = S * R * T;

 

  • XMMatrixRotationRollPitchYaw 로 XYZ 회전을 한 번에 처리

 

2. 뷰(View) 행렬 생성 방식

// 강의 기반 예시 코드
View = XMMatrixLookAtLH(eyePos, focusPos, up);

// 강의 코드
// LookAt 대신 LookTo 사용
view = XMMatrixLookToLH(m_viewEyePos, m_viewEyeDir, m_viewUp);

 

  • LookAtLH(eye, target, up) 은 초점 위치를 직접 넘기는 반면,
  • LookToLH(eye, dir, up) 은 시선 방향 벡터(dir) 를 넘겨서 뷰를 계산

둘 다 초점을 잡고 계산한 걸 넘겨주냐,
초첨 위치와 타깃을 넘겨준 뒤 계산은 함수에 맡기느냐 차이다.
 
// DX 내부 코드

inline XMMATRIX XM_CALLCONV XMMatrixLookToLH
(
    FXMVECTOR EyePosition,
    FXMVECTOR EyeDirection,
    FXMVECTOR UpDirection
) noexcept
{
    assert(!XMVector3Equal(EyeDirection, XMVectorZero()));
    assert(!XMVector3IsInfinite(EyeDirection));
    assert(!XMVector3Equal(UpDirection, XMVectorZero()));
    assert(!XMVector3IsInfinite(UpDirection));

    XMVECTOR R2 = XMVector3Normalize(EyeDirection);

    XMVECTOR R0 = XMVector3Cross(UpDirection, R2);
    R0 = XMVector3Normalize(R0);

    XMVECTOR R1 = XMVector3Cross(R2, R0);

    XMVECTOR NegEyePosition = XMVectorNegate(EyePosition);

    XMVECTOR D0 = XMVector3Dot(R0, NegEyePosition);
    XMVECTOR D1 = XMVector3Dot(R1, NegEyePosition);
    XMVECTOR D2 = XMVector3Dot(R2, NegEyePosition);

    XMMATRIX M;
    M.r[0] = XMVectorSelect(D0, R0, g_XMSelect1110.v);
    M.r[1] = XMVectorSelect(D1, R1, g_XMSelect1110.v);
    M.r[2] = XMVectorSelect(D2, R2, g_XMSelect1110.v);
    M.r[3] = g_XMIdentityR3.v;

    M = XMMatrixTranspose(M);

    return M;
}

 
 
 

3. 투영(Projection) 행렬 생성 방식

강의 기반 수정된 예시 코드

if (usePersp)
  Proj = XMMatrixPerspectiveFovLH(fovY, aspect, nearZ, farZ);
else
  Proj = XMMatrixOrthographicLH(width, height, nearZ, farZ);

 
 
강의 코드

if (m_usePerspectiveProjection) {
  projection = XMMatrixPerspectiveFovLH(...);
} else {
  // 화면 중심을 원점(0,0)에 두고, 좌·우·하·상 범위를 직접 지정
  projection = XMMatrixOrthographicOffCenterLH(
    -m_aspect, m_aspect, -1.0f, 1.0f, m_nearZ, m_farZ);
}
  • 직교투영 시 OrthographicOffCenterLH 를 써서 뷰볼의 경계를
    left/right/bottom/top 으로 세밀하게 조절

 

    // Create a rasterizer state
    D3D11_RASTERIZER_DESC rastDesc;
    ZeroMemory(&rastDesc, sizeof(D3D11_RASTERIZER_DESC)); // Need this
    rastDesc.FillMode = D3D11_FILL_MODE::D3D11_FILL_SOLID;
    // rastDesc.FillMode = D3D11_FILL_MODE::D3D11_FILL_WIREFRAME;
    rastDesc.CullMode = D3D11_CULL_MODE::D3D11_CULL_NONE;
    rastDesc.FrontCounterClockwise = false;
    rastDesc.DepthClipEnable = true; // <- zNear, zFar 확인에 필요

 
rastDesc.DepthClipEnable zFar에 의해 클리핑이 된다.
zFar 은 뒷면의 어디까지 렌더링 할 것인가?
zNear은 얼마나 가까이 까지 렌더링 할 것인가?
 
 

    // 프로젝션
    // m_aspect = AppBase::GetAspectRatio(); // <- GUI에서 조절
    if (m_usePerspectiveProjection) {
        m_constantBufferData.projection = XMMatrixPerspectiveFovLH(
            XMConvertToRadians(m_projFovAngleY), m_aspect, m_nearZ, m_farZ);
    } else {
        m_constantBufferData.projection =
            XMMatrixOrthographicOffCenterLH(-m_aspect, m_aspect, -1.0f, 1.0f, m_nearZ, m_farZ);
    }
    m_constantBufferData.projection = m_constantBufferData.projection.Transpose();

m_aspect의 값이 작을 때는 화면이 짧다 라고 생각해서 물체를 옆으로 늘려서 랜더링 하는 것이다.
m_aspect의 값이 클 때는 화면이 길다 라고 생각해서 물체를 위로 늘려서 렌더링 하는 것이다.
 
 

4. 행렬 전치(Transpose)

강의 코드

m_constantBufferData.model      = model.Transpose();
m_constantBufferData.view       = view .Transpose();
m_constantBufferData.projection = projection.Transpose();

 

  • 강의 기반 예시 코드 예시에는 전치(transpose)를 생략했지만,
  • 새 코드 에서는 GPU(특히 HLSL 상수 버퍼) 에 맞게 모든 행렬을 전치 한 뒤 복사

 
 
상수 버퍼 업데이트는
두 코드 모두
AppBase::UpdateBuffer(m_constantBufferData, m_constantBuffer) 혹은 유사 함수로
CPU 메모리에서 GPU 상수 버퍼로 복사
 
 
내용 정리
 

  1. 월드 행렬: XMMatrix… vs Matrix::Create… + 회전 순서
  2. 뷰 행렬: LookAtLH vs LookToLH
  3. 투영 행렬: OrthographicLH vs OrthographicOffCenterLH
  4. Transpose: 새 코드에선 셰이더 호환을 위해 모든 행렬을 전치
  5. 상수 버퍼 복사 흐름은 유사하나, 전치 위치가 다르다.

 
 
 
 

'그래픽스 > DX11' 카테고리의 다른 글

D3D11 Resize Viewport  (0) 2025.06.10
HLSL 개요  (2) 2025.06.03
D3D11 조명 관련 노멀 벡터 변환  (2) 2025.02.03
D3D11 애파인 변환(=아핀 변환)  (1) 2024.12.16
D3D11 회전축에 대한 벡터의 회전  (1) 2024.12.13