Unity Study

2025-12-17_SmoothDamp, Critical Damped Oscillation

NyumMa 2025. 12. 17. 19:34

Damping, Critical Oscillation

 

TPS 시스템에서는 사용자의 캐릭터 조작에 있어서

자연스럽고 부드러운 경험을 주는 것이 중요하다.

 

마우스의 동작이나 wasd의 동작을 input 값에 따라서 즉시 반응해 버리면

갑자기 오브젝트가 0에서 1로 움직이는 느낌을 주게 되므로 연속적이지 못하다.

 

현실에서 물체들을 움직일 때는 속도가 0일 때 목표하는 속도 및 회전까지 가속도를 갖고 움직이며 중간에 일부 등속도가 유지되다가 다시 감소하는 형태를 띄게 된다.

현실의 현상들과 맞지 않는 로직 처리는 경험에 있어서 괴리감을 주기 때문에

이를 해결하기 위해서 감쇠라는 개념을 도입한다(Damping)


게임은 하나의 실시간 변화형 영상 매체이다.

영상을 출력하기 위해서는 촬영을 사전에 해야 하며 이를 위해 카메라라는 도구를 쓴다.

카메라는 현실에서 질량이 있는, 심지어 들고 다니기 상당히 무거운 도구이다.

따라서 카메라를 움직인다면 질량을 가지고 있기 때문에 즉각 반응하지 않는

관성의 효과를 어느정도 갖고 있어야 한다.

 

관성 + 탄성 + 에너지 감소

를 구현하는 효과를 제어공학에서 감쇠진동이라고 하며

카메라에서 카메라의 UX 친화적인 움직임을 구현하기 위해서 damping이라는 기능을 사용한다.

 

감쇠진동은 기본적으로 2가지 물리법칙과 한가지의 가상 효과 처리를 사용한다.

1.     뉴턴 제 1법칙

2.     훅의 법칙 

3.     감쇠 진동 

 

감쇠 진동은 에너지 보존을 깨도록 하는 항이다. 여기에서는 용수철의 운동으로 인한 열 에너지, 소리에너지 등의 요소가 포함되어 있고 진동 감쇠의 중요한 포인트.

가속도와 속도는 변위의 이계도함수와 일계도함수

 

질량에 대한 항에 관계없이 속도와 변위를 조절하고자 하므로

여기서

첫번째 항은 가속도를 의미하고

두번째 항은 감쇠진동을

세번째 항은 훅의 법칙을 의미함

 

 에너지 감소가 없는 훅의 법칙을 표현할 때

 

해당 식에서 harmonic oscillator의 표현식을 대비하여

로 자연 진동수를 표현할 수 있고 이는 용수철 상수와 질량게 관계없이

해당 시스템의 주기를 표현하는데 있어서 중요한 지표였음

 

목적은 용수철의 탄성이 작든 크든 질량이 작든 크든 내가 원하는 주기 T(smoothtime)을 제어할 수 있다는 점에서 질량항과 용수철 항을 고려하지 않고 표현할 수 있다는 점에서 의의가 있다.

 

따라서

 

여기서 감쇠 항에 대해서 자연 진동과 달리 감쇠항이 도입됨으로써

기본적으로 에너지 보존 시 감쇠항을 통해 에너지 손실 속도를 w와 비교하여 처리됨  

에 대해서 이 항이 나타내는 의미에 대해서 SI 단위 분석을 먼저 해 보자.

에서 단위 분석을 하는 과정에서 부호는 생략하여

를 통해 임의로 도입한 감쇠상수 c

라는 것을 알 수 있고

따라서

단위를 의미하는 것을 알 수 있다.

 

아이러니하게도

또한

에서

따라서

자연진동에서

이였으므로

즉 감쇠항과 진동항은 서로 단위가 같기 때문에 같은 단위로써 비교를 할 수 있게 된다.

여기서 진동수와 감쇠 상수 c와의 연관성을 나타내기 위해 기호 ζ(zeta)를 사용하여

진동수와 감쇠항 간의 관계를 나타내어 보자

그러면 위에서

로 표현할 수 있게 됨.

이렇게 함으로써

2항과 3항의 변수 ω를 통해 공통적으로 나타낼 수 있게 되었다.

 

해당 식을 보았을 대 변수 x에 대해서

꼴과 같으며 조건에서

1.     a,b는 상수

2.     선형 미분방정식

3.     동차 방정식

이므로 2계 선형 상미분방정식의 해를 구하는 것과 같다.

 

기본적으로 아이디어는 지수함수(밑이 자연상수인)를 도입하여 풀이한다.

밑이 e인 지수함수를 미분해도 똑같은 지수함수의 형태로 나오기 때문에

따라서 지수함수로 치환해서 풀든 나오는 해가 같다고 생각해 볼 수 있다.

이렇게 함으로써 이계 미분방정식을 이차 방정식의 해로 치환하여 풀이가 가능하다.

 

를 통해 r의 해를 구한 후 이를 대입하여 실제 x의 해를 구한다.

따라서

로 표현이 가능하며 해당 식에서의 r의 해를 구하는 것이 목표가 된다.

 

한편, 미분 방정식에서 해의 상태에 대해서

각각 어떤 의미를 담고 있는지 먼저 알아보자.

 

감쇠항이 없는 용수철의 진동에서 방정식에 대해 해를 풀었을 때

마찬가지로 상수계수 2계 미분 방정식이므로

과정으로

r의 해가 복소수를 가져야만 진동을 나타낸다는 것을 간접 추정해 볼 수 있다.

그렇다면 판별식에서 만약 0이 되거나 양수가 된다면 해당 운동방정식은 진동을 갖지 않은 상태라는 것을 간접적으로 추정할 수 있게 된다.

 

따라서,

 

해당 식에서 판별식 사용시

0이 되거나 양수 범위가 되도록 하는 것이 해당 시스템에서 용수철과 같이 진동하지 않는 감쇠 상태라는 것을 생각해 볼 수 있다.

 

이때, 판별식에서 0이 되도록 하는 임계계수의 값을

임계 감쇠(crtical damping)라고 한다.

 

한편, 위의 방정식을 풀었을대

해당 판별식이 0이 되도록 하는

zeta2가 되는데, 이렇게 되면 직관적이지 않으므로

보통 경계를 정의할 때 많이 사용하는 범위인 1을 기준으로 나타내기 위해서

로 재정의 하자

 

돌고 돌아서

최종적으로

의 형태가 되었으며

로 정리되어 범위를 나이스하게 나타낼 수 있다.

ζ < 1일 때 진동

ζ = 1일 때 임계감쇠

ζ > 1일 때 과감쇠

 

로 나누어 설계가 가능하다.

 

기본적으로 유니티에서는 ζ=1

임계감쇠 상태를 기준으로 smoothdamp 메서드를 정의한다.

 

ζ < 1경우 target 지점을 벗어나는(진동) 형태가 되며

에서 미분방정식의 해의 풀이에 의해

가 된다.

 

ζ > 1경우 target으로 갈 때 임계감쇠와 비슷하지먼 좀더 둔하게 움직이는 형태로 된다.

여기서 r1,r2중에서는 두 실근이 나오므로 작은 값과 큰 값 2개를 갖게 되기 때문에

해가 작은 값에 의해서 x(t)에서 target까지 도달하는 시간이 둔해지게 됨

 

유니티에서 사용하는 식으로 위의 임계 감쇠 미분방정식 해를 구한다면

에서

로 해가 풀린다.

미분방정식을 풀기 위해 밑이 지수함수인 함수를 빌려 사용하였으므로 다시 x에 대해서 정리하면

2계 상미분방정식에서 중근을 가질 때의 조건에 의해

여기서 초기조건을 통해 A값과 B값을 구해야 한다.

 

거리를 나타내는 최종 결과를 나타낼 때는 x보다는 y 기호가 친숙하므로

xy로 바꿔서 다시 표기하면

 

한편, 훅의 법칙에서는 움직인 변위에 대해 움직이는 변위와 반대 방향으로 적용하는 탄성력을 정의하기 위해 F = -kx로 사용하였는데

 

지금 우리가 구현하고자 하는 cameradamping의 효과는

마우스 움직이는 방향에 따라서 똑같이 용수철과 비슷한 효과를 주는 것을 원하기 때문에 F=-kx에서 변위 x의 효과를 target -current가 아닌 current – target으로 표현해야 함.

 

따라서

로 두도록 하자.

초기 조건에서

이므로

대입하면

 

가 된다.

따라서

 

최종적으로

 

에 의해서 합성함수의 미분으로

 

이것들이 게임 엔진들에서 진동 감쇠를 표현하는 핵심 공식이 된다.


이론 응용단계

 

실제로 유니티에서 Mathf 라이브러리의 정의를 뜯어보면

속도와 변위에 대한 계산식을 통해 SmoothDamping 현상을 나타내는 소스코드가 나타나져 있다.

public static float SmoothDamp(float current, float target, ref float currentVelocity, float smoothTime, [DefaultValue("Mathf.Infinity")] float maxSpeed, [DefaultValue("Time.deltaTime")] float deltaTime)
{
    smoothTime = Max(0.0001f, smoothTime);
    float num = 2f / smoothTime;
    float num2 = num * deltaTime;
    float num3 = 1f / (1f + num2 + 0.48f * num2 * num2 + 0.235f * num2 * num2 * num2);
    float value = current - target;
    float num4 = target;
    float num5 = maxSpeed * smoothTime;
    value = Clamp(value, 0f - num5, num5);
    target = current - value;
    float num6 = (currentVelocity + num * value) * deltaTime;
    float num7 = currentVelocity;
    currentVelocity = (currentVelocity - num * num6) * num3;
    float num8 = target + (value + num6) * num3;
    if (num4 - current > 0f == num8 > num4)
    {
        num8 = num4;
        currentVelocity = ((deltaTime != 0f) ? ((num8 - num4) / deltaTime) : num7);
    }
    return num8;
}
public static float DeltaAngle(float current, float target)
{
    float num = Repeat(target - current, 360f);
    if (num > 180f)
    {
        num -= 360f;
    }
    return num;
}

public static float SmoothDampAngle(float current, float target, ref float currentVelocity, float smoothTime, [DefaultValue("Mathf.Infinity")] float maxSpeed, [DefaultValue("Time.deltaTime")] float deltaTime)
{
    target = current + DeltaAngle(current, target);
    return SmoothDamp(current, target, ref currentVelocity, smoothTime, maxSpeed, deltaTime);
}

 

1. smoothTime = Max(0.0001f, smoothTime);

-> 시간 범위가 작아지면 0으로 나눠지는 꼴이 되므로 오차를 고려하여 0.0001f로 제한

 

2. float num = 2f / smoothTime;

-> num은 여기서 진동수 w의 값을 의미, 진동수와 주기 관계는 서로 역수 관계인데 여기서는 2를 곱한 값을 사용함

-> 유니티 엔진 개발자들이 그렇게 설계한 항인데 왜 그런지 확인해 보면 오차범위를 통해 알 수 있음

해당식에서 target까지 남은 거리에 대해 결론적으로 주도권을 가지는 부분은 지수항에 의존된다.

 

유니티에서 smoothdamp 함수를 사용할 때 smoothime을 사용하는데 이는 target까지 도달 시간을 설정하는 값이 아닌

target까지 도달하는데 몇%의 오차를 두어 설계할 것인지를 의미하는 인자임

프레임당 업데이트 식이 아닌 최종적인 결과로 봤을 때 w값을 어떻게 설정할지 봐 보면

가 되도록 하는 w를 우리는 설정하려 하는 것을 약속으로 한다.

왜 굳이

인가 하면

해당 값은 약 0.135인데

Target까지 남은 거리가 0.135 % 정도 남았다(87% 정도 도달했다)

라는 기준으로 설계를 하는 것이 가장 편하고 직관적이기 때문임

만약

를 하게 되면

각각 지수함수에서 값이

0.36

0.05

정도의 값을 나타내는데

 

0.36의 경우 아직 오차가 크며

0.05의 경우

로 설계해야 하는데 이는 2로 설정한 것 보다 감쇠효가 가 커지므로 2.5와 같은 소숫값도 아닌

가장 나이스하고 직관적인 수치의 상한선인 2를 기준으로 설계한 것.

 

여기서 중요한 것은 SmoothTime이 결코 해당 target까지의 도달 시간을 정의한 것이 아니라는 것을 코드 + 수식 분석을 통해 알 수 있었음  

 

즉, 최종적으로

T는 코드에서 약 87%까지 도달했을떄의 목표 시간을 float smoothtime으로 사용한다는 것.

 

3. float num2 = num * deltaTime;

wdtnum2에 캐싱해 놓은 것

 

유니티에서는 일반적인 물리 현상에 결과를 정의할 때와 다르게 매 순간마다 업데이트 되는 순간 속도 및 순간 변위를 통해 업데이트가 가능하므로

로 표현되어야 한다.

따라서

 

를 사용함

 

4. float num3 = 1f / (1f + num2 + 0.48f * num2 * num2 + 0.235f * num2 * num2 * num2);

아주 중요한 줄인데,

를 계산할 때 deltatime을 자연상수와 함께 제곱해버리면 부동소수점 연산에 의해 computational power가 매우 많이 필요하므로 최적화가 똥이 되어버림.

따라서 수치해석을 통해 값을 근사하게 됨

 

대부분 테일러 근사를 생각하게 되지만 3차항까지 근사시 지수함수의 결과와의 오차가 커지게 되므로

좀더 나이스한 수치근사 방법으로 Padé 근사를 활용한다.

 

5. float value = current - target;

y(t)부분, 훅의 법칙의 표현법을 유지하려고 targetcurrent를 서로 바꾸어 변위 표기를 뒤집어 놓은 것을 확인할 수 있다.

 

6. float num5 = maxSpeed * smoothTime;

value = Clamp(value, 0f - num5, num5);

여기서는 smoothdamp에서 maxspeeed를 제한걸어 놓을 수 있는데 interpolation시 지정한 해당 속도를 넘었을 때 움직임에 대해 최대 속도까지로 제한해 놓은 항임.

 

7. float num6 = (currentVelocity + num * value) * deltaTime;

중간에

를 캐싱한 것

 

8. float num7 = currentVelocity;

currentVelocity = (currentVelocity - num * num6) * num3;

와 동일한 부분

 

9. float num8 = target + (value + num6) * num3;

와 동일한 부분,

 

나머지 아래 코드는 스크립트가 적용된 오브젝트가

target이상으로 넘어갔을 시에 위치 보정을 해주는 코드임.


이것으로 유니티에서 smoothdamp 함수에 대해 내가 할 수 있는 심화 범위까지 정리를 해 보았다.

 

공부할 때 구현해보다가 특정 함수를 사용시 내가 그냥 메소드나 기능을 단순히 사용하는 것에 대해서는 찜찜해서 api에서 어떻게 구현이 되는지까지 찾아보면서 하는 편인데

정리해보면서 솔직히 이정도로 정리가 필요한지에 대해서는 의문이 들었음.

당장 중요한 것은 유니티 게임엔진의 다양한 기능들을 사용하고 공부해서 내공을 쌓고 지식의 폭을 넓히는 것이 주요 목적이므로

공부할 때 그래도 각 인자들이 어떤 의미를 하는지 정도 까지만 살펴보고

앞으로는 심화 공부보다는 기능적 사용에 먼저 익숙해지고 어느정도 개발에 능숙해 졌을 때 하나씩 공부해 나가는 방식으로 피드백이 필요할 것 같다.

 

물론 공부할 때 소스코드나 기능들을 단순히 사용하는 것이 아니라

어떻게 구현되어 있는지 찾아보고 심화로 깊게 알아보는게 실력향상에 도움이 된다고는 하지만

지금단계에서는 여러 구현들에 대해 양치기를 하는 것이 나에게는

더 올바른 공부법이라는 생각이 든다.