2022. 5. 22. 10:35ㆍ개발하다가
이슈
저는 카메라가 부드럽게 움직이게 하기 위해, 원하는 자리에 초당 99%만큼 도달하기를 원했습니다. (이것 외에도 부드럽게 움직이는 방법은 많습니다. 훅 법칙을 적용하는 경우도 있는 것으로 들었습니다.) 현재 위치가 P이고 도달해야 할 위치가 Q라면 1초 후에는 0.01P+0.99Q에 도달해야 한다는 말입니다. (Q가 정지해 있다면)
문제
단적으로 생각해서 답은 지수적인 것과 관련이 있을 것 같습니다. 문제는 구현이겠죠. 게임 프로그램에는 루프가 존재하고(보통 60fps, 144fps) 이 반복 간격은 일정하다고 보장할 수 없습니다. 가장 대표적 원인으로는 매우 사양이 낮은 기기 혹은 매우 바쁜 기기가 있겠죠. 이에 따라 각 프레임 간격이 합쳐져 눈에 띌 정도 시간인 T만큼이 흘렀을 때, g(T)만큼의 변화가 있도록 함수를 설계하는 것이 바람직합니다.
현재 프레임 타임, 즉 이전 프레임으로부터 흐른 시간을 dt라고 할 때 dt가 클수록 카메라의 움직임이 많아야 하는 것은 절대로 확실합니다. 또한 현재의 목적지와 지금 있는 위치의 볼록집합(내분점) 범위를 나갈 이유가 없는 것도 확실합니다. 그래서 일단 이러한 형태를 강제해 봅시다.
이때 이론상 앞뒤가 맞으려면, dt가 무한대로 가면 f(dt)는 1에 수렴해야 하며 dt가 0일 때 f(dt)는 0이어야 합니다. 다른 제한도 많으니 이 부분은 일단 기억만 해 두고 넘어갑시다. 무엇보다도 중요한 제한은 앞에서 나왔듯 dt가 프레임마다 혹은 기기마다 달라진다는 점입니다. 그러니 위 식을 2번 적용해 보겠습니다.
dt에 (dt+dt2)를 대입한 값은 위의 p'' 동일한 결과를 내야 합니다. 다시 적으면 아래와 같습니다.
dt_1과 dt_2의 순서를 바꿔도 동일한 결과를 내야 하긴 하는데 + 연산은 교환법칙이 성립하므로, 위 등식만 성립하면 확인됩니다. 우변을 p와 q로 나누면 이렇게 됩니다.
완전히 전개하면 이렇게 됩니다.
즉, f(dt₂)+f(dt₁)-f(dt₁)f(dt₂)=f(dt₁+dt₂)여야 합니다. 결론만 말씀드리면 1-cⁿ이 그 답입니다. 위의 f(0)=0, dt->inf에서 1로 가는 조건에도 맞고 f(a+b)를 f((x+y)+b) 형태로 표현할 수 있으므로 몇 개의 합으로 구성되더라도 총합이 일정(혹은 비슷)할 때 각각의 선형보간을 적용한 결과가 같을 것으로 생각할 수 있습니다.
이제 c를 적절히 정하면 됩니다. 위에서 나온 바와 같이 원하는 자리에 초당 n%만큼 도달하기를 원한다면 1-c¹이 1-n/100이어야 하겠죠. 그럼 c=n/100일 테니 1-(n/100)^dt를 보간에 넣어야 하니 다음과 같이 적용하면 됩니다.
// p, q는 위치벡터
vec3 new_p = lerp(p, q, 1 - pow(0.01 * n, dt));
// 일반적으로 1-0.01*n은 이미 구해져 있는 값이므로 lerp(q, p, pow(c, dt));와 같은 형태가 될 것
반대로, 99%에 도달하는 데 n초가 걸리길 원한다면 1-cⁿ=0.99, 즉 c=0.01^(1/n)=10^(-2/n)이어야 합니다.
대충 방법은 알았지만 pow는 꽤 오래 걸리는 함수입니다. c를 구하는 것은 정할 때 한 번이면 족하지만 pow는 매 프레임마다 수행해야 해서 부담스러운데, 테일러 급수를 이용한 근사를 이용해 봅시다.
60fps를 기준으로 하여 b=1/60로 하고 전개하면 다음과 같은 코드가 될 겁니다.
// pow(c,dt) 대신
pow(c,1./60) + // n=0
pow(c,1./60) * (x-1./60) * log(c))// n=1
pow(c,1./60) * ((x-1./60) * log(c))*((x-1./60) * log(c)); // n=2
// 조금 더 빠르게
pow(c,1./60) * (1 + (1 + (x-1./60) * log(c)) * (x-1./60) * log(c));
// c가 정해질 때 바로 계산되어 있을 수 있는 식
float zeroth=pow(c, STANDARD_FRAME_TIME);
float logc=log(c);
// 실제 프레임마다 호출되는 식
vec3 new_p = lerp(q, p, zeroth * (1 + (x - STANDARD_FRAME_TIME) * logc));
// 1계까지만 전개할 경우 더 빠른 식: 1 - STANDARD_FRAME_TIME*logc가 미리 계산 가능
vec3 new_p = lerp(q, p, zeroth * (1 - STANDARD_FRAME_TIME * logc + x * logc);
간단하게 1계 전개는 부동소수점 곱 2회 합/차 2회, 2계 전개는 (공통 항이라서) 합과 곱만 1회 추가됩니다. 1계의 경우 위 코드의 마지막 행의 식을 쓰면 곱 2회, 합 1회입니다.
(지수함수의 밑이 2일 때, 2계 미분 항까지 전개하면 2fps까지 떨어질 경우에 약 0.5%, 실제값 약 0.007의 오차가 발생하며 1계 미분 항까지 전개하면 5fps까지 떨어질 경우에 약 0.075%, 실제값 약 0.0085의 오차가 발생합니다. 2fps나 5fps나 도저히 게임 못 할 정도의 프레임 드롭이며, 밑이 커질수록 절대/상대 오차도 커지는데 보통 1보다 커지면 카메라도 꽤 비정상적으로 보일 것이기 때문에 1계 전개 정도만으로 충분히 근사 가능)
정리
1-c^dt를 이용한 선형보간으로, 눈에 띌 정도 시간이 지나면 그 동안 프레임 간격에 관계 없이 거의 비슷한 카메라 움직임을 구현할 수 있습니다. 이는 카메라가 정지하는 n=∞일 때와 카메라가 따라가는 데 지연이 아예 없는 n=0에도 일반적으로 적용될 수 있습니다. 한편 fps가 감내할 만한 범위에 있다면 지수함수를 테일러 1계 전개 정도로 근사해도 유의미한 오차가 발생하지 않습니다.
'개발하다가' 카테고리의 다른 글
GPT가 나보다 낫네요? (0) | 2023.03.05 |
---|---|
C++에서 템플릿 메타프로그래밍하다가 발생한 오류 (0) | 2022.05.31 |
오버플로 없는 short int x 분수 (0) | 2022.05.25 |
원 모양 물체의 2차원 비탄성 충돌에 대한 고민 (0) | 2022.05.10 |
동일 평면 상 원과 선분의 위치 관계, 선분과 선분의 위치 관계 (0) | 2022.05.09 |