Examples

Note! This does not reflect the final language exactly, but should be close enough. There will be more coolness added, such as predefined functions for common particle and emitter behaviour. Also, Haskell's type system is bound to protest when attempting unifying simple operatiors such as + or * for both scalar and vector expressions.

Currently, these examples are untested, but should give you an idea of how things are meant to come together.

Particles

Say that we want to define a particle that is affected only by gravity, starting at origo with a initial upwards velocity. Then we would define it such:

point :: PointE -> ColorE -> FloatE -> BoolE -> Object
point position color radius kill = ...
myparticle = let pos = origo + integrate vel
                 vel = (0,10,0) + integrate acc
		 acc = (0,-9.82,0)
             in point pos red 1 (time > 10)

We also gave it a red color and a radius of 1. The particle will die when the kill-expression becomes true, that is after 10 seconds. Given the current time and the time we created this particle, we can calculate it's position without storing any state at all.

Had we want it to fade from white to red over a period of one second, we could have used two predefined functions, lerp and clamp. lerp performs linear interpolation of two values. clamp normalises a value between 0 and 1.

lerp :: FloatE -> Exp a -> Exp a
clamp :: Exp a -> Exp a

point pos (lerp (clamp time) white red) 1

If we want to have our particles affected by some external force, which is controlled by the outside system (such as the wind direction in a game, something that probably depends on which world or where in a specific world we are), we can define variables to be from the environment:

envVector :: String -> VectorE
-- similar for other types
myparticle = let wind = envVector "wind_direction"
                 pos = origo + integrate vel
                 vel = (0,10,0) + integrate acc
		 acc = (0,-9.82,0) + wind - vel
             in point pos red 1

envVector allows us to read an external variable and use it in our definition of the particle behaviour. However, this is not all. Our definition now has a cycle in it! The velocity depends on the acceleration which depends on the velocity. This is not a problem, since the cycle goes through an integration. As HSpark currently has no ODE-solver, we need to break this cycle in another way. We do this by switching to numeric integration and using the velocity at the previous update when calculating the acceleration for this frame. As we integrate from t=0 and forwards, any evaluation at t=<0 yields zero, which we can use as our initial velocity value.

Most of the times, we want each of our particles to look a bit different, so what if we added a bit of randomness to the initial velocity?

rand :: Rand a => (a -> Exp b) -> Exp b
nrand :: Rand b => (b -> Exp b) -> Exp b
snapshot :: Exp a -> Exp a
myparticle = let pos = origo + integrate vel
		 vel = snapshot (nrand (\(x,z) -> (x,10,z))
		 col = rand (\x -> interpolate x red blue)
	     in point pos col 1

rand and nrand both takes a function and returns an expression. This function should take a random-type and return an expression. A random type could be any of a float, a tuple of floats, a point, a vector or a color. The difference between rand and nrand is that rand gives random values in the 0..1 range, nrand uses the -1..1 range. snapshot evalutes the expression once, when the particle is created. Now our particle has a initial random velocity and is flickering (rather wildly, I might add) in various shades between red and blue.

Emitters

To define an emitter that emits any object (maybe the particle we defined above, or something completely different), at a rate proportional to sin(time), we would write is as a function which takes the object to emit as an argument, and returns an emitter emitting just that object.

emit :: FloatE -> FloatE -> BoolE -> Object -> Object
emit rate quantity kill obj = ...
myemitter p = emit (100 * sin time) 1 False p

This emitter will be creating new objects with a varying rate, one at a time but one hundred per second at maximum. This emitter will keep emitting particles forever.

A complete system

Now we will link together our emitter with our previous particle, and make one emitter that will emit two streams of particles, similar to the example on the front page (which was created using an early version of HSpark that had rather barouque syntax, if I may say so myself :)

(<+>) :: Object -> Object -> Object
part v = 
  let wind = envVector "wind_direction"
      pos = origo + integrate vel
      vel = snapshot v + integrate acc
      acc = (0,-9.82,0) + wind - vel
  in  point pos red (y pos < 0) 1
emit a = 
  let vel = nrand (\(x,z) -> rotatey (x+3,10,z) (a * 2 * pi / 10))
  in  emit 100 1 False (part vel)
mysystem = emit time <+> emit (pi + time)

<+> is just an operator that takes two objects and adds them together. There, our very own particle system. Most of it we recognize from earlier examples. The two emitters will rotate with a speed of one revolution per 10 seconds and the particles will die once they fall below the ground level.

Output to C++ code

Compiling the system to a C++ class is dead easy.

cpp :: String -> Object -> IO ()
cpp "particle_test" mysystem

This will write two files to the disk: particle_test.cpp and particle_test.h. The header file will look something like this:

#include <hspark.h> -- definition of vector/color/etc
namespace hspark {
class particle_test
{
public:
	particle_test();
	
	void update(float time);
	void draw();

	Vector& wind_direction();
private:
	...
};
} // end namespace hspark

So, just create a gl-window, create an instance of the class, set the wind-direction (intially zero) and start updating/drawing to see your particles be born, fly and die.

There will probably be an option to output a complete GLUT-framework and compile/run it on the fly, so that you can test your particles rapidly. (With GUI-controls for any external variables) Also, it will be possible to inspect the particle system to see how many objects are alive/dead etc.

Working in an external framework

Our previous example held most of the things you need to do particle systems, but in order for them to be efficient, you want bulk processing/drawing and heavy sharing. HSpark therefore allows you to avoid creating the system at class instantiation time and rather call a function in the class each time you want something to happen. Say that we want to create explosions at random points in space, but since the particles all look the same and handle the same way, we should keep them within one class. Let's see how we can do that.

trigged :: String -> Object -> Object
particle p = 
  let pos = snapshot p + integrate vel
      vel = snapshot (nrand (\x::VectorE -> x * 10)) + integrate acc
      acc = (0,-9.82,0) - 0.01 * vel
  in point pos red 1 (time > 10)
emitter p = emit (if time == 0 then 1 else 0) 100 True (particle p)

mysystem =
 let init_pos = extPoint "center_pos"
 in  trigged "explosion" (emitter init_pos)

Since we are taking a snapshot of the external variable, we can give it as a one-time argument when creating the system. An example of how things would be if we would like to read continously from an external variable that is specific for each trigged instance is provided below. This will give the following interface class:

class explosions
{
public:
	explosions();	
	void explosion(const Point &center_pos);
	...
};

Note that there is nothing stopping us from having several different particles / emitters in one class. In fact, taken to the extereme, all particles in the world can be handled by one class. This would certainly give HSpark the most opportunity to optimize things as it knows about all particles.

Communication

The one-way communication that we had in the previous example is not always desirable, depending on the framework. If some advanced occlusion culling is used, the framework can probably make intelligent decisions as as to when the system is visible It could also be that we want to track the external variable continously, which we can do by storing the reference for the duration of the system. Now it is possible that the variable that we are tracking may become invalid. The framework must have then have some way of letting us know that the reference is no longer valid.

 This means that we need a way for the framework to:

  1. Query for a bounding volume

  2. Updating the system (thus updating the bounding volume)

  3. Drawing the system

  4. "Snapshotting" external variables so that they will no longer be accessed.

  5. Killing the system completely.

  6. Be informed when the system is expired so that visibility testing is no longer necessary

4 and 5 adress the same problem, but the latter also provides a way to do a clean shutdown.

We solve these problems by introducing two interfaces. One is a callback that is used to notify the framework when the system has expired, the other is a tracker that can be used to query for bounding volumes and system and in

class callback {
public:
	virtual void operator()() = 0;
};
class tracker {
public:
	virtual Sphere& bounding_volume() = 0;
	virtual void update(float time) = 0;
	virtual void draw() = 0;
	virtual void kill() = 0;
	virtual void snapshot() = 0;
};
class AllMyFunkyParticles {
public:	
	tracker * explosion(const Point &center_pos,
		            callback* expired = 0);
	...
}

This way, the scene graph system will have enough information to decide wether the system should be drawn or not, and HSpark will know when to update and draw the particles.

There are a few rules to this protocol. HSpark guarantees that after sending notifying through the callback, or recieving a call to kill or snapshot, it will not access any references given for this system. In return, the framework must guarantee not to call any function in the tracker after any such communication has occured.

In order to guarantee that there will be a valid reference (it is easy, but seriously wrong, to pass a local variable when instantiating the system) one could add it to the tracker instead:

class explosion_tracker : public tracker {
public:
	Point center_pos;
	~explosion_tracker() { snapshot(); } // or kill()
};

This would make things entirely safe, but if the system dies calling snapshot would be an error, as there is no one in the other end to recieve that call. This could be solved by letting HSpark store orphaned trackers until they are snapshotted or killed, whether or no there exist any particles or emitters to be tracked.

Some advanced particle trickery, all in a classic adventure tale setting :)

Assume that we are making a spooky adventure game, where our hero (or heroine) is wading through a swamp in the middle of the night with only a lantern as light source. Naturally, all sorts of moths and mosquitos, whichof there exists plenty, since this is a swamp after all, fly towards the lamp. The game engine spawns swarms of flies that moves to the lantern, circle around it for some time but then depart back to their original position and die. Occasionally, our main character throws fireballs at these swarms, as he or she has been in the swamp since noon and are quite fed up with their abominable biting, buzzing and generally annoying attitude. It may also happen that the lantern goes out (it was gotten rather cheap from a suspicious looking old witch living at the edge of the swamp), in which case our poor adventurer soon ends up being consumed, lantern and all, by the ancient, unfathomable horror that lurks beneath the dark, oily surface. The flies however hangs around for a while, simply because they are too stupid to do anything else.

So, first we define our particle system in HSpark. It will consist of three types of particles:

  1. The mosquitos

  2. The fireball

  3. An explosion (when a fireball hits a swarm, we'll reuse the previous one)

mosquito :: PointE -> PointE -> Object
mosquito lantern_pos init_pos  = 
  let dir = lantern_pos - pos 
      time_to = 30
      pos = snapshot (init_pos) + integrate vel
      vel = (rand (\x::VectorE -> x)) + integrate acc
      acc = clamp (if time < time_to then dir else -dir)
      near_lantern = let b = length dir < 10 && time < time_to
      movement = if near_lantern 
                 then lantern_pos + rotatey (1,0,0) time
 		 else pos
      size = snapshot (rand (\x -> x/2 + 0.2))
  in point movement grey size (time > 50)
firespark :: VectorE -> PointE -> Object
firespark init_dir init_pos =
  let pos = snapshot (init_pos) + integrate (snapshot init_dir)
      col = snapshot (rand (\x -> lerp (clamp (x+sin(time*10))) red yellow))
  in point pos col (1 + sin(time*5) * 0.8) (time > 10)
circle-emit :: PointE -> FloatE -> FloatE -> (PointE -> FloatE) -> Object
circle-emit init_pos radius qty obj =
  let pos = snapshot (nrand (\x -> radius * (normalize x) + swarm_pos))
  in emit (if time <= 0 then 1 else 0) qty True (obj pos)
swarm = 
  let pos = extPoint "start_pos"
      lantern = extPoint "lantern"
      emit = circle-emit pos 2 50 (mosquito lantern)
  in trigged "swarm" emit
fireball =
  let pos = extPoint "start_pos"
      dir = extVector "direction"
      emit = circle-emit pos 0.25 100 (firespark dir)
  in trigged "fireball" emit
system = swarm <+> fireball <+> explosion

*phew*. Our mosquitos should move towards the lantern and circle it for the first 30 seconds, then move away and die after 50 seconds. They have a bit of randomness to their velocity, which should give them some of that erratic insectoid flight pattern. Grey in color and with sizes from 0.2 to 0.7.

The fireballs are much simpler, and just fly along a given direction. They do vary a bit with their color and size.

We use the same emitter for both systems, which will calculate points on the surface of a sphere with given center and radius and apply these points to the objects that it emits.

Finally, we just put these together so that we can export them into the same class. So, how would that class look?

class SwampParticles
{
public:
	SwampParticles();
	tracker* swarm(const Point &start_pos,
                       const Point &lantern,
                       callback *expired = 0);
	tracker* fireball(const Point &start_pos,
			  const Vector &init_dir,
			  callback *expired = 0);
	tracker* explosion(const Point &start_pos,
			   callback *expired = 0);
	...
}; 

There, now we can spawn swarms of flies, they track the lantern. If the lantern dies we can snapshot the system so that they still flie towards the lantern. Or, we could change the lantern position to the center of that swarm's bounding sphere, making the swarm gather on the spot for a while. Fireballs can be thrown, and with some collision checking from the game engine, an explosion can be triggered when a hit occurs, destroying the fireball but creating another system in it's place.

Beyond particle systems - let's write a small game!

If the external variables represent key states, we can make a small game in this language. Take as an example the old arcade hit Moonlander, where the player was set to steer a rocket and try to land softly on a landing pad, avoiding surrounding mountains and conserving as much fuel as possible.

Since we can integrate things, we can keep track of fuel and calculate thrust effect. Checking if the position is near the landing pod and the angle of the ship is straight, we can decide wether a landing is succesful or not. The mountains can be generated with a few randomized sine-waves and the landing pod can be put at the minumum position. At a few of the maxima we can have lava or smoke coming out (games isn't meant to be realistic, they are meant to have fancy graphics :). That's the basic idea of it. Once I get the compiler up to speed, this will surely be a worthy goal.

Hmm.. I wonder if any of this will actually work. :)