애니메이션 읽기
본격적으로 이제 파일로 만든 애니메이션을 긁어서 사용할 수 있게 만드는 작업을 해보자.
Model cpp에 애니메이션 파일을 읽을 수 있게 작성한다.
void Model::ReadAnimation(wstring filename)
{
wstring fullPath = _modelPath + filename + L".clip";
shared_ptr<FileUtils> file = make_shared<FileUtils>();
file->Open(fullPath, FileMode::Read);
shared_ptr<ModelAnimation> animation = make_shared<ModelAnimation>();
animation->name = Utils::ToWString(file->Read<string>());
animation->duration = file->Read<float>();
animation->frameRate = file->Read<float>();
animation->frameCount = file->Read<uint32>();
uint32 keyframesCount = file->Read<uint32>();
for (uint32 i = 0; i < keyframesCount; i++)
{
shared_ptr<ModelKeyframe> keyframe = make_shared<ModelKeyframe>();
keyframe->boneName = Utils::ToWString(file->Read<string>());
uint32 size = file->Read<uint32>();
if (size > 0)
{
keyframe->transforms.resize(size);
void* ptr = &keyframe->transforms[0];
file->Read(&ptr, sizeof(ModelKeyframeData) * size);
}
animation->keyframes[keyframe->boneName] = keyframe;
}
_animations.push_back(animation);
}
읽어오면 이런 모습이다.
애니메이션 동작하기
이제 이걸 그려줘야하는데 기존에는 ModelRenderer에서 이 작업을 해줬는데 기존 작업과 섞을 수 있기 때문에 따로 분리해서 애니메이션만 그려주는 컴포넌트를 만들어보자. 유니티에서도 Animator라는 컴포넌트가 따로 있는 이유이다.
그리고 여기서 이제 애니메이션을 동작할 때 SRT를 매우 주의해야하고 매우 중요하다.
또한 텍스처를 만들때 상수 버퍼로 넘겨주면 참 좋겠지만 그러기엔 데이터 양이 너무 많아서 SRV 를 만들어서 GPU에 텍스처를 넘겨주어야만 한다.
void ModelAnimator::CreateTexture()
{
if (_model->GetAnimationCount() == 0)
return;
_animTransforms.resize(_model->GetAnimationCount());
for (uint32 i = 0; i < _model->GetAnimationCount(); i++)
CreateAnimationTransform(i);
// Creature Texture
{
D3D11_TEXTURE2D_DESC desc;
ZeroMemory(&desc, sizeof(D3D11_TEXTURE2D_DESC));
desc.Width = MAX_MODEL_TRANSFORMS * 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)
{
// 특정 프레임에 해당되는 특정 본의 SRT 구하기
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;
}
// 여기까지 mat는 상위 부모로 가는 변환 행렬
// [ !!!!!!! ]
// 여기서 bone->transform은 모델을 로드하면서 최상위 부모로 변환 하는 변환행렬
Matrix toRootMatrix = bone->transform;
// 그 행렬의 역행렬은 최상위 부모 기준에서 다시 로컬 기준으로 변환하는 행렬
Matrix invGlobal = toRootMatrix.Invert();
int32 parentIndex = bone->parentIndex;
// 이건 모델이 애니메이션이 적용된 프레임의 위치,나타내는 상위까지 가는 SRT
Matrix matParent = Matrix::Identity;
if (parentIndex >= 0)
matParent = tempAnimBoneTransforms[parentIndex];
// 부모가 있다면 현재 자신의 상위 부모로 가는 것 * 최상위 부모까지 곱해서
// 변환행렬을 만듬.
tempAnimBoneTransforms[b] = matAnimation * matParent;
// 결론
// 특정 애니메이션의 프레임에 각 본마다 애니메이션의 위치로 변환하기 위한 SRT는 아래와 같다.
_animTransforms[index].transforms[f][b] = invGlobal * tempAnimBoneTransforms[b];
// 이는 T-pose의 글로벌 좌표가 저장되어 있을 텐데 그 좌표를 inv 해서 T-pose의 로컬로 변환후
// 변환된 값을 애니메이션의 이동된 root의 좌표계로 변환하기 위해서 matAimation * mat Parent를 해주는 것이다.
}
}
}
쉐이더에서 마지막으로 읽기 위한 준비를 하고 애니메이션에 따라 재생해주면 된다.
#include "00. Global.fx"
#include "00. Light.fx"
#define MAX_MODEL_TRANSFORMS 50
struct KeyframeDesc
{
int animIndex;
uint currFrame;
uint nextFrame;
float ratio;
float sumTime;
float speed;
float2 padding;
};
cbuffer KeyframeBuffer
{
KeyframeDesc Keyframes;
};
cbuffer BoneBuffer
{
matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
};
uint BoneIndex;
Texture2DArray TransformMap;
matrix GetAnimationMatrix(VertexTextureNormalTangentBlend input)
{
float indices[4] = { input.blendIndices.x, input.blendIndices.y, input.blendIndices.z, input.blendIndices.w };
float weights[4] = { input.blendWeights.x, input.blendWeights.y, input.blendWeights.z, input.blendWeights.w };
int animIndex = Keyframes.animIndex;
int currFrame = Keyframes.currFrame;
int nextFrame = Keyframes.nextFrame;
float4 c0, c1, c2, c3;
matrix curr = 0;
matrix transform = 0;
for (int i = 0; i < 4; i++)
{
c0 = TransformMap.Load(int4(indices[i] * 4 + 0, currFrame, animIndex, 0));
c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame, animIndex, 0));
c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame, animIndex, 0));
c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame, animIndex, 0));
curr = matrix(c0, c1, c2, c3);
transform += mul(weights[i], curr);
}
return transform;
}
MeshOutput VS(VertexTextureNormalTangentBlend input)
{
MeshOutput output;
matrix m = GetAnimationMatrix(input);
output.position = mul(input.position, m);
output.position = mul(output.position, BoneTransforms[BoneIndex]);
output.position = mul(output.position, W);
output.worldPosition = output.position.xyz;
output.position = mul(output.position, VP);
output.uv = input.uv;
output.normal = mul(input.normal, (float3x3) W);
output.tangent = mul(input.tangent, (float3x3) W);
return output;
}
float4 PS(MeshOutput input) : SV_TARGET
{
//ComputeNormalMapping(input.normal, input.tangent, input.uv);
//float4 color = ComputeLight(input.normal, input.uv, input.worldPosition);
float4 color = DiffuseMap.Sample(LinearSampler, input.uv);
return color;
}
float4 PS_RED(MeshOutput input) : SV_TARGET
{
return float4(1, 0, 0, 1);
}
technique11 T0
{
PASS_VP(P0, VS, PS)
PASS_RS_VP(P1, FillModeWireFrame, VS, PS_RED)
};
애니메이션 트위닝
애니메이션이 다른 애니메이션으로 전환될 때, 뚝 끊어지는게 아닌 자연스럽게 변환하기 위해 트위닝을 추가해주어야한다.
이것을 하기 위해선 두개의 애니메이션을 어떻게 섞을지에 대한 구조체를 먼저 정의해야 한다.
struct TweenFrameDesc
{
float tweenDuration;
float tweenRatio;
float tweenSumTime;
float padding;
KeyframeDesc curr;
KeyframeDesc next;
};
그래서 쉐이더 부분을 다음 애니메이션이 존재한다면 블랜딩 하는 코드를 추가해준다.
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;
}
Model의 애니메이션을 틀어주는 곳은 이렇게 수정해서 TweenData를 밀어준다.
void ModelAnimator::Update()
{
if (_model == nullptr)
return;
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
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);
}
}
걷는것과 멈추는것이 자연스럽게 애니메이션을 섞었다.
'C++ > DirectX 11' 카테고리의 다른 글
[DirectX] GPU - RawBuffer, System Value 분석, TextureBuffer, StructureBuffer (0) | 2024.10.15 |
---|---|
[DirectX] 인스턴싱 - 드로우 콜 (1) | 2024.10.10 |
[DirectX] 애니메이션 - 스키닝, 데이터 (1) | 2024.10.02 |
[DirectX] 모델 - 모델 띄우기, 계층 구조, ImGuI (1) | 2024.09.30 |
[DirectX] 모델 - Assimp, Material 로딩, Mesh 로딩 (0) | 2024.09.27 |