Request for comments: animated starfield mesh

Hello,



I just started playing with jME and would like some input on the following code. It's my attempt to create a classic parallax starfield effect as a backdrop for my menus. I wrote this as I learned to use jME, and I'm wondering if I could achieve a similar effect using a particle system instead of building the mesh myself.



Thanks,

Matt



package com.gamesite.demo;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Arrays;

import com.jme.renderer.Renderer;
import com.jme.scene.BatchMesh;
import com.jme.scene.Spatial;
import com.jme.scene.batch.PointBatch;
import com.jme.scene.state.LightState;
import com.jme.system.DisplaySystem;
import com.jme.util.geom.BufferUtils;
import com.jmex.editors.swing.settings.GameSettingsPanel;
import com.jmex.game.StandardGame;
import com.jmex.game.state.BasicGameState;
import com.jmex.game.state.DebugGameState;
import com.jmex.game.state.GameState;
import com.jmex.game.state.GameStateManager;

/**
 * The StarfieldGameState displays a set of points in a scene using QUEUE_ORTHO mode to simulate a parallax scrolling star
 * field.
 *
 * @author Matthew Woodard
 */
public class StarfieldDemoGameState extends BasicGameState {
   /** The number of stars to render */
   private int mNumStars;

   /** The depth of the field - the number of steps between the 'front' and 'back' parallax layers */
   private int mDepth;

   /** The size of the stars */
   private float mStarSize;

   /** The velocity of the field in the X direction - individual stars are proportional to their depth */
   private float mVelocityX = 0.0f;

   /** The velocity of the field in the Y direction - individual stars are proportional to their depth */
   private float mVelocityY = 0.0f;

   /** The star field controller adjusts the position of the stars over time */
   protected StarfieldController mStarfieldController;

   /** The mesh onto which the stars are drawn */
   protected BatchMesh mBackgroundMesh;

   /**
    * The StarfieldController moves the stars in the mesh.
    *
    * @author Matthew Woodard
    */
   public class StarfieldController {
      /** The width of the field */
      private int mWidth;

      /** The height of the field */
      private int mHeight;

      /** The star positions */
      private FloatBuffer mStarPositionBuffer;

      /** The star colors (dim in back, bright in front) */
      private FloatBuffer mStarColorBuffer;

      /** The indexes to render (all points) */
      private IntBuffer mStarIndexBuffer;

      /** The points to render */
      private PointBatch mPointBatch;

      /** The star depth positions used to determine color and scaled movement */
      private float mPositionZ[];

      /**
       * Initializes all of the buffers required to render the stars.
       *
       * @param width
       *           the width of the field
       * @param height
       *           the height of the field
       */
      public StarfieldController(int width, int height) {
         mWidth = width;
         mHeight = height;

         mPositionZ = new float[mNumStars];
         for (int i = 0; i < mNumStars; i++) {
            mPositionZ[i] = (int) (Math.random() * mDepth) + 1;
         }

         Arrays.sort(mPositionZ);

         mStarIndexBuffer = BufferUtils.createIntBuffer(mNumStars);
         mStarColorBuffer = BufferUtils.createFloatBuffer(4 * mNumStars);
         mStarPositionBuffer = BufferUtils.createFloatBuffer(3 * mNumStars);
         for (int star = 0; star < mNumStars; star++) {
            float z = mPositionZ[star] / (float) mDepth;

            mStarPositionBuffer.put((float) (Math.random() * width)).put((float) (Math.random() * height)).put(z);
            mStarColorBuffer.put(z).put(z).put(z).put(1);
            mStarIndexBuffer.put(star);
         }
         mStarPositionBuffer.flip();
         mStarColorBuffer.flip();
         mStarIndexBuffer.flip();

         mPointBatch = new PointBatch();
         mPointBatch.setVertexBuffer(mStarPositionBuffer);
         mPointBatch.setColorBuffer(mStarColorBuffer);
         mPointBatch.setIndexBuffer(mStarIndexBuffer);
         mPointBatch.setPointSize(mStarSize);
      }

      /**
       * Invoked in order to move the star field. All movement is on the X,Y plane to simulate a classic parallax star field. Z
       * movement could be simulated by adjusting the Z position, which would adjust the intensity and relative motion of each
       * star over time.
       *
       * @param tpf
       *           the time per frame, used to maintain smooth movement regardless of frame rate
       */
      public void update(float tpf) {
         for (int i = 0; i < mNumStars; i++) {
            int index = i * 3;

            float x = mStarPositionBuffer.get(index);
            float y = mStarPositionBuffer.get(index + 1);
            float z = mPositionZ[i];

            x += (mVelocityX * tpf * 10.0f) * (z / mDepth);
            y += (mVelocityY * tpf * 10.0f) * (z / mDepth);

            if (x >= mWidth) {
               x = 0;
            } else if (x < 0) {
               x = mWidth - 1;
            }
            if (y >= mHeight) {
               y = 0;
            } else if (y < 0) {
               y = mHeight - 1;
            }

            mStarPositionBuffer.put(index, x);
            mStarPositionBuffer.put(index + 1, y);
         }
      }
   }

   /**
    * Constructs a new state that renders a simulated parallax star field on a mesh.
    *
    * @param n
    *           the name of the state
    * @param numStars
    *           the number of stars to render
    * @param depth
    *           the parallax field depth
    * @param starSize
    *           the size of the stars
    */
   public StarfieldDemoGameState(String n, int numStars, int depth, float starSize, float velocityX, float velocityY) {
      super(n);

      mDepth = depth;
      mStarSize = starSize;
      mNumStars = numStars;
      mVelocityX = velocityX;
      mVelocityY = velocityY;

      initGameState();
   }

   /**
    * Updates the star field, moving the stars in a parallax manner.
    *
    * @see com.jmex.game.state.BasicGameState#update(float)
    */
   @Override
   public void update(float tpf) {
      super.update(tpf);

      mStarfieldController.update(tpf);
   }

   /**
    * Initializes the controller and the star mesh.
    */
   protected void initGameState() {
      DisplaySystem displaySystem = DisplaySystem.getDisplaySystem();

      mStarfieldController = new StarfieldController(displaySystem.getWidth(), displaySystem.getHeight());

      mBackgroundMesh = new BatchMesh("Starfield", mStarfieldController.mPointBatch);
      mBackgroundMesh.setCullMode(Spatial.CULL_NEVER);
      mBackgroundMesh.setLightCombineMode(LightState.OFF);
      mBackgroundMesh.setRenderQueueMode(Renderer.QUEUE_ORTHO);
      mBackgroundMesh.updateRenderState();

      getRootNode().attachChildAt(mBackgroundMesh, 0);
   }
  
  
   /**
    * @return the x velocity
    */
   public float getVelocityX() {
      return mVelocityX;
   }

   /**
    * @param velocityX the x velocity to set
    */
   public void setVelocityX(float velocityX) {
      mVelocityX = velocityX;
   }

   /**
    * @return the y velocity
    */
   public float getVelocityY() {
      return mVelocityY;
   }

   /**
    * @param velocityY the y velocity to set
    */
   public void setVelocityY(float velocityY) {
      mVelocityY = velocityY;
   }

   /**
    * Demonstrate layered star fields.
    *
    * @param args no arguments expected
    * @throws InterruptedException thrown by gaem.start()
    */
   public static void main(String[] args) throws InterruptedException {
      // Instantiate StandardGame
      StandardGame game = new StandardGame("A Starfield Demo");

      // Show settings screen
      GameSettingsPanel.prompt(game.getSettings());

      // Start StandardGame, it will block until it has initialized successfully, then return
      game.start();
     
      // The Demo causes the stars to move in the +X and +Y directions
      float velocityX = 10.0f;
      float velocityY = 10.0f;
     
      // Layered fields create multiple star sizes.
      BasicGameState[] gameStates = new BasicGameState[] {
          new StarfieldDemoGameState("Starfield 1", 15, 20, 3.0f, velocityX, velocityY),
          new StarfieldDemoGameState("Starfield 2", 30, 20, 2.5f, velocityX, velocityY),
          new StarfieldDemoGameState("Starfield 3", 60, 20, 2.0f, velocityX, velocityY),
          new StarfieldDemoGameState("Starfield 4", 120, 20, 1.5f, velocityX, velocityY),
          new StarfieldDemoGameState("Starfield 5", 240, 20, 1.0f, velocityX, velocityY),
          new StarfieldDemoGameState("Starfield 6", 480, 20, 0.5f, velocityX, velocityY),
          new DebugGameState()};
   
      for (GameState gameState : gameStates) {
         GameStateManager.getInstance().attachChild(gameState);
         gameState.setActive(true);
      }
   }
}

Nice work. There is also a very good class that accomplishes what you are attempting. Do a search for StarfieldGameState.