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

전에 풀었던 문제에서 x좌표의 오름차순에서 y좌표의 오름차순으로 바뀐 문제이다. 정렬함수만 살짝 바꿔주면 풀 수 있는 문제이다. pair에서 first보다 second를 먼저 비교하고 second가 같을 경우 first를 비교해주면 된다.

 

정답코드

#include <cstdio>
#include <vector>
#include <algorithm>

using namespace std;

vector<pair<int, int>> vp;

bool cmp(pair<int, int> p1, pair<int, int> p2) {
    if (p1.second == p2.second) {
        return p1.first < p2.first;
    }
    return p1.second < p2.second;
}

int main() {
    int n;
    scanf("%d", &n);

    vp.reserve(n);

    for (int i = 0; i < n; i++) {
        int x, y;
        scanf("%d %d", &x, &y);
        vp.push_back({ x, y });
    }

    sort(vp.begin(), vp.end(), cmp);

    for (int i = 0; i < n; i++) {
        printf("%d %d\n", vp[i].first, vp[i].second);
    }

    return 0;
}

1. 애니메이션 활용2

이제 모델은 정상적으로 나오기 때문에 각 본의 상대적인 좌표를 통해 연산을 해서 애니메이션을 실행시킬 수 있어야한다. 우선 우리가 스키닝을 통해 얻어온 정보를 쉐이더쪽에 넘겨줘야한다. 이를 위해 Global.fx에 

구조체를 추가해주자.

Global.fx

/////////////////
// VertexBuffer //
/////////////////

struct VertexTextureNormalTangentBlend
{
    float4 position : POSITION;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
    float3 tangent : TANGENT;
    float4 blendIndices : BLEND_INDICES;
    float4 blendWeights : BLEND_WEIGHTS;
};

 

이때 어떤 버퍼를 통해 값을 넣어주면 될지 생각해보아야하는데 상수버퍼는 많은 데이터를 넣어주지는 못한다. 그렇기 때문에 다른 방법을 사용해주어야한다. 이때 텍스처에 정보를 담아서 전달해주도록하자. 이 텍스처에는 모든 애니메이션, 본의 transform정보를 넣어주자 

 

이를 위해 KeyFrame 과 Transform을 저장할 구조체를 선언해주고 이를 채우고 텍스처로 만드는 함수를 만들어주자. 이때 텍스처를 GPU에 넘겨주기 위하여 쉐이더리소스뷰도 사용해주자.

일단 우리가 custom file로 만들어준 정보를 가져오기 위해 본과 프레임을 순회하면서 특정 본의 특정 프레임의 SRT를 하나의 Transform으로 묶어서 vector에 누적하여 저장해주자. 이때의 Matrix는 상위노드에 대한 상대 Matrix이다. 

이 Transfrom과 기본자세인 T에서의 정점의 상대좌표, Root node로 가는 Transform을 가진 Transform의 역행렬을 구한 다음 우리가 구한 SRT를 곱해두면 된다. 

한 정점에 영향을 주는 뼈가 최대 4개이기 때문에 이 작업을 4번 해주어야하지만 핵심은 위와 같다.

 

ModelAnimator.cpp

void ModelAnimator::CreateAnimationTransform(uint32 index)
{
	vector<Matrix> tempAnimBoneTransforms(MAX_MODEL_TRANSFORMS, Matrix::Identity);

	shared_ptr<ModelAnimation> animation = _model->GetAnimationByIndex(index);

	for (uint32 f = 0; f < animation->frameCount; f++)
	{
		for (uint32 b = 0; b < _model->GetBoneCount(); b++)
		{
			shared_ptr<ModelBone> bone = _model->GetBoneByIndex(b);

			Matrix matAnimation;

			shared_ptr<ModelKeyframe> frame = animation->GetKeyframe(bone->name);
			if (frame != nullptr)
			{
				ModelKeyframeData& data = frame->transforms[f];

				Matrix S, R, T;
				S = Matrix::CreateScale(data.scale.x, data.scale.y, data.scale.z);
				R = Matrix::CreateFromQuaternion(data.rotation);
				T = Matrix::CreateTranslation(data.translation.x, data.translation.y, data.translation.z);

				matAnimation = S * R * T;
			}
			else
			{
				matAnimation = Matrix::Identity;
			}

			// [ !!!!!!! ]
			Matrix toRootMatrix = bone->transform;
			Matrix invGlobal = toRootMatrix.Invert();

			int32 parentIndex = bone->parentIndex;

			Matrix matParent = Matrix::Identity;
			if (parentIndex >= 0)
				matParent = tempAnimBoneTransforms[parentIndex];
			
			tempAnimBoneTransforms[b] = matAnimation * matParent;

			// 결론
			_animTransforms[index].transforms[f][b] = invGlobal * tempAnimBoneTransforms[b];
		}
	}
}

이렇게 해주면 데이터가 매우 크기때문에 텍스처리소스를 만들어주고 이를 묘사해주는 SRV- 쉐이더리소스뷰를 만들어서 

GPU로 넘겨주도록하자. 

이때 행렬은 float가 16개 있는 것으로 64바이트이다. 하지만 최대로 텍스처에 넣어줄 수 있는 바이트는 16바이트이다. 그렇기 때문에 우리가 4개로 쪼개서 가져가도록 해주자. 

 

우리가 버퍼크기를 초기화해줄때 malloc을 사용한다. 이때 잠깐 malloc / free 와 new / delete의 차이를 생각해보자면 생성자 호출이 안된다는 차이가 있다. malloc은 생성자가 필요하지 않지만 new는 생성자가 필요하다. 그리고 포인터 연산을 할때 BYTE 나 Character 형으로의 cast 없이 바꾼다면 바이트 씩 넘어가는 것이 아니라 ~칸 씩 넘어가는 등의 원하는 

결과값과 다른 값이 나올 수 있기 때문에 cast를 해주는게 원하는 곳으로 접근하기 위하여 덧셈 뺄셈 연산을 할  좋다.

이러한 연산을 통하여 2차원 배열에서 파편화되어 있을수도있는 데이터를 연속적인 malloc을 통해 결합해준다.

 

리소스를 만들어줄 때는 지금은 여러가지 텍스처를 통해 넘겨주도록 하기 위해서 서브 텍스처를 여러 개 만들어주자

이렇게 만들어준 텍스처를 SRV를 통해 GPU로 넘겨주면 된다.

 

ModelAnimator.cpp

void ModelAnimator::CreateTexture()
{
	if (_model->GetAnimationCount() == 0)
		return;

	_animTransforms.resize(_model->GetAnimationCount());
	for (uint32 i = 0; i < _model->GetAnimationCount(); i++)
		CreateAnimationTransform(i);

	//CreateTexture
	{
		D3D11_TEXTURE2D_DESC desc;
		ZeroMemory(&desc, sizeof(D3D11_TEXTURE2D_DESC));
		desc.Width = MAX_MODEL_TRANSFORMS * 4;		//4칸으로 쪼개기
		desc.Height = MAX_MODEL_KEYFRAMES;
		desc.ArraySize = _model->GetAnimationCount();		//텍스처배열
		desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // 16바이트
		desc.Usage = D3D11_USAGE_IMMUTABLE;
		desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
		desc.MipLevels = 1;
		desc.SampleDesc.Count = 1;

		//한층
		const uint32 dataSize = MAX_MODEL_TRANSFORMS * sizeof(Matrix);
		//층을 프레임만큼
		const uint32 pageSize = dataSize * MAX_MODEL_KEYFRAMES;
		//애니메이션 개수만큼
		void* mallocPtr = ::malloc(pageSize * _model->GetAnimationCount());
	
		// 파편화된 데이터를 조립한다.
		for (uint32 c = 0; c < _model->GetAnimationCount(); c++)
		{
			uint32 startOffset = c * pageSize;

			//포인터 연산 - ~칸씩 앞으로 가기 때문에
			BYTE* pageStartPtr = reinterpret_cast<BYTE*>(mallocPtr) + startOffset;

			
			for (uint32 f = 0; f < MAX_MODEL_KEYFRAMES; f++)
			{
				void* ptr = pageStartPtr + dataSize * f;
				::memcpy(ptr, _animTransforms[c].transforms[f].data(), dataSize);
			}
		}

		// 리소스 만들기
		vector<D3D11_SUBRESOURCE_DATA> subResources(_model->GetAnimationCount());

		for (uint32 c = 0; c < _model->GetAnimationCount(); c++)
		{
			void* ptr = (BYTE*)mallocPtr + c * pageSize;
			subResources[c].pSysMem = ptr;
			subResources[c].SysMemPitch = dataSize;
			subResources[c].SysMemSlicePitch = pageSize;
		}

		//텍스처 배열이 전달된다.
		HRESULT hr = DEVICE->CreateTexture2D(&desc, subResources.data(), _texture.GetAddressOf());
		CHECK(hr);

		::free(mallocPtr);
	
	}

	// Create SRV
	{
		D3D11_SHADER_RESOURCE_VIEW_DESC desc;
		ZeroMemory(&desc, sizeof(desc));
		desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
		desc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
		desc.Texture2DArray.MipLevels = 1;
		desc.Texture2DArray.ArraySize = _model->GetAnimationCount();

		HRESULT hr = DEVICE->CreateShaderResourceView(_texture.Get(), &desc, _srv.GetAddressOf());
		CHECK(hr);
	}
}

 

2.애니메이션활용3

일단 전에 하던거에 이어서 애니메이션에 관련된 정보(현재 실행중인 애니메이션 번호, 프레임)을 넘겨줄 수 있도록 해당정보를 GPU쪽으로 넘겨주는 RenderManager에서 구조체와 값을 밀어넣어주는 함수를 만들어주자.

RenderManager.h

//Animation
struct KeyframeDesc
{
	int32 animIndex = 0;
	uint32 currentFrame = 0;
	//TODO
	uint32 nextFrame = 0;
	float ratio = 0.f;
	float sumTime = 0.f;
	float speed = 1.f;
	Vec2 padding;
};

class RenderManager
{
	DECLARE_SINGLE(RenderManager);

public:
	void PushKeyframeData(const KeyframeDesc& desc);
private:
	KeyframeDesc _keyframeDesc;
	shared_ptr<ConstantBuffer<KeyframeDesc>> _keyframeBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _keyframeEffectBuffer;
};

RenderManager.cpp

void RenderManager::Init(shared_ptr<Shader> shader)
{
	_keyframeBuffer = make_shared<ConstantBuffer<KeyframeDesc>>();
	_keyframeBuffer->Create();
	_keyframeEffectBuffer = _shader->GetConstantBuffer("KeyframeBuffer");
}

void RenderManager::PushKeyframeData(const KeyframeDesc& desc)
{
	_keyframeDesc = desc;
	_keyframeBuffer->CopyData(_keyframeDesc);
	_keyframeEffectBuffer->SetConstantBuffer(_keyframeBuffer->GetComPtr().Get());
}

 

쉐이더쪽에서도 값을 받아줄 수 있도록 구조체와 버퍼, 텍스처배열을 선언해주자.

AnimationDemo.fx

#define MAX_MODEL_KEYFRAMES 500

struct KeyframeDesc
{
    int animIndex;
    uint currFrame;
    uint nexFrame;
    float ratio;        //프레임 블렌딩용
    float sumTime;
    float speed;
    float2 padding;
};

cbuffer KeyframeBuffer
{
    KeyframeDesc keyframes;
};

Texture2DArray TransformMap;

 

 

쉐이더와 연결해주는 부분을 만들어주자. 애니메이션 변경부분은 이전에 사용했던 ImGui를 사용해서 버튼을 통해 변경되게 만들어주자.

ModelAnimator.cpp

void ModelAnimator::Update()
{
    //Anim Update
    ImGui::InputInt("AnimIndex", &_keyframeDesc.animIndex);
    _keyframeDesc.animIndex %= _model->GetAnimationCount();
    ImGui::InputInt("CurrFrame", (int*)&_keyframeDesc.currFrame);
    _keyframeDesc.currFrame %= _model->GetAnimationByIndex(_keyframeDesc.animIndex)->frameCount;

    //애니메이션 현재 프레임 정보
    RENDER->PushKeyframeData(_keyframeDesc);

    //SRV를 전달
    _shader->GetSRV("TransformMap")->SetResource(_srv.Get());
}

 

이렇게 해주면 아직 쉐이더 코드를 완성하지 않아서 실제로 애니메이션이 바뀌지는 않지만 ImGui창을 통해 값을 변경할 수 있다는 것을 알 수 있다. 이때 오버플로우를 방지하기 위하여 나눗셈 연산을 해주고 있다.

 

이제 쉐이더 코드쪽에서 내가 원하는 부분을 뽑아서 적용할 수 있도록 함수를 만들어 주자. 여기서 글로벌좌표를 상대좌표로 변환한 뒤에 다시 글로벌 좌표로 변환하는 부분이 들어가게 될 것이다. 입력이 들어올 때는 연관된 뼈 번호와 얼마나 영향을 받는지는 기록되어있는 상태이다. 이 값을 추출하고 이를 통해 행렬을 누적해주면 된다. 이때 16바이트 4개로 나누어 주었기때문에 4칸을 건너뛰어야하고 이를 위해 곱연산을 넣어준다. 이를 4번 반복해서 가중치에 따른 행렬이 누적되기 때문에 이 행렬을 통해 움직이는 연산을 해주면 된다.

AnimationDemo.fx

matrix GetAnimationWorldMatrix(VertexTextureNormalTangentBlend input)
{
    float indices[4] = { input.blendIndices.x, input.blendIndices.y, input.blendIndices.z, input.blendIndices.w };
	float weights[4] = { input.blendWeights.x, input.blendWeights.y, input.blendWeights.z, input.blendWeights.w };
    
    int animIndex = Keyframes.animIndex;
    int currFrame = Keyframes.currFrame;
    //int nextFrame = Keyframes.nextFrame;
    
    float4 c0, c1, c2, c3;
    matrix curr = 0;
    matrix transform = 0;

    //각 뼈의 가중치에 따른 행렬 누적
    for (int i = 0; i < 4; i++)
    {
        c0 = TransformMap.Load(int4(indices[i] * 4 + 0, currFrame, animIndex, 0));
        c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame, animIndex, 0));
        c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame, animIndex, 0));
        c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame, animIndex, 0));

        curr = matrix(c0, c1, c2, c3);
        //누적 메트릭스
        transform += mul(weights[i], curr);
    }

    return transform;
}

 

이렇게 해주면 기본 자세에서는 Global에서 상대좌표로 가고 여기서 다시 새로운 자세의 Global로 가는 행렬을 구해줄 수 있다.

이 함수를 VS단계에서 입력을 받는 단계에 곱해주면 이제 새로운 자세가 출력될 수 있게 된다.

AnimationDemo.fx

MeshOutput VS(VertexTextureNormalTangentBlend input)
{
    MeshOutput output;
    
    //TODO
    //최종 변환 행렬
    matrix m = GetAnimationMatrix(input);
    
    output.position = mul(input.position, m);
    output.position = mul(output.position, W);
    //투영좌표 제외
    output.worldPosition = output.position.xyz;
    output.position = mul(output.position, VP);
    output.uv = input.uv;
    //회전만 고려
    output.normal = mul(input.normal, (float3x3) W);
    output.tangent = mul(input.tangent, (float3x3) W);

    return output;
}

 

이렇게 해주면 current frame을 증가시키면 조금씩이지만 움직이는 모습을 볼 수 있고 AnimIndex를 증가시키면 다른 애니메이션도 실행되는 모습을 볼 수 있다. 지금은 current frame을 증가시키면 뚝뚝 끊기는 느낌이 드는데 이렇게 되는 이유는 우리가 정점의 위치를 강제로 프레임 단위로 강제로 조정해주고 있기 때문이다. 

이러한 문제점을 고치기 위해 보간을  사용해주자. 이때 현재 상태와 다음 상태의 재생비율에 따라 섞어 주면 된다. 이 부분을 함수를 수정하여 구현해보자.

AnimationDemo.fx

matrix GetAnimationMatrix(VertexTextureNormalTangentBlend input)
{
    float indices[4] = { input.blendIndices.x, input.blendIndices.y, input.blendIndices.z, input.blendIndices.w };
	float weights[4] = { input.blendWeights.x, input.blendWeights.y, input.blendWeights.z, input.blendWeights.w };
    
    int animIndex = Keyframes.animIndex;
    int currFrame = Keyframes.currFrame;
    int nextFrame = Keyframes.nextFrame;
    float ratio = Keyframes.ratio;
    
    float4 c0, c1, c2, c3;
    float4 n0, n1, n2, n3;
    
    matrix curr = 0;
    matrix next = 0;
    matrix transform = 0;

    //각 뼈의 가중치에 따른 행렬 누적
    for (int i = 0; i < 4; i++)
    {
        c0 = TransformMap.Load(int4(indices[i] * 4 + 0, currFrame, animIndex, 0));
        c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame, animIndex, 0));
        c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame, animIndex, 0));
        c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame, animIndex, 0));
        curr = matrix(c0, c1, c2, c3);
        
        n0 = TransformMap.Load(int4(indices[i] * 4 + 0, nextFrame, animIndex, 0));
        n1 = TransformMap.Load(int4(indices[i] * 4 + 1, nextFrame, animIndex, 0));
        n2 = TransformMap.Load(int4(indices[i] * 4 + 2, nextFrame, animIndex, 0));
        n3 = TransformMap.Load(int4(indices[i] * 4 + 3, nextFrame, animIndex, 0));
        next = matrix(n0, n1, n2, n3);
        
        //누적 메트릭스
        matrix result = lerp(curr, next, ratio);

        transform += mul(weights[i], result);
    }

    return transform;
}


이렇게 해준뒤 이제 우리가 직접 시간을 움직이는 것에서 자동으로 시간이 흐르고 이에 따라 애니메이션이 실행되는 방식으로 바꿔주자. 이 작업은 ModelAnimator에서 해주게 되는데 이때 델타타임을 가지고 이 작업을 해주자.

ModelAnimator.cpp

void ModelAnimator::Update()
{
	if (_model == nullptr)
		return;

	//TODO
	if (_texture == nullptr)
		CreateTexture();

	_keyframeDesc.sumTime += DT;

	shared_ptr<ModelAnimation> current = _model->GetAnimationByIndex(_keyframeDesc.animIndex);
	if (current)
	{
		//30프레임이면 1/30초 마다 다음 프레임
		float timePerFrame = 1 / (current->frameRate * _keyframeDesc.speed);
		if (_keyframeDesc.sumTime >= timePerFrame)
		{
			_keyframeDesc.sumTime = 0.f;
			_keyframeDesc.currFrame = (_keyframeDesc.currFrame + 1) % current->frameCount;
			_keyframeDesc.nextFrame = (_keyframeDesc.currFrame + 1) % current->frameCount;
		}

		_keyframeDesc.ratio = (_keyframeDesc.sumTime / timePerFrame);
	}

	//Anim Update
	ImGui::InputInt("AnimIndex", &_keyframeDesc.animIndex);
	_keyframeDesc.animIndex %= _model->GetAnimationCount();
	ImGui::InputFloat("Speed", &_keyframeDesc.speed, 0.5f, 4.f);

	//애니메이션 현재 프레임 정보
	RENDER->PushKeyframeData(_keyframeDesc);

	//SRV를 전달
	_shader->GetSRV("TransformMap")->SetResource(_srv.Get());

	//Bones -> shader
	BoneDesc boneDesc;

	const uint32 boneCount = _model->GetBoneCount();

	for (uint32 i = 0; i < boneCount; i++)
	{
		shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i);
		boneDesc.transforms[i] = bone->transform;
	}
	RENDER->PushBoneData(boneDesc);

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

	const auto& meshes = _model->GetMeshes();
	for (auto& mesh : meshes)
	{
		if (mesh->material)
			mesh->material->Update();

		//BoneIndex
		_shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex);

		uint32 stride = mesh->vertexBuffer->GetStride();
		uint32 offset = mesh->vertexBuffer->GetOffset();

		//각 매쉬를 버퍼에 할당
		DC->IASetVertexBuffers(0, 1, mesh->vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
		DC->IASetIndexBuffer(mesh->indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);

		_shader->DrawIndexed(0, _pass, mesh->indexBuffer->GetCount(), 0, 0);
	}
}

 

이렇게 해주면 훨씬 부드럽게 움직이는 것을 볼 수 있다.

 

이제 다음 시간에는 뛰다가 멈춘다는 등의 이어지는 애니메이션을 만들어볼 것이다. 

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

 

pair와 vector를 사용해서 좌표를 저장하고 정렬에 비교함수를 설정해서 비교하면 되겠다고 생각해서 해봤는데 시간초과가 떴다. 그래서 입력이 커서 그런가 싶어서 입출력 최적화를 하기위해 아래 코드를 넣었지만 그래도 시간초과가 떴다.

// 입출력 속도 최적화
    ios::sync_with_stdio(false);
    cin.tie(NULL);

 

이유를 찾아보니 cin과 cout 보다는 scanf와 printf가 성능이 더 좋다는 것을 알게되어서 이렇게 입출력을 바꿔주니 정답이었다. 

 

 

초기코드(cin, cout 사용)

#include "iostream"
#include "utility"
#include "vector"
#include "algorithm"

using namespace std;

vector<pair<int, int>> vp;

bool cmp(pair<int, int> p1, pair<int, int> p2)
{
	if (p1.first == p2.first) {
		return p1.second < p2.second;  // x 좌표가 같을 때는 y 좌표로 비교
	}
	return p1.first < p2.first;  // x 좌표로 비교
}
int main()
{
	// 입출력 속도 최적화
	ios::sync_with_stdio(false);
	cin.tie(NULL);

	int n;

	cin >> n;

	vp.reserve(n);  // 미리 벡터의 메모리를 예약하여 성능 최적화

	for (int i = 0; i < n; i++)
	{
		int x, y;
		cin >> x >> y;
		vp.push_back({ x,y });
	}

	sort(vp.begin(), vp.end(), cmp);

	for (int i = 0; i < vp.size(); i++)
	{
		cout << vp[i].first << " " << vp[i].second << endl;
	}

	return 0;
}

 

 

정답코드(printf & scanf 사용)

#include <cstdio>
#include <vector>
#include <algorithm>

using namespace std;

vector<pair<int, int>> vp;

bool cmp(pair<int, int> p1, pair<int, int> p2) {
    if (p1.first == p2.first) {
        return p1.second < p2.second;
    }
    return p1.first < p2.first;
}

int main() {
    int n;
    scanf("%d", &n);

    vp.reserve(n);

    for (int i = 0; i < n; i++) {
        int x, y;
        scanf("%d %d", &x, &y);
        vp.push_back({ x, y });
    }

    sort(vp.begin(), vp.end(), cmp);

    for (int i = 0; i < n; i++) {
        printf("%d %d\n", vp[i].first, vp[i].second);
    }

    return 0;
}

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

 

 

처음에는 문제를 이해하지 못했었는데 지그재그로 이해하기보다는 대각선으로 보는게 이해하기 쉽다. 몇번째 대각선에 속해있는 분수인지 구하고 짝수번째 대각선이면 위에서 아래로 가고 홀수번째 대각선이면 아래에서 위로 가기때문에 몇번째 분수인지만 알면 분모와 분자를 구해서 출력해주면 된다.

 

 

정답코드

#include "iostream"

using namespace std;

int main()
{
	// d: 현재 대각선 번호 , cnt : 분수의 총 개수
	int x, d = 1, cnt = 0;

	cin >> x;

	//대각선에 속하는 개수까지 증가시켜줌
	while (cnt+d < x)
	{
		cnt += d;
		d++;
	}

	//순서-지금까지의 분수 개수 = 위치
	int idx = x - cnt;
	int answer_x, answer_y;				//분자, 분모

	//짝수이면 아래 홀수이면 위로 감
	if (d % 2 == 0)
	{
		answer_x = idx;
		answer_y = d - idx + 1;

	}
	else
	{
		answer_x = d - idx + 1;
		answer_y = idx;
	}

	cout << answer_x << "/" << answer_y << endl;

	return 0;
}



★ ★ ★ ★ ★ ★ ★ ★ 꿀팁 ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

만약 코드가 다 정상인데 모델이나 화면이 제대로 뜨지않는다면 InTermediate폴더안에 Debug를 지우고 다시 전체빌드를 하면 정상적으로 작동하는 경우가 많다!

1. 애니메이션 데이터 추출

오늘은 애니메이션에 필요한 데이터를 추출해보자. fbx 파일에서 애니메이션에 관한 정보를 가지고 오기 위하여 먼저 그에 맞는 구조체를 선언해주자.

AsTypes.h

//Animation Clip

struct asKeyframeData
{
	float time; 
	//SRT
	Vec3 scale;
	Quaternion rotation;
	Vec3 translation;
};

//한프레임
struct asKeyFrame
{
	string boneName;
	vector<asKeyframeData> transforms;
};


struct asAnimation
{
	string name;
	uint32 frameCount;		//몇프레임짜리
	float frameRate;
	float duration;
	vector<asKeyFrame> keyframes;
};

//캐시용
struct asAnimationNode
{
	aiString name;
	vector<asKeyframeData> keyframe;
};

 

이 구조체에 맞게 데이터를 가져오면 된다.  우선 애니메이션에 관련된 SRT데이터를 캐시해서 가지고 오자.

그 다음 캐싱한 구조체 변수를 통해 우리가 만든 구조체에 해당 정보를 저장하는 방식으로 데이터를 가져오자. 그리고 이를 확인할 수 있게 파일로 저장해주자.

Converter.cpp

void Converter::ExportAnimationData(wstring savePath, uint32 index)
{
	wstring finalPath = _modelPath + savePath + L".clip";
	assert(index < _scene->mNumAnimations);
	shared_ptr<asAnimation> animation = ReadAnimationData(_scene->mAnimations[index]);
	WriteAnimationData(animation, finalPath);
}

shared_ptr<asAnimation> Converter::ReadAnimationData(aiAnimation* srcAnimation)
{
	shared_ptr<asAnimation> animation = make_shared<asAnimation>();
	animation->name = srcAnimation->mName.C_Str();
	animation->frameRate = (float)srcAnimation->mTicksPerSecond;
	animation->frameCount = (uint32)srcAnimation->mDuration + 1;

	//임시 저장
	map<string, shared_ptr<asAnimationNode>> cacheAnimNodes;

	for (uint32 i = 0; i < srcAnimation->mNumChannels; i++)
	{
		aiNodeAnim* srcNode = srcAnimation->mChannels[i];

		//애니메이션 노드 데이터 파싱
		shared_ptr<asAnimationNode> node = ParseAnimationNode(animation, srcNode);
	
		// 현재 찾은 노드 중에 제일 긴 시간으로 애니메이션 시간 갱신
		animation->duration = max(animation->duration, node->keyframe.back().time);
	
		cacheAnimNodes[srcNode->mNodeName.C_Str()] = node;
	}

	ReadKeyframeData(animation, _scene->mRootNode, cacheAnimNodes);

	return animation;
}

//SRT추출함수
//SRT추출함수
shared_ptr<asAnimationNode> Converter::ParseAnimationNode(shared_ptr<asAnimation> animation, aiNodeAnim* srcNode)
{
	std::shared_ptr<asAnimationNode> node = make_shared<asAnimationNode>();
	node->name = srcNode->mNodeName;

	uint32 keyCount = max(max(srcNode->mNumPositionKeys, srcNode->mNumScalingKeys), srcNode->mNumRotationKeys);

	for (uint32 k = 0; k < keyCount; k++)
	{
		asKeyframeData frameData;

		bool found = false;
		uint32 t = node->keyframe.size();

		// Position
		if (::fabsf((float)srcNode->mPositionKeys[k].mTime - (float)t) <= 0.0001f)
		{
			aiVectorKey key = srcNode->mPositionKeys[k];
			frameData.time = (float)key.mTime;
			::memcpy_s(&frameData.translation, sizeof(Vec3), &key.mValue, sizeof(aiVector3D));

			found = true;
		}

		// Rotation
		if (::fabsf((float)srcNode->mRotationKeys[k].mTime - (float)t) <= 0.0001f)
		{
			aiQuatKey key = srcNode->mRotationKeys[k];
			frameData.time = (float)key.mTime;

			frameData.rotation.x = key.mValue.x;
			frameData.rotation.y = key.mValue.y;
			frameData.rotation.z = key.mValue.z;
			frameData.rotation.w = key.mValue.w;

			found = true;
		}

		// Scale
		if (::fabsf((float)srcNode->mScalingKeys[k].mTime - (float)t) <= 0.0001f)
		{
			aiVectorKey key = srcNode->mScalingKeys[k];
			frameData.time = (float)key.mTime;
			::memcpy_s(&frameData.scale, sizeof(Vec3), &key.mValue, sizeof(aiVector3D));

			found = true;
		}

		if (found == true)
			node->keyframe.push_back(frameData);
	}

	// Keyframe 늘려주기
	if (node->keyframe.size() < animation->frameCount)
	{
		uint32 count = animation->frameCount - node->keyframe.size();
		asKeyframeData keyFrame = node->keyframe.back();

		for (uint32 n = 0; n < count; n++)
			node->keyframe.push_back(keyFrame);
	}

	return node;
}

void Converter::ReadKeyframeData(shared_ptr<asAnimation> animation, aiNode* srcNode, map<string, shared_ptr<asAnimationNode>>& cache)
{
	shared_ptr<asKeyframe> keyframe = make_shared<asKeyframe>();
	keyframe->boneName = srcNode->mName.C_Str();

	shared_ptr<asAnimationNode> findNode = cache[srcNode->mName.C_Str()];
	
	for (uint32 i = 0; i < animation->frameCount; i++)
	{
		asKeyframeData frameData;

		if (findNode == nullptr)
		{
			//원래 노드가 가지고있는 transform 추출
			Matrix transform(srcNode->mTransformation[0]);
			transform = transform.Transpose();
			frameData.time = (float)i;
			transform.Decompose(OUT frameData.scale, OUT frameData.rotation, OUT frameData.translation);
		}
		else
		{
			frameData = findNode->keyframe[i] ;
		}

		keyframe->transforms.push_back(frameData);
	}

	// 애니메이션 키프레임 채우기
	animation->keyframes.push_back(keyframe);

	for (uint32 i = 0; i < srcNode->mNumChildren; i++)
		ReadKeyframeData(animation, srcNode->mChildren[i], cache);
}

//바이너리파일로 저장- 빠르다
void Converter::WriteAnimationData(shared_ptr<asAnimation> animation, wstring finalPath)
{
	auto path = filesystem::path(finalPath);

	// 폴더가 없으면 만든다.
	filesystem::create_directory(path.parent_path());

	shared_ptr<FileUtils> file = make_shared<FileUtils>();
	file->Open(finalPath, FileMode::Write);

	file->Write<string>(animation->name);
	file->Write<float>(animation->duration);
	file->Write<float>(animation->frameRate);
	file->Write<uint32>(animation->frameCount);

	file->Write<uint32>(animation->keyframes.size());

	for (shared_ptr<asKeyframe> keyframe : animation->keyframes)
	{
		file->Write<string>(keyframe->boneName);

		//배열 넣을때는 사이즈먼저 넣어주기
		file->Write<uint32>(keyframe->transforms.size());
		file->Write(&keyframe->transforms[0], sizeof(asKeyframeData) * keyframe->transforms.size());
	}
}

 

이렇게 해주면 clip 파일이 저장된다.

 

2.애니메이션1

이제 저장한 파일의 값을 가져와서 애니메이션을 구현해보자. 유니티를 예로 들자면 Skinned Mesh Renderer와 같이 

매쉬렌더러도 다양하게 존재한다. 우리는 이렇게 까지는 구현하지 않겠지만 Model Renderer와 Animation Renderer는 구별하여 만들어주자. 

일단 Model 클래스에서 애니메이션을 불러오는 함수를 만들어주자. 이를 위해서 Animation 정보를 가지고 있을 ModelAnimation이라는 클래스를 만들어주자

 

ModelAnimation.h

#pragma once

struct ModelBone;
struct ModelMesh;
struct ModelAnimation;

class Model : public enable_shared_from_this<Model>
{
public:
	Model();
	~Model();

public:
	//custom -> Memory
	void ReadMaterial(wstring filename);
	void ReadModel(wstring filename);
	void ReadAnimation(wstring filename);

	//헬퍼함수
	uint32 GetMaterialCount() { return static_cast<uint32>(_materials.size()); }
	vector<shared_ptr<Material>>& GetMaterials() { return _materials; }
	shared_ptr<Material> GetMaterialByIndex(uint32 index) { return _materials[index]; }
	shared_ptr<Material> GetMaterialByName(const wstring& name);

	uint32 GetMeshCount() { return static_cast<uint32>(_meshes.size()); }
	vector<shared_ptr<ModelMesh>>& GetMeshes() { return _meshes; }
	shared_ptr<ModelMesh> GetMeshByIndex(uint32 index) { return _meshes[index]; }
	shared_ptr<ModelMesh> GetMeshByName(const wstring& name);

	uint32 GetBoneCount() { return static_cast<uint32>(_bones.size()); }
	vector<shared_ptr<ModelBone>>& GetBones() { return _bones; }
	shared_ptr<ModelBone> GetBoneByIndex(uint32 index) { return (index < 0 || index >= _bones.size() ? nullptr : _bones[index]); }
	shared_ptr<ModelBone> GetBoneByName(const wstring& name);

	uint32 GetAnimationCount() { return _animations.size(); }
	vector<shared_ptr<ModelAnimation>>& GetAnimations() { return _animations; }
	shared_ptr<ModelAnimation> GetAnimationByIndex(UINT index) { return (index < 0 || index >= _animations.size()) ? nullptr : _animations[index]; }
	shared_ptr<ModelAnimation> GetAnimationByName(wstring name);

private:
	//캐쉬정보 채워주기	
	void BindCacheInfo();

private:
	wstring _modelPath = L"../Resources/Models/";
	wstring _texturePath = L"../Resources/Textures/";

private:
	shared_ptr<ModelBone> _root;
	vector<shared_ptr<Material>> _materials;
	vector<shared_ptr<ModelBone>> _bones;
	vector<shared_ptr<ModelMesh>> _meshes;
	vector<shared_ptr<ModelAnimation>> _animations;
};

ModelAnimation.cpp

#include "pch.h"
#include "ModelAnimation.h"

shared_ptr<ModelKeyframe> ModelAnimation::GetKeyframe(const wstring& name)
{
	auto findit = keyframes.find(name);
	if (findit == keyframes.end())
		return nullptr;

	return findit->second;
}

 

이렇게 만들어준 클래스를 바탕으로 우리가 저장해준 파일형식대로 파일내용을 가져온다음 함수가 호출될때마다 vector에 저장하도록 만들어주자.

Model.h

#pragma once

struct ModelBone;
struct ModelMesh;
struct ModelAnimation;

class Model : public enable_shared_from_this<Model>
{
public:
	Model();
	~Model();

public:
	//custom -> Memory
	void ReadMaterial(wstring filename);
	void ReadModel(wstring filename);
	void ReadAnimation(wstring filename);

	//헬퍼함수
	uint32 GetMaterialCount() { return static_cast<uint32>(_materials.size()); }
	vector<shared_ptr<Material>>& GetMaterials() { return _materials; }
	shared_ptr<Material> GetMaterialByIndex(uint32 index) { return _materials[index]; }
	shared_ptr<Material> GetMaterialByName(const wstring& name);

	uint32 GetMeshCount() { return static_cast<uint32>(_meshes.size()); }
	vector<shared_ptr<ModelMesh>>& GetMeshes() { return _meshes; }
	shared_ptr<ModelMesh> GetMeshByIndex(uint32 index) { return _meshes[index]; }
	shared_ptr<ModelMesh> GetMeshByName(const wstring& name);

	uint32 GetBoneCount() { return static_cast<uint32>(_bones.size()); }
	vector<shared_ptr<ModelBone>>& GetBones() { return _bones; }
	shared_ptr<ModelBone> GetBoneByIndex(uint32 index) { return (index < 0 || index >= _bones.size() ? nullptr : _bones[index]); }
	shared_ptr<ModelBone> GetBoneByName(const wstring& name);

	uint32 GetAnimationCount() { return _animations.size(); }
	vector<shared_ptr<ModelAnimation>>& GetAnimations() { return _animations; }
	shared_ptr<ModelAnimation> GetAnimationByIndex(UINT index) { return (index < 0 || index >= _animations.size()) ? nullptr : _animations[index]; }
	shared_ptr<ModelAnimation> GetAnimationByName(wstring name);

private:
	//캐쉬정보 채워주기	
	void BindCacheInfo();

private:
	wstring _modelPath = L"../Resources/Models/";
	wstring _texturePath = L"../Resources/Textures/";

private:
	shared_ptr<ModelBone> _root;
	vector<shared_ptr<Material>> _materials;
	vector<shared_ptr<ModelBone>> _bones;
	vector<shared_ptr<ModelMesh>> _meshes;
	vector<shared_ptr<ModelAnimation>> _animations;
};

 

Model.cpp

void Model::ReadAnimation(wstring filename)
{
	wstring fullPath = _modelPath + filename + L".clip";

	shared_ptr<FileUtils> file = make_shared<FileUtils>();
	file->Open(fullPath, FileMode::Read);

	shared_ptr<ModelAnimation> animation = make_shared<ModelAnimation>();

	animation->name = Utils::ToWString(file->Read<string>());
	animation->duration = file->Read<float>();
	animation->frameRate = file->Read<float>();
	animation->frameCount = file->Read<uint32>();

	uint32 keyframesCount = file->Read<uint32>();

	for (uint32 i = 0; i < keyframesCount; i++)
	{
		shared_ptr<ModelKeyframe> keyframe = make_shared<ModelKeyframe>();
		keyframe->boneName = Utils::ToWString(file->Read<string>());

		//Transform 개수
		uint32 size = file->Read<uint32>();
		if (size > 0)
		{
			keyframe->transforms.resize(size);
			void* ptr = &keyframe->transforms[0];
			file->Read(&ptr, sizeof(ModelKeyframeData) * size);
		}

		//해쉬맵
		animation->keyframes[keyframe->boneName] = keyframe;
	}

	//중첩되어 저장
	_animations.push_back(animation);
}

shared_ptr<ModelAnimation> Model::GetAnimationByName(wstring name)
{
	for (auto& animation : _animations)
	{
		if (animation->name == name)
			return animation;
	}

	return nullptr;
}

 

이렇게 해준다음 vector에서 push_back 다음부분에 break point를 잡아두고 실제 구동되는 클래스를 실행시켜보면 정상적으로 작동되고 있는것을 볼 수 있다.

AnimationDemo.cpp

#include "pch.h"
#include "AnimationDemo.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 "ImGuiDemo.h"

void AnimationDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"16. AnimationDemo.fx");

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

	CreateKachujin();

	RENDER->Init(_shader);
}

void AnimationDemo::Update()
{
	_camera->Update();
	RENDER->Update();

	{
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		RENDER->PushLightData(lightDesc);
	}

	{
		_obj->Update();
	}
}

void AnimationDemo::Render()
{

}

void AnimationDemo::CreateKachujin()
{
	shared_ptr<class Model> m1 = make_shared<Model>();
	m1->ReadModel(L"Kachujin/Kachujin");
	m1->ReadMaterial(L"Kachujin/Kachujin");
	m1->ReadAnimation(L"Kachujin/Idle");
	m1->ReadAnimation(L"Kachujin/Run");
	m1->ReadAnimation(L"Kachujin/Slash");

	_obj = make_shared<GameObject>();
	_obj->GetOrAddTransform()->SetPosition(Vec3(0, 0, 1));
	_obj->GetOrAddTransform()->SetScale(Vec3(0.01f));

	_obj->AddComponent(make_shared<ModelRenderer>(_shader));
	{
		_obj->GetModelRenderer()->SetModel(m1);
		//_obj->GetModelAnimator()->SetPass(1);
	}

}

 

 

이제 유니티에서 Animator과 같이 애니메이션을 렌더링해주는 클래스를 만들어주자.

일단은 모델자체를 읽어오는 것부터 시작해보자.

ModelAnimator.h

#pragma once
#include "Component.h"

class Model;

class ModelAnimator : public Component
{
	using Super = Component;

public:
	ModelAnimator(shared_ptr<Shader> shader);
	~ModelAnimator();


	virtual void Update() override;

	void SetModel(shared_ptr<Model> model);
	void SetPass(uint8 pass) { _pass = pass; }

private:
	shared_ptr<Shader>	_shader;
	uint8				_pass = 0;		//통로
	shared_ptr<Model>	_model;

};

ModelAnimator.cpp

#include "pch.h"
#include "ModelAnimator.h"
#include "Material.h"
#include "ModelMesh.h"
#include "Model.h"

ModelAnimator::ModelAnimator(shared_ptr<Shader> shader)
	: Super(ComponentType::Animator), _shader(shader)
{

}

ModelAnimator::~ModelAnimator()
{
}

void ModelAnimator::Update()
{
	if (_model == nullptr)
		return;

	//TODO

	//Bones -> shader
	BoneDesc boneDesc;

	const uint32 boneCount = _model->GetBoneCount();

	for (uint32 i = 0; i < boneCount; i++)
	{
		shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i);
		boneDesc.transforms[i] = bone->transform;
	}
	RENDER->PushBoneData(boneDesc);

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

	const auto& meshes = _model->GetMeshes();
	for (auto& mesh : meshes)
	{
		if (mesh->material)
			mesh->material->Update();

		//BoneIndex
		_shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex);

		uint32 stride = mesh->vertexBuffer->GetStride();
		uint32 offset = mesh->vertexBuffer->GetOffset();

		//각 매쉬를 버퍼에 할당
		DC->IASetVertexBuffers(0, 1, mesh->vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
		DC->IASetIndexBuffer(mesh->indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);

		_shader->DrawIndexed(0, _pass, mesh->indexBuffer->GetCount(), 0, 0);
	}
}

void ModelAnimator::SetModel(shared_ptr<Model> model)
{
	_model = model;

	const auto& materials = _model->GetMaterials();
	for (auto& material : materials)
	{
		material->SetShader(_shader);
	}
}

 

이렇게 해준다음 GameObejct클래스에서 이 클래스에 대한 헬퍼함수를 만들어주자

GameObject.h

#pragma once
#include "Component.h"
class MonoBehaviour;
class Transform;
class Camera;
class MeshRenderer;
class ModelRenderer;
class ModelAnimator;

class GameObject : public enable_shared_from_this<GameObject>
{
public:
	GameObject();
	~GameObject();

	void Awake();
	void Start();
	void Update();
	void LateUpdate();
	void FixedUpdate();

	shared_ptr<Component> GetFixedComponent(ComponentType type);
	shared_ptr<Transform> GetTransform();
	shared_ptr<Camera> GetCamera();
	shared_ptr<MeshRenderer> GetMeshRenderer();
	shared_ptr<ModelRenderer> GetModelRenderer();
	shared_ptr<ModelAnimator> GetModelAnimator();

	shared_ptr<Transform> GetOrAddTransform();
	void AddComponent(shared_ptr<Component> component);

protected:
	array<shared_ptr<Component>, FIXED_COMPONENT_COUNT> _components;
	vector<shared_ptr<MonoBehaviour>> _scripts;
};

 

GameObject.cpp

std::shared_ptr<ModelAnimator> GameObject::GetModelAnimator()
{
	shared_ptr<Component> component = GetFixedComponent(ComponentType::Animator);
	return static_pointer_cast<ModelAnimator>(component);
}

 

이렇게 해준다음 이 랜더러를 모델 오브젝트에 추가해주고 model과 pass를 정해주면 된다.

 

AnimationDemo.cpp

#include "pch.h"
#include "AnimationDemo.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"

void AnimationDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"16. AnimationDemo.fx");

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

	CreateKachujin();

	RENDER->Init(_shader);
}

void AnimationDemo::Update()
{
	_camera->Update();
	RENDER->Update();

	{
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		RENDER->PushLightData(lightDesc);
	}

	{
		_obj->Update();
	}
}

void AnimationDemo::Render()
{

}

void AnimationDemo::CreateKachujin()
{
	shared_ptr<class Model> m1 = make_shared<Model>();
	m1->ReadModel(L"Kachujin/Kachujin");
	m1->ReadMaterial(L"Kachujin/Kachujin");
	m1->ReadAnimation(L"Kachujin/Idle");
	m1->ReadAnimation(L"Kachujin/Run");
	m1->ReadAnimation(L"Kachujin/Slash");

	_obj = make_shared<GameObject>();
	_obj->GetOrAddTransform()->SetPosition(Vec3(0, 0, 1));
	_obj->GetOrAddTransform()->SetScale(Vec3(0.01f));

	_obj->AddComponent(make_shared<ModelAnimator>(_shader));
	{
		_obj->GetModelAnimator()->SetModel(m1);
		_obj->GetModelAnimator()->SetPass(1);
	}
}

 

이렇게 되면 정상적으로 모델이 출력된다.

 

 

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

 

5킬로그램 봉지를 최대한 쓰고 나머지에 3킬로그램 봉지를 쓰면서 정확하게 N그램에 맞춰야한다. 그게 불가능하다면 -1을 출력해준다.

 

정답코드

#include "iostream"

using namespace std;

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

    int cnt = 0;

    
    while (n >= 0) {
        if (n % 5 == 0) {  // 5로 나누어 떨어지면
            cnt += n / 5;  // 5kg 봉지 개수 더함
            cout << cnt << endl;
            return 0;
        }
        n -= 3;  // 5로 나누어 떨어지지 않으면 3kg 봉지를 하나 사용
        cnt++;  // 3kg 봉지 사용했으므로 봉지 개수 추가
    }

    // 만약 정확히 N kg을 만들 수 없으면 -1 출력
    cout << -1 << endl;
    return 0;
}

 

정답코드2 

https://www.inflearn.com/course/lecture?courseSlug=directx11-%EA%B2%8C%EC%9E%84%EA%B0%9C%EB%B0%9C-%EB%8F%84%EC%95%BD%EB%B0%98&unitId=161145

 

학습 페이지

 

www.inflearn.com

 

 

이제 불러온 모델을 통해 애니메이션을 구현해보자 

1.이론

2D 애니메이션이었다면 우리가 여러 스프라이트를 틀어주는 방식으로 구현할 수 있을 것이다. 하지만 3D에서 이런방식

으로 구현하기에는 효율적이지 않기 때문에 각 노드, 뼈를 움직이는 것으 애니메이션을 구현한다.

이때 한 뼈가 움직인다면 다른 뼈를 어떻게 처리해줄 것인지가 중요하다. 여기서 우리가 전에 사용했던 Transform을 계층관계를 통해 다시 사용하게 된다. 

여기서 우리가 처음에 불러온 정점을 사용하게 된다. 이 정점은 초기 자세에서의 글로벌 좌표이다. 이것을 역행렬을 통해 다시 상대좌표로 바꾼다음 이  상대좌표를 바꾸고 다시 글로벌 좌표로 바꾸면 되는 것이다.

하지만 이 정점을 모두 움직이기에는 용량이 너무 크기 때문에 정점을 뼈대에 따라 움직이게 해주면 된다. 한 정점이 여러 뼈대에 영향을 받을 수 있는데 이럴 때는 비율을 정해두고 영향을 받도록 한다. 이것을 스키닝이라고 한다.

 

2.스키닝

일단 애니메이션에 들어가기에 앞서 스키닝에 대한 실습을 진행해보자.

스킨은 자신이 어떠한 뼈에 영향을 받아서 움직일 것인지에 대한 정보를 말한다.

이때 스킨 정보에 관련된 변수는 만들어두었던 VertexData의 구조체에서 변수 중에 blendIndicies는 뼈대의 번호 blendWeights는 그 뼈대에 얼마나 영향을 받을 것인가를 저장해준다. 

우선 스킨에 대한 정보를 추출해야한다 매쉬를 순회하면서 뼈대가 있다면 본을 순회하면서 연관된 정점과 가중치 정보를 전달해주어야하는데 Assimp에서는 이 정보가 정점이 아닌 뼈대에 들어있기 때문에 이 정보를 추출해서 정점에 Parsing하는 작업이 필요하다.

이를 위해 정점마다 뼈번호와, 가중치를 저장할 구조체를 선언해주고 이를 파싱해줄 구조체도 만들어주자.

AsTypes.h

//Animation

struct asBlendWeight
{
	//뼈 목록
	Vec4 indices = Vec4(0, 0, 0, 0);
	Vec4 weights = Vec4(0, 0, 0, 0);

	void Set(uint32 index, uint32 boneIndex, float weight)
	{
		float i = (float)boneIndex;
		float w = weight;

		switch (index)
		{
		case 0: indices.x = i; weights.x = w; break;
		case 1: indices.y = i; weights.y = w; break;
		case 2: indices.z = i; weights.z = w; break;
		case 3: indices.w = i; weights.w = w; break;
		}
	}
};

//영향을 주는 각 뼈들 - 정점마다 - (뼈번호, 가중치)
struct asBoneWeights
{
	void AddWeights(uint32 boneIndex, float weight)
	{
		if (weight <= 0.0f)
			return;
		
		//weight 값이 나보다 작은애가 있는지 찾는다.-> 위치찾기
		auto findIt = std::find_if(boneWeights.begin(), boneWeights.end(),
			[weight](const Pair& p) {return weight > p.second; });

		//그 위치앞에 추가
		boneWeights.insert(findIt, Pair(boneIndex, weight));
	}

	asBlendWeight GetBlendWeight()
	{
		asBlendWeight blendWeights;

		for (uint32 i = 0; i < boneWeights.size(); i++)
		{
			if (i >= 4)
				break;

			//가중치설정
			blendWeights.Set(i, boneWeights[i].first, boneWeights[i].second);
		}

		return blendWeights;
	}

	// 정규화 합 1로 맞춰주기 
	void Normalize()
	{
		if (boneWeights.size() >= 4)
			boneWeights.resize(4);

		float totalWeight = 0.f;
		for (const auto& item : boneWeights)
			totalWeight += item.second;

		float scale = 1.f / totalWeight;
		for (const auto& item : boneWeights)
			item.second *= scale;
	}

	using Pair = pair<int32, float>;
	vector<Pair> boneWeights;

};

 

이 구조체를 사용하여 정점마다 뼈대 가중치 정보를 먼저 가져오고이를 정점마다 4개의 뼈/ 가중치 목록으로 채워주는 함수를 완성해주자.

Converter.cpp

void Converter::ReadSkinData()
{
	for (uint32 i = 0; i < _scene->mNumMeshes; i++)
	{
		aiMesh* srcMesh = _scene->mMeshes[i];
		if (srcMesh->HasBones() == false)
			continue; 

		shared_ptr<asMesh> mesh = _meshes[i];

		vector<asBoneWeights> tempVertexBoneWeights;
		tempVertexBoneWeights.resize(mesh->vertices.size());

		//Bone 순회 -> 연관된 VertexId, Weight 구해서 기록
		for (uint32 b = 0; b < srcMesh->mNumBones; b++)
		{
			aiBone* srcMeshBone = srcMesh->mBones[b];
			uint32 boneIndex = GetBoneIndex(srcMeshBone->mName.C_Str());

			for (uint32 w = 0; w < srcMeshBone->mNumWeights; w++)
			{
				uint32 index = srcMeshBone->mWeights[w].mVertexId;
				float weight = srcMeshBone->mWeights[w].mWeight;

				//정점마다 가중치 넣어주기
				tempVertexBoneWeights[index].AddWeights(boneIndex, weight);
			}
		}

		//최종 결과 계산 - 정점에 있는 뼈의 번호로 정리
		for (uint32 v = 0; v < tempVertexBoneWeights.size(); v++)
		{
			tempVertexBoneWeights[v].Normalize();

			asBlendWeight blendWeight = tempVertexBoneWeights[v].GetBlendWeight();
			mesh->vertices[v].blendIndices = blendWeight.indices;
			mesh->vertices[v].blendWeights = blendWeight.weights;
		}
	}
}

 

이렇게 해주면 정점마다 가중치정보가 저장되는데 이를 확인하기 위해 이 정보를 모두 엑셀파일에 저장하고 이 파일을 확인해보자.

Converter.cpp

void Converter::ExportModelData(wstring savePath)
{
	wstring finalPath = _modelPath + savePath + L".mesh";
	ReadModelData(_scene->mRootNode, -1, -1);		//메모리 -> CustomData
	ReadSkinData();

	//Write CSV File
	{
		FILE* file;
		::fopen_s(&file, "../Vertices.csv", "w");

		for (shared_ptr<asBone>& bone : _bones)
		{
			string name = bone->name;
			::fprintf(file, "%d,%s\n", bone->index, bone->name.c_str());
		}

		::fprintf(file, "\n");

		for (shared_ptr<asMesh>& mesh : _meshes)
		{
			string name = mesh->name;
			::printf("%s\n", name.c_str());

			for (UINT i = 0; i < mesh->vertices.size(); i++)
			{
				Vec3 p = mesh->vertices[i].position;
				Vec4 indices = mesh->vertices[i].blendIndices;
				Vec4 weights = mesh->vertices[i].blendWeights;

				::fprintf(file, "%f,%f,%f,", p.x, p.y, p.z);
				::fprintf(file, "%f,%f,%f,%f,", indices.x, indices.y, indices.z, indices.w);
				::fprintf(file, "%f,%f,%f,%f\n", weights.x, weights.y, weights.z, weights.w);
			}
		}

		::fclose(file);
	}

	WriteModelFile(finalPath);		//메모리 -> 파일
}

이렇게 해주면 정보가 담긴 엑셀파일이 생겨있는것을 볼 수 있다.

 

 

이제 뼈대를 움직일 때 이 정보를 바탕으로 정점을 움직여주면 된다. 이제 관절의 움직임과 전체적인 애니메이션 데이터를 가지고 와서 사용해주면 될것같다.



이제 GUI를 사용해 보자. GUI는 ImGUI라는 라이브러리를 가져와서 사용해볼 것인데 이것은 따로 라이브러리 파일이 있다기보다는 솔루션파일에서 필요한 부분의 소스코드를 가져와서 사용하면 된다.

일단 ImGUI를 사용하기위한 소스코드를 모두 가져오고 AssimpTool에서 새로운 클래스를 만들어서 이곳에서 우리가 가져와서 사용해보도록하자

가져오는 파일목록

ImGuiDemo.h

#pragma once
#include "IExecute.h"

class ImGuiDemo : public IExecute
{
public:
	void Init() override;
	void Update() override;
	void Render() override;

	void Test();

private:
	bool show_demo_window = true;
	bool show_another_window = false;
	Vec4 clear_color = Vec4(0.f);
};

ImGuiDemo.cpp

#include "pch.h"
#include "ImGuiDemo.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"

void ImGuiDemo::Init()
{
	//Setup Dear ImGui context
	IMGUI_CHECKVERSION();
	ImGui::CreateContext();
	ImGuiIO& io = ImGui::GetIO(); (void)io;
	io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;     // Enable Keyboard Controls
	io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;      // Enable Gamepad Controls

	//Setup Dear ImGui style
	ImGui::StyleColorsDark();
	//ImGui::StyleColorsLight();

	//Setup Platform/Renderer backends
	ImGui_ImplWin32_Init(GAME->GetGameDesc().hWnd);
	ImGui_ImplDX11_Init(DEVICE.Get(), DC.Get());
}

void ImGuiDemo::Update()
{
	// Start the Dear ImGui frame
	ImGui_ImplDX11_NewFrame();
	ImGui_ImplWin32_NewFrame();
	ImGui::NewFrame();

	//UI
	Test();
}

void ImGuiDemo::Render()
{
	// Rendering
	ImGui::Render();

	/*const float clear_color_with_alpha[4] = { clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w };
	g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, NULL);
	g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, clear_color_with_alpha);*/

	ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
}

void ImGuiDemo::Test()
{
	// 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
	if (show_demo_window)
		ImGui::ShowDemoWindow(&show_demo_window);

	// 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window.
	{
		static float f = 0.0f;
		static int counter = 0;

		ImGui::Begin("Hello, world!");                          // Create a window called "Hello, world!" and append into it.

		ImGui::Text("This is some useful text.");               // Display some text (you can use a format strings too)
		ImGui::Checkbox("Demo Window", &show_demo_window);      // Edit bools storing our window open/close state
		ImGui::Checkbox("Another Window", &show_another_window);

		ImGui::SliderFloat("float", &f, 0.0f, 1.0f);            // Edit 1 float using a slider from 0.0f to 1.0f
		ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color

		if (ImGui::Button("Button"))                            // Buttons return true when clicked (most widgets return true when edited/activated)
			counter++;
		ImGui::SameLine();
		ImGui::Text("counter = %d", counter);

		//ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
		ImGui::End();
	}

	// 3. Show another simple window.
	if (show_another_window)
	{
		ImGui::Begin("Another Window", &show_another_window);   // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked)
		ImGui::Text("Hello from another window!");
		if (ImGui::Button("Close Me"))
			show_another_window = false;
		ImGui::End();
	}
}

 

이렇게 해주면 일단 뭔가 뜨기는한다. 하지만 눌러도 반응이 없고 잘려서 보인다.

 

 

일단 왜 마우스입력이 처리되지 않는지 살펴보기 위해 우리가 입력을 처리했던 제일 메인함수인 Game으로 돌아가보고 ImGUI 샘플코드와 비교해보자. 비교해보면 샘플에서는 입력을 처리하기 위해 윈도우 헨틀를 두고 사용하고 있다는 것을 알 수 있다. 우리도 이 방식을 활용하여 전방선언을 해준 뒤에 Extern 하여 사용해주자

Game.cpp

#include "pch.h"
#include "Game.h"
#include "IExecute.h"

extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

LRESULT CALLBACK Game::WndProc(HWND handle, UINT message, WPARAM wParam, LPARAM lParam)
{
	if (ImGui_ImplWin32_WndProcHandler(handle, message, wParam, lParam))
		return true;

	switch (message)
	{
	case WM_SIZE:
		break;
	case WM_CLOSE:
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		return ::DefWindowProc(handle, message, wParam, lParam);
	}
}

 

이렇게 해주면 이제 입력이 처리되는 모습을 볼 수 있다.

 

이제 샘플코드의 내용을 분석하고 우리의 방식대로 활용해보자. 우선 Test함수에서 2번부분을 복사한 다음 우리의 방식
대로 사용해보자. 

ImGuiDemo.cpp

void ImGuiDemo::Test()
{
	// 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
	if (show_demo_window)
		ImGui::ShowDemoWindow(&show_demo_window);

	// 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window.
	{
		static float f = 0.0f;
		static int counter = 0;

		ImGui::Begin("Hello, world!");                          // Create a window called "Hello, world!" and append into it.

		ImGui::Text("This is some useful text.");               // Display some text (you can use a format strings too)
		ImGui::Checkbox("Demo Window", &show_demo_window);      // Edit bools storing our window open/close state
		ImGui::Checkbox("Another Window", &show_another_window);

		ImGui::SliderFloat("float", &f, 0.0f, 1.0f);            // Edit 1 float using a slider from 0.0f to 1.0f
		ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color

		if (ImGui::Button("Button"))                            // Buttons return true when clicked (most widgets return true when edited/activated)
			counter++;
		ImGui::SameLine();
		ImGui::Text("counter = %d", counter);

		//ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
		ImGui::End();
	}

	//MY Code
	{
		static float f = 0.0f;
		static int counter = 0;

		ImGui::Begin("Hello, world!");                          // Create a window called "Hello, world!" and append into it.
		
		ImGui::Text("Sudden Hello From Me");

		//ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
		ImGui::End();
	}

	// 3. Show another simple window.
	if (show_another_window)
	{
		ImGui::Begin("Another Window", &show_another_window);   // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked)
		ImGui::Text("Hello from another window!");
		if (ImGui::Button("Close Me"))
			show_another_window = false;
		ImGui::End();
	}
}

 

이렇게 해주게되면 밑의 결과처럼 Hello World창에서 Text가 제일 밑에 출력되고 있다. 

 

생각을 해보면 Begin의 키값에 따라 지정된 영역에 추가되는 것이라고 볼 수 있다. Begin의 값을 다르게 하면 다른 창이 새로 생기는 것을 볼 수 있다.

 

이제 우리가 작업할 때 새로운 부분을 추가하는 매니저 클래스를 만들고 이 매니저에 초기화하는 부분과 렌더해주는 

부분을 넣어주자

ImGuiManager.h

#pragma once

class ImGuiManager
{
	DECLARE_SINGLE(ImGuiManager);

public:
	void Init();
	void Update();
	void Render();

};

ImGuiManager.cpp

#include "pch.h"
#include "ImGuiManager.h"


void ImGuiManager::Init()
{
	IMGUI_CHECKVERSION();
	ImGui::CreateContext();
	ImGuiIO& io = ImGui::GetIO(); (void)io;
	io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;     // Enable Keyboard Controls
	io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;      // Enable Gamepad Controls

	// Setup Dear ImGui style
	ImGui::StyleColorsDark();
	//ImGui::StyleColorsLight();

	// Setup Platform/Renderer backends
	ImGui_ImplWin32_Init(GAME->GetGameDesc().hWnd);
	ImGui_ImplDX11_Init(DEVICE.Get(), DC.Get());
}

void ImGuiManager::Update()
{
	ImGui_ImplDX11_NewFrame();
	ImGui_ImplWin32_NewFrame();
	ImGui::NewFrame();
}

void ImGuiManager::Render()
{
	// Rendering
	ImGui::Render();
	ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
}

 

이렇게 해줬으면 매니저를 Game쪽에서 사용해서 초기화 및 렌더부분을 추가해주고 Demo 클래스에서 초기화 및 렌더부분은 제거해주자

Game.cpp

#include "pch.h"
#include "Game.h"
#include "IExecute.h"

extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

WPARAM Game::Run(GameDesc& desc)
{
	_desc = desc;
	assert(_desc.app != nullptr);

	// 1) 윈도우 창 정보 등록
	MyRegisterClass();

	// 2) 윈도우 창 생성
	if (!InitInstance(SW_SHOWNORMAL))
		return FALSE;
		
	GRAPHICS->Init(_desc.hWnd);
	TIME->Init();
	INPUT->Init(_desc.hWnd);
	GUI->Init();
	
	_desc.app->Init();

	MSG msg = { 0 };

	while (msg.message != WM_QUIT)
	{
		if (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
		{
			::TranslateMessage(&msg);
			::DispatchMessage(&msg);
		}
		else
		{
			Update();
		}
	}

	return msg.wParam;
}


ATOM Game::MyRegisterClass()
{
	WNDCLASSEXW wcex;

	wcex.cbSize = sizeof(WNDCLASSEX);

	wcex.style = CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc = WndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = _desc.hInstance;
	wcex.hIcon = ::LoadIcon(NULL, IDI_WINLOGO);
	wcex.hCursor = ::LoadCursor(nullptr, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.lpszMenuName = NULL;
	wcex.lpszClassName = _desc.appName.c_str();
	wcex.hIconSm = wcex.hIcon;

	return RegisterClassExW(&wcex);
}

BOOL Game::InitInstance(int cmdShow)
{
	RECT windowRect = { 0, 0, _desc.width, _desc.height };
	::AdjustWindowRect(&windowRect, WS_OVERLAPPEDWINDOW, false);

	_desc.hWnd = CreateWindowW(_desc.appName.c_str(), _desc.appName.c_str(), WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, 0, windowRect.right - windowRect.left, windowRect.bottom - windowRect.top, nullptr, nullptr, _desc.hInstance, nullptr);

	if (!_desc.hWnd)
		return FALSE;

	::ShowWindow(_desc.hWnd, cmdShow);
	::UpdateWindow(_desc.hWnd);

	return TRUE;
}

LRESULT CALLBACK Game::WndProc(HWND handle, UINT message, WPARAM wParam, LPARAM lParam)
{
	if (ImGui_ImplWin32_WndProcHandler(handle, message, wParam, lParam))
		return true;

	switch (message)
	{
	case WM_SIZE:
		break;
	case WM_CLOSE:
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		return ::DefWindowProc(handle, message, wParam, lParam);
	}
}

void Game::Update()
{
	TIME->Update();
	INPUT->Update();

	GRAPHICS->RenderBegin();

	GUI->Update();
	_desc.app->Update();
	_desc.app->Render();
	GUI->Render();

	GRAPHICS->RenderEnd();
}

 

ImGuiDemo.cpp

#include "pch.h"
#include "ImGuiDemo.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"

void ImGuiDemo::Init()
{
	//Setup Dear ImGui context
	//IMGUI_CHECKVERSION();
	//ImGui::CreateContext();
	//ImGuiIO& io = ImGui::GetIO(); (void)io;
	//io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;     // Enable Keyboard Controls
	//io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;      // Enable Gamepad Controls

	//Setup Dear ImGui style
	//ImGui::StyleColorsDark();
	//ImGui::StyleColorsLight();

	//Setup Platform/Renderer backends
	//ImGui_ImplWin32_Init(GAME->GetGameDesc().hWnd);
	//ImGui_ImplDX11_Init(DEVICE.Get(), DC.Get());
}

void ImGuiDemo::Update()
{
	// Start the Dear ImGui frame
	//ImGui_ImplDX11_NewFrame();
	//ImGui_ImplWin32_NewFrame();
	//ImGui::NewFrame();

	//UI
	Test();
}

void ImGuiDemo::Render()
{
	// Rendering
	//ImGui::Render();

	/*const float clear_color_with_alpha[4] = { clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w };
	//g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, NULL);
	//g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, clear_color_with_alpha);*/

	//ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
}

void ImGuiDemo::Test()
{
	// 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
	if (show_demo_window)
		ImGui::ShowDemoWindow(&show_demo_window);

	// 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window.
	{
		static float f = 0.0f;
		static int counter = 0;

		ImGui::Begin("Hello, world!");                          // Create a window called "Hello, world!" and append into it.

		ImGui::Text("This is some useful text.");               // Display some text (you can use a format strings too)
		ImGui::Checkbox("Demo Window", &show_demo_window);      // Edit bools storing our window open/close state
		ImGui::Checkbox("Another Window", &show_another_window);

		ImGui::SliderFloat("float", &f, 0.0f, 1.0f);            // Edit 1 float using a slider from 0.0f to 1.0f
		ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color

		if (ImGui::Button("Button"))                            // Buttons return true when clicked (most widgets return true when edited/activated)
			counter++;
		ImGui::SameLine();
		ImGui::Text("counter = %d", counter);

		//ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
		ImGui::End();
	}

	//MY Code
	{
		static float f = 0.0f;
		static int counter = 0;

		ImGui::Begin("My World!",nullptr,
			ImGuiWindowFlags_NoTitleBar |
			ImGuiWindowFlags_NoMove);                         
		
		ImGui::Text("Sudden Hello From Me");

		//ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
		ImGui::End();
	}

	// 3. Show another simple window.
	if (show_another_window)
	{
		ImGui::Begin("Another Window", &show_another_window);   // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked)
		ImGui::Text("Hello from another window!");
		if (ImGui::Button("Close Me"))
			show_another_window = false;
		ImGui::End();
	}
}

 

이렇게 해주면 정상적으로 작동한다.



 

타워 대신에 탱크fbx 파일을 통해 탱크를 로드해보자 

AssimpTool Class에서 fbx 파일을 가져와서 Material과 Model로 Export 해주고 이를 받아와서 화면에 띄워주자

AssimpTool.cpp

void AssimpTool::Init()
{

	{
		shared_ptr<Converter> converter = make_shared<Converter>();

		// FBX -> Memory
		converter->ReadAssetFile(L"Tank/Tank.fbx");

		//Memory -> CustomData (File)
		converter->ExportMaterialData(L"Tank/Tank");
		converter->ExportModelData(L"Tank/Tank");

		//CustomData (File) -> Memory

	}
}

 

이렇게 해서 실행해주면 mesh와 xml 파일이 생기게 된다.

그리고 다시 StaticMeshDemo 클래스에서 우리가 저장해준 형식 탱크를 가져와서 ModelRenderer를 통해 화면에 띄워주자

StaticMeshDemo.cpp

void StaticMeshDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"15. ModelDemo.fx");

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

	//CreateTower();
	CreateTank();

	RENDER->Init(_shader);
}

void StaticMeshDemo::CreateTank()
{
	// CustomData -> Memory
	shared_ptr<class Model> m1 = make_shared<Model>();
	m1->ReadModel(L"Tank/Tank");
	m1->ReadMaterial(L"Tank/Tank");

	_obj = make_shared<GameObject>();
	_obj->GetOrAddTransform()->SetPosition(Vec3(0, 0, 50));
	_obj->GetOrAddTransform()->SetScale(Vec3(0.1f));

	_obj->AddComponent(make_shared<ModelRenderer>(_shader));
	{
		_obj->GetModelRenderer()->SetModel(m1);
		_obj->GetModelRenderer()->SetPass(1);
	}
}

 

이렇게 해주면 이제 탱크가 화면에 나온다.

 

보면 탱크와 비슷하지만 조금 이상하다는 것을 알 수 있다. 이렇게 되는 이유는 우리가 Bone 즉 노드에 대한 정보를 활용하지 않고 그냥 화면에 띄워주기만 했기때문이다. 지금 각 노드의 좌표가 루트로 향하는 좌표로 바꿔져있지만 지금은 그 좌표를 쓰고 있지 않기 때문에 이 개별노드를 배치할 때 처리를 해주어야 한다. 

 

일단 쉐이더쪽에 각 뼈대의 Transform을 받아줄 수 있도록 Matrix를 조장할 수 있는 버퍼를 선언해주고 이 쉐이더로 값을 넣을 수 있게 RenderManager에 구조체와 버퍼를 선언 및 초기화해주고 데이터를 할당해주는 함수를 만들어주자.

RenderManager.h

#pragma once
#include "ConstantBuffer.h"

class Shader;

struct GlobalDesc
{
	Matrix V = Matrix::Identity;
	Matrix P = Matrix::Identity;
	Matrix VP = Matrix::Identity;
	Matrix VInv = Matrix::Identity;
};

struct TransformDesc
{
	Matrix W = Matrix::Identity;
};

//Light
struct LightDesc
{
	Color ambient = Color(1.f, 1.f, 1.f, 1.f);
	Color diffuse = Color(1.f, 1.f, 1.f, 1.f);
	Color specular = Color(1.f, 1.f, 1.f, 1.f);
	Color emissive = Color(1.f, 1.f, 1.f, 1.f);

	Vec3 direction;
	float padding0;
};

struct MaterialDesc
{
	Color ambient = Color(0.f, 0.f, 0.f, 1.f);
	Color diffuse = Color(1.f, 1.f, 1.f, 1.f);
	Color specular = Color(0.f, 0.f, 0.f, 1.f);
	Color emissive = Color(0.f, 0.f, 0.f, 1.f);
};


//Bone
#define MAX_BONE_TRANSFORMS 50

struct BoneDesc
{
	Matrix transforms[MAX_BONE_TRANSFORMS];
};

class RenderManager
{
	DECLARE_SINGLE(RenderManager);

public:
	void Init(shared_ptr<Shader> shader);
	void Update();

	void PushGlobalData(const Matrix& view, const Matrix& projection);
	void PushTransformData(const TransformDesc& desc);
	void PushLightData(const LightDesc& desc);
	void PushMaterialData(const MaterialDesc& desc);
	void PushBoneData(const BoneDesc& desc);

private:
	shared_ptr<Shader> _shader;

	//프레임마다 한번만 세팅
	GlobalDesc _globalDesc;
	shared_ptr<ConstantBuffer<GlobalDesc>> _globalBuffer;
	//정보넘겨주기
	ComPtr<ID3DX11EffectConstantBuffer> _globalEffectBuffer;


	TransformDesc _transformDesc;
	shared_ptr<ConstantBuffer<TransformDesc>> _transformBuffer;
	//정보넘겨주기
	ComPtr<ID3DX11EffectConstantBuffer> _transformEffectBuffer;

	LightDesc _lightDesc;
	shared_ptr<ConstantBuffer<LightDesc>> _lightBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _lightEffectBuffer;

	MaterialDesc _materialDesc;
	shared_ptr<ConstantBuffer<MaterialDesc>> _materialBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _materialEffectBuffer;

	BoneDesc _boneDesc;
	shared_ptr<ConstantBuffer<BoneDesc>> _boneBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _boneEffectBuffer;
};

RenderManager.cpp

void RenderManager::Init(shared_ptr<Shader> shader)
{
	_shader = shader;

	_globalBuffer = make_shared<ConstantBuffer<GlobalDesc>>();
	_globalBuffer->Create();
	_globalEffectBuffer = _shader->GetConstantBuffer("GlobalBuffer");

	_transformBuffer = make_shared<ConstantBuffer<TransformDesc>>();
	_transformBuffer->Create();
	_transformEffectBuffer = _shader->GetConstantBuffer("TransformBuffer");

	_lightBuffer = make_shared<ConstantBuffer<LightDesc>>();
	_lightBuffer->Create();
	_lightEffectBuffer = _shader->GetConstantBuffer("LightBuffer");

	_materialBuffer = make_shared<ConstantBuffer<MaterialDesc>>();
	_materialBuffer->Create();
	_materialEffectBuffer = _shader->GetConstantBuffer("MaterialBuffer");

	_boneBuffer = make_shared<ConstantBuffer<BoneDesc>>();
	_boneBuffer->Create();
	_boneEffectBuffer = _shader->GetConstantBuffer("BoneBuffer");
}

void RenderManager::PushBoneData(const BoneDesc& desc)
{
	_boneDesc = desc;
	_boneBuffer->CopyData(_boneDesc);
	_boneEffectBuffer->SetConstantBuffer(_boneBuffer->GetComPtr().Get());
}

 

 

이제 이 Bone의 매트릭스를 곱해줘서 로컬좌표계로 가도록 처리해주자

ModelDemo.fx

#define MAX_MODEL_TRANSFORMS 50

cbuffer BoneBuffer
{
    matrix BoneTransform[MAX_MODEL_TRANSFORMS];
};

uint BoneIndex;
  
MeshOutput VS(VertexTextureNormalTangent input)
{
    MeshOutput output;
    output.position = mul(input.position, BoneTransform[BoneIndex]);
    output.position = mul(output.position, W);
    output.worldPosition = output.position.xyz;
    output.position = mul(output.position, VP);
    output.uv = input.uv;
    //회전만 고려
    output.normal = mul(input.normal, (float3x3) W);
    output.tangent = mul(input.tangent, (float3x3) W);

    return output;
}

 

 

이렇게 해주면 이제 탱크모양이 정상적으로 나오게된다.

 

처음에 쉐이더에 들어가는 좌표는 우리가 Converter 클래스를 통해 가져온 Vertex 이다. 즉, 각 물체의 정점정보를 전달하고 있는 것이다. 이 좌표는 자기자신을 기준으로 하는 좌표계이다.

 

처음에 중심에 모여있던 이유는 각 계층별로 루트노드가 기준이 아니라 그냥 자기자신의 좌표를 바탕으로 원점에 배치가 되기 때문에 모든 계층이 겹쳐서 보였던 것이다. 그래서 이것을 바꿔주기 위해 VertexShader 단계에서 루트에서부터 타고오면서 자신의 부모의 좌표를 통해 계산된 자신의 Transform의 좌표를 통해 계산해주었다.

이렇게 해주어야 원래 루트를 기준으로 하는 로컬좌표를 계산해준 것으로 정해준 모양대로 출력될 수 있는 것이다.

 

 

2.Bone,Mesh 가져오기

이제 모델의 정보를 파싱해오기 위해서 루트노드(-1,-1)에서 부터 계층적으로 정보를 읽어주는 작업해주도록 하자. 

지금은 스캘레톤을 저장하는 것 보다는 계층별로 접근하는 노드 하나하나가 Bone 뼈대라고 생각하고 저장해보자 이렇게 트리 구조의 정보를 가져올 떄는 재귀함수를 사용하는 것이 좋다. 

fbx에서 transform 정보를 가져오려면 어떤 좌표로 저장되어 있는지 알아야한다. fbx에서는 transform이 Relative 좌표로 저장되어 있기 때문에 부모의 Transform Matrix를 가져와서 곱해주는 것으로 루트로 향하는 상대좌표로 바꾸어 준다. 

이때 루트노드부터 재귀적으로 함수가 작동하기 때문에 루트로 향하는 상대좌표로 바꿔줄 수 있는 것이다. 

Converter.cpp

//노드- 데이터 저장단위
void Converter::ReadModelData(aiNode* node, int32 index, int32 parent)
{
	shared_ptr<asBone> bone = make_shared<asBone>();
	bone->index = index;
	bone->parent = parent;
	bone->name = node->mName.C_Str();

	//transform 추출 - 주소를 받아주기
	Matrix transform(node->mTransformation[0]);
	//fbx -> 4*4가 전치되어있어서 다시 전치해주어야한다.
	// 상대좌표
	bone->transform = transform.Transpose();


	//루트에 가져온 메트릭스 - 재귀
	Matrix matParent = Matrix::Identity;
	if (parent >= 0)
		matParent = _bones[parent]->transform;
	//Local(Root) Transform
	bone->transform = bone->transform * matParent;

	_bones.push_back(bone);

	//Mesh
	ReadMeshData(node, index);


	//계층타기 - 재귀
	for (uint32 i = 0; i < node->mNumChildren; i++)
		ReadModelData(node->mChildren[i], _bones.size(), index);
}

Mesh도 Bone을 가져올 때 같이 작동하도록 하자. Mesh는 하나의 Mesh에 SubMesh가 여러개 있을 수도 있기 때문에 이 서브 매쉬도 어떻게 관리할지도 생각해보아야한다. 

각 노드에 저장된 매쉬데이터는 제일 상위노드인 Scene노드에서 몇번쨰 매쉬에 데이터가 저장되어 있는지 인덱스 정보를 가지고 있다. 그렇기 때문에 인덱스를 가지고 온 다음  Scene에서 mesh를 인덱스 번호를 통해 가져오면 된다. 각 서브Mesh는 정점의 크기를 통해 indices 벡터를 관리해주어서 각 서브 Mesh를 하나의 Vector에서 관리할 수 있도록 하자.

Converter.cpp

void Converter::ReadMeshData(aiNode* node, int32 bone)
{
	if (node->mNumMeshes < 1)
		return;

	shared_ptr<asMesh> mesh = make_shared<asMesh>();
	mesh->name = node->mName.C_Str();
	mesh->boneIndex = bone;

	for (uint32 i = 0; i < node->mNumMeshes; i++)
	{
		//Scene에서 매쉬 번호
		uint32 index = node->mMeshes[i];
		const aiMesh* srcMesh = _scene->mMeshes[index]; 

		// Material Name - Material 과의 관계를 위해
		const aiMaterial* material = _scene->mMaterials[srcMesh->mMaterialIndex];
		mesh->materialName = material->GetName().C_Str();

		//시작 정점번호 - 서브매쉬 구분을 위하여
		const uint32 startVertex = mesh->vertices.size();

		for (uint32 v = 0; v < srcMesh->mNumVertices; v++)
		{
			// Vertex
			VetexType vertex;
			::memcpy(&vertex.position, &srcMesh->mVertices[v], sizeof(Vec3));

			// UV
			if (srcMesh->HasTextureCoords(0))
				::memcpy(&vertex.uv, &srcMesh->mTextureCoords[0][v], sizeof(Vec2));

			// Normal
			if (srcMesh->HasNormals())
				::memcpy(&vertex.normal, &srcMesh->mNormals[v], sizeof(Vec3));

			mesh->vertices.push_back(vertex);
		}

		// Index
		for (uint32 f = 0; f < srcMesh->mNumFaces; f++)
		{
			aiFace& face = srcMesh->mFaces[f];

			for (uint32 k = 0; k < face.mNumIndices; k++)
				mesh->indices.push_back(face.mIndices[k] + startVertex);
		}
	}

	_meshes.push_back(mesh);
}

 

3. 모델 띄우기

이제 최종적으로 저장된 파일을 메모리로 가져와서 띄워보자. 일단 파일을 읽어오고 그 파일을 쓰고 읽어오는 코드를 담은 FileUtils클래스를 만들어주자.

FileUtils.h

#pragma once

//파일입출력용
enum FileMode :uint8
{
	Write,
	Read,
};

class FileUtils
{
public:
	FileUtils();
	~FileUtils();

	void Open(wstring filePath, FileMode mode);

	//쓸때 어떤자료형 쓸지 모름
	template<typename T>
	void Write(const T& data)
	{
		DWORD numOfBytes = 0;
		assert(::WriteFile(_handle, &data, sizeof(T), (LPDWORD)&numOfBytes, nullptr));
	}
	//템플릿특수화 - 예외적 상황가정
	template<>
	void Write<string>(const string& data)
	{
		return Write(data);
	}

	void Write(void* data, uint32 dataSize);
	void Write(const string& data);

	template<typename T>
	void Read(OUT T& data)
	{
		DWORD numOfBytes = 0;
		assert(::ReadFile(_handle, &data, sizeof(T), (LPDWORD)&numOfBytes, nullptr));
	}

	template<typename T>
	T Read()
	{
		T data;
		Read(data);
		return data;
	}

	void Read(void** data, uint32 dataSize);
	void Read(OUT string& data);

private:
	HANDLE _handle = INVALID_HANDLE_VALUE;
};

FileUtils.cpp

#include "pch.h"
#include "FileUtils.h"


FileUtils::FileUtils()
{

}

FileUtils::~FileUtils()
{
	if (_handle != INVALID_HANDLE_VALUE)
	{
		::CloseHandle(_handle);
		_handle = INVALID_HANDLE_VALUE;
	}
}

// 읽기 / 쓰기모드이냐에 따라 다르게
//WinAPI
void FileUtils::Open(wstring filePath, FileMode mode)
{
	if (mode == FileMode::Write)
	{
		_handle = ::CreateFile(
			filePath.c_str(),
			GENERIC_WRITE,
			0,
			nullptr,
			CREATE_ALWAYS,
			FILE_ATTRIBUTE_NORMAL,
			nullptr
		);
	}
	else
	{
		_handle = ::CreateFile
		(
			filePath.c_str(),
			GENERIC_READ,
			FILE_SHARE_READ,
			nullptr,
			OPEN_EXISTING,
			FILE_ATTRIBUTE_NORMAL,
			nullptr
		);
	}

	assert(_handle != INVALID_HANDLE_VALUE);
}

void FileUtils::Write(void* data, uint32 dataSize)
{
	uint32 numOfBytes = 0;
	assert(::WriteFile(_handle, data, dataSize, reinterpret_cast<LPDWORD>(&numOfBytes), nullptr));
}

void FileUtils::Write(const string& data)
{
	uint32 size = (uint32)data.size();
	Write(size);

	if (data.size() == 0)
		return;

	Write((void*)data.data(), size);
}

void FileUtils::Read(void** data, uint32 dataSize)
{
	uint32 numOfBytes = 0;
	assert(::ReadFile(_handle, *data, dataSize, reinterpret_cast<LPDWORD>(&numOfBytes), nullptr));
}

void FileUtils::Read(OUT string& data)
{
	uint32 size = Read<uint32>();

	if (size == 0)
		return;

	//사이즈만큼 동적할당
	char* temp = new char[size + 1];
	temp[size] = 0;
	Read((void**)&temp, size);
	data = temp;
	delete[] temp;
}

 

이제 이 클래스를 활용하여 Bone 과 Mesh 정보를 하나로 모아서 모델정보로 저장하는 함수를 완성해주자 이때 각 구조체내부의 변수의 순서와 자료형을 잘 맞춰주자. 

Converter.cpp

//바이너리 파일로 관리
void Converter::WriteModelFile(wstring finalPath)
{
	auto path = filesystem::path(finalPath);

	// 폴더가 없으면 만든다.
	filesystem::create_directory(path.parent_path());

	shared_ptr<FileUtils> file = make_shared<FileUtils>();
	file->Open(finalPath, FileMode::Write);

	// Bone Data
	file->Write<uint32>(_bones.size());
	for (shared_ptr<asBone>& bone : _bones)
	{
		//순서, 자료형 잘맞춰주기 
		file->Write<int32>(bone->index);
		file->Write<string>(bone->name);
		file->Write<int32>(bone->parent);
		file->Write<Matrix>(bone->transform);
	}

	// Mesh Data
	file->Write<uint32>(_meshes.size());
	for (shared_ptr<asMesh>& meshData : _meshes)
	{
		file->Write<string>(meshData->name);
		file->Write<int32>(meshData->boneIndex);
		file->Write<string>(meshData->materialName);

		// Vertex Data
		file->Write<uint32>(meshData->vertices.size());
		file->Write(&meshData->vertices[0], sizeof(VertexType) * meshData->vertices.size());

		// Index Data
		file->Write<uint32>(meshData->indices.size());
		file->Write(&meshData->indices[0], sizeof(uint32) * meshData->indices.size());
	}
}

 

이제 메모리로 가져와서 모델을 띄워보자. 일단 우리가 메모리로 가져오기 위해 엔진쪽에서 모델정보와 모델의 Bone, Mesh 정보를 가지고 있을 Model, ModelMesh 클래스를 만들어주자.

ModelMesh.h

#pragma once

struct ModelBone
{
	wstring name;
	int32 index;
	int32 parentIndex;
	shared_ptr<ModelBone> parent; // Cache

	Matrix transform;
	vector<shared_ptr<ModelBone>> children; // Cache
};

struct ModelMesh
{
	//버퍼 채워주기
	void CreateBuffers();

	wstring name;

	// Mesh
	shared_ptr<Geometry<ModelVertexType>> geometry = make_shared<Geometry<ModelVertexType>>();
	shared_ptr<VertexBuffer> vertexBuffer;
	shared_ptr<IndexBuffer> indexBuffer;

	// Material
	wstring materialName = L"";
	shared_ptr<Material> material; // Cache

	// Bones
	int32 boneIndex;
	shared_ptr<ModelBone> bone; // Cache;
};

ModelMesh.cpp

#include "pch.h"
#include "ModelMesh.h"

void ModelMesh::CreateBuffers()
{
	vertexBuffer = make_shared<VertexBuffer>();
	vertexBuffer->Create(geometry->GetVertices());
	indexBuffer = make_shared<IndexBuffer>();
	indexBuffer->Create(geometry->GetIndices());
}

Model.h

#pragma once

struct ModelBone;
struct ModelMesh;

class Model : public enable_shared_from_this<Model>
{
public:
	Model();
	~Model();

public:
	//custom -> Memory
	void ReadMaterial(wstring filename);
	void ReadModel(wstring filename);

	//헬퍼함수
	uint32 GetMaterialCount() { return static_cast<uint32>(_materials.size()); }
	vector<shared_ptr<Material>>& GetMaterials() { return _materials; }
	shared_ptr<Material> GetMaterialByIndex(uint32 index) { return _materials[index]; }
	shared_ptr<Material> GetMaterialByName(const wstring& name);

	uint32 GetMeshCount() { return static_cast<uint32>(_meshes.size()); }
	vector<shared_ptr<ModelMesh>>& GetMeshes() { return _meshes; }
	shared_ptr<ModelMesh> GetMeshByIndex(uint32 index) { return _meshes[index]; }
	shared_ptr<ModelMesh> GetMeshByName(const wstring& name);

	uint32 GetBoneCount() { return static_cast<uint32>(_bones.size()); }
	vector<shared_ptr<ModelBone>>& GetBones() { return _bones; }
	shared_ptr<ModelBone> GetBoneByIndex(uint32 index) { return (index < 0 || index >= _bones.size() ? nullptr : _bones[index]); }
	shared_ptr<ModelBone> GetBoneByName(const wstring& name);
private:
	//캐쉬정보 채워주기	
	void BindCacheInfo();

private:
	wstring _modelPath = L"../Resources/Models/";
	wstring _texturePath = L"../Resources/Textures/";

private:
	shared_ptr<ModelBone> _root;
	vector<shared_ptr<Material>> _materials;
	vector<shared_ptr<ModelBone>> _bones;
	vector<shared_ptr<ModelMesh>> _meshes;
};

Model.cpp

#include "pch.h"
#include "Model.h"
#include "Utils.h"
#include "FileUtils.h"
#include "tinyxml2.h"
#include <filesystem>
#include "Material.h"
#include "ModelMesh.h"


Model::Model()
{

}

Model::~Model()
{

}

void Model::ReadMaterial(wstring filename)
{
	wstring fullPath = _texturePath + filename + L".xml";
	auto parentPath = filesystem::path(fullPath).parent_path();

	tinyxml2::XMLDocument* document = new tinyxml2::XMLDocument();
	tinyxml2::XMLError error = document->LoadFile(Utils::ToString(fullPath).c_str());
	assert(error == tinyxml2::XML_SUCCESS);

	tinyxml2::XMLElement* root = document->FirstChildElement();
	tinyxml2::XMLElement* materialNode = root->FirstChildElement();

	while (materialNode)
	{
		shared_ptr<Material> material = make_shared<Material>();

		tinyxml2::XMLElement* node = nullptr;

		node = materialNode->FirstChildElement();
		material->SetName(Utils::ToWString(node->GetText()));

		// Diffuse Texture
		node = node->NextSiblingElement();
		if (node->GetText())
		{
			wstring textureStr = Utils::ToWString(node->GetText());
			if (textureStr.length() > 0)
			{
				auto texture = RESOURCES->GetOrAddTexture(textureStr, (parentPath / textureStr).wstring());
				material->SetDiffuseMap(texture);
			}
		}

		// Specular Texture
		node = node->NextSiblingElement();
		if (node->GetText())
		{
			wstring texture = Utils::ToWString(node->GetText());
			if (texture.length() > 0)
			{
				wstring textureStr = Utils::ToWString(node->GetText());
				if (textureStr.length() > 0)
				{
					auto texture = RESOURCES->GetOrAddTexture(textureStr, (parentPath / textureStr).wstring());
					material->SetSpecularMap(texture);
				}
			}
		}

		// Normal Texture
		node = node->NextSiblingElement();
		if (node->GetText())
		{
			wstring textureStr = Utils::ToWString(node->GetText());
			if (textureStr.length() > 0)
			{
				auto texture = RESOURCES->GetOrAddTexture(textureStr, (parentPath / textureStr).wstring());
				material->SetNormalMap(texture);
			}
		}

		// Ambient
		{
			node = node->NextSiblingElement();

			Color color;
			color.x = node->FloatAttribute("R");
			color.y = node->FloatAttribute("G");
			color.z = node->FloatAttribute("B");
			color.w = node->FloatAttribute("A");
			material->GetMaterialDesc().ambient = color;
		}

		// Diffuse
		{
			node = node->NextSiblingElement();

			Color color;
			color.x = node->FloatAttribute("R");
			color.y = node->FloatAttribute("G");
			color.z = node->FloatAttribute("B");
			color.w = node->FloatAttribute("A");
			material->GetMaterialDesc().diffuse = color;
		}

		// Specular
		{
			node = node->NextSiblingElement();

			Color color;
			color.x = node->FloatAttribute("R");
			color.y = node->FloatAttribute("G");
			color.z = node->FloatAttribute("B");
			color.w = node->FloatAttribute("A");
			material->GetMaterialDesc().specular = color;
		}

		// Emissive
		{
			node = node->NextSiblingElement();

			Color color;
			color.x = node->FloatAttribute("R");
			color.y = node->FloatAttribute("G");
			color.z = node->FloatAttribute("B");
			color.w = node->FloatAttribute("A");
			material->GetMaterialDesc().emissive = color;
		}

		_materials.push_back(material);

		// Next Material
		materialNode = materialNode->NextSiblingElement();
	}

	BindCacheInfo();
}

void Model::ReadModel(wstring filename)
{
	wstring fullPath = _modelPath + filename + L".mesh";

	shared_ptr<FileUtils> file = make_shared<FileUtils>();
	file->Open(fullPath, FileMode::Read);

	// Bones
	{
		const uint32 count = file->Read<uint32>();

		for (uint32 i = 0; i < count; i++)
		{
			shared_ptr<ModelBone> bone = make_shared<ModelBone>();
			bone->index = file->Read<int32>();
			bone->name = Utils::ToWString(file->Read<string>());
			bone->parentIndex = file->Read<int32>();
			bone->transform = file->Read<Matrix>();

			_bones.push_back(bone);
		}
	}

	// Mesh
	{
		const uint32 count = file->Read<uint32>();

		for (uint32 i = 0; i < count; i++)
		{
			shared_ptr<ModelMesh> mesh = make_shared<ModelMesh>();

			mesh->name = Utils::ToWString(file->Read<string>());
			mesh->boneIndex = file->Read<int32>();

			// Material
			mesh->materialName = Utils::ToWString(file->Read<string>());

			//VertexData
			{
				const uint32 count = file->Read<uint32>();
				vector<ModelVertexType> vertices;
				vertices.resize(count);

				void* data = vertices.data();
				file->Read(&data, sizeof(ModelVertexType) * count);
				mesh->geometry->AddVertices(vertices);
			}

			//IndexData
			{
				const uint32 count = file->Read<uint32>();

				vector<uint32> indices;
				indices.resize(count);

				void* data = indices.data();
				file->Read(&data, sizeof(uint32) * count);
				mesh->geometry->AddIndices(indices);
			}

			mesh->CreateBuffers();

			_meshes.push_back(mesh);
		}
	}

	BindCacheInfo();
}


std::shared_ptr<Material> Model::GetMaterialByName(const wstring& name)
{
	for (auto& material : _materials)
	{
		if (material->GetName() == name)
			return material;
	}

	return nullptr;
}

std::shared_ptr<ModelMesh> Model::GetMeshByName(const wstring& name)
{
	for (auto& mesh : _meshes)
	{
		if (mesh->name == name)
			return mesh;
	}

	return nullptr;
}

std::shared_ptr<ModelBone> Model::GetBoneByName(const wstring& name)
{
	for (auto& bone : _bones)
	{
		if (bone->name == name)
			return bone;
	}

	return nullptr;
}

void Model::BindCacheInfo()
{
	// Mesh에 Material 캐싱
	for (const auto& mesh : _meshes)
	{
		// 이미 찾았으면 스킵
		if (mesh->material != nullptr)
			continue;

		mesh->material = GetMaterialByName(mesh->materialName);
	}

	// Mesh에 Bone 캐싱
	for (const auto& mesh : _meshes)
	{
		// 이미 찾았으면 스킵
		if (mesh->bone != nullptr)
			continue;

		mesh->bone = GetBoneByIndex(mesh->boneIndex);
	}

	// Bone 계층 정보 채우기
	if (_root == nullptr && _bones.size() > 0)
	{
		_root = _bones[0];

		for (const auto& bone : _bones)
		{
			if (bone->parentIndex >= 0)
			{
				bone->parent = _bones[bone->parentIndex];
				bone->parent->children.push_back(bone);
			}
			else
			{
				bone->parent = nullptr;
			}
		}
	}
}

 

그리고 이 모델을 Render해주는 부분을 MeshRenderer처럼 컴포넌트로 만들어서 관리해주도록 하자.

ModelRenderer.h

#pragma once
#include "Component.h"

class Model;
class Shader;
class Material;

class ModelRenderer : public Component
{
	using Super = Component;

public:
	ModelRenderer(shared_ptr<Shader> shader);
	virtual ~ModelRenderer();

	virtual void Update() override;

	void SetModel(shared_ptr<Model> model);
	void SetPass(uint8 pass) { _pass = pass; }
	
private:
	shared_ptr<Shader>	_shader;
	uint8				_pass = 0;		//통로
	shared_ptr<Model>	_model;

};

ModelRenderer.cpp

#include "pch.h"
#include "ModelRenderer.h"
#include "Material.h"
#include "ModelMesh.h"
#include "Model.h"

ModelRenderer::ModelRenderer(shared_ptr<Shader> shader)
	: Super(ComponentType::ModelRenderer), _shader(shader)
{

}

ModelRenderer::~ModelRenderer()
{

}


void ModelRenderer::Update()
{
	if (_model == nullptr)
		return;

	auto world = GetTransform()->GetWorldMatrix();
	RENDER->PushTransformData(TransformDesc{ world });

	const auto& meshes = _model->GetMeshes();
	for (auto& mesh : meshes)
	{
		if (mesh->material)
			mesh->material->Update();

		uint32 stride = mesh->vertexBuffer->GetStride();
		uint32 offset = mesh->vertexBuffer->GetOffset();

		DC->IASetVertexBuffers(0, 1, mesh->vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
		DC->IASetIndexBuffer(mesh->indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);

		_shader->DrawIndexed(0, _pass, mesh->indexBuffer->GetCount(), 0, 0);
	}
}


void ModelRenderer::SetModel(shared_ptr<Model> model)
{
	_model = model;

	const auto& materials = _model->GetMaterials();
	for (auto& material : materials)
	{
		material->SetShader(_shader);
	}
}

 

이제 모델을 띄워보기위해 메인 클래스인 StaticMeshModel를 만들어주자.

StaticMeshModel.h

#pragma once
#include "IExecute.h"

class StaticMeshDemo : public IExecute
{
public:
	void Init() override;
	void Update() override;
	void Render() override;

	void CreateTower();

private:
	shared_ptr<Shader> _shader;
	shared_ptr<GameObject> _obj;
	shared_ptr<GameObject> _camera;
};

StaticMeshModel.cpp

#include "pch.h"
#include "StaticMeshDemo.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"

void StaticMeshDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"15. ModelDemo.fx");

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

	CreateTower();

	RENDER->Init(_shader);
}

void StaticMeshDemo::Update()
{
	_camera->Update();
	RENDER->Update();

	{
		LightDesc lightDesc;
		lightDesc.ambient = Vec4(0.4f);
		lightDesc.diffuse = Vec4(1.f);
		lightDesc.specular = Vec4(0.f);
		lightDesc.direction = Vec3(1.f, 0.f, 1.f);
		RENDER->PushLightData(lightDesc);
	}

	{
		_obj->Update();
	}
}

void StaticMeshDemo::Render()
{

}

void StaticMeshDemo::CreateTower()
{
	// CustomData -> Memory
	shared_ptr<class Model> m1 = make_shared<Model>();
	m1->ReadModel(L"Tower/Tower");
	m1->ReadMaterial(L"Tower/Tower");

	_obj = make_shared<GameObject>();
	_obj->GetOrAddTransform()->SetPosition(Vec3(0, 0, 50));
	_obj->GetOrAddTransform()->SetScale(Vec3(1.0f));

	_obj->AddComponent(make_shared<ModelRenderer>(_shader));
	{
		_obj->GetModelRenderer()->SetModel(m1);
		_obj->GetModelRenderer()->SetPass(1);
	}
}

 

그리고 물체를 좀 더 잘 보이도록 빨간색 와이어프레임으로 보이게 쉐이더를 수정해주자

ModelDemo.fx

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

MeshOutput VS(VertexTextureNormalTangent input)
{
    MeshOutput output;
    output.position = mul(input.position, W);
    output.worldPosition = output.position.xyz;
    output.position = mul(output.position, VP);
    output.uv = input.uv;
    //회전만 고려
    output.normal = mul(input.normal, (float3x3) W);
    output.tangent = mul(input.tangent, (float3x3) W);

    return output;
}

float4 PS(MeshOutput input) : SV_TARGET
{
	//ComputeNormalMapping(input.normal, input.tangent, input.uv);
	//float4 color = ComputeLight(input.normal, input.uv, input.worldPosition);
    float4 color = DiffuseMap.Sample(LinearSampler, input.uv);

    return color;
}

float4 PS_RED(MeshOutput input) : SV_TARGET
{
    return float4(1, 0, 0, 1);
}

technique11 T0
{
	PASS_VP(P0, VS, PS)
    PASS_RS_VP(P1, FillModeWireFrame, VS, PS_RED)
};

 

이렇게 해준 다음 실행해보면 타워가 잘 로드된것을 볼 수 있다.

 



+ Recent posts