AE 脚本入门:产生随机平移的对象
AE 脚本入门:产生随机平移的对象
AE 不仅有表达式,它还是有脚本的。表达式系统算是脚本的一个子集吧,脚本可以实现
下面是一个需求。经过分析发现,还 AE 脚本来得更直接。
需求
给定几十个图片,每个图片要在随机一段时间内,从视口外一个点平移到另一个点,并且经过视口中心位置。
如果在 Unity 等游戏软件做这个效果显然是很简单的。
下面是我的学习记录。
官方脚本手册:https://ae-scripting.docsforadobe.dev/
关于脚本语言
AE 表达式有两种引擎:ExtendScript 和 JavaScript。其中 ExtendScript 只支持到 ES3,语法很垃圾,而 JavaScript 支持到 ES2018。暂时没有发现更改脚本引擎的方法。
ExtendScript 有多拉呢?这玩意不支持常用的 ES6 语法,如 let、箭头函数、字符串内插、解构赋值。如果用 const,运行第二次会提示重复声明。
但用疑似魔法的方式扩展了语法,这样就支持了向量运算(这在 js 环境是不支持的)
[1, 2] * 4; // [4, 8]
[1, 2] + [3, 4]; // [4, 6]
以及 ()
调用(这个其实在 js 可以直接改属性来实现)
// TFAE
mylayer.position;
mylayer("position");
mylayer.property("position");
// TFAE
mylayer(1);
mylayer.property(1);
环境配置
众所周知 VSCode 是第一万能编辑器,肯定有人提供 AE 脚本扩展的。AE 有提供内置编辑器,不过易用性显然不如第三方 IDE,这里不作考虑。
即便没有扩展,VSCode 也足以用于编写并执行脚本。一个官方案例:
afterfx -r c:\script_path\example_script.jsx
(这条疑似有误)其中 AfterFX.exe
位于 Adobe After Effects <VERSION>\Support Files
(Windows)。可以直接命令行执行脚本。
下面使用 VSCode 的扩展。安装 ExtendScript Debugger 和 ExtendScript。第二个说是能提供 intellisense,但是使用时貌似没有提供,可能是打开方式有误?
注意 ExtendScript Debugger 有 V1 和 V2 的区别,网上一些教程用的是 deprecate 的 V1,和 V2 在使用上有较大区别,要按照官网指示做迁移。
这里在 Run and Debug 里点击自动生成 ExtendScript 的 launch.json
,什么都不用改,即可运行。默认如下:
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "extendscript-debug",
"request": "attach",
"name": "Attach to ExtendScript Engine"
},
{
"type": "extendscript-debug",
"request": "launch",
"name": "Launch Script in ExtendScript Engine"
}
]
}
选择带 launch
的那个进入调试。需要启动 AE 并打开项目文件。
然后会让你选择应用,安装的 Adobe 软件都支持。
如果不想每次运行都重复选择可以在 configurations
里加上目标平台(具体名称见之前提示的选项):
{
"hostAppSpecifier": "aftereffects-22.0",
"type": "extendscript-debug",
"request": "launch",
"name": "Launch Script in ExtendScript Engine"
}
入门示例
一个最简单的程序 Hello world
使用 alert
在对话框中输出信息。第 2 个参数为标题。
alert("Hello world!", app.version);
使用 writeLn 在“信息”中输出。
注意在 AE 中下标是从 1 开始的。
writeLn(app.project.item(1))
这里输出两行是因为我运行了两次,它不会自动清除之前输出。
顺便说一个小小的坑点。AE 的 ExtendScript 没有 JSON(因为是 ES3 之后的),输出时也不会把一个 Object 的成员输出出来,这对调试造成很大麻烦。
下面就要接触到更多数据了。
获取图层
var layer = app.project.activeItem.layer(1); // 需要保证当前 item 是 comp
writeLn(app.project.activeItem.numLayers);
writeLn(layer.name);
例:随机分布所有图层。
var comp = app.project.activeItem;
var width = comp.width;
var height = comp.height;
for (var i = 1; i <= comp.numLayers; i++) {
var layer = app.project.activeItem.layer(i);
layer.position.setValue([Math.random() * width, Math.random() * height]);
}
调试
下面我们尝试断点调试。断点暂停后,发现 VARIABLES 里没有显示内容,并且提示:Details: CDATA is not closed.
。
我们可以看看官方脚本是怎么写的。
Details
{
// Sort Layers by In Point.jsx
//
// This script reorders layers in the active comp, sorted by inPoint.
//
function SortLayersByInPoint(thisObj)
{
var proj = app.project;
var scriptName = "Sort Layers by In Point";
function sortByInpoint(comp_layers, unlockedOnly) {
var total_number = comp_layers.length;
while (total_number >= 2) {
var layer_was_moved = false;
for (j = 1; j <= total_number; j++) {
// if you want to reverse the sort order, use "<" instead of ">".
if (comp_layers[j].inPoint > comp_layers[total_number].inPoint) {
if (comp_layers[j].locked) {
if (unlockedOnly==false) {
comp_layers[j].locked = false;
comp_layers[j].moveAfter(comp_layers[total_number]);
comp_layers[total_number].locked = true;
layer_was_moved = true;
}
} else {
comp_layers[j].moveAfter(comp_layers[total_number]);
layer_was_moved = true;
}
}
}
if (!layer_was_moved) {
total_number = total_number-1 ;
}
}
}
// change this to true if you want to leave locked layers untouched.
var unlockedOnly = false;
if (proj) {
var activeItem = app.project.activeItem;
if (activeItem != null && (activeItem instanceof CompItem)) {
app.beginUndoGroup(scriptName);
var activeCompLayers = activeItem.layers;
sortByInpoint(activeCompLayers, unlockedOnly);
app.endUndoGroup();
} else {
alert("Please select an active comp to use this script", scriptName);
}
} else {
alert("Please open a project first to use this script.", scriptName);
}
}
SortLayersByInPoint(this);
}
注意到,它用一个大括号包裹了一个 SortLayersByInPoint
函数,再去调用它。如果我们也这么做,效果如何?
可以看到,在函数内部,可以显示出具体的变量名称。这样调试就不需要用输出的方式来显示变量值了。暂时不清楚最外层的大括号的作用,去掉也能正常调试。
如果把 this
传进去,可以获取全局变量。这里 self
和 Global 内容是一样的。
完成功能
下面,我们正式完成功能。首先是主函数:
function main(self) {
var comp = app.project.activeItem;
app.beginUndoGroup("Random Distribution");
for (var i = 1; i <= comp.numLayers; i++) {
setProperties(comp, comp.layer(i));
}
app.endUndoGroup();
}
这里使用了 demo 中也用到的 beginUndoGroup
和 endUndoGroup
,用于撤销和恢复。
接下来,我们要使用 AE 提供的脚本 api 来完成需求,即完成这个函数
function setProperties(comp, layer) {
// TODO
}
Step 1:设置位置关键帧
根据文档可知,position
是所有 Layer
类都拥有的 property,通过 setValue
来设置值,setValueAtTime
来设置关键帧(时间为合成时间,单位为秒)。position
的值类型是向量,可以是 [x, y]
或 [x, y, z]
,z
缺省为 0。
假设我们用 random_st()
获取了随机的坐标,不难设置它。这里随机生成了 layer 的 in 和 out 时间,在这两点插入关键帧。
顺便还可以设置 scale
,设置方法和 position
几乎一模一样。因为不涉及关键帧,这里仅仅是 setValue
。
var st = random_st(comp.width, comp.height);
var start_pos = st[0], end_pos = st[1];
var start_time = randf(0, 60), end_time = start_time + randf(5, 10);
layer.inPoint = start_time;
layer.outPoint = end_time;
layer.position.setValueAtTime(start_time, start_pos);
layer.position.setValueAtTime(end_time, end_pos);
layer.scale.setValue(randf2(50, 200));
但是这样做存在一个问题。多次操作会增加多余的关键帧,导致插值并不是预期的线性。需要添加一个函数 clearKeys(layer.position)
来清除 position
的所有关键帧。
AE 并没有提供移除所有关键帧的函数,只有移除某一帧的函数。所以我们曲线救国,循环移除第一帧直到没有关键帧剩下。
function clearKeys(property) {
while (property.numKeys > 0) {
property.removeKey(1);
}
}
那么如何实现随机起点和终点呢?我们希望这个物体能在画面中停留足够时间,且在画面外出现和消失,保证良好体验。下面是我提出的一个方法,欢迎改进。
在归一化坐标空间中,画面坐标范围为 。设置边界距离 ,在区域 内随机取点 ,得到经过的画面中间一点。设经过 的线段 端点为 。取倾斜角 。当 时,近似认为 先与 和 相交,再与 和 相交。不难求出
当 时,同样使用上式,但是把 换成 ,再交换所有 和 。
最后将坐标还原到画布空间 得到 。由于我们只考虑了 的范围,还需要以 概率交换起点和终点坐标,得到完整的随机分布。
效果如下(截图暂未设置随机缩放):
Step 2: 设置颜色
AE 的 Layer
(包括派生类)并没有提供图层颜色的选项,只能用 effect 来实现。图片层是一个 AVLayer
,它拥有 effect
成员。layer.effect
为图层的所有效果,其类型为 PropertyGroup
,即属性的集合。
跟之前一样,需要先清除所有效果:
clearEffects(layer);
function clearEffects(layer) {
while (layer.effect.numProperties > 0) {
layer.effect(1).remove();
}
}
这里用 numProperties
获取属性数量(effect 是 Property
),然后循环获取第 1 个 effect 调用其 remove
方法来移除。
使用的 effect 可以是 ADBE Change To Color
(注册名,中文名为 “更改为颜色”
)。需要修改两个属性值:
var effect = layer.effect.addProperty("ADBE Change To Color"); // 更改为颜色
var effectProp1 = effect("ADBE Change To Color-0002"); // 至
effectProp1.setValue(/* Random color */);
var effectProp2 = effect("ADBE Change To Color-0003"); // 更改
effectProp2.setValue(4); // "色相、亮度和饱和度"
这里用注册名或 i18n 名都可以访问。effectProp1
是一个向量类型的属性,表示颜色更改成什么,取值稍后再说。effectProp2
是一个 dropdown 类型的属性,取值为 ,表示选择第几个选项。这里 "色相、亮度和饱和度"
是第 4 个选项,因此填 4。
再说随机生成颜色。这里我们考虑先从 HSL 空间随机生成,再转为 RGB。表达式里有 hslToRgb
全局函数,但是貌似在脚本里没有。于是从网上找了两个来(两个都能用):
function hslToRgb(hsl) {
var h = hsl[0] / 360;
var s = hsl[1] / 100;
var l = hsl[2] / 100;
if (s == 0)
return [Math.round(l * 255), Math.round(l * 255), Math.round(l * 255)];
var rgb = [h + 1 / 3, h, h - 1 / 3];
var q = l >= 0.5 ? (l + s - l * s) : (l * (1 + s));
var p = 2 * l - q;
for (var i = 0; i < rgb.length; i++) {
var tc = rgb[i];
if (tc < 0) {
tc += 1;
} else if (tc > 1) {
tc -= 1;
}
if (tc < (1 / 6)) {
tc = p + (q - p) * 6 * tc;
} else if ((1 / 6) <= tc && tc < 0.5) {
tc = q;
} else if (0.5 <= tc && tc < (2 / 3)) {
tc = p + (q - p) * (4 - 6 * tc);
} else {
tc = p;
}
rgb[i] = Math.round(tc * 255);
}
return rgb;
}
function hslToRgb2(hsl) {
var h = hsl[0];
var s = hsl[1] / 100;
var l = hsl[2] / 100;
var k = function (n) { return (n + h / 30) % 12 };
var a = s * Math.min(l, 1 - l);
var f = function (n) { return l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); }
return [255 * f(0), 255 * f(8), 255 * f(4)];
}
这两个函数接收参数均为 [h, s, l]
格式,取值为 ,;返回值为 [r, g, b]
,取值为 。但是 AE 中的颜色范围是 ,需要对结果归一化。于是有了这样的调用:
effectProp.setValue(hslToRgb([randf(0, 360), randf(50, 90), randf(50, 90)]) / 255);
脚本最终执行结果如下:
到这里我们的任务就完成了。AE 用脚本来做批处理操作还是很容易的,但是因为其支持的 ES 版本太老旧,也没有 typing 支持,写起来麻烦。好在 VSCode 调试能帮大忙。后续会分享其他脚本编写。