Add Object to bone

게임 개발에서 스켈레탈 애니메이션은 캐릭터 표현에 필수적이지만, 모든 뼈를 매 프레임 업데이트하는 것은 상당한 연산 비용이 듭니다. 이 프로젝트에서는 필요한 뼈만 업데이트하는 방식을 통해 최적화를 하였습니다.

최적화 접근법: "필요한 뼈만 업데이트"

일반적인 캐릭터 모델에는 수십 또는 수백 개의 뼈가 있지만, 이 중 많은 뼈는 내부 구조용으로만 사용되고 실제로 외부 객체가 부착되지 않습니다. 이 프로젝트에서는 다음 원칙을 적용했습니다:

"UI상에서 선택한 오브젝트, 뼈들 중 뼈가 아닌 GameObject가 자식으로 들어있는 뼈만 Update하도록 하자!"

이 접근법을 구현하기 위해 사용된 시스템을 분석해 보겠습니다.

관련 데이터 구조 (GameObject 클래스)

// GameObject.h의 뼈 관련 필드들
private:
    bool _isBoneObject = false;           // 이 객체가 뼈인지 표시
    bool _hasNonBoneChildren = false;     // 뼈가 아닌 자식이 있는지 여부
    vector<int> _activeBoneIndices;       // 활성화될 뼈 인덱스 목록
    weak_ptr<GameObject> _nonBoneChildrenParent; // 비뼈 객체의 부모 뼈
    weak_ptr<GameObject> _boneParentObject;      // 이 뼈가 속한 부모 객체
    int _boneIndex;                       // 모델의 뼈 배열에서의 인덱스

관련 접근자/설정자 메서드:

void SetBoneObjectFlag(bool flag) { _isBoneObject = flag; }
bool GetBoneObjectFlag() { return _isBoneObject; }
void SetBoneParentObject(shared_ptr<GameObject> parent) { _boneParentObject = parent; }
weak_ptr<GameObject> GetBoneParentObject() { return _boneParentObject; }
void SetHasNoneBoneChildrenFlag(bool flag) { _hasNonBoneChildren = flag; }
bool GetHasNoneBoneChildrenFlag() { return _hasNonBoneChildren; }
void SetBoneIndex(int index) { _boneIndex = index; }
int GetBoneIndex() { return _boneIndex; }
vector<int> GetActiveBoneIndices() { return _activeBoneIndices; }

void AddActiveBoneIndex(int index) { 
    _activeBoneIndices.push_back(index); 
    // 중복 제거를 위한 정렬 및 유니크 처리
    std::sort(_activeBoneIndices.begin(), _activeBoneIndices.end());
    auto last = std::unique(_activeBoneIndices.begin(), _activeBoneIndices.end());
    _activeBoneIndices.erase(last, _activeBoneIndices.end());
}

void RemoveActiveBoneIndex(int index) { 
    _activeBoneIndices.erase(std::remove(_activeBoneIndices.begin(), 
    _activeBoneIndices.end(), index), _activeBoneIndices.end()); 
}
void SetNonBoneChildrenParent(shared_ptr<GameObject> parent) { _nonBoneChildrenParent = parent; }
shared_ptr<GameObject> GetNoneBoneChildrenParent() { return _nonBoneChildrenParent.lock(); }

Animator에서의 뼈 업데이트 로직

Animator::UpdateBoneTransforms() 함수는 성능 최적화의 핵심입니다:

void Animator::UpdateBoneTransforms()
{
    if (auto model = _model.lock())  // weak_ptr를 shared_ptr로 변환
    {
        if (!_currClip)
            return;

        shared_ptr<Transition> currTransition = _currClip->transition;
        bool isBlending = (currTransition != nullptr && _blendAnimDesc.blendSumTime > 0.0f);

        // 핵심: 모든 뼈가 아닌 활성화된 뼈만 순회
        for (int boneIndex : GetGameObject()->GetActiveBoneIndices())
        {
            const auto& bone = model->GetBoneByIndex(boneIndex);
            shared_ptr<GameObject> boneObject = FindBoneObjectByIndex(bone->index);
            if (!boneObject || bone->index < 0)
                continue;

            Matrix finalTransform;
            
            // 애니메이션 블렌딩 또는 단일 애니메이션 처리
            // ... [애니메이션 계산 코드] ...
            
            // 최종 변환 행렬 분해 및 적용
            Vec3 scale, translation;
            Quaternion rotation;
            finalTransform.Decompose(scale, rotation, translation);

            auto boneTransform = boneObject->transform();
            boneTransform->SetLocalPosition(translation);
            boneTransform->SetQTLocaslRotation(rotation);
            boneTransform->SetLocalScale(scale);
        }
    }
}

이 함수의 핵심은 GetGameObject()->GetActiveBoneIndices()에서 반환하는 뼈 인덱스만 업데이트한다는 점입니다. 모든 뼈가 아닌, 활성화된 뼈만 처리함으로써 상당한 성능 이득을 얻을 수 있습니다.

계층 구조 관리와 활성 뼈 등록

GUIManager::HandleBoneObjectParenting 함수는 계층 구조가 변경될 때 활성 뼈를 관리합니다:

void HandleBoneObjectParenting(shared_ptr<GameObject> child, shared_ptr<GameObject> newParent)
{
    if (newParent && newParent->GetBoneObjectFlag())
    {
        // 중요: 뼈 오브젝트에 새로운 자식이 추가될 때 해당 뼈를 활성 뼈 목록에 추가
        newParent->GetBoneParentObject().lock()->AddActiveBoneIndex(newParent->GetBoneIndex());
        _tempBoneIndex = INT_MAX;
        
        // 자식이 뼈가 아닌 경우 특별 처리
        if (!child->GetBoneObjectFlag())
        {
            newParent->SetHasNoneBoneChildrenFlag(true);
            child->SetNonBoneChildrenParent(newParent);
        }
    }
}

이 함수는 다음과 같은 중요한 작업을 수행합니다:

  1. 자식 객체가 뼈 객체의 하위로 배치될 때 호출됩니다.

  2. 부모가 뼈 객체인 경우:

    1. 루트 모델 객체(BoneParentObject)의 활성 뼈 목록에 이 뼈의 인덱스를 추가합니다.

    2. 자식이 뼈가 아니라면 부모 뼈의 _hasNonBoneChildren 플래그를 설정합니다.

    3. 자식에게 부모 뼈 참조를 설정합니다(SetNonBoneChildrenParent).

활성 뼈 인덱스 관리 흐름

이 시스템의 전체 흐름은 다음과 같습니다:

┌─────────────────────┐
│ 모델 로드 및 초기화   │
│ (뼈 GameObject 생성) │
└──────────┬──────────┘


┌─────────────────────┐
│ 계층 구조 설정        │
│ (부모-자식 관계)      │
└──────────┬──────────┘


┌─────────────────────┐      ┌──────────────────────────┐
│ UI에서 객체를 뼈에    │────▶│ HandleBoneObjectParenting│
│ 자식으로 추가         │     │ 호출                      │
└──────────┬──────────┘     └──────────┬────────────────┘
           │                           │
           │                           ▼
           │              ┌───────────────────────────────────────────────┐
           │              │ newParent->GetBoneParentObject().lock()->     │
           │              │ AddActiveBoneIndex(newParent->GetBoneIndex()) │
           │              │ (부모 뼈를 활성 뼈 목록에 추가)                   │
           │              └──────────────┬────────────────────────────────┘
           │                             │
           ▼                            ▼
┌─────────────────────┐      ┌─────────────────────┐
│ 애니메이션 업데이트    │────▶│ UpdateBoneTransforms│
│ (Animator::Update)  │      │ (활성 뼈만 변환)      │
└─────────────────────┘      └─────────────────────┘

최적화 효과 분석

이 접근법은 다음과 같은 중요한 최적화 효과를 제공합니다:

  1. 선택적 뼈 업데이트: 모든 뼈가 아닌, 실제로 필요한 뼈만 업데이트합니다.

    1. 일반적인 모델에서 100개의 뼈가 있을 때, 실제로 외부 객체가 붙은 뼈는 10-20개 정도일 수 있습니다.

    2. 90%의 뼈 업데이트 연산을 절약할 수 있습니다.

  2. 자동 활성화: 객체가 뼈에 부착될 때 해당 뼈와 그 상위 계층이 자동으로 활성화됩니다.

    1. 수동으로 어떤 뼈를 업데이트할지 관리할 필요가 없습니다.

  3. 계층적 관계 유지: 실제 계층 구조는 그대로 유지하면서 업데이트만 최적화합니다.

    1. 이를 통해 애니메이션 품질 저하 없이 성능 향상을 얻을 수 있습니다.

Last updated