캐릭터 Cloth Simulation LOD 전환 시 옷자락 튐 줄이기: 위치·속도 상태 인계 방법

캐릭터 Cloth Simulation LOD 전환 때 옷자락이 튀는 원인을 좌표계·메시 매핑·속도 불연속 관점에서 분석하고 위치·속도·제약 상태를 안전하게 인계하는 구현 절차와 디버깅 기준을 정리합니다.

✓ 복사 완료!
챕터에셋 압축 & 로딩 최적화
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
캐릭터 Cloth Simulation LOD 전환 시 옷자락 튐 줄이기: 위치·속도 상태 인계 방법

핵심 요약

Cloth Simulation LOD 전환 시 옷자락이 튀는 주된 원인은 새 LOD가 이전 시뮬레이션의 위치와 속도를 이어받지 못하고 초기화되기 때문입니다. 이전 LOD의 위치와 속도를 공통 좌표계에서 새 시뮬레이션 메시로 매핑한 뒤 제약 조건을 재투영하면 전환 충격을 크게 줄일 수 있습니다.

핵심은 위치만 복사하지 않는 것입니다. position, velocity, 필요할 때만 constraint lambda를 함께 인계하고 LOD 선택에는 히스테리시스를 적용해야 합니다.

Cloth Simulation LOD 전환에서 옷자락이 튀는 이유는 무엇인가?

캐릭터의 Cloth Simulation은 보통 LOD0에서는 많은 시뮬레이션 버텍스와 제약 조건을 사용하고 LOD1 또는 LOD2에서는 더 적은 버텍스를 사용합니다. 화면에 표시되는 렌더 메시와 실제 물리 계산에 사용하는 시뮬레이션 메시가 서로 다를 수도 있습니다.

LOD가 바뀌는 순간 다음과 같은 불연속이 발생합니다.

원인전환 시 발생하는 현상결과
시뮬레이션 메시 토폴로지 변경버텍스 수와 제약 구조가 달라짐기존 상태를 1:1로 복사할 수 없음
위치만 전달새 솔버의 속도가 0 또는 기본값으로 시작옷자락이 갑자기 멈추거나 튐
좌표계 불일치월드 좌표와 컴포넌트 좌표를 혼용캐릭터 이동·회전 때 큰 오프셋 발생
서로 다른 바인드 포즈동일한 버텍스 위치가 다른 기준을 가짐옷이 몸에서 벌어지거나 파고듦
제약 조건 재생성기존 제약의 누적 보정값이 사라짐전환 직후 과도한 진동 발생
LOD 판정 플리핑짧은 거리에서 LOD가 반복 전환됨옷자락이 계속 흔들림

특히 PBD 또는 XPBD 계열 솔버는 위치뿐 아니라 이전 프레임의 위치와 속도, 제약 조건의 누적 보정값을 이용해 안정성을 유지합니다. 새 LOD가 이 값을 잃으면 동일한 프레임의 애니메이션이라도 다른 초기 조건에서 계산을 시작합니다.

상태 인계는 어떤 데이터를 전달해야 하는가?

권장하는 상태 패킷은 다음과 같습니다.

struct ClothHandoffState
{
    Transform rootTransformWorld;
    Array<Vector3> positionWorld;
    Array<Vector3> velocityWorld;
    Array<Vector3> previousPositionWorld;
    Array<float> constraintLambda;
    uint64 sourceTopologyId;
    float simulationTime;
};

모든 항목을 항상 전달할 필요는 없습니다. positionWorld와 velocityWorld는 기본 항목으로 보고 constraintLambda는 소스와 타깃의 제약 구조가 호환될 때만 전달합니다.

상태기본 처리비고
위치반드시 매핑새 LOD의 초기 위치가 됨
속도반드시 매핑옷자락의 관성과 움직임을 유지
이전 위치속도 재구성에 사용Verlet 또는 PBD 구현에서 유용
제약 누적값호환될 때만 전달제약 ID와 순서가 달라지면 폐기
충돌 접촉 상태보통 폐기 후 재생성충돌체와 접촉점이 바뀔 수 있음
수면·슬립 상태보수적으로 초기화전환 직후 한두 프레임 재평가

제약 조건이 다른 LOD에 lambda를 무조건 복사하면 오히려 폭발적인 보정이 발생할 수 있습니다. LOD별 엣지 제약, 굽힘 제약, 충돌 제약의 ID가 동일한 경우에만 제한적으로 재사용합니다.

공통 좌표계로 변환해야 하는 이유

소스 LOD와 타깃 LOD의 컴포넌트 공간이 서로 다르면 위치 차이를 직접 계산하면 안 됩니다. 가장 단순한 방법은 전환 순간의 월드 공간에서 상태를 샘플링하고 타깃 솔버가 요구하는 공간으로 변환하는 것입니다.

캐릭터 루트가 이동하거나 회전하는 경우에는 위치뿐 아니라 속도에도 루트 운동을 반영해야 합니다. 월드 속도에서 캐릭터의 선속도와 각속도에 의한 속도를 제거한 뒤 타깃 컴포넌트 공간으로 변환합니다.

vroot=Rroot−1(vworld−vrootLinear−ω×(pworld−proot))v_{root} = R_{root}^{-1} \left(v_{world} - v_{rootLinear} - \omega \times (p_{world} - p_{root})\right)

여기서 RrootR_{root}는 루트 회전 행렬이고 ω\omega는 루트의 각속도입니다. 엔진이 컴포넌트 공간 속도를 직접 제공한다면 해당 API를 사용하는 편이 안전합니다.

월드 공간을 공통 기준으로 사용하기 어렵다면 최소한 동일한 캐릭터 루트 기준의 root space를 사용해야 합니다. 소스 LOD는 캐릭터 A의 컴포넌트 공간이고 타깃 LOD는 캐릭터 B의 컴포넌트 공간인 식의 혼용은 피해야 합니다.

Cloth Simulation LOD 전환에서 소스 시뮬레이션 메시의 상태가 타깃 메시로 전달되는 과정을 보여주는 다이어그램

서로 다른 LOD 메시의 상태를 어떻게 매핑할까?

시뮬레이션 버텍스 수가 다르므로 인덱스 기준 복사는 사용할 수 없습니다. 권장 방식은 소스 시뮬레이션 메시의 삼각형과 무게중심 좌표를 저장해 두는 것입니다.

타깃 버텍스가 소스 삼각형 a,b,ca,b,c에 대응하고 무게중심 좌표가 wa,wb,wcw_a,w_b,w_c라면 위치는 다음과 같이 보간합니다.

psample=wapa+wbpb+wcpcp_{sample} = w_a p_a + w_b p_b + w_c p_c

속도도 같은 가중치로 보간합니다.

vsample=wava+wbvb+wcvcv_{sample} = w_a v_a + w_b v_b + w_c v_c

옷의 두께나 바인드 포즈 차이를 보정하려면 소스 삼각형의 법선 방향 오프셋을 추가합니다.

ptarget=psample+nsourcedoffsetp_{target} = p_{sample} + n_{source} d_{offset}

여기서 doffsetd_{offset}은 바인드 포즈에서 측정한 타깃 버텍스와 소스 표면 사이의 거리입니다. 이 매핑은 런타임에 매 프레임 최근접 삼각형을 탐색하기보다 에셋 임포트 또는 쿠킹 단계에서 미리 생성하는 편이 좋습니다.

매핑 데이터에 포함할 항목

  • 소스 삼각형 인덱스 3개
  • 무게중심 좌표 3개
  • 법선 방향 오프셋
  • 매핑 유효성 플래그
  • 소스와 타깃의 바인드 포즈 ID
  • 최대 허용 매핑 거리

매핑 거리가 임계값을 넘거나 소스 삼각형이 삭제된 경우에는 해당 버텍스를 단순 보간하지 않습니다. 타깃의 애니메이션 위치와 바인드 포즈 오프셋을 사용하고 속도는 주변 유효 버텍스의 평균값으로 대체하는 방식이 더 안전합니다.

위치와 속도를 함께 인계하는 구현 순서는?

다음 순서로 전환하면 대부분의 튐 문제를 재현 가능하게 제어할 수 있습니다.

Step 1. LOD 전환 직전에 소스 상태를 캡처한다

시뮬레이션 솔버가 현재 프레임 계산을 완료하고 렌더 메시가 결과를 읽기 직전에 상태를 캡처합니다. 애니메이션 포즈를 적용하기 전이나 중간 제약 반복 중에 캡처하면 프레임마다 다른 상태가 저장될 수 있습니다.

ClothHandoffState CaptureState(SourceCloth source)
{
    ClothHandoffState state;
    state.rootTransformWorld = source.GetRootTransformWorld();
    state.positionWorld = source.GetParticlePositionsWorld();
    state.velocityWorld = source.GetParticleVelocitiesWorld();
    state.previousPositionWorld = source.GetPreviousParticlePositionsWorld();
    state.sourceTopologyId = source.GetTopologyId();
    state.simulationTime = source.GetSimulationTime();
    return state;
}

렌더 보간을 사용하는 경우에는 보간된 렌더 버텍스가 아니라 물리 솔버가 보유한 실제 파티클 상태를 읽어야 합니다.

Step 2. 타깃 LOD에 위치와 속도를 매핑한다

void TransferState(
    const ClothHandoffState& source,
    const MappingTable& mapping,
    TargetCloth target)
{
    for (int t = 0; t < target.ParticleCount(); ++t)
    {
        const Mapping& m = mapping[t];

        if (!m.valid)
        {
            target.SetPosition(t, target.GetAnimatedPositionWorld(t));
            target.SetVelocity(t, Vector3::Zero());
            continue;
        }

        Vector3 p =
            m.weights.x * source.positionWorld[m.triangle.x] +
            m.weights.y * source.positionWorld[m.triangle.y] +
            m.weights.z * source.positionWorld[m.triangle.z];

        Vector3 v =
            m.weights.x * source.velocityWorld[m.triangle.x] +
            m.weights.y * source.velocityWorld[m.triangle.y] +
            m.weights.z * source.velocityWorld[m.triangle.z];

        Vector3 n = source.GetInterpolatedNormal(m.triangle, m.weights);
        target.SetPosition(t, p + n * m.normalOffset);
        target.SetVelocity(t, v);
    }
}

실제 엔진에서는 SetPosition이 다음 시뮬레이션 프레임에 덮어써질 수 있습니다. 따라서 타깃 솔버의 초기 파티클 버퍼나 프리딕션 버퍼에 넣어야 하며 API 명칭은 엔진과 물리 플러그인에 맞게 조정해야 합니다.

Step 3. 타깃 제약 조건을 재투영한다

상태를 넣은 직후 바로 렌더링하지 말고 타깃 LOD의 제약 조건을 최소 한 번 적용합니다. 길이 제약, 고정점, 충돌 제약의 순서는 타깃 LOD 정의를 따릅니다.

void StabilizeAfterHandoff(TargetCloth target)
{
    target.PredictParticles();
    target.SolveDistanceConstraints(1);
    target.SolveBendingConstraints(1);
    target.SolveCollisionConstraints(1);
    target.RecomputeVelocitiesFromPositions();
}

여기서 속도를 무조건 0으로 재설정하면 옷자락의 관성이 사라집니다. 대신 제약 투영 이후 위치 변화로 속도를 다시 계산하고 비정상적으로 큰 값만 프로젝트별 상한으로 제한합니다.

Step 4. 짧은 시간 동안 보정량을 감쇠한다

소스와 타깃 메시의 차이가 크면 완전히 동일한 상태를 만들 수 없습니다. 이때는 전환 후 2~4프레임 동안 보정량을 감쇠합니다.

α=smoothstep(0,1,kN)\alpha = smoothstep(0, 1, \frac{k}{N}) pk=(1−α)phandoff+αptargetProjected p_k = (1-\alpha)p_{handoff} + \alpha p_{targetProjected}

속도에도 같은 방식으로 적용할 수 있지만 위치와 속도를 서로 다른 감쇠율로 처리하면 더 자연스러운 결과를 얻을 수 있습니다. 일반적으로 위치는 빠르게 안정화하고 속도는 한두 프레임 더 유지하는 편이 옷자락의 관성을 보존하는 데 유리합니다.

void ApplyHandoffBlend(TargetCloth target, int frame, int blendFrames)
{
    float alpha = SmoothStep(0.0f, 1.0f,
        float(frame + 1) / float(blendFrames));

    for (int i = 0; i < target.ParticleCount(); ++i)
    {
        Vector3 p = Lerp(target.handoffPosition[i],
                         target.projectedPosition[i], alpha);
        Vector3 v = Lerp(target.handoffVelocity[i],
                         target.projectedVelocity[i], alpha);
        target.SetPosition(i, p);
        target.SetVelocity(i, v);
    }
}

핵심은 두 시뮬레이션을 계속 병렬 실행하는 것이 아니라 타깃 솔버의 초기 상태를 안정화하는 것입니다. 병렬 시뮬레이션과 렌더 크로스페이드는 품질이 가장 높지만 캐릭터 수가 많을 때 비용이 커질 수 있습니다.

LOD 전환 플리핑은 어떻게 막을까?

상태 인계가 정확해도 LOD 판정이 매 프레임 바뀌면 문제가 반복됩니다. 거리 기준에는 진입 거리와 이탈 거리를 다르게 설정하는 히스테리시스를 사용합니다.

상황LOD0 유지 조건LOD1 전환 조건
카메라가 가까워짐이미 LOD0이면 유지LOD0 진입 거리보다 멀어질 때만 전환
카메라가 멀어짐LOD1 이탈 거리보다 가까우면 유지LOD1 진입 거리보다 멀어질 때 전환
급격한 카메라 이동최소 유지 시간 적용전환 예약 후 다음 동기점에서 실행

예를 들어 LOD0 진입 거리를 8m, LOD1 전환 거리를 10m로 두면 8~10m 구간에서 LOD가 반복 전환되는 것을 줄일 수 있습니다. 실제 값은 캐릭터 화면 크기와 카메라 FOV에 맞춰 프로파일링해야 합니다.

전환은 다음과 같은 단일 흐름으로 관리하는 것이 좋습니다.

flowchart LR
    A[LOD 판정] --> B{전환 필요}
    B -->|아니오| C[현재 솔버 계속 실행]
    B -->|예| D[소스 위치와 속도 캡처]
    D --> E[공통 좌표계 변환]
    E --> F[타깃 메시로 삼각형 매핑]
    F --> G[타깃 제약 재투영]
    G --> H[2~4프레임 보정 감쇠]
    H --> C

잘못된 상태 인계 방식과 개선 방법

잘못된 방식왜 문제가 되는가개선 방법
버텍스 인덱스로 위치 복사LOD별 토폴로지가 다름삼각형·무게중심 매핑 사용
위치만 복사하고 속도는 0관성이 끊김위치와 속도를 함께 전달
렌더 메시에서 상태 샘플링스키닝과 물리 결과가 섞임시뮬레이션 파티클 버퍼 사용
월드 위치를 로컬 위치로 직접 입력루트 이동·회전에서 오프셋 발생공통 root space 사용
모든 제약 lambda 복사제약 ID와 길이가 달라짐호환 제약만 복사하고 나머지는 초기화
전환 프레임에 큰 속도 제한움직임이 갑자기 죽음비정상 값만 제한하고 감쇠 적용
거리 임계값 하나만 사용경계에서 LOD 플리핑 발생히스테리시스와 최소 유지 시간 적용

디버깅에서 무엇을 측정해야 하는가?

눈으로 옷자락을 확인하는 것만으로는 원인을 구분하기 어렵습니다. 전환 직전과 직후에 다음 값을 기록하면 위치 문제와 속도 문제를 분리할 수 있습니다.

Epos=maxi∥piafter−pibefore∥E_{pos} = max_i \|p_i^{after} - p_i^{before}\| Evel=maxi∥viafter−vibefore∥E_{vel} = max_i \|v_i^{after} - v_i^{before}\|

추가로 다음 항목을 시각화하면 좋습니다.

  • 소스와 타깃 파티클 위치를 서로 다른 색으로 표시
  • 매핑된 소스 삼각형과 타깃 버텍스 연결선 표시
  • 전환 직후의 속도를 선분으로 표시
  • 매핑 실패 버텍스를 빨간색으로 표시
  • 충돌 제약 위반과 바디 관통 깊이 기록
  • LOD 전환 프레임과 현재 blend alpha 표시

LOD 전환 직후의 파티클 위치, 속도 벡터, 매핑 실패 버텍스를 보여주는 디버그 오버레이 예시

테스트 케이스는 정지 상태만으로 충분하지 않습니다. 루트 모션이 있는 걷기와 달리기, 급회전, 점프 착지, 바람 임펄스, 벽이나 캐릭터 몸체에 옷이 눌린 상황을 각각 확인해야 합니다.

판정 기준은 프로젝트의 허용 범위로 정해야 합니다. 예를 들어 옷자락 최대 위치 변화, 속도 스파이크, 몸체 관통 깊이, 안정화에 걸린 프레임 수를 기록하고 전환 전후의 평균값과 최대값을 비교합니다.

실무 적용 체크리스트

  1. LOD별 시뮬레이션 메시의 바인드 포즈와 root space가 일치하는지 확인합니다.
  2. 렌더 버텍스가 아니라 물리 파티클 버퍼에서 소스 위치와 속도를 캡처합니다.
  3. 타깃 파티클마다 소스 삼각형 인덱스, barycentric weight, 법선 오프셋을 준비합니다.
  4. 위치와 속도를 타깃 솔버의 초기 상태에 입력합니다.
  5. 타깃 LOD의 제약 조건을 재투영하고 속도를 재계산합니다.
  6. 제약 구조가 다르면 constraint lambda와 접촉 상태를 폐기합니다.
  7. 2~4프레임의 보정 감쇠를 적용하고 비정상적인 속도만 제한합니다.
  8. LOD 거리 히스테리시스와 최소 유지 시간을 설정합니다.
  9. 루트 모션, 급회전, 충돌, 바람 임펄스 상황에서 E_pos와 E_vel을 측정합니다.

자주 묻는 질문 FAQ

위치만 보간하면 옷자락 튐이 해결되나요?

대부분 해결되지 않습니다. 위치를 맞춰도 새 솔버의 속도가 0이면 관성이 끊기므로 위치와 속도를 함께 매핑해야 합니다.

LOD가 다르면 constraint lambda도 복사해야 하나요?

아닙니다. 제약 ID와 구조가 동일하게 호환될 때만 선택적으로 복사합니다. 제약 구조가 다르면 lambda를 초기화하고 타깃 제약을 다시 풀어야 합니다.

두 LOD의 메시 토폴로지가 완전히 다르면 어떻게 하나요?

소스 삼각형과 barycentric weight를 사용하는 매핑 테이블을 에셋 제작 단계에서 생성합니다. 매핑이 실패한 버텍스는 애니메이션 위치와 주변 속도로 대체하고 해당 영역은 짧은 보정 감쇠를 적용합니다.

정리

Cloth Simulation LOD 전환의 옷자락 튐은 단순한 렌더 LOD 문제가 아니라 시뮬레이션 상태가 불연속적으로 바뀌는 문제입니다. 공통 좌표계에서 소스 LOD의 위치와 속도를 캡처하고 타깃 시뮬레이션 메시로 삼각형 매핑한 뒤 제약 조건을 재투영하면 전환 충격을 줄일 수 있습니다.

여기에 제약 상태의 호환성 검사, 2~4프레임 보정 감쇠, LOD 히스테리시스를 결합하면 캐릭터 이동과 카메라 변화가 큰 상황에서도 안정적인 Cloth Simulation LOD 파이프라인을 구성할 수 있습니다.