불필요한 작업을 피하기 위해 실제로 필요할 때까지 그 일을 미룬다.

 

장면 그래프는 계층적이다.

매 프레임마다 모든 객체의 월드변환 계산을 해야 한다면 성능에 크게 영향을 준다.

 

위 그림에서 해적, 배, 앵무새가 동시에 움직여(지역 변환이 바뀌어) 3개 객체의 월드 변환을 매번 재계산한다면

 

상위 객체의 지역 변환이 바뀌면, 해당 객체의 하위 객체들의 월드 변환 값을 다시 계산해야 한다.

위 그림에서 예를 들자면 배의 지역 변환(=월드 변환) 값과 해적의 지역 변환 값이 바뀔 때마다 객체들의 월드 변환을 계산한다면 앵무새의 월드 변환은 두 번 계산되게 된다. 

배의 지역변환이 수정될때

ㄴ 수정된 배의 지역변환 X 해적 지역변환 X 앵무새 지역 변환

해적의 지역변환이 수정될 때

ㄴ 수정된 배의 지역변환 X 해적 지역변환 X 앵무새 지역 변환

 

재계산 미루기

DirectX으로 배우는 게임 프로그래밍 23장 메시 스키닝에서 이 주제에 대해 다뤘었음

https://lemonyun.tistory.com/64?category=1020933 

 

23. 캐릭터 애니메이션

23.1 뼈대 좌표계들의 계통구조 캐릭터의 골격은 계통구조로 만들어진다. 예를들면 팔은 상박, 하박 손으로 이루어지고 이들은 부모 자식 관계이기 때문에부모가 회전하면 자식들도 회전한다.

lemonyun.tistory.com

지역 변환 값 변경과 월드 변환 값 업데이트를 분리하려 한다.

 

객체에 플래그를 두어 변경이 필요한 월드 변환 값을 렌더링 직전에 한번만 계산하도록 한다.

 

위 예제에서 해적의 지역 변환 값이 변경되었다면 해적 객체와 앵무새 객체의 더티 플래그를 설정한다.

모든 객체의 지역 변환 값 갱신이 끝나면 바뀐 객체 (더티 플래그가 설정된) 의 월드 변환을 상위 객체 부터 하향식으로 계산한다. 객체의 월드 변환 =  (부모 객체의 월드 변환) X (자신의 지역 변환) 이므로 객체 하나의 월드 변환을 계산하는데 행렬 곱셈 한번이면 된다.

 

객체의 월드 변환을 갱신하면 더티 플래그를 비활성화 하여 월드 변환이 최신 상태임을 알려야 한다.

 

18.4 언제 쓸 것인가?

파생 값이 사용되는 횟수보다 기본 값이 더 자주 변경되어야 한다.

ㄴ 이 패턴은 도중에 기본 값 (지역 변환) 이 바뀌는 바람에 계산해놓은 파생 값 (월드 변환) 이 사용 전에 무효화되는 것 막는다. 이 예제에서 만약 한 프레임에 하나의 객체의 지역 변환만 바뀔 수 있다고 하면 더티 플래그는 의미가 없어진다. (지역 변환이 한 번만 바뀌기 때문에 파생 값이 무효화 될 일이 없어서)

 

DirectX 의 프레임 자원의 동기화를 위해 NumFramesDirty = "프레임 자원 수"로 두고 Update할 때마다 NumFramesDirty-- 을 시켰었다.

 

18.5 주의사항

너무 오래 지연하려면 비용이 든다.

예제 같은 월드 좌표 계산은 한 프레임 안에서도 금방 할 수 있기 때문에 크게 문제가 되지 않는다. 하지만 전체를 처리하는데 상당한 시간이 걸리는 작업이 있다면 결과가 필요할 때 처리를 시작하면 화면 멈춤 현상이 생길 수 있다.

 

상태가 변할 때마다 플래그를 켜야 한다.

이 패턴에서는 캐시(기본 값) 무효화 = 기본 값이 바뀌었을 때 더티 플래그를 켜주는 것

기본 값을 변경하는 모든 코드가 더티 플래그를 같이 설정하도록 주의해야 한다. 어느 한 곳에서라도 놓치면 잡기 어려운 버그가 발생할 수 있다.

 

이전 파생 값을 메모리에 저장해둬야 한다.

메모리보다 시간이 남는다면 더티 플래그 패턴을 쓰지 않고 파생 값이 필요할 때마다 계산하여 사용한뒤 버리면 된다. 

더티 플래그 패턴은 속도를 위해 메모리를 희생한다.

 

18.6 예제 코드

class Transform {
public:
    static Transform origin();
    Transform combine(Transform& other);
};

class GraphNode {
public:
    GraphNode(Mesh* mesh)
    : mesh_(mesh),
      local_(Transform::origin()),
      dirty_(true) {}
    
    void render(Transform parentWorld, bool dirty) {
        dirty |= dirty_;
        if(dirty) {
            // 부모의 월드 변환과 자신의 지역 변환을 결합하여 자신의 월드 변환을 생성
            world_ = local_.combine(parentWorld); 
            dirty_ = false;
        }
        
        if (mesh_) renderMesh(mesh_, world_);
        
        for (int i = 0; i < numChildren_; i++) {
            children_[i]->render(world_, dirty);
        }
    }
    
    void setTransform(Transform local) { 
        local_ = local;
        // 노드 하나의 dirty 플래그만 바꿔도 된다.
        // 하위 노드의 render함수의 인수로 dirty_ 값을 전달하기 때문에
        dirty_ = true;
    }
private:
    Transform world_; // 이전에 계산한 월드 변환 값 저장
    Transform local_; // 상위 노드 기준 지역 변환 값 저장
    Mesh* mesh_;
    GraphNode* children_[MAX_CHILDREN];
    int numChildren_;
    bool dirty_;
}
    
    
// 사용 예시

GraphNode* graph_ = new GraphNode(NULL); // 최상위 노드(장면 그래프)

// 노드를 루트 노드에 추가
GraphNode* sub_ = new GraphNode(graph_);

graph_->setTransform(Transform 객체);
graph_->render(Transform::origin(), false);

18.7 디자인 결정

더티 플래그를 언제 끌 것인가(데이터를 갱신할 것인가)?

결과값이 필요할 때

ㄴ 계산 시간이 오래 걸린다면 거슬리는 멈춤 현상이 생길 수 있다.

 

미리 정해놓은 지점에서 할 때

ㄴ 로딩 화면이나 컷신이 나오는 동안 처리하면 지연 작업 처리가 플레이 경험에 영향을 미치지 않는다.

ㄴ 특정 위치로 플레이어가 이동했을 때 처리가 진행되도록 설계한 경우에는 작업 처리 시점을 제어할 수 없다.

 

백그라운드로 처리할 때

처음 값을 변경할 때 정해진 타이머를 추가하고 타이머가 돌아왔을 때 지금까지의 변경사항을 처리한다.

ㄴ 작업 처리 주기를 정할 수 있다. 

ㄴ 비동기 작업을 지원해야 한다. (멀티 스레딩 같은 기법으로 처리해야 한다.)

+ Recent posts