Articles Archive
Articles Search
Director Wiki
 

Simulating turbulent particle behavior with Lingo

December 14, 1999
by Glenn Mitchell

A sample movie is available for download in Mac or PC format

The beauty and hypnotic effects of turbulent systems are everywhere. The bubbles that form in a gin and tonic, a flurry of snow blown past the window and the smoke from a cigarette as it curls above the ashtray. Damn you mother nature! Two days into a much anticipated two-week snowboarding holiday, I now find myself with a broken collarbone and plenty of spare time to finally write this article.

While still a very long way from creating an immersive environment, the technique described here may help bring a small glimpse of (pseudo) real world behaviors onto the screen.

This article describes how to simulate a turbulent response in a group of sprites. To relieve stress on both the CPU and the developer's brain, I've left the textbook I found in the bookshelf and recreated all of the mathematics, not to accurately create a turbulent system but simply to make it all look nice.

Producing the turbulence effect can be broken down into three main calculations:

  1. Tracking the mouse location and calculating the direction of movement and velocity.
  2. Calculating the distance of each sprite from the mouse location - this will determine the degree to which the sprite is affected. The further away the mouse movement, the smaller the influence on the sprite.
  3. Using the results from the above, we calculate the new position of the sprite. To introduce a swirling "turbulent" behavior, an imparted spin is added. The swirl is created by the mouse passing to one side of a particle and an 'angular velocity' is induced, the radius of the swirl is proportional to the angular velocity, increasing as the spin is imparted and collapsing back again as the particle slows.

Okay, that's basically it, let's take a look at the code...

Before we get to steps 1, 2 or 3 from above, there's a whole bunch of variables to declare and initialize. I'll do this in the script channel when entering the first frame.

-- declare and set variables
on enterFrame
  global mlocx, mlocy  -- old mouse location data 
  global mlocxnew, mlocynew  -- new mouse location data 
  global xvel, yvel, vel  -- mouse velocity data
  global xo, yo  -- original x,y position of the sprite   
  global x, y  -- current x,y position of the sprite 
  global a  -- frictional coefficient 
  global rn  -- radius of the 'swirl' 
  global dx, dy  -- linear displacement of sprite 
  global dxt, dyt  -- total displacement of sprite 
  global angn  -- the current angle within the 'swirl' 
  global angdelt  -- the radial velocity of the 'swirl' 
  global rot  -- the rotational influence on the sprite
  -- declare arrays
  put [] into angn 
  put [] into xo 
  put [] into yo 
  put [] into x 
  put [] into y 
  put [] into rn 
  put [] into dxt 
  put [] into dyt 
  put [] into dx 
  put [] into dy 
  put [] into angdelt 
  put [] into rot
  repeat with count = 1 to 110 
  
  -- loop through channel numbers to setup 
  -- arrays of sprite values
    setAt angn, count, 0 
    setAt rn, count, 0 
    setAt dxt, count, 0 
    setAt dyt, count, 0 
    setAt dx, count, 0.0001 
    setAt dy, count, 0.0001 
    setAt angdelt, count, 0 
    setAt rot, count, 0
    -- set the initial coordinates to the sprite 
    -- locations in frame 1
    setAt xo, count, the loch of sprite count 
    setAt yo, count, the locv of sprite count 
    setAt x, count, the loch of sprite count 
    setAt y, count, the locv of sprite count
  end repeat
  put 0.0001 into xvel 
  put 0.0001 into yvel 
  put 0.0001 into vel 
  put the mouseh into mlocxnew 
  put the mousev into mlocynew 
  put 0.95 into a
  put the mouseh into mlocx 
  put the mousev into mlocy
end

Tracking the mouse movements

Knowing the previous and current mouse locations, we can calculate the x, y and total velocities of the mouse.

Firstly we move the 'current' mouse location data (mlocxnew,mlocynew) into the 'previous' mouse location variable(mlocx,mlocy)as we are about to get the updated mouse location (mouseh,mousev)and store it in (mlocxnew,mlocynew).

Using Pythagoras, we know that the hypotenuse in a right-angled triangle is equal to the squareroot of the sum of the other two sides squared.

power(vel,2)=(power(xvel,2) + power(yvel,2))

Taking the squareroot of both sides gives us the following.

vel = sqrt(power(xvel,2) + power(yvel,2))

where: xvel = mlocxnew - mlocx and yvel = mlocynew - mlocy

vel = sqrt(power(xvel,2) + power(yvel,2))

Therefore:

put (mlocxnew - mlocx) into xvel 
put (mlocynew - mlocy) into yvel 
put sqrt(power(xvel,2) + power(yvel,2)) into vel

For simplicity I'm doing all this in a frame script 'on exitFrame' and looping here with a 'go the frame'

When you have other things happening in the score, you can attach this as a behavior to one of the sprites and just stretch it along the score with the other particle sprites.

If you are doing this and you need to wait in a particular frame, the 'exitFrame' will not get passed during the wait, use a looping 'go the frame' instead. Also watch out for transitions and other places where the playback head will stop or slow.

-- calculate the mouse velocity
on exitFrame
  global mlocx, mlocy, mlocxnew, mlocynew, xvel, yvel, vel
  -- Put the old mouse position into mloc[x,y] 
  put mlocxnew into mlocx 
  put mlocynew into mlocy
  -- Put the new mouse position into mloc[x,y]new 
  put the mouseh into mlocxnew 
  put the mousev into mlocynew
  -- Put the distance traveled by the mouse on 
  -- each axis into [x,y]vel 
  put (mlocxnew - mlocx) into xvel 
  put (mlocynew - mlocy) into yvel
  -- Put the distance as a straight line into 
  -- vel (pythagoras theorem)   
  put sqrt(power(xvel,2) + power(yvel,2)) into vel
  go the frame
end

Okay, the following script is attached to each of the particles and will calculate how the location of the sprite is affected by the mouse movement. This is done using an 'on exitFrame'.

Calculating the sprite movement

Calculation of infOnSprite

Using Pythagoras again, we first calculate 'infOnSprite'; this is the distance of the sprite from the mouse and is used to adjust the degree to which the sprite is affected by the mouse velocity. (As the mouse gets further away from the sprite, the influence it has on the sprite is decreased.)

power(infOnSprite,2)=(power(a,2) + power(b,2))

Taking the squareroot of both sides gives us the following.

infOnSprite = sqrt(power(a,2) + power(b,2))

where: a = x - mlocxnew and b = y - mlocynew

vel = sqrt(power(a,2) + power(b,2))

Therefore:

set infOnSprite to ((sqrt(power((getAt(x,spnum)- ¬
  mlocxnew),2)  + power((mlocynew - ¬
  getAt(y,spnum)),2))+5))

Note:

'infOnSprite' is increased by adding 5 in this equation. This prevents the effect on the sprite from becoming too great; remember, these are inversely proportional and if 'infOnSprite' is allowed to get too small the imparted effect on the sprite will be too great.

'spnum' is the sprite number of the particle and is used to index the arrays.

Calculation of Linear Displacement

The basic effect of the mouse on a sprite is to impart a linear velocity'dx,dy' that is in the same direction as the mouse velocity'xvel,yvel' but reduced based on the 'infOnSprite'value. This linear velocity is cumulative, that is the velocity vectors 'dx,dy'of a sprite are continually added to. If this were not the case, the sprites would only move when the mouse was moving and only in that same direction. We need to simulate inertia.

On the other hand, having the sprites being added to continually would leave the sprites whizzing around, and this would appear unnatural (unless you're in a frictionless environment) so we incorporate a frictional coefficient 'a' which will gradually slow the sprites. (This was declared in the initial variable declarations as 0.95)

Therefore:

setAt dx,spnum, ((0.5*xvel/infOnSprite)+(getAt(dx,spnum)*a)) 
setAt dy,spnum, ((0.5*yvel/infOnSprite)+(getAt(dy,spnum)*a))

Note:

I have multiplied'xvel'and'xvel'by 0.5 (effectively halving the calculated displacement; this was just a personal choice as I felt that this displacement was initially too great).

The displacement velocities 'dx,dy' are at a rate "per frame".

It is only the previously calculated velocities 'dx,dy' that are multiplied by frictional coefficient 'a'.

Calculation of Rotational Effect

The linear displacement is pretty cool, but to get the swirling effect, we also need to calculate the degree of spin that is imparted on the sprite.

The rate of rotation or 'angular velocity' in another cumulative value, continually added to or subtracted from as the influence changes. It too is gradually decreased by the frictional co-efficient'a'.

As the angular velocity increases, the radius is increased causing the sprite to spiral out from the point of origin and then collapse back in again as the rate decays. The calculated angular velocity 'rot' is also inversely proportional to 'infOnSprite', decreasing as the distance from the mouse increases.

To calculate the direction of the imparted spin, firstly we determine whether the mouse is above or below the sprite (mlocy < or > y).

If the mouse is above the sprite (mlocy < y) then '(0.2/infOnSprite)*vel' is used, if the mouse is below the sprite then the value will be negative '(-0.2/infOnSprite)*vel'

(Once again, the 0.2 is a personal choice for the degree of effect the mouse has).

This is fine if the mouse is moving to the left (mlocxnew < mlocx) but if the mouse is moving to the right, then 'rot' needs to be negated. (-1 *getAt (rot,spnum))

'rot' is the new effect on the angular velocity; it is now added to the compound angular velocity'angdelt'. As with the linear velocities, these angular velocities are also in a "per frame" timebase.

setAt angdelt, spnum, (getAt (angdelt, spnum) + ¬
  getAt (rot, spnum))

The actual angle within the rotation is kept in variable 'angn', this is now updated by adding the updated radial velocity 'angdelt'.

setAt angn, spnum, (getAt (angdelt, spnum) + ¬
  getAt (angn, spnum))

The radius of the swirl 'rn'is now calculated, it is directly proportional to the angular velocity 'angdelt'.

setAt rn, spnum, (getAt (angdelt, spnum) * 30)

(again, the factor of 30 is a personal choice)

If the radius is less than 1 pixel then we can set the rotational variables to zero as they're not really doing much at all.

if abs(getAt (rn, spnum)) < 1 then     
  setAt angn, spnum, 0     
  setAt angdelt, spnum, 0
end if

As mentioned, the angular velocity 'angdelt' is also decayed by the frictional coefficient 'a', we'll do that now.

setAt angdelt, spnum, (getAt (angdelt, spnum) * a)

Adding it all up

Well, we've now got new values for the linear displacement and the new angle and radius for the simulated swirl. We can put it all together.

Firstly we add displacement 'dx,dy' to 'dxt,dyt' (the total linear displacement from the sprite's origin)

setAt dxt, spnum, (getAt (dxt, spnum) + getAt (dx, spnum)) 
setAt dyt, spnum, (getAt (dyt, spnum) + getAt (dy, spnum))

To calculate the x and y offsets from the swirl, we use sine and cosine functions of the angle multiplied by the radius.

The y offset due to the 'swirl'

(cos(getAt (angn, spnum)) * getAt (rn, spnum)

The x offset due to the 'swirl'

(sin(getAt (angn, spnum)) * getAt (rn, spnum)

The total linear displacements from the sprite's origin 'dxt,dyt' are now added to the sprites origin 'xo,yo' and also the x,y offsets from the swirl.

setAt y, spnum, ((cos(getAt(angn, spnum)) * ¬
  getAt(rn, spnum)) +getAt(yo,  spnum) +getAt(dyt, spnum) *a) 
setAt x, spnum, ((sin(getAt(angn, spnum)) *getAt(rn, spnum)) + ¬
  getAt(xo,  spnum) +getAt(dxt, spnum) *a)

The above sets variables 'x' and 'y' which are the new coordinates of the sprite on the stage. All we need to do now is update the sprites position.

set the loch of sprite spnum to getAt (x, spnum) 
set the locv of sprite spnum to getAt (y, spnum)

Here's the whole sprite script with minimal comments:

-- calculate sprite displacement
on exitframe me
  global mLocX, mLocY 
  global mLocXNew, mlocynew 
  global xvel, yvel
  global vel
  global xo, yo
  global x, y
  global r
  global a
  global rn
  global dxt, dyt
  global dx, dy
  global angn 
  global angdelt
  global rot 
  global spnum
  put the spriteNum of me into spnum
  set infOnSprite to ((sqrt(power((getAt(x,spnum) - ¬
    mlocxnew), 2) + power((mlocynew - ¬
    getAt(y,spnum)),2)) +5))
  -- set up the linear displacement 
  setAt dx,spnum, ((0.5*xvel/infOnSprite) + ¬
    (getAt(dx,spnum)*a)) 
  setAt dy,spnum, ((0.5*yvel/infOnSprite) + ¬
    (getAt(dy,spnum)*a)) 
  -- rotational influence calc if the mouse 
  -- is moving to the left
  if mlocy < getAt(y,spnum) then setAt rot,spnum, ¬
    ((0.2/infOnSprite)*vel) 
  if mlocy > getAt(y,spnum) then setAt rot,spnum,¬
    ((-0.2/infOnSprite)*vel) 
  -- if the mouse movement is to the right
  if mlocxnew > mlocx then setAt rot,spnum, (-1 * ¬
    getAt (rot,spnum)) 
  -- left nor right
  if mlocxnew = mlocx then setAt rot,spnum,0 
  -- calculate new angular velocity
  setAt angdelt, spnum, (getAt (angdelt, spnum) + ¬
    getAt (rot, spnum)) 
  -- set the new angle
  setAt angn, spnum, (getAt (angdelt, spnum) + ¬
    getAt (angn, spnum)) 

  -- calculate the radius of the swirl
  setAt rn, spnum, (getAt (angdelt, spnum) * 30) 
  
  -- work out the radius from the rate of rotation
  if abs(getAt (rn, spnum)) < 1 then
    setAt angn, spnum, 0
    setAt angdelt, spnum, 0 
  end if
  
  -- Decay the rate of rotation due to friction 'a'
  setAt angdelt, spnum, (getAt (angdelt, spnum) * a) 
  -- Add displacement influence
  setAt dxt, spnum, (getAt (dxt, spnum) + getAt ¬
    (dx, spnum))
  setAt dyt, spnum, (getAt (dyt, spnum) + getAt ¬
    (dy, spnum))
  -- calculate new x,y position of the sprite
  setAt y, spnum, ((cos(getAt (angn, spnum)) * ¬
    getAt (rn, spnum)) +  getAt (yo, spnum) + ¬
    getAt (dyt, spnum) *a)
    
  setAt x, spnum, ((sin(getAt (angn, spnum)) * ¬
    getAt (rn, spnum)) +  getAt (xo, spnum) + ¬
    getAt (dxt, spnum) *a) 
  -- update the position of the sprite
  set the loch of sprite spnum to getAt (x, spnum)
  set the locv of sprite spnum to getAt (y, spnum)
  
end exitframe

Wow, That's it!

Some other Ideas

One of the initialized variables has not really been used much in the code: the original sprite locations ('xo,yo'). We can use these coordinates to produce a kind of elasticity whereby the particles will be drawn back to their original positions. This works well in this example where cast members are text characters building a string. To produce this effect, we are subtracting a percentage of the displacement from the newly calculated position.

By setting up a number of coordinate arrays, the particles can be made to swarm into a pre-determined arrangement triggered by some other factor. See this rollover triggered example.

In another variation, we can add other effectors to the particle locations. In this example, we have a number of sine waves combining to produce a fluid drifting effect. Here the particles drift around naturally in simulated eddies and currents and can also be 'stirred up' into a frenzy.

Well, that's about it, have fun, be creative. Show me what you get up to!

Of course if you come up with an optimized process for all this, I'd love to see it! - as I said, I'm not a mathematician or 'hardcore' programmer, so there's bound to be more than a few rough edges.

Copyright 1997-2024, Director Online. Article content copyright by respective authors.