DirectX

16. SimpleMath 실습

devRiripong 2023. 12. 29.
반응형

사전 작업

 

Game.cpp에서 

void Game::CreateGeometry()
{
	// VertexData - CPU 메모리에 있는 거 
	// 1 3
	// 0 2
	{
		_vertices.resize(4);

		_vertices[0].position = Vec3(-0.5f, -0.5f, 0.f); 
		_vertices[0].uv = Vec2(0.f, 1.f); 
		// _vertices[0].color = Color(1.f, 0.f, 0.f, 1.f); 

		_vertices[1].position = Vec3(-0.5f, 0.5f, 0.f);
		_vertices[1].uv = Vec2(0.f, 0.f);
		// _vertices[1].color = Color(1.f, 0.f, 0.f, 1.f);

		_vertices[2].position = Vec3(0.5f, -0.5f, 0.f);
		_vertices[2].uv = Vec2(1.f, 1.f);
		// _vertices[2].color = Color(1.f, 0.f, 0.f, 1.f);

		_vertices[3].position = Vec3(0.5f, 0.5f, 0.f);
		_vertices[3].uv = Vec2(1.f, 0.f);
		// _vertices[3].color = Color(1.f, 0.f, 0.f, 1.f);
	}

이렇게 uv값을 5에서 1로 되돌려 주고, 

 

void Game::CreateSamplerState()
{
	D3D11_SAMPLER_DESC desc; 
	ZeroMemory(&desc, sizeof(desc));
	// 여기다가 마음에 드는 옵션들을 세팅해 보면된다. 
	desc.AddressU = D3D11_TEXTURE_ADDRESS_BORDER;
	desc.AddressV = D3D11_TEXTURE_ADDRESS_BORDER;
	desc.AddressW = D3D11_TEXTURE_ADDRESS_BORDER;

여기서는 MIRROR에서 BORDER로 되돌려준다.
그러면 그냥 이미지가 1장이 화면에 띄어지게 된다. 

 

 

오늘 수업 내용

 

1. SampleMath 라이브러리 보기 

강의 프로젝트를 다운 받고, SimpleMath.cpp, .h, .inl 삼총사가 있는데 이걸 복사해서 작업하는 프로젝트에 갖고 온다. 
Headers 필터 안에 Utils라는 필터를 만들어서 넣는다.
SimpleMath.h를 보면 microsoft에서 배포하는 라이브러리 라는 것을 알 수 있다. 그냥 구글에서 검색하면 사용할 수 있게 배포하고 있다. 
직접 만들 수도 있겠지만 혹시 실수 했을 때 DX코드가 잘못됐는지 만든 라이브러리 코드가 잘못됐는지 찾기가 어려울 수 있다. 

앞으로 자주 사용할 예정이니 Types.h에 SimpleMath를 추가해주자.

#include <Windows.h>
#include "DirectXMath.h"
#include "SimpleMath.h"

그리고

using Vec2 = DirectX::XMFLOAT2;
using Vec3 = DirectX::XMFLOAT3;
using Vec4 = DirectX::XMFLOAT4;
using Color = DirectX::XMFLOAT4;

 

이렇게 DirectX에서 제공하는 것들을 사용했었는데

//using Vec2 = DirectX::XMFLOAT2;
//using Vec3 = DirectX::XMFLOAT3;
//using Vec4 = DirectX::XMFLOAT4;
using Color = DirectX::XMFLOAT4;

using Vec2 = DirectX::SimpleMath::Vector2;
using Vec3 = DirectX::SimpleMath::Vector3;
using Vec4 = DirectX::SimpleMath::Vector4;
using Matrix = DirectX::SimpleMath::Matrix;

 

이렇게 SimpleMath의 것을 사용하게 수정한다.
using의 것을 바꿔줬기 때문에 기존의 코드를 수정하지 않아도 잘 빌드가 된다는 것을 알 수 있다.

삼각함수, 벡터, 메트릭스, 행렬 이런 걸 공부했는데, 직접 만들어도 되지만 생각보다 작업의 규모가 크기 때문에 필수는 아니고 최소한 코드를 살펴보면 된다. 
예를 들어 Vector3를 f12로 타고 들어 가면, operator, length등이 다 구현이 되어 있다. length의 XMLoadFloat3에 타고 들어가면 #if defined(_XM_NO_INTRINSICS_) 가 있는데 여기로 들어가지 않는 행렬에 최적화 된 부분의 코드는 이해가 안될 수 있다.  #if defined(_XM_NO_INTRINSICS_) 이 버전의 코드가 이해하기 쉽다. 이 부분을 사용하면 속도는 최적화가 아니지만 이해하기 쉬운 코드가 실행이 되는 걸 알 수 있다.

pch.h에 가서

 #define _XM_NO_INTRINSICS_

 

을 넣어주면, 그 부분의 코드가 실행된다. 


SimpleMath.h의 코드를 한번씩 분석해 보면 좋다. 내적Dot, 외적Cross, Normalize 구현하는 것도 나온다.

 

Matrix를 살펴보면, Matrix는 XMFLOAT4X4를 상속받고 있다. 

struct XMFLOAT4X4
{
    union
    {
        struct
        {
            float _11, _12, _13, _14;
            float _21, _22, _23, _24;
            float _31, _32, _33, _34;
            float _41, _42, _43, _44;
        };
        float m[4][4];
    };

XMFLOAT4X4는 union으로 두가지 방법으로 사용할 수 있게 해 놓았다. 이렇게 행렬을 어떻게 만들어 놨는지 볼 수 있다. 

 

행렬의 4칙연산, 역행렬, 전치행렬(transpose), SRT과 관련된 부분이 있는지 체크한다. 
Create시리즈가 있다. STR 각각의 행렬을 만들 때 넣었던 내용과 완전히 동일한 값을 채워 주는 것을 볼 수 있다. 

코사인, 사인 구하는게 속도가 느려서 서버 같은 데서는 사용할 거 같은 값들의 근사치들을 계산해 놓은 다음에 그걸 들고 있기도 한다. 

 

이 라이브러리 쓸 때 주의할 점은 오른손 좌표계를 사용하기 때문에 왼손 좌표계로 바꾸려면 Forward벡터와 Backward벡터의 방향을 수정해야 한다. XMMatrixLookAtLH 같은 함수를 사용하거나, 알맞은 벡터 값을 인자로 넣어서 수정해 사용할 수 있다. 아애 라이브러리에 LH를 넣어 놓은 라이브러리 버도 있으니 그걸 사용하면 편할 수 있다.

1. Forward와 Backward 벡터:일반적으로 오른손 좌표계에서는 Forward 벡터가 z축의 양의 방향을, Backward 벡터가 z축의 음의 방향을 나타냅니다. 왼손 좌표계로 전환할 때 이 두 벡터의 방향을 반대로 바꾸는 것이 일반적입니다.
2. Up, Down 벡터: 이들은 보통 y축을 따르며, 좌표계 전환에 영향을 받지 않습니다.    
3. Right, Left 벡터: x축을 따르며, 이 또한 보통 좌표계 전환에 영향을 받지 않습니다.    
4. 변환 행렬(Transform Matrices): 오른손 좌표계와 왼손 좌표계는 회전 방향이 반대입니다. 따라서 회전을 다루는 변환 행렬도 수정해야 할 수 있습니다.    
5. 카메라 및 뷰 행렬: 3D 그래픽에서 카메라의 위치와 방향을 설정하는 데 사용되는 뷰 행렬도 좌표계에 따라 달라집니다.  
6. 법선 벡터(Normal Vectors): 오브젝트의 표면 법선을 계산할 때 좌표계가 바뀌면 결과도 달라집니다.

 

이게 분석이 끝났다면 지금까지 배웠던 모든 내용들을 할 준비가 된 것이다.


SRT뿐만 아니라 나머지 다른 함수들도 어딘가에 만들어 놨다.
왠만해서는 들어가 있으니 이걸 이용해서 만들면 된다.

 

2. ConstantBuffer 실습

몇 가지 테스트를 해볼건데 몇 개만 고쳐본다.

 

constant buffer를 수정해 볼 건데 원래는 VS에서 offset 값으로 이미지를 움직이게 하는 것을 전 시간에 했었다.

struct TransformData
{
	Vec3 offset; 
	float dummy;  
	// 컨스턴트 버퍼를 만들 때는 16바이트 정렬을 해야하기 때문에 
};

Struct.h의 TransformData에 가서 기존의 내용을 삭제하고, 

struct TransformData
{
	Matrix matWorld = Matrix::Identity;;
	Matrix matView = Matrix::Identity;;
	Matrix matProjection = Matrix::Identity; // 항등행렬
};

초기값을 항등행렬로 맞춰준다.

 

그리고 Game::Update()에서 뭔가를 설정해주면 된다. 

유니티에서 보면 대부분 SRT를 갖고 있다. 

Game.h에 변수를 선언해 준다.

	Vec3 _localPosition = { 0.f, 0.f, 0.f }; 
	Vec3 _localRotation = { 0.f, 0.f, 0.f };
	Vec3 _localScale = { 1.f, 1.f, 1.f };

이걸 유니티에서처럼 고쳐줄 수가 있다.

 

그럼 그거에 따라서 Update에서 무엇을 할 것이다. 

void Game::Update()
{
	// Scale Rotation Translation

	Matrix matScale = Matrix::CreateScale(_localScale); 
	Matrix matRotation = Matrix::CreateRotationX(_localRotation.x); 
	matRotation *= Matrix::CreateRotationY(_localRotation.y); 
	matRotation *= Matrix::CreateRotationZ(_localRotation.z); 
	// x,y,z 축 회전 행렬 곱하는 순서는 상관 없다. 
	Matrix matTranslation = Matrix::CreateTranslation(_localPosition);

	Matrix matWorld = matScale * matRotation * matTranslation; // SRT
	_transformData.matWorld = matWorld;

아직 까지는 카메라가 없으니까, world에다가 넣어 줬다. matView와 matProjection은 Identity상태로 냅뒀다. 

빌드를 해보면 문제가 없다. 

Update의 전체 코드를 보면 다음과 같다. 

void Game::Update()
{
	// Scale Rotation Translation

	Matrix matRotation = Matrix::CreateRotationX(_localRotation.x); 
	matRotation *= Matrix::CreateRotationY(_localRotation.y); 
	matRotation *= Matrix::CreateRotationZ(_localRotation.z); // x,y,z 축 회전 행렬 곱하는 순서는 상관 없다. 
	Matrix matTranslation = Matrix::CreateTranslation(_localPosition);

	Matrix matWorld = matScale * matRotation * matTranslation; // SRT
	_transformData.matWorld = matWorld; 

	D3D11_MAPPED_SUBRESOURCE subResource; 
	ZeroMemory(&subResource, sizeof(subResource));

	_deviceContext->Map(_constantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource); 
	::memcpy(subResource.pData, &_transformData, sizeof(_transformData)); 
	_deviceContext->Unmap(_constantBuffer.Get(), 0); 
}

_transformData 설정을 해줬고, 이걸 shader에게 넘기면 된다. 

Shader는 Default.hlsl에 가서 VS에서 받아 줬었어. 

cbuffer TransformData : register(b0)
{ 
    // float4 offset; // 이전 시간 코드 
	// 이번에는 그냥 float4가 아니라 matrix를 넘겨줄 것이다. 
	row_major matrix matWorld;
	row_major matrix matView;
	row_major matrix matProjection;		
}

한가지 조심해야 할 점은 row_major를 붙여줘야 하는데 이건 visual studio에서 사용하는 HLSL의 순서와 Game::Update 코드상에서 만드는 순서랑 위치가 달라서 그렇다. transpose를 이용해서 배꿔주거나, 행우선, 열 우선 이런 게 있다. 아니면 shader에서 지금처럼 row_major를 붙여서 힌트를 줘야 하는데 그 부분은 당장은 중요하지 않으니 일단은 만들어 본다.

 

Game::Update에서 WVP 가 각각 세팅이 된 것이고, 그 WVP를 여기 TransformData에 꽂아 준 것이다. 
상수 버퍼에 꽂아 줬으면 그걸 사용할 수 있게 된 거니까 VS만 고쳐주면 된다. 
예를 들면 다음과 같이 만들어 본다. 

 

VS_OUTPUT VS(VS_INPUT input)
{ 
    VS_OUTPUT output; 
    
    // WVP
    float4 position = mul(input.position, matWorld); // W
    position = mul(position, matView); // V
    position = mul(position, matProjection); // P 
    
//  output.position = input.position + offset; // 이전 코드
    output.position = position; 
    output.uv = input.uv;  
    
    return output; 
}

이렇게 만들어 본다.

Update에서 딱히 Identity 상태에서 고친게 없다 보니까, 실행을 해보면 

아무런 문제 없이 똑같은 화면이 뜬다. 

만약 Update에서 

void Game::Update()
{
	// Scale Rotation Translation

	Matrix matScale = Matrix::CreateScale(_localScale / 3);

 

이렇게 스케일을 1/3 해주면 

 

이렇게 작아진다. 

우리가 행렬식으로 만들어준 모든 공식들이 정상적으로 동작을 한다고 볼 수 있다. 

 

void Game::Update()
{
	// Scale Rotation Translation

	_localPosition.x += 0.001f;

 

이 코드를 추가해주면 지난 시간 때 한 것 처럼 이미지가 움직이는 것도 구현할 수 있다. 

결론적으로 유니티에서 했던 것처럼 Game.h에서 처럼 Scale, Rotation, Translation 이 세가지 정보를 각각 들고 있다가 그걸 이용해서 Update에서 처럼 행렬식으로 만들어 준 다음에 그 행렬식들을 곱해 만든 matWorld를  _transformData 즉 상수 버퍼를 통해서 전달을 해주게 되면, 그 전달 받은 애를 이용해서  VS에서 연산이 되고 있구나 라는 걸 알 게 된 거다. 

Scale, Position, Rotation은 물체마다 상이하기 때문에 물체마다 각각 다시 연산해줘서 상수 버퍼에 꽂아 줘야 한다. 그럼에도 이점이 있는게 물체 하나가 간단한 삼각형이 아니라 복잡한 3D오브젝트면 정점갯수가 몇천 몇만이 되니까 행렬을 이용해서 CPU의 부담을 줄여주는 역할이 크게 도움이 된다. 

다중에 World, View, Projection을 한번에 묶어서 Update에서 한번에 계산해서 넘겨주는 것도 방법이다.  지금은 VS에서 각각 곱해주고 있다.

 

3. 맺음말 

여기까지 왔으면 거의 절반까지 온 것이다. 

VS에서 WVP를 연산하고
나중에는 World Transform 뿐만이 아니라 카메라가 등장하면 카메라 좌표에 따라서 View 변환을 만들어 주게 되는데 
matView는 단순하다. 카메라의 월드를 구해준 다음에 역행렬을 구해주면 된다.  
matWorld.Invert();  
해주면 World의 역행렬이 되니까 World에서 local 영역으로 가는 행렬이 만들어지는데 View행렬이라는게  카메라의 로컬영역으로 끌고 오는 거라고 했었어. 

View, Projection 에 관한 행렬들을 구해주고 Update에서 꽂아 주면, 카메라에 따라 동작하는 것도 구현할 수 있게 된다. 

지금은 View, Projection을 Identity 자기 자신을 뱉는 애로 해서 효과가 없었지만, 지난 번 보다 나아진 건 이제 World, VIew, Projection를 통해서 이미지를 출력 해준거. 

지금은 월드 좌표랑 동일하게 카메라가 바라보고 있다고 간주할 수 있는 거고, 
나중에 카메라의 위치를 바꾸게 되면 테스트 할 수 있게 된다. 

FPS는 플레이어 머리에, 롤은 대각선 위에 붙이면 된다. 

 


3D게임, 2D게임 기본적 원리는 동일하다. 

유니티에서도 기본적으로 3D엔진인데 카메라 각도롤 바꿔서 직교 투영으로 보면 2D게임인거다. 

3D엔진 공부하면 2D도 따라오는 것이다.

이걸 개선 시켜서 다음 시간부터는 본격적으로 2D게임 만들 수 있는 구조를 만들어 볼 것이다. 

 


다음 시간은 골격을 잡아것이다. 

지금까지는 Game에 몰빵했는데 어떻게 분리해서 관리할 것인가. 

전체 골격 복습해야 분리하는게 이해 잘 될 것이다.

반응형

'DirectX' 카테고리의 다른 글

18. 프레임워크 제작_InputAssembler  (0) 2024.01.01
17. 프레임워크 제작_Graphics  (0) 2023.12.30
15. Screen 변환 행렬  (0) 2023.12.22
14. Projection 변환 행렬  (0) 2023.12.22
13. View 변환 행렬  (0) 2023.12.21

댓글