Animator
3D 모델 애니메이션 시스템은 다음과 같은 핵심 원리를 기반으로 합니다:
효율적인 데이터 구조: 메시, 뼈, 머티리얼, 애니메이션 데이터를 효율적으로 관리하는 구조
계층적 뼈 구조: 부모-자식 관계로 연결된 뼈 계층 구조를 통한 자연스러운 움직임 표현
좌표계 변환 처리: T-포즈 기준의 루트 변환을 로컬 변환으로 변환하고 적용하는 명확한 처리 과정
GPU 최적화: 애니메이션 데이터를 GPU 친화적인 텍스처 형태로 변환하여 효율적인 처리 가능
블렌딩 지원: 두 애니메이션 간 부드러운 전환을 위한 블렌딩 구현
전체 데이터 흐름도
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ .mesh파일 │ │ .xml 파일 │ │ .clip 파일 │
│ (메시 & 뼈) │ │ (머티리얼) │ │ (애니메이션) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ ReadModel() │ │ ReadMaterial() │ │ ReadAnimation()│
└────────┬───────┘ └────────┬───────┘ └────────┬───────┘
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 모델 데이터 │ │ 머티리얼 데이터 │ │ 애니메이션 데이터 │
│ (정점, 뼈구조) │ │ (텍스처, 속성) │ │ (키프레임, 변환) │
└────────┬───────┘ └────────┬───────┘ └────────┬───────┘
│ │ │
└──────────────┬──────────────────────────────┘
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│BindCacheInfo() │ │CreateAnimation │
│ (관계 설정) │ │ Transform() │
└────────┬───────┘ └────────┬───────┘
│ │
│ ▼
│ ┌────────────────┐
│ │ CreateTexture()│
│ │(GPU 텍스처 생성) │
│ └────────┬───────┘
│ │
└──────────────┬──────────────┘
│
▼
┌────────────────┐
│ 렌더링 준비 │
│ (버퍼, 셰이더) │
└────────┬───────┘
│
▼
┌──────────────────┐
│ 셰이더에서 처리 │
│(스키닝, 애니메이션 │
└──────────────────┘
메시와 뼈 데이터 로드 과정
데이터 로드 흐름도
┌──────────────────┐
│ .mesh 파일 │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ FileUtils::Open │ ─→ 바이너리 파일 열기
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 뼈 데이터 읽기 │ ─→ 인덱스, 이름, 부모 인덱스, 변환 행렬
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 메시 데이터 읽기 │ ─→ 이름, 뼈 인덱스, 머티리얼 이름
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 정점 데이터 읽기 │ ─→ 위치, UV, 노말, 블렌드 정보
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 인덱스 데이터 읽기 │ ─→ 삼각형 구성 인덱스
└────────┬─────────┘
│
▼
┌───────────────────┐
│CalculateTangents()│ ─→ 노말맵 적용을 위한 탄젠트 계산
└────────┬──────────┘
│
▼
┌──────────────────┐
│ CreateBuffers() │ ─→ GPU 버퍼 생성(VB, IB)
└────────┬─────────┘
│
▼
┌──────────────────┐
│ BindCacheInfo() │ ─→ 메시-머티리얼, 메시-뼈, 뼈-부모 관계 설정
└──────────────────┘
핵심 코드 분석
// 1. 뼈 데이터 로드
const uint32 count = file->Read<uint32>(); // 뼈 개수 읽기
for (uint32 i = 0; i < count; i++) {
shared_ptr<ModelBone> bone = make_shared<ModelBone>();
bone->index = file->Read<int32>(); // 인덱스
bone->name = Utils::ToWString(file->Read<string>()); // 이름
bone->parentIndex = file->Read<int32>(); // 부모 인덱스
bone->transform = file->Read<Matrix>(); // 변환 행렬(T-포즈 기준)
_bones.push_back(bone);
}
// 2. 메시 데이터 로드
const uint32 count = file->Read<uint32>(); // 메시 개수 읽기
for (uint32 i = 0; i < count; i++) {
shared_ptr<ModelMesh> mesh = make_shared<ModelMesh>();
mesh->name = Utils::ToWString(file->Read<string>()); // 메시 이름
mesh->boneIndex = file->Read<int32>(); // 연결된 뼈 인덱스
mesh->materialName = Utils::ToWString(file->Read<string>()); // 머티리얼 이름
// 3. 정점 데이터 읽기
const uint32 count = file->Read<uint32>(); // 정점 개수
vector<VertexTextureNormalTangentBlendData> vertices;
vertices.resize(count);
void* data = vertices.data();
file->Read(&data, sizeof(VertexTextureNormalTangentBlendData) * count);
// 4. 인덱스 데이터 읽기
const uint32 indices_count = file->Read<uint32>(); // 인덱스 개수
vector<uint32> indices;
indices.resize(indices_count);
void* indices_data = indices.data();
file->Read(&indices_data, sizeof(uint32) * indices_count);
mesh->geometry->AddIndices(indices);
// 5. 탄젠트 계산 및 정점 추가
CalculateTangents(vertices, indices);
mesh->geometry->AddVertices(vertices);
// 6. GPU 버퍼 생성
mesh->CreateBuffers();
_meshes.push_back(mesh);
}
// 7. 관계 설정
BindCacheInfo();
BindCacheInfo() 함수는 모델의 여러 구성 요소 간의 관계를 설정하는 중요한 역할을 합니다:
메시와 머티리얼 연결
메시와 뼈 연결
뼈 계층 구조 구성 (부모-자식 관계)
Material 데이터 로드 과정
데이터 로드 흐름도
┌──────────────────┐
│ .xml 파일 │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ XMLDocument 로드 │ ─→ tinyxml2 라이브러리 사용
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 머티리얼 노드 반복 │
└────────┬─────────┘
│
▼
┌──────────────────┐ ┌─────────────────┐
│ 머티리얼 이름 설정 │ ───→ │ Material 객체 생성│
└────────┬─────────┘ └─────────────────┘
│
▼
┌──────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 텍스처 경로 파싱 │ ───→ │ Texture 객체 생성│ ───→│리소스 관리자에 등록│
└────────┬─────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌──────────────────┐ ┌──────────────────┐
│ 머티리얼 속성 파싱 │ ───→ │ 색상 값(RGBA) 설정│
└────────┬─────────┘ └──────────────────┘
│
▼
┌─────────────────┐
│ 셰이더 설정 │
└────────┬────────┘
│
▼
┌────────────────────┐
│Material 컬렉션에 추가│
└────────────────────┘
Material 로드 핵심 요소
XML 파일에서 다음 정보를 로드합니다:
Material 이름: 고유 식별자
텍스처 경로: 디퓨즈, 스페큘러, 노말 맵
머티리얼 속성: 앰비언트, 디퓨즈, 스페큘러, 이미시브 색상
// 1. XML 파일 로드
tinyxml2::XMLDocument* document = new tinyxml2::XMLDocument();
document->LoadFile(Utils::ToString(fullPath).c_str());
// 2. 각 머티리얼 노드 처리
while (materialNode) {
shared_ptr<Material> material = make_shared<Material>();
// 3. 머티리얼 이름 설정
node = materialNode->FirstChildElement();
material->SetName(Utils::ToWString(node->GetText()));
// 4. 디퓨즈 텍스처 로드
node = node->NextSiblingElement();
if (node->GetText()) {
wstring textureStr = Utils::ToWString(node->GetText());
if (textureStr.length() > 0) {
shared_ptr<Texture> diffuseTexture = make_shared<Texture>();
diffuseTexture->CreateTexture((parentPath / textureStr).wstring());
diffuseTexture->SetName(L"Tower_Diffuse");
RESOURCE.AddResource(diffuseTexture->GetName(), diffuseTexture);
material->SetDiffuseMap(diffuseTexture);
}
}
// (스페큘러, 노말 텍스처도 유사하게 처리)
// 5. 머티리얼 속성 설정
MaterialDesc materialDesc;
// 앰비언트 색상
node = node->NextSiblingElement();
Color color;
color.x = node->FloatAttribute("R");
color.y = node->FloatAttribute("G");
color.z = node->FloatAttribute("B");
color.w = node->FloatAttribute("A");
materialDesc.ambient = color;
// (디퓨즈, 스페큘러, 이미시브 색상도 유사하게 처리)
// 6. 머티리얼 완성
material->SetShader(_shader);
material->SetMaterialDesc(materialDesc);
_materials.push_back(material);
// 다음 머티리얼로 이동
materialNode = materialNode->NextSiblingElement();
}
애니메이션 데이터 로드 및 텍스처 변환 과정
애니메이션 처리 흐름도
┌──────────────────────┐
│ .clip 파일 │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ ReadAnimation() │ ─→ 기본 정보 및 키프레임 데이터 로드
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 애니메이션 메타데이터 │ ─→ 이름, 지속시간, 프레임 레이트, 프레임 수
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 키프레임 데이터 │ ─→ 각 뼈마다의, 각 프레임별 변환 정보
└──────────┬───────────┘
│
▼
┌────────────────────────┐
│CreateAnimationTransform│ ─→ 뼈마다, 프레임마다 변환 행렬 계산
└──────────┬─────────────┘
│
▼
┌──────────────────────┐ ┌───────────────────┐
│ T-포즈 행렬 → 역행렬 │──→│ invGlobal 계산 │
└──────────┬───────────┘ └───────────────────┘
│
▼
┌──────────────────────┐
│계층 변환(부모 영향 적용)│ ─→ 뼈 계층 구조 고려
└──────────┬───────────┘
│
▼
┌──────────────────────┐ ┌───────────────────────────────────────┐
│ 최종 변환 행렬 계산 │──→│ invGlobal * tempAnimBoneTransforms[b] │
└──────────┬───────────┘ └───────────────────────────────────────┘
│
▼
┌──────────────────────┐
│ CreateTexture() │ ─→ 텍스처 생성 및 데이터 패킹
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 2D 텍스처 배열 생성 │ ─→ 포맷: DXGI_FORMAT_R32G32B32A32_FLOAT
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 셰이더 리소스 뷰 생성 │ ─→ GPU에서 접근 가능한 형태로 변환
└──────────────────────┘
주요 코드 분석 및 접근 방식
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;
}
이 코드는 애니메이션의 기본 정보와 각 뼈마다의 키프레임 데이터를 로드합니다.
애니메이션 변환 행렬 계산 (CreateAnimationTransform)
이 부분은 Root 기준 Transform을 뼈 계층 구조에 맞게 변환하는 핵심 과정입니다:
for (uint32 f = 0; f < animation->frameCount; f++) {
for (uint32 b = 0; b < GetBoneCount(); b++) {
shared_ptr<ModelBone> bone = GetBoneByIndex(b);
// 1) 애니메이션 로컬 변환 행렬 얻기
Matrix matAnimation;
shared_ptr<ModelKeyframe> frame = animation->GetKeyframe(bone->name);
if (frame != nullptr) {
ModelKeyframeData& data = frame->transforms[f];
Matrix S = Matrix::CreateScale(data.scale);
Matrix R = Matrix::CreateFromQuaternion(data.rotation);
Matrix T = Matrix::CreateTranslation(data.translation);
matAnimation = S * R * T;
} else {
matAnimation = Matrix::Identity;
}
// 2) T-포즈의 글로벌 변환 행렬의 역행렬 계산
Matrix toRootMatrix = bone->transform; // T-포즈에서의 글로벌 변환
Matrix invGlobal = toRootMatrix.Invert(); // 로컬 좌표계로 변환하는 행렬
// 3) 부모 영향 적용
int32 parentIndex = bone->parentIndex;
Matrix matParent = Matrix::Identity;
if (parentIndex >= 0)
matParent = tempAnimBoneTransforms[parentIndex];
// 4) 애니메이션 변환을 글로벌 좌표계로 변환
tempAnimBoneTransforms[b] = matAnimation * matParent;
// 5) 최종 변환 행렬 계산: 로컬→애니메이션→글로벌
_animTransforms[index].transforms[f][b] = invGlobal * tempAnimBoneTransforms[b];
}
}
애니메이션 변환 과정 시각화
┌───────────────┐ ┌───────────────────┐ ┌───────────────┐
│ T-포즈 │ │ 애니메이션 적용 │ │ 최종 포즈 │
│ 정점 위치 │──┬───→ │ (뼈 공간 변환) │───────→│ 정점 위치 │
└───────────────┘ │ └───────────────────┘ └───────────────┘
│ ↑
│ │
┌───────────────┐ │ ┌───────────────────┐
│ T-포즈 뼈 │ │ │ 부모 뼈의 영향 │
│ 변환 행렬 │ ──┘ │ 적용 │
└───────────────┘ └───────────────────┘
핵심 이해 포인트:
T-포즈에서 뼈 변환: 각 뼈는 T-포즈에서의 글로벌 변환 행렬(bone->transform)을 가집니다.
로컬 좌표계로 변환: 이 글로벌 변환의 역행렬(invGlobal)을 사용하여 로컬 좌표계로 변환합니다.
애니메이션 변환 적용: 키프레임 데이터(스케일, 회전, 이동)를 사용하여 애니메이션 변환 행렬을 계산합니다.
부모 영향 적용: 부모 뼈의 변환을 적용하여 계층 구조를 유지합니다.
최종 변환 계산: invGlobal * tempAnimBoneTransforms[b]를 통해 최종 변환 행렬을 계산합니다.
invGlobal: T-포즈 글로벌 → 로컬 변환
tempAnimBoneTransforms[b]: 애니메이션 적용 후 글로벌 변환
애니메이션 데이터를 텍스처로 변환 (CreateTexture)
// 1. 텍스처 설명자 설정
D3D11_TEXTURE2D_DESC desc;
desc.Width = MAX_MODEL_TRANSFORMS * 4; // 각 행렬은 4개의 행으로 분할
desc.Height = MAX_MODEL_KEYFRAMES;
desc.ArraySize = GetAnimationCount(); // 애니메이션 개수만큼 배열 크기
desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // 각 요소는 4개의 float
desc.Usage = D3D11_USAGE_IMMUTABLE;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
// 2. 메모리 할당 및 데이터 패킹
void* mallocPtr = ::malloc(pageSize * GetAnimationCount());
for (uint32 c = 0; c < 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);
}
}
// 3. 서브리소스 데이터 설정
vector<D3D11_SUBRESOURCE_DATA> subResources(GetAnimationCount());
for (uint32 c = 0; c < GetAnimationCount(); c++) {
void* ptr = (BYTE*)mallocPtr + c * pageSize;
subResources[c].pSysMem = ptr;
subResources[c].SysMemPitch = dataSize;
subResources[c].SysMemSlicePitch = pageSize;
}
// 4. 텍스처 생성
DEVICE->CreateTexture2D(&desc, subResources.data(), _texture.GetAddressOf());
// 5. 셰이더 리소스 뷰 생성
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 = GetAnimationCount();
DEVICE->CreateShaderResourceView(_texture.Get(), &srvDesc, _srv.GetAddressOf());
텍스처의 메모리 레이아웃 시각화
┌───────────────────────────────────────────────────────┐
│ 텍스처 배열 │
├────────────────────────────────────────────────────────┤
│ │
│ [애니메이션 0] │
│ ┌────────────────────────────────────────────────┐ │
│ │프레임 0: [bone0_row0][bone0_row1]...[boneN_row3]│ │
│ │프레임 1: [bone0_row0][bone0_row1]...[boneN_row3]│ │
│ │... │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ [애니메이션 1] │
│ ┌────────────────────────────────────────────────┐ │
│ │프레임 0: [bone0_row0][bone0_row1]...[boneN_row3]│ │
│ │프레임 1: [bone0_row0][bone0_row1]...[boneN_row3]│ │
│ │... │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘
셰이더에서의 활용
// 1. 텍스처에서 뼈 변환 행렬 로드
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);
// 2. 두 프레임 사이 보간
matrix result = lerp(curr, next, ratio[0]);
// 3. 두 애니메이션 사이 블렌딩(필요시)
result = lerp(result, nextResult, blendFrames[input.instanceID].blendRatio);
// 4. 가중치 적용
transform += mul(weights[i], result);
// 5. 정점 변환에 적용
output.position = mul(input.position, m);
Animator의 개념
Animator는 3D 모델의 다양한 애니메이션 클립들과 그 사이의 전환(트랜지션)을 관리하는 시스템입니다. 이는 게임 캐릭터나 객체의 여러 동작 상태(달리기, 점프, 공격 등)를 자연스럽게 연결하고 제어할 수 있게 해줍니다.
class Animator : public Component
{
using Super = Component;
public:
Animator();
virtual ~Animator();
virtual void Update() override;
virtual void Start() override;
// 애니메이션 클립 관련 함수들
void AddClip(const string& name, int animIndex, bool isLoop = false);
void SetEntryClip(const string& clipName);
void SetCurrentClip(const string& name) { _currClip = GetClip(name); }
// 트랜지션 관련 함수들
void AddTransition(const string& clipAName, const string& clipBName);
void SetTransitionFlag(shared_ptr<Transition> transition, bool flag);
void SetTransitionHasExit(shared_ptr<Transition> transition, bool flag);
void SetTransitionExitTime(shared_ptr<Transition> transition, float exitTime);
void SetTransitionOffset(shared_ptr<Transition> transition, float offset);
void SetTransitionDuration(shared_ptr<Transition> transition, float duration);
// 조건 관련 함수들
void AddCondition(shared_ptr<Transition> transition, const string& paramName,
Parameter::Type paramType, Condition::CompareType compareType);
void RemoveCondition(shared_ptr<Transition> transition, int index);
// ... 기타 다양한 함수들 ...
shared_ptr<Clip> _entry;
shared_ptr<Clip> _currClip;
shared_ptr<Transition> _currTransition;
map<string, shared_ptr<Clip>> _clips;
vector<shared_ptr<Transition>> _transitions;
vector<Parameter> _parameters;
private:
// ... 기타 데이터 멤버들 ...
};
Animator 컴포넌트는 다음 주요 기능을 제공합니다:
애니메이션 클립 관리: 여러 애니메이션 클립을 등록하고 관리
트랜지션 시스템: 클립 간 자연스러운 전환을 위한 트랜지션 정의
파라미터 기반 제어: 불리언, 정수, 부동소수점 파라미터를 통한 상태 전환 조건 설정
기본 클립 설정: 시작 시 자동으로 재생될 기본 클립 지정
Animator에서 사용하는 구조들
Clip 구조체
struct Clip
{
string name;
bool isLoop = false;
bool isEndFrame = false;
int animIndex;
float speed = 1.0f;
float progressRatio = 0.0f; // 0.0f ~ 1.0f 사이의 진행 비율
ImVec2 pos; // Animator에디터의 노드 위치
shared_ptr<Transition> transition;
vector <shared_ptr<Transition>> transitions;
vector<AnimationEvent> events;
};
name: 클립의 고유 이름
isLoop: 반복 재생 여부
animIndex: 모델의 애니메이션 배열에서의 인덱스
progressRatio: 현재 재생 진행률(0~1)
transitions: 이 클립에서 다른 클립으로의 전환 정보
Transition 구조체
struct Transition
{
weak_ptr<Clip> clipA;
weak_ptr<Clip> clipB;
bool flag = false;
bool hasCondition = false;
bool hasExitTime = false;
float exitTime = 1.0f;
float transitionOffset = 0.0f;
float transitionDuration = 0.3f;
vector<Condition> conditions;
};
clipA / clipB: 시작 클립과 목표 클립
flag: 트랜지션 활성화 여부
hasExitTime: exitTime에 도달해야만 전환이 시작되는지 여부
exitTime: 소스 클립의 얼마만큼 진행 후 전환을 시작할지(0~1)
transitionDuration: 전환에 걸리는 시간(초)
conditions: 이 트랜지션이 발생하기 위한 조건들의 배열
Parameter와 Condition 구조체
struct Parameter
{
enum class Type { Bool, Int, Float };
string name;
Type type;
union Value
{
bool boolValue;
int intValue;
float floatValue;
Value() : boolValue(false) {} // 기본 생성자
} value;
};
struct Condition
{
enum class CompareType { Equals, NotEqual, Greater, Less };
string parameterName;
Parameter::Type parameterType;
CompareType compareType;
union Value
{
bool boolValue;
int intValue;
float floatValue;
Value() : boolValue(false) {}
} value;
};
이 구조들은 애니메이션 전환 조건을 정의합니다:
Parameter: 애니메이터의 상태를 나타내는 변수(불리언, 정수, 부동소수점)
Condition: 파라미터가 특정 값과 특정 관계인지 검사하는 조건(같음, 다름, 큼, 작음)
애니메이션 블렌딩 구조체
struct KeyframeDesc
{
int animIndex;
uint currFrame;
uint nextFrame;
float ratio;
float sumTime;
float speed;
int activeAnimation;
int padding3;
};
struct blendFrameDesc
{
float blendDuration;
float blendRatio;
float blendSumTime;
float padding;
KeyframeDesc curr;
KeyframeDesc next;
};
struct BlendAnimDesc
{
bool animState = false;
int curr_frame = 0;
int next_frame = 0;
int curr_anim_index = -1;
int next_anim_index = -1;
float ratio = 0.0f;
float sumTime = 0.0f;
float blendRatio = 0.0f;
float padding;
};
이 구조체들은 두 애니메이션 사이의 블렌딩(혼합) 정보를 처리합니다:
KeyframeDesc: 현재 프레임, 다음 프레임, 두 프레임 사이의 혼합 비율 등을 포함
blendFrameDesc: 두 애니메이션 클립 사이의 블렌딩 정보를 포함
GUIManager에서 애니메이터 에디팅
GUIManager는 애니메이터 그래프를 시각적으로 편집할 수 있는 인터페이스를 제공합니다. 핵심 기능은 다음과 같습니다:
void GUIManager::RenderNode(NodeData& node)
{
ImGui::PushID(node.name.c_str());
ImVec2 nodePos = ImGui::GetCursorScreenPos();
nodePos.x += node.pos.x;
nodePos.y += node.pos.y;
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 nodeSize(NODE_WIDTH, NODE_HEIGHT);
// 노드의 배경과 테두리
ImU32 nodeColor = node.isEntry ? IM_COL32(100, 200, 100, 255) : IM_COL32(60, 60, 60, 255);
ImU32 nodeBorderColor = (_selectedNode == &node) ? IM_COL32(255, 255, 0, 255) : IM_COL32(200, 200, 200, 255);
// 노드 그리기
drawList->AddRectFilled(nodePos, ImVec2(nodePos.x + nodeSize.x, nodePos.y + nodeSize.y), nodeColor, 4.0f);
drawList->AddRect(nodePos, ImVec2(nodePos.x + nodeSize.x, nodePos.y + nodeSize.y), nodeBorderColor, 4.0f, 0, 2.0f);
// 제목 영역
drawList->AddRectFilled(
nodePos,
ImVec2(nodePos.x + nodeSize.x, nodePos.y + TITLE_HEIGHT),
IM_COL32(40, 40, 40, 255), 4.0f, ImDrawFlags_RoundCornersTop
);
// 노드 제목 텍스트
ImVec2 textPos = ImVec2(nodePos.x + 5, nodePos.y + 2);
drawList->AddText(textPos, IM_COL32(255, 255, 255, 255), node.name.c_str());
// ... 기타 노드 내용 렌더링 ...
ImGui::PopID();
}
트랜지션 설정
void GUIManager::AddCondition(shared_ptr<Transition> transition, const string& paramName, Parameter::Type paramType, Condition::CompareType compareType)
{
Condition condition;
condition.parameterName = paramName;
condition.parameterType = paramType;
condition.compareType = compareType;
switch (paramType)
{
case Parameter::Type::Bool:
condition.value.boolValue = false;
break;
case Parameter::Type::Int:
condition.value.intValue = 0;
break;
case Parameter::Type::Float:
condition.value.floatValue = 0.0f;
break;
}
_selectedAnimator->AddCondition(transition, paramName, paramType, compareType);
}
클립 사이의 연결 그리기
void GUIManager::DrawConnection(ImDrawList* drawList, const ImVec2& start, const ImVec2& end)
{
ImVec2 startPos(start.x + NODE_WIDTH / 2, start.y + NODE_HEIGHT / 2);
ImVec2 endPos(end.x + NODE_WIDTH / 2, end.y + NODE_HEIGHT / 2);
// 방향 벡터 계산
ImVec2 direction = ImVec2(endPos.x - startPos.x, endPos.y - startPos.y);
float length = sqrt(direction.x * direction.x + direction.y * direction.y);
if (length > 0) {
direction.x /= length;
direction.y /= length;
}
// 곡선의 제어점 계산
float controlPointDist = min(100.0f, length * 0.4f);
ImVec2 tanStart = ImVec2(startPos.x + direction.x * controlPointDist, startPos.y + direction.y * controlPointDist);
ImVec2 tanEnd = ImVec2(endPos.x - direction.x * controlPointDist, endPos.y - direction.y * controlPointDist);
// 베지어 곡선 그리기
drawList->AddBezierCubic(
startPos, tanStart, tanEnd, endPos,
IM_COL32(200, 200, 200, 255), 2.0f
);
// 화살표 그리기
float angle = atan2(endPos.y - tanEnd.y, endPos.x - tanEnd.x);
ImVec2 arrowPos = ImVec2(
endPos.x - 10 * cos(angle),
endPos.y - 10 * sin(angle)
);
DrawArrowHead(drawList, arrowPos, angle, IM_COL32(200, 200, 200, 255));
}
Animator의 동작 원리와 셰이더 연동
Animator::Update 함수
void Animator::Update()
{
if (_model.expired())
{
_model = GetGameObject()->GetComponent<MeshRenderer>()->GetModel();
if (_model.expired()) // model을 가져오지 못한 경우
return;
}
shared_ptr<Clip> currClip = _currClip;
shared_ptr<Transition> currTransition = currClip->transition;// animator->_currTransition;
int currIndex = currClip->animIndex;
int nextIndex = currTransition != nullptr ? currTransition->clipB.lock()->animIndex : INT_MAX;
_blendAnimDesc.SetAnimIndex(currIndex, nextIndex);
_blendAnimDesc.curr.speed = currClip->speed;
// 현재 애니메이션 업데이트
shared_ptr<ModelAnimation> current = _model.lock()->GetAnimationByIndex(_blendAnimDesc.curr.animIndex);
if (current)
{
// 현재 프레임의 시간 계산 (프레임 수를 프레임 레이트로 나누어 초 단위로 변환)
float clipLength = (current->frameCount - 1) / current->frameRate;
float currentTime = currClip->progressRatio * clipLength;
float currentRatio = currClip->progressRatio;
float frameStep = 1.0f / current->frameCount; // 한 프레임당 증가량
// 이벤트 체크
for (auto& event : currClip->events)
{
// 현재 프레임 구간에 이벤트가 있는지 확인
if (!event.isFuctionCalled && event.time > (currentRatio - frameStep) && event.time <= currentRatio)
{
InvokeAnimationEvent(event.function);
event.isFuctionCalled = true;
}
}
currClip->progressRatio = static_cast<float>(_blendAnimDesc.curr.currFrame) / (current->frameCount - 1);
// exitTime 도달 여부 체크
if (currClip->transition && currClip->transition->hasExitTime)
{
if (currClip->progressRatio >= currClip->transition->exitTime)
{
currClip->isEndFrame = true;
}
}
float timePerFrame = 1 / (current->frameRate * _blendAnimDesc.curr.speed);
_blendAnimDesc.curr.sumTime += TIME.GetDeltaTime();
// 한 프레임이 끝났는지 체크
if (_blendAnimDesc.curr.sumTime >= timePerFrame)
{
_blendAnimDesc.curr.sumTime = 0.f;
// 마지막 프레임 체크
if (_blendAnimDesc.curr.currFrame >= current->frameCount - 1)
{
currClip->isEndFrame = true;
for (auto& event : currClip->events)
{
if (event.isFuctionCalled)
event.isFuctionCalled = false;
}
if (currClip->isLoop)
{
_blendAnimDesc.curr.currFrame = 0;
_blendAnimDesc.curr.nextFrame = 1;
}
else
{
_blendAnimDesc.curr.currFrame = current->frameCount - 1;
_blendAnimDesc.curr.nextFrame = current->frameCount - 1;
}
}
else
{
_blendAnimDesc.curr.currFrame++;
_blendAnimDesc.curr.nextFrame = min(_blendAnimDesc.curr.currFrame + 1, current->frameCount - 1);
}
}
_blendAnimDesc.curr.ratio = (_blendAnimDesc.curr.sumTime / timePerFrame);
}
// 트랜지션 처리
if (currTransition != nullptr)
{
// Case 1: Has Exit Time O + Condition O
if (currTransition->hasExitTime && currTransition->hasCondition)
{
if (currClip->isEndFrame && currTransition->flag)
{
HandleTransitionBlend(currTransition);
}
}
// Case 2: Has Exit Time O + Condition X
else if (currTransition->hasExitTime && !currTransition->hasCondition)
{
if (currClip->isEndFrame)
{
HandleTransitionBlend(currTransition);
}
}
// Case 3: Has Exit Time X + Condition O
else if (!currTransition->hasExitTime && currTransition->hasCondition)
{
if (currTransition->flag)
{
HandleTransitionBlend(currTransition);
}
}
// Case 4: Has Exit Time X + Condition X는 의미 없으므로 구현하지 않음
}
_blendAnimDesc.curr.activeAnimation = 1;
_blendAnimDesc.next.activeAnimation = 1;
UpdateBoneTransforms();
}
이 함수는 다음을 수행합니다:
트랜지션 중이면 블렌딩 처리
아니면 현재 클립을 재생하고 진행률 업데이트
조건을 체크하여 트랜지션 플래그 설정
애니메이션 이벤트가 있으면 처리
진행 중인 트랜지션이 있으면 해당 트랜지션으로 전환
트랜지션 블렌딩 처리
void Animator::HandleTransitionBlend(shared_ptr<Transition>& transition)
{
// transitionOffset 적용: 다음 애니메이션의 시작 시점 조절
if (auto model = _model.lock())
{
if (_blendAnimDesc.blendSumTime == 0.0f) // 블렌딩 시작 시
{
shared_ptr<ModelAnimation> next = model->GetAnimationByIndex(_blendAnimDesc.next.animIndex);
if (next)
{
// Offset 위치로 다음 애니메이션 시작 프레임 설정
float offsetFrame = next->frameCount * transition->transitionOffset;
_blendAnimDesc.next.currFrame = static_cast<int>(offsetFrame);
_blendAnimDesc.next.nextFrame = (_blendAnimDesc.next.currFrame + 1) % next->frameCount;
_blendAnimDesc.next.sumTime = 0.f;
}
}
_blendAnimDesc.blendSumTime += TIME.GetDeltaTime();
_blendAnimDesc.blendRatio = _blendAnimDesc.blendSumTime / transition->transitionDuration;
if (_blendAnimDesc.blendRatio > 1.0f)
{
animationSumTime = 0.0f;
_blendAnimDesc.ClearNextAnim(transition->clipB.lock()->animIndex);
// 현재 클립의 isEndFrame 초기화
if (auto currClip = _currClip)
{
currClip->isEndFrame = false;
for (auto& event : currClip->events)
{
if (event.isFuctionCalled)
event.isFuctionCalled = false;
}
}
// 다음 클립의 isEndFrame도 초기화
if (auto nextClip = GetClip(transition->clipB.lock()->name))
nextClip->isEndFrame = false;
SetCurrentClip(transition->clipB.lock()->name);
SetCurrentTransition();
}
else
{
// 다음 애니메이션 업데이트
shared_ptr<ModelAnimation> next = model->GetAnimationByIndex(_blendAnimDesc.next.animIndex);
if (next)
{
// 다음 클립의 진행률 업데이트
if (auto nextClip = transition->clipB.lock())
{
nextClip->progressRatio = static_cast<float>(_blendAnimDesc.next.currFrame) / (next->frameCount - 1);
}
_blendAnimDesc.next.sumTime += TIME.GetDeltaTime();
float timePerFrame = 1 / (next->frameRate * _blendAnimDesc.next.speed);
if (_blendAnimDesc.next.ratio >= 1.0f)
{
_blendAnimDesc.next.sumTime = 0.f;
_blendAnimDesc.next.currFrame = (_blendAnimDesc.next.currFrame + 1) % next->frameCount;
_blendAnimDesc.next.nextFrame = (_blendAnimDesc.next.currFrame + 1) % next->frameCount;
}
_blendAnimDesc.next.ratio = (_blendAnimDesc.next.sumTime / timePerFrame);
}
}
}
}
이 함수는 두 애니메이션 클립 간의 블렌딩을 처리합니다:
두 클립의 현재 상태(프레임, 진행률 등) 계산
블렌딩 비율 계산(시간 기반)
블렌딩 정보를 셰이더에 전달하기 위한 구조체 설정
블렌딩이 완료되면 목표 클립으로 완전히 전환
조건 확인 및 트랜지션 플래그 설정
void Animator::CheckConditionsAndSetFlag()
{
for (shared_ptr<Transition> transition : _transitions)
{
bool prevFlag = transition->flag;
bool isAllConditionSatisfy = true;
for (const Condition& condition : transition->conditions)
{
bool conditionSatisfied = false;
// 파라미터 값 가져오기
switch (condition.parameterType)
{
case Parameter::Type::Bool:
{
bool paramValue = GetBool(condition.parameterName);
bool conditionValue = condition.value.boolValue;
switch (condition.compareType)
{
case Condition::CompareType::Equals:
conditionSatisfied = (paramValue == conditionValue);
break;
case Condition::CompareType::NotEqual:
conditionSatisfied = (paramValue != conditionValue);
break;
}
break;
}
case Parameter::Type::Int:
{
int paramValue = GetInt(condition.parameterName);
int conditionValue = condition.value.intValue;
switch (condition.compareType)
{
case Condition::CompareType::Equals:
conditionSatisfied = (paramValue == conditionValue);
break;
case Condition::CompareType::NotEqual:
conditionSatisfied = (paramValue != conditionValue);
break;
case Condition::CompareType::Greater:
conditionSatisfied = (paramValue > conditionValue);
break;
case Condition::CompareType::Less:
conditionSatisfied = (paramValue < conditionValue);
break;
}
break;
}
case Parameter::Type::Float:
{
// ... 부동소수점 조건 체크 ...
}
}
// 하나의 조건이라도 만족하지 않으면 전체 조건은 실패
if (!conditionSatisfied)
{
isAllConditionSatisfy = false;
break;
}
}
if (isAllConditionSatisfy)
{
transition->flag = true;
}
else
transition->flag = false;
SetClipCurrentTransition(transition->clipA.lock());
if (prevFlag != transition->flag)
{
// XML에 Animator 설정 저장
if (auto clipA = transition->clipA.lock())
if (auto clipB = transition->clipB.lock())
SCENE.UpdateAnimatorTransitionFlagInXML(
SCENE.GetActiveScene()->GetSceneName(),
GetGameObject()->GetName(),
clipA->name, clipB->name,
transition->flag, transition->hasCondition);
}
}
}
이 함수는 모든 트랜지션에 대해:
각 조건을 파라미터 값과 비교하여 검사
exitTime 조건이 있으면 현재 클립의 진행률이 충분한지 체크
모든 조건이 만족되면 트랜지션 플래그를 활성화
셰이더에서의 애니메이션 블렌딩 처리
matrix GetAnimationMatrix(VS_INPUT 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] = blendFrames[input.instanceID].curr.animIndex;
currFrame[0] = blendFrames[input.instanceID].curr.currFrame;
nextFrame[0] = blendFrames[input.instanceID].curr.nextFrame;
ratio[0] = blendFrames[input.instanceID].curr.ratio;
animIndex[1] = blendFrames[input.instanceID].next.animIndex;
currFrame[1] = blendFrames[input.instanceID].next.currFrame;
nextFrame[1] = blendFrames[input.instanceID].next.nextFrame;
ratio[1] = blendFrames[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, blendFrames[input.instanceID].blendRatio);
}
// 가중치 적용
transform += mul(weights[i], result);
}
return transform;
}
이 셰이더 함수는:
각 정점에 영향을 주는 뼈 인덱스와 가중치를 가져옴
현재 애니메이션과 전환 중인 애니메이션(있는 경우)의 프레임 정보를 로드
TransformMap 텍스처에서 각 뼈의 변환 행렬을 가져옴
프레임 사이의 보간을 통해 부드러운 애니메이션 제공
두 애니메이션 사이의 블렌딩(필요한 경우)
각 뼈의 가중치를 적용하여 최종 변환 행렬 계산
애니메이터 시스템 시각화
┌───────────────────┐
│ 애니메이션 클립 │
│ (달리기, 점프 등) │
└─────────┬─────────┘
│
▼
┌───────────────────┐ ┌─────────────────┐
│ 파라미터 │─────────▶│ 조건 │
│ (속도, 점프 등) │ │ (속도 > 5 등) │
└───────────────────┘ └─────┬───────────┘
│
▼
┌───────────────────┐ ┌─────────────────┐
│ 현재 애니메이션 │─────────▶│ 트랜지션 │
│ (달리기) │ │ (달리기→점프) │
└───────────────────┘ └─────┬───────────┘
│
▼
┌───────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 애니메이션 블렌딩 │─────────▶ │ 블렌딩 데이터 │─────────▶ │ 셰이더 처리 │
│ (달리기+점프 혼합) │ │ (비율, 프레임) │ │ (정점 변환 적용) │
└───────────────────┘ └─────────────────┘ └─────────────────┘
애니메이션 이벤트
애니메이션 이벤트 실행 흐름
등록 단계:
REGISTER_MONOBEHAVIOR_METHOD 매크로를 통해 함수 등록
클래스 이름과 함수 이름으로 구성된 키로 MethodRegistry에 저장
이벤트 설정 단계:
GUIManager를 통해 애니메이션 타임라인에 이벤트 추가
애니메이션 클립의 특정 시간(0-1 사이의 진행률)에 이벤트 연결
게임오브젝트의 MonoBehaviour에서 사용 가능한 함수 목록에서 선택
런타임 실행 단계:
Animator::Update에서 애니메이션 진행률 추적
진행률이 이벤트 시간에 도달하면 InvokeAnimationEvent 호출
MethodRegistry에서 함수 포인터를 가져와 실행
이벤트 플래그를 설정하여 중복 호출 방지
리셋 단계:
애니메이션 루프가 완료되면 모든 이벤트 플래그 리셋
다음 루프에서 이벤트가 다시 발생할 수 있도록 함
이벤트 관련 구조체
struct AnimationEvent
{
float time; // 이벤트 발생 시간
AvailableFunction function; // 호출할 함수 이름
bool isFuctionCalled = false;
// (선택) std::string paramStr; 등 파라미터도 추가 가능
};
struct AvailableFunction
{
MonoBehaviour* script;
string functionKey;
};
time: 애니메이션 진행률(0~1 사이)에 해당하는 이벤트 발생 시간
function: 호출할 함수 정보(스크립트 객체와 함수 키)
isFuctionCalled: 이벤트가 이미 호출되었는지 추적하는 플래그
MethodRegistry 시스템
class MethodRegistry
{
protected:
MethodRegistry() {}
virtual ~MethodRegistry() {}
public:
using MethodType = std::function<void(MonoBehaviour*)>;
MethodRegistry(const MethodRegistry&) = delete;
static MethodRegistry& GetInstance()
{
static MethodRegistry instance;
return instance;
}
void registerMethod(const std::string& key, MethodType method) {
_methods[key] = method;
}
MethodType getMethod(const std::string& key) {
auto it = _methods.find(key);
if (it != _methods.end()) {
return it->second;
}
return nullptr;
}
const std::unordered_map<std::string, MethodType>& GetAllMethods() const {
return _methods;
}
private:
std::unordered_map<std::string, MethodType> _methods;
};
MethodRegistry는 함수 참조를 문자열 키로 관리하는 전역 레지스트리입니다. 이것은 런타임에 함수를 동적으로 찾고 호출할 수 있게 해줍니다.
REGISTER_MONOBEHAVIOR_METHOD 매크로
#define REGISTER_MONOBEHAVIOR_METHOD(ClassType, MethodName) \
static bool s_register_##ClassType##_##MethodName = [](){ \
std::string className = typeid(ClassType).name(); \
std::string key = className + "::" + #MethodName; \
MR.registerMethod(key, \
[](MonoBehaviour* obj){ \
auto derived = static_cast<ClassType*>(obj); \
derived->MethodName(); \
} \
); \
return true; \
}();
이 매크로는 MonoBehavior 파생 클래스의 메서드를 MethodRegistry에 등록하는 과정을 간소화합니다:
클래스와 메서드 이름으로 고유한 키를 생성
함수 포인터를 생성하여 이 키에 등록
정적 변수를 통해 프로그램 시작 시 자동으로 등록되도록 함
이벤트 함수 탐색 및 실행 메커니즘
vector<AvailableFunction> Animator::GetAvailableFunctions()
{
vector<AvailableFunction> availableFunctions;
auto owner = GetGameObject();
if (!owner)
return availableFunctions;
const auto& registeredMethods = MR.GetAllMethods();
for (auto& comp : owner->GetComponents())
{
MonoBehaviour* mb = dynamic_cast<MonoBehaviour*>(comp.get());
if (!mb)
continue;
string className = typeid(*mb).name();
for (const auto& [key, method] : registeredMethods)
{
if (key.find(className + "::") == 0)
{
availableFunctions.push_back({ mb, key });
}
}
}
return availableFunctions;
}
이 함수는 애니메이터가 부착된 게임 오브젝트에서 사용 가능한 모든 이벤트 함수 목록을 생성합니다:
게임 오브젝트에 부착된 모든 컴포넌트를 검색
MonoBehaviour 파생 컴포넌트만 필터링
각 컴포넌트의 클래스 이름을 가져옴
등록된 모든 메서드 중 해당 클래스에 속하는 메서드만 필터링하여 목록에 추가
함수 실행: InvokeAnimationEvent
void Animator::InvokeAnimationEvent(const AvailableFunction& function)
{
if (auto method = MR.getMethod(function.functionKey))
{
method(function.script);
}
}
이 함수는 등록된 이벤트 함수를 실행합니다:
MethodRegistry에서 함수 키에 해당하는 함수 포인터를 가져옴
함수가 존재하면 MonoBehaviour 객체를 인자로 전달하여 실행
애니메이션 재생 중 이벤트 처리
// 이벤트 체크
for (auto& event : currClip->events)
{
// 현재 진행률이 이벤트에 있는지 확인
if (!event.isFuctionCalled && event.time > (currentRatio - frameStep) && event.time <= currentRatio)
{
InvokeAnimationEvent(event.function);
event.isFuctionCalled = true;
}
}
이 코드는 다음 작업을 수행합니다:
현재 재생 중인 클립의 모든 이벤트를 순회
현재 애니메이션 진행률이 이벤트 시간에 도달했는지 확인
아직 호출되지 않은 이벤트라면 InvokeAnimationEvent를 통해 함수 호출
호출 후 isFuctionCalled 플래그를 true로 설정하여 중복 호출 방지
애니메이션 루프 시 이벤트 리셋
if (_blendAnimDesc.curr.currFrame >= current->frameCount - 1)
{
currClip->isEndFrame = true;
for (auto& event : currClip->events)
{
if (event.isFuctionCalled)
event.isFuctionCalled = false;
}
if (currClip->isLoop)
{
_blendAnimDesc.curr.currFrame = 0;
_blendAnimDesc.curr.nextFrame = 1;
}
// ...
}
이 코드는 애니메이션이 마지막 프레임에 도달했을 때:
클립의 isEndFrame 플래그를 true로 설정
모든 이벤트의 isFuctionCalled 플래그를 false로 리셋하여 다음 루프에서 다시 호출될 수 있도록 함
반복 재생을 위해 프레임 인덱스를 초기화(isLoop가 true인 경우)
Last updated