Ribbon Trail

I threw this little missile trail class together and it works just fine, but I need some advice on performance issues as it seems to drop frame rates if I have too many which seems odd because it’s fairly simple only consisting of 2 quads per segment and maybe 20-30 segments per trail depending on the speed of the object. I’ve tried several techniques such as SharedNodes and such but they make no difference. I’d be thrilled if anyone has any ideas.







Required texture (you’ll need to rename it):







package imp.ingame.missile;

import com.jme.renderer.ColorRGBA;
import com.jme.scene.Node;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.LightState;
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.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;

public class RibbonTrail extends Node{
      
   protected Vector3f               lastPos;
         
   protected Node                  rootNode;
   
   protected long                  segmentDuration;
   
   protected float                  segmentLength;
   
   protected MaterialState          materialState;
   
   protected ZBufferState             zBufferState;
   
   protected AlphaState             alphaState;
   
   protected TextureState             textureState;
   
   protected Quaternion             quadRotation;
   
   protected Quaternion             segNodeRotation;

    /**
     *
     * <code>RibbonTrail</code> Creates a new trail effect behind any node
     * to which it is attached.
     *
     * @param rootNode
     *            the node to which the trail will attach individual segments,
     *            such as a root node of the scene.
     * @param segmentDuration
     *            the duration from the instant that a segment is created
     *            to the time it is completely faded from the scene.
     * @param segmentLength
     *            the length a segment will stretch before creating a new one.         
     *           
     * @author Scott Oehlerking         
     */
   public RibbonTrail(Node rootNode, long segmentDuration, float segmentLength){
      super();
      this.segmentDuration = segmentDuration;
      this.segmentLength = segmentLength;
      this.lastPos = new Vector3f(this.getWorldTranslation());
      this.rootNode = rootNode;
      
      zBufferState = DisplaySystem.getDisplaySystem().getRenderer().createZBufferState();
      alphaState = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
      materialState = DisplaySystem.getDisplaySystem().getRenderer().createMaterialState();
      materialState.setEmissive(new ColorRGBA(150f, 150f, 150f, 0.3f));
      
      textureState = DisplaySystem.getDisplaySystem().getRenderer().createTextureState();
      textureState.setEnabled(true);
      
      Texture texture = TextureManager.loadTexture(RibbonSegment.class
            .getClassLoader().getResource(
                  "resources/texture/effects/ribbon.png"),
            Texture.MM_LINEAR_LINEAR, Texture.FM_LINEAR);
      textureState.setTexture(texture);
      
      zBufferState.setWritable(false);
      zBufferState.setEnabled(true);
      zBufferState.setFunction(ZBufferState.CF_LEQUAL);
      
      alphaState.setBlendEnabled(true);
      alphaState.setSrcFunction(AlphaState.SB_SRC_ALPHA);
      alphaState.setDstFunction(AlphaState.DB_ONE);
      alphaState.setTestEnabled(true);
      alphaState.setTestFunction(AlphaState.TF_GREATER);
      
      quadRotation = new Quaternion();
      quadRotation.fromAngleAxis(1.5707f, Vector3f.UNIT_X);

      segNodeRotation = new Quaternion();
      segNodeRotation.fromAngleAxis(1.5707f, Vector3f.UNIT_Y);
   }
   
    public void updateWorldData(float time) {
       super.updateWorldData(time);
       if (getWorldTranslation().distance(lastPos) > segmentLength){
          //Add a new segment
          rootNode.attachChild(new RibbonSegment(getWorldTranslation(), segmentDuration, segmentLength));
          lastPos.set(getWorldTranslation());
       }
   }
      
    private class RibbonSegment extends Node {
       
       protected Vector3f               startPos;
          
       protected Quad                  quad1;
       
       protected Quad                  quad2;
          
       protected Vector3f               parentPos;
       
       protected float                  maxLength;
       
       protected float                  maxLife;
       
       protected float                  life;
          
       protected Vector3f                midpoint;
       
       protected ColorRGBA             color;
          
       protected boolean                maxed = false;

       protected Node                  segnode;
              
       protected RibbonSegment(Vector3f parentPos, long life, float maxLength){
          super("RibNode");
          this.life = life;
          this.maxLife = life;
          this.parentPos = parentPos;
          this.startPos = new Vector3f(parentPos);
          this.maxLength = maxLength;
          this.midpoint = new Vector3f();

          setupGeometry();
          setLocalTranslation(startPos);
       }
       
       protected void setupGeometry(){
          segnode = new Node("subnode");
          quad1 = new Quad("RibSeg1", 0, 0);
          quad2 = new Quad("RibSeg2", 0, 0);
          
          color = new ColorRGBA(1, 1, 1, 1);
          
          quad1.setRenderState(zBufferState);
          quad1.setRenderState(alphaState);
          quad1.setRenderState(materialState);
          quad1.setRenderState(textureState);
          quad1.setDefaultColor(color);
          segnode.attachChild(quad1);
          quad1.updateRenderState();
          
          quad2.setRenderState(zBufferState);
          quad2.setRenderState(alphaState);
          quad2.setRenderState(materialState);
          quad2.setRenderState(textureState);
          quad2.setDefaultColor(color);
          segnode.attachChild(quad2);
          quad2.updateRenderState();
          
          segnode.setLocalRotation(segNodeRotation);
          quad2.setLocalRotation(quadRotation);
          
          segnode.setModelBound(new BoundingBox());          
          attachChild(segnode);
       }
       
        public void updateWorldData(float time) {
            super.updateWorldData( time );
            if (!maxed){
                midpoint.set((startPos.x + parentPos.x)/2, (startPos.y + parentPos.y)/2, (startPos.z + parentPos.z)/2);
               setLocalTranslation(midpoint);
               lookAt(parentPos, Vector3f.UNIT_Y);
               
              quad1.resize(startPos.distance(parentPos), 6);
              quad2.resize(startPos.distance(parentPos), 6);

              if (startPos.distance(parentPos) > maxLength){
                 maxed = true;
                 lockTransforms();
                 lockBounds();
              }
            }

           if (life <= 0){
              getParent().detachChild(this);
           } else {
                life -= time;
                color.a = life/maxLife;
           }
        }
    }
}

Sure that would be great.



Just to clarify, it's not really creating a new segment per frame. Only when the length of the previous segment exceeds the preset length.

Well the most obvious thing is that you're creating a ribbon segment (each containing two quad meshes) per segment, per frame, per ribbon in the game… No surprise that you've got a performance drop. To increase performance, you should maintain a single mesh for each ribbon and add quads to it by modifying the geometry directly like it is done in the jME particle system. If you want, I might be able to fix up your code tomorrow when I have time.