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);
}
}
}
이 함수는 다음과 같은 중요한 작업을 수행합니다:
자식 객체가 뼈 객체의 하위로 배치될 때 호출됩니다.
부모가 뼈 객체인 경우:
루트 모델 객체(BoneParentObject)의 활성 뼈 목록에 이 뼈의 인덱스를 추가합니다.
자식이 뼈가 아니라면 부모 뼈의 _hasNonBoneChildren 플래그를 설정합니다.
자식에게 부모 뼈 참조를 설정합니다(SetNonBoneChildrenParent).
활성 뼈 인덱스 관리 흐름
이 시스템의 전체 흐름은 다음과 같습니다:
┌─────────────────────┐
│ 모델 로드 및 초기화 │
│ (뼈 GameObject 생성) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 계층 구조 설정 │
│ (부모-자식 관계) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐ ┌──────────────────────────┐
│ UI에서 객체를 뼈에 │────▶│ HandleBoneObjectParenting│
│ 자식으로 추가 │ │ 호출 │
└──────────┬──────────┘ └──────────┬────────────────┘
│ │
│ ▼
│ ┌───────────────────────────────────────────────┐
│ │ newParent->GetBoneParentObject().lock()-> │
│ │ AddActiveBoneIndex(newParent->GetBoneIndex()) │
│ │ (부모 뼈를 활성 뼈 목록에 추가) │
│ └──────────────┬────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ 애니메이션 업데이트 │────▶│ UpdateBoneTransforms│
│ (Animator::Update) │ │ (활성 뼈만 변환) │
└─────────────────────┘ └─────────────────────┘
최적화 효과 분석
이 접근법은 다음과 같은 중요한 최적화 효과를 제공합니다:
선택적 뼈 업데이트: 모든 뼈가 아닌, 실제로 필요한 뼈만 업데이트합니다.
일반적인 모델에서 100개의 뼈가 있을 때, 실제로 외부 객체가 붙은 뼈는 10-20개 정도일 수 있습니다.
90%의 뼈 업데이트 연산을 절약할 수 있습니다.
자동 활성화: 객체가 뼈에 부착될 때 해당 뼈와 그 상위 계층이 자동으로 활성화됩니다.
수동으로 어떤 뼈를 업데이트할지 관리할 필요가 없습니다.
계층적 관계 유지: 실제 계층 구조는 그대로 유지하면서 업데이트만 최적화합니다.
이를 통해 애니메이션 품질 저하 없이 성능 향상을 얻을 수 있습니다.
Last updated