프로그래밍 공부
작성일
2023. 11. 17. 17:32
작성자
WDmil
728x90

애니메이션의 저장을 clip형태로 하였음으로, 이제 데이터를 보관하고, Texture2D형태로 구현한 뒤, DeviceContext에서 출력할 일만 남았다.

 

일단 DeviceContext로 넘겨줄, ConstBuffer를 정의해주어야 한다.

	// ClipTransform 구조체: 각 프레임 및 본에 대한 변환 매트릭스를 저장하는 구조체
	struct ClipTransform
	{
		Matrix transform[MAX_FRAME][MAX_BONE]; // 각 프레임 및 본에 대한 변환 매트릭스 배열
	};

	// Frame 구조체: 애니메이션 재생에 사용되는 프레임 정보를 저장하는 구조체
	struct Frame
	{
		int clip = 0;        // 현재 재생 중인 클립의 인덱스
		int curFrame = 0;    // 현재 프레임 인덱스
		int nextcurFrame = 0;    // 다음 프레임 인덱스
		float time = 0;      // 현재 프레임에서의 경과 시간
		float scale = 1.0f;   // 애니메이션 재생 속도 조절용 스케일
		Float3 def;
	};

	// FrameBuffer 클래스: 상속된 ConstBuffer를 사용하여 프레임 데이터를 GPU에 전달하는 클래스
	class FrameBuffer : public ConstBuffer
	{
	public:
		FrameBuffer() : ConstBuffer(&frame, sizeof(Frame)) {}; // 프레임 데이터를 상속된 ConstBuffer에 전달

		Frame* GetData() { return &frame; } // 프레임 데이터에 대한 포인터 반환

	private:
		Frame frame; // 프레임 데이터를 저장하는 구조체 인스턴스
	};

위와같은 구조체를 정의해주면 된다.

 

각 데이터는, 현재 재생중인 클립과, 현재 프래임, 다음 프레임, 현재 프레임과 다음 프레임의 백분율을 1로 하였을 때, 현재 % 위치.

 

scale은 애니메이션의 속도이다.

 

Flaot3 def는, 16byte를 맞춰주기 위한 더미데이터 이다.

 

이제 Model을 생성하고 재생해주는, ModelAnimator객체를 생성하자.

 

해당 객체는 Model을 상속받고, 애니메이션을 저장하고 그 애니메이션을 모델에 갱신시켜주는 역할을 한다.

// ModelAnimator 클래스: Model 클래스를 상속하여 모델의 애니메이션을 다루는 클래스
class ModelAnimator : public Model
{
protected:
	// ClipTransform 구조체: 각 프레임 및 본에 대한 변환 매트릭스를 저장하는 구조체
	struct ClipTransform
	{
		Matrix transform[MAX_FRAME][MAX_BONE]; // 각 프레임 및 본에 대한 변환 매트릭스 배열
	};

	// Frame 구조체: 애니메이션 재생에 사용되는 프레임 정보를 저장하는 구조체
	struct Frame
	{
		int clip = 0;        // 현재 재생 중인 클립의 인덱스
		int curFrame = 0;    // 현재 프레임 인덱스
		int nextcurFrame = 0;    // 다음 프레임 인덱스
		float time = 0;      // 현재 프레임에서의 경과 시간
		float scale = 1.0f;   // 애니메이션 재생 속도 조절용 스케일
		Float3 def;
	};

	// FrameBuffer 클래스: 상속된 ConstBuffer를 사용하여 프레임 데이터를 GPU에 전달하는 클래스
	class FrameBuffer : public ConstBuffer
	{
	public:
		FrameBuffer() : ConstBuffer(&frame, sizeof(Frame)) {}; // 프레임 데이터를 상속된 ConstBuffer에 전달

		Frame* GetData() { return &frame; } // 프레임 데이터에 대한 포인터 반환

	private:
		Frame frame; // 프레임 데이터를 저장하는 구조체 인스턴스
	};

public:
	// 생성자: 모델 이름을 인자로 받아 초기화
	ModelAnimator(string name);
	// 소멸자: 동적으로 할당된 자원 해제
	~ModelAnimator();

	// 업데이트 함수: 프레임 및 애니메이션 업데이트 수행
	void Update();
	// 렌더 함수: 모델을 렌더링
	void Render();
	// GUI 렌더 함수: 애니메이션 관련 GUI 렌더링
	void GUIRender();

	// 클립 읽기 함수: 애니메이션 클립을 읽어와서 저장
	void ReadClip(string clipName, UINT clipNum = 0, UINT count = 0);
	// 텍스처 생성 함수: 텍스처를 생성하여 저장
	void CreateTexture();

protected:
	// 클립 변환 생성 함수: 지정된 인덱스에 대한 클립 변환 매트릭스 생성
	void CreateClipTransform(UINT index);

protected:
	vector<ModelClip*> clips; // 모델 클립들을 저장하는 벡터

	ClipTransform* clipTransforms; // 원본 모델 변환 매트릭스 배열
	ClipTransform* nodeTransforms; // 보정 모델 변환 매트릭스 배열

	ID3D11Texture2D* texture = nullptr; // 텍스처를 나타내는 Direct3D 텍스처 인터페이스
	ID3D11ShaderResourceView* srv;      // 셰이더에서 사용하는 텍스처를 나타내는 인터페이스

	FrameBuffer* frameBuffer; // 상속된 ConstBuffer를 사용하여 프레임 데이터를 GPU에 전달하는 인스턴스

	bool AutoAnimation = false; // 자동 애니메이션 재생 여부를 나타내는 플래그
	float nowFrame = 0;         // 현재 프레임을 나타내는 변수
};

 

각 객체의 저장데이터를 확인해보자.

 

모델 클립을 저장하는 벡터가 있다.

 

이것은 저번에 작성하였던, 2D이미지 형태의 모션데이터를. 가지고있는 이미지객체를. 배열로 저장하는 vector이다.

 

ClopTransform은, 각 프레임 본에대한 변환 매트릭스 배열인데,

 

이차원배열로 이루어져있는걸 알 수 있듯, 각 프레임 본에 대한 Matrix값을 2D이미지로 변환하기 전의 데이터 라고 이해하면 된다.

 

원본과 보정모델이 나누어져 있는 것 은, 원본값은 원본값을 현재 모션의 이동에 대해 이동한것이고,

 

보정값은. 거기에서 Vertice간 떨어진 값을을 보정하여. 떨어지지 않도록 한 보정된 각 Vertice간 Vector를 저장한다.

 

texture는, Buffer로 전달할 Texture2D버퍼를 의미하고,

srv는 이미지형태로 변환된 모션데이터를 말한다.

 

FrameBuffer는, 현재 Anim의 상태를 전달한다고 이해하면 된다.

 

bool값과 nowFrmae은, 프레임간 보간작업을 위해 설정한것이다.


LightPixelInput VS(VertexUVNormalTangentBlend input)
{
	LightPixelInput output;
	// 입력 꼭짓점을 월드 공간으로 변환합니다.
	matrix transform = mul(SkinWorld(input.indices, input.weights), world);
	
	output.pos = mul(input.pos, transform);
	// 월드 공간에서의 위치를 저장합니다.
	output.worldPos = output.pos;
	// 뷰 공간에서의 위치를 저장합니다.
	output.viewPos = invView._41_42_43;
	
	// 위치를 뷰 공간으로 변환합니다.
	output.pos = mul(output.pos, view);
	// 위치를 투영 공간으로 변환합니다.
	output.pos = mul(output.pos, projection);
	
	// UV 좌표를 저장합니다.
	output.uv = input.uv;
	
	// 노멀과 탄젠트를 월드 공간으로 변환하고, 
	// 빈노멀(노멀과 탄젠트에 수직인 벡터)을 계산합니다.
	output.normal = mul(input.normal, (float3x3) transform);
	output.tangent = mul(input.tangent, (float3x3) transform);
	output.binormal = cross(output.normal, output.tangent);
	
	return output;
}

우선, 데이터를 넘기기전에 VertexHeader와, 사용하던 hlsl을 재작업해야한다.

 

원래 CPU내에서는 오브젝트가 움직이지 않지만, GrapicCard에서는 오브젝트가 움직인것 처럼 처리해야한다.

 

그렇기 때문에, 모션데이터와 Vertice데이터간의 보간작업을 처리한다.

 

struct VertexUVNormalTangentBlend
{
	float4 pos : POSITION;
	float2 uv : UV;
	float3 normal : NORMAL;
	float3 tangent : TANGENT;
	float4 indices : BLENDINDICES;
	// 특정 본들, 본에 꼽혀있을 때, 어떤 본에 가중치를 줄 것인지 웨이트가 들어감.
	float4 weights : BLENDWEIGHTS;
};

cbuffer FrameBuffer : register(b3)
{
	int clip;
	int curFrame;
	int nextcurFrame;
	float time;
}

Texture2DArray transformMap : register(t0);

matrix SkinWorld(float4 indices, float4 weights)
{
	matrix transform = 0;
	matrix cur;
	matrix nextcur;
	
	matrix curanim;
	
	float4 c0, c1, c2, c3;
	float4 n0, n1, n2, n3;
	
	// 매트릭스 뽑는것.
	[unroll(4)] // 제한하는 구문. 4개이상 절때 돌아갈 수 없게 제한한다.
	for (int i = 0; i < 4; i++)
	{
		c0 = transformMap.Load(int4(indices[i] * 4 + 0, curFrame, clip, 0));
		c1 = transformMap.Load(int4(indices[i] * 4 + 1, curFrame, clip, 0));
		c2 = transformMap.Load(int4(indices[i] * 4 + 2, curFrame, clip, 0));
		c3 = transformMap.Load(int4(indices[i] * 4 + 3, curFrame, clip, 0));
		
		cur = matrix(c0, c1, c2, c3);
		
		n0 = transformMap.Load(int4(indices[i] * 4 + 0, nextcurFrame, clip, 0));
		n1 = transformMap.Load(int4(indices[i] * 4 + 1, nextcurFrame, clip, 0));
		n2 = transformMap.Load(int4(indices[i] * 4 + 2, nextcurFrame, clip, 0));
		n3 = transformMap.Load(int4(indices[i] * 4 + 3, nextcurFrame, clip, 0));
		
		nextcur = matrix(n0, n1, n2, n3);
		
		curanim = lerp(cur, nextcur, time);
		transform += mul(weights[i], curanim);
	}
	
	return transform;
}

위와같이 Vertex가 구성된다.

 

받아온 Texture2DArrray를 참조해서, 현재 Vertice의 위치값을 ViewMatrix에 변환하여 적용하여. Vertice가 실제로는 움직이지 않았지만, 움직인것 처럼 보이게 조작해준다.

 

TransformMap에서는 load를 통해서, 지정된 위치값의 vertice가 어떤식으로 조작되는지 transform에 정의해서 반영한다.

 

nextcur과 cur로 두개가 나누어진 이유는 선형보간 때문에 나누어놓았다.


픽셀간 선형보간 처리

 

각 프레임간 vertice는 뚝뚝 끊어지는것 처럼 데이터가 움직이게 된다.

 

vertice가 뚝뚝 끊어지는것 처럼 움직이는 이유는, hlsl에서 각 vertice가 다음 프레임으로 진입하였을 때,

 

그 프레임에 해당되는 vertice로 변화하도록 위치값을 Matrix만큼 갱신하기 때문인데,

 

이러한 현상을 해결하기위해 선형보간 작업을 한다.

 

이후 Matrix와 이전 Matrix를 알고, 현재 Matrix가 진행된지 시간이 얼마나 진행되었는지 알게 된다면.

 

우리는 현 matrix와 이후 matirx가 이동된 vertice간 거리를 알 고 있음으로.

 

그 거리를 프레임단위로 미분한다음, 현재 진행%만큼 곱해주어 적분치게 되면.

 

부드럽게 프레임이 움직이는것 처럼. 뚝뚝 끊기는 현상이 사라질것 이다.

 

선형보간 이전 영상

위와같은 방식으로 진행해보자.

 

 

 

선형보간 이후 영상

 

다음과 같이 선형보간 작업이 진행된 것을 볼 수 있다.

 

즉, 각 프레임간 Transform이 변화하는것 을 한번에 변화하는것 이아닌. 전 프레임과 이후 프레임이 천천히 변화하도록 해주는것 이다.

 

 


 

hlsl을 작업하였음으로, 이제 ModelAinmation과 Clip을 저장하고 출력하게 해보자.

void ModelExporter::ExportClip(string clipName)
{
	FOR(scene->mNumAnimations)
	{
		Clip* clip = ReadClip(scene->mAnimations[i]);
		WriteClip(clip, clipName, i);
	}
}

전에 만들어놓았던 ModelExport에 ExportClip함수를 추가한다.

 

모션또한 FBX파일로 받아오고 출력하고 저장할 수 있음으로 활용한다.

Clip* ModelExporter::ReadClip(aiAnimation* animation)
{
	// 새로운 Clip 객체를 동적으로 생성합니다.
	Clip* clip = new Clip();

	// 애니메이션의 이름을 Clip 객체에 할당합니다.
	clip->name = animation->mName.C_Str();

	// 애니메이션의 초당 틱 수를 Clip 객체에 할당합니다.
	clip->tickPerSecond = (float)animation->mTicksPerSecond;

	// 애니메이션의 총 프레임 수를 Clip 객체에 할당합니다.
	clip->frameCount = (UINT)(animation->mDuration) + 1;

	// 애니메이션 채널(노드)의 수만큼 ClipNode 객체를 담을 벡터를 할당합니다.
	vector<ClipNode> clipNodes;
	clipNodes.reserve(animation->mNumChannels);

	// 각 애니메이션 채널(노드)에 대해 반복합니다.
	FOR(animation->mNumChannels)
	{
		// 현재 채널(노드)을 가져옵니다.
		aiNodeAnim* srcNode = animation->mChannels[i];

		// 새로운 ClipNode 객체를 생성하고 이름을 설정합니다.
		ClipNode node;
		node.name = srcNode->mNodeName;

		// KeyData 객체를 생성하여 각 키프레임의 위치, 회전, 스케일을 저장합니다.
		KeyData data;

		// 위치 키프레임 정보 저장
		data.positions.resize(srcNode->mNumPositionKeys);
		for (UINT k = 0; k < srcNode->mNumPositionKeys; k++)
		{
			KeyVector keyPos;
			aiVectorKey key = srcNode->mPositionKeys[k];
			keyPos.time = key.mTime;
			memcpy_s(&keyPos.value, sizeof(Float3),
				&key.mValue, sizeof(aiVector3D));

			data.positions[k] = keyPos;
		}

		// 회전 키프레임 정보 저장
		data.rotations.resize(srcNode->mNumRotationKeys);
		for (UINT k = 0; k < srcNode->mNumRotationKeys; k++)
		{
			KeyQuat keyRot;
			aiQuatKey key = srcNode->mRotationKeys[k];
			keyRot.time = key.mTime;

			keyRot.value.x = (float)key.mValue.x;
			keyRot.value.y = (float)key.mValue.y;
			keyRot.value.z = (float)key.mValue.z;
			keyRot.value.w = (float)key.mValue.w;

			data.rotations[k] = keyRot;
		}

		// 스케일 키프레임 정보 저장
		data.scales.resize(srcNode->mNumScalingKeys);
		for (UINT k = 0; k < srcNode->mNumScalingKeys; k++)
		{
			KeyVector keyScale;
			aiVectorKey key = srcNode->mScalingKeys[k];
			keyScale.time = key.mTime;
			memcpy_s(&keyScale.value, sizeof(Float3),
				&key.mValue, sizeof(aiVector3D));

			data.scales[k] = keyScale;
		}

		// SetClipNode 함수를 통해 키프레임 데이터를 ClipNode에 설정합니다.
		SetClipNode(data, clip->frameCount, node);

		// 현재 처리한 ClipNode을 벡터에 추가합니다.
		clipNodes.push_back(node);
	}

	// 읽어온 키프레임 데이터를 이용하여 애니메이션의 프레임 데이터를 설정합니다.
	ReadKeyFrame(clip, scene->mRootNode, clipNodes);

	// 생성한 Clip 객체를 반환합니다.
	return clip;
}

 

각 모션데이터를 FBX에서 읽어오고 출력할 수 있게 Assimp가 정의해줌으로 활용해주자.

 

void ModelExporter::WriteClip(Clip* clip, string clipName, UINT index)
{
	string file = "Models/Clips/" + name + "/" + clipName + to_string(index) + ".clip";

	CreateFolders(file);

	BinaryWriter* writer = new BinaryWriter(file);
	writer->String(clip->name);
	writer->UInt(clip->frameCount);
	writer->Float(clip->tickPerSecond);

	writer->UInt(clip->keyFrames.size());
	for (KeyFrame* keyFrame : clip->keyFrames)
	{
		writer->String(keyFrame->boneName);
		writer->UInt(keyFrame->transforms.size());
		writer->Byte(keyFrame->transforms.data(), sizeof(KeyTransform) * keyFrame->transforms.size());

		delete keyFrame;
	}

	delete clip;

	delete writer;
}

 

WriteClip을 통해 FBX에서 가져온 데이터를 순서대로 byte순으로 저장하고, 쓸 수 있도록 해준다.

 

void ModelAnimator::ReadClip(string clipName, UINT clipNum, UINT count)
{
    // 재시도 횟수가 2보다 작거나 같은지 확인
    assert(count <= 2);

    // 클립 파일의 경로 구성
    string file = "Models/Clips/" + name + "/" + clipName + to_string(clipNum) + ".clip";

    // BinaryReader 객체를 사용하여 클립 파일 읽기 시도
    BinaryReader* reader = new BinaryReader(file);

    // 클립 파일 읽기에 실패하면서 재시도 횟수가 남아 있을 경우
    if (reader->IsFailed())
    {
        // FBX 파일에서 클립을 내보내기 위한 ModelExporter 객체 생성
        file = "Models/Animations/" + name + "/" + clipName + ".fbx";
        ModelExporter* exporter = new ModelExporter(name, file);

        // 클립 내보내기 수행
        exporter->ExportClip(clipName);
        delete exporter; // ModelExporter 객체 해제

        // 재귀적으로 ReadClip 함수 호출하여 클립을 다시 시도
        ReadClip(clipName, clipNum, count + 1);
        return;
    }

    // 성공적으로 클립 파일을 읽어왔을 때의 처리
    ModelClip* clip = new ModelClip(); // 새로운 ModelClip 객체 생성
    clip->name = reader->String(); // 클립의 이름 설정
    clip->frameCount = reader->Int(); // 클립의 프레임 수 설정
    clip->tickPerSecond = reader->Float(); // 클립의 초당 틱 수 설정

    UINT boneCount = reader->UInt(); // 클립에 포함된 본의 수 설정
    FOR(boneCount)
    {
        KeyFrame* keyFrame = new KeyFrame(); // 새로운 KeyFrame 객체 생성

        keyFrame->boneName = reader->String(); // 키프레임에 속한 본의 이름 설정
        UINT size = reader->UInt(); // 키프레임의 변환 매트릭스 크기 설정

        if (size > 0)
        {
            keyFrame->transforms.resize(size); // 변환 매트릭스 배열 크기 설정

            // 변환 매트릭스 데이터를 바이트 단위로 읽어와서 keyFrame->transforms에 저장
            void* ptr = (void*)keyFrame->transforms.data();
            reader->Byte(&ptr, sizeof(KeyTransform) * size);
        }

        // 클립의 키프레임 맵에 현재 키프레임 추가
        clip->keyFrames[keyFrame->boneName] = keyFrame;
    }

    // 읽어온 클립을 모델 애니메이터의 클립 벡터에 추가
    clips.push_back(clip);

    delete reader; // BinaryReader 객체 해제
}

필요한 데이터를 Clip으로 가져오는 함수이다.

 

각 데이터를 가져올때 실패한다면, 경로상에 FBX파일이 존재하는지 확인하고, 다시 가져와서 Clip파일을 생성할 수 있도록 해준다.

void ModelAnimator::CreateTexture()
{
    // 애니메이션 클립의 개수를 얻습니다.
    UINT clipCount = clips.size();

    // clipTransforms 및 nodeTransforms 배열에 메모리를 할당합니다.
    clipTransforms = new ClipTransform[clipCount];
    nodeTransforms = new ClipTransform[clipCount];

    // 각 애니메이션 클립에 대해 clipTransforms 및 nodeTransforms를 생성합니다.
    FOR(clipCount)
        CreateClipTransform(i);

    // 텍스처에 대한 설명을 설정합니다.
    D3D11_TEXTURE2D_DESC desc = {};
    desc.Width = MAX_BONE * 4;
    desc.Height = MAX_FRAME;
    desc.MipLevels = 1;
    desc.ArraySize = clipCount;
    desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
    desc.SampleDesc.Count = 1;
    desc.Usage = D3D11_USAGE_IMMUTABLE;
    desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;

    UINT pitchSize = MAX_BONE * sizeof(Matrix);
    UINT pageSize = pitchSize * MAX_FRAME;

    // 가상 메모리를 할당하여 텍스처 데이터를 저장할 공간을 만듭니다.
    void* p = VirtualAlloc(nullptr, pageSize * clipCount,
        MEM_RESERVE, PAGE_READWRITE);

    // 각 클립에 대해 텍스처 데이터를 생성합니다.
    FOR(clipCount)
    {
        UINT start = i * pageSize;

        for (UINT y = 0; y < MAX_FRAME; y++)
        {
            void* temp = (BYTE*)p + pitchSize * y + start;

            // 텍스처 데이터를 메모리에 복사합니다.
            VirtualAlloc(temp, pitchSize, MEM_COMMIT, PAGE_READWRITE);
            memcpy(temp, clipTransforms[i].transform[y], pitchSize);
        }
    }

    // D3D11_SUBRESOURCE_DATA 배열을 생성하여 텍스처 데이터를 담습니다.
    D3D11_SUBRESOURCE_DATA* subResource = new D3D11_SUBRESOURCE_DATA[clipCount];

    FOR(clipCount)
    {
        void* temp = (BYTE*)p + i * pageSize;

        // D3D11_SUBRESOURCE_DATA에 텍스처 데이터 및 크기 정보를 설정합니다.
        subResource[i].pSysMem = temp;
        subResource[i].SysMemPitch = pitchSize;
        subResource[i].SysMemSlicePitch = pageSize;
    }

    // 텍스처를 생성합니다.
    DEVICE->CreateTexture2D(&desc, subResource, &texture);

    // 할당된 자원을 정리합니다.
    delete[] subResource;
    VirtualFree(p, 0, MEM_RELEASE);

    // 셰이더 리소스 뷰를 생성합니다.
    D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
    srvDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
    srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
    srvDesc.Texture2DArray.MipLevels = 1;
    srvDesc.Texture2DArray.ArraySize = clipCount;

    DEVICE->CreateShaderResourceView(texture, &srvDesc, &srv);
}

 

각 애니메이션 데이터를 2DTexture형태로 재정의하고, 그 텍스쳐를 DeviceContext로 전달해준다.

 

 

728x90