/*
* 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.animation;
import com.jme3.effect.ParticleEmitter;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;
import com.jme3.util.TempVars;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* EffectTrack is a track to add to an existing animation, to emmit particles
* during animations for example : exhausts, dust raised by foot steps, shock
* waves, lightnings etc...
*
* usage is
* <pre>
* AnimControl control model.getControl(AnimControl.class);
* EffectTrack track = new EffectTrack(existingEmmitter, control.getAnim("TheAnim").getLength());
* control.getAnim("TheAnim").addTrack(track);
* </pre>
*
* if the emitter has emmits 0 particles per seconds emmitAllPArticles will be
* called on it at time 0 + startOffset. if it he it has more it will start
* emmit normally at time 0 + startOffset.
*
*
* @author Nehon
*/
public class EffectTrack implements ClonableTrack {
private static final Logger logger = Logger.getLogger(EffectTrack.class.getName());
private ParticleEmitter emitter;
private float startOffset = 0;
private float particlesPerSeconds = 0;
private float length = 0;
private boolean emitted = false;
private boolean initialized = false;
//control responsible for disable and cull the emitter once all particles are gone
private KillParticleControl killParticles = new KillParticleControl();
public static class KillParticleControl extends AbstractControl {
ParticleEmitter emitter;
boolean stopRequested = false;
boolean remove = false;
public KillParticleControl() {
}
@Override
public void setSpatial(Spatial spatial) {
super.setSpatial(spatial);
if (spatial != null) {
if (spatial instanceof ParticleEmitter) {
emitter = (ParticleEmitter) spatial;
} else {
throw new IllegalArgumentException("KillParticleEmitter can only ba attached to ParticleEmitter");
}
}
}
@Override
protected void controlUpdate(float tpf) {
if (remove) {
emitter.removeControl(this);
return;
}
if (emitter.getNumVisibleParticles() == 0) {
emitter.setCullHint(CullHint.Always);
emitter.setEnabled(false);
emitter.removeControl(this);
stopRequested = false;
}
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
}
@Override
public Control cloneForSpatial(Spatial spatial) {
KillParticleControl c = new KillParticleControl();
//this control should be removed as it shouldn't have been persisted in the first place
//In the quest to find the less hackish solution to achieve this,
//making it remove itself from the spatial in the first update loop when loaded was the less bad.
c.remove = true;
c.setSpatial(spatial);
return c;
}
};
//Anim listener that stops the Emmitter when the animation is finished or changed.
private class OnEndListener implements AnimEventListener {
public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
stop();
}
public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
}
}
/**
* default constructor only for serialization
*/
public EffectTrack() {
}
/**
* Creates and EffectTrack
*
* @param emitter the emmitter of the track
* @param length the length of the track (usually the length of the
* animation you want to add the track to)
*/
public EffectTrack(ParticleEmitter emitter, float length) {
this.emitter = emitter;
//saving particles per second value
this.particlesPerSeconds = emitter.getParticlesPerSec();
//setting the emmitter to not emmit.
this.emitter.setParticlesPerSec(0);
this.length = length;
//Marking the emitter with a reference to this track for further use in deserialization.
setUserData(this);
}
/**
* Creates and EffectTrack
*
* @param emitter the emmitter of the track
* @param length the length of the track (usually the length of the
* animation you want to add the track to)
* @param startOffset the time in second when the emitter will be triggerd
* after the animation starts (default is 0)
*/
public EffectTrack(ParticleEmitter emitter, float length, float startOffset) {
this(emitter, length);
this.startOffset = startOffset;
}
/**
* Internal use only
*
* @see Track#setTime(float, float, com.jme3.animation.AnimControl,
* com.jme3.animation.AnimChannel, com.jme3.util.TempVars)
*/
public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars) {
if (time >= length) {
return;
}
//first time adding the Animation listener to stop the track at the end of the animation
if (!initialized) {
control.addListener(new OnEndListener());
initialized = true;
}
//checking fo time to trigger the effect
if (!emitted && time >= startOffset) {
emitted = true;
emitter.setCullHint(CullHint.Dynamic);
emitter.setEnabled(true);
//if the emitter has 0 particles per seconds emmit all particles in one shot
if (particlesPerSeconds == 0) {
emitter.emitAllParticles();
if (!killParticles.stopRequested) {
emitter.addControl(killParticles);
killParticles.stopRequested = true;
}
} else {
//else reset its former particlePerSec value to let it emmit.
emitter.setParticlesPerSec(particlesPerSeconds);
}
}
}
//stops the emmiter to emit.
private void stop() {
emitter.setParticlesPerSec(0);
emitted = false;
if (!killParticles.stopRequested) {
emitter.addControl(killParticles);
killParticles.stopRequested = true;
}
}
/**
* Retruns the length of the track
*
* @return length of the track
*/
public float getLength() {
return length;
}
/**
* Clone this track
*
* @return
*/
@Override
public Track clone() {
return new EffectTrack(emitter, length, startOffset);
}
/**
* This method clone the Track and search for the cloned counterpart of the
* original emmitter in the given cloned spatial. The spatial is assumed to
* be the Spatial holding the AnimControl controling the animation using
* this Track.
*
* @param spatial the Spatial holding the AnimControl
* @return the cloned Track with proper reference
*/
public Track cloneForSpatial(Spatial spatial) {
EffectTrack effectTrack = new EffectTrack();
effectTrack.particlesPerSeconds = this.particlesPerSeconds;
effectTrack.length = this.length;
effectTrack.startOffset = this.startOffset;
//searching for the newly cloned ParticleEmitter
effectTrack.emitter = findEmitter(spatial);
if (effectTrack.emitter == null) {
logger.log(Level.WARNING, "{0} was not found in {1} or is not bound to this track", new Object[]{emitter.getName(), spatial.getName()});
effectTrack.emitter = emitter;
}
removeUserData(this);
//setting user data on the new emmitter and marking it with a reference to the cloned Track.
setUserData(effectTrack);
effectTrack.emitter.setParticlesPerSec(0);
return effectTrack;
}
/**
* recursive function responsible for finding the newly cloned Emitter
*
* @param spat
* @return
*/
private ParticleEmitter findEmitter(Spatial spat) {
if (spat instanceof ParticleEmitter) {
//spat is a PArticleEmitter
ParticleEmitter em = (ParticleEmitter) spat;
//getting the UserData TrackInfo so check if it should be attached to this Track
TrackInfo t = (TrackInfo) em.getUserData("TrackInfo");
if (t != null && t.getTracks().contains(this)) {
return em;
}
return null;
} else if (spat instanceof Node) {
for (Spatial child : ((Node) spat).getChildren()) {
ParticleEmitter em = findEmitter(child);
if (em != null) {
return em;
}
}
}
return null;
}
public void cleanUp() {
TrackInfo t = (TrackInfo) emitter.getUserData("TrackInfo");
t.getTracks().remove(this);
if (t.getTracks().isEmpty()) {
emitter.setUserData("TrackInfo", null);
}
}
/**
*
* @return the emitter used by this track
*/
public ParticleEmitter getEmitter() {
return emitter;
}
/**
* Sets the Emitter to use in this track
*
* @param emitter
*/
public void setEmitter(ParticleEmitter emitter) {
if (this.emitter != null) {
TrackInfo data = (TrackInfo) emitter.getUserData("TrackInfo");
data.getTracks().remove(this);
}
this.emitter = emitter;
//saving particles per second value
this.particlesPerSeconds = emitter.getParticlesPerSec();
//setting the emmitter to not emmit.
this.emitter.setParticlesPerSec(0);
setUserData(this);
}
/**
*
* @return the start offset of the track
*/
public float getStartOffset() {
return startOffset;
}
/**
* set the start offset of the track
*
* @param startOffset
*/
public void setStartOffset(float startOffset) {
this.startOffset = startOffset;
}
private void setUserData(EffectTrack effectTrack) {
//fetching the UserData TrackInfo.
TrackInfo data = (TrackInfo) effectTrack.emitter.getUserData("TrackInfo");
//if it does not exist, we create it and attach it to the emitter.
if (data == null) {
data = new TrackInfo();
effectTrack.emitter.setUserData("TrackInfo", data);
}
//adding the given Track to the TrackInfo.
data.addTrack(effectTrack);
}
private void removeUserData(EffectTrack effectTrack) {
//fetching the UserData TrackInfo.
TrackInfo data = (TrackInfo) effectTrack.emitter.getUserData("TrackInfo");
//if it does not exist, we create it and attach it to the emitter.
if (data == null) {
return;
}
//removing the given Track to the TrackInfo.
data.getTracks().remove(effectTrack);
}
/**
* Internal use only serialization
*
* @param ex exporter
* @throws IOException exception
*/
public void write(JmeExporter ex) throws IOException {
OutputCapsule out = ex.getCapsule(this);
//reseting the particle emission rate on the emitter before saving.
emitter.setParticlesPerSec(particlesPerSeconds);
out.write(emitter, "emitter", null);
out.write(particlesPerSeconds, "particlesPerSeconds", 0);
out.write(length, "length", 0);
out.write(startOffset, "startOffset", 0);
//Setting emission rate to 0 so that this track can go on being used.
emitter.setParticlesPerSec(0);
}
/**
* Internal use only serialization
*
* @param im importer
* @throws IOException Exception
*/
public void read(JmeImporter im) throws IOException {
InputCapsule in = im.getCapsule(this);
this.particlesPerSeconds = in.readFloat("particlesPerSeconds", 0);
//reading the emitter even if the track will then reference its cloned counter part if it's loaded with the assetManager.
//This also avoid null pointer exception if the model is not loaded via the AssetManager.
emitter = (ParticleEmitter) in.readSavable("emitter", null);
emitter.setParticlesPerSec(0);
//if the emitter was saved with a KillParticleControl we remove it.
// Control c = emitter.getControl(KillParticleControl.class);
// if(c!=null){
// emitter.removeControl(c);
// }
//emitter.removeControl(KillParticleControl.class);
length = in.readFloat("length", length);
startOffset = in.readFloat("startOffset", 0);
}
}