이제 본격적으로 Directx 11에서 그리기 연산이 어떻게 이루어지는 지 살펴보자.

우선 위처럼 하나의 정육면체 큐브를 만든다고 했을 때 이 큐브가 어떻게 만들어지는 지 생각해보자.

이 큐브도 렌더링파이프라인을 모두 거쳐서 나오게 될 것이다.

우선 IA 단계에서 정점과 인덱스 정보를 묘사하는 아이인 Input Layout을 통해 넘겨주고 이를 VS단계에서 카메라에 어떻게 보이게 되는지 행렬곱 연산을 적용시켜 줄 것이다. 그리고 RS단계에서 색상을 보간해줘서 위와 같이 보이게 해야할 것 이다. 그렇게 해주려면 정점과 색상에 대한 정보가 있어야 할 것이다.

 

코드를 살펴보자면 우선  vertexBuffer와 indexBuffer에 정보를 채워서 GPU쪽에 보내줘야한다.

ComPtr<ID3D11Buffer> _vertexBuffer;
ComPtr<ID3D11Buffer> _indexBuffer;

 

버퍼로 전달하고 싶은 정보를 구조체로 정의하고 정보를 채워준다음 버퍼 정보 정의해서 하나의 버퍼를 만들어 주면 된다.

이때 큐브모양을 만들어 줄 것이기 때문에 정점을 8개로 만들어주었다.

struct Vertex
{
	XMFLOAT3 Pos;
	XMFLOAT4 Color;
};

// Create vertex buffer
Vertex vertices[] =
{
	{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4((const float*)&Colors::White)   },
	{ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4((const float*)&Colors::Black)   },
	{ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4((const float*)&Colors::Red)     },
	{ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4((const float*)&Colors::Green)   },
	{ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Blue)    },
	{ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Yellow)  },
	{ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Cyan)    },
	{ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4((const float*)&Colors::Magenta) }
};

D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * 8;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = vertices;
HR(_device->CreateBuffer(&vbd, &vinitData, _vertexBuffer.GetAddressOf()));

 

이렇게 정점만 넘겨줘도 그려줄 수는 있지만 삼각형으로 그리다보면 겹치는 부분이 많이 생기기 때문에 인덱스 정보를 

같이 넘겨준다. 인덱스 버퍼도 버텍스 버퍼와 동일한 방식으로 만들어주면 된다.

// Create the index buffer

uint32 indices[] = {
	// front face
	0, 1, 2,
	0, 2, 3,

	// back face
	4, 6, 5,
	4, 7, 6,

	// left face
	4, 5, 1,
	4, 1, 0,

	// right face
	3, 2, 6,
	3, 6, 7,

	// top face
	1, 5, 6,
	1, 6, 2,

	// bottom face
	4, 0, 3,
	4, 3, 7
};

D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(uint32) * 36;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
ibd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = indices;
HR(_device->CreateBuffer(&ibd, &iinitData, _indexBuffer.GetAddressOf()));

 

그리고 shader를 사용할 때 Com객체처럼 사용하는데 이때 이름을 맞춰주면 가져오고 세팅할 때 연동이 돼서 편하게 사용

할 수 있다.

_fxWorldViewProj = _fx->GetVariableByName("gWorldViewProj")->AsMatrix();

_fxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));

 

VS단계를 보면 이렇게 정점이 월드 뷰 프로젝션 변환 행렬이 곱채져서 반환된다는 것을 볼 수 있다.

cbuffer cbPerObject
{
	float4x4 gWorldViewProj;
};

struct VertexIn
{
	float3 PosL  : POSITION;
    float4 Color : COLOR;
};

struct VertexOut
{
	float4 PosH  : SV_POSITION;
    float4 Color : COLOR;
};

VertexOut VS(VertexIn vin)
{
	VertexOut vout;
	
	// Transform to homogeneous clip space.
	vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
	
	// Just pass vertex color into the pixel shader.
    vout.Color = vin.Color;
    
    return vout;
}

 

그 다음 물체가 어떻게 생겼는지 묘사해주는 Input Layout 단계이다. 이때 쉐이더의 구조체 변수와 클래스 변수와 이름과 구조를 동일하게 맞춰주어야 한다.

void BoxDemo::BuildVertexLayout()
{
	// Create the vertex input layout.
	D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
	{
		{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
		{"COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}
	};

	// Create the input layout
	D3DX11_PASS_DESC passDesc;
	_tech->GetPassByIndex(0)->GetDesc(&passDesc);

	HR(_device->CreateInputLayout(vertexDesc, 2, passDesc.pIAInputSignature, passDesc.IAInputSignatureSize, _inputLayout.GetAddressOf()));
}

 

그 다음에는 렌더타켓뷰와 깊이 스텐실뷰를 클리어해주는 것으로 그림 그릴 도화지를 밀어주고 IA단계의 인풋레이아웃을 묘사한 부분을 실제 DeviceContext와 연결시켜주고 물체를 어떤 기본 물체로 그려줄지 세팅하는 토폴로지 단계를 하는 것을 볼 수 있다. 정점버퍼와 인덱스버퍼도 연결시켜준다.

_deviceContext->IASetInputLayout(_inputLayout.Get());
_deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

uint32 stride = sizeof(Vertex);
uint32 offset = 0;
_deviceContext->IASetVertexBuffers(0, 1, _vertexBuffer.GetAddressOf(), &stride, &offset);
_deviceContext->IASetIndexBuffer(_indexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);

 

이렇게 해준다음 월드,뷰,프로젝션 변환행렬을 쉐이더쪽에 정보를 넘겨주고 드려주면 된다. 인덱스 버퍼를 통해 그려주는 DrawIndexed를 사용하는 것을 볼 수 있다.

그려준 도화지를 Present를 통해 제출한다.

// Set constants
XMMATRIX world = ::XMLoadFloat4x4(&_world);
XMMATRIX view = ::XMLoadFloat4x4(&_view);
XMMATRIX proj = ::XMLoadFloat4x4(&_proj);
XMMATRIX worldViewProj = world * view * proj;

_fxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));

D3DX11_TECHNIQUE_DESC techDesc;
_tech->GetDesc(&techDesc);
for (uint32 p = 0; p < techDesc.Passes; ++p)
{
	_tech->GetPassByIndex(p)->Apply(0, _deviceContext.Get());

	// 36 indices for the box.
	_deviceContext->DrawIndexed(36, 0, 0);
}

HR(_swapChain->Present(0, 0));

 

이렇게 해주면 위의 큐브가 보이게 되는 것이다.

강의 링크

https://inf.run/1HYWy

 

학습 페이지

 

www.inflearn.com

 

파이프라인을 설명할 때는 처음부터 각 단계에 집중하기보다는 이 파이프라인으로 무엇을 해주는 지를 포괄적으로 

설명 하는 것이 좋다.

3D 게임을 만든다고 했을 때 3D 게임 세상을 2D화면으로 표현해주기 위해 변환을 해줘야하는 데 변환해주는 일련의 규칙적인 단계들을 모아서 렌더링 파이프라인이라고 한다. 파이프라인의 단계는 밑의 그림과 같다 이런 작업을 GPU를 통해 이루어지게 된다.

https://learn.microsoft.com/ko-kr/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline

 

● IA(Input Asssembler)단계

입력을 조립하는 단계로 정점을 건네주고 그 정점이 삼각형이나 선, 점인지 어떤식으로 구성되어 있는 지에 관한 정보와 

정점만을 건네주면 리소스가 너무 커지기 때문에 색인정보, 즉 인덱스 정보를 같이 넘겨준다. 이때는 좌표가 아직까지 로컬

좌표이다.

이때 로컬좌표는 자기 자신의 좌표계를 기준으로 하는 좌표로 자신의 원점을 중심으로 기하학적으로 어떻게 생겼는지에 관한 좌표정보이다.

 

● VS(Vertex Shader)단계

이 단계에서는 물체를 이루는 기본 도형인 삼각형의 정점별로 실행되는 단계로 World, View, Projection변환 행렬을 곱해주는 것으로 로컬좌표의 물체를 실제 월드에 배치할 수 있다.

 

● RS(Rasterizer State)단계

이 단계는 카메라의 값에 따라 그려주지 않아도 되는 오브젝트를 걸러주는 클래핑과 같은 작업과 후면으로 된 오브젝트는 그려주지 않는 등의 작업을 해주고 마지막으로 보간 작업을 해줘서 정점을 픽셀 단위로 만들어 준다.

● PS(Pixel Shader)단계

픽셀 단위로 넘어온 것의 색상을 정해주는 단계로 일반적인 물체의 색상 뿐만 아니라 여러가지 조명 연산이 적용된 색상까지 반영해주게 된다.

 

● OM(Ouput Merge)단계

색상까지 정해진 후에 텍스처나 렌더타켓을 그려주거나 렌더타켓 뷰를 설정해서 Swap Chain을 연결해준 다음 후면 버퍼 

전면 버퍼에 최종적으로 그려주게 되는 단계이다. 그려주고 Present를 해주면 화면에 출력 된다.

 

● 행렬

 

SRT연산에서 행렬연산을 많이 해주게 되는데 이때 x y z 에서 1을 붙여주는 이유는 선형 변환을 위해서 필요하다.

만약 이 부분이 0이 된다면 선형 이동을 하지 않는다. 방향벡터를 이용해서 방향만 적용할 때 이렇게 사용하면 회전

만 적용된다. 

1을 붙여주면 TransformCode 이고 0을 붙여주면 TransformNormal 이라고 보면 된다.

 

행렬 연산에서 우선 순위는 Scale Rotation(자전) Translation Roation(공전) Parent(계층) 이다.

 

https://www.acmicpc.net/problem/24416

 

의사코드를 보고 피보나치 수를 구하는 2가지 함수를 만들고 코드1 코드2번 자리에 횟수를 기록하는 부분을 추가해주면 된다.

 

정답코드

#include <iostream>
#include <vector>

using namespace std;

int cnt1 = 0; // 재귀 호출 횟수 카운트

// 재귀 방식 피보나치 함수
long fib(int n) {
    if (n == 1 || n == 2) {
        cnt1++;
        return 1;
    }
    else {
        return (fib(n - 1) + fib(n - 2));
    }
}

// 동적 프로그래밍 방식 피보나치 함수
int fib2(int n) {
    vector<int> f(n + 1); // 배열 크기를 n + 1로 선언
    f[1] = f[2] = 1;
    int cnt2 = 0;

    // 피보나치 수열 계산 및 코드2 실행 횟수 카운트
    for (int i = 3; i <= n; i++) {
        f[i] = f[i - 1] + f[i - 2];
        cnt2++;
    }

    return cnt2; // 코드2 실행 횟수 반환
}

int main() {
    int n;
    cin >> n;

    fib(n); // 재귀 피보나치 계산
    int cnt2 = fib2(n); // 동적 프로그래밍 피보나치 계산

    cout << cnt1 << " " << cnt2 << endl;

    return 0;
}


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

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

살펴보자.

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

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

그렇게하기 위해 상수버퍼에 넣어줄 구조체타입을 정의해주고 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://www.acmicpc.net/problem/1010

 

 

조합을 통해 풀 수 있는 문제이다. mCn을 통해 다리를 지을 수 있는 경우의 수를 출력해주면 된다.

 

정답코드

#include <iostream>

using namespace std;

int combination(int n, int k) {
	if (k > n - k) k = n - k;  // 계산량을 줄이기 위해 k를 더 작은 값으로 설정
	long long result = 1;
	for (int i = 0; i < k; i++) {
		result *= (n - i);
		result /= (i + 1);
	}
	return result;
}

int main()
{
	int t;

	cin >> t;

	for (int i = 0; i < t; i++)
	{
		int n, m;
		cin >> n >> m;
		cout << combination(m, n) << endl;
	}
}

강의

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;
}

 

 

+ Recent posts