Random Motion



Let There Be Light

With a little modification, we can apply our random motion expression to a 3D light and generate a nice random flickering. In this case, "segments" are set to last from between 1.0 to 1.5 seconds, and of that time, the light will be flickering for .5 to .8 seconds. Note that during the flicker periods, we have to set the second parameter of seedRandom() back to false so that we will get a different value for the light's intensity on each frame.

random flickering light


segMin = 1.0; //minimum segment duration
segMax = 1.5; //maximum segment duration
flickerDurMin = .5;
flickerDurMax = .8;

end = 0;
j = 0;
while ( time >= end){
  j += 1;
  seedRandom(j,true);
  start = end;
  end += random(segMin,segMax);
}
flickerDur = random(flickerDurMin,flickerDurMax);
if (time > end - flickerDur){
  seedRandom(1,false);
  random(100);
}else{
  100
}

Pushing the Limits

Now let's really jump into the deep end of the pool and look at a much more complex example. First we need to lay out the requirements for this little project. At the beginning, we want each layer to position itself randomly at a grid intersection (which means we can develop our expression for one layer and then duplicate the layer a bunch of times and have the duplicates randomly populate the grid). Then we want to generate a smooth random motion that only occurs in the x or y direction. Furthermore, we want this motion to be directed along a grid. Each time a layer reaches a grid intersection, we want it pause there for a pre-determined length of time. Here's the code for this monster:

random motion within a grid


origin = [10,10]; //upper left hand corner of grid
dimX = 8; //number of columns
dimY = 6; //number of rows
gap = 20; // distance between cells (pixels)
gridRate = 300; //speed (pixels per second)
holdTime = .2; //(seconds)

end = 0;
j = 0;

while (time >= end){
  seedRandom(j,true);
  startX = Math.floor(random(dimX))*gap + origin[0];
  startY = Math.floor(random(dimY))*gap + origin[1];
  j +=1;
  seedRandom(j,true)
  start = end;
  endX = Math.floor(random(dimX))*gap + origin[0];
  endY = Math.floor(random(dimY))*gap + origin[1];
  deltaX = Math.abs(endX - startX);
  deltaY = Math.abs(endY - startY);

  end += (deltaX + deltaY)/gridRate + 2*holdTime;
}

p1 = start + deltaX/gridRate;
p2 = p1 + holdTime;
p3 = p2 + deltaY/gridRate;

if (time < p1){
  ease(time,start,p1,[startX,startY],[endX,startY])
}else if (time < p2){
  [endX,startY]
}else if (time < p3){
  ease(time,p2,p3,[endX,startY],[endX,endY])
}else{
  [endX,endY]
}

Let's take a closer look at this thing. The first block of code defines the parameters that control the structure of the grid and the speed of layer movement. "origin" is the upper-left-hand corner of the grid. "dimY" and "dimX" define the number of rows and columns in the grid. "gap" defines the size of each cell in the grid. "gridRate" is the maximum speed of a layer moving through the grid. "holdTime" defines how long a layer will pause when it reaches the end of its movement in one direction, before heading off in another direction. Each segment will consist of a movement in the x direction, a hold, a movement in the y direction, and another hold. The "while" loop in this expression is a little different than the ones we've looked at so far. In this expression, the segment duration will be determined by the rate of speed of the layer, the distance it has to travel, and how long it will hold at each destination. After the completion of the "while" loop, we have the starting and ending x and y coordinates for this segment. The rest of the code just checks to see if we're in the x movement, the first hold, the y movement, or the second hold phase of the segment and positions the layer accordingly.


Making it 3D

Now let's take a look at what it would take to make this 3D. First let's take a look at the code:

random motion grid in 3D


origin = [10,10,0]; //upper left hand corner of grid
dimX = 8; //number of columns
dimY = 6; //number of rows
dimZ = 6; //number of planes
gap = 20; // distance between cells (pixels)
gridRate = 250; //speed (pixels per second)
holdTime = .2; //(seconds)

end = 0;
j = 0;

while (time >= end){
  seedRandom(j,true);
  startX = Math.floor(random(dimX))*gap + origin[0];
  startY = Math.floor(random(dimY))*gap + origin[1];
  startZ = Math.floor(random(dimZ))*gap + origin[2];
  j +=1;
  seedRandom(j,true)
  start = end;
  endX = Math.floor(random(dimX))*gap + origin[0];
  endY = Math.floor(random(dimY))*gap + origin[1];
  endZ = Math.floor(random(dimZ))*gap + origin[2];
  deltaX = Math.abs(endX - startX);
  deltaY = Math.abs(endY - startY);
  deltaZ = Math.abs(endZ - startZ);

  end += (deltaX + deltaY + deltaZ)/gridRate + 3*holdTime;
}

p1 = start + deltaX/gridRate;
p2 = p1 + holdTime;
p3 = p2 + deltaY/gridRate;
p4 = p3 + holdTime;
p5 = p4 + deltaZ/gridRate;

if (time < p1){
  ease(time,start,p1,[startX,startY,startZ],[endX,startY,startZ])
}else if (time < p2){
  [endX,startY,startZ]
}else if (time < p3){
  ease(time,p2,p3,[endX,startY,startZ],[endX,endY,startZ])
}else if (time < p4){
  [endX,endY,startZ]
}else if (time < p5){
  ease(time,p4,p5,[endX,endY,startZ],[endX,endY,endZ])
}else{
  [endX,endY,endZ]
}

You'll notice that the 3D code has exactly the same structure as the 2D code - it's just been expanded to accommodate the z dimension.


Only a Matter of Time

Let's move from spatial randomness to randomness in time. We're going to look at some examples of how you can create some interesting time effects by applying random expressions to the Time Remapping property of a layer. In this first example, we'll start with the basic random expression that we were using for random motion before we introduced the concept of random durations. So what we're shooting for here is an expression that, every segment duration (.5 seconds in this case) a new random target time is selected. Then Time Remapping will interpolate to the target time over the duration of the segment. So the effect will be time randomly sliding back and forth. Only two minor modifications have been made to the original code: "minVal" has been changed to the layer's In Point, and "maxVal" has been changed to the layer's Out Point minus the segment duration (so we don't slide off the end of the footage.)

random time remapping


segDur = .5;// duration of each "segment" of random time movement
minVal = inPoint;
maxVal = outPoint - segDur;

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);

With just a minor modification, we can change the code to generate random "jump cuts". In this example, every .5 seconds time will jump to a random point between "minVal" and "maxVal" and play normally from there. Here's the code:

random jump cuts


segDur = .5;// duration of each "segment" of random time
minVal = inPoint;
maxVal = outPoint - segDur;

seed = Math.floor(time/segDur);
segStart = seed*segDur;
seedRandom(seed,true);
startVal =  random(minVal,maxVal);
endVal = startVal + segDur;
linear(time,segStart,segStart + segDur, startVal, endVal);

By simply removing a few lines of code from the previous example, we can turn it into a random stills generator. Every .5 seconds, time will be randomly remapped and held there for the duration of the segment. Here's the modified code:

random stills


segDur = .5;// duration of each "segment" of random time
minVal = inPoint;
maxVal = outPoint - segDur;

seed = Math.floor(time/segDur);
seedRandom(seed,true);
random(minVal,maxVal);

Now we can turn this thing into an automated random still grid by scaling our layer down, adding the following Position code, and duplicating the layer as many times as necessary to populate the grid. Each new layer will move itself into position automatically. In this example, I set the Scale to 45% and set the number of rows and columns to 2 to generate a 2x2 grid of random stills. Here's the code for the Position expression:

random still grid


numRows = 2;
numCols = 2;

w = width*scale[0]/100;
h = height*scale[1]/100;
horizGap = (thisComp.width- w*numRows)/(numRows + 1);
vertGap = (thisComp.height - h*numCols)/(numCols + 1);
j = index - 1;
col = j%numCols;
row = Math.floor(j/numCols);
x = (col + .5)*w + (col + 1)*horizGap;
y = (row + .5)*h + (row + 1)*vertGap;
[x,y]


Plenty of Wiggle Room

The After Effects implementation of expressions has a wonderful built-in random motion generator - the wiggle() method. For a lot of applications, wiggle() provides all the randomness you will need. It's similar in concept to the wiggler keyframe assistant but you use wiggle() inside an expression. wiggle() takes five parameters, but for our purposes, we're only going to use the first two. The first parameter is frequency (in wiggles per second) and the second parameter is amplitude (in the units of the property being wiggled). If you're wiggling a Position property, for example, amplitude is in pixels. If you're wiggling a Rotation property, amplitude is in degrees, etc. For our first example, a solid layer has been keyframed to move from left to right across the middle of the comp. This expression has been added to the Position property:

wiggle in x and y directions


wiggle(5,25)

The expression is telling After Effects to wiggle the Position property 5 times a second with value between -25 and 25 pixels. Wiggle takes on the dimensions of the property it's applied to, so in our case it is wiggling both x and y (independently).

If we only want y to wiggle, we have to change our expression a little. We want the non-wiggled value of x and the wiggled value of y. Here's the expression that will do that:

wiggle only in y direction


[position[0],wiggle(5,25)[1]]

The above expression gets the x coordinate from the keyframe-generated value of Position and the y coordinate from the wiggle-generated value.

Just for fun, let's apply a wiggle expression to the Brush Position parameter of the Write-on effect. We'll use the y-only version of the expression and keyframe the Brush Position to move from left to right across the comp. Here's the expression:


wiggling the write-on effect


[value[0],wiggle(5,70)[1]]

Let's take a look at one final example of the wiggle() method where we'll use it to create a "swarming" behavior. In this example we'll create a null layer named "master" that we will move in a circle. Each of our "swarming" layers will have the following expression applied to its Position property. Note that we have to subtract out the layer's Position because it is included in the value supplied by wiggle() (we just want the "wiggle" part, which we will add to the Position of "master").


using wiggle to create swarming behavior


masterPos = thisComp.layer("master").position;
masterPos + wiggle(4,25) - position

Over the Top

OK - one last monster set of expressions to look at before we wrap up this random motion section. The concept behind this one is easy - it's the implementation that's a little messy. Say we have an image that's quite a bit larger than our comp. We want to apply an expression that will let us randomly pan and scan the image - zoom in, zoom out, move the camera, etc. The trick is that we don't ever want to show the area outside the borders of the image. That means the scaling and positioning have to be coordinated. In this case we'll make the Position expression constrained by the Scale expression. That means that whatever random scale is selected at any given time will constrain the range of Position values to those that won't show the area beyond the borders of the image. To accomplish this, the Position expression will need to know the start and end Scale values for the current segment. We'll actually do the calculation for the random Scale start and end values in a Point Control so that both values will be available to both the Position and Scale expressions. In case you're not familiar with what a Point Control is - it's one of the Expression Controls that was added in AE 5.5 and you'll find it in the Expression Controls section of the Effect menu. The reason we use a Point Control is that it is a two-element array so we can stick both the start and end Scale values in there. Without the Point Control, the Position expression would only have access to the current Scale value. Here's the expression for the Point Control:

random pan and scan


scaleTime = 1;
holdTime = 0.5;
maxScale = 200;

widthRatio = thisComp.width/width;
heightRatio = thisComp.height/height;
minScale = Math.max(widthRatio,heightRatio)*100;
totalTime = scaleTime + holdTime;
seed = Math.floor(time/totalTime) + 2;
seedRandom(seed,true);
newScale = random(minScale,maxScale);
seedRandom(seed - 1,true);
oldScale = random(minScale,maxScale);
[oldScale,newScale];

Here's the code for the Position expression:


scaleTime = 1;
holdTime = 0.5;

pixFactor = source.pixelAspect/thisComp.pixelAspect;
totalTime = scaleTime + holdTime;
segTime = time%totalTime;
oldScale = effect("Point Control").param("Point")[0];
newScale = effect("Point Control").param("Point")[1];
seed = Math.floor(time/totalTime)+13;
seedRandom(seed,true);
maxX = (width/2)*(newScale/100)*pixFactor;
minX = thisComp.width-(width/2)*(newScale/100)*pixFactor;
maxY = (height/2)*(newScale/100);
minY = thisComp.height-(height/2)*(newScale/100);
newPos = random([minX,minY],[maxX,maxY]);
seedRandom(seed-1,true);
maxX = (width/2)*(oldScale/100)*pixFactor;
minX = thisComp.width-(width/2)*(oldScale/100)*pixFactor;
maxY = (height/2)*(oldScale/100);
minY = thisComp.height-(height/2)*(oldScale/100);
oldPos = random([minX,minY],[maxX,maxY]);
if(segTime  >  scaleTime){
  newPos
}else{
  ease(segTime,0,scaleTime,oldPos,newPos)
}

And finally, the code for the Scale expression:


scaleTime = 1;
holdTime = 0.5;

totalTime = scaleTime + holdTime;
segTime = time%totalTime;
oldScale = effect("Point Control").param("Point")[0];
newScale = effect("Point Control").param("Point")[1];
if(segTime > scaleTime){
  [newScale,newScale]
}else{
  ease(segTime,0,scaleTime,[oldScale,oldScale],[newScale,newScale])
}

The code is bulky but not too complicated. The Position expression is complicated a little by the code necessary to detect and compensate for a mismatch between pixel aspect ratios of the comp and the image (wherever you see "pixFactor" in the expression.)

Thankfully, if you need a set of expressions like this, you can just copy and paste these.




previous next