이번엔 빌보드를 활용해서 풀대신에 눈을 내리게 해보자

빌보드를 활용해서 만들어 줄텐데 비슷한 부분이 많지만 몇가지 달라지는 부분이 있기 때문에 이 부분을 중점으로

살펴보자.

일단 눈을 만들 때 풀을 만들어줄 때 처럼 인위적으로 몇개를 넣어줘서 위치를 지정하는 것이 아니라 구역을 정해주면 그 구역에 지정해준 카운트만큼 눈송이를 뿌려주는 방식이다. 또한 눈 효과를 위해서 시간이 얼마나 경과되었는지에 대한 

정보를 상수버퍼로 넣어줘야 한다. 

그렇게하기 위해 상수버퍼에 넣어줄 구조체타입을 정의해주고 shader쪽에서 이를 push하는 코드를 만들어주자.

BindShaderDesc.h

//어떻게 뿌릴지
struct SnowBillboardDesc
{
	Color color = Color(1, 1, 1, 1);

	Vec3 velocity = Vec3(0, -5, 0);
	float drawDistance = 0;

	Vec3 origin = Vec3(0, 0, 0);
	//흔들림강도
	float turbulence = 5;

	Vec3 extent = Vec3(0, 0, 0);
	float time;
};

 

Shader.h

void PushSnowData(const SnowBillboardDesc& desc);

SnowBillboardDesc _snowDesc;
shared_ptr<ConstantBuffer<SnowBillboardDesc>> _snowBuffer;
ComPtr<ID3DX11EffectConstantBuffer> _snowEffectBuffer;

Shader.cpp

void Shader::PushSnowData(const SnowBillboardDesc& desc)
{
	if (_snowEffectBuffer == nullptr)
	{
		_snowBuffer = make_shared<ConstantBuffer<SnowBillboardDesc>>();
		_snowBuffer->Create();
		_snowEffectBuffer = GetConstantBuffer("SnowBuffer");
	}

	_snowDesc = desc;
	_snowBuffer->CopyData(_snowDesc);
	_snowEffectBuffer->SetConstantBuffer(_snowBuffer->GetComPtr().Get());
}

 

SnowBillboard에서는 일단 생성자부분에서 눈을 모두 만들어주고 update가 필요한것만 update해주는 식으로 구현해주자.

SnowBillboard.h

#pragma once
#include "Component.h"

//정점 최대개수
#define MAX_BILLBOARD_COUNT 500

struct VertexSnow
{
	Vec3 position;
	Vec2 uv;
	Vec2 scale;
	Vec2 random;
};

class SnowBillboard : public Component
{
	using Super = Component;

public:
	//범위, 그려줄 개수
	SnowBillboard(Vec3 extent, int32 drawCount = 100);
	~SnowBillboard();

	void Update();

	void SetMaterial(shared_ptr<Material> material) { _material = material; }
	void SetPass(uint8 pass) { _pass = pass; }

private:
	vector<VertexSnow> _vertices;
	vector<uint32> _indices;
	shared_ptr<VertexBuffer> _vertexBuffer;
	shared_ptr<IndexBuffer> _indexBuffer;

	//그려줄 애들의 수
	int32 _drawCount = 0;

	shared_ptr<Material> _material;
	uint8 _pass = 0;

	SnowBillboardDesc _desc;
	//경과시간
	float _elpasedTime = 0.f;
};

SnowBillboard.cpp

#include "pch.h"
#include "SnowBillboard.h"
#include "Material.h"
#include "Camera.h"
#include "MathUtils.h"



SnowBillboard::SnowBillboard(Vec3 extent, int32 drawCount) : Super(ComponentType::SnowBillBoard)
{
	_desc.extent = extent;
	//적절한범위
	_desc.drawDistance = _desc.extent.z * 2.0f;
	_drawCount = drawCount;

	//사각형이기 때문에 *4
	const int32 vertexCount = _drawCount * 4;
	_vertices.resize(vertexCount);

	//범위안에서 랜덤하게 만들어주기
	for (int32 i = 0; i < _drawCount * 4; i += 4)
	{
		
		Vec2 scale = MathUtils::RandomVec2(0.1f, 0.5f);

		Vec3 position;
		position.x = MathUtils::Random(-_desc.extent.x, _desc.extent.x);
		position.y = MathUtils::Random(-_desc.extent.y, _desc.extent.y);
		position.z = MathUtils::Random(-_desc.extent.z, _desc.extent.z);

		Vec2 random = MathUtils::RandomVec2(0.0f, 1.0f);

		_vertices[i + 0].position = position;
		_vertices[i + 1].position = position;
		_vertices[i + 2].position = position;
		_vertices[i + 3].position = position;

		_vertices[i + 0].uv = Vec2(0, 1);
		_vertices[i + 1].uv = Vec2(0, 0);
		_vertices[i + 2].uv = Vec2(1, 1);
		_vertices[i + 3].uv = Vec2(1, 0);

		_vertices[i + 0].scale = scale;
		_vertices[i + 1].scale = scale;
		_vertices[i + 2].scale = scale;
		_vertices[i + 3].scale = scale;

		_vertices[i + 0].random = random;
		_vertices[i + 1].random = random;
		_vertices[i + 2].random = random;
		_vertices[i + 3].random = random;
	}

	_vertexBuffer = make_shared<VertexBuffer>();
	_vertexBuffer->Create(_vertices, 0);

	const int32 indexCount = _drawCount * 6;
	_indices.resize(indexCount);

	for (int32 i = 0; i < _drawCount; i++)
	{
		_indices[i * 6 + 0] = i * 4 + 0;
		_indices[i * 6 + 1] = i * 4 + 1;
		_indices[i * 6 + 2] = i * 4 + 2;
		_indices[i * 6 + 3] = i * 4 + 2;
		_indices[i * 6 + 4] = i * 4 + 1;
		_indices[i * 6 + 5] = i * 4 + 3;
	}

	_indexBuffer = make_shared<IndexBuffer>();
	_indexBuffer->Create(_indices);
}

SnowBillboard::~SnowBillboard()
{
}

void SnowBillboard::Update()
{
	
	_desc.origin = CUR_SCENE->GetMainCamera()->GetTransform()->GetPosition();
	_desc.time = _elpasedTime;
	//시간흐르게
	_elpasedTime += DT;

	auto shader = _material->GetShader();

	// Transform
	auto world = GetTransform()->GetWorldMatrix();
	shader->PushTransformData(TransformDesc{ world });

	// GlobalData
	shader->PushGlobalData(Camera::S_MatView, Camera::S_MatProjection);

	// SnowData
	shader->PushSnowData(_desc);

	// Light
	_material->Update();

	// IA
	_vertexBuffer->PushData();
	_indexBuffer->PushData();

	shader->DrawIndexed(0, _pass, _drawCount * 6);
}

 

이렇게 해주고 쉐이더코드를 만들어주자. 핵심은 빌보드를 사용해서 물체를 뿌리고 세부수치를 조절해주면 되는 것이다. 

우선 빌보드를 설정하는 메인코드를 만들어주고 그 다음에 쉐이더코드를 만들어주자.

SnowDemo.cpp

#include "pch.h"
#include "SnowDemo.h"
#include "RawBuffer.h"
#include "TextureBuffer.h"
#include "Material.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "Graphics.h"
#include "SphereCollider.h"
#include "Scene.h"
#include "AABBBoxCollider.h"
#include "OBBBoxCollider.h"
#include "Terrain.h"
#include "Camera.h"
#include "Button.h"
#include "Billboard.h"
#include "SnowBillboard.h"

void SnowDemo::Init()
{
	_shader = make_shared<Shader>(L"29. SnowDemo.fx");

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		camera->GetCamera()->SetCullingMaskLayerOnOff(Layer_UI, true);
		CUR_SCENE->Add(camera);
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}

	//빌보드
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
		//범위, 개수
		obj->AddComponent(make_shared<SnowBillboard>(Vec3(100, 100, 100), 10000));
		{
			// Material
			{
				shared_ptr<Material> material = make_shared<Material>();
				material->SetShader(_shader);
				auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\grass.png");
				//auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
				material->SetDiffuseMap(texture);
				MaterialDesc& desc = material->GetMaterialDesc();
				desc.ambient = Vec4(1.f);
				desc.diffuse = Vec4(1.f);
				desc.specular = Vec4(1.f);
				RESOURCES->Add(L"Veigar", material);

				obj->GetSnowBillBoard()->SetMaterial(material);
			}
		}

		CUR_SCENE->Add(obj);
	}

}

void SnowDemo::Update()
{

}

void SnowDemo::Render()
{

}

 

이제 쉐이더코드를 만들어주면 되는데 이때, SnowBillboard에서 만들어준 Vertex부분과 상수버퍼의 Desc와 구조를 맞춰주어야하고 은은하게 어둡게 보이게 해주는 알파블랜딩을 위해 Ouput에서 내보내주기 위해 vertexoutput에 추가해주자. 

그리고 글로벌 쉐이더쪽에 가서 알파블랜딩이 되도록 쉐이더 코드를 만들어주자.

Global.fx

////////////////
// BlendState //
////////////////

BlendState AlphaBlend
{
    AlphaToCoverageEnable = false;

    BlendEnable[0] = true;
    //밑에 2개 섞어주기
    SrcBlend[0] = SRC_ALPHA;
    //1-a
    DestBlend[0] = INV_SRC_ALPHA;
    BlendOp[0] = ADD;

    SrcBlendAlpha[0] = One;
    DestBlendAlpha[0] = Zero;
    BlendOpAlpha[0] = Add;

    RenderTargetWriteMask[0] = 15;
};


#define PASS_BS_VP(name, bs, vs, ps)				
pass name											
{													
	SetBlendState(bs, float4(0, 0, 0, 0), 0xFF);	
    SetVertexShader(CompileShader(vs_5_0, vs()));	
    SetPixelShader(CompileShader(ps_5_0, ps()));	
}

 

SnowDemo.fx

#include "00. Global.fx"
#include "00. Light.fx"
#include "00. Render.fx"

//상수버퍼- 구조 맞춰주기
cbuffer SnowBuffer
{
    float4 Color;
    float3 Velocity;
    float DrawDistance;

    float3 Origin;
    float Turbulence;

    float3 Extent;
    float Time;
};

struct VertexInput
{
    float4 position : POSITION;
    float2 uv : TEXCOORD;
    float2 scale : SCALE;
    float2 random : RANDOM;
};

struct V_OUT
{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD;
    //알파 블랜딩 - 은은하게 어둡게 보이도록
    float alpha : ALPHA;
};

V_OUT VS(VertexInput input)
{
    V_OUT output;

    //얼마만큼 이동했는지 
    float3 displace = Velocity * Time * 100;

    //눈 이동시키기
    // input.position.y += displace;
    input.position.y = Origin.y + Extent.y - (input.position.y - displace) % Extent.y;
    input.position.x += cos(Time - input.random.x) * Turbulence;
    input.position.z += cos(Time - input.random.y) * Turbulence;
    //input.position.xyz = Origin + (Extent + (input.position.xyz + displace) % Extent) % Extent - (Extent * 0.5f);

    float4 position = mul(input.position, W);

    float3 up = float3(0, 1, 0);
    //float3 forward = float3(0, 0, 1);
    float3 forward = position.xyz - CameraPosition(); // BillBoard
    float3 right = normalize(cross(up, forward));

    position.xyz += (input.uv.x - 0.5f) * right * input.scale.x;
    position.xyz += (1.0f - input.uv.y - 0.5f) * up * input.scale.y;
    position.w = 1.0f;

    output.position = mul(mul(position, V), P);
    output.uv = input.uv;

    output.alpha = 1.0f;

    // Alpha Blending
    float4 view = mul(position, V);
    output.alpha = saturate(1 - view.z / DrawDistance) * 0.8f;

    return output;
}

float4 PS(V_OUT input) : SV_Target
{
    float4 diffuse = DiffuseMap.Sample(LinearSampler, input.uv);
    
    diffuse.rgb = Color.rgb * input.alpha * 2.0f;
    diffuse.a = diffuse.a * input.alpha * 1.5f;
    
    return diffuse;
}

technique11 T0
{
    PASS_BS_VP(P0, AlphaBlend, VS, PS)
}

 

이렇게 해주고 실행해주면 눈이 흔들리는 것을 볼 수 있다.



빌보드는 만약 오브젝트가 배치가 되어있다고 했을 때 카메라가 어느 방향에서 보아도 항상 오브젝트가 카메라의 정면을 향하고 있는 것이다. 이 빌보드는 파티클이나 눈이나 비 같이 이런 입자들도 우리를 바라봐야 하기 때문에 여기에 사용될 수 있을 것이다.

 

만약 물체하나에만 빌보드를 적용시킨다고하면 움직일 때마다 물체의 rotation을 수정해주거나 쉐이더코드를 수정해줄 수 있다.

이때 rotation을 수정해주는 방법을 해보자. 우선 오브젝트의 3개의 벡터(right,up,look)를 구해주자. 우선 look 벡터는 

카메라를 바라봐야 한다. 임의로 위쪽방향을 가르키는 벡터를 건네줘서 up과 look을 외적해줘서 서로 수직인 right 벡터를 구하면 된다. 이렇게 구한 right와 look을 통해 up을 구해주면 된다. 

이 방식을 우리는 SimpleMath 라이브러리의 CreateWorld를 통해 해줄 것이다.

BillBoardDemo.cpp

void BillBoardTest::Update()
{
	auto go = GetGameObject();

	Vec3 up = Vec3(0, 1, 0);
	Vec3 cameraPos = CUR_SCENE->GetMainCamera()->GetTransform()->GetPosition();
	Vec3 myPos = GetTransform()->GetPosition();

	Vec3 forward = cameraPos - myPos;
	forward.Normalize();

	Matrix lookMatrix = Matrix::CreateWorld(myPos, forward, up);
}

 

이렇게 만들어주고 회전값을 적용시켜줘야하는데 지금 Rotation함수는 Vec3로 값을 받고 있는데 우리는 Quaternion을 통해 회전값을 받아오기 때문에 우리가 만들어준 Quaternion을 오일러각도로 바꿔주는 함수를 static으로 선언해서 외부에서

사용할 수 있도록 하자.

Transform.cpp

Vec3 Transform::ToEulerAngles(Quaternion q)) 
{
	Vec3 angles;

	// roll (x-axis rotation)
	double sinr_cosp = 2 * (q.w * q.x + q.y * q.z);
	double cosr_cosp = 1 - 2 * (q.x * q.x + q.y * q.y);
	angles.x = std::atan2(sinr_cosp, cosr_cosp);

	// pitch (y-axis rotation)
	double sinp = 2 * (q.w * q.y - q.z * q.x);
	if (std::abs(sinp) >= 1)
		angles.y = std::copysign(3.14159f / 2, sinp); // use 90 degrees if out of range
	else
		angles.y = std::asin(sinp);

	// yaw (z-axis rotation)
	double siny_cosp = 2 * (q.w * q.z + q.x * q.y);
	double cosy_cosp = 1 - 2 * (q.y * q.y + q.z * q.z);
	angles.z = std::atan2(siny_cosp, cosy_cosp);

	return angles;
}

 

이렇게 해주고 Script부분을 완성해주고 오브젝트에 컴포넌트로 붙여주자.

BillBoardDemo.cpp

// Mesh
{
	auto obj = make_shared<GameObject>();
	obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
	obj->GetOrAddTransform()->SetScale(Vec3(2.f));
	obj->AddComponent(make_shared<MeshRenderer>());
	obj->AddComponent(make_shared<BillBoardTest>());
	{
		obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
	}
	{
		auto mesh = RESOURCES->Get<Mesh>(L"Quad");
		obj->GetMeshRenderer()->SetMesh(mesh);
		obj->GetMeshRenderer()->SetPass(0);
	}

	CUR_SCENE->Add(obj);
}

void BillBoardTest::Update()
{
	auto go = GetGameObject();

	Vec3 up = Vec3(0, 1, 0);
	Vec3 cameraPos = CUR_SCENE->GetMainCamera()->GetTransform()->GetPosition();
	Vec3 myPos = GetTransform()->GetPosition();

	Vec3 forward = cameraPos - myPos;
	forward.Normalize();

	Matrix lookMatrix = Matrix::CreateWorld(myPos, forward, up);

	Vec3 S, T;
	Quaternion R;
	lookMatrix.Decompose(S, R, T);

	Vec3 rot = Transform::ToEulerAngles(R);

	GetTransform()->SetRotation(rot);
}

 

이렇게 실행해보면 하나의 물체에서는 빌보드가 잘 작동하는 것을 볼 수 있다.

 

이제 여러물체를 만들고 이 모든 물체가 나를 바라보도록 만들어보자. 물체를 많이 만들어줄 때 인스턴싱을 우리가 사용했었는데 이 방법을 빌보드로 적용시키려면 정점을 우선 넘겨주고 쉐이더에서 각 정점의 위치를 고정시켜주거나 기본 정점을 가지고 이 정점을 여러개로 늘려주는 작업을 Geometry 쉐이더에서 해줄 수도 있다.

지금은 물체를 여러개 만들어주는 것이 아니라 정점을 여러개로 늘려서 쉐이더에 건네주고 첫 위치를 일단은 건네준 다음에 그 자체를 일단 하나의 통 매쉬로 만들어서 건네주고 세부적인 메쉬둘 간의 좌표 계산을 쉐이더에서 해주도록 하자.

 

매쉬를 늘리는 것처럼 정점을 늘리는 방식으로 만들고 이 정점을 묶어서 한번만 건네주면 된다. 이때 위치는 고정되어있고 바뀌지않는다. 이렇게 해주기 위해 Geometry클래스처럼 정점과 인덱스 정보를 넘겨줄 수 있게 만들어주자. 이때 위치와 스케일을 동일하게 해서 쉐이더쪽에 전달해주게 되는데 이렇게 하는 이유는 좌표와 스케일 연산을 쉐이더쪽에서 해주기 위해서 이다.

BillBoard.h

#pragma once
#include "Component.h"

struct VertexBillboard
{
	Vec3 position;
	Vec2 uv;
	Vec2 scale;
};

//정점 최대개수
#define MAX_BILLBOARD_COUNT 500

class Billboard : public Component
{
	using Super = Component;

public:
	Billboard();
	~Billboard();

	void Update();
	//만들어줄 위치
	void Add(Vec3 position, Vec2 scale);

	void SetMaterial(shared_ptr<Material> material) { _material = material; }
	void SetPass(uint8 pass) { _pass = pass; }

private:
	vector<VertexBillboard> _vertices;
	vector<uint32> _indices;
	shared_ptr<VertexBuffer> _vertexBuffer;
	shared_ptr<IndexBuffer> _indexBuffer;

	//그려줄 애들의 수
	int32 _drawCount = 0;
	//그려준 애들의 수 - 바뀔때만 갱신
	int32 _prevCount = 0;

	shared_ptr<Material> _material;
	uint8 _pass = 0;
};

BillBoard.cpp

#include "pch.h"
#include "Billboard.h"
#include "Material.h"
#include "Camera.h"

Billboard::Billboard() : Super(ComponentType::BillBoard)
{
	//사각형이라고 가정했을 때 
	int32 vertexCount = MAX_BILLBOARD_COUNT * 4;
	int32 indexCount = MAX_BILLBOARD_COUNT * 6;		//삼각형 2개

	_vertices.resize(vertexCount);
	_vertexBuffer = make_shared<VertexBuffer>();
	//갱신할 수 있도록 true
	_vertexBuffer->Create(_vertices, 0, true);

	_indices.resize(indexCount);

	for (int32 i = 0; i < MAX_BILLBOARD_COUNT; i++)
	{
		_indices[i * 6 + 0] = i * 4 + 0;
		_indices[i * 6 + 1] = i * 4 + 1;
		_indices[i * 6 + 2] = i * 4 + 2;
		_indices[i * 6 + 3] = i * 4 + 2;
		_indices[i * 6 + 4] = i * 4 + 1;
		_indices[i * 6 + 5] = i * 4 + 3;
	}

	_indexBuffer = make_shared<IndexBuffer>();
	_indexBuffer->Create(_indices);
}

Billboard::~Billboard()
{
}

void Billboard::Update()
{
	//바뀌었다면
	if (_drawCount != _prevCount)
	{
		_prevCount = _drawCount;

		D3D11_MAPPED_SUBRESOURCE subResource;
		DC->Map(_vertexBuffer->GetComPtr().Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
		{
			memcpy(subResource.pData, _vertices.data(), sizeof(VertexBillboard) * _vertices.size());
		}
		DC->Unmap(_vertexBuffer->GetComPtr().Get(), 0);
	}

	auto shader = _material->GetShader();

	// Transform
	auto world = GetTransform()->GetWorldMatrix();
	shader->PushTransformData(TransformDesc{ world });

	// GlobalData
	shader->PushGlobalData(Camera::S_MatView, Camera::S_MatProjection);

	// Light
	_material->Update();

	// IA
	_vertexBuffer->PushData();
	_indexBuffer->PushData();

	shader->DrawIndexed(0, _pass, _drawCount * 6);
}

void Billboard::Add(Vec3 position, Vec2 scale)
{
	//동일한 위치 동일한 스케일 -> 쉐이더에서 계산하도록
	_vertices[_drawCount * 4 + 0].position = position;
	_vertices[_drawCount * 4 + 1].position = position;
	_vertices[_drawCount * 4 + 2].position = position;
	_vertices[_drawCount * 4 + 3].position = position;

	_vertices[_drawCount * 4 + 0].uv = Vec2(0, 1);
	_vertices[_drawCount * 4 + 1].uv = Vec2(0, 0);
	_vertices[_drawCount * 4 + 2].uv = Vec2(1, 1);
	_vertices[_drawCount * 4 + 3].uv = Vec2(1, 0);

	_vertices[_drawCount * 4 + 0].scale = scale;
	_vertices[_drawCount * 4 + 1].scale = scale;
	_vertices[_drawCount * 4 + 2].scale = scale;
	_vertices[_drawCount * 4 + 3].scale = scale;

	_drawCount++;
}

 

여기서 주의깊게 봐야할 부분은 인스턴싱에서 물체를 늘렸던 것과 달리 여기서는 정점을 늘리고 있다는 것이다.

이제 쉐이더코드를 만들어주자. 좌표 변환행렬을 직접 적용시켜줘서 좌표를 사각형처럼 나오게 만들어주자. 지금은 아직 카메라쪽으로 바라보는 것 대신 임의의 forward행렬을 넣어주자.

BillBoardDemo.fx

#include "00. Global.fx"
#include "00. Light.fx"
#include "00. Render.fx"

struct VertexInput
{
    float4 position : POSITION;
    float2 uv : TEXCOORD;
    float2 scale : SCALE;
};

struct V_OUT
{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD;
};

V_OUT VS(VertexInput input)
{
    V_OUT output;
    
    float4 position = mul(input.position, W);
    
    float3 up = float3(0, 1, 0);
    float3 forward = float3(0, 0, 1); // TODO : 카메라 바라보게 만들어 줘야한다.
    float3 right = normalize(cross(up, forward));
    
    //직접 좌표변환 
    position.xyz += (input.uv.x - 0.5f) * right * input.scale.x;
    position.xyz += (1.0f - input.uv.y - 0.5f) * up * input.scale.y;
    position.w = 1.0f;
    
    output.position = mul(mul(position, V), P);
    
    output.uv = input.uv;
    
    return output;
}

float4 PS(V_OUT input) : SV_Target
{
    float4 diffuse = DiffuseMap.Sample(LinearSampler, input.uv);
    
    return diffuse;
}

technique11 T0
{
    pass P0
    {
        //SetRasterizerState(FillModeWireFrame);
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, PS()));
    }
}

 

이렇게 해주고 메인코드에서 빌보드 오브젝트를 만들고 오브젝트 여러개를 만들고 테스트해보자.

BillBoardDemo.cpp

// Billboard
{
	auto obj = make_shared<GameObject>();
	obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
	obj->AddComponent(make_shared<Billboard>());
	{
		// Material
		{
			shared_ptr<Material> material = make_shared<Material>();
			material->SetShader(_shader);
			auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\grass.png");
			//auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
			material->SetDiffuseMap(texture);
			MaterialDesc& desc = material->GetMaterialDesc();
			desc.ambient = Vec4(1.f);
			desc.diffuse = Vec4(1.f);
			desc.specular = Vec4(1.f);
			RESOURCES->Add(L"Veigar", material);

			obj->GetBillboard()->SetMaterial(material);
		}
	}

	for (int32 i = 0; i < 500; i++)
	{

		Vec2 scale = Vec2(1 + rand() % 3, 1 + rand() % 3);
		Vec2 position = Vec2(-100 + rand() % 200, -100 + rand() % 200);

		obj->GetBillboard()->Add(Vec3(position.x, scale.y * 0.5f, position.y), scale);
	}

	CUR_SCENE->Add(obj);
}

 

이렇게 해주면 다양한 크기와 위치의 사각형이 나오게 된다. 아직 카메라방향을 바라보게하는 것은 작동하지 않는다.

 

빌보드를 활용하여 풀과 같은 오브젝트를 생성해줄 수 도 있을 것이다. 

만약 빌보드 기능을 켜주려고 한다면 VS 부분에서 forward벡터를 카메라와 자신의 좌표를 통한 연산으로 수정해주면 될 것이다.

BillBoardDemo.fx

V_OUT VS(VertexInput input)
{
    V_OUT output;
    
    float4 position = mul(input.position, W);
    
    float3 up = float3(0, 1, 0);
    //float3 forward = float3(0, 0, 1); // TODO : 카메라 바라보게 만들어 줘야한다.
    float3 forward = position.xyz - CameraPosition(); // BillBoard
    float3 right = normalize(cross(up, forward));
    
    //직접 좌표변환 
    position.xyz += (input.uv.x - 0.5f) * right * input.scale.x;
    position.xyz += (1.0f - input.uv.y - 0.5f) * up * input.scale.y;
    position.w = 1.0f;
    
    output.position = mul(mul(position, V), P);
    
    output.uv = input.uv;
    
    return output;
}

 

이렇게 해주면 이리저리 움직여도 카메라를 쳐다보는 것을 볼 수 있다.

 

만약 여기에 풀 이미지를 넣어주면  실제 풀처럼 될 것이다. 

지금은 이미지의 알파값을 넣어주지 않아서 흰부분이 저렇게 보이는데 이것은 쉐이더의 PS 픽쉘쉐이더 단계에서 해주면 된다.

BillBoardDemo.fx

float4 PS(V_OUT input) : SV_Target
{
    float4 diffuse = DiffuseMap.Sample(LinearSampler, input.uv);
    
    //알파값 조정 일정이하 없애기
    if (diffuse.a < 0.3f)
        discard;
    
    return diffuse;
}

 

이렇게 해주면 진짜 풀 같이 만들어진 것을 볼 수 있다.


이번에는 만들어준 직교투영 카메라를 활용하여 버튼을 만들고 기능까지 붙여주자. 지금은 레이케스팅을 할 때, 원근 투영된 뷰포트에 대한 값을 찾아주고 있는데

이를 직교투영된 방식으로 해준다고하면 충돌체크는 OBB를 통해 해주면 될 것이고 방향을 잘 맞춰서 레이케스팅을 해주면 충돌이 이루어지면서 클릭이 될 것이다. 

 

Unity에서는 UI를 생성하면 기본적으로 EventSystem이라는 오브젝트가 자동으로 생성된다. 이 EventSystem을 통해 

피격 판정이 이루어졌는지 체크한다. 

우리는 클릭한 스크린 좌표를 가져와서 클릭한 좌표에 이 버튼이 포함되는지 확인해주는 방식으로 구현해보자. 

우선 버튼클래스를 만들어주고 버튼을 눌렀을 때 콜백함수가 호출되도록 만들어주자. 이때 콜백함수는 Std의 Function  변수를 사용해주자.

이 변수를 사용할 때 주의해야할 점이 있는데 만약 연결되는 콜백함수가 static이나 전역이면 문제가 없지만

람다나 멤버함수, 람다캡쳐를 통해 전달해주게 된다면 문제가 생길 수 있다는 것이다.

이때 람다캡처는 외부에 있던 함수를 복사해서 넣어줄 수 있는 것인데 이때 포인터가 들어가면 포인터가 외부에서 해제된 경우 없어진 주소를 참조하는 것이기 때문에 문제가 생길 수 있다.

스마트 포인터를 쓴다고 하면 참조가 계속되어서  릴리즈가 안되는 문제가 생길 수 있다.

 

Button.h

#pragma once
#include "Component.h"
class Button : public Component
{
	using Super = Component;

public:
	Button();
	virtual ~Button();

	bool Picked(POINT screenPos);

	void Create(Vec2 screenPos, Vec2 size, shared_ptr<class Material> material);

	void AddOnClickedEvent(std::function<void(void)> func);
	void InvokeOnClicked();

private:
	std::function<void(void)> _onClicked;
	RECT _rect;
};

Button.cpp

#include "pch.h"
#include "Button.h"
#include "MeshRenderer.h"
#include "Material.h"

Button::Button() : Super(ComponentType::Button)
{

}

Button::~Button()
{
}

bool Button::Picked(POINT screenPos)
{
	//해당영역에 들어가 있는지 확인해줌
	return ::PtInRect(&_rect, screenPos);
}

void Button::Create(Vec2 screenPos, Vec2 size, shared_ptr<class Material> material)
{
	auto go = _gameObject.lock();

	float height = GRAPHICS->GetViewport().GetHeight();
	float width = GRAPHICS->GetViewport().GetWidth();

	//실제 배치할 좌표구하기 
	float x = screenPos.x - width / 2;
	float y = height / 2 - screenPos.y;
	Vec3 position = Vec3(x, y, 0);

	go->GetOrAddTransform()->SetPosition(position);
	go->GetOrAddTransform()->SetScale(Vec3(size.x, size.y, 1));

	go->SetLayerIndex(Layer_UI);

	if (go->GetMeshRenderer() == nullptr)
		go->AddComponent(make_shared<MeshRenderer>());

	go->GetMeshRenderer()->SetMaterial(material);

	auto mesh = RESOURCES->Get<Mesh>(L"Quad");
	go->GetMeshRenderer()->SetMesh(mesh);
	go->GetMeshRenderer()->SetPass(0);

	//Picking
	//사각형 상하좌우 실제 영역구해주기
	_rect.left = screenPos.x - size.x / 2;
	_rect.right = screenPos.x + size.x / 2;
	_rect.top = screenPos.y - size.y / 2;
	_rect.bottom = screenPos.y + size.y / 2;
}

void Button::AddOnClickedEvent(std::function<void(void)> func)
{
	_onClicked = func;
}

void Button::InvokeOnClicked()
{
	if (_onClicked)
		_onClicked();
}

 

UI를 피킹하는 코드를 Scene클래스에 추가해주자.

Scene.cpp

void Scene::PickUI()
{
	if (INPUT->GetButtonDown(KEY_TYPE::LBUTTON) == false)
		return;

	if (GetUICamera() == nullptr)
		return;

	//누른 좌표가져오기
	POINT screenPt = INPUT->GetMousePos();

	shared_ptr<Camera> camera = GetUICamera()->GetCamera();

	const auto gameObjects = GetObjects();

	for (auto& gameObject : gameObjects)
	{
		if (gameObject->GetButton() == nullptr)
			continue;

		if (gameObject->GetButton()->Picked(screenPt))
			gameObject->GetButton()->InvokeOnClicked();
	}
}

 

이렇게 해주고 실제로 테스트해보기 위해 메인코드를 만들어주자. 아까 위에서 봤던 문제에서 핵심은 람다캡처에서 

메모리 문제가 생길 수 있기 때문에 주의해서 사용하거나 전역이나 static함수를 사용해주자.

람다에서 캡처는 fuctor에서 주고자하는 값을 멤버변수로 들고있는 것이라고 보면 된다.

ButtonDemo.cpp

#include "pch.h"
#include "ButtonDemo.h"
#include "RawBuffer.h"
#include "TextureBuffer.h"
#include "Material.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "Graphics.h"
#include "SphereCollider.h"
#include "Scene.h"
#include "AABBBoxCollider.h"
#include "OBBBoxCollider.h"
#include "Terrain.h"
#include "Camera.h"
#include "Button.h"

void ButtonDemo::Init()
{
	_shader = make_shared<Shader>(L"23. RenderDemo.fx");

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		camera->GetCamera()->SetCullingMaskLayerOnOff(Layer_UI, true);
		CUR_SCENE->Add(camera);
	}

	//UI_Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->GetCamera()->SetProjectionType(ProjectionType::Orthographic);
		camera->GetCamera()->SetNear(1.f);
		camera->GetCamera()->SetFar(100.f);

		//UI만 그려주게
		camera->GetCamera()->SetCullingMaskAll();
		camera->GetCamera()->SetCullingMaskLayerOnOff(Layer_UI, false);
		CUR_SCENE->Add(camera); 
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
		material->SetDiffuseMap(texture);
		MaterialDesc& desc = material->GetMaterialDesc();
		desc.ambient = Vec4(1.f);
		desc.diffuse = Vec4(1.f);
		desc.specular = Vec4(1.f);
		RESOURCES->Add(L"Veigar", material);
	}
	
	// Mesh - 버튼 역활
	{
		auto obj = make_shared<GameObject>();
		obj->AddComponent(make_shared<Button>());

		obj->GetButton()->Create(Vec2(100, 100), Vec2(100, 100), (RESOURCES->Get<Material>(L"Veigar")));

		obj->GetButton()->AddOnClickedEvent([obj]() { CUR_SCENE->Remove(obj); });

		CUR_SCENE->Add(obj);
	}

	// Mesh
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
		obj->GetOrAddTransform()->SetScale(Vec3(2.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}

		CUR_SCENE->Add(obj);
	}
}

void ButtonDemo::Update()
{

}

void ButtonDemo::Render()
{

}

 

이렇게 람다 캡처를 사용해서 콜백함수를 만들어준 다음 실행시켜보면 버튼이 잘 없어지는 것을 볼 수 있다.



이제 UI에 대해 알아보고 코드로 구현해볼 것인데 먼저 직교투영 카메라에 대해 알아보자

 

직교투영은 원근투영과 달리 절두체 평면이 직사각형 모양으로 크기가 유지된 채로 투영되는 화면이다. unity에서 UI는 영화에 CG를 붙이는 것처럼 기존 결과물에 추가적으로 붙이는 것을 볼 수 있다.

 

또다른 방법에는 직교투영과 원근투영 카메라를 둘 다 만든다음에 적절히 조합해서 결과물을 만드는 방법도 있다. 

우선 원근투영과 직교투영을 구분하고 카메라를 여러개 만들어서 사용할 수 있게 하자.

 

우선은 카메라가 여러개 등장할 수 있도록 구조를 바꿔주자. 지금은 물체가 인스턴싱으로 생성될 때 마지막으로 적용된 

뷰, 프로젝션 행렬을 통해 물체들이 연산되어 만들어지고 있는데 이것을 카메라 마다 돌아가면서 갱신하도록 바꿔주어야한다. 

그러려면 카메라마다 자기가 찍을 애들을 렌더링하는 코드를 넣어줘야할 것이다. 즉 레이어에 따라서 카메라에서 비트플래그를 통해 그려줄지 안그려줄지를 선택해서 렌더링해주면 될 것이다. 또한 원근투영이 가능하도록 프로젝션 연산부분을 수정해주자.

Camera.h

#pragma once
#include "Component.h"

enum class ProjectionType
{
	Perspective, // 원근 투영
	Orthographic, // 직교 투영
};

class Camera : public Component
{
	using Super = Component;
public:
	Camera();
	virtual ~Camera();

	virtual void Update() override;

	void SetProjectionType(ProjectionType type) { _type = type; }
	ProjectionType GetProjectionType() { return _type; }

	void UpdateMatrix();

	void SetNear(float value) { _near = value; }
	void SetFar(float value) { _far = value; }
	void SetFOV(float value) { _fov = value; }
	void SetWidth(float value) { _width = value; }
	void SetHeight(float value) { _height = value; }

	Matrix& GetViewMatrix() { return _matView; }
	Matrix& GetProjectionMatrix() { return _matProjection; }

	float GetWidth() { return _width; }
	float GetHeight() { return _height; }

private:
	ProjectionType _type = ProjectionType::Perspective;
	Matrix _matView = Matrix::Identity;
	Matrix _matProjection = Matrix::Identity;

	float _near = 1.f;
	float _far = 1000.f;
	float _fov = XM_PI / 4.f;
	float _width = 0.f;
	float _height = 0.f;

public:
	static Matrix S_MatView;
	static Matrix S_MatProjection;


public:
	//관련된 물체들을 소팅해서 가져오기
	void SortGameObject();

	void Render_Forward();

	void SetCullingMaskLayerOnOff(uint8 layer, bool on)
	{
		if (on)
			_cullingMask |= (1 << layer);
		else
			_cullingMask &= ~(1 << layer);
	}

	//아무것도 그려주지않겠다
	void SetCullingMaskAll() { SetCullingMask(UINT32_MAX); }
	void SetCullingMask(uint32 mask) { _cullingMask = mask; }
	//비트연산을 통해 그려줄지 안그려줄지 판단 
	bool IsCulled(uint8 layer) { return (_cullingMask & (1 << layer)) != 0; }

private:
	//비트마스크 - 그릴지 안그릴지에 대한 규칙
	uint32 _cullingMask = 0;
	vector<shared_ptr<GameObject>> _vecForward;

};

Camera.cpp

#include "pch.h"
#include "Camera.h"
#include "Scene.h"

Matrix Camera::S_MatView = Matrix::Identity;
Matrix Camera::S_MatProjection = Matrix::Identity;

Camera::Camera() : Super(ComponentType::Camera)
{
	_width = static_cast<float>(GAME->GetGameDesc().width);
	_height = static_cast<float>(GAME->GetGameDesc().height);
}

Camera::~Camera()
{

}

void Camera::Update()
{
	UpdateMatrix();

}

void Camera::SortGameObject()
{
	shared_ptr<Scene> scene = CUR_SCENE;
	unordered_set<shared_ptr<GameObject>>& gameObjects = scene->GetObjects();

	_vecForward.clear();

	for (auto& gameObject : gameObjects)
	{
		if (IsCulled(gameObject->GetLayerIndex()))
			continue;

		if (gameObject->GetMeshRenderer() == nullptr
			&& gameObject->GetModelRenderer() == nullptr
			&& gameObject->GetModelAnimator() == nullptr)
			continue;

		_vecForward.push_back(gameObject);
	}
}

void Camera::Render_Forward()
{
	S_MatView = _matView;
	S_MatProjection = _matProjection;

	//카메라에서 물체 그려주기
	GET_SINGLE(InstancingManager)->Render(_vecForward);
}

void Camera::UpdateMatrix()
{
	Vec3 eyePosition = GetTransform()->GetPosition();
	Vec3 focusPosition = eyePosition + GetTransform()->GetLook();
	Vec3 upDirection = GetTransform()->GetUp();

	_matView = S_MatView = ::XMMatrixLookAtLH(eyePosition, focusPosition, upDirection);

	if (_type == ProjectionType::Perspective)
	{
		_matProjection = ::XMMatrixPerspectiveFovLH(_fov, _width / _height, _near, _far);
	}
	else
	{
		_matProjection = ::XMMatrixOrthographicLH(_width, _height, _near, _far);
	}

	
}

 

이제 Scene에서 Render하는 부분을 따로 만들어주고 원근투영 타입의 메인카메라와 직교투영의 UI카메라를 가져오는 헬퍼 함수를 추가해주자.

Scene.h

#pragma once


class Scene
{
public:
	virtual void Start();
	virtual void Update();
	virtual void LateUpdate();
	
	virtual void Render();

	//추가
	virtual void Add(shared_ptr<GameObject> object);
	//제거
	virtual void Remove(shared_ptr<GameObject> object);

	
	unordered_set<shared_ptr<GameObject>>& GetObjects() { return _objects; }
	shared_ptr<GameObject> GetMainCamera();
	shared_ptr<GameObject> GetUICamera();
	shared_ptr<GameObject> GetLight() { return _lights.empty() ? nullptr : *_lights.begin(); }

	shared_ptr<class GameObject> Pick(int32 screenX, int32 screenY);

	void CheckCollision();

private:
	//물체를 가지고있는 추가 삭제 편하지만 순회에는 안좋다 검색활용
	unordered_set<shared_ptr<GameObject>> _objects;
	//카메라
	unordered_set<shared_ptr<GameObject>> _cameras;
	//빛
	unordered_set<shared_ptr<GameObject>> _lights;
};

 

Scene.cpp

void Scene::Render()
{
	for (auto& camera : _cameras)
	{
		camera->GetCamera()->SortGameObject();
		camera->GetCamera()->Render_Forward();
	}
}

shared_ptr<GameObject> Scene::GetMainCamera()
{
	for (auto& camera : _cameras)
	{
		if (camera->GetCamera()->GetProjectionType() == ProjectionType::Perspective)
			return camera;
	}

	return nullptr;
}

shared_ptr<GameObject> Scene::GetUICamera()
{
	for (auto& camera : _cameras)
	{
		if (camera->GetCamera()->GetProjectionType() == ProjectionType::Orthographic)
			return camera;
	}

	return nullptr;
}

 

이렇게 만들어준 Render부분을 SceneManager에서 호출해서 실제로 Render가 되도록 만들어주자.

SceneManager.cpp

#include "pch.h"
#include "SceneManager.h"

void SceneManager::Update()
{
	if (_currentScene == nullptr)
		return;

	_currentScene->Update();
	_currentScene->LateUpdate();

	_currentScene->Render();
}

 

이렇게 해주면 이전과 같이 잘 작동하는 것을 볼 수 있다.

이제 여기서 직교 투영 카메라를 추가로 배치하고 버튼역활을 할 오브젝트를 추가하고 테스트해보자

OrthoGraphicDemo.cpp

#include "pch.h"
#include "OrthoGraphicDemo.h"
#include "RawBuffer.h"
#include "TextureBuffer.h"
#include "Material.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "Graphics.h"
#include "SphereCollider.h"
#include "Scene.h"
#include "AABBBoxCollider.h"
#include "OBBBoxCollider.h"
#include "Terrain.h"
#include "Camera.h"

void OrthoGraphicDemo::Init()
{
	_shader = make_shared<Shader>(L"23. RenderDemo.fx");

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		camera->GetCamera()->SetCullingMaskLayerOnOff(Layer_UI, true);
		CUR_SCENE->Add(camera);
	}

	//UI_Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->GetCamera()->SetProjectionType(ProjectionType::Orthographic);
		camera->GetCamera()->SetNear(1.f);
		camera->GetCamera()->SetFar(100.f);

		//UI만 그려주게
		camera->GetCamera()->SetCullingMaskAll();
		camera->GetCamera()->SetCullingMaskLayerOnOff(Layer_UI, false);
		CUR_SCENE->Add(camera); 
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
		material->SetDiffuseMap(texture);
		MaterialDesc& desc = material->GetMaterialDesc();
		desc.ambient = Vec4(1.f);
		desc.diffuse = Vec4(1.f);
		desc.specular = Vec4(1.f);
		RESOURCES->Add(L"Veigar", material);
	}
	
	// Mesh - 버튼 역활
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f, 200.f, 0.f));
		//크게해도 UI라서 작게나올것이다.
		obj->GetOrAddTransform()->SetScale(Vec3(200.f));
		obj->AddComponent(make_shared<MeshRenderer>());

		obj->SetLayerIndex(Layer_UI);
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Quad");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}

		CUR_SCENE->Add(obj);
	}

	// Mesh
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
		obj->GetOrAddTransform()->SetScale(Vec3(2.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}

		CUR_SCENE->Add(obj);
	}
}

void OrthoGraphicDemo::Update()
{

}

void OrthoGraphicDemo::Render()
{

}

 

이렇게 해주면 한 오브젝트는 카메라가 움직여도 그래도 있지만 한 오브젝트는 카메라가 멀어지면 작아지는 모습을 볼 수 있다.

강의

https://inf.run/Ju6ZN

 

학습 페이지

 

www.inflearn.com

 

3D 그래픽스에서 가장 기본적인 구성 단위 중 하나인 Triangle의 Point Test와 Intersection, Raycast에 대해 알아보자 이 삼각형을 통해 우리는 네비게이션 매쉬(갈 수 있는 범위를 정해준다.)와 같은 요소를 만들어 줄 수 있다.

1. Point Test

만약 삼각형을 클릭했을 때 그것을 판별해야한다고 할 때 Point Test를 하게 될 것이다. 이때 삼각형을 구성하는 벡터들을 사용해서 교차 벡터를 계산하고 이들의 내적을 검사해준다. 

// 점이 삼각형 내부에 있는지 판단하는 함수
bool MathUtils::PointInTriangle(const Point3D& p, const Triangle3D& t)
{
	Vec3 a = t.a - p; // 점 p에서 삼각형의 꼭짓점 a로의 벡터
	Vec3 b = t.b - p; // 점 p에서 삼각형의 꼭짓점 b로의 벡터
	Vec3 c = t.c - p; // 점 p에서 삼각형의 꼭짓점 c로의 벡터

	// 삼각형의 각 변에 대해 점 p를 포함하는 평면의 법선 벡터 계산
	Vec3 normPBC = b.Cross(c); // PBC의 법선 벡터 (u)
	Vec3 normPCA = c.Cross(a); // PCA의 법선 벡터 (v)
	Vec3 normPAB = a.Cross(b); // PAB의 법선 벡터 (w)

	// 법선 벡터들의 방향이 모두 같은지 확인하여 점이 삼각형 내부에 있는지 판단
	if (normPBC.Dot(normPCA) < 0.0f)
		return false; // PBC와 PCA 법선 벡터가 서로 반대 방향이면 점은 삼각형 내부에 없음

	else if (normPBC.Dot(normPAB) < 0.0f)
		return false; // PBC와 PAB 법선 벡터가 서로 반대 방향이면 점은 삼각형 내부에 없음

	return true; // 그 외의 경우 점은 삼각형 내부에 있음
}

※벡터 투영

투영은 벡터에서 다른 벡터의 선분을 구하기 위한 함수로 정사영이해서 비율을 알 수 있다.

// 벡터 a를 벡터 b에 투영하는 함수
Vec3 MathUtils::ProjectVecOnVec(Vec3 a, Vec3 b)
{
	b.Normalize(); // 벡터 b를 정규화

	float dist = a.Dot(b); // 벡터 a와 정규화된 벡터 b의 내적 계산

	return b * dist; // 투영된 벡터 반환
}

2. Intersection

충돌 검사는 삼각형을 구성하는 점으로 평면 방정식을 생성하고 이를 통해 충돌과 물리 계산을해주면 된다.

// 삼각형으로부터 평면을 생성하는 함수
Plane3D MathUtils::FromTriangle(const Triangle3D& t)
{
	Plane3D result;

	// 삼각형의 두 변의 벡터를 외적하여 평면의 법선 벡터를 계산
	result.normal = (t.b - t.a).Cross(t.c - t.a);
	result.normal.Normalize(); // 법선 벡터를 정규화

	// 평면의 거리 계산 (법선 벡터와 삼각형의 한 꼭짓점의 내적으로 계산)
	result.distance = result.normal.Dot(t.a);

	return result;
}

3. Raycast

삼각형과 레이케스팅의 충돌 검사는 위의 코드로 만들어준 평면을 통해 교차점을 찾아주면 된다. 

//삼각형 내의 점에 대한 바리센트릭 좌표를 계산
Vec3 MathUtils::Barycentric(const Point3D& p, const Triangle3D& t)
{
	return Vec3();
}

// 레이와 삼각형의 충돌 검사
bool MathUtils::Raycast(const Triangle3D& triangle, const Ray3D& ray, OUT float& distance)
{
	Plane3D plane = FromTriangle(triangle); // 삼각형으로부터 평면 생성

	float t = 0;
	// 레이와 평면의 충돌 검사
	if (!Raycast(plane, ray, OUT t))
		return false; // 충돌하지 않으면 false 반환

	// 충돌 지점 계산
	Point3D result = ray.origin + ray.direction * t;

	// 충돌 지점에 대한 바리센트릭 좌표 계산
	Vec3 barycentric = Barycentric(result, triangle);

	// 바리센트릭 좌표를 사용하여 충돌 지점이 삼각형 내부에 있는지 확인
	if (barycentric.x >= 0.0f && barycentric.x <= 1.0f &&
		barycentric.y >= 0.0f && barycentric.y <= 1.0f &&
		barycentric.z >= 0.0f && barycentric.z <= 1.0f)
	{
		distance = t; 
		return true; 
	}
	return false; // 충돌하지 않음
}

 


강의

https://inf.run/Ju6ZN

 

학습 페이지

 

www.inflearn.com

 

1.Intersection

물체들끼리의 충돌했을 때 상호작용을 해주기 위해서는 우선 충돌을 탐지해야한다. 탐지하는 것을 Intersection이라고 한다.

 

구와 구

만약 교점이 있는지 확인하려면 각 원의 원점간의 거리를 계산했을 그 크기가 각 반지름의 합과 같거나 작다면 붙어있거나 

구가 충돌이 있거나 겹쳐져있는 것이다. 이때 계산의 

bool MathUtils::SphereSphere(const Sphere3D& s1, const Sphere3D& s2)
{
    float sum = s1.radius + s2.radius;
    //제곱으로 비교
    float sqDistance = (s1.position - s2.position).LengthSquared();
    return sqDistance <= sum * sum;
}

 

구와 AABB

구와 AABB는 원과 원을 비교할 때와 비슷한 느낌으로 원과 AABB의 최근접점까지의 거리와 반지름을 비교해서 최근접점까지의 거리가 더 짧으면 충돌이 있다는 것을 알 수 있다.

bool MathUtils::SphereAABB(const Sphere3D& sphere, const AABB3D& aabb)
{
    Point3D closestPoint = ClossetPoint(aabb, sphere.position);
    float distSq = (sphere.position - closestPoint).LengthSquared();
    float radiusSq = sphere.radius * sphere.radius;
    return distsq < radiusSq;
}

 

구와 OBB

구와 obb는 AABB와 같이 최근접점과의 거리와 반지름을 비교해주면 된다.

bool MathUtils::SphereOBB(const Sphere3D& sphere, const OBB3D& obb)
{

    Point3D closestPoint = ClossetPoint(obb, sphere.position);
    float distSq = (sphere.position - closestPoint).LengthSquared();
    float radiusSq = sphere.radius * sphere.radius;
    return distsq < radiusSq;
}

 

구와Plane

구와 Plane도 동일하게 최근접점과의거리와 반지름을 비교해주면 된다.

// 구와 평면의 충돌 검사
bool MathUtils::SphereToPlane(const Sphere3D& sphere, const Plane3D& plane)
{
    Point3D closestPoint = ClosestPoint(plane, sphere.position); 
    float distSq = (sphere.position - closestPoint).LengthSquared(); 
    float radiusSq = sphere.radius * sphere.radius; 
    return distSq < radiusSq; 
}

AABB와 AABB

각 큐브의 min max를 통해 비교해준다. 사이에 있는 값이 있는지 검사해준다.

// AABB와 AABB의 충돌 검사
bool MathUtils::AABBToAABB(const AABB3D& aabb1, const AABB3D& aabb2)
{
    Point3D aMin = AABB3D::GetMin(aabb1); // aabb1의 최소 좌표
    Point3D aMax = AABB3D::GetMax(aabb1); // aabb1의 최대 좌표
    Point3D bMin = AABB3D::GetMin(aabb2); // aabb2의 최소 좌표
    Point3D bMax = AABB3D::GetMax(aabb2); // aabb2의 최대 좌표

    return (aMin.x <= bMax.x && aMax.x >= bMin.x) &&
        (aMin.y <= bMax.y && aMax.y >= bMin.y) &&
        (aMin.z <= bMax.z && aMax.z >= bMin.z); // 모든 축에 대해 겹치면 true
}

AABB와 OBB

각 선분에 대해 기준이 되는 축을 하나 정하고 프로젝션(SAT알고리)을 모든 선분에 해줬을 때 선분이 모든 축에서 겹치는 부분이 있다면 충돌이 있는 것이다. 이때 정확한 판별을 위하여 외적한 축을 바탕으로도 이 연산을 해주어야 한다.

// AABB의 주어진 축에 대한 구간을 계산
Interval3D MathUtils::GetInterval(const AABB3D& aabb, const Vec3& axis)
{
    Vec3 i = AABB3D::GetMin(aabb); // 최소 좌표
    Vec3 a = AABB3D::GetMax(aabb); // 최대 좌표

    Vec3 vertex[8] =
    {
        Vec3(i.x, a.y, a.z), Vec3(i.x, a.y, i.z), Vec3(i.x, i.y, a.z), Vec3(i.x, i.y, i.z),
        Vec3(a.x, a.y, a.z), Vec3(a.x, a.y, i.z), Vec3(a.x, i.y, a.z), Vec3(a.x, i.y, i.z),
    };

    Interval3D result; // 구간 초기화
    result.min = result.max = axis.Dot(vertex[0]); // 첫 정점을 기준으로 초기화

    for (int i = 1; i < 8; ++i)
    {
        float projection = axis.Dot(vertex[i]);
        result.min = min(result.min, projection); // 최소값 갱신
        result.max = max(result.max, projection); // 최대값 갱신
    }
    return result; // 계산된 구간 반환
}

// OBB와 주어진 축에 대한 구간(Interval) 계산
Interval3D MathUtils::GetInterval(const OBB3D& obb, const Vec3& axis)
{
    Vec3 vertex[8]; // OBB의 꼭짓점을 저장할 배열

    Vec3 C = obb.position; // OBB의 중심 위치
    Vec3 E = obb.size; // OBB의 크기(각 축에 대한 반 길이)

    vector<Vec3> A; // OBB의 축
    A.push_back(obb.orientation.Right()); // OBB의 오른쪽 방향 축
    A.push_back(obb.orientation.Up()); // OBB의 위쪽 방향 축
    A.push_back(obb.orientation.Backward()); // OBB의 뒤쪽 방향 축

    // OBB의 8개 꼭짓점 계산
    vertex[0] = C + A[0] * E.x + A[1] * E.y + A[2] * E.z;
    vertex[1] = C - A[0] * E.x + A[1] * E.y + A[2] * E.z;
    vertex[2] = C + A[0] * E.x - A[1] * E.y + A[2] * E.z;
    vertex[3] = C + A[0] * E.x + A[1] * E.y - A[2] * E.z;
    vertex[4] = C - A[0] * E.x - A[1] * E.y - A[2] * E.z;
    vertex[5] = C + A[0] * E.x - A[1] * E.y - A[2] * E.z;
    vertex[6] = C - A[0] * E.x + A[1] * E.y - A[2] * E.z;
    vertex[7] = C - A[0] * E.x - A[1] * E.y + A[2] * E.z;

    // 주어진 축에 대해 OBB의 꼭짓점들을 투영하여 최소/최대 값 계산
    Interval3D result;
    result.min = result.max = axis.Dot(vertex[0]); // 첫 꼭짓점으로 초기화

    for (int i = 1; i < 8; ++i)
    {
        float projection = axis.Dot(vertex[i]); // 꼭짓점을 축에 투영
        result.min = min(result.min, projection); // 최소값 갱신
        result.max = max(result.max, projection); // 최대값 갱신
    }
    return result; // 계산된 구간 반환
}

// AABB와 OBB가 주어진 축에 대해 겹치는지 검사
bool MathUtils::OverlapOnAxis(const AABB3D& aabb, const OBB3D& obb, const Vec3& axis)
{
    Interval3D a = GetInterval(aabb, axis); // AABB의 구간 계산
    Interval3D b = GetInterval(obb, axis); // OBB의 구간 계산
    return ((b.min <= a.max) && (a.min <= b.max)); // 구간이 겹치면 true 반환
}

// AABB와 OBB의 충돌 검사
bool MathUtils::AABBToOBB(const AABB3D& aabb, const OBB3D& obb)
{
    Vec3 test[15] = // 충돌 검사에 사용될 축
    {
        Vec3(1,0,0), // AABB 축 1
        Vec3(0,1,0), // AABB 축 2
        Vec3(0,0,1), // AABB 축 3
        obb.orientation.Right(), // OBB 축 1
        obb.orientation.Up(), // OBB 축 2
        obb.orientation.Backward(), // OBB 축 3
        // 외적으로 생성된 추가 축
    };

    // 추가 축 계산 (AABB 축과 OBB 축의 외적)
    for (int i = 0; i < 3; ++i)
    {
        test[6 + i * 3 + 0] = test[i].Cross(test[3]);
        test[6 + i * 3 + 1] = test[i].Cross(test[4]);
        test[6 + i * 3 + 2] = test[i].Cross(test[5]);
    }

    // 모든 축에 대해 겹치는지 검사
    for (int i = 0; i < 15; ++i)
    {
        if (!OverlapOnAxis(aabb, obb, test[i])) // 하나라도 겹치지 않으면 false 반환
            return false;
    }

    return true; // 모두 겹치면 true 반환
}

 

AABB와 PLANE

평명은 쭉 뻗어나가는 것으로 거리 계산을 통해 해주면 된다.

// AABB와 평면의 충돌 검사
bool MathUtils::AABBToPlane(const AABB3D& aabb, const Plane3D& plane)
{
    float pLen = aabb.size.x * fabsf(plane.normal.x) + 
        aabb.size.y * fabsf(plane.normal.y) +
        aabb.size.z * fabsf(plane.normal.z);

    float dot = plane.normal.Dot(aabb.position); // 평면의 법선과 AABB 중심의 내적
    float dist = dot - plane.distance; // 평면 상수와의 차이 계산

    return fabsf(dist) <= pLen; // 겹치는지 여부 반환
}

 

OBB와 OBB

이것도 동일하게 축을 하나 정하고 이를 모든 선분을 검사해주면 된다.

// 두 OBB가 주어진 축에 대해 겹치는지 검사
bool MathUtils::OverlapOnAxis(const OBB3D& obb1, const OBB3D& obb2, const Vec3& axis)
{
    Interval3D a = GetInterval(obb1, axis); // obb1의 구간 계산
    Interval3D b = GetInterval(obb2, axis); // obb2의 구간 계산
    return ((b.min <= a.max) && (a.min <= b.max)); // 구간이 겹치면 true 반환
}

// 두 OBB의 충돌 검사
bool MathUtils::OBBToOBB(const OBB3D& obb1, const OBB3D& obb2)
{
    Vec3 test[15] = // 충돌 검사에 사용될 축
    {
        obb1.orientation.Right(), // OBB1 축 1
        obb1.orientation.Up(), // OBB1 축 2
        obb1.orientation.Backward(), // OBB1 축 3
        obb2.orientation.Right(), // OBB2 축 1
        obb2.orientation.Up(), // OBB2 축 2
        obb2.orientation.Backward(), // OBB2 축 3
        // 외적으로 생성된 추가 축은 여기에서 계산됨
    };

    // 추가 축 계산 (OBB1 축과 OBB2 축의 외적)
    for (int i = 0; i < 3; ++i)
    {
        for (int j = 0; j < 3; ++j) {
            test[6 + i * 3 + j] = test[i].Cross(test[3 + j]);
        }
    }

    // 모든 축에 대해 겹치는지 검사
    for (int i = 0; i < 15; ++i)
    {
        if (!OverlapOnAxis(obb1, obb2, test[i])) // 하나라도 겹치지 않으면 false 반환
            return false;
    }

    return true; // 모두 겹치면 true 반환
}

 

Plane과 Plane

각 평면의 법선벡터를 외적해서 평행한지 검사하고 평행하지 않으면 충돌이 있는것이다.

// 두 평면의 충돌 검사
bool MathUtils::PlaneToPlane(const Plane3D& plane1, const Plane3D& plane2)
{
    Vec3 d = plane1.normal.Cross(plane2.normal); // 두 평면의 법선 벡터의 외적
    return d.Dot(d) != 0; // 외적의 결과가 0이 아니면 두 평면은 평행하지 않은 것으로 간주
}

 

 

2.RayCasting

Sphere

Sphere와 레이의 충돌여부 판정은 레이의 시작점과 구의 중심점까지의 거리와 반지름을 비교해서 판정한다. 

// 구와 레이의 충돌 검사
bool MathUtils::Raycast(const Sphere3D& sphere, const Ray3D& ray, OUT float& distance)
{
	Vec3 e = sphere.position - ray.origin; // 레이의 시작점에서 구의 중심까지의 벡터

	float rSq = sphere.radius * sphere.radius; 
	float eSq = e.LengthSquared(); // e 벡터의 길이의 제곱

	float a = e.Dot(ray.direction); // 레이 방향과 e 벡터의 내적

	//피타고라스 공식
	float bSq = eSq - (a * a); // b의 제곱(삼각형의 한 변의 제곱)
	float f = sqrt(rSq - bSq); // f는 삼각형의 다른 변(구의 반지름에서 b를 뺀 값)

	// 실제 충돌이 발생하지 않는 경우 -> 내적값으로 구한 반지름이랑 차이가 나면
	if (rSq - (eSq - (a * a)) < 0.0f)
		return false;

	// 레이의 시작점이 구 내부에 있는 경우
	if (eSq < rSq)
	{
		distance = a + f; // 구를 뚫고 나가는 지점까지의 거리
		return true;
	}
	// 구 외부에서 시작하여 구에 닿지 않는 경우
	distance = a - f; // 구에 가장 가까운 점까지의 거리
	return false;
}

 

AABB

 AABB와의 충돌계산은 위 사진과 같이 큐브를 위에서 봤다고 가정하고 2차원으로 생각하고 보면 평면에 대해 교차점을 찾아내면 되는데 이는 Cyrus-Beck 클리핑 알고리즘을 변형한 형태로 수행되며, tmin과 tmax를 통해 충돌이 발생하는지 판단한다. 

이 테스트는 3번에 걸쳐서 해주면 된다.

// AABB와 레이의 충돌 검사 (Cyrus-Beck clipping algorithm을 사용)
bool MathUtils::Raycast(const AABB3D& aabb, const Ray3D& ray, OUT float& distance)
{
	Vec3 min = AABB3D::GetMin(aabb); // AABB의 최소 좌표
	Vec3 max = AABB3D::GetMax(aabb); // AABB의 최대 좌표

	// 각 축에 대해 레이가 AABB의 두 평면(최소값, 최대값)과 만나는 t 값을 계산
	float t1 = (min.x - ray.origin.x) / ray.direction.x;
	float t2 = (max.x - ray.origin.x) / ray.direction.x;

	float t3 = (min.y - ray.origin.y) / ray.direction.y;
	float t4 = (max.y - ray.origin.y) / ray.direction.y;

	float t5 = (min.z - ray.origin.z) / ray.direction.z;
	float t6 = (max.z - ray.origin.z) / ray.direction.z;

	// 가장 큰 최소 t 값(tmin)과 가장 작은 최대 t 값(tmax)을 계산
	float tmin = fmaxf(fmaxf(fminf(t1, t2), fminf(t3, t4)), fminf(t5, t6));
	float tmax = fminf(fminf(fmaxf(t1, t2), fmaxf(t3, t4)), fmaxf(t5, t6));

	// tmax가 0보다 작으면 레이는 AABB의 뒤쪽을 향함
	if (tmax < 0)
		return false;

	// tmin이 tmax보다 크면 레이는 AABB를 교차하지 않음
	if (tmin > tmax)
		return false;

	// 실제 충돌 거리 계산
	distance = (tmin < 0.0f) ? tmax : tmin;
	return true;
}

Plane

평면과의 충돌 검사는 레이가 평행한지 검사하고 아니라면 교점을 찾아내는 과정을 수행해주면 된다. 

// 평면과 레이의 충돌 검사
bool MathUtils::Raycast(const Plane3D& plane, const Ray3D& ray, OUT float& distance)
{
	float nd = ray.direction.Dot(plane.normal); // 레이 방향과 평면의 법선의 내적
	float pn = ray.origin.Dot(plane.normal); // 레이의 시작점과 평면의 법선의 내적

	// nd가 0보다 크거나 같으면 레이와 평면은 평행하거나 레이가 평면에서 멀어짐
	if (nd >= 0.0f)
		return false;

	// 실제 충돌 거리 계산
	float t = (plane.distance - pn) / nd;
	if (t >= 0.0f) {
		distance = t; // 충돌 지점까지의 거리
		return true;
	}

	return false;
}

 

 

 

1. 기본도형

오늘은 3d 기본 도형에 대해 알아보고 구현해보자 

알아볼 기본도형은 점, 선, 구등이 있다.

점은 말그대로 하나의 좌표이다.

 

선은 시작과 끝이 있는 선분이다. 

 

레이는 시작점이 있으면서 한쪽 방향으로 쭉 뻗어나가는 광선이다.

 

구는 중심점과 반지름을 가진 도형으로 중심에서 반지름까지의 모든 점을 합쳐주면 하나의 구가 되는 것이다.

 

AABB는 x,y,z축이 정렬되어(평행한) 만들어진 Box모양이다. 

 

OBB는 x,y,z축이 정렬되어 있지않은 채 만들어 Box모양이다. 

 

Plane은 평면으로 만들 때 점3개를 이용하여 만들거나 노멀벡터와 한점이나 노멀벡터와 평면과 원점까지의 거리를 활용 하여 만들어줄 수 있다.

 

Triangle은 3개의 점으로 이루어진 도형으로 이때 Union이라는 것을 사용해볼 것인데 이는 자료형이 3개의 각기 다른 자료형으로 표현가능하다는 것이다.

 

Primitive3D.h

#pragma once

// *************
// Point3D
// *************
using Point3D = Vec3;

// *************
// Line3D
// *************

struct Line3D
{
	Point3D start = Point3D(0.f);
	Point3D end = Point3D(0.f);

	float Length() { return Vec3::Distance(start, end); }
	//제곱급
	float LengthSq() { return Vec3::DistanceSquared(start, end); }
};

// *************
// Ray3D
// *************

struct Ray3D
{
	Point3D origin = Point3D(0.f);
	Vec3 direction = Vec3(0.f);

	void NormalizeDirection() { direction.Normalize(); }

	//Ray3D생성
	static Ray3D FromPoints(const Point3D& from, const Point3D& to) { return Ray3D{ from, to - from }; }
};

// *************
// Sphere3D
// *************

struct Sphere3D
{
	Point3D position;
	float radius;
};

// *************
// AABB
// *************

struct AABB3D
{
	Point3D position = Vec3(0.f);
	Vec3 size = Vec3(1.f, 1.f, 1.f);

	//최소점
	static Vec3 GetMin(const AABB3D& aabb)
	{
		Vec3 p1 = aabb.position + aabb.size;
		Vec3 p2 = aabb.position - aabb.size;
		return Vec3(fminf(p1.x, p2.x), fminf(p1.y, p2.y), fminf(p1.z, p2.z));
	}

	//최대점
	static Vec3 GetMax(const AABB3D& aabb)
	{
		Vec3 p1 = aabb.position + aabb.size;
		Vec3 p2 = aabb.position - aabb.size;
		return Vec3(fmaxf(p1.x, p2.x), fmaxf(p1.y, p2.y), fmaxf(p1.z, p2.z));
	}

	//최소 최대주면 AABB 생성해주는
	static AABB3D FromMinMax(const Vec3& min, const Vec3& max)
	{
		return AABB3D((min + max) / 2, (max - min) / 2);
	}
};

// *****************
// OBB
// *****************

struct OBB3D
{
	Point3D position;
	Vec3 size;
	Matrix orientation;
	// Vec4 quaternion;
	// Vec3 rotation;
};

// *****************
// Plane3D
// *****************

// 삼각형 (정점3개)
// 노멀 + 정점 1개
// 노멀 + 거리
struct Plane3D
{
	Vec3 normal;
	float distance;
};

// *****************
// Triangle3D
// *****************

struct Triangle3D
{
	//3개모두 사용할 수 있다.
	union
	{
		struct
		{
			Point3D a;
			Point3D b;
			Point3D c;
		};
		Point3D points[3];
		float values[9];
	};
};

 

 

2.Point Test

이제 만약 어떤 점이 있다면 그것이 도형에 포함이 되어있는지 확인하는 이론에 대해 배워보고 코드로 구현해보자. 

먼저 구에 대해 Point Test를 해보자. 중심점과의 거리를 통해 판별할 수 있다.

bool MathUtils::PointInSphere(const Point3D& point, const Sphere3D& sphere) {
    float magSq = (point - sphere.position).LengthSquared();
    float radSq = sphere.radius * sphere.radius;
    return magSq <= radSq;
}

Point3D MathUtils::ClossetPoint(const Sphere3D& sphere, const Point3D& point) {
    Vec3 sphereToPointDir = (point - sphere.position);
    sphereToPointDir.Normalize();
    return sphere.position + sphereToPointDir * sphere.radius;
}

AABB도 안에 포함되어있는지 표면에 있는거에서 가장 가까운 점이 어느것인지 찾아보자. 이때 위에서 만들어준 min과 max값을 활용하여 검사해줄 수 있다.

bool MathUtils::PointInAABB(const Point3D& point, const AABB3D& aabb) {
    Point3D min = AABB3D::GetMin(aabb);
    Point3D max = AABB3D::GetMax(aabb);

    if (point.x < min.x || point.y < min.y || point.z < min.z)
        return false;
    if (point.x > max.x || point.y > max.y || point.z > max.z)
        return false;

    return true;
}

Point3D MathUtils::ClossetPoint(const AABB3D& aabb, const Point3D& point) {
    Point3D result = point;
    Point3D minPt = AABB3D::GetMin(aabb);
    Point3D maxPt = AABB3D::GetMax(aabb);

    result.x = max(result.x, minPt.x);
    result.y = max(result.y, minPt.y);
    result.z = max(result.z, minPt.z);

    result.x = min(result.x, minPt.x);
    result.y = min(result.y, minPt.y);
    result.z = min(result.z, minPt.z);

    return result;
}

OBB도 동일하게 검사해주는데 이때 AABB로 변환해준 다음에 테스트를 해줄 수 도 있고 아니면 매축에 프로젝션 연산을 해주면 된다.

bool MathUtils::PointInOBB(const Point3D& point, const OBB3D& obb) {
    Vec3 dir = point - obb.position;

    vector<Vec3> axis = { obb.orientation.Right(), obb.orientation.Up(), obb.orientation.Backward() };
    vector<float> size = { obb.size.x, obb.size.y, obb.size.z };

    for (int i = 0; i < 3; i++) {
        float distance = dir.Dot(axis[i]);

        if (distance > size[i] || distance < -size[i])
            return false;
    }

    return true;
}

Point3D MathUtils::ClossetPoint(const OBB3D& obb, const Point3D& point) {
    Vec3 dir = point - obb.position;
    Point3D result;
    vector<Vec3> axis = { obb.orientation.Right(), obb.orientation.Up(), obb.orientation.Backward() };
    vector<float> size = { obb.size.x, obb.size.y, obb.size.z };

    for (int i = 0; i < 3; i++) {
        float distance = dir.Dot(axis[i]);

        distance = clamp(distance, -size[i], size[i]);
        result = result + (axis[i] * distance);
    }

    return obb.position + result;
}

Plane은 위에서 구해준 방법대로 점과 노멀벡터를 내적해주는 것으로 Distance를 구할 수 있는데 이 값을 검사해주면 된다.

bool MathUtils::PointInPlane(const Point3D& point, const Plane3D& plane) {
    float dot = point.Dot(plane.normal);
    return fabs(dot - plane.distance) < FLT_EPSILON;
}

Point3D MathUtils::ClossetPoint(const Plane3D& plane, const Point3D& point) {
    float dot = point.Dot(plane.normal);
    float distance = dot - plane.distance;
    return point - plane.normal * distance;
}

 

Line은 내적을 통해서 구해줄텐데 내적했을 때 거리가 0인지 검사하는 것으로 내부에 있는지 파악할 수 있다. 가장 가까운 점은 내적을 통한 크기와 기존 크기를 통해 비율을 구해서 활용해주면 된다.

bool MathUtils::PointInLine(const Point3D& point, const Line3D& line) {
    Point3D closest = ClossetPoint(line, point);
    return (closest - point).LengthSquared() == 0.f;
}

Point3D MathUtils::ClossetPoint(const Line3D& line, const Point3D& point) {
    Vec3 lineVec = line.end - line.start;
    float t = (point - line.start).Dot(lineVec) / lineVec.Dot(lineVec);
    t = clamp(t, 0.0f, 1.0f);
    return line.start + lineVec * t;
}

 

Ray는 내적을 통해 구해줄텐데 수직을 내렸을 때의 좌표를 구해주면 되는 것이기 때문에 방향벡터와 정점의 좌표 - 시작점을 내적해주고 이를 방향벡터에 곱해주면 위치를 구해줄 수 있다. 그리고 내부에 있는지 확인하려면 시작좌표와 비교하고 아니라면 시작좌표와의 방향벡터와 기존 레이의 방향벡터가 내적했을 때 1이면 같은것이기 때문에 내부에 있는것으로 판단해주면 된다.

bool MathUtils::PointInRay(const Point3D& point, const Ray3D& ray) {
    if (point == ray.origin)
        return true;
    Vec3 norm = point - ray.origin;
    norm.Normalize();
    return fabs(norm.Dot(ray.direction) - 1.0f) < FLT_EPSILON;
}

Point3D MathUtils::ClossetPoint(const Ray3D& ray, const Point3D& point) {
    float t = (point - ray.origin).Dot(ray.direction);
    t = fmaxf(t, 0.0f);
    return ray.origin + ray.direction * t;
}


지금까지는 물체간의 충돌만을 구현해보았다. 만약 물체가 아니라 땅을 설치하고 이것과의 충돌을 어떤식으로 구현할지가 문제이다. 

땅은 그리드 방식으로 만들어주자.

CollisionDemo.cpp

//Terrain
{
	auto obj = make_shared<GameObject>();
	obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
	obj->AddComponent(make_shared<MeshRenderer>());
	{
		auto mesh = make_shared<Mesh>();
		mesh->CreateGrid(10, 10);
		obj->GetMeshRenderer()->SetMesh(mesh);
		obj->GetMeshRenderer()->SetPass(0);
	}
	{
		obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
	}
}

 

이제 이땅에 어떻게 Collider를 넣어줄지 생각해보자. 기존에 넣어줬던대로 큰 Collider를 그냥 넣어주면 언덕과 같은 부분과 같이 높이가 반영되어있는 땅에는 제대로 적용이 되지않을 것이다. 그렇기 때문에 이 땅을 삼각형 단위로 좌표를 연산해서 이것을 이용해서 세분화된 상태로 충돌이 처리될 수 있도록 해야한다.

 

이런기능을 만들어주기 위하여 Terrain클래스 컴포넌트로 만들어서 관리할 수 있도록 하자.

Terrain.h

#pragma once
#include "Component.h"

class Terrain : public Component
{
	using Super = Component;

public:
	Terrain();
	~Terrain();

	void Create(int32 sizeX, int32 sizeZ, shared_ptr<Material> material);

	int32 GetSizeX() { return _sizeX; }
	int32 GetSizeZ() { return _sizeZ; }

	//bool Pick(int32 screenX, int32 screenY, Vec3& pickPos, float& distance);

private:
	shared_ptr<Mesh> _mesh;
	int32 _sizeX = 0;
	int32 _sizeZ = 0;
};

Terrain.cpp

#include "pch.h"
#include "Terrain.h"
#include "MeshRenderer.h"
#include "Camera.h"

Terrain::Terrain() : Super(ComponentType::Terrain)
{

}

Terrain::~Terrain()
{

}

void Terrain::Create(int32 sizeX, int32 sizeZ, shared_ptr<Material> material)
{
	_sizeX = sizeX;
	_sizeZ = sizeZ;

	//Weak포인터라서 변환필요
	auto go = _gameObject.lock();

	go->GetOrAddTransform();

	if (go->GetMeshRenderer() == nullptr)
		go->AddComponent(make_shared<MeshRenderer>());

	_mesh = make_shared<Mesh>();
	_mesh->CreateGrid(sizeX, sizeZ);

	go->GetMeshRenderer()->SetMesh(_mesh);
	go->GetMeshRenderer()->SetPass(0);
	go->GetMeshRenderer()->SetMaterial(material);
}

 

사용은 다음과 같이 하면된다.

CollisionDemo.cpp

	//Terrain
	{
		auto obj = make_shared<GameObject>();
		obj->AddComponent(make_shared<Terrain>());
		obj->GetTerrain()->Create(10, 10, RESOURCES->Get<Material>(L"Veigar"));

		CUR_SCENE->Add(obj);
	}

 

이제 피킹기능을 넣어주자. 중요한점은 사각형을 구성하는 2개의 삼각형을 기준으로 피킹검사를 하고 이를 모든 사각형에 반복하면 된다.

Terrain.h

bool Terrain::Pick(int32 screenX, int32 screenY, Vec3& pickPos, float& distance)
{
	Matrix W = GetTransform()->GetWorldMatrix();
	Matrix V = Camera::S_MatView;
	Matrix P = Camera::S_MatProjection;

	Viewport& vp = GRAPHICS->GetViewport();

	//near far
	Vec3 n = vp.Unproject(Vec3(screenX, screenY, 0), W, V, P);
	Vec3 f = vp.Unproject(Vec3(screenX, screenY, 1), W, V, P);

	Vec3 start = n;
	Vec3 direction = f - n;
	direction.Normalize();

	Ray ray = Ray(start, direction);

	
	const auto& vertices = _mesh->GetGeometry()->GetVertices();

	for (int32 z = 0; z < _sizeZ; z++)
	{
		for (int32 x = 0; x < _sizeX; x++)
		{
			uint32 index[4];
			//사각형 각 점
			index[0] = (_sizeX + 1) * z + x;
			index[1] = (_sizeX + 1) * z + x + 1;
			index[2] = (_sizeX + 1) * (z + 1) + x;
			index[3] = (_sizeX + 1) * (z + 1) + x + 1;

			Vec3 p[4];
			for (int32 i = 0; i < 4; i++)
				p[i] = vertices[index[i]].position;

			//  [2]
			//   |	\
			//  [0] - [1]
			//삼각형 테스트
			if (ray.Intersects(p[0], p[1], p[2], OUT distance))
			{
				pickPos = ray.position + ray.direction * distance;
				return true;
			}

			//  [2] - [3]
			//   	\  |
			//		  [1]
			if (ray.Intersects(p[3], p[1], p[2], OUT distance))
			{
				pickPos = ray.position + ray.direction * distance;
				return true;
			}
		}
	}

	return false;
}

 

이렇게 해주고 Scene에서 Picking을 검사하는 부분에 이 Pick함수를 실행하도록 만들어주자.

Scene.cpp

shared_ptr<class GameObject> Scene::Pick(int32 screenX, int32 screenY)
{
	shared_ptr<Camera> camera = GetCamera()->GetCamera();

	float width = GRAPHICS->GetViewport().GetWidth();
	float height = GRAPHICS->GetViewport().GetHeight();
	

	Matrix projectionMatrix = camera->GetProjectionMatrix();

	//스크린 ->뷰포트공식
	float viewX = (+2.0f * screenX / width - 1.0f) / projectionMatrix(0, 0);
	float viewY = (-2.0f * screenY / height + 1.0f) / projectionMatrix(1, 1);

	Matrix viewMatrix = camera->GetViewMatrix();
	Matrix viewMatrixInv = viewMatrix.Invert();

	const auto& gameObjects = GetObjects();

	float minDistance = FLT_MAX;
	shared_ptr<GameObject> picked;

	for (auto& gameObject : gameObjects)
	{
		if (gameObject->GetCollider() == nullptr)
			continue;

		// ViewSpace에서의 Ray 정의 ViewSpace 0 0 0 -> 카메라
		Vec4 rayOrigin = Vec4(0.0f, 0.0f, 0.0f, 1.0f);
		Vec4 rayDir = Vec4(viewX, viewY, 1.0f, 0.0f);

		// WorldSpace에서의 Ray 정의
		Vec3 worldRayOrigin = XMVector3TransformCoord(rayOrigin, viewMatrixInv);		//위치까지
		Vec3 worldRayDir = XMVector3TransformNormal(rayDir, viewMatrixInv);				//위치는 그대로 방향만
		worldRayDir.Normalize();

		// WorldSpace에서 연산
		Ray ray = Ray(worldRayOrigin, worldRayDir);

		float distance = 0.f;
		if (gameObject->GetCollider()->Intersects(ray, OUT distance) == false)
			continue;

		if (distance < minDistance)
		{
			minDistance = distance;
			picked = gameObject;
		}
	}

	for (auto& gameObjcect : gameObjects)
	{
		if (gameObjcect->GetTerrain() == nullptr)
			continue;

		Vec3 pickPos;
		float distance = 0.f;
		if (gameObjcect->GetTerrain()->Pick(screenX, screenY, OUT pickPos, OUT distance) == false)
			continue;

		if (distance < minDistance)
		{
			minDistance = distance;
			picked = gameObjcect;

		}
	}

	return picked;
}

 

이렇게 해준다음 실행해보면 땅을 클릭했을 때 잘 없어지는 것을 볼 수 있다.

 

만약 땅이 아니라 복잡한 매쉬와 충돌이나 레이케스팅을 한다면 이런 삼각형으로 나눠서 하는방법을 응용할 수 있을 것이다. 하지만 이렇게하면 부하가 엄청나다. 그렇기 때문에 간단한 도형으로 된 충돌 판정용 매쉬나 Collider 가지고 충돌을 판별해주는게 나을 수 있다.

조금 더 효율적으로 하려면 레이어를 구분해서 레이어끼리 공식을 만들어서 작동시켜주면 된다.



 

이제 BoxCollider를 만들어보자. Cube Collider는 OBB와 AABB로 나눠진다. 

실제 Unity 상에서 Cube를 만들어보면 Box collider가 Component로 붙어있는채로 나온다.

이때 AABB는 이 Box Collider가 월드 좌표의 축과 동일한 경우이다. 이렇게 하는 경우 연산이 쉽다

OBB는 이와 반대로 축이 일치하지 않는 경우라고 볼 수 있다.

성능을 우선으로 할지 미세한 판정을  우선으로 할지에 따라 사용하는 방식이 다르다. 만약 비스듬하게 있는 물체가 

있다고 했을 때, 이 물체에 Box Collider를 씌울 때, 정말 물체에 맞게 Collider영역을 만들면 물체에 충돌 판정을 잘할 수 있겠지만 성능이 떨어질 것이다. 만약 축에 맞게 영역을 맞춰주면 성능은 좋아지겠지만 판정에 문제가 생길 수도 있다.

 

이론상으로는 이렇게 나뉘지만, 보통 오브젝트가 회전이나 변형이 많이 이루어지기 때문에 일반적으로 많이 쓰이는 것은 

OBB라고 보면 된다.

지금은 공부하는 차원에서 2가지 다 만들고 통합해주는 방식으로 만들어주자.

우선 레이케스팅하는 부분은 이전에 만들어준 Sphere부분과 동일하게 만들어주면 된다.

AABBoxCollider.h

#pragma once
#include "BaseCollider.h"


class AABBBoxCollider : public BaseCollider
{
public:
	AABBBoxCollider();
	virtual ~AABBBoxCollider();

	virtual void Update() override;
	virtual bool Intersects(Ray& ray, OUT float& distance) override;

	BoundingBox& GetBoundingBox() { return _boundingBox; }

private:
	BoundingBox _boundingBox;
};

 

AABoxCollider.cpp

#include "pch.h"
#include "AABBBoxCollider.h"

AABBBoxCollider::AABBBoxCollider() : BaseCollider(ColliderType::AABB)
{

}

AABBBoxCollider::~AABBBoxCollider()
{

}

void AABBBoxCollider::Update()
{

}

bool AABBBoxCollider::Intersects(Ray& ray, OUT float& distance)
{
	return _boundingBox.Intersects(ray.position, ray.direction, OUT distance);
}

 

OBBBoxCollider.h

#pragma once
#include "BaseCollider.h"

class OBBBoxCollider : public BaseCollider
{
public:
	OBBBoxCollider();
	virtual ~OBBBoxCollider();

	virtual void Update() override;
	virtual bool Intersects(Ray& ray, OUT float& distance) override;

	BoundingOrientedBox& GetBoundingBox() { return _boundingBox; }

private:
	BoundingOrientedBox _boundingBox;
};

OBBBoxCollider.cpp

#include "pch.h"
#include "OBBBoxCollider.h"

OBBBoxCollider::OBBBoxCollider() : BaseCollider(ColliderType::OBB)
{

}

OBBBoxCollider::~OBBBoxCollider()
{

}

void OBBBoxCollider::Update()
{

}

bool OBBBoxCollider::Intersects(Ray& ray, OUT float& distance)
{
	return _boundingBox.Intersects(ray.position, ray.direction, OUT distance);
}

 

이렇게 해주고 Collider를 테스트하도록 메인코드에 새로운 오브젝트를 하나생성하고 테스트해보자 테스트해보기 위해 

오브젝트가 움직이는 코드를 넣어주자.

CollisionDemo.cpp

#include "pch.h"
#include "CollisionDemo.h"
#include "RawBuffer.h"
#include "TextureBuffer.h"
#include "Material.h"
#include "SceneDemo.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "Graphics.h"
#include "SphereCollider.h"
#include "Scene.h"
#include "AABBBoxCollider.h"
#include "OBBBoxCollider.h"

void CollisionDemo::Init()
{
	_shader = make_shared<Shader>(L"23. RenderDemo.fx");

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		CUR_SCENE->Add(camera);
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
		material->SetDiffuseMap(texture);
		MaterialDesc& desc = material->GetMaterialDesc();
		desc.ambient = Vec4(1.f);
		desc.diffuse = Vec4(1.f);
		desc.specular = Vec4(1.f);
		RESOURCES->Add(L"Veigar", material);
	}

	// Mesh
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(3.f, 0.f, 0.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}
		//Collider
		{
			auto collider = make_shared<SphereCollider>();
			collider->SetRadius(0.5f);
			obj->AddComponent(collider);
		}
		{
			obj->AddComponent(make_shared<MoveScript>());
		}

		CUR_SCENE->Add(obj);
	}

	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Cube");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}
		//Collider
		{
			auto collider = make_shared<AABBBoxCollider>();
			collider->GetBoundingBox().Extents = Vec3(0.5f);
			obj->AddComponent(collider);
		}

		CUR_SCENE->Add(obj);
	}
}

void CollisionDemo::Update()
{
	if (INPUT->GetButtonDown(KEY_TYPE::LBUTTON))
	{
		int32 mouseX = INPUT->GetMousePos().x;
		int32 mouseY = INPUT->GetMousePos().y;

		//Picking
		auto pickObj = CUR_SCENE->Pick(mouseX, mouseY);
		if (pickObj)
		{
			CUR_SCENE->Remove(pickObj);
		}
	}
}

void CollisionDemo::Render()
{

}

void MoveScript::Update()
{
	auto pos = GetTransform()->GetPosition();
	pos.x -= DT * 1.0f;
	GetTransform()->SetPosition(pos);
}

 

이렇게 해주면 둘다 클릭하면 없어지는 것을 볼 수 있다.

 

OBB를 사용해서도 테스트를 해보자 이때 큐브를 살짝 돌려주고. OBB에 회전값을 전달해주어야하는데 이때 잠깐보자면 BoundingOrientedBox에서 Orientation으로 물체의 회전을 관리해주고 있는데 이는 쿼터니언 값으로 Float4값을 가지고있다. 이를 쿼터니언 값으로 전달해주어야 한다.

CollisionDemo.cpp

#include "pch.h"
#include "CollisionDemo.h"
#include "RawBuffer.h"
#include "TextureBuffer.h"
#include "Material.h"
#include "SceneDemo.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "Graphics.h"
#include "SphereCollider.h"
#include "Scene.h"
#include "AABBBoxCollider.h"
#include "OBBBoxCollider.h"

void CollisionDemo::Init()
{
	_shader = make_shared<Shader>(L"23. RenderDemo.fx");

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		CUR_SCENE->Add(camera);
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
		material->SetDiffuseMap(texture);
		MaterialDesc& desc = material->GetMaterialDesc();
		desc.ambient = Vec4(1.f);
		desc.diffuse = Vec4(1.f);
		desc.specular = Vec4(1.f);
		RESOURCES->Add(L"Veigar", material);
	}

	// Mesh
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(3.f, 0.f, 0.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}
		//Collider
		{
			auto collider = make_shared<SphereCollider>();
			collider->SetRadius(0.5f);
			obj->AddComponent(collider);
		}
		{
			obj->AddComponent(make_shared<MoveScript>());
		}

		CUR_SCENE->Add(obj);
	}

	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Cube");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}
		//Collider
		/*{
			auto collider = make_shared<AABBBoxCollider>();
			collider->GetBoundingBox().Extents = Vec3(0.5f);
			obj->AddComponent(collider);
		}*/
		{
		obj->GetOrAddTransform()->SetRotation(Vec3(0, 45, 0));

		auto collider = make_shared<OBBBoxCollider>();
		collider->GetBoundingBox().Extents = Vec3(0.5f);
		collider->GetBoundingBox().Orientation = Quaternion::CreateFromYawPitchRoll(45, 0, 0);
		obj->AddComponent(collider);
		}

		CUR_SCENE->Add(obj);
	}
}

void CollisionDemo::Update()
{
	if (INPUT->GetButtonDown(KEY_TYPE::LBUTTON))
	{
		int32 mouseX = INPUT->GetMousePos().x;
		int32 mouseY = INPUT->GetMousePos().y;

		//Picking
		auto pickObj = CUR_SCENE->Pick(mouseX, mouseY);
		if (pickObj)
		{
			CUR_SCENE->Remove(pickObj);
		}
	}
}

void CollisionDemo::Render()
{

}

void MoveScript::Update()
{
	auto pos = GetTransform()->GetPosition();
	pos.x -= DT * 1.0f;
	GetTransform()->SetPosition(pos);
}

 

이렇게 되어있으면 회전되어있는 큐브와 구 클릭했을 때 잘 없어진다.

 

이제 여기에 Collider끼리 충돌했을 때를 이벤트를 구현해보자

SphereCollider.cpp

bool SphereCollider::Intersects(shared_ptr<BaseCollider>& other)
{
	ColliderType type = other->GetColliderType();

	//어떤 Collider인지 판별
	switch (type)
	{
	case ColliderType::Sphere:
		return _boundingSphere.Intersects(dynamic_pointer_cast<SphereCollider>(other)->GetBoundingSphere());
	case ColliderType::AABB:
		return _boundingSphere.Intersects(dynamic_pointer_cast<AABBBoxCollider>(other)->GetBoundingBox());
	case ColliderType::OBB:
		return _boundingSphere.Intersects(dynamic_pointer_cast<OBBBoxCollider>(other)->GetBoundingBox());
	}

	return false;
}

 

AABBBoxCollider.cpp

bool AABBBoxCollider::Intersects(shared_ptr<BaseCollider>& other)
{
	ColliderType type = other->GetColliderType();

	switch (type)
	{
	case ColliderType::Sphere:
		return _boundingBox.Intersects(dynamic_pointer_cast<SphereCollider>(other)->GetBoundingSphere());
	case ColliderType::AABB:
		return _boundingBox.Intersects(dynamic_pointer_cast<AABBBoxCollider>(other)->GetBoundingBox());
	case ColliderType::OBB:
		return _boundingBox.Intersects(dynamic_pointer_cast<OBBBoxCollider>(other)->GetBoundingBox());
	}
}

OBBBoxCollider.cpp

bool OBBBoxCollider::Intersects(shared_ptr<BaseCollider>& other)
{
	ColliderType type = other->GetColliderType();

	switch (type)
	{
	case ColliderType::Sphere:
		return _boundingBox.Intersects(dynamic_pointer_cast<SphereCollider>(other)->GetBoundingSphere());
	case ColliderType::AABB:
		return _boundingBox.Intersects(dynamic_pointer_cast<AABBBoxCollider>(other)->GetBoundingBox());
	case ColliderType::OBB:
		return _boundingBox.Intersects(dynamic_pointer_cast<OBBBoxCollider>(other)->GetBoundingBox());
	}

	return false;
}

 

이렇게 해주고 이제 충돌검사를 하는 코드를 Scene클래스에 넣어줘서 물체들의 상태가 Update된 이후인 LateUpdate에서 충돌체크를 해주자.

Scene.cpp

void Scene::CheckCollision()
{
	vector<shared_ptr<BaseCollider>> colliders;

	//Collider 가진애들 모두 찾아주기
	for (shared_ptr<GameObject> object : _objects)
	{
		if (object->GetCollider() == nullptr)
			continue;

		colliders.push_back(object->GetCollider());
	}

	// BruteForce
	for (int32 i = 0; i < colliders.size(); i++)
	{
		for (int32 j = i + 1; j < colliders.size(); j++)
		{
			shared_ptr<BaseCollider>& other = colliders[j];
			//만약 i번째랑 j번째랑 충돌했다면
			if (colliders[i]->Intersects(other))
			{
				int a = 3;
			}
		}
	}
}

 

이렇게 해주고 Break Point를 int a=3에 두면  충돌했을 때 Break가 걸리는 것을 볼 수 있다.


강의

https://inf.run/LBTDz

 

학습 페이지

 

www.inflearn.com

 

오늘은 충돌에 대해 알아보자. 보통 충돌은 Mesh가 아니라 Collider를 통해 이루어진다. 만약 Mesh로 충돌 연산을 하게 된다면 수많은 삼각형에 대한 연산을 해야하기 때문에 연산량을 줄이기 위하여 간략한 기본 도형으로 이루어진 Collider를 통해 연산을 한다.

 

Collider 클래스는 Base Collider라는 클래스를 만들어서 상속받게하자. 지금은 Box, Sphere형태의 Collider만 만들어주도록 하자. Box형 Collider에는 OBB랑 AABB 두가지 방식이 있는데 두가지 다 만들어보자.

 

우선 BaseCollider에서 모든 Collider가 가지고 있어야하는 기능을 넣어주자. 이 기능에는 Get함수와 기본적인 Collider 종류, 레이케스팅을 통해 Collider를  관통했는지 와 다른 물체들 끼리 충돌이 일어났는 지 체크하는 기능이 필요하다.

레이케스팅은 Math라이브러리 이용하여 구현해주면 된다. 앞으로 갈 수 있다거나 땅에 붙어있는 상태를 관찰할 때는 충돌체를 사용하는 것도 방법이지만 레이케스팅을 사용해주는 게 더 좋을 수도 있다. 

BaseCollider.h

#pragma once
#include "Component.h"

enum class ColliderType
{
	Sphere,
	AABB,
	OBB,
};

class BaseCollider : public Component
{
public:
	BaseCollider(ColliderType colliderType);
	virtual ~BaseCollider();

	//레이케스팅 OUT은 명시용 기능 x 
	virtual bool Intersects(Ray& ray, OUT float& distance) = 0;
	ColliderType GetColliderType() { return _colliderType; }

protected:
	ColliderType _colliderType;
};

BaseCollider.cpp

#include "pch.h"
#include "BaseCollider.h"

BaseCollider::BaseCollider(ColliderType colliderType)
	: Component(ComponentType::Collider), _colliderType(colliderType)
{

}

BaseCollider::~BaseCollider()
{

 

기능을 완성한 다음에 ComponentType과 GameObject에서 가져오는 부분을 추가해주자.

GameObject.cpp

shared_ptr<BaseCollider> GameObject::GetCollider()
{
	shared_ptr<Component> component = GetFixedComponent(ComponentType::Collider);
	return static_pointer_cast<BaseCollider>(component);
}

이를 바탕으로Sphere Collider를 만들어주자. Sphere 모양은 BoudingSphere 기능을 사용해주자.이 BoudingSphere는

중심점 위치와 반지름으로 구성되어있고 충돌 및 레이케스팅 관련 기능들을 지원해준다. 이 BoudingSpehre의  세부옵션을 설정할 수 있게 Get함수를 만들어주자.

SphereCollider.h

#pragma once
#include "BaseCollider.h"

class SphereCollider : public BaseCollider
{
public:
	SphereCollider();
	virtual ~SphereCollider();

	virtual void Update() override;
	virtual bool Intersects(Ray& ray, OUT float& distance) override;

	void SetRadius(float radius) { _radius = radius; }
	BoundingSphere& GetBoundingSphere() { return _boundingSphere; }

private:
	float _radius = 1.f;
	//충돌용 구
	BoundingSphere _boundingSphere;
};

SphereCollider.cpp

#include "pch.h"
#include "SphereCollider.h"

SphereCollider::SphereCollider()
	: BaseCollider(ColliderType::Sphere)
{

}

SphereCollider::~SphereCollider()
{

}

void SphereCollider::Update()
{
	_boundingSphere.Center = GetGameObject()->GetTransform()->GetPosition();

	Vec3 scale = GetGameObject()->GetTransform()->GetScale();
	//스캐일중에 제일큰애따라서 반지름 증가하도록
	_boundingSphere.Radius = _radius * max(max(scale.x, scale.y), scale.z);
}

bool SphereCollider::Intersects(Ray& ray, OUT float& distance)
{
	return _boundingSphere.Intersects(ray.position, ray.direction, OUT distance);
}

 

이제 메인함수에서 Sphere Collider를 만들어서 Component로 넣어주자. 

#include "pch.h"
#include "CollisionDemo.h"
#include "RawBuffer.h"
#include "TextureBuffer.h"
#include "Material.h"
#include "SceneDemo.h"
#include "GeometryHelper.h"
#include "Camera.h"
#include "GameObject.h"
#include "CameraScript.h"
#include "MeshRenderer.h"
#include "Mesh.h"
#include "Material.h"
#include "Model.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Mesh.h"
#include "Transform.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "Light.h"
#include "Graphics.h"
#include "SphereCollider.h"

void CollisionDemo::Init()
{
	_shader = make_shared<Shader>(L"23. RenderDemo.fx");

	// Camera
	{
		auto camera = make_shared<GameObject>();
		camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<CameraScript>());
		CUR_SCENE->Add(camera);
	}

	// Light
	{
		auto light = make_shared<GameObject>();
		light->AddComponent(make_shared<Light>());
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.1f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		light->GetLight()->SetLightDesc(lightDesc);
		CUR_SCENE->Add(light);
	}

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
		material->SetDiffuseMap(texture);
		MaterialDesc& desc = material->GetMaterialDesc();
		desc.ambient = Vec4(1.f);
		desc.diffuse = Vec4(1.f);
		desc.specular = Vec4(1.f);
		RESOURCES->Add(L"Veigar", material);
	}

	// Mesh
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			obj->GetMeshRenderer()->SetPass(0);
		}
		//Collider
		{
			auto collider = make_shared<SphereCollider>();
			collider->SetRadius(0.5f);
			obj->AddComponent(collider);
		}

		CUR_SCENE->Add(obj);
	}
}

void CollisionDemo::Update()
{
	if (INPUT->GetButtonDown(KEY_TYPE::LBUTTON))
	{
		int32 mouseX = INPUT->GetMousePos().x;
		int32 mouseY = INPUT->GetMousePos().y;

		//Picking
	}
}

void CollisionDemo::Render()
{

}

그리고 테스트하기 위해 피킹 코드를 만들어주자. 피킹함수는 Scene 클래스에서 만들어주자. 

피킹함수에서는 스크린 좌표에서 클릭한 부분을 3D좌표로 바꿔주어야한다.

Scene.cpp

shared_ptr<class GameObject> Scene::Pick(int32 screenX, int32 screenY)
{
	shared_ptr<Camera> camera = GetCamera()->GetCamera();

	float width = GRAPHICS->GetViewport().GetWidth();
	float height = GRAPHICS->GetViewport().GetHeight();
	

	Matrix projectionMatrix = camera->GetProjectionMatrix();

	//스크린 ->뷰포트공식
	float viewX = (+2.0f * screenX / width - 1.0f) / projectionMatrix(0, 0);
	float viewY = (-2.0f * screenY / height + 1.0f) / projectionMatrix(1, 1);

	Matrix viewMatrix = camera->GetViewMatrix();
	Matrix viewMatrixInv = viewMatrix.Invert();

	const auto& gameObjects = GetObjects();

	float minDistance = FLT_MAX;
	shared_ptr<GameObject> picked;

	for (auto& gameObject : gameObjects)
	{
		if (gameObject->GetCollider() == nullptr)
			continue;

		// ViewSpace에서의 Ray 정의 ViewSpace 0 0 0 -> 카메라
		Vec4 rayOrigin = Vec4(0.0f, 0.0f, 0.0f, 1.0f);
		Vec4 rayDir = Vec4(viewX, viewY, 1.0f, 0.0f);

		// WorldSpace에서의 Ray 정의
		Vec3 worldRayOrigin = XMVector3TransformCoord(rayOrigin, viewMatrixInv);		//위치까지
		Vec3 worldRayDir = XMVector3TransformNormal(rayDir, viewMatrixInv);				//위치는 그대로 방향만
		worldRayDir.Normalize();

		// WorldSpace에서 연산
		Ray ray = Ray(worldRayOrigin, worldRayDir);

		float distance = 0.f;
		if (gameObject->GetCollider()->Intersects(ray, OUT distance) == false)
			continue;

		if (distance < minDistance)
		{
			minDistance = distance;
			picked = gameObject;
		}
	}

	return picked;
}

 

이렇게 만들어준 함수를 메인에서 사용하도록하자. 테스트를 위해 피킹된 물체는 없어지는 걸로 해두자.

void CollisionDemo::Update()
{
	if (INPUT->GetButtonDown(KEY_TYPE::LBUTTON))
	{
		int32 mouseX = INPUT->GetMousePos().x;
		int32 mouseY = INPUT->GetMousePos().y;

		//Picking
		auto pickObj = CUR_SCENE->Pick(mouseX, mouseY);
		if (pickObj)
		{
			CUR_SCENE->Remove(pickObj);
		}
	}
}

 

이렇게하면 피킹된 오브젝트가 없어진다.

+ Recent posts