으음. 참고로 저는 DirectXMath.h(Windows 8 or higher)를 사용합니다..'만'
옛날에 책 따라서 만든 Box 예제가 있길래 그 위에다가 대충 테스트 할려고 구현한거라 XNAMath를 사용하게 됐습니다 ㅇㅂㅇ;

XNAMath(혹은 DirectXMath.h를 include 한 후 using namespace DirectX를 타이핑 했다면)와 Bullet Physics를 동시에 사용하게 되면 C2084에러를 보게 되는데 이 때는 당황하지 마시고

#define BT_NO_SIMD_OPERATOR_OVERLOADS
#include <btBulletDynamicsCommon.h>

이렇게 처리해 주시면됩니다.

자 시작해볼까요?

먼저 엔진을 사용할 준비를 해야겠죠? Bullet Physics Hello World예제는 한번 보고 오셨을거라 생각합니다.
아직 안보신 분은 Hello World! <- 클릭.

btBroadphaseInterface* broadphase = new btDbvtBroadphase;
btDefaultCollisionConfiguration* collisionConfiguration = new btDefaultCollisionConfiguration;

btCollisionDispatcher* dispatcher = new btCollisionDispatcher(collisionConfiguration);
btSequentialImpulseConstraintSolver* solver = new btSequentialImpulseConstraintSolver;
btDiscreteDynamicsWorld* dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration);

// 제가 테스트 해볼 박스 위치가 (-1,-1,-1), (1,1,1)입니다. 그러니 아래처럼 해줘야죠.
btCollisionShape* boxCollisionShape = new btBoxShape(btVector3(1, 1, 1));
// 변환따위 하지 않습니다. 카메라는 돌아가지만 변환은 하지 않습니다.
btDefaultMotionState* motionStatenew btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, btVector3(0, 0, 0)));

btRigidBody::btRigidBodyConstructionInfo rigidBodyCI(0, bulletUtil.motionState, bulletUtil.boxCollisionShape, btVector3(0, 0, 0))
btRigidBody* rigidBody =  new btRigidBody(rigidBodyCI);
dynamicsWord->addRigidBody(rigidBody);

이제 여러분의 DirectX 프레임워크에 있는 마우스 이벤트 처리 함수에 다음과 같이 코딩해봅니다.


void PickingTest::OnMouseDown(WPARAM btnState, int x, int y)
{
	XMMATRIX proj = XMLoadFloat4x4(&m_Proj); // Projection 행렬입니다.
	XMMATRIX view = XMLoadFloat4x4(&m_View); // View 행렬입니다.

	XMVECTOR rayOrigin = XMLoadFloat4(&XMFLOAT4(0, 0, 0, 1.0f));
	// 혹시 Origin 점 위치가 왜 원점인지 이해가 안가시나요?
	//      /                 |                                             |
	//    /                   |                                             |
	//  /                     |                                             |
	//뷰----광선---NEAR--------->물체                  FAR
	//  \                    |                                             |
	//     \                 |                                             |
	//        \              |                                             |
	//
	// 즉, 광선의 시작점은 카메라 공간(View)에서 카메라 위치인 0,0,0 에서 시작됩니다.
	//
	XMVECTOR rayDirection = XMLoadFloat4(&XMFLOAT4(
		((float)(2.0f * x) / (float)mScreenViewport.Width - 1.0f) / m_Proj._11,
		((float)(-2.0f * y) / (float)mScreenViewport.Height + 1.0f) / m_Proj._22,
		1.0f,
		0.0f));
	// 광선의 시작지점이 원점이므로, 이 원점에서 어떤 "방향"으로 광선이 발사될지 정해줘야합니다.
	// 광선의 방향은 Point(x, y, z) - O(0, 0, 0) = Point(x, y, z)이므로 
	// 저희는 스크린 포인트의 마우스 위치를 뷰 행렬까지 옮겨주면 될 듯 합니다.
	// 원래 Loacl * World * View * Proj * ViewPort 순서대로 곱한 결과가 최종 스크린 좌표이므로(마우스는 이미 최종 스크린 좌표입니다.)
	// Local * World * View = ScreenPos * ViewPort^-1 * Proj^-1 이겠네요.

	XMVECTOR det2 = XMMatrixDeterminant(view); // 만약 DirectXMath 가 아니라 XNAMath 를 사용하신다면 반드시 필요합니다.
	XMMATRIX invView = XMMatrixInverse(&det2, view); // XNAMath XMMatrixInverse는 첫번째 인자가 nullptr 일 수 없습니다. DirectXMath는 nullptr 가능.

	rayOrigin = XMVector4Transform(rayOrigin, invView);
	rayDirection = XMVector4Transform(rayDirection, invView);

	rayDirection = XMVector4Normalize(rayDirection) * 1000.0f;
	// 방향을 구한다음에 길이를 재설정해줍니다. 으음. 길이를 설정해줘야 하더군요. 노말라이즈 안하고 그냥 값 넣어도 작동은 됩니다.

	XMFLOAT3 rayOriginF;
	XMFLOAT3 rayDirectionF;

	XMStoreFloat3(&rayOriginF, rayOrigin);
	XMStoreFloat3(&rayDirectionF, rayDirection);

	// 아래 코드는 이제 보시면 이해 할 것입니다.
	// 자세한 함수 사용 방법은 Bullet Physics Document를 참고해주세요.
	btCollisionWorld::ClosestRayResultCallback rayCallback(
		btVector3(rayOriginF.x, rayOriginF.y, rayOriginF.z),
		btVector3(rayDirectionF.x, rayDirectionF.y, rayDirectionF.z));
	bulletUtil.dynamicsWorld->rayTest(
		btVector3(rayOriginF.x, rayOriginF.y, rayOriginF.z),
		btVector3(rayDirectionF.x, rayDirectionF.y, rayDirectionF.z),
		rayCallback);

	if (rayCallback.hasHit())
	{
		SetWindowText(mhMainWnd, L"성공");
	}
	else
	{
		SetWindowText(mhMainWnd, L"실패");
	}
}

아래는 결과. 마우스를 캡쳐 할 수 있는 캡쳐 프로그램이 없어서 그냥 그림판으로 대충 동그라미 그렸습니다. 동그라미 가운데 부분이 클릭지점 입니다.




고딩 수학을 말아먹은 관계로 뭔가 틀린 부분이 있다면 알려주시면 감사하겠습니다.

우와. 포스팅량이 늘었군요. 자축 자축.
에... 2부에서 예고했던데로 커서를 그려보고 왔습니다.
사실 만드는건 한 시간도 안걸렸는데 놀다보니(...) 포스팅이 좀 늦어졌네요.
별것도 아닌데 3부를 괜히 날려먹는거 아닌가? 몇 부작으로 갈 생각이지? 라는 생각은 저 멀리 다른 차원으로 집어던져버리고 빨리 코드를 살펴보겠습니다.

일단 간단하게 한개만 인클루드 해보죠.

#include <usp10.h>

좋습니다. 자 이제 함수 하나를 만들어 보겠습니다.

// 귀찮으니 전역 변수로 때워봅시다.
SCRIPT_CONTROL ScriptControl;
SCRIPT_STATE ScriptState;
SCRIPT_STRING_ANALYSIS analysis = nullptr;

void InitScriptStringAnalysis(HDC hdc, std::wstring str)
{
	if ( analysis == nullptr )
	{
		ZeroMemory(&ScriptControl, sizeof(ScriptControl));
		ZeroMemory(&ScriptState, sizeof(ScriptState));
		ScriptApplyDigitSubstitution(nullptr, &ScriptControl, &ScriptState);
	}
	else
		ScriptStringFree(&analysis);

	ScriptStringAnalyse(hdc, str.c_str(), str.length() + 1,
						str.length() * 1.5 + 16,
						-1,
						SSA_BREAK | SSA_GLYPHS | SSA_FALLBACK | SSA_LINK,
						0,
						&ScriptControl,
						&ScriptState,
						nullptr,
						nullptr,
						nullptr,
						&analysis);
}


으음. 뭐. 그럭저럭 테스트 해볼만 한 함수가 나왔습니다.

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	int wmId, wmEvent;
	PAINTSTRUCT ps;
	HDC hdc;
	static HIMC imeID = nullptr;
	static std::wstring str;

	wchar_t debug[256] = { 0 };
	static int x = 0;

	RECT rect = { 0, 0, 0, 0 };

	switch ( message )
	{
		case WM_ACTIVATE:
			if ( imeID == nullptr )
			{
				imeID = ImmCreateContext();
				ImmAssociateContext(hWnd, imeID);
			}
			break;
		case WM_IME_STARTCOMPOSITION:
			break;
		case WM_IME_CHAR:
			str.push_back((wchar_t)wParam);
			InvalidateRect(hWnd, NULL, TRUE);
			break;
		case WM_CHAR:
			if ( (wchar_t)wParam == '\b' )
			{
				str.pop_back();
			}
			else
			{
				str.push_back((wchar_t)wParam);
			}
			InvalidateRect(hWnd, NULL, true);
			break;
		case WM_PAINT:
			hdc = BeginPaint(hWnd, &ps);
			// DT_CALCRECT옵션은 출력할 글자의 범위를 구하는 옵션입니다.
			// 따라서, 아래 함수를 호출하게 되면 rect변수에 출력할 글자의 범위를 구해서 던져줍니다.
			// 여기에서 우리는 top와 bottom만 필요합니다. 전체 width 는 필요 없으니까요.
			DrawText(hdc, str.empty() ? L"A":str.c_str(), -1, &rect, DT_CALCRECT | DT_CENTER);
			DrawText(hdc, str.c_str(), str.length(), &rect, DT_CENTER);

			// 이 함수를 호출하게 되면 ScriptStringCPtoX를 사용하기 위한
			// SCRIPT_STRING_ANALYSIS 를 얻게됩니다. 자세한 내용은 MSDN을 참고해주세요.
			InitScriptStringAnalysis(hdc, str.c_str());
			ScriptStringCPtoX(analysis, str.length(), false, &nTrail);

			// 커서를 그립니다. DrawText에서 얻어온 글자의 출력 범위를 top와 bottom 으로 하고,
			// ScriptStringCPtoX에서 얻어온 마지막 글자 인덱스의 위치를 left로 하고 여기서 1을 더해 width가 1인
			// 사각형을 그립니다.
			Rectangle(hdc, x, rect.top, x + 1, rect.bottom);
			wsprintf(debug, L"커서 위치 : %d %d %d %d", x, rect.top, x + 1, rect.bottom);
			SetWindowText(hWnd, debug);

			EndPaint(hWnd, &ps);
			break;
		case WM_DESTROY:
			ImmDestroyContext(imeID);
			PostQuitMessage(0);
			break;
		// 코드가 길어져서 기본 프로시저 함수에서 수정되지 않은 이벤트는 쓰지 않았습니다.
	}
	return 0;
}

자 그럼 사진을 하나 투척해보죠.


짞짞짞.

으음. 하지만 커서가 안움직이니 뭔가 허전하군요. 명색의 커서인데 움직여야 하지 않겠어요? 움직였으면 움직인 자리에 무언가 입력하거나 삭제도 되야죠.
으음.. 한번 해보죠. 재밋을거같으니.

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	// 기존 변수 생략
	static int nowPosition = 0;

	switch ( message )
	{
		case WM_ACTIVATE:
			if ( imeID == nullptr )
			{
				imeID = ImmCreateContext();
				ImmAssociateContext(hWnd, imeID);
			}
			break;
		case WM_IME_STARTCOMPOSITION:
			break;
		case WM_IME_CHAR:
		case WM_CHAR:
			if ( (wchar_t)wParam == '\b' )
			{
				if ( nowPosition == str.length() )
				{
					str.erase(nowPosition - 1);
				}
				else
				{
					// 중간 문자를 삭제하기 위한 코드입니다.
					const wchar_t* strBack = str.c_str();
					strBack = &strBack[nowPosition];

					str.erase(nowPosition - 1);
					str.append(strBack);
				}
				nowPosition = nowPosition - 1 < 0 ? 0:nowPosition - 1;
			}
			else
			{
				// 중간에 글자를 삽입하기 위해 insert로 바꾸었습니다.
				str.insert(nowPosition, (wchar_t*)&wParam);
				nowPosition = nowPosition + 1 > str.length() ? str.length():nowPosition + 1;
			}
			InvalidateRect(hWnd, NULL, true);
			break;
		case WM_KEYDOWN:
			if ( (int)wParam == VK_LEFT )
			{
				nowPosition = nowPosition - 1 < 0 ? 0:nowPosition - 1;
				InvalidateRect(hWnd, NULL, true);
			}
			else if ( (int)wParam == VK_RIGHT )
			{
				nowPosition = nowPosition + 1 > str.length() ? str.length():nowPosition + 1;
				InvalidateRect(hWnd, NULL, true);
			}
			break;
		case WM_PAINT:
			hdc = BeginPaint(hWnd, &ps);
			DrawText(hdc, str.empty() ? L"A":str.c_str(), -1, &rect, DT_CALCRECT | DT_CENTER);
			DrawText(hdc, str.c_str(), str.length(), &rect, DT_CENTER);


			InitScriptStringAnalysis(hdc, str.c_str());
			ScriptStringCPtoX(analysis, nowPosition, false, &x);

			Rectangle(hdc, x, rect.top, x + 1, rect.bottom);
			wsprintf(debug, L"커서 위치 : %d %d %d %d", x, rect.top, x + 1, rect.bottom);
			SetWindowText(hWnd, debug);
			EndPaint(hWnd, &ps);
			break;
		case WM_DESTROY:
			ImmDestroyContext(imeID);
			PostQuitMessage(0);
			break;
		default:
			return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

이 기세라면! 마우스로 클릭해서 커서를 옮기는 것도 가능할거같군요. 확인해 보는것도 좋겠지만 이번 편은 ScriptString를 테스트해보기 위한 편이었으니 그냥 안할려고 합니다 ㅇㅅ.
작동되는거 확인했으니 됐죠 뭐 ㅋㅅㅋ.

이번에는 GDI로 직접 EditBox를 만들어 보았습니다. 코드는 좀 지저분하지만 어떠신지요?
중복된 코드를 지우기 위해서 WM_IME_CHAR에 keybd_event로 VK_RIGHT를 누르게 해 보았는데 왜인지 모르겠지만 한글 타이핑에 문제가 생기더군요.
원인을 모르겠네요. 아시는분 알려주시면 감사하겟습니다(_ _ )

그런고로! 다음에는 DirectX11에 직접 적용시켜 볼 차례군요.
자 그럼 다음시간에! (언제가 될지 모르겠지만 노력해 보겠습니다..)

커서 위치에 주목!

DirectX9와 10은  DXUT에 기본적으로 EditBox가 들어있습니다. 물론 아름답지 못하지만 나름 커스터마이징이 가능하더군요.
그 외에 약간의 버그같지 않은 버그라고 하면 한/영키로 한영 전환을 해도 여전히 인디게이터에 '가'라고 표시된다는 거랄까요 ㅋㅅㅋ.
이건 전에 고쳤었는데 굳이 소스를 보관할 가치를 못느껴서 포맷할때 같이 버렸던걸로 기억합니다. 조금 아쉽네요.

그래서 이번 주제는... DirectX11에도 EditBox를 만들어보자 입니다.
미리 말씀드리지만 이 글은 완결이 안날 수도 있습니다. 에 그러니까 갑자기 글이 뚝 끊기고 엉뚱한 XBMC 버그가 고쳐지는 글이 올라올지도 모르고 시작에서 멈출 수 있다는거죠. 또한 만약 막 DX에 입문하신 분이 희망찬 마음을 가지고 이 글을 읽기 시작하실까봐요... 사전에 일러두자면 저도 입문자라(...) 많은 도움은 드리지 못할거란 사실은 인지해 주셨으면합니다.

아니, DirectX에 왜 EditBox가 필요하죠? DX로 뭘 만들 작정이십니까?
EditBox를 DX에서 만든다고 하면 저런 질문이 먼저 들어오더군요. 뭐 보통 선배분들께 조언을 구한지라 저렇게 존댓말이 아니었지만요 ㅋㅅㅋ.
이유는 간단합니다. 채팅해야죠. 제가 원하는건 채팅이 되는 온라인게임이라 이말입니다.
그리고 한 가지 이유가 더 있는데 나중에 트위터랑 연동시켜서 귀여운 캐릭터가(...) 멘션을 알려주거나 작성한 트윗을 보내주는 등의 기능을 만들겁니다.
그 언제 만들어 질지 모르는 프로그램을 위해서(귀여운 캐릭터 모델이 있으면 완성될지도.) 한 번 노력해보죠.

왜 OpenGL이 아닌가요?
이유는 간단합니다. 제가 DirectX를 졸라 좋아하거든요. 뭐 미련맞은 짓이죠. 그래서 위에 써놓았지만, 이 글은 중간에 멈출 수도 있습니다.
OpenGL을 쓰면 EditBox를 구현할 필요가 없냐 라고 묻는다면 역시 그건 또 아니지만 뭔가 다른 방법이 있지 않을까 싶습니다.

자 그럼 혼자 질문하고 혼자 답하는 재미없는 글은 그만쓰고 어서 코드를 살펴보죠.

제가 이 글을 쓰기 위해 준비한 준비물은 다음과 같습니다.

DirectX SDK Sample - CustomUI
Direct2D - DirectWrite.
DirectX11.

자 그럼, 궁극적으로 EditBox는 뭐를 해야 할까요?
물론 글씨를 편집해야죠. 그러기 위해선 일단 글자를 출력해야합니다.

글씨를 출력한다. DirectX에서 글씨의 출력은 어떻게 해야 하는 걸까요?
렌더링을 해야합니다. 텍스쳐를 만들고 그 위에 글씨를 써서 렌더링 해야 합니다.

지금 '여보쇼, 이게 무슨 강아지 엿 씹어 먹는 소리야? 한글이 몇 글자 인줄 알아? unicodmap에서 hangul을 검색해서 나오는 카테고리의 모든 글자 수를 더하면 자그마치 11,524자나 된다고. 이걸언제 다 텍스쳐로 만들어서 로드하고 있어?' 라고 생각하신분 계시면 블로그 닫기 전에 잠시만 기다려주세요.
막 구글검색 하다가 미처 발견하지 못하신것 같은데 '순수하게' DirectX11만 사용한다면 당연히 그런 삽질을 해야겠지만 MS가 그렇게 멍청하게 DX를 만들어 놓진 않았습니다.
DXGI(DirectX Graphics infrastructure)를 사용하면 동기화된 공유 Surface를 만들 수 있다 이겁니다.

으음. 제가 개 엿씹어 먹는 수준의 설명을 하고있는것 같으니 이쯤에서 그냥 링크를 하나 뿌리죠. 이 링크 하나면 모든 설명이 끝납니다.
(절대로 코드를 올리기가 귀찮은게 아닐거라고 제 자신을 속여ㅂ...)

http://www.braynzarsoft.net/index.php?p=D3D11FONT

저 링크에 이 글의 모든게 들어있습니다. 사실 저는 윗 글을 보고 만들었거든요.
뭐 윗 글을 살짝 변형해서 설치 된 폰트가 아닌 어딘가의 경로에 별도로 있는 폰트를 로드 가능하게 수정했지만 그리 어려운 작업은 아닙니다.

영어가 보기 귀찮으시다면 어쩔수 없지만 이대로 글을 맺자니 사진도 없고 뭔가 허전하니 일단 뭐라도 써보죠.

위 링크에 들어가서 영어 말고 코드를 대충 읽고 오시면 대략적으로 다음과 같은 방법으로 진행된다는걸 아실겁니다.

DX11 디바이스를 생성합니다. 뭐 기타 등등의 렌더링에 필요한 초기화는 여러분이 만드신(혹은 쓰시는) 프레임워크나 라이브러리에 아주 잘 만들어놓여져있겠죠.
DX10 디바이스를 생성합니다. 뭐 렌더링 할건 아니구 공유 텍스쳐를 만들때 잠깐 사용되니 생성만 하시면 됩니다.
DX11 텍스쳐를 만듭니다.
위에서 만든 텍스쳐를 이용해 DX10과의 공유 텍스쳐를 생성합니다.
D2DFactroy를 하나 만들어서 DXGI의 Surface를 렌더 타겟으로 지정한 ID2DRenterTarget 객체를 하나 만듭니다.
ID2DRenterTarget을 이용해서 공유 텍스쳐에 글씨를 그리고
마지막으로 DirectX11 디바이스 컨텍스트에 텍스쳐를 3번째 단계에서 만든 텍스쳐로 설정하고 사각형 하나를 렌더링합니다.

끝.

왜 DX10 디바이스를 생성할까요? 위 링크에서 보면 마이크로소프트에서 충분한 종류의 DirectX11과 직접적으로 작동하는 Direct2D를 만들지 않았다고합니다.ㅜㅜ
뭐 그러니 DX10 디바이스를 생성해서 잠시 쓰는거죠 ㅇㅅㅇ.

프레임을 생각하면(이게 의외로 프레임 저하 현상이 심각합니다.) 초기 한번만 텍스쳐를 만들고 그 이후로는 만들어진 텍스쳐를 사용하는게 유리할 것 같습니다.
그러니까 어떤 문자를 처음 띄울 때는 텍스쳐를 생성하고 그 이후에는 만들어진 텍스쳐를 계속해서 사용하는거죠.

자 그럼 2부가 만들어지길 바라며 글을 마치죠.

사용된 폰트는 구글에서 배포한 NotoSansCJK-Regular.OTF입니다. 어떻게 설치되지 않은 폰트를 로드하는지는 다음에 시간나면 써보겠습니다. 하지만 Direct2D를 이용하면 어렵지 않은 일이니 Visual Studio의 인텔리센스 기능을 이용해서 잠시 함수를 뒤적거리시다가 삘 꽂히는 함수 하나를 발견 하신 다음에  MSDN을 뒤져보면 금방 찾으실 거라 믿습니다.

+ Recent posts