/* * Copyright (c) 2009-2012 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.jme3.effect; import com.jme3.bounding.BoundingBox; import com.jme3.effect.ParticleMesh.Type; import com.jme3.effect.influencers.DefaultParticleInfluencer; import com.jme3.effect.influencers.ParticleInfluencer; import com.jme3.effect.shapes.EmitterPointShape; import com.jme3.effect.shapes.EmitterShape; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.math.Matrix3f; import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.renderer.queue.RenderQueue.Bucket; import com.jme3.renderer.queue.RenderQueue.ShadowMode; import com.jme3.scene.Geometry; import com.jme3.scene.Spatial; import com.jme3.scene.control.Control; import com.jme3.util.TempVars; import com.jme3.util.clone.Cloner; import com.jme3.util.clone.JmeCloneable; import java.io.IOException; /** * <code>ParticleEmitter</code> is a special kind of geometry which simulates * a particle system. * <p> * Particle emitters can be used to simulate various kinds of phenomena, * such as fire, smoke, explosions and much more. * <p> * Particle emitters have many properties which are used to control the * simulation. The interpretation of these properties depends on the * {@link ParticleInfluencer} that has been assigned to the emitter via * {@link ParticleEmitter#setParticleInfluencer(com.jme3.effect.influencers.ParticleInfluencer) }. * By default the implementation {@link DefaultParticleInfluencer} is used. * * @author Kirill Vainer */ public class ParticleEmitter extends Geometry { private boolean enabled = true; private static final EmitterShape DEFAULT_SHAPE = new EmitterPointShape(Vector3f.ZERO); private static final ParticleInfluencer DEFAULT_INFLUENCER = new DefaultParticleInfluencer(); private ParticleEmitterControl control; private EmitterShape shape = DEFAULT_SHAPE; private ParticleMesh particleMesh; private ParticleInfluencer particleInfluencer = DEFAULT_INFLUENCER; private ParticleMesh.Type meshType; private Particle[] particles; private int firstUnUsed; private int lastUsed; // private int next = 0; // private ArrayList<Integer> unusedIndices = new ArrayList<Integer>(); private boolean randomAngle; private boolean selectRandomImage; private boolean facingVelocity; private float particlesPerSec = 20; private float timeDifference = 0; private float lowLife = 3f; private float highLife = 7f; private Vector3f gravity = new Vector3f(0.0f, 0.1f, 0.0f); private float rotateSpeed; private Vector3f faceNormal = new Vector3f(Vector3f.NAN); private int imagesX = 1; private int imagesY = 1; private ColorRGBA startColor = new ColorRGBA(0.4f, 0.4f, 0.4f, 0.5f); private ColorRGBA endColor = new ColorRGBA(0.1f, 0.1f, 0.1f, 0.0f); private float startSize = 0.2f; private float endSize = 2f; private boolean worldSpace = true; //variable that helps with computations private transient Vector3f temp = new Vector3f(); private transient Vector3f lastPos; public static class ParticleEmitterControl implements Control, JmeCloneable { ParticleEmitter parentEmitter; public ParticleEmitterControl() { } public ParticleEmitterControl(ParticleEmitter parentEmitter) { this.parentEmitter = parentEmitter; } @Override public Control cloneForSpatial(Spatial spatial) { return this; // WARNING: Sets wrong control on spatial. Will be // fixed automatically by ParticleEmitter.clone() method. } @Override public Object jmeClone() { try { return super.clone(); } catch( CloneNotSupportedException e ) { throw new RuntimeException("Error cloning", e); } } @Override public void cloneFields( Cloner cloner, Object original ) { this.parentEmitter = cloner.clone(parentEmitter); } public void setSpatial(Spatial spatial) { } public void setEnabled(boolean enabled) { parentEmitter.setEnabled(enabled); } public boolean isEnabled() { return parentEmitter.isEnabled(); } public void update(float tpf) { parentEmitter.updateFromControl(tpf); } public void render(RenderManager rm, ViewPort vp) { parentEmitter.renderFromControl(rm, vp); } public void write(JmeExporter ex) throws IOException { } public void read(JmeImporter im) throws IOException { } } @Override public ParticleEmitter clone() { return clone(true); } @Override public ParticleEmitter clone(boolean cloneMaterial) { return (ParticleEmitter)super.clone(cloneMaterial); } /** * The old clone() method that did not use the new Cloner utility. */ public ParticleEmitter oldClone(boolean cloneMaterial) { ParticleEmitter clone = (ParticleEmitter) super.clone(cloneMaterial); clone.shape = shape.deepClone(); // Reinitialize particle list clone.setNumParticles(particles.length); clone.faceNormal = faceNormal.clone(); clone.startColor = startColor.clone(); clone.endColor = endColor.clone(); clone.particleInfluencer = particleInfluencer.clone(); // remove original control from the clone clone.controls.remove(this.control); // put clone's control in clone.control = new ParticleEmitterControl(clone); clone.controls.add(clone.control); // Reinitialize particle mesh switch (meshType) { case Point: clone.particleMesh = new ParticlePointMesh(); clone.setMesh(clone.particleMesh); break; case Triangle: clone.particleMesh = new ParticleTriMesh(); clone.setMesh(clone.particleMesh); break; default: throw new IllegalStateException("Unrecognized particle type: " + meshType); } clone.particleMesh.initParticleData(clone, clone.particles.length); clone.particleMesh.setImagesXY(clone.imagesX, clone.imagesY); return clone; } /** * Called internally by com.jme3.util.clone.Cloner. Do not call directly. */ @Override public void cloneFields( Cloner cloner, Object original ) { super.cloneFields(cloner, original); this.shape = cloner.clone(shape); this.control = cloner.clone(control); this.faceNormal = cloner.clone(faceNormal); this.startColor = cloner.clone(startColor); this.endColor = cloner.clone(endColor); this.particleInfluencer = cloner.clone(particleInfluencer); // change in behavior: gravity was not cloned before -pspeed this.gravity = cloner.clone(gravity); // So, simply setting the mesh type will cause all kinds of things // to happen: // 1) the new mesh gets created. // 2) it is set to the Geometry // 3) the particles array is recreated because setNumParticles() // // ...so this should be equivalent but simpler than half of the old clone() // method. Note: we do not ever want to share particleMesh so we do not // clone it at all. setMeshType(meshType); // change in behavior: temp and lastPos were not cloned before... // perhaps because it was believed that 'transient' fields were exluded // from cloning? (they aren't) // If it was ok for these to be shared because of how they are used // then they could just as well be made static... else I think it's clearer // to clone them. this.temp = cloner.clone(temp); this.lastPos = cloner.clone(lastPos); } public ParticleEmitter(String name, Type type, int numParticles) { super(name); setBatchHint(BatchHint.Never); // ignore world transform, unless user sets inLocalSpace this.setIgnoreTransform(true); // particles neither receive nor cast shadows this.setShadowMode(ShadowMode.Off); // particles are usually transparent this.setQueueBucket(Bucket.Transparent); meshType = type; // Must create clone of shape/influencer so that a reference to a static is // not maintained shape = shape.deepClone(); particleInfluencer = particleInfluencer.clone(); control = new ParticleEmitterControl(this); controls.add(control); switch (meshType) { case Point: particleMesh = new ParticlePointMesh(); this.setMesh(particleMesh); break; case Triangle: particleMesh = new ParticleTriMesh(); this.setMesh(particleMesh); break; default: throw new IllegalStateException("Unrecognized particle type: " + meshType); } this.setNumParticles(numParticles); // particleMesh.initParticleData(this, particles.length); } /** * For serialization only. Do not use. */ public ParticleEmitter() { super(); setBatchHint(BatchHint.Never); } public void setShape(EmitterShape shape) { this.shape = shape; } public EmitterShape getShape() { return shape; } /** * Set the {@link ParticleInfluencer} to influence this particle emitter. * * @param particleInfluencer the {@link ParticleInfluencer} to influence * this particle emitter. * * @see ParticleInfluencer */ public void setParticleInfluencer(ParticleInfluencer particleInfluencer) { this.particleInfluencer = particleInfluencer; } /** * Returns the {@link ParticleInfluencer} that influences this * particle emitter. * * @return the {@link ParticleInfluencer} that influences this * particle emitter. * * @see ParticleInfluencer */ public ParticleInfluencer getParticleInfluencer() { return particleInfluencer; } /** * Returns the mesh type used by the particle emitter. * * * @return the mesh type used by the particle emitter. * * @see #setMeshType(com.jme3.effect.ParticleMesh.Type) * @see ParticleEmitter#ParticleEmitter(java.lang.String, com.jme3.effect.ParticleMesh.Type, int) */ public ParticleMesh.Type getMeshType() { return meshType; } /** * Sets the type of mesh used by the particle emitter. * @param meshType The mesh type to use */ public void setMeshType(ParticleMesh.Type meshType) { this.meshType = meshType; switch (meshType) { case Point: particleMesh = new ParticlePointMesh(); this.setMesh(particleMesh); break; case Triangle: particleMesh = new ParticleTriMesh(); this.setMesh(particleMesh); break; default: throw new IllegalStateException("Unrecognized particle type: " + meshType); } this.setNumParticles(particles.length); } /** * Returns true if particles should spawn in world space. * * @return true if particles should spawn in world space. * * @see ParticleEmitter#setInWorldSpace(boolean) */ public boolean isInWorldSpace() { return worldSpace; } /** * Set to true if particles should spawn in world space. * * <p>If set to true and the particle emitter is moved in the scene, * then particles that have already spawned won't be effected by this * motion. If set to false, the particles will emit in local space * and when the emitter is moved, so are all the particles that * were emitted previously. * * @param worldSpace true if particles should spawn in world space. */ public void setInWorldSpace(boolean worldSpace) { this.setIgnoreTransform(worldSpace); this.worldSpace = worldSpace; } /** * Returns the number of visible particles (spawned but not dead). * * @return the number of visible particles */ public int getNumVisibleParticles() { // return unusedIndices.size() + next; return lastUsed + 1; } /** * Set the maximum amount of particles that * can exist at the same time with this emitter. * Calling this method many times is not recommended. * * @param numParticles the maximum amount of particles that * can exist at the same time with this emitter. */ public final void setNumParticles(int numParticles) { particles = new Particle[numParticles]; for (int i = 0; i < numParticles; i++) { particles[i] = new Particle(); } //We have to reinit the mesh's buffers with the new size particleMesh.initParticleData(this, particles.length); particleMesh.setImagesXY(this.imagesX, this.imagesY); firstUnUsed = 0; lastUsed = -1; } public int getMaxNumParticles() { return particles.length; } /** * Returns a list of all particles (shouldn't be used in most cases). * * <p> * This includes both existing and non-existing particles. * The size of the array is set to the <code>numParticles</code> value * specified in the constructor or {@link ParticleEmitter#setNumParticles(int) } * method. * * @return a list of all particles. */ public Particle[] getParticles() { return particles; } /** * Get the normal which particles are facing. * * @return the normal which particles are facing. * * @see ParticleEmitter#setFaceNormal(com.jme3.math.Vector3f) */ public Vector3f getFaceNormal() { if (Vector3f.isValidVector(faceNormal)) { return faceNormal; } else { return null; } } /** * Sets the normal which particles are facing. * * <p>By default, particles * will face the camera, but for some effects (e.g shockwave) it may * be necessary to face a specific direction instead. To restore * normal functionality, provide <code>null</code> as the argument for * <code>faceNormal</code>. * * @param faceNormal The normals particles should face, or <code>null</code> * if particles should face the camera. */ public void setFaceNormal(Vector3f faceNormal) { if (faceNormal == null || !Vector3f.isValidVector(faceNormal)) { this.faceNormal.set(Vector3f.NAN); } else { this.faceNormal = faceNormal; } } /** * Returns the rotation speed in radians/sec for particles. * * @return the rotation speed in radians/sec for particles. * * @see ParticleEmitter#setRotateSpeed(float) */ public float getRotateSpeed() { return rotateSpeed; } /** * Set the rotation speed in radians/sec for particles * spawned after the invocation of this method. * * @param rotateSpeed the rotation speed in radians/sec for particles * spawned after the invocation of this method. */ public void setRotateSpeed(float rotateSpeed) { this.rotateSpeed = rotateSpeed; } /** * Returns true if every particle spawned * should have a random facing angle. * * @return true if every particle spawned * should have a random facing angle. * * @see ParticleEmitter#setRandomAngle(boolean) */ public boolean isRandomAngle() { return randomAngle; } /** * Set to true if every particle spawned * should have a random facing angle. * * @param randomAngle if every particle spawned * should have a random facing angle. */ public void setRandomAngle(boolean randomAngle) { this.randomAngle = randomAngle; } /** * Returns true if every particle spawned should get a random * image. * * @return True if every particle spawned should get a random * image. * * @see ParticleEmitter#setSelectRandomImage(boolean) */ public boolean isSelectRandomImage() { return selectRandomImage; } /** * Set to true if every particle spawned * should get a random image from a pool of images constructed from * the texture, with X by Y possible images. * * <p>By default, X and Y are equal * to 1, thus allowing only 1 possible image to be selected, but if the * particle is configured with multiple images by using {@link ParticleEmitter#setImagesX(int) } * and {#link ParticleEmitter#setImagesY(int) } methods, then multiple images * can be selected. Setting to false will cause each particle to have an animation * of images displayed, starting at image 1, and going until image X*Y when * the particle reaches its end of life. * * @param selectRandomImage True if every particle spawned should get a random * image. */ public void setSelectRandomImage(boolean selectRandomImage) { this.selectRandomImage = selectRandomImage; } /** * Check if particles spawned should face their velocity. * * @return True if particles spawned should face their velocity. * * @see ParticleEmitter#setFacingVelocity(boolean) */ public boolean isFacingVelocity() { return facingVelocity; } /** * Set to true if particles spawned should face * their velocity (or direction to which they are moving towards). * * <p>This is typically used for e.g spark effects. * * @param followVelocity True if particles spawned should face their velocity. * */ public void setFacingVelocity(boolean followVelocity) { this.facingVelocity = followVelocity; } /** * Get the end color of the particles spawned. * * @return the end color of the particles spawned. * * @see ParticleEmitter#setEndColor(com.jme3.math.ColorRGBA) */ public ColorRGBA getEndColor() { return endColor; } /** * Set the end color of the particles spawned. * * <p>The * particle color at any time is determined by blending the start color * and end color based on the particle's current time of life relative * to its end of life. * * @param endColor the end color of the particles spawned. */ public void setEndColor(ColorRGBA endColor) { this.endColor.set(endColor); } /** * Get the end size of the particles spawned. * * @return the end size of the particles spawned. * * @see ParticleEmitter#setEndSize(float) */ public float getEndSize() { return endSize; } /** * Set the end size of the particles spawned. * * <p>The * particle size at any time is determined by blending the start size * and end size based on the particle's current time of life relative * to its end of life. * * @param endSize the end size of the particles spawned. */ public void setEndSize(float endSize) { this.endSize = endSize; } /** * Get the gravity vector. * * @return the gravity vector. * * @see ParticleEmitter#setGravity(com.jme3.math.Vector3f) */ public Vector3f getGravity() { return gravity; } /** * This method sets the gravity vector. * * @param gravity the gravity vector */ public void setGravity(Vector3f gravity) { this.gravity.set(gravity); } /** * Sets the gravity vector. * * @param x the x component of the gravity vector * @param y the y component of the gravity vector * @param z the z component of the gravity vector */ public void setGravity(float x, float y, float z) { this.gravity.x = x; this.gravity.y = y; this.gravity.z = z; } /** * Get the high value of life. * * @return the high value of life. * * @see ParticleEmitter#setHighLife(float) */ public float getHighLife() { return highLife; } /** * Set the high value of life. * * <p>The particle's lifetime/expiration * is determined by randomly selecting a time between low life and high life. * * @param highLife the high value of life. */ public void setHighLife(float highLife) { this.highLife = highLife; } /** * Get the number of images along the X axis (width). * * @return the number of images along the X axis (width). * * @see ParticleEmitter#setImagesX(int) */ public int getImagesX() { return imagesX; } /** * Set the number of images along the X axis (width). * * <p>To determine * how multiple particle images are selected and used, see the * {@link ParticleEmitter#setSelectRandomImage(boolean) } method. * * @param imagesX the number of images along the X axis (width). */ public void setImagesX(int imagesX) { this.imagesX = imagesX; particleMesh.setImagesXY(this.imagesX, this.imagesY); } /** * Get the number of images along the Y axis (height). * * @return the number of images along the Y axis (height). * * @see ParticleEmitter#setImagesY(int) */ public int getImagesY() { return imagesY; } /** * Set the number of images along the Y axis (height). * * <p>To determine how multiple particle images are selected and used, see the * {@link ParticleEmitter#setSelectRandomImage(boolean) } method. * * @param imagesY the number of images along the Y axis (height). */ public void setImagesY(int imagesY) { this.imagesY = imagesY; particleMesh.setImagesXY(this.imagesX, this.imagesY); } /** * Get the low value of life. * * @return the low value of life. * * @see ParticleEmitter#setLowLife(float) */ public float getLowLife() { return lowLife; } /** * Set the low value of life. * * <p>The particle's lifetime/expiration * is determined by randomly selecting a time between low life and high life. * * @param lowLife the low value of life. */ public void setLowLife(float lowLife) { this.lowLife = lowLife; } /** * Get the number of particles to spawn per * second. * * @return the number of particles to spawn per * second. * * @see ParticleEmitter#setParticlesPerSec(float) */ public float getParticlesPerSec() { return particlesPerSec; } /** * Set the number of particles to spawn per * second. * * @param particlesPerSec the number of particles to spawn per * second. */ public void setParticlesPerSec(float particlesPerSec) { this.particlesPerSec = particlesPerSec; timeDifference = 0; } /** * Get the start color of the particles spawned. * * @return the start color of the particles spawned. * * @see ParticleEmitter#setStartColor(com.jme3.math.ColorRGBA) */ public ColorRGBA getStartColor() { return startColor; } /** * Set the start color of the particles spawned. * * <p>The particle color at any time is determined by blending the start color * and end color based on the particle's current time of life relative * to its end of life. * * @param startColor the start color of the particles spawned */ public void setStartColor(ColorRGBA startColor) { this.startColor.set(startColor); } /** * Get the start color of the particles spawned. * * @return the start color of the particles spawned. * * @see ParticleEmitter#setStartSize(float) */ public float getStartSize() { return startSize; } /** * Set the start size of the particles spawned. * * <p>The particle size at any time is determined by blending the start size * and end size based on the particle's current time of life relative * to its end of life. * * @param startSize the start size of the particles spawned. */ public void setStartSize(float startSize) { this.startSize = startSize; } /** * @deprecated Use ParticleEmitter.getParticleInfluencer().getInitialVelocity() instead. */ @Deprecated public Vector3f getInitialVelocity() { return particleInfluencer.getInitialVelocity(); } /** * @param initialVelocity Set the initial velocity a particle is spawned with, * the initial velocity given in the parameter will be varied according * to the velocity variation set in {@link ParticleEmitter#setVelocityVariation(float) }. * A particle will move toward its velocity unless it is effected by the * gravity. * * @deprecated * This method is deprecated. * Use ParticleEmitter.getParticleInfluencer().setInitialVelocity(initialVelocity); instead. * * @see ParticleEmitter#setVelocityVariation(float) * @see ParticleEmitter#setGravity(float) */ @Deprecated public void setInitialVelocity(Vector3f initialVelocity) { this.particleInfluencer.setInitialVelocity(initialVelocity); } /** * @deprecated * This method is deprecated. * Use ParticleEmitter.getParticleInfluencer().getVelocityVariation(); instead. * @return the initial velocity variation factor */ @Deprecated public float getVelocityVariation() { return particleInfluencer.getVelocityVariation(); } /** * @param variation Set the variation by which the initial velocity * of the particle is determined. <code>variation</code> should be a value * from 0 to 1, where 0 means particles are to spawn with exactly * the velocity given in {@link ParticleEmitter#setStartVel(com.jme3.math.Vector3f) }, * and 1 means particles are to spawn with a completely random velocity. * * @deprecated * This method is deprecated. * Use ParticleEmitter.getParticleInfluencer().setVelocityVariation(variation); instead. */ @Deprecated public void setVelocityVariation(float variation) { this.particleInfluencer.setVelocityVariation(variation); } private Particle emitParticle(Vector3f min, Vector3f max) { int idx = lastUsed + 1; if (idx >= particles.length) { return null; } Particle p = particles[idx]; if (selectRandomImage) { p.imageIndex = FastMath.nextRandomInt(0, imagesY - 1) * imagesX + FastMath.nextRandomInt(0, imagesX - 1); } p.startlife = lowLife + FastMath.nextRandomFloat() * (highLife - lowLife); p.life = p.startlife; p.color.set(startColor); p.size = startSize; //shape.getRandomPoint(p.position); particleInfluencer.influenceParticle(p, shape); if (worldSpace) { worldTransform.transformVector(p.position, p.position); worldTransform.getRotation().mult(p.velocity, p.velocity); // TODO: Make scale relevant somehow?? } if (randomAngle) { p.angle = FastMath.nextRandomFloat() * FastMath.TWO_PI; } if (rotateSpeed != 0) { p.rotateSpeed = rotateSpeed * (0.2f + (FastMath.nextRandomFloat() * 2f - 1f) * .8f); } temp.set(p.position).addLocal(p.size, p.size, p.size); max.maxLocal(temp); temp.set(p.position).subtractLocal(p.size, p.size, p.size); min.minLocal(temp); ++lastUsed; firstUnUsed = idx + 1; return p; } /** * Instantly emits all the particles possible to be emitted. Any particles * which are currently inactive will be spawned immediately. */ public void emitAllParticles() { emitParticles(particles.length); } /** * Instantly emits available particles, up to num. */ public void emitParticles(int num) { // Force world transform to update this.getWorldTransform(); TempVars vars = TempVars.get(); BoundingBox bbox = (BoundingBox) this.getMesh().getBound(); Vector3f min = vars.vect1; Vector3f max = vars.vect2; bbox.getMin(min); bbox.getMax(max); if (!Vector3f.isValidVector(min)) { min.set(Vector3f.POSITIVE_INFINITY); } if (!Vector3f.isValidVector(max)) { max.set(Vector3f.NEGATIVE_INFINITY); } for(int i=0;i<num;i++) { if( emitParticle(min, max) == null ) break; } bbox.setMinMax(min, max); this.setBoundRefresh(); vars.release(); } /** * Instantly kills all active particles, after this method is called, all * particles will be dead and no longer visible. */ public void killAllParticles() { for (int i = 0; i < particles.length; ++i) { if (particles[i].life > 0) { this.freeParticle(i); } } } /** * Kills the particle at the given index. * * @param index The index of the particle to kill * @see #getParticles() */ public void killParticle(int index){ freeParticle(index); } private void freeParticle(int idx) { Particle p = particles[idx]; p.life = 0; p.size = 0f; p.color.set(0, 0, 0, 0); p.imageIndex = 0; p.angle = 0; p.rotateSpeed = 0; if (idx == lastUsed) { while (lastUsed >= 0 && particles[lastUsed].life == 0) { lastUsed--; } } if (idx < firstUnUsed) { firstUnUsed = idx; } } private void swap(int idx1, int idx2) { Particle p1 = particles[idx1]; particles[idx1] = particles[idx2]; particles[idx2] = p1; } private void updateParticle(Particle p, float tpf, Vector3f min, Vector3f max){ // applying gravity p.velocity.x -= gravity.x * tpf; p.velocity.y -= gravity.y * tpf; p.velocity.z -= gravity.z * tpf; temp.set(p.velocity).multLocal(tpf); p.position.addLocal(temp); // affecting color, size and angle float b = (p.startlife - p.life) / p.startlife; p.color.interpolateLocal(startColor, endColor, b); p.size = FastMath.interpolateLinear(b, startSize, endSize); p.angle += p.rotateSpeed * tpf; // Computing bounding volume temp.set(p.position).addLocal(p.size, p.size, p.size); max.maxLocal(temp); temp.set(p.position).subtractLocal(p.size, p.size, p.size); min.minLocal(temp); if (!selectRandomImage) { p.imageIndex = (int) (b * imagesX * imagesY); } } private void updateParticleState(float tpf) { // Force world transform to update this.getWorldTransform(); TempVars vars = TempVars.get(); Vector3f min = vars.vect1.set(Vector3f.POSITIVE_INFINITY); Vector3f max = vars.vect2.set(Vector3f.NEGATIVE_INFINITY); for (int i = 0; i < particles.length; ++i) { Particle p = particles[i]; if (p.life == 0) { // particle is dead // assert i <= firstUnUsed; continue; } p.life -= tpf; if (p.life <= 0) { this.freeParticle(i); continue; } updateParticle(p, tpf, min, max); if (firstUnUsed < i) { this.swap(firstUnUsed, i); if (i == lastUsed) { lastUsed = firstUnUsed; } firstUnUsed++; } } // Spawns particles within the tpf timeslot with proper age float interval = 1f / particlesPerSec; float originalTpf = tpf; tpf += timeDifference; while (tpf > interval){ tpf -= interval; Particle p = emitParticle(min, max); if (p != null){ p.life -= tpf; if (lastPos != null && isInWorldSpace()) { p.position.interpolateLocal(lastPos, 1 - tpf / originalTpf); } if (p.life <= 0){ freeParticle(lastUsed); }else{ updateParticle(p, tpf, min, max); } } } timeDifference = tpf; if (lastPos == null) { lastPos = new Vector3f(); } lastPos.set(getWorldTranslation()); //This check avoids a NaN bounds when all the particles are dead during the first update. if (!min.equals(Vector3f.POSITIVE_INFINITY) && !max.equals(Vector3f.NEGATIVE_INFINITY)) { BoundingBox bbox = (BoundingBox) this.getMesh().getBound(); bbox.setMinMax(min, max); this.setBoundRefresh(); } vars.release(); } /** * Set to enable or disable the particle emitter * * <p>When a particle is * disabled, it will be "frozen in time" and not update. * * @param enabled True to enable the particle emitter */ public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * Check if a particle emitter is enabled for update. * * @return True if a particle emitter is enabled for update. * * @see ParticleEmitter#setEnabled(boolean) */ public boolean isEnabled() { return enabled; } /** * Callback from Control.update(), do not use. * @param tpf */ public void updateFromControl(float tpf) { if (enabled) { this.updateParticleState(tpf); } } /** * Callback from Control.render(), do not use. * * @param rm * @param vp */ private void renderFromControl(RenderManager rm, ViewPort vp) { Camera cam = vp.getCamera(); if (meshType == ParticleMesh.Type.Point) { float C = cam.getProjectionMatrix().m00; C *= cam.getWidth() * 0.5f; // send attenuation params this.getMaterial().setFloat("Quadratic", C); } Matrix3f inverseRotation = Matrix3f.IDENTITY; TempVars vars = null; if (!worldSpace) { vars = TempVars.get(); inverseRotation = this.getWorldRotation().toRotationMatrix(vars.tempMat3).invertLocal(); } particleMesh.updateParticleData(particles, cam, inverseRotation); if (!worldSpace) { vars.release(); } } public void preload(RenderManager rm, ViewPort vp) { this.updateParticleState(0); particleMesh.updateParticleData(particles, vp.getCamera(), Matrix3f.IDENTITY); } @Override public void write(JmeExporter ex) throws IOException { super.write(ex); OutputCapsule oc = ex.getCapsule(this); oc.write(shape, "shape", DEFAULT_SHAPE); oc.write(meshType, "meshType", ParticleMesh.Type.Triangle); oc.write(enabled, "enabled", true); oc.write(particles.length, "numParticles", 0); oc.write(particlesPerSec, "particlesPerSec", 0); oc.write(lowLife, "lowLife", 0); oc.write(highLife, "highLife", 0); oc.write(gravity, "gravity", null); oc.write(imagesX, "imagesX", 1); oc.write(imagesY, "imagesY", 1); oc.write(startColor, "startColor", null); oc.write(endColor, "endColor", null); oc.write(startSize, "startSize", 0); oc.write(endSize, "endSize", 0); oc.write(worldSpace, "worldSpace", false); oc.write(facingVelocity, "facingVelocity", false); oc.write(faceNormal, "faceNormal", new Vector3f(Vector3f.NAN)); oc.write(selectRandomImage, "selectRandomImage", false); oc.write(randomAngle, "randomAngle", false); oc.write(rotateSpeed, "rotateSpeed", 0); oc.write(particleInfluencer, "influencer", DEFAULT_INFLUENCER); } @Override public void read(JmeImporter im) throws IOException { super.read(im); InputCapsule ic = im.getCapsule(this); shape = (EmitterShape) ic.readSavable("shape", DEFAULT_SHAPE); if (shape == DEFAULT_SHAPE) { // Prevent reference to static shape = shape.deepClone(); } meshType = ic.readEnum("meshType", ParticleMesh.Type.class, ParticleMesh.Type.Triangle); int numParticles = ic.readInt("numParticles", 0); enabled = ic.readBoolean("enabled", true); particlesPerSec = ic.readFloat("particlesPerSec", 0); lowLife = ic.readFloat("lowLife", 0); highLife = ic.readFloat("highLife", 0); gravity = (Vector3f) ic.readSavable("gravity", null); imagesX = ic.readInt("imagesX", 1); imagesY = ic.readInt("imagesY", 1); startColor = (ColorRGBA) ic.readSavable("startColor", null); endColor = (ColorRGBA) ic.readSavable("endColor", null); startSize = ic.readFloat("startSize", 0); endSize = ic.readFloat("endSize", 0); worldSpace = ic.readBoolean("worldSpace", false); this.setIgnoreTransform(worldSpace); facingVelocity = ic.readBoolean("facingVelocity", false); faceNormal = (Vector3f)ic.readSavable("faceNormal", new Vector3f(Vector3f.NAN)); selectRandomImage = ic.readBoolean("selectRandomImage", false); randomAngle = ic.readBoolean("randomAngle", false); rotateSpeed = ic.readFloat("rotateSpeed", 0); switch (meshType) { case Point: particleMesh = new ParticlePointMesh(); this.setMesh(particleMesh); break; case Triangle: particleMesh = new ParticleTriMesh(); this.setMesh(particleMesh); break; default: throw new IllegalStateException("Unrecognized particle type: " + meshType); } this.setNumParticles(numParticles); // particleMesh.initParticleData(this, particles.length); // particleMesh.setImagesXY(imagesX, imagesY); particleInfluencer = (ParticleInfluencer) ic.readSavable("influencer", DEFAULT_INFLUENCER); if (particleInfluencer == DEFAULT_INFLUENCER) { particleInfluencer = particleInfluencer.clone(); } if (im.getFormatVersion() == 0) { // compatibility before the control inside particle emitter // was changed: // find it in the controls and take it out, then add the proper one in for (int i = 0; i < controls.size(); i++) { Object obj = controls.get(i); if (obj instanceof ParticleEmitter) { controls.remove(i); // now add the proper one in controls.add(new ParticleEmitterControl(this)); break; } } // compatability before gravity was not a vector but a float if (gravity == null) { gravity = new Vector3f(); gravity.y = ic.readFloat("gravity", 0); } } else { // since the parentEmitter is not loaded, it must be // loaded separately control = getControl(ParticleEmitterControl.class); control.parentEmitter = this; } } }