基础
在表达式的使用中,随机性是最常被问到的问题之一,After effects 为我们提供了一个非常好的工具库来执行它。基本 random() 方法有几种不同的风格。下面的例子可能有助于理解调用 random() 的不同方式:
Random Expression Result
random() // number between 0 and 1
random(6) // number between 0 and 6
random(-2,4) // number between -2 and 4
random([3,4,5]) // vector between [0,0,0] and [3,4,5]
random([3,4,5],[6,7,8]) // vector between [3,4,5] and [6,7,8]
你可以看到 random() 方法非常灵活。
我们将通过几个示例说明如何使用它。假设你想在合成中随机定位一个图层,下面的任何一个 Position 表达式都可以做到:
random([thisComp.width,thisComp.height])
random([0,0],[thisComp.width,thisComp.height])
x = random(thisComp.width);
y = random(thisComp.height);
[x,y]
x = random()*thisComp.width;
y = random()*thisComp.height;
[x,y]
虽然上面的表达式确实随机地定位了层,但如果您尝试使用它们,就会发现 random() 方法的一个严重限制。每一帧都有不同的结果。如果你想有一个层疯狂地到处跳,那么这是很好的。但大多数情况下,你需要的是更可控的随机性。这就是 seerandom() 发挥作用的地方。
随机种子的补救
seedRandom() 是 Adobe 提出的一个聪明的方法,可以让我们控制随机率。事实上,它有不止一种用途。您还可以使用它来更改由 random() 生成的随机数序列。seedRandom() 有两个参数。第一个是随机种子,第二个是 true/false 标志,告诉 After Effects 由 random() 生成的随机数是否将是 “永恒的” 。我们马上会回到 “永恒” 这个话题,但首先让我们看看另一个参数。在其最简单的形式中,seedRandom 只是种子随机数生成器,以产生在每一帧都会改变的特定随机数序列。
例如,这两个表达式将生成完全不同的随机数序列(每一帧都会改变):
seedRandom(3);
random()
seedRandom(999);
random()
好了,接下来是最酷的部分。通过设置 seedRandom() 的第二个参数为 "true" , random() 生成的数字在每一帧都是相同的。下面是一个 Position 的表达式示例,它可以将一个图层移动到一个随机位置,并将其保持在该位置:
seedRandom(3,true);
random([thisComp.width,thisComp.height])
记住,如果没有调用 seedRandom(),图层将在每一帧上弹到不同的位置。这对我们有什么帮助? 让一个图层移动到一个随机位置并停留在那里是非常无聊的,对吧? 是的。但要想获得一个新位置,我们只需要改变种子。因此,为了让图层以一种有序的方式随机移动,我们所要做的就是定期更新种子,并从 random() 获取一个新的随机位置。
让我们构建一个例子。假设我们想让我们的图层每半秒移动到一个新的随机位置。我们只需要在表达式中添加一些新代码,每半秒就会更改种子。我们怎么做呢? 记住,JavaScript 函数 Math.floor() 将四舍五入到最接近的整数。我们可以使用它每半秒生成改变一次的种子,方法是将当前时间除以 0.5 并舍入到最接近的整数。下面是一个例子(你可以改变 “holdTime” 的值来改变每个位置的持有时间长度):
holdTime = .5; //time to hold each position (seconds)
seed = Math.floor(time/holdTime);
seedRandom(seed,true);
random([thisComp.width, thisComp.height])
现在,这一层仍然在随机移动,但每隔半秒移动一次。让我们做一个修改,让我们的移动停留在合成的 “title safe” 区域,我们将通过限制 random() 调用的结果在合成的宽度和高度的 10% 到 90% 之间来做到这一点。
holdTime = .5; //time to hold each position (seconds)
minVal = [0.1*thisComp.width, 0.1*thisComp.height];
maxVal = [0.9*thisComp.width, 0.9*thisComp.height];
seed = Math.floor(time/holdTime);
seedRandom(seed,true);
random(minVal,maxVal)
效果如图
(请注意,有一种稍微简单一点的方法,即结合使用 posterizeTime() 方法和seedRandom(),但我们将在深入研究如何使用表达式操纵时间时研究它)。
好的,但是如果我们想让图层从一个位置平滑地移动到另一个位置呢? 这让我们想到了生成随机运动的一个最重要的概念,即无论何时将种子设置为特定的数字,random() 生成的随机序列总是相同的。例如,如果我们将种子设置为 1 ,random() 可能会给出 .45633 。如果我们把种子换成 2 ,我们可能得到 .78341 。但如果我们把种子变回 1 ,我们又会得到 .45633 。那么这如何帮助我们产生平滑的随机运动呢? 让我们再看看上面的例子。在前半秒,种子被计算为 0 。因此,对于前半秒的每一帧,调用 random() 都会基于 0 的种子生成相同的随机 Position 。前半秒后,种子变为 1 ,随机位置改变。那么,如果我们通过将种子改变为我们将在下半秒使用的值来 “提前窥视” ,看看下一个位置将是什么,并开始往那里前进,会怎么样呢? 序列将为这半秒计算种子, 用它来得到这半秒的随机位置(这将是我们的起始位置), 对种子的值加 1 , 用它来看看在接下来的半秒随机起始位置将是多少(我们将使用它作为当前半秒片段的结束位置)。
下面是代码:
segDur = .5;// duration of each "segment" of random motion
minVal = [0.1*thisComp.width, 0.1*thisComp.height];
maxVal = [0.9*thisComp.width, 0.9*thisComp.height];
seed = Math.floor(time/segDur);
segStart = seed*segDur;
seedRandom(seed,true);
startVal = random(minVal,maxVal);
seedRandom(seed+1,true);
endVal = random(minVal,maxVal);
ease(time,segStart,segStart + segDur, startVal, endVal);
你会注意到,在演示效果图中,我们现在有多个星星,都在不同的随机位置。它们都有相同的随机位置表达式副本,所以您可能想知道为什么它们有不同的位置。结果是,对于任何给定的随机种子,生成的随机值对于每个层都是唯一的。在内部,After Effects 以某种方式修改种子,以便它在每一层、合成,属性,效果等你会注意到,在演示影片中,我们现在有多个星星,都在不同的随机位置。它们都有相同的随机位置表达式副本,所以您可能想知道为什么它们有不同的位置。结果是,对于任何给定的随机种子,生成的随机值对于每个层都是唯一的。在内部,After Effects以某种方式修改种子,以便它对每一层、合成、属性、效果等都是独特的。在大多数情况下,这是一个非常方便的功能,但在某些情况下,能够强制两个不同的层生成相同的随机数也是很好的。
总之,回顾一下,种子是从时间(time)和片段持续时间(duration)派生出来的,并成为我们的 “段号” ,其中段 0 从 time = 0 开始,段 1 从 time = .5 开始,以此类推。一旦我们有了种子,我们将它作为随机数生成器的种子,并调用 random() 来获取这个片段的起始位置。然后我们给种子 + 1,用新的种子给随机数生成器种子,并再次调用 random() 来获取下一个片段的起始位置(当然,这是当前片段的结束位置)。然后我们使用 ease() 在两个位置之间进行插值,以便层从一个位置到下一个位置。您可以将插值从 ease() 更改为 linear() 以获得不同的效果。
这就是在 After Effects 中生成基本随机运动所需的所有内容。啊,要是生活也这么简单就好了。
如果你复制图层几次并预览合成,你会看到图层移动到不同的位置,但所有图层的移动是同步的。也就是说,它们都在同一时间到达新的目的地。这可能是一个有用的效果,但如果你想要一些更混乱的东西呢? 我们来看看你可以做些什么来解决这个问题。
同步的效果
更混乱的效果
一种解决方案非常简单,并给出了非常有用的结果。另一种解决方案则要复杂得多(在代码和理论上),但它确实为 After Effects 中的随机性打开了无尽的可能性。首先是简单的解决方法。我们可以修改表达式,使每一层使用的持续时间是给定范围内的一个随机数,并且每一层都是不同的。下面是修改后的代码:
segMin = .3; //minimum segment duration
segMax = .7; //maximum segment duration
minVal = [0.1*thisComp.width, 0.1*thisComp.height];
maxVal = [0.9*thisComp.width, 0.9*thisComp.height];
seedRandom(index,true);
segDur = random(segMin, segMax);
seed = Math.floor(time/segDur);
segStart = seed*segDur;
seedRandom(seed,true);
startVal = random(minVal,maxVal);
seedRandom(seed+1,true);
endVal = random(minVal,maxVal);
ease(time,segStart,segStart + segDur, startVal, endVal);
正如您所看到的,与上一个版本相比,惟一的变化是开始部分的代码,该代码使用层的索引 seed 随机数生成器,然后调用 random() 来获取片段持续时间。这个简单的改变可能会给这个组合增加一些混乱。
然而,如果你仔细观察它的运动,你就会开始注意到任何给定层的每次运动总是花费相同的时间。如果你真正想要的是每个动作都是随机的时间呢? 这给我们带来了一个主要的障碍,我们必须想出一个新的、非常强大的概念来绕过它。在我们解决它之前,我们需要做点小插曲。
表达式没有记忆
After Effects 中表达式的实现是无状态的。这里有一个好消息: 这是允许你移动时间标记到合成的任何帧上而 After Effects 可以立即开始显示你的帧,因为它不需要知道任何表达式应用在过去做了什么。所以给定任意的帧,After Effects 只是计算表达式,并将结果应用到没有表达式的属性的值(即属性的原始值加上任何关键帧的效果)。这意味着巨大的影响。举个例子可能会有帮助。
假设你想给每一帧的图层旋转添加一个从 0 到 10 度的随机值。你可能会期待零星的移动,但你也可能期待层的旋转总是在顺时针方向增加。代码似乎很简单。看起来这应该可以做到:
rotation + random(10)
效果如下
当我们查看结果时,很明显 After Effects 并没有从一帧到另一帧累积 random() 函数的效果。在每一帧,它重新从旋转的原始值(即 0 )开始,并加上随机数。这显然不是我们想要或期望的。我们该怎么做?
不幸的是,在这种情况下,我们经常需要做的是通过添加到表达式中的代码来逐帧累加。换句话说,我们的表达式必须从坐标系 0 开始重新计算从那以后发生的所有事情。为了实现这一点,我们必须使用一些 JavaScript 来构造一个 “while” 循环,它每次通过合成中的一个帧,并加上随机数。这个循环的代码看起来有点复杂,但是在您执行了几次之后,您开始对所进行的操作有了一些感觉,它就变得容易了。
下面是代码的样子:
j = 0; //initialize loop counter
accum = 0; //initialize random accumulator
seedRandom(index,true)
while (j < time){
accum += random(10);
j += thisComp.frameDuration;
}
rotation + accum
累加循环的效果
现在我们得到了我们想要的。这是由 "while" 循环完成的,该循环遍历花括号之间的代码,直到变量 "j" 大于当前时间。每次通过循环,j 将增加一帧的时间值(thisComp.frameDuration)。当循环结束时,变量 “accum” 包含为这一帧和之前所有帧生成的所有随机数的和。尽管代码不是非常复杂,但这一切都是有代价的。在每一帧,循环必须重新访问之前的所有帧。当你的合成长度增加时,渲染时间可能会开始停滞,因为表达式在每一帧上有越来越多的事情要做。因此,这种技术的使用存在一些实际限制,但对于大多数情况(简短的合成),它的工作就很好了。事实上,您很快就会看到,这种技术的许多应用程序不需要单帧粒度,而且在渲染时间方面成本更低。我的意思是,你最终将在比单帧更大的时间块中遍历合成,这将进一步扩展该技术的有用性。你会看到。
回到之前的随机运动效果
好的,让我们回到随机运动的随机持续时间的问题。在这个过程中,我们最终将使用我们刚刚研究过的循环计算技术。我们来看看需要做什么。我们希望图层从一个位置平滑地移动到下一个位置,我们希望这发生在一个随机的时间量(在我们将定义的范围内)。有了 “seedRandom()” 和 “while” ,我们就可以处理这个问题了。首先,让我们来看看最终的代码:
segMin = .3; //minimum segment duration
segMax = .7; //maximum segment duration
minVal = [0.1*thisComp.width, 0.1*thisComp.height];
maxVal = [0.9*thisComp.width, 0.9*thisComp.height];
end = 0;
j = 0;
while ( time >= end){
j += 1;
seedRandom(j,true);
start = end;
end += random(segMin,segMax);
}
endVal = random(minVal,maxVal);
seedRandom(j-1,true);
dummy=random(); //this is a throw-away value
startVal = random(minVal,maxVal);
ease(time,start,end,startVal,endVal)
随机持续时间的效果
让我们看看相对于前一个版本发生了什么变化。主要的区别在于 “while” 循环的设置和执行。这里发生了几件事。“while” 循环是决定每个运动 “片段” 长度的地方。段长度为 “segMin” 和 “segMax” 之间的一个随机数(。在本例中是 3 秒和 0.7 秒)。我们将通过遍历合成,从时间 0 开始,将随机片段的长度累加,直到我们的 end 到达当前时间。因此,就我们这里的目的而言,我们不需要每次运行表达式时都重新访问每一帧 —— 我们只需要查看每个片段的开始(平均来说,大约每 0.5 秒左右)。同时,我们还为每个片段递增了 “j” 。这使得 “j” 对于每个片段都是唯一的,并且是 “seedRandom()” 的种子的完美候选。所以当我们退出循环时,我们知道当前片段的开始和结束时间( "start" 和 "end" ),我们有种子("j")用来生成结束时间。在循环外部(在右花括号之后),我们知道随机数生成器仍然被当前段的索引("j")所 seed。所以我们再次调用 random() 来获取这个片段的结束位置。现在,除了起始位置,我们已经有了所需的一切(段开始时间("start")、段结束时间("end")和结束位置("endVal"))。我们可以通过备份到前面的种子,重新播种随机数生成器,丢弃第一个随机数,并检索起始位置(“startVal”)来获得它。现在我们只需要应用与之前相同的 ease() 语句来生成我们的随机持续时间、平滑的随机运动。
从现在开始,一切都变得容易多了,因为这只是一个主题的变化。在下一节中,我们将把这些概念扩展到随机化其他属性。