Simple Fireworks

To welcome the new Year i wanted to create a small Firework.
This tutorial is meant for people new to jME.
We will make use of ParticleEffects for the explosions, a Controller to shoot the Firework into the Sky and some 3DText.
In the end it should look something like this:
img248.imageshack.us_img248_8256_happy2008pd8.jpg

resources

A Sparkling texture for the explosion particles.
This texture has a black background, which will be transparent later.
jme.dev.java.net_source_browse_checkout_jme_src_jmetest_data_texture_spark.jpg

the code

The Code is split up into 3 files:

  • HappyNewYear.java is the main class which extends SimpleGame.
  • ExplosionFactory.java the factory class for the Explosion effect, with caching
  • FireworkController.java the Controller which moves the firework into the sky

What needs to be done:
In the main update loop we create a new Firework periodically.
A Controller (FireworkController.java) moves the firework into the sky.
The Controller also keeps track of the lifetime of the Firework. Once the lifetime of a firework has expired,
the Controller spawns a new Explosion at the current location of the Firework and removes it from the Scene.

A firework will be represented by a simple sphere.
A trail of particles is attached to the sphere to represent the .. you guessed it, firework trail :)

Step by Step:
The next few lines will explain the most important code parts.
These code parts alone will make no sense, they are just for explanation. The whole source files are attached below.

  • 1. First we create the firework (sphere with a particle trail)

To be able to move the sphere and particle trail together, we create a parent node for both elements.
Then we attach the sphere and particle effect to this parent node. Since the sphere and particle effect
are now childs of this node, they will automatically follow the node everywhere.
The FireworkController will later only move the Node.

public void spawnFirework() {
        Sphere sphere = new Sphere("s", 5, 5, 0.06f);
        Node spNode = new Node("spNode");
        spNode.attachChild(sphere);
        spNode.setModelBound(new BoundingBox());
        spNode.updateModelBound();
        spNode.getLocalTranslation().set(start);
        // color the Sphere yellow
        sphere.setRenderState(yellowMaterialState);
        // add a controller to move the Sphere upwards
        spNode.addController(new FireworkController(spNode, lifetime, speed));
        spNode.attachChild(createTrail());
        // attach the Sphere to the scene.
        rootNode.attachChild(spNode);
        rootNode.updateGeometricState(0, true);
        rootNode.updateRenderState();
    }
  • 2. Then we create the particle trail effect which we use above (createTrail())

We create a ParticleMesh with the ParticleFactory and set the different attributes to make it look like a small trail.
Important is to set the RepeatType to RT_CYCLE, so that the trail never stops until we remove it from the scene.
The explosion effect for example will be set to RT_CLAMP, cause we only want to play an explosion once.
The returned ParticleMesh is then attached as a child to the spNode (see above).

public ParticleMesh createTrail() {
        ParticleMesh particleGeom = ParticleFactory.buildParticles("trail", 10);
        // add a gravity effect to the particle effect
        particleGeom.addInfluence(SimpleParticleInfluenceFactory
                .createBasicGravity(new Vector3f(0, -0.15f, 0), true));
        particleGeom.setEmissionDirection(new Vector3f(0.0f, 1.0f, 0.0f));
        // allow to shoot only downwards in a 20° angle
        particleGeom.setMaximumAngle(FastMath.DEG_TO_RAD * 170);
        particleGeom.setMinimumAngle(FastMath.DEG_TO_RAD * 190);
        particleGeom.getParticleController().setSpeed(0.2f);
        particleGeom.setMinimumLifeTime(10.0f);
        particleGeom.setMaximumLifeTime(50.0f);
        particleGeom.setStartSize(0.15f);
        particleGeom.setEndSize(0.01f);
        particleGeom.getParticleController().setControlFlow(false);
        // set the repeat type to looping
        particleGeom.getParticleController().setRepeatType(Controller.RT_CYCLE);
        particleGeom.warmUp(10);
        particleGeom.setInitialVelocity(0.005f);
        // the trail should be a simple white
        particleGeom.setStartColor(ColorRGBA.white.clone());
        particleGeom.setEndColor(ColorRGBA.white.clone());
        // apply alpha, texture and ZBuffer renderstates
        ...
        return particleGeom;
    }
  • 3. Now we create the controller which moves our firework upwards

Controllers can be attached to scene elements.
The main part of an controller is the update() method, there we can move the object it is attached to around over time.
Our controller needs to know the lifetime and speed of the firework.
Every update cycle, we reduce the lifetime, once the lifetime is 0, we spawn an explosion effect at the current location of the sphere and remove it from the scene.

    public void update(float time) {
        lifetime -= time;
        // add hSpeed to the X-Axis and speed to the Y-Axis
        object.getLocalTranslation().addLocal((leftright==true?1:-1)* hSpeed*time, vSpeed*time, 0);
 
        if (lifetime <= 0) {
            // the life has come to an end
            // create a nice explosion at the current location and
            // remove this controller from the object and the object from the scene 
            ExplosionFactory.get().spawnExplosion(object.getLocalTranslation().clone());
            object.removeController(this);
            object.removeFromParent();
        }
    }
  • 4. The explosion effect

In step 3 we spawn an explosion with ExplosionFactory.get().spawnExplosion().
The explosion effect could have been made with a simple method like the trail effect.
But then we would have to always create new explosions and let the GarbageCollector delete the old ones, which is not very smart performance wise.
With the ExplosionFactory class we create a pool of about 20 predefined particle effect which can be reused.
If at one time, more than 20 effect need to be active, the pool gets extended automaticly.
see ExplosionFactory.java

Complete source files

HappyNewYear.java

The main Application.

import java.awt.Font;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
 
import com.jme.app.SimpleGame;
import com.jme.bounding.BoundingBox;
import com.jme.bounding.OrientedBoundingBox;
import com.jme.image.Texture;
import com.jme.math.FastMath;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Controller;
import com.jme.scene.Node;
import com.jme.scene.shape.Sphere;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.MaterialState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;
import com.jme.util.Timer;
import com.jmex.effects.particles.ParticleFactory;
import com.jmex.effects.particles.ParticleMesh;
import com.jmex.effects.particles.SimpleParticleInfluenceFactory;
import com.jmex.font3d.Font3D;
import com.jmex.font3d.Text3D;
import com.jmex.font3d.effects.Font3DTexture;
 
public class HappyNewYear extends SimpleGame {
    /** 3DText font and text */
    private Font3D myfont;
    private Text3D text;
    private float textwidth;
 
    private Random rand;
    /** spawnrate of new fireworks */
    private float spawnRate = 0.5f;
    private float lastSpawn = 0;
    /** material state for the sphere */
    private MaterialState yellowMaterialState;
 
    /**
     * Set up all the necessary stuff.
     * Init the Explosionfactory, create the 3DText.
     */
    @Override
    protected void simpleInitGame() {
        display.setTitle("Happy New Year");
        // create a 3DFont from a system font (Arial) */
        myfont = new Font3D(new Font("Arial", Font.PLAIN, 2), 0.1, true, true,true);
        // color the Text
        Font3DGradient gradient = new Font3DGradient(Vector3f.UNIT_Y, ColorRGBA.white, ColorRGBA.red);
        gradient.applyEffect(myfont);
        text = myfont.createText("HAPPY NEW YEAR 2008!", 2, 0);
        // scale Z-Axis of the font down to make it slimmer 
        text.setLocalScale(new Vector3f(1, 1, 0.1f));
        text.updateWorldBound();
        // get the width of our 3DText, we need it later to spawn the Firework
        OrientedBoundingBox bb = (OrientedBoundingBox) text.getWorldBound();
        textwidth = bb.extent.x;
        rootNode.attachChild(text);
 
        // initialize the ExplosionFactory and some other stuff
        ExplosionFactory.init(rootNode);
        rand = new Random();
        yellowMaterialState = display.getRenderer().createMaterialState();
        yellowMaterialState.setAmbient(ColorRGBA.yellow);
 
        // move the cam a bit to the left and backwards
        cam.setLocation(new Vector3f(5, 0, 15));
    }
 
    /**
     * The update Method gets called every frame.
     * Spawn a new Firework periodical.
     */
    @Override
    protected void simpleUpdate() {
        super.simpleUpdate();
        float time = Timer.getTimer().getTimeInSeconds();
        if (lastSpawn + spawnRate < time) {
            spawnFirework();
            lastSpawn = time;
        }
    }
 
    /**
     * Spawn a new firework.
     * Creates a new Sphere at a random position below the 3DText
     * and attaches a controller to it.
     * Location and speed are more or less random.
     */
    public void spawnFirework() {
        float lifetime = 0.2f + rand.nextFloat() * 1f;
        float speed = 6 + rand.nextFloat() * 5;
        // Define a new start loction and create a new Sphere.
        Vector3f start = text.getLocalTranslation().add(
                    textwidth + (rand.nextFloat() * 5 *
                            (rand.nextBoolean() == true ? 1 : -1)), -5, 1);
        Sphere sphere = new Sphere("s", 5, 5, 0.06f);
        Node spNode = new Node("spNode");
        spNode.attachChild(sphere);
        spNode.setModelBound(new BoundingBox());
        spNode.updateModelBound();
        spNode.getLocalTranslation().set(start);
        // color the Sphere yellow
        sphere.setRenderState(yellowMaterialState);
        // add a controller to move the Sphere upwards
        spNode.addController(new FireworkController(spNode, lifetime, speed));
 
        spNode.attachChild(createTrail());
 
        // attach the Sphere to the scene.
        rootNode.attachChild(spNode);
        rootNode.updateRenderState();
    }
 
    /**
     * creates a particle trail.
     * @return particleMesh representing the trail of a firework
     */
    public ParticleMesh createTrail() {
        ParticleMesh particleGeom = ParticleFactory.buildParticles("trail", 10);
        // add a gravity effect to the particle effect
        particleGeom.addInfluence(SimpleParticleInfluenceFactory
                .createBasicGravity(new Vector3f(0, -0.15f, 0), true));
        particleGeom.setEmissionDirection(new Vector3f(0.0f, 1.0f, 0.0f));
        // allow to shoot only downwards in a 20° angle
        particleGeom.setMaximumAngle(FastMath.DEG_TO_RAD * 170);
        particleGeom.setMinimumAngle(FastMath.DEG_TO_RAD * 190);
        particleGeom.getParticleController().setSpeed(0.2f);
        particleGeom.setMinimumLifeTime(10.0f);
        particleGeom.setMaximumLifeTime(50.0f);
        particleGeom.setStartSize(0.15f);
        particleGeom.setEndSize(0.01f);
        particleGeom.getParticleController().setControlFlow(false);
        // set the repeat type to looping
        particleGeom.getParticleController().setRepeatType(Controller.RT_CYCLE);
        particleGeom.warmUp(10);
        particleGeom.setInitialVelocity(0.005f);
        // the trail should be a simple white
        particleGeom.setStartColor(ColorRGBA.white.clone());
        particleGeom.setEndColor(ColorRGBA.white.clone());
 
        // apply alpha, texture and ZBuffer renderstates
        DisplaySystem display = DisplaySystem.getDisplaySystem();
        AlphaState as = display.getRenderer().createAlphaState();
        as = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
        as.setBlendEnabled(true);
        as.setSrcFunction(AlphaState.SB_SRC_ALPHA);
        as.setDstFunction(AlphaState.DB_ONE);
        as.setTestEnabled(true);
        as.setTestFunction(AlphaState.TF_GREATER);
 
        TextureState ts = display.getRenderer().createTextureState();
        ts.setTexture(TextureManager.loadTexture(
                ExplosionFactory.class.getClassLoader()
                        .getResource("spark.jpg"), 
                        Texture.FM_LINEAR, 
                        Texture.FM_LINEAR));
 
        ZBufferState zs = display.getRenderer().createZBufferState();
        zs.setWritable(false);
        zs.setEnabled(true);
        particleGeom.setRenderState(ts);
        particleGeom.setRenderState(as);
        particleGeom.setRenderState(zs);
        return particleGeom;
    }
 
    /**
     * The Entry point.
     * Creates and starts the game.
     */
    public static void main(String[] args) {
        // we only want to see important messages
        Logger.getLogger("com.jme").setLevel(Level.SEVERE);
        Logger.getLogger("com.jmex").setLevel(Level.SEVERE);
 
        HappyNewYear happy = new HappyNewYear();
        happy.setDialogBehaviour(SimpleGame.ALWAYS_SHOW_PROPS_DIALOG);
        happy.start();
    }
}

FireworkController.java

The Controller which moves the fireworks upwards, spawns explosions and removes the firework from the scene.

import java.util.Random;
 
import com.jme.scene.Controller;
import com.jme.scene.Spatial;
 
/**
 * The FireworkController moves the sphere diagonally upwards.
 * When the lifetime of the object is finished, the controller
 * removes himself from the Sphere and removes the Sphere from the Scene.
 */
public class FireworkController extends Controller {
    /** lifetime in seconds */
    private float lifetime;
    /** vertical speed */
    private float vSpeed;
    /** the object to be moved */
    private Spatial object;
    /** horizontal speed */
    private float hSpeed = 2f;
    /** should the object move digonally to the left or right */
    private boolean leftright;
    private Random rand;
 
    /**
     * The constructor, which takes the object to move, the lifetime and vertical speed.
     * @param object object to move
     * @param lifetime lifetime in seconds
     * @param vSpeed vertical speed
     */
    public FireworkController(Spatial object, float lifetime, float vSpeed) {
        this.lifetime = lifetime;
        this.object = object;
        this.vSpeed = vSpeed;
        rand = new Random();
        // should the firework be moved diagonally to the left or right
        leftright = rand.nextBoolean();
    }
 
    /**
     * The update Method gets called every frame.
     * Moves the object upwards and a bit to the left or right.
     * When the lifetime of the object is finished, it gets removed from the scene.
     * An explosion is spawned when the object dies.
     */
    @Override
    public void update(float time) {
        lifetime -= time;
        // add hSpeed to the X-Axis and speed to the Y-Axis
        object.getLocalTranslation().addLocal((leftright==true?1:-1)* hSpeed*time, vSpeed*time, 0);
 
        if (lifetime <= 0) {
            // the life has come to an end
            // create a nice explosion at the current location and
            // remove this controller from the object and the object from the scene 
            ExplosionFactory.get().spawnExplosion(object.getLocalTranslation().clone());
            object.removeController(this);
            object.removeFromParent();
        }
    }
}

ExplosionFactory.java

The ExplosionFactory defines the look of the Particle effect and keeps a pool of a number of (20) those Effects which will be reused.

import java.util.ArrayList;
import java.util.Random;
import java.util.logging.Logger;
 
import com.jme.image.Texture;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Controller;
import com.jme.scene.Node;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;
import com.jmex.effects.particles.ParticleFactory;
import com.jmex.effects.particles.ParticleMesh;
import com.jmex.effects.particles.SimpleParticleInfluenceFactory;
 
/**
 * The ExplosionFactory keeps a pool of a number of the ParticleEffect to reuse them.
 * The color and intensity of the explosion effects are more or less random.
 */
public class ExplosionFactory {
    private static ExplosionFactory instanze = null;
    /** pool of explosions */
    private ArrayList<ParticleMesh> explosions = null;
    /** render states for the particle effects */
    private AlphaState as = null;
    private TextureState ts = null;
    private ZBufferState zs = null;
    /** reference to the root node, to attach the particleeffect to the scene */
    private Node rootNode = null;
    private Random rand;
 
    public static ExplosionFactory get() {
        if (instanze == null) {
            Logger.getLogger("effects").severe("ExplosionFactory not yet initialized");
            Logger.getLogger("effects").severe("call ExplosionFactory.init(rootNode) first");
        }
        return instanze;
    }
 
    public static void init(final Node root) {
        if (instanze == null) {
            instanze = new ExplosionFactory(root);
        }
    }
 
    /**
     * Sets up the Explosion effect and creates a pool of particles with random colors.
     * @param root
     */
    private ExplosionFactory(final Node root) {
        explosions = new ArrayList<ParticleMesh>();
        rootNode = root;
        rand = new Random();
        DisplaySystem display = DisplaySystem.getDisplaySystem();
        as = display.getRenderer().createAlphaState();
        as = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
        as.setBlendEnabled(true);
        as.setSrcFunction(AlphaState.SB_SRC_ALPHA);
        as.setDstFunction(AlphaState.DB_ONE);
        as.setTestEnabled(true);
        as.setTestFunction(AlphaState.TF_GREATER);
 
        ts = display.getRenderer().createTextureState();
        ts.setTexture(TextureManager.loadTexture(
                ExplosionFactory.class.getClassLoader()
                        .getResource("spark.jpg"), 
                        Texture.FM_LINEAR, 
                        Texture.FM_LINEAR));
 
        zs = display.getRenderer().createZBufferState();
        zs.setWritable(false);
        zs.setEnabled(true);
 
        // create a pool of 20 different explosion effects
        for (int i = 0; i < 20; i++) {
            createExplosion();
        }
    }
 
    /**
     * Try to get a inactive particle effect out of the pool.
     * If we can't find one (all are busy), we create a new one and expand the pool. 
     */
    public ParticleMesh getExplosion() {
        for (ParticleMesh e : explosions) {
            if (!e.isActive()) {
                return e;
            }
        }
        return createExplosion();
    }
 
    /**
     * creates a particle effect with random colors
     * @return
     */
    private ParticleMesh createExplosion() {
        ParticleMesh particleGeom = ParticleFactory.buildParticles("explosion", 80);
        // add a gravity effect to the particle effect
        particleGeom.addInfluence(SimpleParticleInfluenceFactory
                .createBasicGravity(new Vector3f(0, -0.15f, 0), true));
        particleGeom.setEmissionDirection(new Vector3f(0.0f, 1.0f, 0.0f));
        // allow to shoot particles in all directions (360°)
        particleGeom.setMaximumAngle(3.1415927f);
        particleGeom.setMinimumAngle(0);
        particleGeom.getParticleController().setSpeed(0.2f);
        particleGeom.setMinimumLifeTime(100.0f);
        particleGeom.setMaximumLifeTime(600.0f);
        particleGeom.setStartSize(0.15f +rand.nextFloat()/10*1.6f);  // values from 0.15 to 0.3
        particleGeom.setEndSize(0.03f);
        particleGeom.getParticleController().setControlFlow(false);
        particleGeom.getParticleController().setRepeatType(Controller.RT_CLAMP);
        particleGeom.warmUp(200);
        // make some explosion appear more powerful than others
        // more velocity means bigger/wider explosions
        particleGeom.setInitialVelocity(0.01f +(rand.nextFloat() / 50));
        // random color
        particleGeom.setStartColor(new ColorRGBA(rand.nextFloat(), rand.nextFloat(), rand.nextFloat(), 1f));
        particleGeom.setEndColor(new ColorRGBA(rand.nextFloat(), rand.nextFloat(), rand.nextFloat(), 0.0f));
        // apply renderstartes
        particleGeom.setRenderState(ts);
        particleGeom.setRenderState(as);
        particleGeom.setRenderState(zs);
        // attach the particle effect to the root node
        rootNode.attachChild(particleGeom);
        rootNode.updateRenderState();
        // add the effect to the pool to reuse it later
        explosions.add(particleGeom);
 
        return particleGeom;
    }
 
    /**
     * Creates a new explosion at a given location.
     * @param location location where the explosion should appear
     */
    public void spawnExplosion(Vector3f location) {
        ParticleMesh mesh = getExplosion();
        mesh.getLocalTranslation().set(location);
        mesh.updateGeometricState(0, true);
        mesh.forceRespawn();
    }
}

Modified for jME2 with error-correction, refactoring

Sources above may throw IndexOutOfBoundsException when particles are detached.
So deferring particle removal until update is ended is added.
Sources below are modified for jME2 and refactored for ease of use.
You just attach / detach class 'Firework' to show / hide firework.

Avoiding IndexOutOfBoundsException

  • 1. ParticleMesh is registered to 'done' list when controller lifetime is over
public void update(final float time) {
	this.lifetime -= time;
	// add hSpeed to the X-Axis and speed to the Y-Axis
	this.object.getLocalTranslation().addLocal((this.leftright == true ? 1 : -1) * this.hSpeed * time,
			this.vSpeed * time, 0);

	if (this.lifetime <= 0) {
		setActive(false);
		this.explosionFactory.addDone(this.object);
	}
}
  • 2. ParticleMesh is actually removed when update() call is ended
public void updateWorldData(final float time) {
	super.updateWorldData(time);
	this.timePassed += time;
	if (this.timePassed > this.spawnRate) {
		spawnFirework();
		this.timePassed = 0;
	}
	this.factory.respawn();
}

...

void respawn() {
	for (Spatial s : this.done) {
		s.clearControllers();
		s.removeFromParent();
		spawnExplosion(s.getLocalTranslation().clone());
	}
	this.done.clear();
}

Complete source files

HappyNewYear.java

import java.awt.Font;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme.app.SimpleGame;
import com.jme.bounding.OrientedBoundingBox;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jmex.font3d.Font3D;
import com.jmex.font3d.Text3D;
import com.jmex.font3d.effects.Font3DGradient;
 
public class HappyNewYear extends SimpleGame {
    /** 3DText font and text */
    private Font3D myfont;
    private Text3D text;
    private float textwidth;
    
    /**
     * Set up all the necessary stuff.
     * Init the Explosionfactory, create the 3DText.
     */
    @Override
    protected void simpleInitGame() {
        display.setTitle("Happy New Year");
        // create a 3DFont from a system font (Arial) */
        myfont = new Font3D(new Font("Arial", Font.PLAIN, 2), 0.1, true, true,true);
        // color the Text
        Font3DGradient gradient = new Font3DGradient(Vector3f.UNIT_Y, ColorRGBA.white, ColorRGBA.red);
        gradient.applyEffect(myfont);
        text = myfont.createText("HAPPY NEW YEAR 2009!", 2, 0);
        // scale Z-Axis of the font down to make it slimmer 
        text.setLocalScale(new Vector3f(1, 1, 0.1f));
        text.updateWorldBound();
        // get the width of our 3DText, we need it later to spawn the Firework
        OrientedBoundingBox bb = (OrientedBoundingBox) text.getWorldBound();
        textwidth = bb.extent.x;
        rootNode.attachChild(text);
        Firework firework = new Firework();
        rootNode.attachChild(firework);
 
        // move the cam a bit to the left and backwards
        cam.setLocation(new Vector3f(5, 0, 15));
    }
 
 
    /**
     * The Entry point.
     * Creates and starts the game.
     */
    public static void main(String[] args) {
        // we only want to see important messages
        Logger.getLogger("com.jme").setLevel(Level.SEVERE);
        Logger.getLogger("com.jmex").setLevel(Level.SEVERE);
        
        HappyNewYear happy = new HappyNewYear();
        happy.setConfigShowMode(ConfigShowMode.AlwaysShow);
        happy.start();
    }
}

Firework

import java.util.Random;

import com.jme.image.Texture;
import com.jme.image.Texture.MagnificationFilter;
import com.jme.image.Texture.MinificationFilter;
import com.jme.math.FastMath;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Controller;
import com.jme.scene.Node;
import com.jme.scene.shape.Sphere;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.MaterialState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;
import com.jmex.effects.particles.ParticleFactory;
import com.jmex.effects.particles.ParticleMesh;
import com.jmex.effects.particles.SimpleParticleInfluenceFactory;

@SuppressWarnings("serial")
public class Firework extends Node {

	private static final String	PATH_SPARK	= "spark.jpg";
	/** spawnrate of new fireworks */
	private float				spawnRate	= 0.5f;
	private float				timePassed;
	/** material state for the sphere */
	private final MaterialState	sphereMatState;
	private float	minLifetime;
	private float	maxLifetime;
	private float	minSpeed;
	private float	maxSpeed;
	private final ExplosionFactory factory;


	/**
	 * Set up all the necessary Stuff. Init the Explosionfactory, create the 3DText.
	 */
	public Firework() {

		// initialize the ExplosionFactory and some other stuff
		this.factory = new ExplosionFactory(this, PATH_SPARK);
		this.sphereMatState = DisplaySystem.getDisplaySystem().getRenderer().createMaterialState();
		this.sphereMatState.setAmbient(ColorRGBA.white);
		this.minLifetime = 0.2f;
		this.maxLifetime = 1.2f;
		this.minSpeed = 6;
		this.maxSpeed = 11;
	}
	
	
	public void setExplosionSize(int size) {
		this.factory.setExplosionSize(size);
	}


	public void setSpawnRate(float rate) {
		this.spawnRate = rate;
	}
	
	public void setLifetime(float min, float max) {
		this.minLifetime = min;
		this.maxLifetime = max;
	}
	
	public void setSpeed(float min, float max) {
		this.minSpeed = min;
		this.maxSpeed = max;
	}


	@Override
	public void updateWorldData(final float time) {
		super.updateWorldData(time);
		this.timePassed += time;
		if (this.timePassed > this.spawnRate) {
			spawnFirework();
			this.timePassed = 0;
		}
		this.factory.respawn();
	}


	/**
	 * Spawn a new firework.<br>
	 * Creates a new Sphere at a random position below the 3DText and attaches a controller to it.
	 * Location and speed are more or less random.
	 */
	private void spawnFirework() {
		final Random rand = FastMath.rand;
		final float lifetime = this.minLifetime + rand.nextFloat() * (this.maxLifetime - this.minLifetime);
		final float speed = this.minSpeed + rand.nextFloat() * (this.maxSpeed - this.minSpeed);
		// Define a new start loction and create a new Sphere.
		final Vector3f start = new Vector3f(rand.nextFloat() * 10f - 5f, 0, 0);
		final Sphere sphere = new Sphere("s", 5, 5, 0.06f);
		final Node spNode = new Node("firesphere");
		spNode.attachChild(sphere);
		spNode.getLocalTranslation().set(start);
		// color the Sphere yellow
		sphere.setRenderState(this.sphereMatState);
		// add a controller to move the Sphere upwards
		spNode.addController(new FireworkController(spNode, lifetime, speed, this.factory));

		spNode.attachChild(createTrail());

		// attach the Sphere to the scene.
		this.attachChild(spNode);
		this.updateRenderState();
	}


	/**
	 * creates a particle trail.
	 * 
	 * @return particleMesh representing the trail of a firework
	 */
	private ParticleMesh createTrail() {
		final Renderer r = DisplaySystem.getDisplaySystem().getRenderer();
		final ParticleMesh particleGeom = ParticleFactory.buildParticles("trail", 10);
		// add a gravity effect to the particle effect
		particleGeom.addInfluence(SimpleParticleInfluenceFactory.createBasicGravity(new Vector3f(0, -0.15f, 0), true));
		particleGeom.setEmissionDirection(new Vector3f(0.0f, 1.0f, 0.0f));
		// allow to shoot only downwards in a 20° angle
		particleGeom.setMaximumAngle(FastMath.DEG_TO_RAD * 170);
		particleGeom.setMinimumAngle(FastMath.DEG_TO_RAD * 190);
		particleGeom.getParticleController().setSpeed(0.2f);
		particleGeom.setMinimumLifeTime(10.0f);
		particleGeom.setMaximumLifeTime(50.0f);
		particleGeom.setStartSize(0.3f);
		particleGeom.setEndSize(0.1f);
		particleGeom.getParticleController().setControlFlow(false);
		// set the repeat type to looping
		particleGeom.getParticleController().setRepeatType(Controller.RT_CYCLE);
		particleGeom.warmUp(10);
		particleGeom.setInitialVelocity(0.005f);
		// the trail should be a simple white
		particleGeom.setStartColor(ColorRGBA.white.clone());
		particleGeom.setEndColor(ColorRGBA.white.clone());

		// apply alpha, texture and ZBuffer renderstates
		final BlendState bs = r.createBlendState();
		particleGeom.setRenderState(bs);
		bs.setBlendEnabled(true);
		bs.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
		bs.setDestinationFunction(BlendState.DestinationFunction.One);
		bs.setTestEnabled(true);
		bs.setTestFunction(BlendState.TestFunction.GreaterThan);

		TextureState texState = r.createTextureState();
		Texture tex = TextureManager.loadTexture(PATH_SPARK, MinificationFilter.NearestNeighborNoMipMaps, MagnificationFilter.NearestNeighbor);
		texState.setTexture(tex);
		particleGeom.setRenderState(texState);

		final ZBufferState zs = r.createZBufferState();
		particleGeom.setRenderState(zs);
		zs.setWritable(false);
		zs.setEnabled(true);
		particleGeom.updateRenderState();
		return particleGeom;
	}
}

ExplosionFactory.java

import java.util.ArrayList;
import java.util.Random;
import java.util.logging.Logger;

import com.jme.image.Texture;
import com.jme.image.Texture.MagnificationFilter;
import com.jme.image.Texture.MinificationFilter;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Controller;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.TextureState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jme.util.TextureManager;
import com.jmex.effects.particles.ParticleFactory;
import com.jmex.effects.particles.ParticleMesh;
import com.jmex.effects.particles.SimpleParticleInfluenceFactory;


/**
 * The ExplosionFactory keeps a pool of a number of the ParticleEffect to reuse them.<br>
 * The color and intensity of the explosion effects are more or less random.<br>
 */
class ExplosionFactory {
	public static final Logger log = Logger.getLogger(ExplosionFactory.class.getName());

	private static final int		POOL_SIZE	= 20;

	/** pool of explosions */
	private ArrayList<ParticleMesh>	explosions;
	/** render states for the particle effects */
	private BlendState				blendState;
	private TextureState			texState;
	private ZBufferState			zbufferState;
	/** reference to the root node, to attach the particleeffect to the scene */
	private Node					rootNode;
	private final ArrayList<Spatial> done;
	private final Random			rand;
	
	private int	explosionSize	= 100;
	


	/**
	 * Sets up the Explosion effect and creates a pool of particles with random colors.
	 * 
	 * @param root
	 */
	ExplosionFactory(final Node root, String imagePath) {
		final Renderer r = DisplaySystem.getDisplaySystem().getRenderer();
		this.explosions = new ArrayList<ParticleMesh>();
		this.rootNode = root;
		this.rand = new Random();
		this.done = new ArrayList<Spatial>();
		this.blendState = r.createBlendState();
		this.blendState.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
		this.blendState.setDestinationFunction(BlendState.DestinationFunction.One);
		this.blendState.setBlendEnabled(true);

		this.texState = r.createTextureState();
		Texture tex = TextureManager.loadTexture(imagePath, MinificationFilter.NearestNeighborNoMipMaps, MagnificationFilter.NearestNeighbor);
		this.texState.setTexture(tex);

		this.zbufferState = r.createZBufferState();
		this.zbufferState.setWritable(false);

		// create a pool of 20 different explosion effects
		for (int i = 0; i < POOL_SIZE; i++) {
			createExplosion();
		}
	}
	
	void setExplosionSize(int size) {
		if (this.explosionSize == size)
			return;
		for (ParticleMesh mesh : this.explosions) {
			mesh.removeFromParent();
		}
		this.explosions.clear();
		this.explosionSize = size;
	}


	/**
	 * Try to get a inactive particle effect out of the pool.<br>
	 * If we can't find one (all are busy), we create a new one and expand the pool.<br>
	 */
	ParticleMesh getExplosion() {
		for (ParticleMesh e : this.explosions) {
			if (!e.isActive()) {
				return e;
			}
		}
		return createExplosion();
	}


	/**
	 * creates a particle effect with random colors
	 * 
	 * @return
	 */
	private ParticleMesh createExplosion() {
		final ParticleMesh particleGeom = ParticleFactory.buildParticles("explosion", this.explosionSize);
		// add a gravity effect to the particle effect
		particleGeom.addInfluence(SimpleParticleInfluenceFactory.createBasicGravity(new Vector3f(0, -0.15f, 0), true));
		particleGeom.setEmissionDirection(new Vector3f(0.0f, 1.0f, 0.0f));
		// allow to shoot particles in all directions (360°)
		particleGeom.setMaximumAngle(3.1415927f);
		particleGeom.setMinimumAngle(0);
		particleGeom.getParticleController().setSpeed(0.2f);
		particleGeom.setMinimumLifeTime(100.0f);
		particleGeom.setMaximumLifeTime(600.0f);
		particleGeom.setStartSize(1f + this.rand.nextFloat()*0.16f);
		particleGeom.setEndSize(0.03f);
		particleGeom.getParticleController().setControlFlow(false);
		particleGeom.getParticleController().setRepeatType(Controller.RT_CLAMP);
		particleGeom.warmUp(200);
		// make some explosion appear more powerful than others
		// more velocity means bigger/wider explosions
		particleGeom.setInitialVelocity(0.01f + (this.rand.nextFloat() / 50));
		// random color
		particleGeom.setStartColor(new ColorRGBA(this.rand.nextFloat(), this.rand.nextFloat(), this.rand.nextFloat(),
			1f));
		particleGeom.setEndColor(new ColorRGBA(this.rand.nextFloat(), this.rand.nextFloat(), this.rand.nextFloat(),
			0.0f));
		// apply renderstartes
		particleGeom.setRenderState(this.texState);
		particleGeom.setRenderState(this.blendState);
		particleGeom.setRenderState(this.zbufferState);
		// attach the particle effect to the root node
		this.rootNode.attachChild(particleGeom);
		this.rootNode.updateRenderState();
		// add the effect to the pool to reuse it later
		this.explosions.add(particleGeom);
		return particleGeom;
	}


	/**
	 * Creates a new explosion at a given location.
	 * 
	 * @param location
	 *            location where the explosion should appear
	 */
	void spawnExplosion(final Vector3f location) {
		final ParticleMesh mesh = getExplosion();
		mesh.getLocalTranslation().set(location);
		mesh.updateWorldData(0);
		mesh.forceRespawn();
	}
	
	
	void respawn() {
		for (Spatial s : this.done) {
			s.clearControllers();
			s.removeFromParent();
			spawnExplosion(s.getLocalTranslation().clone());
		}
		this.done.clear();
	}
	
	
	void addDone(final Spatial work) {
		this.done.add(work);
	}
}

FireworkController.java

import java.util.Random;

import com.jme.scene.Controller;
import com.jme.scene.Spatial;

/**
 * The FireworkController moves the sphere diagonally upwards.<br>
 * When the lifetime of the object is finished,<br>
 * the controller removes himself from the Sphere and removes the Sphere from the Scene.<br>
 */
@SuppressWarnings("serial")
class FireworkController extends Controller {
	/** lifetime in seconds */
	private float			lifetime;
	/** vertical speed */
	private final float		vSpeed;
	/** the object to be moved */
	private final Spatial	object;
	/** horizontal speed */
	private final float		hSpeed	= 2f;
	/** should the object move digonally to the left or right */
	private final boolean	leftright;
	private final Random	rand;
	private final ExplosionFactory	explosionFactory;


	/**
	 * The constructor, which takes the object to move, the lifetime and vertical speed.
	 * 
	 * @param object_
	 *            object to move
	 * @param lifetime_
	 *            lifetime in seconds
	 * @param vSpeed_
	 *            vertical speed
	 */
	FireworkController(final Spatial object_, final float lifetime_, final float vSpeed_, final ExplosionFactory factory) {
		this.explosionFactory = factory;
		this.lifetime = lifetime_;
		this.object = object_;
		this.vSpeed = vSpeed_;
		this.rand = new Random();
		// should the firework be moved diagonally to the left or right
		this.leftright = this.rand.nextBoolean();
	}


	/**
	 * The update Method gets called every frame.<br>
	 * Moves the object upwards and a bit to the left or right.<br>
	 * When the lifetime of the object is finished, it gets removed from the scene.<br>
	 * An explosion is spawned when the object dies.
	 */
	@Override
	public void update(final float time) {
		this.lifetime -= time;
		// add hSpeed to the X-Axis and speed to the Y-Axis
		this.object.getLocalTranslation().addLocal((this.leftright == true ? 1 : -1) * this.hSpeed * time,
				this.vSpeed * time, 0);

		if (this.lifetime <= 0) {
			setActive(false);
			this.explosionFactory.addDone(this.object);
		}
	}
}


/var/www/wiki/data/pages/simple_fireworks.txt · Last modified: 2010/05/03 01:22 by newacct  
Recent changes · Show pagesource · Login

Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki

subscribe to jME latest jme headlines


site design by bleedcrimson designs © 2008