[GTL] W03 - Static Mesh, Viewports

brights2ella·2025년 4월 5일
0

정글게임테크랩1기

목록 보기
5/14

리팩토링

저번 주에서 선택한 엔진에 싱글톤이 13개나 있어서 리팩토링을 단행했다.

위와 같은 구조를 아래로 변경하였다.

구조 분석

그래도 쓸만한 구조가 많았다. 특히 DX11 리소스와 외부 에셋 관리하는 부분에서 구조가 잘 짜여 있었다.

대부분 static으로 정의된 Find(FString) 함수를 통해 Pool에 있는 리소스를 찾아와 사용하는 방식이다.

OBJ, MTL 파일

Wavefront라는 회사에서 만든 3D 메쉬, 머터리얼을 표현하는 파일이다.

참고

OBJ

v 0.123 0.234 0.345 1.0
v ...
...
vt 0.500 1 [0]
vt ...
...
vn 0.707 0.000 0.707
vn ...
...
vp 0.310000 3.210000 2.100000
vp ...
...
f 1 2 3
f 3/1 4/2 5/3
f 6/4/1 3/5/3 7/6/5
f 7//1 8//2 9//3
f ...
...
l 5 8 1 2 4 9

한 줄에 키워드 하나와 여러 숫자가 들어가며 정보를 표시한다.

  • v: 버텍스. x, y, z, [r, g, b] 를 받는다.
    이때 각 좌표는 오른손좌표계를 따른다. 필요하다면 -z를 시켜줘야 한다.
  • vt: 텍스쳐 좌표. u, [v, w] 를 받는다.
  • vn: 버텍스 노말. x, y, z를 받는다. 단위벡터가 아닐 수도 있다.
  • vp: 일반적으로 쓰지 않음.
  • f: 면. v/vt/vn 식으로 각각의 인덱스를 받는다.
  • l: 선. v의 각 인덱스를 받는다.

참고: wikipedia

MTL

OBJ 파일에 아래와 같이 작성되어 있으면 링크된 파일을 찾아 읽어들인다.

mtllib [external .mtl file name]

다중 뷰포트

Github Commit

SWindow, SSplitter, SWorldWindow를 두어 트리 구조를 구성하여 무한 분할 다중 뷰포트를 구현하였다.

SWindow

모든 SWindow는 parent를 가지며, Screen 좌표계의 Rect를 가져 마우스가 클릭했는지 안했는지를 확인할 수 있다.

class ACamera;
class SSplitter;
struct FViewportClient;
class SWindow {
public:
	FRect Rect;
	friend class JsonSaveHelper;
	virtual bool IsHover(FVector2D coord) const {
		return Rect.Contains(coord);
	}

	virtual void OnResizeUpdate();
	virtual void Render() {}
	virtual void OnMouseDown(FVector2D) {}
	virtual void OnMouseUp(FVector2D) {}
	virtual void OnMousePressed(FVector2D) {}
	virtual void OnMouseReleased(FVector2D) {}

	FVector2D GetNDCPosInWindow(FVector2D InCoord);

	std::shared_ptr<SSplitter> child = nullptr;
	std::shared_ptr<SWindow> parent = nullptr;
};

또한 외부에서 현재 선택한 SWindow가 어느 것인지를 판단하는 함수를 재귀적으로 호출한다.

std::shared_ptr<SWindow> UEditorManager::GetHoveringWindow(
	const FVector& InMouseScreenPos,
	const std::shared_ptr<SWindow> InSWindow
) {
	if ( !InSWindow->IsHover(FVector2D(InMouseScreenPos.X, InMouseScreenPos.Y)) )
		return nullptr;
	if ( InSWindow->child == nullptr ) {
		return InSWindow;
	} else {
		std::shared_ptr<SWindow> child = InSWindow->child;
		std::shared_ptr<SWindow> childLT = InSWindow->child->SideLT;
		std::shared_ptr<SWindow> childRB = InSWindow->child->SideRB;
		std::shared_ptr<SWindow> resChild = GetHoveringWindow(InMouseScreenPos, child);
		std::shared_ptr<SWindow> resLT = GetHoveringWindow(InMouseScreenPos, childLT);
		std::shared_ptr<SWindow> resRB = GetHoveringWindow(InMouseScreenPos, childRB);
		if ( resChild != nullptr )
			return resChild;
		else if ( resLT != nullptr )
			return resLT;
		else if ( resRB != nullptr )
			return resRB;
		else
			return nullptr;
	}
}

SWorldWindow

SWindow를 상속받고 FViewportClient를 통해 월드를 렌더한다. 또한 자식이 없는 leaf 노드이다.

class SWorldWindow: public SWindow {
	friend class JsonSaveHelper;
private:
	FViewportClient* viewportClient;
public:
	SWorldWindow() {};
	SWorldWindow(FViewportClient* viewportClient);
	~SWorldWindow();
	inline FViewportClient* GetViewportClient() { return viewportClient; }
	virtual void OnResizeUpdate() override;
	virtual void OnMousePressed(FVector2D InCoord);
};

이때 FViewportClient는 아래와 같이 Camera와 Viewport만을 가진다.

struct FViewportClient {
	ACamera* camera;
	FViewport viewport;
	FViewportClient(ACamera* camera, FViewport viewport) : camera(camera), viewport(viewport) {};
	void PrepareRender();
};

SSplitter

Github Commit

child이 없으며 대신 SWindow 2개를 SideLT, SideRB로 가진다. 또한 SplitPos를 가져 SideLTSideRB의 크기 비율을 업데이트해 준다.

class SSplitter : public SWindow {
protected:
	bool bIsDragging = false;
public:
	std::shared_ptr<SWindow> SideLT; // Left/Top 영역
	std::shared_ptr<SWindow> SideRB; // Right/Bottom 영역
	float SplitPos = 0.5f; // 분할 비율 (0.0~1.0)
	virtual void OnResize() = 0;
	virtual FRect GetSplitterRect() const = 0;

	bool IsHover(FVector2D coord) const override {
		FRect splitterRect = GetSplitterRect();
		return splitterRect.Contains(coord);
	}

	void OnMousePressed(FVector2D coord) override {
		if (IsHover(coord)) bIsDragging = true;
	}

	void OnMouseReleased(FVector2D coord) override {
		bIsDragging = false;
	}
};


class SSplitterH : public SSplitter { // 가로 분할
	friend class JsonSaveHelper;
private:
	float SplitterWidth;
public:
	SSplitterH() {
		SplitterWidth = 5.0f;
	};
	SSplitterH(std::shared_ptr<SWindow> Left, std::shared_ptr<SWindow> Right) {
		SideLT = Left;
		SideRB = Right;
		SplitterWidth = 5.0f;
	}
	void OnResize() override {
		const float splitX = Rect.Width() * SplitPos;
		SideLT->Rect = FRect(Rect.Left, Rect.Top, splitX, Rect.Height());
		SideRB->Rect = FRect(Rect.Left + splitX, Rect.Top,
			Rect.Width(), Rect.Height());
		SideLT->OnResizeUpdate();
		SideRB->OnResizeUpdate();
	}
	virtual void OnResizeUpdate() override;

	void OnMouseDown(FVector2D coord) override;
private:
	FRect GetSplitterRect() const {
		return Rect;
	}
};

FObjArchive

void FObjArchive::ObjToBinary(const FString& filePath, const TArray<FVertexTextureArray> vertices, 
	const TArray<uint32> indices, TArray<uint32> intervals, const TArray<FTextureCount>& textureCounts,
	const TArray<FSubMesh>& subMeshes)
{
	std::ofstream objBinary;
	objBinary.open(filePath.ToWideString(), std::ios::binary);
	if (!objBinary) {
		MsgBoxAssert("can't open file");
		return;
	}
	// 정점 개수 저장 (uint32)
	uint32 vertexCount = vertices.Num();
	objBinary.write(reinterpret_cast<const char*>(&vertexCount), sizeof(uint32));

	// 인덱스 개수 저장 (uint32)
	uint32 indexCount = indices.Num();
	objBinary.write(reinterpret_cast<const char*>(&indexCount), sizeof(uint32));

	// 간격 데이터 개수 저장 (uint32)
	uint32 intervalCount = intervals.Num();
	objBinary.write(reinterpret_cast<const char*>(&intervalCount), sizeof(uint32));
    
	...

reinterpret_cast<const char*>(&struct)를 이용해 파싱한 Obj 파일을 Binary로 저장하고 불러들일 수 있었다. 처음 로딩하는 시간이 조금 덜 걸리게 되었다.

TroubleShooting

MSVC에서 static_assert(false, ...);

환경마다 다르게 동작하는 듯하다. 조금 찾아보니 MSVC에서 static_assert(false, ...)가 나오면 맥락 상관없이 컴파일 에러를 때려버린다는 듯하다. 실제로는 if constexpr를 사용해서 구분하는데도 말이다.

static CharType* Strcpy(CharType* dest, const CharType* src)
{
    if constexpr (std::is_same_v<CharType, char>)
    {
        return std::strcpy(dest, src);
    }
    else if constexpr (std::is_same_v<CharType, wchar_t>)
    {
        return std::wcscpy(dest, src);
    }
    else
    {
        static_assert(false, "Unsupported character type!");
        return nullptr;
    }
}

false 대신 sizeof(Type) == 0 처럼 한 단계 늦게 평가되는 식을 쓰면 무사히 컴파일된다.

    else
    {
        static_assert(sizeof(CharType) == 0, "Unsupported character type!");
        return nullptr;
    }

0개의 댓글