[익스프레션] 무거운 모션 그래픽 템플릿 최적화하기

모션 그래픽 템플릿이 무거운 이유와 최적화하는 방법

After Effect Blender expression Premiere 모션 그래픽 템플릿 애프터이펙트 익스프레션 프리미어

익스프레션으로 반응형 자막 템플릿을 만들 때 무거워 지는 작업을 최적화할 수 있는 방법에 대해 다뤄보도록 하겠습니다.

이전 시간 모션 그래픽 템플릿 체크하기

먼저 이전 시간에 만든 모션 그래픽 템플릿을 프리미어에서 확인해보겠습니다.

영상을 확인해보면 모션 그래픽 템플릿을 얹었을 때부터 시간이 제법 걸리는 것을 확인할 수 있습니다. 그리고 텍스트를 수정한 뒤 프리뷰를 제대로 볼 수 없을 정도로 버벅되는 것을 볼 수 있습니다. 그런데 일정 시간이 지나서(아마도 애프터 이펙트에서 숫자 패드 0을 눌러서 프리 렌더를 걸었을 때 걸리는 시간 인 듯) 프리미어에서 이 모션 그래픽 템플릿을 한번 읽은 후에는 프리뷰가 잘 보입니다.
하지만 이 과정을 텍스트나 속성들을 수정할 때마다 겪어야 합니다. 이렇게 되면 그야말로 작업 지옥이 열리겠죠. 생각만 해도 끔찍해집니다.
아마 애프터 이펙트로 모션 그래픽 템플릿을 만들어서 프리미어에서 작업해보신 분들은 이 무거움을 느끼고 다른 방법을 찾아 작업하실 것으로 생각이 듭니다.

모션 그래픽 템플릿이 무거운 이유?

먼저 애프터 이펙트에서 작업을 할 때, 레이어를 쌓으면 쌓을수록, 이펙트를 주면 줄수록, 애니메이션을 주면 줄수록 점점 무거워질 수 밖에 없습니다. 러닝타임에 따라 매 프레임마다 각 레이어에 각 속성마다 값들을 실시간으로 계속 체크하기 때문이죠.
따라서 체크해야하는 값이 늘어나면 늘어날수록 점점 느려질 수 밖에 없겠죠.

초당 30프레임의 템플릿을 제작한다면 1초에 30번씩 각 레이어의 각 속성 값들을 체크합니다. 60프레임의 템플릿을 제작한다면? 1초에 60번씩 체크합니다.

사실 이전 시간에 만든 템플릿의 경우 애프터 이펙트에서 크게 작업을 많이 한 것은 아닙니다. 실제 렌더되는 레이어도 4개 밖에 없고, 모션도 4개 밖에 들어가지 않았으니까요. 박스에 모션 블러를 넣었다고는 하지만 이렇게까지 느리게 렌더링될만한 컴포지션은 아닙니다.

그렇다면 가정이 하나 생기는데요. 바로 익스프레션으로 작업을 하게 되면 애프터 이펙트에서는 익스프레션으로 값을 출력하기 위해 매 프레임마다 해당 값이 얼마인지 체크한다는 것입니다.

지난 시간에 만든 모션 그래픽 템플릿의 프리렌더 속도를 측정해 봤습니다. 일단, 지난 시간에 만든 그대로와, 모션 블러가 들어가 있는 박스를 삭제하고 텍스트만 출력했을 때, 모션블러가 모션이 있는 부분만 적용되도록 박스 레이어를 두 개로 나눠놨을 때, 박스에서 모션블러만 제거했을 때로 구분하여 녹화해 봤습니다.

최대한 정확하게 측정하기 위해 프리 렌더 전에 모두 캐시를 삭제하고 진행했습니다.

영상을 보면 역시 모션 블러의 역할이 렌더링에서는 크게 작용합니다. 박스에서 모션블러만 빼더라도 렌더링이 한결 수월한 것을 알 수 있습니다. 그런데 모션 블러를 모션이 있는 구간만 적용하기 위해 Box 레이어를 분리하는 것은 큰 차이가 없습니다. 오히려 레이어를 분리한 것이 조금 더 느려집니다.

프레임 별 렌더 타임 체크하기

모션 블러는 말 그대로 모션이 들어갈 때만 적용되는 블러입니다. 모션이 없을 때는 블러 효과가 적용되지 않는다는 것이겠죠. 따라서 레이어를 하나 더 쌓아서 오히려 익스프레션 코드가 더 추가되었을 때 렌더 속도가 느려지는 것을 확인할 수 있습니다.

사실 이 부분은 그 효과가 크게 미미하다고 봐야겠죠. 왜냐하면 레이어를 하나 추가했지만 익스프레션을 계산하는 레이어 자체는 동일합니다. 레이어를 중복되지 않도록 끊었기 때문이죠. 그럼에도 불구하고 레이어가 쌓이는 것이 애프터 이펙트에서는 조금 더 부담스러운 것 같습니다.

만약 이 작업을 다시 반복한다면 레이어를 두 개로 나눈 것 자체는 크게 속도에서 차이가 날 것 같지는 않습니다. 지금은 이렇게 나왔지만 순간 컴퓨터가 뭔가 과부하가 걸렸을 수도 있고…실제로 모션 블러만 뺀 박스를 넣었을 때 박스 레이어를 삭제한 것과 큰 차이 없었습니다.

여기서 주목해야할 점은 애프터 이펙트 아래 있는 프레임별 렌더 타임입니다.

애프터 이펙트 하단에 달팽이 모양 버튼을 클릭하면 레이어별로 렌더 타임도 확인할 수 있으니 체츠해보세요. 역시 생각대로 Box가 모션블러 때문에 가장 느립니다.

모션 블러가 들어간 박스의 경우 한 프레임 렌더하는 데 9초가 걸리기도 합니다. 그리고 모션이 끝나고 나니까 렌더 속도가 0으로 떨어지는 것을 확인할 수 있죠.

그런데 재밌는 것은 RenderText_Right와 RenderText_Left에서 생각외로 계속 렌더링 시간이 길게 나타나고 있습니다. Slow Fade On 모션이 끝난 상태에서도 렌더 타임이 계속 높게 나타납니다.

즉, 애프터 이펙트에서 매 프레임마다 RnederText에 텍스트가 어떤 것이 들어가야 하는지, 색은 뭐인지, 위치는 어디에 있어야 하는지 기타 등등 모든 속성들을 체크하고 있다는 것입니다.

특히 익스프레션 코드가 복잡해지면 복잡해질수록 계산을 하는 데 시간이 걸릴 수 밖에 없겠죠. 따라서 프레임별 렌더 타임을 단축시키지 위해선 익스프레션 코드를 좀 더 단순화해서 효율적인 코드로 수정하는 방법 밖에는 없습니다.

그런데 코드를 깔끔하고 단순하게 로직을 짜는 것은 사실 초보에게는 불가능에 가깝죠. 오랫동안 로직을 짜던 개발자들이 빌드 타임, 렌더 타임을 줄이기 위한 효율적인 코드를 작성하기 위해 많은 노력을 기울입니다. 하지만 저도 그렇고…이 블로그를 보시는 분들께 그 정도의 효율적인 코드를 작성하는 것은 매우 어려운 일이라고 저는 생각이 듭니다.

프레임별 렌더링 효율화를 위한 방법

애프터 이펙트는 시간을 다룹니다. 매 프레임, 매 초마다 레이어의 속성 값이 바뀌는지 체크를 해야하고, 그리고 그 값이 얼마가 될 지 계산하고 체크해서 화면에 뿌려주게 됩니다. 즉, 변수가 많아지면 많아질수록 애프터 이펙트는 복잡한 계산을 해야하고 화면에 렌더링하는 것을 힘들어하겠죠.

익스프레션을 키프레임 애니메이션으로 바꾸는 방법

우선 복잡한 익스프레션 코드로 만들어 내는 변수들의 값을 상수로 바꿔서 키프레임 애니메이션으로 바꿔버리는 방법이 있습니다.

기존의 컴포지션을 복사해서 Basic_KeyAnimation이라는 컴포지션으로 바꿨습니다.

자 이제 모든 레이어에서 익스프레션 코드가 작성된 속성들을 전부 선택하고 키프레임 어시스턴트에서 익스프레션을 키프레임으로 컨버팅 해보세요. 단축키 EE(더블클릭)로 익스프레션이 적용된 속성만 펼칠 수 있습니다.

이렇게 복잡한 익스프레션 코드로 계산하는 값을 미리 매 프레임마다 계산해서 키프레임 애니메이션으로 바꿔주는 기능입니다.

하지만 이 기능은 극명한 한계점이 있습니다. 바로 속성의 값들을 바꿀 수 없다는 것입니다. 우리가 만드는 템플릿은 반응형으로 텍스트의 길이를 실시간으로 계산하고, 속성들을 계산해서 자막에 따라서 자동으로 바뀌어야 합니다. 하지만 키프레임 애니메이션으로 바꾼 순간 이미 모든 속성의 값들은 모든 프레임마다 상수로 결정되어 버리죠.

따라서 우리가 텍스트를 바꾸려고 했을 때 텍스트가 바뀌지 않는다던가(만약 텍스트 부분의 익스프레션도 키프레임 애니메이션으로 바꿨을 경우) 아니면 포인트 텍스트가 달라졌는데 포인트 텍스트의 박스 크기가 틀어진다던지, 위치 값이 바뀌지 않는 등의 문제가 발생합니다.

이렇게 값이 틀어져 버립니다. 이를 원래대로 돌리려면 키프레임들을 모두 삭제하고, 익스프레션 코드 블록 들어갔다 나와서 익스프레션 활성화 시키고, 다시 텍스트 바꿔서 제대로 동작하는지 체크하고 그 다음 또 키프레임 애니메이션으로 변환하고…
이러한 과정을 거쳐야 하는데 이는 매우 불편한 작업이 되겠죠.

즉, 키프레임 어시스턴트는 이미 완성된 애니메이션의 익스프레션 코드를 키프레임 애니메이션으로 업데이트 해서 프리뷰를 빠르게 보거나 실제 렌더링 속도를 줄일 때 사용하는 기능입니다.

익스프레션 코드 중 Time으로 접근하는 방법

먼저 우리는 익스프레션 코드 중 Time에 집중해보고자 합니다. 지금까지 계속 다루던 코드 중에 sourceRectAtTime()이 있죠. 코드를 보면 Rectangle의 source 데이터를 불러오는 코드입니다. 그런데 At Time이 있죠. 즉, 시간에 따른 사각형의 소스 데이터를 받는 코드입니다. sourceRectAtTime(2)라고 쓰면 2초일 때 사각형의 소스 데이터를 불러옵니다.

즉, 그동안은 매 프레임 마다 변할 수 있는 값인 sourceRectAtTime()을 실시간으로 불러왔다면 이번에는 sourceRectAtTime(2) 이렇게 정해진 시간의 값(고정된 값)을 매 프레임마다 불러오는 형태로 코드를 업그레이드 하겠습니다.

그리고 sourceRectAtTime()와 같이 사용하기 좋은 코드가 바로 valueAtTime()입니다. 애프터 이펙트의 모든 속성들은 그 값을 가집니다. 그리고 그 값이 매 프레임마다 있겠죠. 즉, 모든 속성들의 값을 변수에서 상수로 바꿔주기 위해 valueAtTime()을 사용하겠습니다. 그리고 스타일은 getStyleAt()을 사용하도록 하겠습니다.

기존의 컴포지션을 복제해서 Basic_Test_1이라고 만들었습니다.

모든 익스프레션 코드을 업그레이드 하도록 하겠습니다.

//RenderText_Rigth - Source Text

totalText = thisComp.layer("Text").text.sourceText;
pointText = thisComp.layer("PointText").text.sourceText.valueAtTime(0);

//valueAtTime()이 들어간 부분이 어딘지 확인하세요. split으로 나누기 전에 들어갑니다.
//즉, text.sourceText.valueAtTime()으로 들어갑니다.
//[""]는 이미 상수이므로 수정하지 않습니다.
//텍스트가 중간에 바뀌지 않습니다. 시작할 때만 불러오면 되므로 valueAtTime(0)으로 시작할 때의 값을 불러옵니다.
renderText = (pointText.length === 0 || totalText.valueAtTime(0).indexOf(pointText) === -1) ? [""] : totalText.valueAtTime(0).split(pointText)[1];

//텍스트의 스타일은 valueAtTime이 아닌 getStyleAt을 사용합니다.
//여기서 totalText.style 대신 getStyleAt(index, atTime)을 사용해서 totalText의 0초(atTime)일 때 첫번째 글자(index)의 스타일을 불러왔습니다.
totalText.getStyleAt(0,0).setText(renderText);
//RenderText_Rigth - Anchor Point

//앵커포인트 역시 텍스트가 결정되고 나면 해당 값이 바뀔 일이 없습니다. 따라서 0초 일 때의 값을 기준으로 세팅하겠습니다.
a = thisLayer.sourceRectAtTime(0);

x = a.left + a.width/2;
y = a.top + a.height;

[x, y]
//RenderText_Rigth - Position
//valueAtTime()값이 어디 들어가는지 잘 체크해보세요. 배열의 원소를 참조한다면 배열의 원소를 선택하기 전에 들어가야 합니다.
//transform.position.valueAtTime(0)[1] 이런식으로 들어갑니다.

totalText = thisComp.layer("Text").text.sourceText.valueAtTime(0);

leftTextLayer = thisComp.layer("RenderText_Center");
leftText = leftTextLayer.text.sourceText.valueAtTime(0);

text_blank = thisComp.layer("RenderText_Control").effect("Text_blank")("Slider").valueAtTime(0);
default_Text_blank = thisComp.layer("RenderText_Control").effect("Default_Text_blank")("Slider").valueAtTime(0);

default_X_Position = leftTextLayer.transform.position.valueAtTime(0)[0] + leftTextLayer.sourceRectAtTime(0).width/2 + thisLayer.sourceRectAtTime(0).width/2;

x = leftText.length !== totalText.indexOf(thisLayer.text.sourceText.valueAtTime(0)) - totalText.indexOf(leftText) || thisLayer.text.sourceText.valueAtTime(0).length !== thisLayer.text.sourceText.valueAtTime(0).replace(/^\s+/, "").length ? default_X_Position + text_blank : default_X_Position + default_Text_blank;

y = thisComp.layer("RenderText_Left").transform.position.valueAtTime(0)[1];

[x, y]
//RenderText_Center - Source Text

totalText = thisComp.layer("Text").text.sourceText.valueAtTime(0);
pointText = thisComp.layer("PointText").text.sourceText;

renderText = (pointText.valueAtTime(0).length === 0 || totalText.indexOf(pointText.valueAtTime(0)) === -1) ? [""] : pointText.valueAtTime(0).replace(/\s+$/, "");

pointText.getStyleAt(0,0).setText(renderText);
//RenderText_Center - Anchor Point

a = thisLayer.sourceRectAtTime(0);

x = a.left + a.width/2;
y = a.top + a.height;

[x, y]
//RenderText_Center - Position

totalText = thisComp.layer("Text").text.sourceText.valueAtTime(0);

leftTextLayer = thisComp.layer("RenderText_Left");
leftText = leftTextLayer.text.sourceText.valueAtTime(0);

text_blank = thisComp.layer("RenderText_Control").effect("Text_blank")("Slider").valueAtTime(0);
default_Text_blank = thisComp.layer("RenderText_Control").effect("Default_Text_blank")("Slider").valueAtTime(0);

default_X_Position = leftTextLayer.transform.position.valueAtTime(0)[0] + leftTextLayer.sourceRectAtTime(0).width/2 + thisLayer.sourceRectAtTime(0).width/2;

x = leftText.length !== totalText.indexOf(thisLayer.text.sourceText.valueAtTime(0)) || thisLayer.text.sourceText.valueAtTime(0).length !== thisLayer.text.sourceText.valueAtTime(0).replace(/^\s+/, "").length ? default_X_Position + text_blank : default_X_Position + default_Text_blank;

y = leftTextLayer.transform.position.valueAtTime(0)[1];

[x, y]
//RenderText_Left - Source Text

totalText = thisComp.layer("Text").text.sourceText;
pointText = thisComp.layer("PointText").text.sourceText.valueAtTime(0);

renderText = (pointText.length === 0 || totalText.indexOf(pointText) === -1) ? totalText.valueAtTime(0) : totalText.valueAtTime(0).split(pointText)[0].replace(/\s+$/, "");

totalText.getStyleAt(0,0).setText(renderText);
//RenderText_Left - Anchor Point

a = thisLayer.sourceRectAtTime(0);

x = a.left + a.width/2;
y = a.top + a.height;

[x, y]
//RenderText_Left - Position

//thisComp.width는 이미 상수입니다. 컴포지션 사이즈는 타임에 따라 변하지 않습니다.
x = thisComp.width*0.1+thisLayer.sourceRectAtTime(0).width/2;
y = transform.position.valueAtTime(0)[1];
[x, y]
//Box - Size

const layerName = thisLayer.name;
const renderTextLayer = thisComp.layer("RenderText_Center");

const wb = thisComp.layer(layerName+"_Control").effect("Width_Blank")("Slider").valueAtTime(0);

const boxWidth = renderTextLayer.text.sourceText.valueAtTime(0).length === 0 ? 0 : renderTextLayer.sourceRectAtTime(0).width + wb;

[boxWidth, 20]
//Box - Fill Color

thisComp.layer("PointText").text.sourceText.getStyleAt(0,0).fillColor.concat(1);
//Box - Rectangle Transform Anchor Point

x = content("Rectangle 1").content("Rectangle Path 1").size.valueAtTime(0)[0]/2;
[-x, 0]
//Box - Anchor Point

a = thisLayer.sourceRectAtTime(0);

x = a.left + a.width/2;
y = a.top + a.height;

[x, y]
//Box - Position

thisComp.layer("RenderText_Center").transform.position.valueAtTime(0);

이렇게 valueAtTime(0), sourceRectAtTime(0), getStyleAt(0,0)을 이용해서 변수로 작동하던 코드를 전부 상수로 바꿔주었습니다.

프리 렌더 속도가 확실히 개선된 것을 느낄 수 있습니다. 이때 Box의 위치가 틀어졌는데 그 이유는 박스 레이어의 앵커 포인트를 모션이 끝나지 않은 0초일 때의 값을 기준으로 코드를 넣었기 때문입니다.

a = thisLayer.sourceRectAtTime(2);

x = a.left + a.width/2;
y = a.top + a.height;

[x, y]

박스 레이어의 앵커 포인트를 모션이 끝난 후인 2초일 때의 값으로 계산합니다. 그럼 박스의 포지션이 제대로 돌아간 것을 확인할 수 있습니다.

확실히 변수로 인식되던 값을 상수로 바꿨을 때 렌더 속도가 빨라진 것을 느낄 수 있었습니다. 하지만 여전히 모션이 전부 끝나고 나서도 RenderText 레이어들이 계산을 하고 있습니다.

어디서 계산을 하고 있는지 확인해보면 텍스트를 불러오는 데 많은 계산을 하고 있는 것을 확인할 수 있습니다.

이제 이 부분을 조금 더 최적화 해보도록 하겠습니다. 최적화를 위해 지난 반응형 박스 자막 만들기 마지막 시간에 이야기 했던 posterizeTime()을 사용하겠습니다. 현재 작업한 레이어들의 경우 값이 변하는 속성은 한 두개 입니다.

즉, 나머지는 변하지 않는 값들을 매 프레임마다 체크를 하고 있는 것이죠. 그렇다면 변하지 않는 값들은 더이상 체크하지 않도록 강제로 프레임 수를 줄여보도록 하겠습니다.

익스프레션이 적용된 모든 코드에 posterizeTime(0)을 적용했습니다. 다음과 같은 형태로 적용합니다.

if (time > 0) {
	posterizeTime(0);
	totalText.getStyleAt(0,0).setText(renderText);
} else {
	totalText.getStyleAt(0,0).setText(renderText);
}
// 익스프레션으로 작업한 코드의 값들은 모두 프레임에 따라 변하지 않는 상수입니다.
// 그래서 처음에 0초일 때만 값을 불러오고, 나머지 프레임에서는 모두 0프레임으로 강제로 프레임을 줄였습니다.
if (time > 0) {
	posterizeTime(0);
	[x, y]
} else {
	[x, y]
}

이렇게 모든 익스프레션 코드를 업데이트하고 확인해본 영상입니다.

어떤가요? 확실히 빨라지지 않았나요? 모션 블러가 적용되어 있는데도 거의 실시간으로 렌더링이 가능해졌습니다.

프리미어에서 확인하기

이 상태로 모션 그래픽 템플릿을 저장한 뒤 프리미어에서 테스트 해보겠습니다. Basic_Test_1 컴포지션을 새로운 컴포지션으로 감싼 뒤 모션이 끝나는 2초 뒤로 posterizeTime을 적용하고 뽑습니다. Region도 지정해주고요.

사실 그럼에도 불구하고 애프터 이펙트가 들어가는 순간 프리미어는 무거워 집니다. 하지만 적어도 작업을 못할 정도는 아니게 되었습니다. 만약 아직도 프리미어에서 느리다면 미디어 캐시를 삭제하고 시도해보세요. 그럼 조금 더 빨라집니다.

모션 그래픽 템플릿을 넣어도 프리미어에서는 애프터 이펙트를 백그라운드로 돌리면서 계산을 하기 때문에 느려지게 되죠.

다이나믹 링크로 애프터 이펙트의 컴포지션을 다이렉트로 연결하는 것과 모션 그래픽 템플릿으로 작업하는 것의 차이는 애프터 이펙트 파일을 외부 폴더에서 불러오느냐 프리미어 프로에서 생성한 Motion Graphics Template Media 폴더에서 불러오느냐의 차이가 생길 뿐 연산 자체는 애프터 이펙트를 백그라운드로 돌려서 하게 됩니다. 그리고 프로젝트 파일 전체를 옮겼을 때 모션 그래픽 템플릿 폴더도 함께 옮기기 때문에 링크가 깨지지 않는다는 차이가 생기게 됩니다.

이번 시간에 작업한 모션 그래픽 템플릿입니다. 필요하신 분들은 다운 받아보세요.

그럼 이만~

P.S 포스팅을 하면서 템플릿들을 하나씩 정리하며 업로드 하는 사이트를 만들고 있습니다. 다운 받으시는 분들이 늘어나고 반응이 괜찮으면 회원 가입을 받고 서로의 자료를 공유하는 식으로 발전시키면 어떨까 생각됩니다. 편하게 구경해보시고 템플릿 다운받아 가세요.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다