2024년 12월 15일 일요일

DirectX Tutorial - RectTransform 구현

 

유니티의 요거처럼

하지만 UI 관련 객체를 쉽게 다루기 위해 2D용으로 구현하였음



버튼같은 요소들은 각각 자기만의 위치 정보를 갖고 있으며

이 정보를 가지고 렌더링에 활용하면 되는데

컴퓨터 마다 화면 크기가 다르므로 중심기준 좌표로만 그리기 쉽지 않음




그래서 모든 요소들의 위치, 회전 등의 좌표를 계산해주는 클래스를 구현하여

이를 상속시켜 요소마다 좌표 계산등의 함수를 따로 안만들게 했음




#ifndef _RECT_TRANSFORM_
#define _RECT_TRANSFORM_

#include <DirectXMath.h>
#include <d2d1.h>

#include "Global.h"

enum ALIGNMENT//정렬 플래그
{
	ALIGNMENT_CENTER,//정중앙
	ALIGNMENT_LEFT,//좌측 중앙
	ALIGNMENT_RIGHT,//우측 중앙
	ALIGNMENT_TOP,//상단 중앙
	ALIGNMENT_BOTTOM,//하단 중앙
	ALIGNMENT_LEFT_TOP,//좌상단
	ALIGNMENT_LEFT_BOTTOM,//좌하단
	ALIGNMENT_RIGHT_TOP,//우상단
	ALIGNMENT_RIGHT_BOTTOM,//우하단
};

class RectTransform//요소의 위치 수정 및 정렬 기능을 제공하는 추상 클래스
{
protected:
	RectTransform()
	{
		//기본 설정 데이터
		m_localPosition = DirectX::XMFLOAT3(0.0f, 0.0f, 0.0f);
		m_worldPosition = DirectX::XMFLOAT3(0.0f, 0.0f, 0.0f);
		m_rotation = DirectX::XMFLOAT3(0.0f, 0.0f, 0.0f);
		m_scale = DirectX::XMFLOAT3(1.0f, 1.0f, 1.0f);
		m_align = ALIGNMENT_CENTER;
		m_worldMatrix = DirectX::XMMatrixIdentity();
		m_screenArea = D2D1::RectF(0.0f, 0.0f, 100.0f, 100.0f);
	}

	~RectTransform()
	{
	}

public:
	//상대 위치 설정
	void SetLocalPosition(const float& x, const float& y, const float& z)
	{
		m_localPosition = DirectX::XMFLOAT3(x, y, z);
	}

	//회전 설정
	void SetRotation(const float& x, const float& y, const float& z)
	{
		m_rotation = DirectX::XMFLOAT3(x, y, z);
	}

	//크기 설정
	void SetScale(const float& x, const float& y, const float& z)
	{
		m_scale = DirectX::XMFLOAT3(x, y, z);
	}

	//정렬 설정
	void SetAlign(const unsigned int& flag)
	{
		m_align = flag;
	}

	//객체의 위치, 회전 등을 반영한 월드 좌표계, 스크린 좌표계 데이터 최신화
	void UpdateTransform()
	{
		UpdateWorldMatrix();
		UpdateScreenArea();
	}

	//월드 행렬 가져오기
	DirectX::XMMATRIX GetWorldMatrix()
	{
		return m_worldMatrix;
	}

	//상대 좌표 얻기
	DirectX::XMFLOAT3 GetLocalPosition()
	{
		return m_localPosition;
	}

	//절대 좌표 얻기
	DirectX::XMFLOAT3 GetWorldPosition()
	{
		return m_worldPosition;
	}

	//회전 얻기
	DirectX::XMFLOAT3 GetRotation()
	{
		return m_rotation;
	}

	//크기 얻기
	DirectX::XMFLOAT3 GetScale()
	{
		return m_scale;
	}

	//정렬 얻기
	unsigned int GetAlign()
	{
		return m_align;
	}

	//스크린 좌표 얻기
	D2D1_RECT_F GetScreenArea()
	{
		return m_screenArea;
	}

private:
	DirectX::XMMATRIX Alignment(const unsigned int& screenWidth, const unsigned int& screenHeight, const unsigned int& flag)
	{
		DirectX::XMMATRIX mat;
		float x, y;

		switch (flag)
		{
		case ALIGNMENT_CENTER://정중앙
			//상대좌표가 곧 절대좌표
			m_worldPosition = DirectX::XMFLOAT3(m_localPosition.x / m_scale.x, m_localPosition.y / m_scale.y, m_localPosition.z / m_scale.z);
			mat = DirectX::XMMatrixTranslation(m_worldPosition.x, m_worldPosition.y, m_worldPosition.z);
			break;
		case ALIGNMENT_LEFT://좌측 중앙
			x = CalculateElementPosition(screenWidth, m_scale.x);
			m_worldPosition = DirectX::XMFLOAT3(-x + (m_localPosition.x / m_scale.x), (m_localPosition.y / m_scale.y), (m_localPosition.z / m_scale.z));
			mat = DirectX::XMMatrixTranslation(m_worldPosition.x, m_worldPosition.y, m_worldPosition.z);
			break;
		case ALIGNMENT_RIGHT://우측 중앙
			x = CalculateElementPosition(screenWidth, m_scale.x);
			m_worldPosition = DirectX::XMFLOAT3(x + (m_localPosition.x / m_scale.x), (m_localPosition.y / m_scale.y), (m_localPosition.z / m_scale.z));
			mat = DirectX::XMMatrixTranslation(m_worldPosition.x, m_worldPosition.y, m_worldPosition.z);
			break;
		case ALIGNMENT_TOP://상단 중앙
			y = CalculateElementPosition(screenHeight, m_scale.y);
			m_worldPosition = DirectX::XMFLOAT3((m_localPosition.x / m_scale.x), y + (m_localPosition.y / m_scale.y), (m_localPosition.z / m_scale.z));
			mat = DirectX::XMMatrixTranslation(m_worldPosition.x, m_worldPosition.y, m_worldPosition.z);
			break;
		case ALIGNMENT_BOTTOM://하단 중앙
			y = CalculateElementPosition(screenHeight, m_scale.y);
			m_worldPosition = DirectX::XMFLOAT3((m_localPosition.x / m_scale.x), -y + (m_localPosition.y / m_scale.y), (m_localPosition.z / m_scale.z));
			mat = DirectX::XMMatrixTranslation(m_worldPosition.x, m_worldPosition.y, m_worldPosition.z);
			break;
		case ALIGNMENT_LEFT_TOP://좌상단
			x = CalculateElementPosition(screenWidth, m_scale.x);
			y = CalculateElementPosition(screenHeight, m_scale.y);
			m_worldPosition = DirectX::XMFLOAT3(-x + (m_localPosition.x / m_scale.x), y + (m_localPosition.y / m_scale.y), (m_localPosition.z / m_scale.z));
			mat = DirectX::XMMatrixTranslation(m_worldPosition.x, m_worldPosition.y, m_worldPosition.z);
			break;
		case ALIGNMENT_LEFT_BOTTOM://좌하단
			x = CalculateElementPosition(screenWidth, m_scale.x);
			y = CalculateElementPosition(screenHeight, m_scale.y);
			m_worldPosition = DirectX::XMFLOAT3(-x + (m_localPosition.x / m_scale.x), -y + (m_localPosition.y / m_scale.y), (m_localPosition.z / m_scale.z));
			mat = DirectX::XMMatrixTranslation(m_worldPosition.x, m_worldPosition.y, m_worldPosition.z);
			break;
		case ALIGNMENT_RIGHT_TOP://우상단
			x = CalculateElementPosition(screenWidth, m_scale.x);
			y = CalculateElementPosition(screenHeight, m_scale.y);
			m_worldPosition = DirectX::XMFLOAT3(x + (m_localPosition.x / m_scale.x), y + (m_localPosition.y / m_scale.y), (m_localPosition.z / m_scale.z));
			mat = DirectX::XMMatrixTranslation(m_worldPosition.x, m_worldPosition.y, m_worldPosition.z);
			break;
		case ALIGNMENT_RIGHT_BOTTOM://우하단
			x = CalculateElementPosition(screenWidth, m_scale.x);
			y = CalculateElementPosition(screenHeight, m_scale.y);
			m_worldPosition = DirectX::XMFLOAT3(x + (m_localPosition.x / m_scale.x), -y + (m_localPosition.y / m_scale.y), (m_localPosition.z / m_scale.z));
			mat = DirectX::XMMatrixTranslation(m_worldPosition.x, m_worldPosition.y, m_worldPosition.z);
			break;
		default://ALIGNMENT_CENTER
			//상대좌표가 곧 절대좌표
			m_worldPosition = DirectX::XMFLOAT3(m_localPosition.x / m_scale.x, m_localPosition.y / m_scale.y, m_localPosition.z / m_scale.z);
			mat = DirectX::XMMatrixTranslation(m_worldPosition.x, m_worldPosition.y, m_worldPosition.z);
		}

		return mat;
	}

	float CalculateElementPosition(const unsigned int& resolution, const float& scale)
	{
		if (scale < 1.0f)
		{
			return static_cast<float>(resolution) / 2.0f - 0.5f;
		}

		return static_cast<float>(resolution) / (scale * 2.0f) - 0.5f;
	}

	//월드 행렬 업데이트
	void UpdateWorldMatrix()
	{
		DirectX::XMMATRIX pos, rot, scale;

		m_worldMatrix = DirectX::XMMatrixIdentity();

		pos = Alignment(SCREEN_WIDTH, SCREEN_HEIGHT, m_align);
		rot = DirectX::XMMatrixRotationRollPitchYaw(m_rotation.x, m_rotation.y, m_rotation.z);
		scale = DirectX::XMMatrixScaling(m_scale.x, m_scale.y, m_scale.z);

		m_worldMatrix = DirectX::XMMatrixMultiply(m_worldMatrix, pos);
		m_worldMatrix = DirectX::XMMatrixMultiply(m_worldMatrix, scale);
		m_worldMatrix = DirectX::XMMatrixMultiply(m_worldMatrix, rot);

		return;
	}

	//스크린 좌표 업데이트
	void UpdateScreenArea()
	{
		m_screenArea = D2D1::RectF(
			m_worldPosition.x * m_scale.x + (SCREEN_WIDTH - m_scale.x) / 2,
			m_worldPosition.y * -m_scale.y + (SCREEN_HEIGHT - m_scale.y) / 2,
			m_worldPosition.x * m_scale.x + (SCREEN_WIDTH - m_scale.x) / 2 + m_scale.x,
			SCREEN_HEIGHT//어차피 범위 지정해줘도 글자가 길면 뚫고 나가므로 최대치로 고정
		);

		return;
	}

private:
	DirectX::XMMATRIX m_worldMatrix;
	DirectX::XMFLOAT3 m_localPosition;
	DirectX::XMFLOAT3 m_worldPosition;
	DirectX::XMFLOAT3 m_rotation;
	DirectX::XMFLOAT3 m_scale;
	D2D1_RECT_F m_screenArea;
	unsigned int m_align;
};

#endif

2D라도 Z좌표가 0일 뿐, 3D처럼 다뤄야 함

그리고 렌더링 순서에 따라서 요소가 가려지고 이상한게 맨앞에 있다던가

그런 문제도 생각해둬야 해서 Z축으로 그걸 해결할 생각 



아무튼 정렬하는걸 구현하기 위해

요소의 사이즈와 정점 위치를 적당히 계산해서

화면 해상도 기준 절대 좌표로 만드는 부분을 먼저 만듦


일단 연관관계를 추측하려고 몇개 버튼 추가해놓고 위치 이리저리 옮겼다

크기 줄였다 늘렸다 하면서 실험한 결과가 이 계산


정점은 -0.5~+0.5 으로 고정해두고

멤버 변수에 저장된 크기랑 좌표로 윈도우 벽에 딱 붙여서 위치하게

x 또는 y 좌표를 리턴하는 함수임

예외로 사이즈가 1미만은 나누기에 관여 못하게 막음


머 이유는 어차피 크기 1일때 1픽셀 만큼 엄청 작게 보이고

0이거나 음수일 경우를 방지하기 위해



그리고 텍스트의 경우 좌표가 화면 중심이 아닌

좌상단이 기준점임 이걸 스크린 좌표라 하던데

이를 추가로 계산하는 부분도 구현

WinApi의 Rect는 long이고 요건 float



enum으로 어떤 정렬인지 정의하고 이거에 맞춰서 위치 계산

참고로 localPosition는 정렬된 위치로부터 이동할 상대 좌표 


이걸 상속하고 요소를 새로 만들면 됨




결과

GitHub - h117562/Tutorial_UI2: Button, Slider, TextField...

소스코드

2024년 12월 14일 토요일

DirectWrite - TextBox 구현

Dwrite로 텍스트 박스 구현을 해보았음


텍스트 관련 입력은 윈도우 프로시저 WNDPROC 함수에서

WM_CHAR 메시지를 처리하여 매개변수로 전달하고

마우스 클릭 같은건 DirectInput으로 처리하였음



LRESULT CALLBACK SystemClass::MessageHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{

	if (msg == WM_CHAR)
	{
		if (wparam == VK_BACK)
		{
			//백스페이스의 경우 '\b'가 저장되므로 예외로 문자를 지우는 함수 호출 (\n는 괜찮음)
			InputClass::GetInstance().RemoveLastChar();
		}
		else
		{
			//글자가 입력될 때 마다 inputClass의 멤버 변수에 저장
			InputClass::GetInstance().AddText(static_cast<wchar_t>(wparam));
		}
	}
	
	return DefWindowProc(hwnd, msg, wparam, lparam);
}


LRESULT CALLBACK WndProc(HWND hwnd, UINT umessage, WPARAM wparam, LPARAM lparam)
{
	switch (umessage)
	{
	case WM_DESTROY:
	{
		PostQuitMessage(0);
		return 0;
	}
	case WM_CLOSE:
	{
		PostQuitMessage(0);
		return 0;
	}
	default:
	{
		return ApplicationHandle->MessageHandler(hwnd, umessage, wparam, lparam);
	}
	}
}

이렇게 전역 클래스의 멤버 변수에 문자를 

계속 밀어 넣는 방식으로 처리

백스페이스는 문자를 뒤에서 부터 지우는 함수를 호출



이렇게 저장된 문자열은 TextBox 클래스에서 사용



TextBox의 현재 상태는 Enum으로 평소 상태, 포커싱 상태 두가지로

텍스트 박스를 클릭하면 포커싱 상태로 전환하고 

그외 다른 부분을 누르면 원상태로 복귀하게 함



텍스트 박스의 문자열은 당연히 Dwrite로 렌더링 하는데

텍스트 정렬, 색상, 굵기 등을 담당하는 브러쉬와 포맷을

클래스의 멤버 변수로 저장해놨다가 각 텍스트 박스마다

다른 글꼴을 갖게 설계




TextClass 에서 새로운 브러쉬와 포맷을 설정 가능한 함수를 구현



IDWriteTextFormat, ID2D1SolidColorBrush 는 반드시 더블 포인터로 전달




크기, 위치 적당히 조정해주고 브러쉬 포맷까지 초기화

참고로 글꼴은 밑의 사진의 뻘건 글씨로 돼있는걸로 적으면 됨




결과



소스 코드

GitHub - h117562/Tutorial_UI2: Button, Slider, TextField...


c++ thread.h

 c++에서 쓰레드 돌릴려면 thread.h 헤더를 쓰면 되는데 이 친구는 쓰레드가 아직 실행 중인지, 아니면 강제 종료하거나 하는 함수가 없어서 조금 아쉬운 애다. std::thread 는 로컬 변수로 선언하든 new 동적 할당을 하든 start 함...