com.lyeeedar.Graphics.ParticleEffects.ParticleEmitter.java Source code

Java tutorial

Introduction

Here is the source code for com.lyeeedar.Graphics.ParticleEffects.ParticleEmitter.java

Source

/*******************************************************************************
 * Copyright (c) 2013 Philip Collin.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * 
 * Contributors:
 *     Philip Collin - initial API and implementation
 ******************************************************************************/
package com.lyeeedar.Graphics.ParticleEffects;

import java.io.Serializable;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Mesh;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.VertexAttribute;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.Pools;
import com.badlogic.gdx.utils.ObjectMap.Entry;
import com.badlogic.gdx.utils.OrderedMap;
import com.lyeeedar.Roguelike3D.Graphics.Lights.LightManager;
import com.lyeeedar.Roguelike3D.Graphics.Lights.PointLight;
import com.lyeeedar.Utils.Bag;
import com.lyeeedar.Utils.FileUtils;

public class ParticleEmitter implements Serializable {

    private transient static final int VERTEX_SIZE = 9;

    private static final long serialVersionUID = 6308492057144008114L;

    public enum ParticleAttribute {
        SPRITE, SIZE, COLOUR, VELOCITY
    }

    public final String UID;
    public String name;

    // ----- Particle Parameters ----- //
    private TimelineValue[] sprite;
    private TimelineValue[] size;
    private TimelineValue[] colour;
    private TimelineValue[] velocity;
    // ----- End Particle Parameters ----- //

    // ----- Emitter parameters ----- //
    public int maxParticles;
    public float particleLifetime;
    public float particleLifetimeVar;
    public float emissionTime;
    public float ex, ey, ez;
    public int emissionType;
    public int blendFuncSRC;
    public int blendFuncDST;
    public String atlasName;
    // ----- End Emitter parameters ----- //

    // ----- Light ----- //
    private float lightAttenuation;
    private float lightPower;
    private boolean isLightStatic;
    private Color lightColour;
    private boolean lightFlicker;
    private float lightx, lighty, lightz;
    // ----- End Light ----- //

    // ----- Transient Variables ----- //
    public transient float distance = 0;
    private transient static ShaderProgram shader;
    private transient static String currentAtlas;
    private transient static int currentBlendSRC, currentBlendDST;
    public transient TextureAtlas atlas;
    public transient Texture atlasTexture;
    private transient float[][] topLeftTexCoords;
    private transient float[][] topRightTexCoords;
    private transient float[][] botLeftTexCoords;
    private transient float[][] botRightTexCoords;
    private transient Bag<Particle> active;
    private transient Bag<Particle> inactive;
    private transient Vector3 quad;
    private transient float[] vertices;
    private transient Mesh mesh;
    private transient Random ran;
    private transient Matrix4 tmpMat;
    private transient Matrix4 tmpRot;
    private transient Vector3 pos;
    private transient int signx;
    private transient int signy;
    private transient int signz;
    private transient int v;
    private transient float emissionCD;
    private transient PointLight light;
    private transient int i;
    private transient int i2;
    private transient int arrayLen;
    // ----- End Transient Variables ----- //

    // ----- Non-Essential Variables ----- //
    private String lightUID;
    private float x, y, z;
    private float radius;
    // ----- End Non-Essential Variables ----- //

    public ParticleEmitter(float particleLifetime, float particleLifetimeVar, float emissionTime, float ex,
            float ey, float ez, int emissionType, int blendFuncSRC, int blendFuncDST, String atlasName,
            String name) {
        this.UID = this.toString() + this.hashCode() + System.currentTimeMillis() + System.nanoTime();
        this.name = name;

        this.particleLifetime = particleLifetime;
        this.particleLifetimeVar = particleLifetimeVar;
        this.emissionTime = emissionTime;
        this.ex = ex;
        this.ey = ey;
        this.ez = ez;
        this.emissionType = emissionType;
        this.blendFuncSRC = blendFuncSRC;
        this.blendFuncDST = blendFuncDST;
        this.atlasName = atlasName;
    }

    public int getActiveParticles() {
        return active.size;
    }

    public float getRadius() {
        return radius;
    }

    public Vector3 getPosition() {
        return pos.set(x, y, z);
    }

    public void setPosition(float x, float y, float z) {
        this.x = x;
        this.y = y;
        this.z = z;

        if (light != null)
            light.position.set(x + lightx, y + lighty, z + lightz);
    }

    public void setTimeline(TimelineValue[] sprite, TimelineValue[] size, TimelineValue[] colour,
            TimelineValue[] velocity) {
        this.sprite = sprite;
        this.size = size;
        this.colour = colour;
        this.velocity = velocity;
    }

    public TimelineValue[] getSpriteTimeline() {
        return sprite;
    }

    /**
     * Set the sprite number timeline. <p>
     *  Each point in the timeline is specified by a float array. <br> array[0] is the time in the timeline, array[1] is the sprite number (casted to an int)
     *  Optionally setting interpolated to true will linearly interpolate between each value in the timeline
     * @param values
     */
    public void setSpriteTimeline(boolean interpolated, float[]... values) {
        this.sprite = new TimelineValue[values.length];

        for (int i = 0; i < values.length; i++) {
            this.sprite[i] = new TimelineValue(values[i][0], values[i][1]);
        }

        for (int i = 0; i < values.length - 1; i++) {
            this.sprite[i].setInterpolated(true, this.sprite[i + 1]);
        }
    }

    public void setSpriteTimeline(List<TimelineValue> values) {
        TimelineValue[] array = new TimelineValue[values.size()];
        sprite = values.toArray(array);
    }

    public TimelineValue[] getSizeTimeline() {
        return size;
    }

    /**
     * Set the size timeline. <p>
     *  Each point in the timeline is specified by a float array. <br> array[0] is the time in the timeline, array[1] is the width, array[2] is the height
     *  Optionally setting interpolated to true will linearly interpolate between each value in the timeline
     * @param interpolated
     * @param values
     */
    public void setSizeTimeline(boolean interpolated, float[]... values) {
        this.size = new TimelineValue[values.length];

        for (int i = 0; i < values.length; i++) {
            this.size[i] = new TimelineValue(values[i][0], values[i][1], values[i][2]);
        }

        for (int i = 0; i < values.length - 1; i++) {
            this.size[i].setInterpolated(true, this.size[i + 1]);
        }
    }

    public void setSizeTimeline(List<TimelineValue> values) {
        TimelineValue[] array = new TimelineValue[values.size()];
        size = values.toArray(array);
    }

    public TimelineValue[] getColourTimeline() {
        return colour;
    }

    /**
     * Set the colour timeline. <p>
     *  Each point in the timeline is specified by a float array. <br> array[0] is the time in the timeline, array[1] is red, array[2] is green, array[3] is blue and array[4] is alpha
     *  Optionally setting interpolated to true will linearly interpolate between each value in the timeline
     * @param interpolated
     * @param values
     */
    public void setColourTimeline(boolean interpolated, float[]... values) {
        this.colour = new TimelineValue[values.length];

        for (int i = 0; i < values.length; i++) {
            this.colour[i] = new TimelineValue(values[i][0], values[i][1], values[i][2], values[i][3],
                    values[i][4]);
        }

        for (int i = 0; i < values.length - 1; i++) {
            this.colour[i].setInterpolated(true, this.colour[i + 1]);
        }
    }

    public void setColourTimeline(List<TimelineValue> values) {
        TimelineValue[] array = new TimelineValue[values.size()];
        colour = values.toArray(array);
    }

    public TimelineValue[] getVelocityTimeline() {
        return velocity;
    }

    /**
     * Set the velocity timeline. <p>
     *  Each point in the timeline is specified by a float array. <br> array[0] is the time in the timeline, array[1] is the x velocity, array[2] is the y velocity and array[3] is the z velocity
     *  Optionally setting interpolated to true will linearly interpolate between each value in the timeline
     * @param interpolated
     * @param values
     */
    public void setVelocityTimeline(boolean interpolated, float[]... values) {
        this.velocity = new TimelineValue[values.length];

        for (int i = 0; i < values.length; i++) {
            this.velocity[i] = new TimelineValue(values[i][0], values[i][1], values[i][2], values[i][3]);
        }

        for (int i = 0; i < values.length - 1; i++) {
            this.velocity[i].setInterpolated(true, this.velocity[i + 1]);
        }
    }

    public void setVelocityTimeline(List<TimelineValue> values) {
        TimelineValue[] array = new TimelineValue[values.size()];
        velocity = values.toArray(array);
    }

    /**
     * Method to create a basic particle emitter. <p>
     * The particles in this emitter will have a constant width and height,
     *  a constant velocity (vx, vy, vz)
     *   and will interpolate its colour from the start colour to the end over the particles lifetime.
     *   The image used will be the sprite in the atlas designated as 'sprite0'.
     * @param width
     * @param height
     * @param start
     * @param end
     * @param vx
     * @param vy
     * @param vz
     */
    public void createBasicEmitter(float width, float height, Color start, Color end, float vx, float vy,
            float vz) {
        this.sprite = new TimelineValue[] { new TimelineValue(0, 0) };

        this.size = new TimelineValue[] { new TimelineValue(0, width, height) };

        this.colour = new TimelineValue[] { new TimelineValue(0, start.r, start.g, start.b, start.a),
                new TimelineValue(particleLifetime, end.r, end.g, end.b, end.a) };
        this.colour[0].setInterpolated(true, this.colour[1]);

        this.velocity = new TimelineValue[] { new TimelineValue(0, vx, vy, vz) };
    }

    public void addLight(boolean isStatic, float attenuation, float power, Color colour, boolean flicker, float x,
            float y, float z) {
        this.lightAttenuation = attenuation;
        this.lightPower = power;
        this.isLightStatic = isStatic;
        this.lightColour = colour;
        this.lightFlicker = flicker;
        this.lightx = x;
        this.lighty = y;
        this.lightz = z;
    }

    public void create() {

        if (shader == null) {
            shader = new ShaderProgram(SHADER_VERTEX, SHADER_FRAGMENT);
        }

        ran = new Random();

        quad = Pools.obtain(Vector3.class).set(0, 0, 0);
        tmpMat = Pools.obtain(Matrix4.class).idt();
        tmpRot = Pools.obtain(Matrix4.class).idt();
        pos = Pools.obtain(Vector3.class).set(0, 0, 0);

        calculateRadius();
        reloadParticles();
        reloadTextures();
    }

    public void dispose() {
        if (quad != null)
            Pools.free(quad);
        quad = null;
        if (tmpMat != null)
            Pools.free(tmpMat);
        tmpMat = null;
        if (tmpRot != null)
            Pools.free(tmpRot);
        tmpRot = null;
        if (pos != null)
            Pools.free(pos);
        pos = null;
        if (mesh != null)
            mesh.dispose();
        mesh = null;
    }

    public void getLight(LightManager lightManager) {
        if (lightColour == null)
            return;

        if (light != null) {
            if (isLightStatic)
                lightManager.removeStaticLight(light.UID);
            else
                lightManager.removeDynamicLight(light.UID);

            light = null;
        }

        light = new PointLight(new Vector3(x + lightx + (ex / 2f), y + lighty + ey, z + lightz + (ez / 2)),
                lightColour.cpy(), lightAttenuation, lightPower);
        lightUID = light.UID;

        if (isLightStatic)
            lightManager.addStaticLight(light);
        else
            lightManager.addDynamicLight(light);
    }

    public void calculateParticles() {
        maxParticles = (int) ((float) particleLifetime / (float) emissionTime);
    }

    public void calculateRadius() {
        this.radius = ex + ey + ez;
    }

    public void reloadParticles() {
        active = new Bag<Particle>(maxParticles);
        inactive = new Bag<Particle>(maxParticles);

        for (int i = 0; i < maxParticles; i++) {
            Particle p = new Particle();
            inactive.add(p);
        }

        vertices = new float[maxParticles * VERTEX_SIZE * 4];

        if (mesh != null)
            mesh.dispose();
        mesh = new Mesh(false, maxParticles * 4, maxParticles * 6,
                new VertexAttribute(Usage.Position, 3, "a_position"),
                new VertexAttribute(Usage.Generic, 4, "a_colour"),
                new VertexAttribute(Usage.TextureCoordinates, 2, "a_texCoords"));
        mesh.setVertices(vertices);
        mesh.setIndices(genIndices(maxParticles));

    }

    public void reloadTextures() {
        atlas = FileUtils.loadAtlas(atlasName);
        Set<Texture> atlasTextures = atlas.getTextures();
        Iterator<Texture> itr = atlasTextures.iterator();

        atlasTexture = itr.next();

        int maxIndex = 0;
        for (TimelineValue spriteTL : sprite) {
            int index = (int) spriteTL.getValues()[0];

            if (index > maxIndex)
                maxIndex = index;
        }

        topLeftTexCoords = new float[maxIndex + 1][2];
        topRightTexCoords = new float[maxIndex + 1][2];
        botLeftTexCoords = new float[maxIndex + 1][2];
        botRightTexCoords = new float[maxIndex + 1][2];

        for (int i = 0; i < maxIndex + 1; i++) {
            AtlasRegion region = atlas.findRegion("sprite" + i);

            float[] tl = { (float) region.getRegionX() / (float) atlasTexture.getWidth(),
                    (float) region.getRegionY() / (float) atlasTexture.getHeight() };
            float[] tr = {
                    (float) (region.getRegionX() + region.getRegionWidth()) / (float) atlasTexture.getWidth(),
                    (float) region.getRegionY() / (float) atlasTexture.getHeight() };
            float[] bl = { (float) region.getRegionX() / (float) atlasTexture.getWidth(),
                    (float) (region.getRegionY() + region.getRegionHeight()) / (float) atlasTexture.getHeight() };
            float[] br = {
                    (float) (region.getRegionX() + region.getRegionWidth()) / (float) atlasTexture.getWidth(),
                    (float) (region.getRegionY() + region.getRegionHeight()) / (float) atlasTexture.getHeight() };

            topLeftTexCoords[i] = tl;
            topRightTexCoords[i] = tr;
            botLeftTexCoords[i] = bl;
            botRightTexCoords[i] = br;
        }
    }

    public short[] genIndices(int faces) {
        short[] indices = new short[faces * 6];

        for (short i = 0; i < faces; i++) {
            indices[(i * 6) + 0] = (short) ((i * 4) + 0);
            indices[(i * 6) + 1] = (short) ((i * 4) + 1);
            indices[(i * 6) + 2] = (short) ((i * 4) + 2);

            indices[(i * 6) + 3] = (short) ((i * 4) + 1);
            indices[(i * 6) + 4] = (short) ((i * 4) + 3);
            indices[(i * 6) + 5] = (short) ((i * 4) + 2);
        }
        return indices;
    }

    public void render() {
        if (currentBlendSRC == blendFuncSRC && currentBlendDST == blendFuncDST) {
        } else {
            Gdx.gl.glBlendFunc(blendFuncSRC, blendFuncDST);
            currentBlendSRC = blendFuncSRC;
            currentBlendDST = blendFuncDST;
        }

        if (currentAtlas != null && atlasName.equals(currentAtlas)) {

        } else {
            atlasTexture.bind(0);
            shader.setUniformi("u_texture", 0);

            currentAtlas = atlasName;
        }

        mesh.render(shader, GL20.GL_TRIANGLES, 0, active.size * 4);
    }

    public static void begin(Camera cam) {
        Gdx.gl.glEnable(GL20.GL_BLEND);
        Gdx.gl.glEnable(GL20.GL_DEPTH_TEST);
        Gdx.gl.glDepthMask(false);

        shader.begin();
        shader.setUniformMatrix("u_pv", cam.combined);
    }

    public static void end() {
        shader.end();

        Gdx.gl.glDepthMask(true);
        Gdx.gl.glDisable(GL20.GL_BLEND);

        currentAtlas = null;
        currentBlendSRC = currentBlendDST = 0;
    }

    public void update(float delta, Camera cam) {
        if (light != null) {
            light.positionAbsolutely(x + lightx + (ex / 2f), y + lighty + ey, z + lightz + (ez / 2));
            if (lightFlicker)
                light.attenuation = (float) (lightAttenuation
                        * (1 - ((1 - ((float) inactive.size / (float) active.size))) / 2));
        }

        tmpRot.set(cam.view).inv();
        tmpRot.getValues()[Matrix4.M03] = 0;
        tmpRot.getValues()[Matrix4.M13] = 0;
        tmpRot.getValues()[Matrix4.M23] = 0;

        Iterator<Particle> pItr = active.iterator();

        i = 0;
        while (pItr.hasNext()) {
            Particle p = pItr.next();

            float[] velocity = getAttributeValue(p.lifetime, ParticleAttribute.VELOCITY);

            p.update(delta, velocity[0], velocity[1], velocity[2]);

            if (p.lifetime > particleLifetime) {
                pItr.remove();
                inactive.add(p);
                continue;
            }

            tmpMat.setToTranslation(p.x, p.y, p.z).mul(tmpRot);

            int sprite = (int) getAttributeValue(p.lifetime, ParticleAttribute.SPRITE)[0];
            float[] size = getAttributeValue(p.lifetime, ParticleAttribute.SIZE);
            float[] colour = getAttributeValue(p.lifetime, ParticleAttribute.COLOUR);

            quad.set(-size[0] / 2, size[1] / 2, 0).mul(tmpMat);

            v = 0;

            vertices[(i * VERTEX_SIZE * 4) + v + 0] = quad.x;
            vertices[(i * VERTEX_SIZE * 4) + v + 1] = quad.y;
            vertices[(i * VERTEX_SIZE * 4) + v + 2] = quad.z;

            vertices[(i * VERTEX_SIZE * 4) + v + 3] = colour[0];
            vertices[(i * VERTEX_SIZE * 4) + v + 4] = colour[1];
            vertices[(i * VERTEX_SIZE * 4) + v + 5] = colour[2];
            vertices[(i * VERTEX_SIZE * 4) + v + 6] = colour[3];

            vertices[(i * VERTEX_SIZE * 4) + v + 7] = topLeftTexCoords[sprite][0];
            vertices[(i * VERTEX_SIZE * 4) + v + 8] = topLeftTexCoords[sprite][1];

            quad.set(size[0] / 2, size[1] / 2, 0).mul(tmpMat);

            v += VERTEX_SIZE;

            vertices[(i * VERTEX_SIZE * 4) + v + 0] = quad.x;
            vertices[(i * VERTEX_SIZE * 4) + v + 1] = quad.y;
            vertices[(i * VERTEX_SIZE * 4) + v + 2] = quad.z;

            vertices[(i * VERTEX_SIZE * 4) + v + 3] = colour[0];
            vertices[(i * VERTEX_SIZE * 4) + v + 4] = colour[1];
            vertices[(i * VERTEX_SIZE * 4) + v + 5] = colour[2];
            vertices[(i * VERTEX_SIZE * 4) + v + 6] = colour[3];

            vertices[(i * VERTEX_SIZE * 4) + v + 7] = topRightTexCoords[sprite][0];
            vertices[(i * VERTEX_SIZE * 4) + v + 8] = topRightTexCoords[sprite][1];

            quad.set(-size[0] / 2, -size[1] / 2, 0).mul(tmpMat);

            v += VERTEX_SIZE;

            vertices[(i * VERTEX_SIZE * 4) + v + 0] = quad.x;
            vertices[(i * VERTEX_SIZE * 4) + v + 1] = quad.y;
            vertices[(i * VERTEX_SIZE * 4) + v + 2] = quad.z;

            vertices[(i * VERTEX_SIZE * 4) + v + 3] = colour[0];
            vertices[(i * VERTEX_SIZE * 4) + v + 4] = colour[1];
            vertices[(i * VERTEX_SIZE * 4) + v + 5] = colour[2];
            vertices[(i * VERTEX_SIZE * 4) + v + 6] = colour[3];

            vertices[(i * VERTEX_SIZE * 4) + v + 7] = botLeftTexCoords[sprite][0];
            vertices[(i * VERTEX_SIZE * 4) + v + 8] = botLeftTexCoords[sprite][1];

            quad.set(size[0] / 2, -size[1] / 2, 0).mul(tmpMat);

            v += VERTEX_SIZE;

            vertices[(i * VERTEX_SIZE * 4) + v + 0] = quad.x;
            vertices[(i * VERTEX_SIZE * 4) + v + 1] = quad.y;
            vertices[(i * VERTEX_SIZE * 4) + v + 2] = quad.z;

            vertices[(i * VERTEX_SIZE * 4) + v + 3] = colour[0];
            vertices[(i * VERTEX_SIZE * 4) + v + 4] = colour[1];
            vertices[(i * VERTEX_SIZE * 4) + v + 5] = colour[2];
            vertices[(i * VERTEX_SIZE * 4) + v + 6] = colour[3];

            vertices[(i * VERTEX_SIZE * 4) + v + 7] = botRightTexCoords[sprite][0];
            vertices[(i * VERTEX_SIZE * 4) + v + 8] = botRightTexCoords[sprite][1];

            i++;
        }
        mesh.setVertices(vertices);

        emissionCD -= delta;

        arrayLen = inactive.size;

        if (arrayLen == 0)
            return;

        while (emissionCD < 0 && arrayLen > 0) {
            Particle p = inactive.remove(0);

            if (emissionType == 0) {
                signx = (ran.nextInt(2) == 0) ? 1 : -1;
                signy = (ran.nextInt(2) == 0) ? 1 : -1;
                signz = (ran.nextInt(2) == 0) ? 1 : -1;
                p.set(particleLifetimeVar * ran.nextFloat(), x + (float) (ex * ran.nextGaussian() * signx),
                        y + (float) (ey * ran.nextGaussian() * signy),
                        z + (float) (ez * ran.nextGaussian() * signz));

            } else {
                System.err.println("Invalid emission type! " + emissionType);
            }
            active.add(p);

            emissionCD += emissionTime;
            arrayLen--;
        }
    }

    private float[] getAttributeValue(float time, ParticleAttribute pa) {
        TimelineValue tv = null;
        if (pa == ParticleAttribute.SPRITE) {
            tv = searchTimeline(time, sprite);
        } else if (pa == ParticleAttribute.SIZE) {
            tv = searchTimeline(time, size);
        } else if (pa == ParticleAttribute.COLOUR) {
            tv = searchTimeline(time, colour);
        } else if (pa == ParticleAttribute.VELOCITY) {
            tv = searchTimeline(time, velocity);
        }

        return tv.getValuesInterpolated(time);
    }

    private TimelineValue searchTimeline(float time, TimelineValue[] value) {
        arrayLen = value.length;
        for (i2 = 0; i2 < arrayLen; i2++) {
            if (value[i2].time > time) {
                return value[i2 - 1];
            }
        }
        return value[arrayLen - 1];
    }

    public ParticleEmitter copy() {
        ParticleEmitter copy = new ParticleEmitter(particleLifetime, particleLifetimeVar, emissionTime, ex, ey, ez,
                emissionType, blendFuncSRC, blendFuncDST, atlasName, name);
        copy.maxParticles = maxParticles;

        TimelineValue[] cpySprite = new TimelineValue[sprite.length];
        for (int i = 0; i < sprite.length; i++)
            cpySprite[i] = sprite[i].copy();
        copy.sprite = cpySprite;

        TimelineValue[] cpySize = new TimelineValue[size.length];
        for (int i = 0; i < size.length; i++)
            cpySize[i] = size[i].copy();
        copy.size = cpySize;

        TimelineValue[] cpyColour = new TimelineValue[colour.length];
        for (int i = 0; i < colour.length; i++)
            cpyColour[i] = colour[i].copy();
        copy.colour = cpyColour;

        TimelineValue[] cpyVelocity = new TimelineValue[velocity.length];
        for (int i = 0; i < velocity.length; i++)
            cpyVelocity[i] = velocity[i].copy();
        copy.velocity = cpyVelocity;

        if (lightUID != null)
            copy.addLight(isLightStatic, lightAttenuation, lightPower, lightColour, lightFlicker, lightx, lighty,
                    lightz);

        return copy;
    }

    /**
     * A particle, containing its current lifetime and position.
     * @author Philip
     *
     */
    class Particle {
        float lifetime;
        float x, y, z;

        public Particle() {
        }

        public void update(float delta, float vx, float vy, float vz) {
            lifetime += delta;
            x += vx * delta;
            y += vy * delta;
            z += vz * delta;
        }

        public void set(float lifetime, float x, float y, float z) {
            this.lifetime = lifetime;
            this.x = x;
            this.y = y;
            this.z = z;
        }
    }

    public static class TimelineValue implements Serializable, Json.Serializable {
        private static final long serialVersionUID = -4434625075360858305L;

        public float[] values;
        public float time;

        public boolean interpolated = false;
        public float[] valueStep;
        public float[] interpolatedValues;

        private transient float timeStep;
        private transient int arrayLen;
        private transient int i;

        public TimelineValue() {
        }

        public TimelineValue(float time, float... values) {
            this.time = time;
            this.values = values;
        }

        public float[] getValues() {
            return values;
        }

        protected void setValues(boolean interpolated, float[] valueStep, float[] interpolatedValues) {
            this.interpolated = interpolated;
            this.valueStep = valueStep;
            this.interpolatedValues = interpolatedValues;
        }

        public float[] getValuesInterpolated(float currentTime) {
            if (!interpolated) {
                return values;
            }

            timeStep = currentTime - time;
            arrayLen = values.length;
            for (i = 0; i < arrayLen; i++) {
                float value = values[i] + (valueStep[i] * timeStep);
                interpolatedValues[i] = value;
            }

            return interpolatedValues;
        }

        public void setInterpolated(boolean interpolated, TimelineValue nextValue) {
            this.interpolated = interpolated;

            if (interpolated) {
                interpolatedValues = new float[values.length];
                valueStep = new float[values.length];

                arrayLen = nextValue.values.length;
                for (i = 0; i < arrayLen; i++) {
                    float value = (nextValue.values[i] - values[i]) / (nextValue.time - time);
                    valueStep[i] = value;
                }
            }
        }

        public TimelineValue copy() {
            TimelineValue copy = new TimelineValue(time, values);
            copy.setValues(interpolated, valueStep, interpolatedValues);
            return copy;
        }

        public void write(Json json) {
            json.writeValue("time", time);
            json.writeValue("values", values);
            json.writeValue("interpolated", interpolated);
            if (interpolated)
                json.writeValue("value step", valueStep);
        }

        public void read(Json json, OrderedMap<String, Object> jsonMap) {
            Iterator<Entry<String, Object>> itr = jsonMap.entries().iterator();
            while (itr.hasNext()) {
                Entry<String, Object> entry = itr.next();
                if (entry.key.equals("time")) {
                    time = (Float) entry.value;
                } else if (entry.key.equals("values")) {
                    values = (float[]) entry.value;
                } else if (entry.key.equals("interpolated")) {
                    interpolated = (Boolean) entry.value;
                    if (interpolated) {
                        interpolatedValues = new float[values.length];
                    }
                } else if (entry.key.equals("value step")) {
                    valueStep = (float[]) entry.value;
                }
            }
        }
    }

    /**
     * Write this particle emitter to the given json instance.
     * @param json
     */
    private void write(Json json) {
        json.writeObjectStart();
        json.writeValue("name", name);
        json.writeValue("sprite", sprite);
        json.writeValue("size", size);
        json.writeValue("colour", colour);
        json.writeValue("velocity", velocity);

        json.writeValue("max particles", maxParticles);
        json.writeValue("particle lifetime", particleLifetime);
        json.writeValue("particle lifetime variance", particleLifetimeVar);
        json.writeValue("emission time", emissionTime);
        json.writeValue("emission x", ex);
        json.writeValue("emission y", ey);
        json.writeValue("emission z", ez);
        json.writeValue("emission type", emissionType);
        json.writeValue("blend func SRC", blendFuncSRC);
        json.writeValue("blend func DST", blendFuncDST);
        json.writeValue("atlas name", atlasName);

        if (lightUID != null) {
            json.writeValue("light attenuation", lightAttenuation);
            json.writeValue("light power", lightPower);
            json.writeValue("light static", isLightStatic);
            json.writeValue("light flicker", lightFlicker);
            json.writeValue("light colour", lightColour);
            json.writeValue("light offset x", lightx);
            json.writeValue("light offset y", lighty);
            json.writeValue("light offset z", lightz);
        }

        json.writeObjectEnd();
    }

    /**
     * Get a json instance set up for reading and writing a particle emitter
     * @return
     */
    public static Json getJson(Json json) {
        json.setSerializer(ParticleEmitter.class, new Json.Serializer<ParticleEmitter>() {
            @SuppressWarnings("rawtypes")
            public void write(Json json, ParticleEmitter emitter, Class knownType) {
                emitter.write(json);
            }

            @SuppressWarnings({ "unchecked", "rawtypes" })
            public ParticleEmitter read(Json json, Object jsonData, Class type) {

                // ----- Particle Parameters ----- //
                TimelineValue[] sprite = null;
                TimelineValue[] size = null;
                TimelineValue[] colour = null;
                TimelineValue[] velocity = null;
                // ----- End Particle Parameters ----- //

                // ----- Emitter parameters ----- //
                String name = null;
                int maxParticles = 0;
                float particleLifetime = 0;
                float particleLifetimeVar = 0;
                float emissionTime = 0;
                float ex = 0, ey = 0, ez = 0;
                int emissionType = 0;
                int blendFuncSRC = 0;
                int blendFuncDST = 0;
                String atlasName = null;
                // ----- End Emitter parameters ----- //

                // ----- Light ----- //
                float lightAttenuation = 0;
                float lightPower = 0;
                boolean isLightStatic = false;
                Color lightColour = null;
                boolean lightFlicker = false;
                float lightx = 0, lighty = 0, lightz = 0;
                // ----- End Light ----- //

                OrderedMap<String, Object> jsonMap = (OrderedMap<String, Object>) jsonData;
                Iterator<Entry<String, Object>> itr = jsonMap.entries();

                while (itr.hasNext()) {
                    Entry<String, Object> entry = itr.next();

                    if (entry.key.equals("sprite")) {
                        sprite = json.readValue(TimelineValue[].class, entry.value);
                    } else if (entry.key.equals("size")) {
                        size = json.readValue(TimelineValue[].class, entry.value);
                    } else if (entry.key.equals("colour")) {
                        colour = json.readValue(TimelineValue[].class, entry.value);
                    } else if (entry.key.equals("velocity")) {
                        velocity = json.readValue(TimelineValue[].class, entry.value);
                    }

                    else if (entry.key.equals("name")) {
                        name = (String) entry.value;
                    } else if (entry.key.equals("max particles")) {
                        maxParticles = ((Float) entry.value).intValue();
                    } else if (entry.key.equals("particle lifetime")) {
                        particleLifetime = (Float) entry.value;
                    } else if (entry.key.equals("particle lifetime variance")) {
                        particleLifetimeVar = (Float) entry.value;
                    } else if (entry.key.equals("emission time")) {
                        emissionTime = (Float) entry.value;
                    } else if (entry.key.equals("emission x")) {
                        ex = (Float) entry.value;
                    } else if (entry.key.equals("emission y")) {
                        ey = (Float) entry.value;
                    } else if (entry.key.equals("emission z")) {
                        ez = (Float) entry.value;
                    } else if (entry.key.equals("emission type")) {
                        emissionType = ((Float) entry.value).intValue();
                    } else if (entry.key.equals("blend func SRC")) {
                        blendFuncSRC = ((Float) entry.value).intValue();
                    } else if (entry.key.equals("blend func DST")) {
                        blendFuncDST = ((Float) entry.value).intValue();
                    } else if (entry.key.equals("atlas name")) {
                        atlasName = (String) entry.value;
                    }

                    else if (entry.key.equals("light attenuation")) {
                        lightAttenuation = (Float) entry.value;
                    } else if (entry.key.equals("light power")) {
                        lightPower = (Float) entry.value;
                    } else if (entry.key.equals("light static")) {
                        isLightStatic = (Boolean) entry.value;
                    } else if (entry.key.equals("light flicker")) {
                        lightFlicker = (Boolean) entry.value;
                    } else if (entry.key.equals("light colour")) {
                        lightColour = json.readValue(Color.class, entry.value);
                    } else if (entry.key.equals("light offset x")) {
                        lightx = (Float) entry.value;
                    } else if (entry.key.equals("light offset y")) {
                        lighty = (Float) entry.value;
                    } else if (entry.key.equals("light offset z")) {
                        lightz = (Float) entry.value;
                    }
                }

                ParticleEmitter emitter = new ParticleEmitter(particleLifetime, particleLifetimeVar, emissionTime,
                        ex, ey, ez, emissionType, blendFuncSRC, blendFuncDST, atlasName, name);
                emitter.maxParticles = maxParticles;

                emitter.setTimeline(sprite, size, colour, velocity);

                if (lightColour != null)
                    emitter.addLight(isLightStatic, lightAttenuation, lightPower, lightColour, lightFlicker, lightx,
                            lighty, lightz);

                return emitter;
            }
        });

        return json;
    }

    public static ParticleEmitter load(String file) {
        Json json = getJson(new Json());

        return json.fromJson(ParticleEmitter.class, file);
    }

    public static ParticleEmitter load(FileHandle file) {
        Json json = getJson(new Json());

        return json.fromJson(ParticleEmitter.class, file);
    }

    private static final String SHADER_VERTEX =

            "attribute vec3 a_position;" + "\n" + "attribute vec4 a_colour;" + "\n" + "attribute vec2 a_texCoords;"
                    + "\n" +

                    "uniform mat4 u_pv;" + "\n" +

                    "varying vec4 v_colour;" + "\n" + "varying vec2 v_texCoords;" + "\n" +

                    "void main() {" + "\n" + "v_colour = a_colour;" + "\n" + "v_texCoords = a_texCoords;" + "\n" +

                    "gl_Position = u_pv * vec4(a_position, 1.0);" + "\n" + "}";

    private static final String SHADER_FRAGMENT = "#ifdef GL_ES\n" + "precision highp float;\n" + "#endif\n" +

            "uniform sampler2D u_texture;" + "\n" +

            "varying vec4 v_colour;" + "\n" + "varying vec2 v_texCoords;" + "\n" +

            "void main() {" + "\n" + "gl_FragColor = texture2D(u_texture, v_texCoords) * v_colour;" + "\n" + "}";

    private static final ParticleEmitterComparator comparator = new ParticleEmitterComparator();

    public static Comparator<ParticleEmitter> getComparator() {
        return comparator;
    }

    static class ParticleEmitterComparator implements Comparator<ParticleEmitter> {
        public int compare(ParticleEmitter p1, ParticleEmitter p2) {
            return (p1.distance < p2.distance) ? 1 : -1;
        }
    }
}