Animator

3D 모델 애니메이션 시스템은 다음과 같은 핵심 원리를 기반으로 합니다:

  1. 효율적인 데이터 구조: 메시, 뼈, 머티리얼, 애니메이션 데이터를 효율적으로 관리하는 구조

  2. 계층적 뼈 구조: 부모-자식 관계로 연결된 뼈 계층 구조를 통한 자연스러운 움직임 표현

  3. 좌표계 변환 처리: T-포즈 기준의 루트 변환을 로컬 변환으로 변환하고 적용하는 명확한 처리 과정

  4. GPU 최적화: 애니메이션 데이터를 GPU 친화적인 텍스처 형태로 변환하여 효율적인 처리 가능

  5. 블렌딩 지원: 두 애니메이션 간 부드러운 전환을 위한 블렌딩 구현

전체 데이터 흐름도

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   .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() 함수는 모델의 여러 구성 요소 간의 관계를 설정하는 중요한 역할을 합니다:

  1. 메시와 머티리얼 연결

  2. 메시와 뼈 연결

  3. 뼈 계층 구조 구성 (부모-자식 관계)

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-포즈 뼈     │   │    │  부모 뼈의 영향     │
│  변환 행렬     │ ──┘    │     적용           │
└───────────────┘        └───────────────────┘

핵심 이해 포인트:

  1. T-포즈에서 뼈 변환: 각 뼈는 T-포즈에서의 글로벌 변환 행렬(bone->transform)을 가집니다.

  2. 로컬 좌표계로 변환: 이 글로벌 변환의 역행렬(invGlobal)을 사용하여 로컬 좌표계로 변환합니다.

  3. 애니메이션 변환 적용: 키프레임 데이터(스케일, 회전, 이동)를 사용하여 애니메이션 변환 행렬을 계산합니다.

  4. 부모 영향 적용: 부모 뼈의 변환을 적용하여 계층 구조를 유지합니다.

  5. 최종 변환 계산: invGlobal * tempAnimBoneTransforms[b]를 통해 최종 변환 행렬을 계산합니다.

    1. invGlobal: T-포즈 글로벌 → 로컬 변환

    2. 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 컴포넌트는 다음 주요 기능을 제공합니다:

  1. 애니메이션 클립 관리: 여러 애니메이션 클립을 등록하고 관리

  2. 트랜지션 시스템: 클립 간 자연스러운 전환을 위한 트랜지션 정의

  3. 파라미터 기반 제어: 불리언, 정수, 부동소수점 파라미터를 통한 상태 전환 조건 설정

  4. 기본 클립 설정: 시작 시 자동으로 재생될 기본 클립 지정

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;
};
  1. name: 클립의 고유 이름

  2. isLoop: 반복 재생 여부

  3. animIndex: 모델의 애니메이션 배열에서의 인덱스

  4. progressRatio: 현재 재생 진행률(0~1)

  5. 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;
};
  1. clipA / clipB: 시작 클립과 목표 클립

  2. flag: 트랜지션 활성화 여부

  3. hasExitTime: exitTime에 도달해야만 전환이 시작되는지 여부

  4. exitTime: 소스 클립의 얼마만큼 진행 후 전환을 시작할지(0~1)

  5. transitionDuration: 전환에 걸리는 시간(초)

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

이 구조들은 애니메이션 전환 조건을 정의합니다:

  1. Parameter: 애니메이터의 상태를 나타내는 변수(불리언, 정수, 부동소수점)

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

이 구조체들은 두 애니메이션 사이의 블렌딩(혼합) 정보를 처리합니다:

  1. KeyframeDesc: 현재 프레임, 다음 프레임, 두 프레임 사이의 혼합 비율 등을 포함

  2. 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();
	
}

이 함수는 다음을 수행합니다:

  1. 트랜지션 중이면 블렌딩 처리

  2. 아니면 현재 클립을 재생하고 진행률 업데이트

  3. 조건을 체크하여 트랜지션 플래그 설정

  4. 애니메이션 이벤트가 있으면 처리

  5. 진행 중인 트랜지션이 있으면 해당 트랜지션으로 전환

  • 트랜지션 블렌딩 처리

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

이 함수는 두 애니메이션 클립 간의 블렌딩을 처리합니다:

  1. 두 클립의 현재 상태(프레임, 진행률 등) 계산

  2. 블렌딩 비율 계산(시간 기반)

  3. 블렌딩 정보를 셰이더에 전달하기 위한 구조체 설정

  4. 블렌딩이 완료되면 목표 클립으로 완전히 전환

  • 조건 확인 및 트랜지션 플래그 설정

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

이 함수는 모든 트랜지션에 대해:

  1. 각 조건을 파라미터 값과 비교하여 검사

  2. exitTime 조건이 있으면 현재 클립의 진행률이 충분한지 체크

  3. 모든 조건이 만족되면 트랜지션 플래그를 활성화

  • 셰이더에서의 애니메이션 블렌딩 처리

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

이 셰이더 함수는:

  1. 각 정점에 영향을 주는 뼈 인덱스와 가중치를 가져옴

  2. 현재 애니메이션과 전환 중인 애니메이션(있는 경우)의 프레임 정보를 로드

  3. TransformMap 텍스처에서 각 뼈의 변환 행렬을 가져옴

  4. 프레임 사이의 보간을 통해 부드러운 애니메이션 제공

  5. 두 애니메이션 사이의 블렌딩(필요한 경우)

  6. 각 뼈의 가중치를 적용하여 최종 변환 행렬 계산

  • 애니메이터 시스템 시각화

┌───────────────────┐
│    애니메이션 클립  │
│  (달리기, 점프 등)  │
└─────────┬─────────┘


┌───────────────────┐           ┌─────────────────┐
│     파라미터       │─────────▶│     조건         │
│  (속도, 점프 등)   │           │  (속도 > 5 등)   │
└───────────────────┘           └─────┬───────────┘


┌───────────────────┐           ┌─────────────────┐
│   현재 애니메이션   │─────────▶│    트랜지션      │
│      (달리기)      │           │  (달리기→점프)   │
└───────────────────┘           └─────┬───────────┘


┌───────────────────┐            ┌─────────────────┐           ┌─────────────────┐
│  애니메이션 블렌딩  │─────────▶  │   블렌딩 데이터  │─────────▶ │  셰이더 처리     │
│  (달리기+점프 혼합) │            │  (비율, 프레임)  │            │ (정점 변환 적용) │
└───────────────────┘            └─────────────────┘           └─────────────────┘

애니메이션 이벤트

애니메이션 이벤트 실행 흐름

  1. 등록 단계:

    1. REGISTER_MONOBEHAVIOR_METHOD 매크로를 통해 함수 등록

    2. 클래스 이름과 함수 이름으로 구성된 키로 MethodRegistry에 저장

  2. 이벤트 설정 단계:

    1. GUIManager를 통해 애니메이션 타임라인에 이벤트 추가

    2. 애니메이션 클립의 특정 시간(0-1 사이의 진행률)에 이벤트 연결

    3. 게임오브젝트의 MonoBehaviour에서 사용 가능한 함수 목록에서 선택

  3. 런타임 실행 단계:

    1. Animator::Update에서 애니메이션 진행률 추적

    2. 진행률이 이벤트 시간에 도달하면 InvokeAnimationEvent 호출

    3. MethodRegistry에서 함수 포인터를 가져와 실행

    4. 이벤트 플래그를 설정하여 중복 호출 방지

  4. 리셋 단계:

    1. 애니메이션 루프가 완료되면 모든 이벤트 플래그 리셋

    2. 다음 루프에서 이벤트가 다시 발생할 수 있도록 함

이벤트 관련 구조체

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에 등록하는 과정을 간소화합니다:

  1. 클래스와 메서드 이름으로 고유한 키를 생성

  2. 함수 포인터를 생성하여 이 키에 등록

  3. 정적 변수를 통해 프로그램 시작 시 자동으로 등록되도록 함

이벤트 함수 탐색 및 실행 메커니즘

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

이 함수는 애니메이터가 부착된 게임 오브젝트에서 사용 가능한 모든 이벤트 함수 목록을 생성합니다:

  1. 게임 오브젝트에 부착된 모든 컴포넌트를 검색

  2. MonoBehaviour 파생 컴포넌트만 필터링

  3. 각 컴포넌트의 클래스 이름을 가져옴

  4. 등록된 모든 메서드 중 해당 클래스에 속하는 메서드만 필터링하여 목록에 추가

함수 실행: InvokeAnimationEvent

void Animator::InvokeAnimationEvent(const AvailableFunction& function)
{
	if (auto method = MR.getMethod(function.functionKey))
	{
		method(function.script);
	}
}

이 함수는 등록된 이벤트 함수를 실행합니다:

  1. MethodRegistry에서 함수 키에 해당하는 함수 포인터를 가져옴

  2. 함수가 존재하면 MonoBehaviour 객체를 인자로 전달하여 실행

애니메이션 재생 중 이벤트 처리

// 이벤트 체크
for (auto& event : currClip->events)
{
    // 현재 진행률이 이벤트에 있는지 확인
    if (!event.isFuctionCalled && event.time > (currentRatio - frameStep) && event.time <= currentRatio)
    {
        InvokeAnimationEvent(event.function);
        event.isFuctionCalled = true;
    }
}

이 코드는 다음 작업을 수행합니다:

  1. 현재 재생 중인 클립의 모든 이벤트를 순회

  2. 현재 애니메이션 진행률이 이벤트 시간에 도달했는지 확인

  3. 아직 호출되지 않은 이벤트라면 InvokeAnimationEvent를 통해 함수 호출

  4. 호출 후 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;
    }
    // ...
}

이 코드는 애니메이션이 마지막 프레임에 도달했을 때:

  1. 클립의 isEndFrame 플래그를 true로 설정

  2. 모든 이벤트의 isFuctionCalled 플래그를 false로 리셋하여 다음 루프에서 다시 호출될 수 있도록 함

  3. 반복 재생을 위해 프레임 인덱스를 초기화(isLoop가 true인 경우)

Last updated