DirectX

78_Viewport

devRiripong 2024. 3. 27.
반응형

로컬→월드→ViewSpace→Projection→NDC(normalized device coordinates)→Viewport

 

화면의 중앙을 클릭하면 카메라 위치에서 화면 중앙까지 가는 레이저를 쏘고 그 레이저가 어떤 애를 피격하면 어떤 물체를 클릭했다고 인지하는 것이다.

 

클릭한 것은 2D화면을 클릭했지만 3D 물체를 선택하게 된 것은 레이저로 쏴 가지고 맞췄다는 얘기가 된다.

 

레일저를 쏘는게 꼭 2D에서 3D를 쏘는 데만 쓰이는게 아니라 움직일 때 충돌 판정하는데 이용하거나 앞 방향으로 레이저르 쏴서 앞에 뭐가 있으면 멈추게 하는 식으로 응용할 수 있다. 

 

레이저를 쏘려면 2D화면을 3D화면으로 바꿀 수 있어야 한다.

 

2D에서 3D로 갈 때 깊이가 중요하다. 깊이라는 추가 정보를 줄 필요가 있다.

2D좌표를 눌렀는데 어떻게 3D좌표를 구했느냐

캐릭터를 선택해서 진행하는 모든 게임에서는 2D에서 3D변환이 필요하다.

 

 

만들던 코드에서 클릭하면 레이저를 쏴서 선택되는 걸 어떻게 할지 고민이다.

 

Camera에도 넣을 수 있지만 Viewport에 기능을 넣어본다.

지금까지 Viewport( 3D 씬을 2D 디스플레이에 투영하는 데 실제로 보여지는 영역 )라는 개념을 사용 안하고 있었다.

Graphics.h를 보면 _viewport라는 게 있다.

	// Misc
	D3D11_VIEWPORT _viewport = { 0 };

여기다가 추가로 프로젝션을 한다라고 하면,

Projection을 하기 위해서 아까처럼 3D에서 2D변환을 하기 위해서 가장 중요한 게 이 Viewport의 개념이다.

최종적인 화면크기나 그런거랑 연관이 있기 때문에 이 Viewport를 별도의 class로 빼가지고 관리를 해보도록 한다.

 

 

1. Viewport 클래스

Engine/01. Graphics필터에 Viewport 클래스를 생성한다.

얘가 들고 있어야 할 정보는 사실상 D3D11_VIEWPORT만 있어도 충분하다.

private:
	D3D11_VIEWPORT _vp; 

D3D11_VIEWPORT 안에 필요한 정보가 있다.

typedef struct D3D11_VIEWPORT
    {
    FLOAT TopLeftX;
    FLOAT TopLeftY;
    FLOAT Width;
    FLOAT Height;
    FLOAT MinDepth;
    FLOAT MaxDepth;
    } 	D3D11_VIEWPORT;

 

이 행렬이랑 D3D11_VIEWPORT 의 내부적인 정보들이랑 일치한다고 보면 된다.

 

이걸 이용해서 projection 공식을 만들어 볼 것이다.

라이브러리를 찾아보면 있을 것인데 그걸 이용해서 해도 되고, 직접 구현해 볼 수도 있다.

Vec3 Viewport::Project(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P)
{
	Matrix wvp = W * V * P; // 만약 World에 있던 좌표면 W는 Identity를 넣어주면 된다. 

	// 어떤 라이브러리에선 TransformCoord(좌표 포함 이동), TransformNormal(방향만 이동)로 되어 있는 거 x,y,z,w에서 w가 0이냐에 따라 달라지는 거 
	Vec3 p = Vec3::Transform(pos, wvp); 
	// 프로젝션이 된 다음의 좌표
	p.x = (p.x + 1.0f) * (_vp.Width / 2) + _vp.TopLeftX; 
	p.y = (-p.y + 1.0f) * (_vp.Height / 2) + _vp.TopLeftY; 
	p.z = p.z * (_vp.MaxDepth - _vp.MinDepth) + _vp.MinDepth; 

	return p; 
}

공식은 중요한게 아니다. MathLibrary에 Projection이라는 함수가 있을텐데 그걸 써도 된다.

이제 이것의 반대 개념, 2D좌표에서 시작해서 깊이값을 이용해 3D로 넘어오는 작업을 해주면 된다.

Vec3 Viewport::Unproject(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P)
{
	Vec3 p = pos;

	p.x = 2.f * (p.x - _vp.TopLeftX) / _vp.Width - 1.f;
	p.y = -2.f * (p.y - _vp.TopLeftY) / _vp.Height + 1.f;
	p.z = ((p.z - _vp.MinDepth) / (_vp.MaxDepth - _vp.MinDepth));

	Matrix wvp = W * V * P;
	Matrix wvpInv = wvp.Invert();

	p = Vec3::Transform(p, wvpInv);
	return p;
}

 

#pragma once
class Viewport
{
public: 
	Viewport(); 
	Viewport(float width, float height, float x = 0, float y = 0, float minDepth=0, float maxDepth =1); 
	~Viewport(); 

	void RSSetviewport(); 
	void Set(float width, float height, float x = 0, float y = 0, float minDepth=0, float maxDepth = 1);

	float GetWidth() { return _vp.Height; }
	float GetHeight() { return _vp.Width; }

	Vec3 Project(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P); 
	Vec3 Unproject(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P); 

private:
	D3D11_VIEWPORT _vp; 
};
#include "pch.h"
#include "Viewport.h"

Viewport::Viewport()
{
	Set(800, 600); 
}

Viewport::Viewport(float width, float height, float x, float y, float minDepth, float maxDepth)
{
	Set(width, height, x, y, minDepth, maxDepth); 
}

Viewport::~Viewport()
{

}

void Viewport::RSSetviewport()
{
	DC->RSSetViewports(1, &_vp); 
}

void Viewport::Set(float width, float height, float x, float y, float minDepth, float maxDepth)
{
	_vp.TopLeftX = x; 
	_vp.TopLeftY = y; 
	_vp.Width = width; 
	_vp.Height = height; 
	_vp.MinDepth = minDepth; 
	_vp.MaxDepth = maxDepth; 
}

Vec3 Viewport::Project(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P)
{
	Matrix wvp = W * V * P;

	// TransformCoord
	// TransformNormal; 
	Vec3 p = Vec3::Transform(pos, wvp);

	p.x = (p.x + 1.0f) * (_vp.Width / 2) + _vp.TopLeftX;
	p.y = (-p.y + 1.0f) * (_vp.Height / 2) + _vp.TopLeftY; 
	p.z = p.z * (_vp.MaxDepth - _vp.MinDepth) + _vp.MinDepth;

	return p; 
}

Vec3 Viewport::Unproject(const Vec3& pos, const Matrix& W, const Matrix& V, const Matrix& P)
{
	Vec3 p = pos;

	p.x = 2.f * (p.x - _vp.TopLeftX) / _vp.Width - 1.f;
	p.y = -2.f * (p.y - _vp.TopLeftY) / _vp.Height + 1.f;
	p.z = ((p.z - _vp.MinDepth) / (_vp.MaxDepth - _vp.MinDepth));

	Matrix wvp = W * V * P;
	Matrix wvpInv = wvp.Invert();

	p = Vec3::Transform(p, wvpInv);
	return p;
}

Viewport라는 클래스가 만들어졌으면

 

2. Graphics 클래스에 Viewport 클래스 적용하기

Graphics.h로 돌아가서 원래는 _viewport가 여기 있었는데

	// Misc
	//D3D11_VIEWPORT _viewport = { 0 };

주석처리 하고

	Viewport _vp; 

방금 만든 Viewport 클래스 버전으로 만들어 준다.

 

_vp로 만들어줬으면

SetViewport를 할 때

public: 
void SetViewport(float width, float height, float x = 0, float y = 0, float minDepth = 0, float maxDepth = 1);

매개변수 값을 전달해서 만들어주는 버전으로 만든다.

그리고 필요한 사람이 가지고 갈 수 있도록

	Viewport& GetViewport() { return _vp; }

이렇게 열어주도록 한다.

 

void Graphics::SetViewport()
{
	_viewport.TopLeftX = 0.0f;
	_viewport.TopLeftY = 0.0f;
	_viewport.Width = static_cast<float>(GAME->GetGameDesc().width);
	_viewport.Height = static_cast<float>(GAME->GetGameDesc().height);
	_viewport.MinDepth = 0.0f;
	_viewport.MaxDepth = 1.0f;
}

이 것을 삭제하고 원하는 버전으로 만들어 준다.

 

Graphics.h에

#include "Viewport.h"

추가하고

void Graphics::SetViewport(float width, float height, float x, float y, float minDepth, float maxDepth)
{
	_vp.Set(width, height, x, y, minDepth, maxDepth); 
}

이렇게 정의하면

void Viewport::Set(float width, float height, float x, float y, float minDepth, float maxDepth)
{
	_vp.TopLeftX = x; 
	_vp.TopLeftY = y; 
	_vp.Width = width; 
	_vp.Height = height; 
	_vp.MinDepth = minDepth; 
	_vp.MaxDepth = maxDepth; 
}

여기에 들어오면서 정보가 세팅된다.

 

Engine을 빌드하면 에러가 나는데

void Graphics::RenderBegin()
{
	_deviceContext->OMSetRenderTargets(1, _renderTargetView.GetAddressOf(), _depthStencilView.Get());
	_deviceContext->ClearRenderTargetView(_renderTargetView.Get(), (float*)(&GAME->GetGameDesc().clearColor));
	_deviceContext->ClearDepthStencilView(_depthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1 ,0);
	_deviceContext->RSSetViewports(1, &_viewport);
}

에서 이제 _viewport가 없으니까

	_deviceContext->RSSetViewports(1, &_viewport);

	_vp.RSSetviewport(); 

이렇게 교체해주면 된다.

 

void Graphics::Init(HWND hwnd)
{
	_hwnd = hwnd;

	CreateDeviceAndSwapChain();
	CreateRenderTargetView(); 
	CreateDepthStencilView();
	SetViewport();
}

여기에도

	SetViewport(GAME->GetGameDesc().width, GAME->GetGameDesc().height);

SetViewport에 인자를 넣어준다.

 

이제 Engine 프로젝트의 빌드가 된다.

오늘의 핵심은 Projection을 넣어준게 핵심이다.

 

이제 이걸 이용해 Projection 테스트를 해본다. 2D좌표로 변환하기도 하고, 3D좌표로 변환하기도 하고 왔다 갔다 하면서 작업이 정상적으로 되는지 살펴보도록 한다.

 

3. ViewportDemo 클래스에서 Viewport 테스트 하기

Client 프로젝트로 가서

물체를 배치해서 거기서 테스트를 할 것이기 때문에

TextureBufferDemo클래스를 복붙하고 이름을 ViewportDemo라고 한다.

Client/Game필터에 넣는다. 코드를 ViewportDemo에 맞게 수정한다.

ComputeShader 관련 코드는 삭제한다.

ViewportDemo::Init에서

auto newSrv = MakeComputeShaderTexture();

를 삭제하고,

auto texture = make_shared<Texture>(); 
texture->SetSRV(newSrv);

이 부분을 Texture를 Load하는 방식으로 복원한다.

auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");

 

Main.cpp에 ViewportDemo세팅한다.

 

Viewport에서 뭔가를 해주는 작업을 ViewportDemo::Update에 넣어주도록 한다.

void ViewportDemo::Update()
{
	static float width = 800.f; 
	static float height = 600.f; 
	static float x = 0.f; 
	static float y = 0.f; 

	ImGui::InputFloat("Width", &width, 10.f); 
	ImGui::InputFloat("Height", &height, 10.f); 
	ImGui::InputFloat("X", &x, 10.f); 
	ImGui::InputFloat("Y", &y, 10.f); 

	GRAPHICS->SetViewport(width, height, x, y); 
}
#include "Viewport.h"

 

클라이언트를 빌드한다.

 

width, height, x, y를 imGui를 이용해 조절할 수 있게 되었다.

 

 

Main의 WinMain에서

	desc.clearColor = Color(0.5f, 0.5f, 0.5f, 0.5f);

이렇게 회색으로 바꾸고,

실행을 해보면

 

시야각과 어디를 바라보고 있는지를 바꿀 수 있다.

viewport는 바라보고 있는 viewport의 크기 -1에서 1로 넘어온 비율을 얼마만의 화면 크기로 렌더링을 해줄 것인지를 얘기한다고 볼 수 있다.

 

4. ViewportDemo에서 Project함수를 이용해 3D좌표를 2D화면으로 넘겼을 때 값 확인하기

다음으로 알아야 할 것은

projection을 했을 때 어떻게 동작할 것인가가 고민거리다.

 

ViewportDemo에서 이어서 작업을 해본다.

어떤 물체를 하나만 위치를 0, 0, 0에 배치했다고 가정을 해본다.

ViewportDemo::Init에서

	for (int32 i = 0; i < 1; i++)
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));

이렇게 수정하고

 

보고 싶은 건 ViewportDemo::Update에서 Projection을 때렸을 때

예를 들어

void ViewportDemo::Update()
{
	static float width = 800.f; 
	static float height = 600.f; 
	static float x = 0.f; 
	static float y = 0.f; 

	ImGui::InputFloat("Width", &width, 10.f); 
	ImGui::InputFloat("Height", &height, 10.f); 
	ImGui::InputFloat("X", &x, 10.f); 
	ImGui::InputFloat("Y", &y, 10.f); 

	GRAPHICS->SetViewport(width, height, x, y); 

	//

	static Vec3 pos = Vec3(2, 0, 0); 
	ImGui::InputFloat3("Pos", (float*)&pos);

	Viewport& vp = GRAPHICS->GetViewport(); 
	Vec3 pos2D = vp.Project(pos, Matrix::Identity, Camera::S_MatView, Camera::S_MatProjection); // 2D화면으로 넘어가는 거 

	ImGui::InputFloat3("Pos2D", (float*)&pos2D); 

	{
		Vec3 temp = vp.Unproject(pos2D, Matrix::Identity, Camera::S_MatView, Camera::S_MatProjection); 
		ImGui::InputFloat3("Recalc", (float*)&temp); 
	}
}

3D좌표가 2D화면으로 넘어 갔을 때 어떤 좌표가 되는지 보는 것이다.

 

중요한 건 3D에서 2D로 갔다가 다시 3D로 돌아갈 때 원래 있던 좌표가 그대로 뜬다는 걸 볼 수 있다.

 

Projection이랑 UnProjection은 3D좌표를 WVP에다가 Projection 이 끝난 후 Viewport 변환 행렬까지

끝나면

2D화면으로 넘어가서

중앙이 400, 300으로 되어 있는 걸 볼 수 있다.

 

깊이 값은 카메라 위치에 따라서 달라 질 수 있다.

 

유니티에서 실습할 때도 2D에서 3D로 넘어갈 때는 어느 깊이를 기준으로 할지 0에서 1사이에 깊이값이 있어야지만 그걸 기준으로 깊이 퍼센티지를 알 수 있으니까 그럴 이용해서 다시 돌아갈 수 있다는 것도 알 수 있다.

가까이 다가가면 0에 가까워지고

멀리가면 1까지 간다.( 2D에서 3D로 변환 시 깊이 값은 0에서 1 사이로 정규화되며, 원칙적으로 1을 초과하지 않는다.)

 

이제 임의로 어떤 화면에 어떤 좌표를 클릭했다 하더라도 그 좌표를 찾아서 다시 3D 좌표로 얼마든지 넘어갈 수 있다. 그러면 결국에는 카메라의 좌표에서 클릭한 물체의 좌표 방향으로 레이저를 쏘면 실질적으로 피킹이 되면서 레이케스팅이 되면서 그 쪽 방향으로 레이저를 쏴서 맞아 떨어지는 애가 있으면 걔를 클릭 상태로 인지하게 만들어 주면 된다.

 

그 다음은 충돌이라는 개념이 있어서 피격해서 충돌이 되었다는 거 알면 picking이 끝난다.

 

클릭이 당연하게 아니다.

 

반응형

'DirectX' 카테고리의 다른 글

80_Picking  (0) 2024.03.28
79_Sphere Collider  (0) 2024.03.28
77_RenderManager 구조정리  (0) 2024.03.24
76_StructureBuffer  (0) 2024.03.23
75_TextureBuffer  (0) 2024.03.22

댓글