EXPRESSIONMask Expression을 활용한 순차적인 애니메이션 제작

motionlab
조회수 299


이번 시간에는 Aescript.com에서 제공하는 튜토리얼 중에서 Mask를 활용한 Expression에 대해 알아보겠습니다. 여러가지 오브젝트들을 Mask의 Path를 통해 리액션하는 애니메이션을 간단하게 만들어보겠습니다.


1. 준비
01. Mask를 이용한 리액션을 애니메이션하기 위해 Layer가 분리된 Illustrator 및 Photoshop 파일을 준비하겠습니다.


02. Mask Expression은 예전에 나온 ‘The Power of Expression’이라는 책에 나오는 내용 중에 하나입니다.


03. 앞서 만든 Layer들이 나뉘어져 있는 파일들을 불러들입니다. Composition - Retain Layer Sizes로 설정한 다음에 불러들입니다.


04. Layer의 차지하는 영역만큼 Layer가 깔끔하게 불러들여졌습니다.


05. 모든 잎의 Layer 중심축을 줄기 부분으로 이동시킵니다. 이렇게 하는 이유는 잎의 크기를 조절하여 커지게 만들기 위해서인데, 줄기 부분부터 커져야 자연스럽게 보이기 때문입니다.


2. Mask Expression 적용
01. MASK라는 Solid Layer를 하나 만듭니다. 여기에서 중요한 것은 Layer의 이름을 반드시 MASK로 해야 한다는 것입니다. 그 이유는 적용할 Expression에 Layer의 이름이 Mask로 이미 설정되어 있기 때문입니다.


02. 방금 만든 MASK Layer에 Mask를 하나 만들어줍니다.


03. 이제 잎들의 Scale을 0-100%로 1초 정도 애니메이션을 시켜줍니다.


04. 이제 가장 중요한 Expression 셋팅을 해야 합니다. File > Project Settings에 들어갑니다. 여기에서 Expressions의 Engine을 JavaScript로 반드시 바꿔줘야 합니다. 사용하고자 하는 Script는 2019 버전 이후부터 작용합니다. After Effects의 Expression도 조금씩 바뀌기 때문에 그렇습니다.


05. 이제 앞서 애니메이션을 한 잎들의 Scale 부분에 Expression을 적용해보겠습니다. 

https://aescripts.com/learn/how-to-make-layers-react-to-a-mask-in-after-effects/ 에 들어가면 Expression이 있고, 이를 Copy하여 Paste하면 됩니다.


아래 텍스트를 COPY 하셔도 됩니다.


// Make Layers React to a Mask
// Based on knowledge from The Power of Expression Book
// https://aescripts.com/the-power-of-expression/
//
// Requires the new 'Javascript' expressions engine introduced in AE 16.0 (CC2019)
// You can set this in File -> Project Settings: https://drop.aescripts.com/NQurXWmG
// If you replace all "let" to "var" this expression will should work in previous versions
//
// v1 initial version
// v2 added feathering support
// v3 added parenting support
// v4 added bezier support

const numDivisions = 5; // Adjust this number for more or less precision
const maskLayer = thisComp.layer("MASK"); // Replace "MASK" with the name of your layer
const maskName = "Mask 1"; // Change this to the name of your mask

const maskPath = maskLayer.mask(maskName).maskPath;
const rawMaskPoints = maskPath.points();
const inTangents = maskPath.inTangents();
const outTangents = maskPath.outTangents();
const isMaskClosed = maskPath.isClosed();
const maskFeather = maskLayer.mask(maskName).maskFeather[0];
const fallOffSquared = Math.pow(maskFeather, 2);

function needsSubdivision(c1, c2) { const tangentThreshold = 0.1; return (length(c1) > tangentThreshold || length(c2) > tangentThreshold);
}

function bezier(t, p1, c1, c2, p2) { var u = 1 - t, tt = t * t, uu = u * u, uuu = uu * u, ttt = tt * t; return [ uuu * p1[0] + 3 * uu * t * c1[0] + 3 * u * tt * c2[0] + ttt * p2[0], uuu * p1[1] + 3 * uu * t * c1[1] + 3 * u * tt * c2[1] + ttt * p2[1] ];
}

function subdivideBezierSegment(p1, c1, c2, p2, numDivisions) { var newPoints = []; for (var i = 0; i <= numDivisions; i++) { var t = i / numDivisions; newPoints.push(bezier(t, p1, c1, c2, p2)); } return newPoints;
}

function transformMaskPoints(layer, pathPoints, inTangents, outTangents, isClosed) { var allPoints = []; var count = isClosed ? pathPoints.length : pathPoints.length - 1;
for (var i = 0; i < count; i++) { var nextIndex = (i + 1) % pathPoints.length; var c1 = pathPoints[i] + outTangents[i]; var c2 = pathPoints[nextIndex] + inTangents[nextIndex];
if (needsSubdivision(outTangents[i], inTangents[nextIndex])) { allPoints = allPoints.concat(subdivideBezierSegment(pathPoints[i], c1, c2, pathPoints[nextIndex], numDivisions)); } else { allPoints.push(pathPoints[i], pathPoints[nextIndex]); } }
return allPoints.map(pt => layer.toComp(pt));
}

const transformedMaskPoints = transformMaskPoints(maskLayer, rawMaskPoints, inTangents, outTangents, isMaskClosed);

function inside(point, path) {
let [minX, maxX, minY, maxY] = [Infinity, -Infinity, Infinity, -Infinity]; for (let i = 0; i < path.length; i++) { minX = Math.min(minX, path[i][0]); maxX = Math.max(maxX, path[i][0]); minY = Math.min(minY, path[i][1]); maxY = Math.max(maxY, path[i][1]); } if (point[0] < minX || point[0] > maxX || point[1] < minY || point[1] > maxY) { return false; }
let inside = false; for (let i = 0, j = path.length - 1; i < path.length; j = i++) { let xi = path[i][0], yi = path[i][1]; let xj = path[j][0], yj = path[j][1]; let intersect = ((yi > point[1]) != (yj > point[1])) && (point[0] < (xj - xi) * (point[1] - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside;
}

function distanceToLineSquared(point, a, b) { let lineLengthSquared = Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2); if (lineLengthSquared == 0) return Math.pow(point[0] - a[0], 2) + Math.pow(point[1] - a[1], 2); let t = ((point[0] - a[0]) * (b[0] - a[0]) + (point[1] - a[1]) * (b[1] - a[1])) / lineLengthSquared; t = Math.max(0, Math.min(1, t)); return Math.pow(point[0] - (a[0] + t * (b[0] - a[0])), 2) + Math.pow(point[1] - (a[1] + t * (b[1] - a[1])), 2);
}

function closestDistanceSquared(point, path) { let closestDistSquared = Infinity; for (let i = 0; i < path.length - 1; i++) { let distSquared = distanceToLineSquared(point, path[i], path[i + 1]); if (distSquared < closestDistSquared) closestDistSquared = distSquared; } return closestDistSquared;
}

const anchorPoint = thisLayer.transform.anchorPoint;
const toCompAnchor = thisLayer.toComp([anchorPoint[0], anchorPoint[1]]);
let effectValue = thisProperty.key(1).value;
let closestDistSquared = closestDistanceSquared(toCompAnchor, transformedMaskPoints);

if (isMaskClosed) { let isInside = inside(toCompAnchor, transformedMaskPoints); if (isInside) { effectValue = thisProperty.key(2).value; } else if (maskFeather > 0 && closestDistSquared <= fallOffSquared) { let normalizedDistance = Math.sqrt(closestDistSquared) / maskFeather; effectValue = linear(normalizedDistance, 0, 1, thisProperty.key(2).value, thisProperty.key(1).value); }
} else { if (maskFeather > 0 && closestDistSquared <= fallOffSquared) { let normalizedDistance = Math.sqrt(closestDistSquared) / maskFeather; effectValue = linear(normalizedDistance, 0, 1, thisProperty.key(2).value, thisProperty.key(1).value); }
}
effectValue;



06. 일단 하나의 Layer Expression을 적용한 다음, 마우스 오른쪽 키를 눌러서 Copy Expression Only를 선택합니다.


07. 나머지 잎들을 선택하고 Ctrl+V 하면 됩니다.



3. Mask Reacting 애니메이션
01. 이제 Mask Path를 애니메이션 시키겠습니다. Mask Path가 처음에는 나뭇잎의 밑 부분에 있다가 점점 화면 위로 올라오는 애니메이션을 만듭니다.


02. Mask 영역이 잎들과 만나는 부분에서 잎들이 생성되는 것을 볼 수 있을 것입니다.


03. Mask 애니메이션을 시켜보면 확인할 수 있습니다. Mask Feather 값을 100-200 정도 설정하면 좀 더 자연스럽게 잎이 자라나는 듯한 느낌을 받을 수 있습니다.


04. 이제 적용시킨 Expression을 확인해보겠습니다. let maskToUse = thisComp.layer("MASK") .mask("Mask 1") .maskPath.points(); 이렇게 Mask를 언급한 부분이 2번 정도 있습니다. Mask Layer를 지정하고, 거기에 만들어진 Mask1이라는 Path를 지정한 것입니다. 이제 이것을 응용해보겠습니다. 일단 Mask Layer를 하나 더 복제하여 MASK2로 이름을 바꿔줍니다. 거기에 있는 Path도 Mask2로 이름을 바꿔줍니다.


05. 그런 다음에 몇 개의 잎들만 선택하여 방금 바꾼 Expression을 적용합니다. 다시 말해, MASK Layer를 2개 만들어 시간차를 두고 애니메이션을 시키기 위해서 입니다.


06. 애니메이션을 확인해보면, 각각의 잎들이 약간의 시간차를 두고 생성되는 애니메이션을 볼 수 있습니다.


07. 이 모든 것이 끝났다면, 이제 뻣뻣한 상태에서 잎들이 생성되는 애니메이션이 아닌, 자연스러운 움직임을 가지면서 생성되는 애니메이션으로 바꿔보겠습니다. 앞서 만든 Comp의 중심축을 맨 하단으로 이동한 다음에 Rotation에 wiggle(1.5,2)를 입력하면, 살짝 흔들리는 애니메이션이 되면서 잎들이 생성되는 애니메이션을 얻을 수 있을 것입니다.






4. 생성하는 문양
01. 이번에는 문양이 생성되는 애니메니션을 만들어보겠습니다. Illustrator에서 만든 파일을 불러들입니다. 각각의 오브젝트들을 Layer로 미리 나눠 놓은 상태입니다. 각 잎의 중심축을 그림처럼 잎의 하단 부분으로 이동시켜줍니다.


02. 가장 길이가 긴 라인을 먼저 애니메이션 시켜보겠습니다. 이것 같은 경우는 다른 방식으로 애니메이션을 시켜보겠습니다.


03. RAMP라는 Solid Layer를 하나 만든 후에 Gradient Ramp 이펙트를 적용합니다. 그림처럼 왼쪽 부분이 흰색, 오른쪽이 검은색으로 그라데이션이 되도록 만듭니다. 그런 다음에 Layer의 눈을 꺼도 됩니다.


04. 이제 가장 긴 라인 Layer에 Gradient Wipe 이펙트를 적용합니다. Gradient Layer를 앞서 만든 RAMP Layer로 설정합니다. 설정 후에 옆에 있는 팝업 메뉴에서 Effects&Masks를 설정해줍니다.


05.애니메이션을 시켜보면 보시는 것처럼 오른쪽으로 자연스럽게 생성되는 애니메이션이 만들어집니다.


06. 앞서 중심축을 모두 이동시킨 잎들을 모두 선택하여 Scale 키프레임 애니메이션을 만듭니다. 0%-100%로 키프레임을 만듭니다. 그런 다음에 좀 더 자연스럽게 잎들이 생성되는 애니메이션을 위해서 Opacity 값도 애니메이션을 시켜주겠습니다.


07. 좀 더 자연스럽게 생성되는 애니메이션을 볼 수 있습니다.


08. 여기에서 문제가 생길 수도 있는데, 투명도를 조절해도 맨 마지막에 생성되는 잎 같은 경우는 줄기 부분의 투명도와 맞지 않을 수 있습니다.


09. 그런 경우에는 해당 Layer 바를 조금 더 오른쪽으로 이동시켜서 좀 더 늦게 생성되도록 만들면 됩니다.


10. 위와 같은 개념으로 각 잎의 Layer들을 좀 더 순차적으로 생성되게 하기 위해 Layer 바의 시작을 그림처럼 다르게 만들면 됩니다.


11. 잎이 생성될 때, 움직이지 않고 너무 뻣뻣한 상태로 생성되는 것이 부자연스럽다고 느껴진다면, 여기에 Expression을 추가하면 됩니다. 잎 하나의 Rotation 값에 Motion2Script > EXCITE를 적용하면 자연스럽게 흔들리는 애니메이션이 가능합니다.


12. 모든 작업이 끝났다면 앞서 만든 Comp Layer를 다른 Composition에 넣고 복제합니다. 복제한 Layer의 Scale 값 중에서 X축 값에 -(마이너스) 값을 주면, 그림처럼 대칭 형태를 만들 수 있습니다.