오늘은 애니메이션을 가진 모델을 인스턴싱할 수 있도록 코드를 수정해주자. 우선 메인코드에서 모델과 모델애니메이션을 부르는 코드를 추가해주자.

AnimInstancingDemo.cpp

void AnimInstancingDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"22. AnimInstancingDemo.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>());

	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");

	for (int32 i = 0; i < 500; i++)
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetPosition(Vec3(rand() % 100, 0, rand() % 100));
		obj->GetOrAddTransform()->SetScale(Vec3(0.01f));
		obj->AddComponent(make_shared<ModelRenderer>(_shader));
		{
			obj->GetModelRenderer()->SetModel(m1);
		}
		_objs.push_back(obj);
	}

	RENDER->Init(_shader);
}

 

이렇게 해주면 애니메이션이 적용되지 않은 기본자세의 모델이 출력되고 있는 모습을 볼 수 있다.

 

 

이제 코드를 본격적으로 수정해보자. 쉐이더코드를 먼저 수정해주자. 우리가 최종적으로 애니메이션 작업을 했던 

TweenDemo.fx 코드를 참고하여 글로벌에서 루트, 루트에서 다시 다른자세로 가는 행렬을 구해줬던 부분을 넣어주는게

핵심이다.

AnimInstancingDemo.fx

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

#define MAX_MODEL_TRANSFORMS 250
#define MAX_MODEL_KEYFRAMES 500


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

struct TweenFrameDesc
{
    float tweenDuration; //트랜지션 지속시간
    float tweenRatio;
    float tweenSumTime;
    float padding;
    KeyframeDesc curr;
    KeyframeDesc next;
};

cbuffer TweenBuffer
{
    TweenFrameDesc TweenFrames;
};

cbuffer BoneBuffer
{
    matrix BoneTransform[MAX_MODEL_TRANSFORMS];
};

uint BoneIndex;
Texture2DArray TransformMap;

struct VS_IN
{
    float4 position : POSITION;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
    float3 tangent : TANGENT;
    float4 blendIndices : BLEND_INDICES;
    float4 blendWeights : BLEND_WEIGHTS;
	// INSTANCING
    uint instanceID : SV_INSTANCEID;
    matrix world : INST;
};

struct VS_OUT
{
    float4 position : SV_POSITION;
    float3 worldPosition : POSITION1;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
};

matrix GetAnimationMatrix(VS_IN 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[2];
    int currFrame[2];
    int nextFrame[2];
    float ratio[2];
    
    animIndex[0] = TweenFrames.curr.animIndex;
    currFrame[0] = TweenFrames.curr.currFrame;
    nextFrame[0] = TweenFrames.curr.nextFrame;
    ratio[0] = TweenFrames.curr.ratio;

    animIndex[1] = TweenFrames.next.animIndex;
    currFrame[1] = TweenFrames.next.currFrame;
    nextFrame[1] = TweenFrames.next.nextFrame;
    ratio[1] = TweenFrames.next.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[0], animIndex[0], 0));
        c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame[0], animIndex[0], 0));
        c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame[0], animIndex[0], 0));
        c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame[0], animIndex[0], 0));
        curr = matrix(c0, c1, c2, c3);

        n0 = TransformMap.Load(int4(indices[i] * 4 + 0, nextFrame[0], animIndex[0], 0));
        n1 = TransformMap.Load(int4(indices[i] * 4 + 1, nextFrame[0], animIndex[0], 0));
        n2 = TransformMap.Load(int4(indices[i] * 4 + 2, nextFrame[0], animIndex[0], 0));
        n3 = TransformMap.Load(int4(indices[i] * 4 + 3, nextFrame[0], animIndex[0], 0));
        next = matrix(n0, n1, n2, n3);

        matrix result = lerp(curr, next, ratio[0]);

		// 다음 애니메이션
        if (animIndex[1] >= 0)
        {
            c0 = TransformMap.Load(int4(indices[i] * 4 + 0, currFrame[1], animIndex[1], 0));
            c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame[1], animIndex[1], 0));
            c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame[1], animIndex[1], 0));
            c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame[1], animIndex[1], 0));
            curr = matrix(c0, c1, c2, c3);

            n0 = TransformMap.Load(int4(indices[i] * 4 + 0, nextFrame[1], animIndex[1], 0));
            n1 = TransformMap.Load(int4(indices[i] * 4 + 1, nextFrame[1], animIndex[1], 0));
            n2 = TransformMap.Load(int4(indices[i] * 4 + 2, nextFrame[1], animIndex[1], 0));
            n3 = TransformMap.Load(int4(indices[i] * 4 + 3, nextFrame[1], animIndex[1], 0));
            next = matrix(n0, n1, n2, n3);

            matrix nextResult = lerp(curr, next, ratio[1]);
            result = lerp(result, nextResult, TweenFrames.tweenRatio);
        }

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

    return transform;
}

VS_OUT VS(VS_IN input)
{
    VS_OUT output;

    //output.position = mul(input.position, BoneTransform[BoneIndex]);    //루트기준좌표
    
    matrix m = GetAnimationMatrix(input);
    output.position = mul(input.position, m);
    //물체마다 World 가져오기
    output.position = mul(output.position, input.world); // W
    output.worldPosition = output.position;
    output.position = mul(output.position, VP);
    output.uv = input.uv;
    output.normal = input.normal;

    return output;
}


float4 PS(VS_OUT input) : SV_TARGET
{
	//float4 color = ComputeLight(input.normal, input.uv, input.worldPosition);
    float4 color = DiffuseMap.Sample(LinearSampler, input.uv);
    return color;
}

technique11 T0
{
	PASS_VP(P0, VS, PS)
};

 

이제 본격적으로 정보를 전달해주는 부분인 ModelAnimator를 수정해주자. 우선 생각해봐야하는 것은 인스턴싱을 한다고 했을 때 어느부분이 동일하고 어느부분이 다를까를 생각해봐야한다. 이때 우리가 애니메이션 별로 매프레임마다 뼈의

위치가 어디있는지 모아서 SRV를 통해 텍스처맵을 전달해주고 있는데 이 부분을 어떻게 해줄지 생각해보자.

일단 텍스처맵은 애니메이션이 급격하게 바뀌는 것이 아니라서 공용으로 사용해주어도 좋을 것 같다.

 

TweenDesc은 현재 애니메이션 정보와 다음 애니메이션 정보를 가지고 있는데 이것을 공용으로 사용해주면 같은 오브젝트 정보를 가지고 있다면 무조건 같은 애니메이션이 실행되기 때문에 같은 오브젝트라도 각기 다르게 움직일 수 있어야 하기 때문에 수정해줘야한다.

일단 이러한 정보를 자기혼자 렌더링하는 함수에서 인스턴싱을 할 수 있도록 Animator 클래스 코드를 수정해주자

ModelAnimator.h

#pragma once
#include "Component.h"

class Model;

struct AnimTransform
{
	using TransformArrayType = array<Matrix, MAX_MODEL_TRANSFORMS>;

	//2차배열
	array<TransformArrayType, MAX_MODEL_KEYFRAMES> transforms;
	
};

class ModelAnimator : public Component
{
	using Super = Component;

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


	void SetModel(shared_ptr<Model> model);
	void SetPass(uint8 pass) { _pass = pass; }
	
	virtual void Update() override;
	void UpdateTweenData();
	void RenderInstancing(shared_ptr<class InstancingBuffer>& buffer);
	InstanceID GetInstanceID();

private:
	void CreateTexture();
	void CreateAnimationTransform(uint32 index);

private:
	vector<AnimTransform> _animTransforms;
	ComPtr<ID3D11Texture2D> _texture;
	//Texture
	ComPtr<ID3D11ShaderResourceView> _srv;

private:
	TweenDesc _tweenDesc;

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

ModelAnimator::ModelAnimator(shared_ptr<Shader> shader)
	: Super(ComponentType::Animator), _shader(shader)
{
	// TEST
	_tweenDesc.next.animIndex = rand() % 3;
	_tweenDesc.tweenSumTime += rand() % 100;
}

ModelAnimator::~ModelAnimator()
{
}

void ModelAnimator::Update()
{

}

void ModelAnimator::UpdateTweenData()
{
	TweenDesc& desc = _tweenDesc;

	desc.curr.sumTime += DT;
	//현재 애니메이션
	{
		shared_ptr<ModelAnimation> currentAnim = _model->GetAnimationByIndex(desc.curr.animIndex);
		if (currentAnim)
		{
			float timePerFrame = 1 / (currentAnim->frameRate * desc.curr.speed);
			if (desc.curr.sumTime >= timePerFrame)
			{
				desc.curr.sumTime = 0;
				desc.curr.currFrame = (desc.curr.currFrame + 1) % currentAnim->frameCount;
				desc.curr.nextFrame = (desc.curr.currFrame + 1) % currentAnim->frameCount;
			}

			desc.curr.ratio = (desc.curr.sumTime / timePerFrame);
		}
	}

	// 다음 애니메이션이 예약 되어 있다면
	if (desc.next.animIndex >= 0)
	{
		desc.tweenSumTime += DT;
		desc.tweenRatio = desc.tweenSumTime / desc.tweenDuration;

		if (desc.tweenRatio >= 1.f)
		{
			// 애니메이션 교체 성공
			desc.curr = desc.next;
			desc.ClearNextAnim();
		}
		else
		{
			// 교체중
			shared_ptr<ModelAnimation> nextAnim = _model->GetAnimationByIndex(desc.next.animIndex);
			desc.next.sumTime += DT;

			float timePerFrame = 1.f / (nextAnim->frameRate * desc.next.speed);

			if (desc.next.ratio >= 1.f)
			{
				desc.next.sumTime = 0;

				desc.next.currFrame = (desc.next.currFrame + 1) % nextAnim->frameCount;
				desc.next.nextFrame = (desc.next.currFrame + 1) % nextAnim->frameCount;
			}

			desc.next.ratio = desc.next.sumTime / timePerFrame;
		}
	}
}

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

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

void ModelAnimator::RenderInstancing(shared_ptr<class InstancingBuffer>& buffer)
{
	if (_model == nullptr)
		return;

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

	//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);

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

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

		mesh->vertexBuffer->PushData();
		mesh->indexBuffer->PushData();

		buffer->PushData();
		_shader->DrawIndexedInstanced(0, _pass, mesh->indexBuffer->GetCount(), buffer->GetCount());
	}
}

InstanceID ModelAnimator::GetInstanceID()
{
	return make_pair((uint64)_model.get(), (uint64)_shader.get());
}

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

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

			// 처음 T 포즈기준
			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];
		}
	}
}

이렇게 해준다음 TweenData를 각각의 오브젝트에 따라 설정해주기 위해 배열로 만들어준다. 이때 쉐이더 코드에서 INSTANCE ID를 통해 몇번째 인스턴싱인지 알 수 있기 때문에 이를 통해 Tweendata에서 몇번째인지도 알 수 있다. 

AnimInstancingDemo.fx

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

#define MAX_MODEL_TRANSFORMS 250
#define MAX_MODEL_KEYFRAMES 500
#define MAX_MODEL_INSTANCE 500


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

struct TweenFrameDesc
{
    float tweenDuration; //트랜지션 지속시간
    float tweenRatio;
    float tweenSumTime;
    float padding;
    KeyframeDesc curr;
    KeyframeDesc next;
};

cbuffer TweenBuffer
{
    TweenFrameDesc TweenFrames[MAX_MODEL_INSTANCE];
};

cbuffer BoneBuffer
{
    matrix BoneTransform[MAX_MODEL_TRANSFORMS];
};

uint BoneIndex;
Texture2DArray TransformMap;

struct VS_IN
{
    float4 position : POSITION;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
    float3 tangent : TANGENT;
    float4 blendIndices : BLEND_INDICES;
    float4 blendWeights : BLEND_WEIGHTS;
	// INSTANCING
    uint instanceID : SV_INSTANCEID;
    matrix world : INST;
};

struct VS_OUT
{
    float4 position : SV_POSITION;
    float3 worldPosition : POSITION1;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
};

matrix GetAnimationMatrix(VS_IN 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[2];
    int currFrame[2];
    int nextFrame[2];
    float ratio[2];
    
    animIndex[0] = TweenFrames[input.instanceID].curr.animIndex;
    currFrame[0] = TweenFrames[input.instanceID].curr.currFrame;
    nextFrame[0] = TweenFrames[input.instanceID].curr.nextFrame;
    ratio[0] = TweenFrames[input.instanceID].curr.ratio;

    animIndex[1] = TweenFrames[input.instanceID].next.animIndex;
    currFrame[1] = TweenFrames[input.instanceID].next.currFrame;
    nextFrame[1] = TweenFrames[input.instanceID].next.nextFrame;
    ratio[1] = TweenFrames[input.instanceID].next.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[0], animIndex[0], 0));
        c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame[0], animIndex[0], 0));
        c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame[0], animIndex[0], 0));
        c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame[0], animIndex[0], 0));
        curr = matrix(c0, c1, c2, c3);

        n0 = TransformMap.Load(int4(indices[i] * 4 + 0, nextFrame[0], animIndex[0], 0));
        n1 = TransformMap.Load(int4(indices[i] * 4 + 1, nextFrame[0], animIndex[0], 0));
        n2 = TransformMap.Load(int4(indices[i] * 4 + 2, nextFrame[0], animIndex[0], 0));
        n3 = TransformMap.Load(int4(indices[i] * 4 + 3, nextFrame[0], animIndex[0], 0));
        next = matrix(n0, n1, n2, n3);

        matrix result = lerp(curr, next, ratio[0]);

		// 다음 애니메이션
        if (animIndex[1] >= 0)
        {
            c0 = TransformMap.Load(int4(indices[i] * 4 + 0, currFrame[1], animIndex[1], 0));
            c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame[1], animIndex[1], 0));
            c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame[1], animIndex[1], 0));
            c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame[1], animIndex[1], 0));
            curr = matrix(c0, c1, c2, c3);

            n0 = TransformMap.Load(int4(indices[i] * 4 + 0, nextFrame[1], animIndex[1], 0));
            n1 = TransformMap.Load(int4(indices[i] * 4 + 1, nextFrame[1], animIndex[1], 0));
            n2 = TransformMap.Load(int4(indices[i] * 4 + 2, nextFrame[1], animIndex[1], 0));
            n3 = TransformMap.Load(int4(indices[i] * 4 + 3, nextFrame[1], animIndex[1], 0));
            next = matrix(n0, n1, n2, n3);

            matrix nextResult = lerp(curr, next, ratio[1]);
            result = lerp(result, nextResult, TweenFrames[input.instanceID].tweenRatio);
        }

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

    return transform;
}

VS_OUT VS(VS_IN input)
{
    VS_OUT output;

    //output.position = mul(input.position, BoneTransform[BoneIndex]);    //루트기준좌표
    
    matrix m = GetAnimationMatrix(input);
    output.position = mul(input.position, m);
    //물체마다 World 가져오기
    output.position = mul(output.position, input.world); // W
    output.worldPosition = output.position;
    output.position = mul(output.position, VP);
    output.uv = input.uv;
    output.normal = input.normal;

    return output;
}


float4 PS(VS_OUT input) : SV_TARGET
{
	//float4 color = ComputeLight(input.normal, input.uv, input.worldPosition);
    float4 color = DiffuseMap.Sample(LinearSampler, input.uv);
    return color;
}

technique11 T0
{
	PASS_VP(P0, VS, PS)
};

 

이 쉐이더에 맞게 정보를 넘겨주는 버퍼를 만들어주자. 이것 또한 배열로 만들어주면된다.

RenderManager.h

struct InstancedTweenDesc
{
	TweenDesc tweens[MAX_MODEL_INSTANCE];
};

class RenderManager
{
	DECLARE_SINGLE(RenderManager);

public:
	void PushTweenData(const InstancedTweenDesc& desc);

private:
	InstancedTweenDesc _tweenDesc;
	shared_ptr<ConstantBuffer<InstancedTweenDesc>> _tweenBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _tweenEffectBuffer;

};

RenderManager.cpp

#include "pch.h"
#include "RenderManager.h"
#include "Camera.h"

void RenderManager::Init(shared_ptr<Shader> shader)
{
	_tweenBuffer = make_shared<ConstantBuffer<InstancedTweenDesc>>();
	_tweenBuffer->Create();
	_tweenEffectBuffer = _shader->GetConstantBuffer("TweenBuffer");
}

void RenderManager::PushTweenData(const InstancedTweenDesc& desc)
{
	_tweenDesc = desc;
	_tweenBuffer->CopyData(_tweenDesc);
	_tweenEffectBuffer->SetConstantBuffer(_tweenBuffer->GetComPtr().Get());
}

 

이렇게 버퍼를 넘겨주는 부분을 만들었지만 인스턴싱버퍼에는 각 오브젝트의 월드좌표만 가지고 있기 때문에 이 ModelAnimator의 TweenDesc 정보를 꺼내서 만들어준 버퍼를 통해 넘겨줘야한다. 

일단 Animator에서 TweenDesc를 가져오는 함수를 만들어주자.

ModelAnimator.h

#pragma once
#include "Component.h"

class ModelAnimator : public Component
{
public:
	TweenDesc& GetTweenDesc() { return _tweenDesc; }

};

 

그렇게 해준다음 매니저에서 정보를 업데이트 해준 다음 각 오브젝트의 TweenDesc를 가져온다음에 버퍼를 통해 밀어넣어주면된다.

InstancingManager.cpp

void InstancingManager::RenderAnimRenderer(vector<shared_ptr<GameObject>>& gameObjects)
{
	map<InstanceID, vector<shared_ptr<GameObject>>> cache;

	for (shared_ptr<GameObject>& gameObject : gameObjects)
	{
		if (gameObject->GetModelAnimator() == nullptr)
			continue;

		const InstanceID instanceId = gameObject->GetModelAnimator()->GetInstanceID();
		cache[instanceId].push_back(gameObject);
	}

	for (auto& pair : cache)
	{
		shared_ptr<InstancedTweenDesc> tweenDesc = make_shared<InstancedTweenDesc>();

		const vector<shared_ptr<GameObject>>& vec = pair.second;

		//if (vec.size() == 1)
		//{
		//	vec[0]->GetModelAnimator()->RenderSingle();
		//}
		//else
		{
			const InstanceID instanceId = pair.first;

			for (int32 i = 0; i < vec.size(); i++)
			{
				const shared_ptr<GameObject>& gameObject = vec[i];
				InstancingData data;
				data.world = gameObject->GetTransform()->GetWorldMatrix();

				AddData(instanceId, data);

				// INSTANCING
				gameObject->GetModelAnimator()->UpdateTweenData();
				tweenDesc->tweens[i] = gameObject->GetModelAnimator()->GetTweenDesc();
			}
			//트윈데이터 넣어주기
			RENDER->PushTweenData(*tweenDesc.get());

			shared_ptr<InstancingBuffer>& buffer = _buffers[instanceId];
			vec[0]->GetModelAnimator()->RenderInstancing(buffer);
		}
	}
}

 

이렇게 해주면 안정적인 프레임으로 각각의 오브젝트들에서 애니메이션이 동작하는것을 볼 수 있다.

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

 

N개의 문자를 받고 받은 문자가 M개의 문자에서 몇개나 있는지 검사하면 된다. 나는 map 자료형을 사용해서 string과 bool을 매치시켜주어서 N개의 문자에서 발견된거라면 true를 배정해주고 이를 m개의 단어를 받으면서 true라면

cnt를 증가시켜주었다. 

 

이때 unordered_map을 자주 사용하는데 

데이터의 정렬이 필요하지 않고, 평균적으로 빠른 성능을 원하는 경우 unordered_map이 적합하고 정렬, 추가적인 메모리 오버헤드 방지, 탐색성능이 예측가능한 상황이 필요할 때  map이 적합하다.

정답코드(map 사)

#include "iostream"
#include "map"
using namespace std;

map<string, bool> mp;
int main()
{
	int n, m, cnt = 0;

	cin >> n >> m;

	for (int i = 0; i < n; i++)
	{
		string word;
		cin >> word;
		mp[word] = true;
	}

	for (int i = 0; i < m; i++)
	{
		string word;
		cin >> word;
		if (mp[word]) cnt++;
	}

	cout << cnt << endl;

	return 0;
}

 

정답코드2(unordered_map 사용)

#include "iostream"
#include "unordered_map"
using namespace std;

unordered_map<string, bool> mp;
int main()
{
	int n, m, cnt = 0;

	cin >> n >> m;

	for (int i = 0; i < n; i++)
	{
		string word;
		cin >> word;
		mp[word] = true;
	}

	for (int i = 0; i < m; i++)
	{
		string word;
		cin >> word;
		if (mp[word]) cnt++;
	}

	cout << cnt << endl;

	return 0;
}

 

 

위가 unordered_map 방식이고 밑이 map방식으로 푼 결과이다. 



오늘은 Enemy 클래스를 만들어보자. Character를 상속받는 Enemy 클래스를 만들어주자. 

 

이 Enemy 클래스는 무기가 Pawn 으로 지정된 객체의 성질에는

Collision Enabled 하기 때문에 World Dynamic로 설정해주어야하고 보이는 것에 Collision 했을 때 막혀야한다.

그리고 카메라와의 충돌은 무시해야 하고 Hit 이벤트를 처리해줄 수 있게 오버랩 이벤트는 True로 설정해주자.

Enemy.cpp


#include "Enemy/Enemy.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/CapsuleComponent.h"


AEnemy::AEnemy()
{
	PrimaryActorTick.bCanEverTick = true;

	GetMesh()->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	GetMesh()->SetGenerateOverlapEvents(true);
	GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);

}

void AEnemy::BeginPlay()
{
	Super::BeginPlay();
	
}

void AEnemy::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void AEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

 

이렇게 하고 컴파일 해준다음에 언리얼에서 이 Enemy 클래스를 바탕으로 블루프린트 클래스를 생성해주자.

 

여기에 사용할 매쉬를 Mixamo에서 다운받아주자

 

이 캐릭터가 피격시에 피격 애니메이션을 출력해줘야하는데 이때 캐릭터가 피격에 따라 움직일 수 도 있다. 

이때 애니메이션 자체에 매쉬를 움직이는 애니메이션이 있는데 이를 Root Motion Animations라고 한다.

이 Root Motion Animations는 루트라는 최상단 뼈 노드를 통해 움직임을 제어한다. 이를 위해 만약 Root 노드가 없다면 블렌더와 같은 편집프로그램으로 만들어줘야한다.

 

이제 여기서 Hit모션이 기능하도록 만들어보자 

우선 전에 만들어둔 HitInterface를 활용하자. Enemy클래스에서 이 인터페이스를 상속받아서 이 인터페이스의 가상함수인 GetHit을 구현하도록 하면 된다.

이때 매개변수로 Const 값을 넘겨주어서 복사가 일어나지 않게 하여 참조의 수를 줄일 수 있다.

Enemy.h


#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Interfaces/HitInterface.h"
#include "Enemy.generated.h"

UCLASS()
class SLASH_API AEnemy : public ACharacter, public IHitInterface
{
	GENERATED_BODY()

public:
	
	AEnemy();

	virtual void Tick(float DeltaTime) override;
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	virtual void GetHit(const FVector& ImpactPoint) override;

protected:
	virtual void BeginPlay() override;


};

 

Enemy.cpp

void AEnemy::GetHit(const FVector& ImpactPoint)
{
	DRAW_SPHERE(ImpactPoint);
}

 

이렇게 만들고 난 다음 Weapon에서 GetHit을 호출하면 되는데 이때 Cast를 통해 부딪힌 객체를 가져와서 사용하면 된다.

Weapon.cpp

void AWeapon::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	const FVector Start = BoxTraceStart->GetComponentLocation();
	const FVector End = BoxTraceEnd->GetComponentLocation();

	TArray<AActor*> ActorsToIgnore;
	ActorsToIgnore.Add(this);
	//충돌 결과
	FHitResult BoxHit;

	UKismetSystemLibrary::BoxTraceSingle(
		this,
		Start,
		End,
		FVector(5.f, 5.f, 5.f),
		BoxTraceStart->GetComponentRotation(),
		ETraceTypeQuery::TraceTypeQuery1,
		false,
		ActorsToIgnore,		//무시할거
		EDrawDebugTrace::ForDuration,
		BoxHit,
		true		//자신무시
	);

	if (BoxHit.GetActor())
	{
		IHitInterface* HitInterface = Cast<IHitInterface>(BoxHit.GetActor());
		if (HitInterface)
		{
			HitInterface->GetHit(BoxHit.ImpactPoint);
		}
	}
}

 

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

 

이것을 활용해서 어느부분에 맞는지에 따라 다른 애니메이션이 출력되도록 할 것이다.

이를 위하여 애니메이션 몽타주를 만들고 몽타주 섹션을 나눈 다음 이 몽타주 섹션을 통해 각각 다른 애니메이션이 재생되도록 하자. 

 

이때 주의해야할 점은 애니메이션 블루프린트 클래스에서 Default Slot을 가져다가 사용해야 정상적으로 활용할 수 있다.

이를 위해 애니메이션 블루 프린트를 만들어주자.

 

다음과 같이 해주면 Idle 애니메이션에서 몽타주섹션에 따라 다른 애니메이션이 출력된다.

그리고 적 블루프린트에 들어가서 애니메이션 모드를 Animation BluePrint모드로 설정한 뒤 만들어둔 애니메이션 블루프린트를 할당해준다.

이렇게 해주면 기본 Idle 애니메이션이 동작하게 된다.

 

 

이렇게 해준 다음에 코드로 돌아가서 이 몽타주섹션이 재생되게 하면 되는데 적에 캐릭터가 칼을 착용할 때 했던 방법을 따라하면 된다. 

우선  우리가 만든 애니메이션 몽타주를 블루프린트에 할당할 수 있게 UAnimMontage 전방선언을 해준 다음 변수로 선언해준다. 

그리고 호출되는 이름에 따라 몽타주의 섹션을 재생해주는 함수를 추가하면 된다. 지금은 기본으로 왼쪽에서 맞았을 때의 몽타주 섹션만 출력되게 하자.

Enemy.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Interfaces/HitInterface.h"
#include "Enemy.generated.h"

class UAnimMontage;

UCLASS()
class SLASH_API AEnemy : public ACharacter, public IHitInterface
{
	GENERATED_BODY()
private:
	/**
	애니메이션 몽타주
	*/
	UPROPERTY(EditDefaultsOnly, Category = Montages)
	UAnimMontage* HitReactMontage;
protected:
	virtual void BeginPlay() override;

	/**
	애니메이션 몽타주
	*/
	void PlayHitReactMontage(const FName& SectionName);
}

 

Enemy.cpp

void AEnemy::PlayHitReactMontage(const FName& SectionName)
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && HitReactMontage)
	{
		AnimInstance->Montage_Play(HitReactMontage);
		AnimInstance->Montage_JumpToSection(SectionName, HitReactMontage);
	}
}

 

그리고 주의해야할 점은 애니메이션에서 애니메이션 디테일에 루트모션 활성화를 켜두어야 맞았을 때 원래자리로 돌아오지 않는다. 이때 루트모션은 Skeleton이 루트를 기준으로 설정되어 있을 때만 가능하다. 

 

이렇게 하고 적을 공격해보면 기본위치에서 피격 애니메이션을 출력한 뒤 옆으로 이동되는 것을 볼 수 있다.

 

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

 

N개의 수를 입력받은 다음에 M개의 수를 입력받는데 이 수가 처음 입력받는 N개의 수 중에 있다면 1을 출력해주고 아니라면 0을 출력해주면 된다. 

 

정답코드

#include "iostream"
#include "unordered_map"
using namespace std;

unordered_map<int, bool> mp;

int main()
{
	int n, m;

	cin >> n;

	for (int i = 0; i < n; i++)
	{
		int x;
		cin >> x;
		mp[x] = true;
	}

	cin >> m;

	for (int i = 0; i < m; i++) {
		int x;
		cin >> x;
		if (mp[x]) {
			cout << 1 << " ";
		}
		else {
			cout << 0 << " ";
		}
	}

	return 0;
}



오늘은 전에 학습했던 드로우콜이 작동하도록 DX코드를 수정해주는 것이 목표이다. 

 

1.MeshRenderer

드로우콜은 mesh와 material이 같다면 묶어서 관리해주고 관리 해주는 것 마다 인스턴스 버퍼가 있으며 이를 통해  인스턴싱이 이루어지도록 해야한다.

Vertex Buffer의 Create 함수의 매개변수 을 다르게 해서 사용하는 InstanceBuffer에서 Create 함수의 매개변수에서 CPU write를 true로 설정해주면 이 값을 오버라이트해서 데이터를 계속 밀어넣을 수 있다. 기존의 방식처럼 맵, 언맵, 데이터 

카피 후 밀어넣기의 과정을 통해 데이터를 갱신해주면 된다. 

중요한 점은 위의 변수들을 하나로 묶어서 관리해주어야 한다는 것이다. 이런 기능을 하기 위해 인스턴싱 매니저 클래스를 만들어주자.

일단은 우리가 InstanceBuffer와 World를 묶어서 관리해준 부분을 InstancingBuffer 라는 클래스를 만들어서 관리해주자. 이 클래스는 매 프레임마다 데이터 초기화 및 데이터를 넣어주고 이 데이터를 GPU쪽으로 밀어주는 기능을 만들어주자.

InstancingBuffer.h

#pragma once

class VertexBuffer;

struct InstancingData
{
	Matrix world;
};

#define MAX_MESH_INSTANCE 500

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

private:
	void CreateBuffer(uint32 maxCount = MAX_MESH_INSTANCE);

public:
	//매 프레임마다 데이터 갱신 및 밀어넣기
	void ClearData();
	void AddData(InstancingData& data);
	void PushData();

public:
	uint32 GetCount() { return static_cast<uint32>(_data.size()); }
	shared_ptr<VertexBuffer> GetBuffer() { return _instanceBuffer; }

	void SetID(uint64 instanceId) { _instanceId = instanceId; }
	uint64 GetID() { return _instanceId; }

private:
	uint64 _instanceId = 0;
	shared_ptr<VertexBuffer> _instanceBuffer;
	uint32 _maxCount = 0;
	vector<InstancingData> _data;
};

InstancingBuffer.cpp

#include "pch.h"
#include "InstancingBuffer.h"

InstancingBuffer::InstancingBuffer()
{
	CreateBuffer(MAX_MESH_INSTANCE);
}

InstancingBuffer::~InstancingBuffer()
{

}

void InstancingBuffer::ClearData()
{
	_data.clear();
}

void InstancingBuffer::AddData(InstancingData& data)
{
	_data.push_back(data);
}

void InstancingBuffer::PushData()
{
	const uint32 dataCount = GetCount();
	if (dataCount > _maxCount)
		CreateBuffer(dataCount);

	D3D11_MAPPED_SUBRESOURCE subResource;

	//뚜겅 열기
	DC->Map(_instanceBuffer->GetComPtr().Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &subResource);
	{
		::memcpy(subResource.pData, _data.data(), sizeof(InstancingData) * dataCount);
	}
	DC->Unmap(_instanceBuffer->GetComPtr().Get(), 0);
	//닫기

	_instanceBuffer->PushData();
}

void InstancingBuffer::CreateBuffer(uint32 maxCount /*= MAX_MESH_INSTANCE*/)
{
	_maxCount = maxCount;
	_instanceBuffer = make_shared<VertexBuffer>();

	vector<InstancingData> temp(maxCount);
	_instanceBuffer->Create(temp, /*slot*/1, /*cpuWrite*/true);
}

 

다시 InstancingManager 코드로 돌아와서 매쉬와 Material을 둘씩 묶어서 관리해주는 코드를 작성해보자. 이때 관리하기 위한 아이디를 발급 해주어야 하는데 스마트 포인터를 사용하는 것을 활용해서 주소값을 통해 같은 주소이면 같은 값이라고 판단하게 하자. 이를 위해 변수를 설정해주자

Types.h

// MeshID / MaterialID
using InstanceID = std::pair<uint64, uint64>;

 

★꿀팁 - 스마트포인터를 사용할 때 &를 붙여주는게 복사가 일어나지 않기 때문에 효율이 좋다 

 

map을 통해 짝을 만들어주고 실제로 렌더하는 함수에서 우선 기존 데이터를 날려주고 씬의 모든 오브젝트를 순회하면서 짝이 맞는 애들을 분류하고 그런애들을 모아서 그려주도록 하자. 

일단 분류하기 위해 MeshRenderer에서 InstanceID를 만들어주기 위한 변수와 헬퍼 함수를 만들어주자. 그리고 지금의 MeshRenderer는  Update를 통해 자기 자신만을 그려주고 있는데 나와 비슷한 오브젝트도 그려주어야 하기 때문에 이제 별도의 함수를 통해 Render하도록 바꿔주자.

MeshRenderer.h

#pragma once
#include "Component.h"

class Mesh;
class Shader;
class Material;

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

	virtual void Update() override;

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

	InstanceID GetInstanceID();

private:
	shared_ptr<Mesh> _mesh;
	shared_ptr<Material> _material;
	uint8 _pass = 0;
};

MeshRenderer.cpp

#include "pch.h"
#include "MeshRenderer.h"
#include "Camera.h"
#include "Game.h"
#include "Mesh.h"
#include "Shader.h"
#include "Material.h"

MeshRenderer::MeshRenderer() : Super(ComponentType::MeshRenderer)
{

}

MeshRenderer::~MeshRenderer()
{

}

void MeshRenderer::RenderInstancing(shared_ptr<class InstancingBuffer>& buffer)
{
	if (_mesh == nullptr || _material == nullptr)
		return;

	auto shader = _material->GetShader();
	if (shader == nullptr)
		return;

	// Light
	_material->Update();

	// IA
	_mesh->GetVertexBuffer()->PushData();
	_mesh->GetIndexBuffer()->PushData();

	buffer->PushData();

	shader->DrawIndexedInstanced(0, _pass, _mesh->GetIndexBuffer()->GetCount(), buffer->GetCount());
}

InstanceID MeshRenderer::GetInstanceID()
{
	//주소값 전달
	return make_pair((uint64)_mesh.get(), (uint64)_material.get());
}

 

이를 활용하여 InstancingManager  클래스를 완성해주자.

InstancingManager.h

#pragma once
#include "InstancingBuffer.h"

class GameObject;


class InstancingManager
{
	DECLARE_SINGLE(InstancingManager);

public:
	void Render(vector<shared_ptr<GameObject>>& gameObjects);
	void Clear() { _buffers.clear(); }
	void ClearData();

private:
	void RenderMeshRenderer(vector<shared_ptr<GameObject>>& gameObjects);

private:
	void AddData(InstanceID instanceId, InstancingData& data);

private:
	//짝 만들어주기
	map<InstanceID/*instanceId*/, shared_ptr<InstancingBuffer>> _buffers;
};

InstancingManager.cpp

#include "pch.h"
#include "InstancingManager.h"
#include "InstancingBuffer.h"
#include "GameObject.h"
#include "MeshRenderer.h"
#include "ModelRenderer.h"
#include "ModelAnimator.h"
#include "Transform.h"
#include "Camera.h"

void InstancingManager::Render(vector<shared_ptr<GameObject>>& gameObjects)
{
	ClearData();

	RenderMeshRenderer(gameObjects);
}

void InstancingManager::RenderMeshRenderer(vector<shared_ptr<GameObject>>& gameObjects)
{
	//임시용- 물체걸러주기
	map<InstanceID, vector<shared_ptr<GameObject>>> cache;

	//분류
	for (shared_ptr<GameObject>& gameObject : gameObjects)
	{
		if (gameObject->GetMeshRenderer() == nullptr)
			continue;

		const InstanceID instanceId = gameObject->GetMeshRenderer()->GetInstanceID();
		cache[instanceId].push_back(gameObject);
	}

	//같은애들 모아서 그려주기
	for (auto& pair : cache)
	{
		const vector<shared_ptr<GameObject>>& vec = pair.second;

		//if (vec.size() == 1)
		//{
		//	vec[0]->GetMeshRenderer()->RenderSingle();
		//}
		//else
		{
			const InstanceID instanceId = pair.first;

			for (int32 i = 0; i < vec.size(); i++)
			{
				const shared_ptr<GameObject>& gameObject = vec[i];
				InstancingData data;
				data.world = gameObject->GetTransform()->GetWorldMatrix();

				AddData(instanceId, data);
			}

			//그려주기- 대표로 그려주기
			shared_ptr<InstancingBuffer>& buffer = _buffers[instanceId];
			vec[0]->GetMeshRenderer()->RenderInstancing(buffer);
		}
	}
}


void InstancingManager::AddData(InstanceID instanceId, InstancingData& data)
{
	//인스턴스 아이디가 있는지 이미 로드했던게 아니라면
	if (_buffers.find(instanceId) == _buffers.end())
		_buffers[instanceId] = make_shared<InstancingBuffer>();

	_buffers[instanceId]->AddData(data);
}

void InstancingManager::ClearData()
{
	for (auto& pair : _buffers)
	{
		shared_ptr<InstancingBuffer>& buffer = pair.second;
		buffer->ClearData();
	}
}

 

이렇게 해주고 우리의 메인코드를 수정해주면 된다.

MeshInstancingDemo.h

#pragma once
class MeshInstancingDemo : public IExecute
{
public:
	void Init() override;
	void Update() override;
	void Render() override;

private:
	shared_ptr<Shader> _shader;
	shared_ptr<GameObject> _camera;
	vector<shared_ptr<GameObject>> _objs;

private:
};

MeshInstancingDemo.cpp

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

void MeshInstancingDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"20. MeshInstancingDemo.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>());

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

	for (int32 i = 0; i < 500; i++)
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetPosition(Vec3(rand() % 100, 0, rand() % 100));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
		}

		_objs.push_back(obj);
	}

	RENDER->Init(_shader);

	

}

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

	{
		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);
		RENDER->PushLightData(lightDesc);
	}

	//INSTANCING
	INSTANCING->Render(_objs);
}

void MeshInstancingDemo::Render()
{
}

 

이렇게 해주면 안정적으로 인스턴싱이 이루어지고 있는 모습을 볼 수 있다.

 

 

중요한 점은 이제 Update에서 자기 자신만을 렌더해주는 것이 아닌 인스턴싱 매니저를 통해 자신과 비슷한 오브젝트도 로드할  수 있게 만들어줘야 한다는 것이다.

 

2.ModelRenderer

이제 우리가 가져온 fbx도 인스턴싱이 가능하도록 코드를 수정해주자. ModelRenderer에서 Update부분에서 자기 자신을 렌더하기 위한 코드에서 새로운 함수를 통해 인스턴싱이 가능하도록 하자. 이때 World 좌표는 인스턴싱매니저에서 관리해주고 있기 때문에 빼주고 IA부분과 World 좌표를 넣어주는 부분, 인스턴싱으로 그려주는 부분을 넣어주자.

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();

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

	void RenderInstancing(shared_ptr<class InstancingBuffer>& buffer);
	InstanceID GetInstanceID();
	
private:
	shared_ptr<Shader>	_shader;
	uint8				_pass = 0;		//통로
	shared_ptr<Model>	_model;

};

ModelRenderer.cpp

void ModelRenderer::RenderInstancing(shared_ptr<class InstancingBuffer>& buffer)
{
	if (_model == nullptr)
		return;

	//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);

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

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

		//IA
		mesh->vertexBuffer->PushData();
		mesh->indexBuffer->PushData();

		//World position 넣어주기
		buffer->PushData();

		_shader->DrawIndexedInstanced(0, _pass, mesh->indexBuffer->GetCount(), buffer->GetCount());

	}
}

InstanceID ModelRenderer::GetInstanceID()
{
	return make_pair((uint64)_model.get(), (uint64)_shader.get());
}

 

이렇게 해주고 이 Model Renderer를 통해 모델을 렌더해주도록 메인코드를 수정해주면 된다.

ModelInstancingDemo.cpp

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

void ModelInstancingDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"21. ModelInstancingDemo.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>());

	shared_ptr<class Model> m1 = make_shared<Model>();
	m1->ReadModel(L"Tower/Tower");
	m1->ReadMaterial(L"Tower/Tower");

	for (int32 i = 0; i < 500; i++)
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetPosition(Vec3(rand() % 100, 0, rand() % 100));
		obj->GetOrAddTransform()->SetScale(Vec3(0.01f));
		obj->AddComponent(make_shared<ModelRenderer>(_shader));
		{
			obj->GetModelRenderer()->SetModel(m1);
		}
		_objs.push_back(obj);
	}

	RENDER->Init(_shader);
}

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

	{
		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);
		RENDER->PushLightData(lightDesc);
	}

	//INSTANCING
	INSTANCING->Render(_objs);
}

void ModelInstancingDemo::Render()
{
}

 

이렇게 해주면 안정적인 프레임으로 모델이 인스턴싱되는것을 볼 수 있다.

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

 

이 문제는 해당 원소의 값보다 작은 서로 다른 좌표의 개수가 몇개인지 출력해주면 되는 문제이다. 그렇기 때문에 벡터

입력을 받은 다음 새로운 벡터를 만들어서 값을 복사한 다음 그 벡터를 정렬하고 중복되는 원소를 제거한 다음 그 벡터에서해당 원소의 위치를 저장해주면 되는 문제이다.

 

정답코드

#include <iostream>
#include <vector>
#include <algorithm>
#include <map>

using namespace std;

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

    vector<int> v(n);
    vector<int> sorted_v;

    // 좌표 입력받기
    for (int i = 0; i < n; i++) {
        cin >> v[i];
        sorted_v.push_back(v[i]);
    }

    // 중복 제거 후 정렬
    sort(sorted_v.begin(), sorted_v.end());
    sorted_v.erase(unique(sorted_v.begin(), sorted_v.end()), sorted_v.end());

    // 좌표 압축 적용: 각 값의 압축된 인덱스를 저장
    map<int, int> compression;
    for (int i = 0; i < sorted_v.size(); i++) {
        compression[sorted_v[i]] = i;
    }

    // 원래 좌표에 대해 압축된 결과 출력
    for (int i = 0; i < n; i++) {
        cout << compression[v[i]] << " ";
    }

    return 0;
}

 

https://inf.run/43pY7

 

학습 페이지

 

www.inflearn.com

 



오늘부터는인스턴싱과 드로우콜에 대해 알아보자. 드로우콜은 최적화에서 많이 등장하는 용어이다.

유니티에서는 게임화면에서 Stats의 Batches라고 보아도 된다. 몇번만에 물체를 그리는 지에 관한 정보이다.

 

만약 물체가 늘어나게 된다면 그려야하는 수도 같이 증가하기 때문에 Batches가 증가하게 된다. 만약 유니티 설정에서 Dynamic Batching을 활성화하게 된다면 배치 수가 확 줄어들게 된다.

만약 지금까지 만든 DX코드에서 물체를 100개 만들려면 실제로 물체를 100번 그려야한다. 드로우 콜은 드로우를 몇번 호출했는지에 관한 정보이다. 지금은 큐브를 화면에 그려주려면 큐브라는 매시모델을 만들어 주고 쉐이더, Material 등을 만들어주고 최종적으로 Mesh Renderer가 그려주게 된다. 

이후에는 렌더링 파이프라인을 세팅해주고 Draw 함수를 호출하고 present 함수로 제출하여 렌더링 파이프라인을 타고 들어가게 된다. 이 모든 과정을 통해 물체가 그려진다. 이때 물체가 늘어나면 이러한 과정이 반복되어야하는데 이를 최소화하면 효율적으로 물체를 그려줄 수 있다.

인스턴싱

 

화면에 여러 큐브를 보면 메시는 같지만 Material은 다르다. Material은 어떤 shader를 사용할지와 어떤 값을 넘겨줄지에 관한 정보가 포함된 파일이라고 보면 된다. 그렇기 때문에 완전하게 같은 물체라는 것은 Mesh도 같고 Material도 같다는

의미이다. 만약 shader와 인자 값들이 같지만 객체의 이름이 다르다면 다른 Material로 인식한다.

 

위의 그림과 같이 초록색의 별을 여러개 그리고 여러 색깔의 별을 하나씩 그린다고 했을 때 색깔을 계속 바꾸면서 그리는 것은 비용이 많이 든다. 이럴 때 초록색 별을 미리 다 그려두고 다른 별을 그리는 것이 효율적일 것이다. 이러한 기술이 인스턴싱에 포함된다. 이러한 방법을 통해 비용(Batches)을 줄일 수 있다.

Skeletal Mesh는 자체의 shader를 통해 만들어주기 때문에 인스턴싱이 적용되지 않는다.

 

이제 코드를 보자면 드로우콜이라는 것은 아래와 같은 Draw함수를 몇번 호출했는 지에 관한 정보이다.

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

 

이때 인스턴싱을 사용하려면 DrawIndexedInstanced 이런 함수를 사용해주면 된다. 일단 단계별로 인스턴싱을 실습해보도록 하자.

 

일단 전에 사용했던 베이가 구와 같은 매쉬를 통해 인스턴싱을 실습해보자.

일단 인스턴싱을 사용하지 않고 기존 코드대로 해보자면

Material은 기존에 가져오던 방식대로 가져오고 오브젝트는 랜덤함수를 사용해서 임의의 위치에 그려주도록 하자.

InstancingDemo.h

#pragma once
class InstancingDemo : public IExecute
{
public:
	void Init() override;
	void Update() override;
	void Render() override;

private:
	shared_ptr<Shader> _shader;
	shared_ptr<GameObject> _camera;
	vector<shared_ptr<GameObject>> _objs;
};

InstancingDemo.cpp

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

void InstancingDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"19. InstancingDemo.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>());

	// 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);
		// INSTANCING
		//_material = material;
	}

	for (int32 i = 0; i < 100; i++)
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetPosition(Vec3(rand() % 100, 0, rand() % 100));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			// INSTANCING
			//_mesh = mesh;
		}

		_objs.push_back(obj);
	}

	RENDER->Init(_shader);
}

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

	{
		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);
		RENDER->PushLightData(lightDesc);
	}

	for (auto& obj : _objs)
	{
		obj->Update();
	}
}

void InstancingDemo::Render()
{
}

 

이렇게 해주고 shader는 이전에 베이가 구를 사용했을 때의 쉐이더와 비슷하게 해주면 된다.

InstancingDemo.fx

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

struct VS_IN
{
    float4 position : POSITION;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
    float3 tangent : TANGENT;
	// INSTANCING
};

struct VS_OUT
{
    float4 position : SV_POSITION;
    float3 worldPosition : POSITION1;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
};

VS_OUT VS(VS_IN input)
{
    VS_OUT output;

    output.position = mul(input.position, W);
    output.worldPosition = output.position;
    output.position = mul(output.position, VP);
    output.uv = input.uv;
    output.normal = input.normal;

    return output;
}


float4 PS(VS_OUT input) : SV_TARGET
{
	//float4 color = ComputeLight(input.normal, input.uv, input.worldPosition);
    float4 color = DiffuseMap.Sample(LinearSampler, input.uv);
    return color;
}

technique11 T0
{
	PASS_VP(P0, VS, PS)
};

 

이렇게 해주면 베이가 구가 100개가 그려지게 된다. 근데 얼마나 효율적으로 렌더링 되고 있는지 확인하기 위해 프레임을 로그찍어서 확인할 수 있도록 하자.

Game.h

class Game
{
private:
	void ShowFps();
};

 

Game.cpp

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

	GRAPHICS->RenderBegin();

	SCENE->Update();

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

	GRAPHICS->RenderEnd();
}

void Game::ShowFps()
{
	uint32 fps = GET_SINGLE(TimeManager)->GetFps();

	WCHAR text[100] = L"";
	::wsprintf(text, L"FPS : %d", fps);

	::SetWindowText(_desc.hWnd, text);

}

 

 하지만 물체의 생성 수를 급격하게 늘려주게 되면 버벅거리면서 Fps가 엄청 떨어는 것을 볼 수 있다.



이제 인스턴싱을 실습해보도록 하자. 이때  DrawIndexedInstanced 함수를 사용해주게 될 것인데 1만에 해당하는 수를 넘겨주면되지만 물체마다 다른 Transform을 처리해주어야한다. 인스턴싱할 좌표들을 연결해준 다음, 인스턴싱과 관련된 버퍼에 넘겨주면 따로 따로 들어오게 된다. 즉 전체 복사작업은 해주어야하지만 shader와 material이 달라지지 않는다고 보면 된다. 공장에서 컨베이너 벨트에서 라인을 유지하고 World값만 교체해주는 것이라고 보면 된다. 

인스턴싱을 위해 관련 정보들을 저장해주자.

Vertex 버퍼를 수정해서 이를 받아 주도록 하자. 이때  constant buffer를 지정해줄 때 사용했던 방법으로 슬롯번호 1번을Instance buffer를 지정해주기 위해 사용하도록 Create 함수를 수정해주자. 지금까지는 Vertex버퍼가 Input Assembler 단계에서 원본의 Vertex 정보만 넣어주면 되기 때문에 IMMUTABLE로 만들어주었지만 이제는 경우에 따라서 달라지도록

해야한다.

VertexBuffer.h

#pragma once

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

	ComPtr<ID3D11Buffer> GetComPtr() { return _vertexBuffer; }
	uint32 GetStride() { return _stride; }
	uint32 GetOffset() { return _offset; }
	uint32 GetCount() { return _count; }
	uint32 GetSlot() { return _slot; }

	template<typename T>
	void Create(const vector<T>& vertices, uint32 slot = 0, bool cpuWrite = false, bool gpuWrite = false)
	{
		_stride = sizeof(T);
		_count = static_cast<uint32>(vertices.size());

		_slot = slot;
		_cpuWrite = cpuWrite;
		_gpuWrite = gpuWrite;

		D3D11_BUFFER_DESC desc;
		ZeroMemory(&desc, sizeof(desc));
		desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
		desc.ByteWidth = (uint32)(_stride * _count);

		if (cpuWrite == false && gpuWrite == false)
		{
			desc.Usage = D3D11_USAGE_IMMUTABLE; // CPU Read, GPU Read
		}
		else if (cpuWrite == true && gpuWrite == false)
		{
			desc.Usage = D3D11_USAGE_DYNAMIC; // CPU Write, GPU Read
			desc.CPUAccessFlags = D3D10_CPU_ACCESS_WRITE;
		}
		else if (cpuWrite == false && gpuWrite == true) // CPU Read, GPU Write
		{
			desc.Usage = D3D11_USAGE_DEFAULT;
		}
		else
		{
			desc.Usage = D3D11_USAGE_STAGING;
			desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE;
		}

		D3D11_SUBRESOURCE_DATA data;
		ZeroMemory(&data, sizeof(data));
		data.pSysMem = vertices.data();

		HRESULT hr = DEVICE->CreateBuffer(&desc, &data, _vertexBuffer.GetAddressOf());
		CHECK(hr);
	}

	void PushData()
	{
		DC->IASetVertexBuffers(_slot, 1, _vertexBuffer.GetAddressOf(), &_stride, &_offset);
	}

private:
	ComPtr<ID3D11Buffer> _vertexBuffer;

	uint32 _stride = 0;
	uint32 _offset = 0;
	uint32 _count = 0;

	uint32 _slot = 0;
	bool _cpuWrite = false;
	bool _gpuWrite = false;
};

 

이제 오브젝트의 업데이트를 수정해주도록 하자. 지금은 업데이트를 통해 모든 오브젝트의 meshRenderer가 update 되고 있는데 이를 대표적으로 한 오브젝트만 업데이트하도록 하자. 

MeshRenderer의 Update 코드를 가져와서 인스턴싱에 맞게 수정해주자. 일단 각각의 World를 가져오는 부분은 없애주고 대표적으로 한번만 InstanceBuffer를 통해  PushData를 하도록 해주자.

InstancingDemo.h

#pragma once
class InstancingDemo : public IExecute
{
public:
	void Init() override;
	void Update() override;
	void Render() override;

private:
	shared_ptr<Shader> _shader;
	shared_ptr<GameObject> _camera;
	vector<shared_ptr<GameObject>> _objs;

private:
	// INSTANCING
	shared_ptr<Mesh> _mesh;
	shared_ptr<Material> _material;

	vector<Matrix> _worlds;
	shared_ptr<VertexBuffer> _instanceBuffer;
};

InstancingDemo.cpp

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

void InstancingDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"19. InstancingDemo.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>());

	// 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);
		// INSTANCING
		_material = material;
	}

	for (int32 i = 0; i < 10000; i++)
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetPosition(Vec3(rand() % 100, 0, rand() % 100));
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
		}
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			obj->GetMeshRenderer()->SetMesh(mesh);
			// INSTANCING
			_mesh = mesh;
		}

		_objs.push_back(obj);
	}

	RENDER->Init(_shader);

	// INSTANCING
	_instanceBuffer = make_shared<VertexBuffer>();

	for (auto& obj : _objs)
	{
		//원래 물체의 글로벌좌표
		Matrix world = obj->GetTransform()->GetWorldMatrix();
		_worlds.push_back(world);
	}

	_instanceBuffer->Create(_worlds, /*slot*/1);
}

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

	{
		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);
		RENDER->PushLightData(lightDesc);
	}

	/*for (auto& obj : _objs)
	{
		obj->Update();
	}*/

	_material->Update();

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

	_mesh->GetVertexBuffer()->PushData();
	_instanceBuffer->PushData();
	_mesh->GetIndexBuffer()->PushData();

	//_shader->DrawIndexed(0, 0, _mesh->GetIndexBuffer()->GetCount(), 0, 0);
	_shader->DrawIndexedInstanced(0, 0, _mesh->GetIndexBuffer()->GetCount(), _objs.size());

}

void InstancingDemo::Render()
{
}

 

이에 맞게 shader코드도 수정해주자. 핵심은 Input.world를 통해 물체마다 World를 세팅해줄 수 있다는 점이다.

InstancingDemo.fx

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

struct VS_IN
{
    float4 position : POSITION;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
    float3 tangent : TANGENT;
	// INSTANCING
    matrix world : INST;
};

struct VS_OUT
{
    float4 position : SV_POSITION;
    float3 worldPosition : POSITION1;
    float2 uv : TEXCOORD;
    float3 normal : NORMAL;
};

//VS_OUT VS(VS_IN input)
//{
//    VS_OUT output;

//    output.position = mul(input.position, W);
//    output.worldPosition = output.position;
//    output.position = mul(output.position, VP);
//    output.uv = input.uv;
//    output.normal = input.normal;

//    return output;
//}

VS_OUT VS(VS_IN input)
{
    VS_OUT output;

    //물체마다 World 가져오기
    output.position = mul(input.position, input.world); // W
    output.worldPosition = output.position;
    output.position = mul(output.position, VP);
    output.uv = input.uv;
    output.normal = input.normal;

    return output;
}


float4 PS(VS_OUT input) : SV_TARGET
{
	//float4 color = ComputeLight(input.normal, input.uv, input.worldPosition);
    float4 color = DiffuseMap.Sample(LinearSampler, input.uv);
    return color;
}

technique11 T0
{
	PASS_VP(P0, VS, PS)
};

 

이렇게 해주면 버벅거림 없이 일정한 FPS가 유지 된다.

 

 

핵심은 원래는 각각의 mesh 와 material을 설정해서 물체를 draw 함수를 통해 그려준 것인데 

인스턴싱버전에서는 초반의 같은 오브젝트의 정보들을 인스턴스 버퍼라는 거대한 버퍼를 밀어넣고 모든 정보를을 하나씩 넣어줘서 한 큐에 처리한다. 다른 정보는 받은 정보를 각각의 World 좌표를 VS단계에서 계산해주는 방식으로 작동한다. 

결국에는 만개가 있더라도 드로우콜을 1번만 호출한 것이라고 보면 된다.

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

 

Pair 자료형을 이중으로 사용해주고 이를 vector에 저장해서 정렬함수를 만들어서 정렬해주었다. 이때 가입한 순서는 int 변수를 하나씩 증가시켜주어서 이를 통해 비교하도록했다.

하지만 pair 자료형을 이중으로 사용하면 가독성이 떨어지고 입력받을 때 명시적으로 관리하기 어렵기 때문에 구조체를 사용해서 관리하는 것도 하나의 방법이다. 

 

정답코드(Pair,Vector사용)

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

using namespace std;

bool cmp(pair<pair<string, int>, int>vp1, pair<pair<string, int>, int>vp2)
{
	if (vp1.first.second == vp2.first.second)
		return vp1.second < vp2.second;
	else
		return vp1.first.second < vp2.first.second;
}

vector<pair<pair<string, int>, int>> vpp;
int main()
{
	int n, cnt = 1;

	cin >> n;

	for (int i = 0; i < n; i++)
	{
		string name;
		int age;
		cin >> age >> name;
		vpp.push_back({ { name,age }, cnt });
		cnt++;
	}

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

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

	return 0;
}

 

정답코드2(구조체 사용)

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 회원 정보를 담는 구조체
struct Member {
    int age;
    string name;
    int order;  // 가입 순서
};

// 정렬 기준 함수: 나이가 같으면 가입 순서대로, 그렇지 않으면 나이 순
bool cmp(const Member &a, const Member &b) {
    if (a.age == b.age) {
        return a.order < b.order;  // 나이가 같으면 가입 순서대로 정렬
    }
    return a.age < b.age;  // 나이 순으로 정렬
}

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

    vector<Member> members;

    // 회원 정보를 입력받기
    for (int i = 0; i < n; i++) {
        Member member;
        cin >> member.age >> member.name;
        member.order = i;  // 가입 순서를 저장
        members.push_back(member);
    }

    // 정렬: 나이 순 -> 나이가 같으면 가입 순서대로
    sort(members.begin(), members.end(), cmp);

    // 결과 출력
    for (const Member &member : members) {
        cout << member.age << " " << member.name << '\n';
    }

    return 0;
}

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

 

중복된 단어를 제거해야 하기 때문에 집합 자료형인 set을 사용하고 set를 vector로 변환해서 다시 정렬함수를 적용하면 된다. 

정렬함수에서는 길이로 먼저 비교하고 사전순은 string 자체를 비교하면 된다.

 

정답코드

#include "iostream"
#include "vector"
#include "set"
#include "algorithm"

using namespace std;

// 정렬 기준 함수
bool cmp(const string& a, const string& b) {
	if (a.length() == b.length()) {
		return a < b;  // 길이가 같으면 사전순 정렬
	}
	return a.length() < b.length();  // 길이가 다르면 짧은 순으로 정렬
}

int main()
{
	int n;

	cin >> n;
	set<string> s;

	for (int i = 0; i < n; i++)
	{
		string word;
		cin >> word;
		s.insert(word);
	}

	//set -> vector
	vector<string> v(s.begin(), s.end());

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

	for (int i = 0; i < v.size(); i++)
	{
		cout << v[i] << endl;
	}

	return 0;
}



1.애니메이션 활용4(트위닝)

이제 애니메이션에서 마지막으로 트위닝이라는 것을 해보도록 하자. 트위닝이란 두 애니메이션이 바뀔때 사이에 트랜지션을 넣어주는 것 이다. 이를 위해 쉐이더 코드에서 트랜지션의 지속시간과 현재, 다음 애니메이션의 정보를 받을 수 있도록 구조체와 버퍼를 선언해주자.

TweenDemo.fx

struct TweenFrameDesc
{
    float tweenDuration;        //트랜지션 지속시간
    float tweenRatio;
    float tweenSumTime;
    float padding;
    KeyframeDesc curr;
    KeyframeDesc next;
};



cbuffer TweenBuffer
{
    TweenFrameDesc TweenFrames;
};

 

이렇게 한 뒤에 이제 현재 애니메이션 사이에서도 보간을 하지만 과 다음 애니메이션과 이어질때도 보간하도록 기존 함수를 수정해주자.

TweenDemo.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[2];
    int currFrame[2];
    int nextFrame[2];
    float ratio[2];
    
    animIndex[0] = TweenFrames.curr.animIndex;
    currFrame[0] = TweenFrames.curr.currFrame;
    nextFrame[0] = TweenFrames.curr.nextFrame;
    ratio[0] = TweenFrames.curr.ratio;

    animIndex[1] = TweenFrames.next.animIndex;
    currFrame[1] = TweenFrames.next.currFrame;
    nextFrame[1] = TweenFrames.next.nextFrame;
    ratio[1] = TweenFrames.next.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[0], animIndex[0], 0));
        c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame[0], animIndex[0], 0));
        c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame[0], animIndex[0], 0));
        c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame[0], animIndex[0], 0));
        curr = matrix(c0, c1, c2, c3);

        n0 = TransformMap.Load(int4(indices[i] * 4 + 0, nextFrame[0], animIndex[0], 0));
        n1 = TransformMap.Load(int4(indices[i] * 4 + 1, nextFrame[0], animIndex[0], 0));
        n2 = TransformMap.Load(int4(indices[i] * 4 + 2, nextFrame[0], animIndex[0], 0));
        n3 = TransformMap.Load(int4(indices[i] * 4 + 3, nextFrame[0], animIndex[0], 0));
        next = matrix(n0, n1, n2, n3);

        matrix result = lerp(curr, next, ratio[0]);

		// 다음 애니메이션
        if (animIndex[1] >= 0)
        {
            c0 = TransformMap.Load(int4(indices[i] * 4 + 0, currFrame[1], animIndex[1], 0));
            c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame[1], animIndex[1], 0));
            c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame[1], animIndex[1], 0));
            c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame[1], animIndex[1], 0));
            curr = matrix(c0, c1, c2, c3);

            n0 = TransformMap.Load(int4(indices[i] * 4 + 0, nextFrame[1], animIndex[1], 0));
            n1 = TransformMap.Load(int4(indices[i] * 4 + 1, nextFrame[1], animIndex[1], 0));
            n2 = TransformMap.Load(int4(indices[i] * 4 + 2, nextFrame[1], animIndex[1], 0));
            n3 = TransformMap.Load(int4(indices[i] * 4 + 3, nextFrame[1], animIndex[1], 0));
            next = matrix(n0, n1, n2, n3);

            matrix nextResult = lerp(curr, next, ratio[1]);
            result = lerp(result, nextResult, TweenFrames.tweenRatio);
        }

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

    return transform;
}

 

이렇게 수정해준 다음 RenderManager 클래스에서 값을 동일하게 넣어줄 수 있도록 구조체를 선언하고 생성자와 다음 애니메이션으로 교체했다면 값을 초기화 해주는 함수도 넣어주자. 그리고 이 버퍼를 초기화하고 데이터를 밀어넣는 함수도 같이 만들어주자.

RenderManager.h

struct TweenDesc
{
	TweenDesc()
	{
		curr.animIndex = 0;
		next.animIndex = -1;
	}

	void ClearNextAnim()
	{
		next.animIndex = -1;
		next.currFrame = 0;
		next.nextFrame = 0;
		next.sumTime = 0;
		tweenSumTime = 0;
		tweenRatio = 0;
	}

	float tweenDuration = 1.0f;
	float tweenDuration = 1.0f;
	float tweenRatio = 0.f;
	float tweenSumTime = 0.f;
	float padding = 0.f;
	KeyframeDesc curr;
	KeyframeDesc next;
};

public:
	void PushTweenData(const TweenDesc& desc);
private:
	TweenDesc _tweenDesc;
	shared_ptr<ConstantBuffer<TweenDesc>> _tweenBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _tweenEffectBuffer;

 

RenderManager.cpp

void RenderManager::Init(shared_ptr<Shader> shader)
{
	_tweenBuffer = make_shared<ConstantBuffer<TweenDesc>>();
	_tweenBuffer->Create();
	_tweenEffectBuffer = _shader->GetConstantBuffer("TweenBuffer");
}

void RenderManager::PushTweenData(const TweenDesc& desc)
{
	_tweenDesc = desc;
	_tweenBuffer->CopyData(_tweenDesc);
	_tweenEffectBuffer->SetConstantBuffer(_tweenBuffer->GetComPtr().Get());
}

 

이렇게 해주고 이제 ModelAnimator의 Update부분을 수정해주면 된다. 중요한 부분은 현재 애니메이션은 진행시키고 만약 다음 애니메이션이 예약되어 있다면 두 애니메이션을 섞어서 재생해주면 된다. 

ModelAnimator.cpp

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

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

	TweenDesc& desc = _tweenDesc;

	desc.curr.sumTime += DT;
	//현재 애니메이션
	{
		shared_ptr<ModelAnimation> currentAnim = _model->GetAnimationByIndex(desc.curr.animIndex);
		if (currentAnim)
		{
			float timePerFrame = 1 / (currentAnim->frameRate * desc.curr.speed);
			if (desc.curr.sumTime >= timePerFrame)
			{
				desc.curr.sumTime = 0;
				desc.curr.currFrame = (desc.curr.currFrame + 1) % currentAnim->frameCount;
				desc.curr.nextFrame = (desc.curr.currFrame + 1) % currentAnim->frameCount;
			}

			desc.curr.ratio = (desc.curr.sumTime / timePerFrame);
		}
	}

	// 다음 애니메이션이 예약 되어 있다면
	if (desc.next.animIndex >= 0)
	{
		desc.tweenSumTime += DT;
		desc.tweenRatio = desc.tweenSumTime / desc.tweenDuration;

		if (desc.tweenRatio >= 1.f)
		{
			// 애니메이션 교체 성공
			desc.curr = desc.next;
			desc.ClearNextAnim();
		}
		else
		{
			// 교체중
			shared_ptr<ModelAnimation> nextAnim = _model->GetAnimationByIndex(desc.next.animIndex);
			desc.next.sumTime += DT;

			float timePerFrame = 1.f / (nextAnim->frameRate * desc.next.speed);

			if (desc.next.ratio >= 1.f)
			{
				desc.next.sumTime = 0;

				desc.next.currFrame = (desc.next.currFrame + 1) % nextAnim->frameCount;
				desc.next.nextFrame = (desc.next.currFrame + 1) % nextAnim->frameCount;
			}

			desc.next.ratio = desc.next.sumTime / timePerFrame;
		}
	}

	//Anim Update
	ImGui::InputInt("AnimIndex", &desc.curr.animIndex);
	_keyframeDesc.animIndex %= _model->GetAnimationCount();

	//다음 애니메이션 골라주기
	static int32 nextAnimIndex = 0;
	if (ImGui::InputInt("NextAnimIndex", &nextAnimIndex))
	{
		nextAnimIndex %= _model->GetAnimationCount();
		desc.ClearNextAnim(); // 기존꺼 밀어주기
		desc.next.animIndex = nextAnimIndex;
	}

	if (_model->GetAnimationCount() > 0)
		desc.curr.animIndex %= _model->GetAnimationCount();



	ImGui::InputFloat("Speed", &desc.curr.speed, 0.5f, 4.f);

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

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

 

이렇게 해주면 이제 자연스럽게 애니메이션이 변경되는 모습을 볼 수 있다.

 

 

2.Sky Box 

 

이제 하늘을 보이도록 만들어보자. 하늘을 보이게 하는 방법은 첫번째는 하늘을 큐브 모양의 형태로 만들어 준 다음에 큐브 모양을 카메라를 따라 다니게 붙여주면 된다. 지금은 조금 더 쉽게 구 형태로 만들어보자.

그리고 이 구가  엄청 멀리 있을 텐데 그 만큼 멀리있는 물체가 있을 때 그 물체도 보이게끔 처리해야한다. 그리고 이 구를 엄청 크게 한다고 해도 뒷면이라서 보이지 않기 때문에 이 부분도 처리를 해주어야한다.

Global.fx

RasterizerState FrontCounterClockwiseTrue
{
    FrontCounterClockwise = true;
};

카메라의 거의 끝에 구가 위치해야하는데 이 처리는 쉐이더쪽에서 하늘쪽을 그릴때만 깊이를 1에 가깝게 세팅해준다.

그리고 구를 쉐이더에서 처리해줄 때 카메라와 같은 위치에 원점이 있어야하기 때문에 월드변환을 스킵해준다.

이때 화면을 회전했을 때 자연스럽게 보이기 위해 회전의 값은 적용하려고 하면 4*4 행렬에서 마지막을 0으로 밀어주면 된다.

이때 투영화와 Rasterizer 단계를 w 값을 나눠주는데 이를 통해 깊이값이 0~1사이로 바뀌게 된다. 이때 투영단계에서 w값에 1의 근사값을 곱해주면 깊이가 카메라의 끝에 위치하게 되기 때문에 저 멀리에 있게 보인다.

SkyDemo.fx

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

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

  
VS_OUT VS(VertexTextureNormalTangent input)
{
    VS_OUT output;

	// Local -> World -> View -> Projection
    float4 viewPos = mul(float4(input.position.xyz, 0), V);
    //투영좌표
    float4 clipSpacePos = mul(viewPos, P);
    output.position = clipSpacePos.xyzw;
    //
    output.position.z = output.position.w * 0.999999f;

    output.uv = input.uv;

    return output;
}

float4 PS(VS_OUT input) : SV_TARGET
{
    float4 color = DiffuseMap.Sample(LinearSampler, input.uv);
    return color;
}


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

 

이렇게 해주고 이제 하늘에 해당하는 Texture를 Material로 Load하고 구를 만들어주면 된다.

SkyDemo.cpp

#include "pch.h"
#include "SkyDemo.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 SkyDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"18. SkyDemo.fx");

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(_shader);
		auto texture = RESOURCES->Load<Texture>(L"Sky", L"..\\Resources\\Textures\\Sky01.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"Sky", material);
	}

	{
		// Object
		_obj = make_shared<GameObject>();
		_obj->GetOrAddTransform();
		_obj->AddComponent(make_shared<MeshRenderer>());
		{
			auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
			_obj->GetMeshRenderer()->SetMesh(mesh);
		}
		{
			auto material = RESOURCES->Get<Material>(L"Sky");
			_obj->GetMeshRenderer()->SetMaterial(material);
		}
	}

	// 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>());


	RENDER->Init(_shader);
}

void SkyDemo::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 SkyDemo::Render()
{

}

 

 

 

만약 깊이를 정해주는 부분에서 1로 밀어주게 되면 

output.position.z = output.position.w * 0.999999f;

이 부분을 그냥 w만 곱해주면 깊이 테스트에서 최대값인 1로 같기때문에 그려주지 않는다. 

만약 월드, 뷰포트 변환 행렬은 스킵하게 된다면

// Local -> World -> View -> Projection
float4 viewPos = mul(float4(input.position.xyz, 0), V);

우리가 아무리 회전을 해도 한 곳만 바라보게 된다. 프로젝션 연산에서 Z값은 w값에 세이브해두고 레스터라이저단계에서 나눠주는 것으로 -1 ~ 1 0~1로 설정해주게 된다.

+ Recent posts