DirectX

69_인스턴싱_ModelAnimation(인스턴싱)

devRiripong 2024. 3. 18.
반응형

1. AnimInstancingDemo 클래스 생성하고 세팅하기

 

ModelInstancingDemo 클래스를 복붙하고 이름을 AnimInstancingDemo라고 한다.

코드를 맞게 수정한다.

	_shader = make_shared<Shader>(L"22. AnimlInstancingDemo.fx");

 

Main에 가서도 세팅한다.

#include "AnimInstancingDemo.h"
	desc.app = make_shared<AnimInstancingDemo>(); // 실행 단위

Client를 빌드한다.

 

AssimpTool의 AniimationDemo의 코드를 참고해 볼 수 있다.

AnimationDemo::CreateKachujin을 보면

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

이런 식으로 만들었다.

AnimInstancingDemo::Init에 복붙 한다.

#include "pch.h"
#include "AnimInstancingDemo.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 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)); // Tower는 너무 크기 때문에 크기를 줄여 준다.
		obj->AddComponent(make_shared<ModelRenderer>(_shader));
		{
			obj->GetModelRenderer()->SetModel(m1); // model이 mesh랑 material 관련 부분들을 다 들고 있는 식으로 만들어 놨다.
		}
		_objs.push_back(obj); 
	}

	RENDER->Init(_shader);
}

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

 

 

2. 22. AnimInstancingDemo.fx 생성하기

21. ModelInstancingDemo.fx를 복붙 해서 22. AnimInstancingDemo.fx로 하고

Client/Shader/Week3 필터에 넣는다.

 

일단 실행을 해본다.

 

애니메이션을 적용하지 않은 상태에서 T자 포즈로 웅장하게 캐릭터들이 나온다.

여기에 추가적으로 애니메이션을 적용시키는 작업을 할 것이다.

 

 

3. AnimInstancingDemo::Init에서 ModelAnimator 컴포넌트를 만들어 obj에 추가하고 Model, Material, Animation 정보를 담은 shared_ptr<class Model> m1을 세팅하기

AnimInstancingDemo::Init에서

		obj->AddComponent(make_shared<ModelRenderer>(_shader));
		{
			obj->GetModelRenderer()->SetModel(m1); // model이 mesh랑 material 관련 부분들을 다 들고 있는 식으로 만들어 놨다.
		}

ModelRenderer에 애니메이션을 섞어서 하면 헷갈릴 수 있으니 가만히 있는 모델과 분리해서 움직이는 거는 ModelAnimator에서 해주기로 한다.

		obj->AddComponent(make_shared<ModelAnimator>(_shader));
		{
			obj->GetModelAnimator()->SetModel(m1); // model이 mesh랑 material 관련 부분들을 다 들고 있는 식으로 만들어 놨다.
		}

이렇게 수정한다.

 

이제는 실행을 해도 동작을 안 한다.

 

4. InstancingManager::Render에서 RenderAnimRenderer 호출하기

InstancingManager.h에

	void RenderAnimRenderer(vector<shared_ptr<GameObject>>& gmaeObjects);

를 추가하고

 

InstancingManager::Render에서

	RenderAnimRenderer(gameObjects);

RenderAnimRenderer을 호출한다.

 

5. InstancingManager::RenderAnimRenderer 함수 정의하기

RenderModelRenderer의 코드를 RenderAnimRenderer에 복붙 해서 수정하는 식으로 구현한다.

복붙을 한 뒤 ModelRenderer가 아닌 ModelAnimator버전으로 수정한다.

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)
	{
		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); // InstanceId에 따라서 InstancingBuffer를 찾아준 다음에 버퍼에다가 데이터를 넣어주는 함수
			}

			// 막타
			// 대표로 할 애를 정하면 된다. 
			shared_ptr<InstancingBuffer>& buffer = _buffers[instanceId];
			vec[0]->GetModelAnimator()->RenderInstancing(buffer);
		}
	}
}

 

6. ModelAnimator::GetInstanceID, ModelAnimator::RenderInstancing 함수 정의하기

그리고 ModelAnimator 클래스로 가서 RenderInstancing과 GetInstanceID 함수 작업을 해준다.

void RenderInstancing(shared_ptr<class InstancingBuffer>& buffer); 
InstanceID GetInstanceID(); 
InstanceID ModelAnimator::GetInstanceID()
{
	return make_pair((uint64)_model.get(), (uint64)_shader.get()); 
}

 

그리고

ModelAnimator::Update()의 코드를 전부 ModelAnimator::RenderInstancing으로 옮기고 ModelAnimator::Update()는 삭제한다.

 

Engine 프로젝트를 빌드한다.

 

7. 22. AnimInstancingDemo.fx 기본 세팅 하기

22. AnimInstancingDemo.fx는 17. TweenDemo.fx를 참고한다.

shader를 기준으로 분석을 해보면서 옮기면서 작업을 한다.

#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 BoneTransforms[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; // current
    float4 n0, n1, n2, n3; // next  
    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, BoneTransforms[BoneIndex]); // Model Global (Root를 기준)로 일단 가기
    
    matrix m = GetAnimationMatrix(input); 
    
    output.position = mul(input.position, m);
    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)
};

대략적으로 코드가 이렇게 된다.

 

문제가 없는지 Client 프로젝트를 빌드한다.

 

8. ModelAnimator를 수정하기_Animation프레임 관리하는 코드는 UpdateTweenData로 옮기고, Instancing한 객체마다 다른 애니메이션을 넣을 준비하기

ModelAnimaor::RenderInstancing에서 호출하는 CreateTexture는

매 프레임마다 뼈의 위치는 어디에 있으니까 싹 모아가지고 그거를 맵으로 만들어서 shader의 Texutre2DArray TransformMap에 전달해 주는 흐름이었다.

 

CreateTexture는 모든 물체끼리 동일할까? 인스턴싱에 따라 각기 따로 파져서 전달해줄 방법을 찾아야 될지 고민이다.

 

Texture2DArray TransformMap;은 공용으로 사용할 수 있을까 나눠야 할까

애니메이션을 실시간으로 고쳐야 되고 이런 게 아닌 이상 얘는 공유해서 사용할 수가 있다고 본다.

 

ModelAnimator::RenderInstancing에서

	TweenDesc& desc = _tweenDesc;

이게 있었는데 뭐 하는 건지 살펴보면

	RENDER->PushTweenData(desc);

TweenData를 세팅하는 부분이었다.

	TweenDesc _tweenDesc;

이것에 따라 가지고 현재 나의 프레임은 얼마인지 등등을 고쳐서 수정하고 있었다.

 

	KeyframeDesc _keyframeDesc; 

이건 사용 안 하니 삭제한다.

 

TweenDesc은 애니메이션이 진행하는 deltaTime이랑 현재 진행하는 애니메이션, 다음 프레임에 진행하는 애니메이션 정보를 들고 있다.

이 아이들은 어떨까

따로 만들어줘야 할지, 통합해서 관리할 수 있을지

이걸 통합하면 모든 캐릭터들이 동일한 애니메이션, 동일한 프레임을 틀고 있는 게 된다. 칼 군무.

의도했다면 상관없지만 각기 따로 움직이는 경우가 많을 것이다.

 

ModelAnimator::RenderInstancing에서

	RENDER->PushTweenData(desc);

0번이라는 애 자체만 자기의 정보를 밀어 넣게 될 것이다.

좀 말이 안 된다.

애니메이션은 각기 따로 움직여야 하지 않을까.

world를 모아서 넣어줬던 거 마냥 통합해서 만들 방법을 찾아야 한다.

그 부분이 달라지는 부분이다.

 

ModelAnimator.h에 Update 함수를 복원한다.

	virtual void Update() override;

매 프레임마다 실행이 되는 아이가 있긴 할 것이다. 보통 애니메이션의 프레임 관리하는 코드들은 사실상 그런 쪽 코드에 같이 끼워 있는 게 맞는 거 같다. 그런 걸 UpdateTweenData로 따로 파준다.

	void UpdateTweenData(); 

이걸 매 프레임마다 누군가 호출해줘야 하는데

AnimInstancingDemo::Update() / INSTANCING->Render(_objs) / RenderAnimRenderer(gameObjects); / gameObject->GetModelAnimator()->UpdateTweenData(); 이런 흐름으로 호출된다. 

 

ModelAnimator::RenderInstancing의 트윈 관련 정보들을 싹 다 모아 가지고 UpdateTweenData에 옮긴다.

RENDER->PushTweenData(desc);

는 자신의 정보만 넣어주는 건 의미 없기 때문에 삭제한다.

 

imGui로 테스트했던 코드들도 삭제한다.

 

모든 애들이 같은 애니를 하면 테스트가 안되니

ModelAnimator::ModelAnimator(shared_ptr<Shader> shader)
	: Super(ComponentType::Animator), _shader(shader)
{
	// TEST
	_tweenDesc.next.animIndex = rand() % 3; // 다음 애니메이션을 무엇으로 틀지
	_tweenDesc.tweenSumTime += rand() % 100; // 현재 시간이 얼마나 경과 되었는지 
}

이렇게 렌덤으로 맞춰준다.

 

ModelAnimator::RenderInstancing이 깔끔해졌으니 다시 보면

TrasnformMap은 공유해서 쓰기로 했었고,

Bone데이터의 경우도 공유해서 하나의 물체가 한다고 가정했다.

Transform은 어차피 인스턴싱을 해서 모든 애들을 통합해서 관리하고 있기 때문에 삭제한다.

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

		buffer->PushData();  // world와 관련된 정보

이걸 추가하고

		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->DrawIndexedInstanced(0, _pass, mesh->indexBuffer->GetCount(), buffer->GetCount()); 

이렇게 맞춰주면 된다.

Engine 프로젝트를 빌드한다.

 

9. 22. AnimInstancingDemo.fx 에서 TweenBuffer 관련 케어하기

아직 남은 게 있다.

cbuffer TweenBuffer
{
    TweenFrameDesc TweenFrames;
};

이것과 관련된 부분들을 케어해야 한다.

TweenFrame이란 게 한 마리만 케어하는 게 아니라 모든 애들이 각기 다 따로 들고 있다고 했으니까 이 부분을 케어해야 하는데

cbuffer BoneBuffer
{
    matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
};

BoneBuffer에 넘겨받았던 것과 마찬가지로 TweenBuffer도 이런 느낌으로 만들어 주면 된다.

 

최대 모델이 몇 개인지 정의를 하고

#define MAX_MODEL_INSTANCE 500

이것에 따라서

cbuffer TweenBuffer
{
    TweenFrameDesc TweenFrames[MAX_MODEL_INSTANCE];
};

이렇게 설정을 바꿔보도록 한다.

 

matrix GetAnimationMatrix(VS_IN input)에서

    animIndex[0] = TweenFrames[?].curr.animIndex;

자기가 몇 번째 인지 어떻게 알 수 있을까?

 

struct VS_IN의

    uint instanceID : SV_InstanceID; 

이게 그 용도다.

몇 번째 instance인지 나타내는 정보라고 볼 수 있다.

 

그래서 VS_IN에 그 정보가 들어있다고 보면 되는 거고,

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;
    result = lerp(result, nextResult, TweenFrames[input.instanceID].tweenRatio);

이렇게 TweenFrames에 몇 번째 인지 넣어주면 된다.

 

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; // current
    float4 n0, n1, n2, n3; // next  
    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;
}

 

무작정 공유해서 칼군무를 추는 게 아니라 어지럽게 내가 추는 춤을 출 수 있게끔 다 나눠준 셈이 된다고 볼 수 있다.

 

Client를 빌드한다.

 

10. RenderManager에서 InstancedTweenDesc을 PushTweenData 하게 수정하기

이제 TweenFrames라는 버퍼를 만들어서 연결해 주는 작업이 필요하다.

지금까지 RenderManager에서 TweenFrame이란 걸 따로 관리해가지고 그걸 PushData로 밀어 넣는 작업을 했었다.

RenderManager.h의

struct TweenDesc를 이용해서 버퍼를 만들고 있었는데

여기서도 MAX_MODEL_INSTANCE가 있어야 한다.

#define MAX_MODEL_INSTANCE 500
struct InstancedTweenDesc
{
	TweenDesc tweens[MAX_MODEL_INSTANCE]; 
};

이렇게 유도를 한다.

 

PushTweenData로 넣어 줬던 작업을 얘로 바꾼다.

	void PushTweenData(const InstancedTweenDesc& desc);
	InstancedTweenDesc _tweenDesc;
	shared_ptr<ConstantBuffer<InstancedTweenDesc>> _tweenBuffer;
	ComPtr<ID3DX11EffectConstantBuffer> _tweenEffectBuffer;

 

 

RenderManager.cpp에서도

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

 

RenderManager::Init에서

	_tweenBuffer = make_shared<ConstantBuffer<InstancedTweenDesc>>();
	_tweenBuffer->Create();
	_tweenEffectBuffer = _shader->GetConstantBuffer("TweenBuffer");

 

이렇게 바꿔주면 배열 형태로 들어가게 될 것이니까 그 부분이 케어가 된다.

 

남은 건 PushTweenData를 만들어 줘서 밀어줘야 한다.

어디서 작업을 해야 할까

일단 TweenDesc _tweenDesc를 외부에서 꺼내 사용할 수 있도록

ModelAnimator.h에

	TweenDesc& GetTweenDesc() { return _tweenDesc; } 

추가한다.

 

11. InstancingManager::RenderAnimRenderer에서 각 인스턴스의 tweenDesc를 채우기

InstancingManager에서 나머지 애들을 다 조립해서 관리하는 김에 그것도 해주도록 한다.

 

RenderAnimRenderer에서 Tween과 관련된 부분들도 한 번에 합쳐서 인스턴싱 매니저 쪽에서 통합해서 갖고 오도록 만들어 볼 건데

 

InstancingManager::RenderAnimRenderer의

AddData(instanceId, data);

가 world 정보를 다 긁어 오는 부분이다.

 

하는 김에 TweenData까지 갖고 와서 넣어주면 되지 않을까

Tween 정보들도 갖고 와서 넣어주도록 한다.

{
    InstancedTweenDesc tweenDesc;

    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); // InstanceId에 따라서 InstancingBuffer를 찾아준 다음에 버퍼에다가 데이터를 넣어주는 함수

        // INSTANCING
        gameObject->GetModelAnimator()->UpdateTweenData(); 
        tweenDesc.tweens[i] = gameObject->GetModelAnimator()->GetTweenDesc();
    }

Engine을 빌드한다.

 

12. InstancingManager::RenderAnimRenderer에서 tweenDesc를 힙영역에 만들기

InstancedTweenDesc가 크기가 클 것이다. TweenDesc tweens의 MaxSize가 500개인데 이게 스택 범위를 넘어가면 용량을 초과해서 빌드가 안되게 된다. 그런 부분을 우회하기 위해서

	InstancedTweenDesc tweenDesc;

이거를 힙 영역에다 만들었다가 하거나, 다른 영역에 옮겨 났다가 하거나 다른 방식을 채택하면 된다.

일단은 힙 영역에다가 만드는 식으로 해본다.

	shared_ptr<InstancedTweenDesc> tweenDesc = make_shared<InstancedTweenDesc>();
	tweenDesc->tweens[i] = gameObject->GetModelAnimator()->GetTweenDesc();

이렇게 Tween 정보를 만들어 주고,

그리고 모든 정보를 취합했으면

			RENDER->PushTweenData(*tweenDesc.get()); 

이렇게 밀어 넣는다.

 

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)
	{
		const vector<shared_ptr<GameObject>>& vec = pair.second;

		//if (vec.size() == 1)
		//{
		//	vec[0]->GetModelAnimator()->RenderSingle(); 
		//    하나만 있을 때는 옛날 버전으로 하면 된다. 
		//}
		//else 무조건 인스턴싱을 적용하는 버전으로 만든다.
		{
			shared_ptr<InstancedTweenDesc> tweenDesc = make_shared<InstancedTweenDesc>();

			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); // InstanceId에 따라서 InstancingBuffer를 찾아준 다음에 버퍼에다가 데이터를 넣어주는 함수
			
				// 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);
		}
	}
}

 

좀 복잡하긴 하지만 아이디어는 통합해서 관리하는 애가 매니저다 보니까

월드 정보를 긁어 오는 김에 Tween 정보도 같이 긁어 와서 그거를 통합해서 넣어주고,

그다음에 RenderInstancing 하는 애는 나머지 정보들만 넣어주게 만들어 봤다.

 

물론 얼마든지 수정할 수 있다. 마음에 안 들면 정보를 모아서 RenderInstancing의 두 번째 인자로 넘겨준다거나 해서 모든 조립 작업은 RenderInstancing에서 하는 것도 방법이다.

 

잘 실행된다면 애니메이션도 이것저것 잘 실행이 되어야 정상적인 상황이다라고 볼 수 있다.

 

실행을 하면

군무가 제각각이다.

이렇게 많아도 정상적으로 잘 돌아가고 있다.

캐릭터까지 이런 식으로 인스턴싱을 할지는 고민이다. 유니티에서도 애니메이션이 들어가는 SkinedMashRenderer의 경우에는 문서를 보면 Instancing이 안되어 있다고 나와있다. 다른 설정 바꿔서 할 수 있지만 기본적으로 안 되어 있다.

 

 

중요한 건 지금까지 해 왔던 걸 Instancing 방식으로 수정을 해 본 것이다.

큰 규모의 뭔가 만들 때 고민 되는 건 단일 프로젝트로 통합해서 모든 경로에 대해서 다 되게끔 만드는 것도 어렵다.

 

쉐이더 많아지는데 어떻게 깔끔하게 관리하는지 이런 게 항상 어려운 부분인데 일단은 지금은 만단 3가지 아이를 통합해서 관리할 건데 pass를 구분해서 0번 pass는 메쉬, 2번은 model, 3번은 애니메이션, 4번은 와이어프레임이나 다른 용도 등등으로 해서 규칙을 정한 다음에 가는 게 맞는 거 같다.

 

지금 사용하는 쉐이더가 늘어나면 관리하는 게 점점 힘들어지기 때문에 고용적인 부분은 하나로 묶어 놓는 방식을 하나 만들어 주고, 진행을 한다.

즉 MeshIntancingDemo, ModelInstancingDemo, AnimInstasncingDemo 이걸 통합해서 다음시간에 하나의 쉐이더에 넣어주고, 그 쉐이더도 다른 쉐이더에서도 공용으로 쓸 수 있도록, 유니티로 치면 표준 쉐이더가 있다. 살펴보면 표준적인 걸 사용하고 있다. 그런 식으로 우리만의 standatrd 쉐이더를 만드는 작업을 할 건데 그 경우에서는 Common에다가 렌더링 하는 부분, Instancing 삼총사를 만드는 부분도 common으로 만들어서 파주는 게 합리적일 것이다.

 

Instancing 만들 때 무엇이 공통이고, 무엇이 따로인지 이게 오늘의 고민거리였다.

하나의 선택이지 최종적인 답안이라고 볼 수는 없다.

 

유니티처럼 범용적으로 사용할 수 있게 만든 게 대단한 거다.

반응형

댓글