NodeBox for OpenGL | Physics

The nodebox.graphics.physics module offers fairly stable functionality to add 2D dynamic effects to an animation, for example a school of fish that moves around fluidly, or a fountain that sprays particles.

Because of the mathematics involved, this module can benefit from installing Psyco.
A version of Psyco precompiled for Mac OS X is included in NodeBox, you can also compile the source manually for other systems. In the nodebox/ext/psyco/src/ folder, execute setup.py from the command line:

> cd nodebox/ext/psyco/src
> python setup.py

 


Vector

A Euclidean vector (sometimes called a geometric or spatial vector, or, as here, simply a vector) is a geometric object that has both a magnitude (or length) and a direction. It is commonly used in computer graphics to define where something is going and how fast it is going.

v = Vector(x=0, y=0, z=0, length=None, angle=None)
v.xyz                                      # Tuple of (x,y,z)-values.
v.xy                                       # Tuple of (x,y)-values.
v.x                                        # Magnitude along x-axis.
v.y                                        # Magnitude along y-axis.
v.z                                        # Magnitude along z-axis.
v.length                                   # Magnitude in 3D, if x,y,z != 0.
v.angle                                    # Direction in 2D, if x,y,z != 0.
v.copy()
v.distance(vector)                         # Returns distance to vector.
v.distance2(vector)                        # Returns distance squared (faster).
v.normalize()                              # Sets length to 1.0.
v.reverse()                                # Sets x,y,z to -x,-y,-z.
v.unit                                     # Returns normalized Vector.
v.reversed                                 # Returns reversed (opposite) Vector.
v.in2D.normal                              # Returns perpendicular Vector.
v.in2D.rotate(degrees)                     # Rotates by degrees.
v.in2D.rotated(degrees)                    # Returns rotated Vector.
v.in2D.angle_to(vector)                    # Returns angle between 2 vectors.
v.dot(vector)                              # Returns dot product Vector.
v.cross(vector)                            # Returns cross product Vector.
v.draw(x, y)                               # Draws the vector at (x,y).

Note: vector rotation works in 2D, so the z-axis will be ignored.
This is because (for example) a vector has multiple perpendicular vectors in 3D instead of one.

Vector objects can be used with math operators, either in-place with +=, -=, *= and /= or to return a new Vector with +, -,* and /. The right operand can be either another Vector or a scalar. For example:

from nodebox.graphics.physics import Vector
 
v  = Vector(1.0, 2.0, 0.0)
v += Vector(2.0, 0.0,0.0)
v *= 2
print v
 
>>> Vector(6.00, 4.00, 0.00)

 


Flocking

Boids is an artificial life program, developed by Craig Reynolds in 1986, which simulates the flocking behavior of birds. Boids is an example of emergent behavior; the complexity of Boids arises from the interaction of individual agents adhering to a set of simple rules:

  • separation: steer to avoid crowding local flockmates,
  • alignment: steer towards the average heading of local flockmates,
  • cohesion: steer to move toward the average position of local flockmates,
  • avoidance: steer to avoid colliding with obstacles,
  • seeking: steer to move toward a target.

Unexpected behavior, such as splitting flocks and reuniting after avoiding obstacles, can be considered emergent. The boids framework is often used in computer graphics to provide realistic-looking representations of flocks of birds and other creatures, such as schools of fish or herds of animals.

Boid

The Boid object represents an agent in a Flock, with an (x,y,z)-position subject to different forces. It has a radius of sight that is used to find local flockmates when calculating cohesion and alignment. It has a radius of personal space that is used when calculating separation.

boid = Boid(flock, x=0, y=0, z=0, sight=70, space=30)
boid.flock                                 # Flock this boid belongs to.
boid.x                                     # Horizontal position.
boid.y                                     # Vertical position.
boid.z                                     # Depth.
boid.depth                                 # Depth, relative between 0.0-1.0.
boid.sight                                 # Radius of sight.
boid.space                                 # Radius of personal space.
boid.velocity                              # Vector.
boid.target                                # Vector to chase.
boid.heading                               # Bearing as an angle in 2D.
boid.dodge                                 # True => very close to an obstacle.
boid.copy() 
boid.near(boid, distance=50)               # True if boid is within distance.
boid.seek(vector)                          # Sets given Vector as target.
boid.update(                               # Update position with given forces.
  separation = 0.2, 
    cohesion = 0.2, 
   alignment = 0.6, 
   avoidance = 0.6, 
      target = 0.2, 
       limit = 15.0)

Obstacle

The Obstacle object can be used to add locations to the flock that the boids will avoid.
Note: sometimes boids will be moving too fast to steer away when they perceive an obstacle, and fly through it. Tweaking the radius of the obstacle and the sight and speed of the boid can remedy this.

obstacle = Obstacle(x=0, y=0, z=0, radius=10)
obstacle.x
obstacle.y
obstacle.z
obstacle.radius
obstacle.copy()

Flock

The Flock object is a list of Boid objects confined to a box area.

flock =  Flock(amount, x, y, width, height, depth=100.0, obstacles=[])
flock.boids                                # List of Boid objects.
flock.obstacles                            # List of Obstacle objects.
flock.x                                    # Area left edge.
flock.y                                    # Area bottom edge.
flock.width                                # Area width.
flock.height                               # Area height.
flock.depth                                # Area depth.
flock.scattered                            # True if boids are not cooperating.
flock.gather                               # Chance of reuniting after scatter.
flock.copy()
flock.by_depth()                           # List of boids sorted by depth.
flock.seek(vector)                         # Sets target Vector for all boids.
flock.sight(distance)                      # Sets sight for all boids.
flock.space(distance)                      # Sets space for all boids.
flock.scatter(gather=0.05)                 # Scatters the flock.
flock.update(                              # Update positions with given forces.
  separation = 0.2, 
    cohesion = 0.2, 
   alignment = 0.6, 
   avoidance = 0.6, 
      target = 0.2, 
       limit = 15.0, 
   constrain = 1.0, 
    teleport = False)

The example below demonstrates a school of fish. Different behavior arises from the interplay between all forces, so you'll have to tweak the numbers to get it right. In this example, Flock.update() has teleport set to True so the boids wrap around the edges instead of turning back. Another thing to remember is to set  limit high enough, otherwise the boids will be too constrained and start moving in a straight line.

from nodebox.graphics import *
from nodebox.graphics.physics import Vector, Boid, Flock, Obstacle
 
flock = Flock(50, x=-50, y=-50, width=700, height=400)
flock.sight(80)
 
def draw(canvas):
    canvas.clear()
    flock.update(separation=0.4, cohesion=0.6, alignment=0.1, teleport=True)
    for boid in flock:
        push()
        translate(boid.x, boid.y)
        scale(0.5 + boid.depth)
        rotate(boid.heading)
        arrow(0, 0, 15)
        pop()
 
canvas.size = 600, 300
canvas.run(draw) 

nodebox-physics-flock

References: vergenet.net/~conrad (2007)

 


Particle system

A particle system is a computer graphics technique to simulate certain fuzzy phenomena which are otherwise very hard to reproduce with conventional rendering techniques: fire, explosions, smoke, moving water, sparks, falling leaves, clouds, fog, snow, dust, meteor tails, hair, fur, grass, or abstract visual effects like glowing trails, magic spells.

Force

A Force causes objects with a mass to accelerate. It acts as a repulsive or attractive dynamic between two particles. A negative strength indicates an attractive force. The closer particles are, the exponentially greater the force will be. The threshold defines a minimum distance to use in calculations – otherwise the force can grow too large (e.g. particles fall outside the canvas before the effect can be perceived) .

force = Force(particle1, particle2, strength=1.0, threshold=100.0)
force.particle1
force.particle2
force.strength
force.threshold
force.apply()

Spring

A Spring exerts attractive resistance when its length changes. It acts as a flexible (but secure) connection between two particles.

spring = Spring(particle1, particle2, length, strength=1.0)
spring.particle1
spring.particle2
spring.strength
spring.length
spring.snapped                             # When True, the spring breaks.
spring.apply()
spring.draw()

Particle

A Particle is an object with a mass that can be subjected to attractive and repulsive forces (unless Particle.fixed is set to True). The object's velocity is an inherent force (e.g. a rocket propeller to escape gravity). A particle can have a life span, which is the number of updates before it disappears. Each System.update(), a particle loses 1 life. Dead particles are not updated or drawn.

particle = Particle(x, y, velocity=(0.0,0.0), mass=10.0, radius=10.0, life=None, fixed=False)
particle.x                                 # Horizontal position.
particle.y                                 # Vertical position.
particle.mass                              # Weight (lighter = greater forces).
particle.radius                            # Used when drawn (radius != mass).
particle.velocity                          # Vector.
particle.life                              # None, or a number.
particle.age                               # 0.0-1.0, if life is defined.
particle.dead                              # True when age=1.0.
particle.fixed                             # Influenced by forces?
particle.draw()

Particles are drawn as circles whose radius diminishes as they age. The Particle.draw() method can of course be overridden with custom behavior. This is what the default method looks like:

class Particle:
    def draw(self, **kwargs):
        r = self.radius * (1 - self.age)
        ellipse(self.x, self.y, r*2, r*2, **kwargs)

System

A System is a collection of particles, particle emitters, forces and springs: 

system = System(gravity=(0,0), drag=0.0)
system.particles                           # List of Particle objects.
system.emitters                            # List of Emitter objects.
system.forces                              # List of Force objects.
system.springs                             # List of Spring objects.
system.gravity                             # Global attractive force.
system.drag                                # Global resisting force.
system.dead                                # True if all particles are dead.
system.append()                            # Particle, Emitter, Force, Spring.
system.force(strength=1.0, threshold=100, source=None, particles=[]) 
system.dynamics(particle, type=None)
system.update(limit=30)
system.draw()
 
  • System.force() creates a new Force that is applied between each two particles.
    The effect this yields (with a repulsive force) is an explosion.
    When source is a Particle, applies the force to this particle against all others.
    When particles are given, only apply the force to these Particle objects.
    The force is applied to particles present in the system, those added later on are not subjected to the force. Be aware that 50 particles wield yield 50 x 50 / 2 = 1250 forces. This has an impact on performance.
  • System.dynamics() returns a list of forces on the given particle, filtered by type (e.g. type=Spring).
  • System.update() updates the location of the particles by applying forces and firing emitters.

Note: the current implementation of System is experimental – it is not always stable. To make it better, a solver (Euler, Runge-Kutta) needs to be added. Right now the whole force is simply added to the particle's position, which has the effect that particles can start "shivering". This is done for performance.

Emitter

The Emitter object can be used as a source that shoots particles in a given direction with a given strength. It can be added to a system with System.append().

emitter = Emitter(x, y, angle=0, strength=1.0, spread=10)
emitter.system                             # System the emitter is part of. 
emitter.particles                          # Particles fired by the emitter.
emitter.x                                  # Horizontal position.
emitter.y                                  # Vertical position.
emitter.angle                              # Firing direction.
emitter.strength                           # Firing force.
emitter.spread                             # Less spread = shoots straighter.
emitter.append(particle, life=100)
 
  • Emitter.append() adds a Particle (i.e. ammo) to the emitter.
    The optional life parameter sets the default lifespan of the particle – dead particles can be reused (e.g. they are fired again) – otherwise the emitter either stops firing or you need to keep adding more and more particles.

Here is a simple example of a system with an emitter spraying particles:

from nodebox.graphics import *
from nodebox.graphics.physics import System, Emitter, Particle, MASS
 
e = Emitter(x=300, y=0, angle=90, spread=60, strength=4)
for i in range(100):
    e.append(
        Particle(0, 0, 
            life = random(50,250), 
            mass = random(10,20), 
          radius = MASS)
system = System(gravity=(0,1), drag=0.005)
system.append(e)
 
def draw(canvas):
    background(1)
    fill(0, 0.5)
    stroke(0, 0.5)
    system.update()    
    system.draw()
 
canvas.size = 600, 300
canvas.run(draw)

nodebox-physics-emitter

References: local.wasp.uwa.edu.au (1998)