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

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