Hi there,
I've already searched the forum but didn't find much useful information on my question. I'm developing a flight simulator and currently I'm seeking for a framerate-friendly way to put a Java2D cockpit with the instruments in the same window the visual system (jME) runs.
I've already tried to do that with the HUD example, i.e. a quad with texture but frame rate drops from 60 to 12 (without any instruments yet) :x Drawing the cockpit in another RenderPass doesn't change much.
Please help me out, how would you do it?
well, i have a fullscreen quad at 3200x1000 (dualscreen 16:10) and the FPS trop hardly noticable from around 160 to around 155.
Maybe you could post your code how you did it?
Ok here you go:
The visual system's GameState is called from jME's StandardGame:
package apollo.renderer.jme.app.states;
import apollo.renderer.*;
import apollo.renderer.jme.*;
import apollo.renderer.jme.celestial.*;
import apollo.renderer.jme.util.*;
import apollo.util.*;
import apollo.xfer.*;
import java.util.concurrent.*;
import java.nio.*;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.*;
import com.jme.system.*;
import com.jme.renderer.*;
import com.jme.app.*;
import com.jme.scene.*;
import com.jme.input.*;
import com.jme.image.*;
import com.jme.math.*;
import com.jme.scene.shape.*;
import com.jme.scene.state.*;
import com.jme.bounding.*;
import com.jme.light.*;
import com.jme.scene.shape.*;
import com.jme.scene.state.*;
import com.jme.renderer.pass.*;
import com.jme.util.*;
import com.jme.util.geom.*;
import com.jmex.effects.*;
import com.jmex.game.*;
import com.jmex.game.state.*;
// *****************************************************************************
public class Simulation extends BasicGameState implements DataListener
// *****************************************************************************
{
// jME specific
/** LWJGL Rederer */
private Renderer renderer;
// Basic scenegraph layout
/** The 3d context node. */
private Node scene=new Node();
/** The 2d context node. */
private Node glassNode=new Node();
private GlassPanel glassPanel;
private TextureState glassTexture;
/** Cockpit handling class. */
public Panel panel;
// Scenegraph lights
/** Lights node */
private LightNode lightNode;
/** Light state */
private LightState lightState;
/** Light coming from the sun */
private SunLight sunLight;
private LensFlare lensFlare;
// Camera
/** This' camera. */
protected Camera camera;
/** Node of the camera. */
protected CameraNode cameraNode;
/** Scenegraph scale factor (applied to <code>rootNode</code>) */
protected float scalef=.0001f;
// Resource links
/** Texture directory. */
public static final String textureDir="./resource/texture/";
/** Elapsed time. */
float elapsedTime=0f;
private Earth earth;
// Recent changes
// Just for the screenshot
Graphics2D gImg;
BufferedImage img;
// Used to draw framerate on the panel from this GameState.
Graphics2D g2d;
RenderPass r1;
RenderPass r2;
// =============================================================================
public Simulation(String name, boolean clouds)
// =============================================================================
{ super(name);
earth=new Earth(clouds);
renderer=DisplaySystem.getDisplaySystem().getRenderer();
initScene();
init2d();
rootNode.updateRenderState();
// Initialize data transfer
DataTransfer.registerReceiver(this);
img=new BufferedImage(640, 480, BufferedImage.TYPE_INT_RGB);
gImg=img.createGraphics();
g2d=panel.getGraphics2D();
r1=new RenderPass();
r1.add(scene);
r2=new RenderPass();
r2.add(glassNode);
} // Constructor ===============================================================
// =============================================================================
public BufferedImage getImage(int x, int y, int width, int height)
// =============================================================================
{
IntBuffer buffer=BufferUtils.createIntBuffer((width-x)*(height-y));
renderer.grabScreenContents(buffer, x, y, width, height);
for (int i=0; i<width-x; i++)
{ for (int j=0; j<height-y; j++)
{ gImg.setColor(new Color(buffer.get(i+j*height)));
gImg.drawLine(i, j, i, j);
}
}
return img;
} // ===========================================================================
/**
* Initializs the 2D part of the scenegraph. <p>
*
* Creates an invisible quad with texture which can be drawn on using
* <code>java.awt.Graphics2D</code>.
*/
// =============================================================================
private void init2d()
// =============================================================================
{
int width=DisplaySystem.getDisplaySystem().getWidth();
int height=DisplaySystem.getDisplaySystem().getHeight();
panel=new Panel("Panel", 0, 0, width, height);
glassNode.attachChild(panel);
glassNode.setRenderQueueMode(Renderer.QUEUE_ORTHO);
glassNode.setLightCombineMode(LightState.OFF);
rootNode.attachChild(glassNode);
glassNode.setCullMode(SceneElement.CULL_NEVER);
glassNode.setModelBound(new BoundingBox());
glassNode.updateModelBound();
} // init2d ====================================================================
/**
* Initializes the 3D part of the scenegraph
*/
// =============================================================================
private void initScene()
// =============================================================================
{
// Setup camera
//
float aspect=(float)DisplaySystem.getDisplaySystem().getWidth()/
(float)DisplaySystem.getDisplaySystem().getHeight();
int width=DisplaySystem.getDisplaySystem().getWidth();
int height=DisplaySystem.getDisplaySystem().getHeight();
camera=DisplaySystem.getDisplaySystem().getRenderer().getCamera();
camera.setFrustumPerspective(45.0f, aspect, 0.1f, 1E20f);
camera.update();
cameraNode=new CameraNode("CameraNode", camera);
cameraNode.updateWorldData(0f);
// Turn on the lights
//
// Create sunLight
sunLight=new SunLight();
// Modify the LightState
lightState=renderer.createLightState();
lightState.detachAll();
lightState.attach(sunLight);
lightState.setEnabled(true);
scene.setRenderState(lightState);
// Make a LightNode to put the sunLight into...
lightNode=new LightNode("NODE SUNLIGHT", lightState);
lightNode.setLight(sunLight);
lightNode.setTarget(scene);
scene.attachChild(lightNode);
// Make LensFlare
lensFlare=sunLight.getLensFlare(renderer);
lensFlare.setRootNode(scene);
lensFlare.setTriangleAccurateOcclusion(true);
lensFlare.setModelBound(new BoundingBox());
lensFlare.updateModelBound();
lensFlare.setCullMode(SceneElement.CULL_NEVER);
scene.attachChild(lensFlare);
// Add objects to the scene
//
scene.attachChild(earth);
earth.attachChild(cameraNode);
rootNode.attachChild(scene);
scene.setLocalScale(scalef);
lightNode.setLightCombineMode(LightState.OFF);
lightNode.setCullMode(SceneElement.CULL_NEVER);
} // initScene =================================================================
// =============================================================================
public void update(float interpolation)
// =============================================================================
{
// update 3d
elapsedTime+=interpolation;
earth.update(interpolation, elapsedTime);
lightNode.setLocalTranslation(earth.getOrbitLocation(elapsedTime).negate());
/* Lens flare center should be at the same distance as the farthest object
* from the viewpoint of the camera.
*/
lensFlare.setLocalTranslation(cameraNode.getLocalTranslation().add(
lightNode.getLocalTranslation().normalize().mult(
cameraNode.getLocalTranslation().length())));
// update 2d
//statsPanel.update(interpolation);
panel.refresh();
g2d.setColor(Color.BLACK);
g2d.fillRect(30, 17, 100, 15);
g2d.setColor(Color.GREEN);
g2d.drawString("FPS: "+Float.toString(1f/interpolation), 30, 30);
// update superclass
super.update(interpolation);
} // update ====================================================================
// =============================================================================
public void update(DataObject obj)
// =============================================================================
{ if (obj instanceof VisualSystemData)
{ VisualSystemData data=(VisualSystemData)obj;
// Camera location and orientation setup
Vector3f location=new Vector3f((float)data.getLocation().X,
(float)data.getLocation().Y, (float)data.getLocation().Z);
Vector3f direction=new Vector3f((float)(10000*data.getDirection().X),
(float)(10000*data.getDirection().Y),
(float)(10000*data.getDirection().Z));
Vector3f up=new Vector3f((float)data.getUp().X, (float)data.getUp().Y,
(float)data.getUp().Z);
cameraNode.setLocalTranslation(location);
cameraNode.lookAt(location.add(direction), up);
}
} // update ====================================================================
// =============================================================================
public void render(float interpolation)
// =============================================================================
{
try
{
renderer.clearBuffers();
r1.doRender(renderer);
r2.doRender(renderer);
/*
renderer.clearBuffers();
// Render 3d components
renderer.draw(scene);
// Render 2d components
renderer.draw(glassNode);
*/
}
catch (Exception ex)
{ ex.printStackTrace();
}
} // render ====================================================================
}
This is the class that handles the cockpit:
package apollo.renderer.jme;
import apollo.renderer.jme.util.*;
import apollo.util.*;
import java.awt.*;
import java.awt.image.*;
import com.jme.system.*;
import com.jme.renderer.*;
import com.jme.app.*;
import com.jme.scene.*;
import com.jme.input.*;
import com.jme.image.*;
import com.jme.math.*;
import com.jme.scene.shape.*;
import com.jme.scene.state.*;
// *****************************************************************************
public class Panel extends Quad
// *****************************************************************************
{
protected GlassPanel glassPanel;
private TextureState glassTexture;
private int width;
private int height;
// =============================================================================
public Panel(String name, int xOffset, int yOffset, int width, int height)
// =============================================================================
{ super(name, TextureTool.getNextPowerOfTwo(width, height),
TextureTool.getNextPowerOfTwo(width, height));
this.width=width;
this.height=height;
int imageSize=TextureTool.getNextPowerOfTwo(width, height);
//setRenderQueueMode(Renderer.QUEUE_ORTHO);
//setLightCombineMode(LightState.OFF);
glassPanel=new GlassPanel(width, height, imageSize);
glassPanel.clear();
Texture texture=new Texture();
texture.setApply(Texture.AM_MODULATE);
texture.setBlendColor(new ColorRGBA(1, 1, 1, 1));
texture.setFilter(Texture.FM_LINEAR);
texture.setImage(glassPanel);
texture.setMipmapState(Texture.MM_LINEAR);
glassTexture=DisplaySystem.getDisplaySystem().getRenderer()
.createTextureState();
glassTexture.setTexture(texture);
glassTexture.setEnabled(true);
setRenderState(glassTexture);
AlphaState as=DisplaySystem.getDisplaySystem().getRenderer()
.createAlphaState();
as.setBlendEnabled(true);
as.setSrcFunction(AlphaState.SB_SRC_ALPHA);
as.setDstFunction(AlphaState.DB_ONE_MINUS_SRC_ALPHA);
as.setTestEnabled(false);
as.setEnabled(true);
setRenderState(as);
setLocalTranslation(new Vector3f(xOffset+imageSize/2,yOffset+imageSize/2,0));
updateRenderState();
System.out.println("===> <"+this.getClass().getName()
+"> 2D graphics initiated.");
} // ===========================================================================
// =============================================================================
public Graphics2D getGraphics2D() {return glassPanel.getGraphics2D();}
// =============================================================================
// =============================================================================
public void refresh()
// =============================================================================
{ glassPanel.refresh();
glassTexture.deleteAll();
} // ===========================================================================
}
Next class is GlassPanel (which is a PaintableImage (jME Image) see below):
package apollo.renderer.jme.util;
import apollo.renderer.jme.util.*;
import java.awt.*;
public class GlassPanel extends PaintableImage
{
private int width;
private int height;
/**
* Creates a new glass panel (invisible image).
*
* @param width Visible width [pixel]
* @param height Visible height [pixel]
* @param trueImageSize Image size to be created.
*/
// =============================================================================
public GlassPanel(int width, int height, int trueImageSize)
// =============================================================================
{ super(trueImageSize, trueImageSize, true);
this.width=width;
this.height=height;
// Set coordinate center at top-left corner of the image.
g2d.translate(0, height);
g2d.scale(1, -1);
clear();
refresh();
} // Constructor ===============================================================
/**
*
*/
// =============================================================================
public Graphics2D getGraphics2D() {return g2d;}
// =============================================================================
/**
* Provides pixel width of the visible part of the <code>GlassPanel</code>.
*
* @return Visible width [pixel]
*/
// =============================================================================
public int getVisibleWidth() {return width;}
// =============================================================================
/**
* Provides pixel height of the visible part of the <code>GlassPanel</code>.
*
* @return Visible height [pixel]
*/
// =============================================================================
public int getVisibleHeight() {return height;}
// =============================================================================
/**
* Draws the glass panel into a <code>Graphics2D</code> context.
*
* @param g2d <code>Graphics2D</code> context to draw in.
*/
// =============================================================================
public void render(Graphics2D g2d) {clear();}
// =============================================================================
}
The PaintableImage, as the name says, the texture image one can draw on by calling getGraphics2D() method.
The image is refreshed at each update cycle.
package apollo.renderer.jme.util;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.logging.Level;
import com.jme.image.Image;
// *****************************************************************************
public class PaintableImage extends Image implements Serializable
// *****************************************************************************
{
protected BufferedImage backImg;
private ByteBuffer scratch;
protected Graphics2D g2d;
protected int width;
protected int height;
// =============================================================================
public PaintableImage(int width, int height, boolean hasAlpha)
// =============================================================================
{
super();
try
{ backImg = new BufferedImage(width, height, hasAlpha
? BufferedImage.TYPE_4BYTE_ABGR
: BufferedImage.TYPE_3BYTE_BGR);
setType(hasAlpha
? com.jme.image.Image.RGBA8888
: com.jme.image.Image.RGB888);
setWidth(backImg.getWidth());
setHeight(backImg.getHeight());
scratch = ByteBuffer.allocateDirect(4 * backImg.getWidth()
* backImg.getHeight());
}
catch (IllegalArgumentException ex)
{ System.out.println("ERROR: Cannot create PaintableImage.");
ex.printStackTrace();
}
this.width=width;
this.height=height;
g2d=backImg.createGraphics();
} // Constructor ===============================================================
// =============================================================================
public Graphics2D getGraphics2D() {return g2d;}
// =============================================================================
/**
* Draws scratch on the image.
*/
// =============================================================================
public void refresh()
// =============================================================================
{ //render(g2d);
/* get the image data */
byte data[] = (byte[])backImg.getRaster().getDataElements(0, 0,
backImg.getWidth(), backImg.getHeight(), null);
scratch.clear();
scratch.put(data, 0, data.length);
scratch.rewind();
setData(scratch);
} // refresh ===================================================================
/**
* Clears the entire image.
*/
// =============================================================================
public void clear()
// =============================================================================
{ g2d.setBackground(new Color(0, 0, 0, 0));
g2d.clearRect(0, 0, getWidth(), getHeight());
} // clear =====================================================================
/**
* Must be implemented by derived classes in order to paint the image.
*
* @param g2d Graphics2D context to be used for drawing.
*/
// =============================================================================
public void render(Graphics2D g2d) {}
// =============================================================================
}
Hard to believe you get 155 FPS! Want that too :-o
well, repainting the texture every frame and getting that repainted texture painted again is just not performing, that i can understand.
What you can try is something like ImposterNode does. Its a Node that manages repainting something like 25 times a second, whereas you can set that value in the constructor. I use this a lot, since the FPS can still be 60 (Vsynced in my case) and the hud gets redrawn 25 times a second instead of 60 times. Play around with that value (25 times looks fluent enough in my case)
What I dont understand is: Why do you repaint the image and not just have a static image of the cockpit(background) and do the rest with multiple quads and/or multitexturing?
The slowest part is probably the Java2D painting. Ideally you would create a system that did this with jME without any J2D dependency. Otherwise you have to use something like ImageGraphics which only updates the parts that were changed in the image.
Thank you for your answers so far. I'll play around a little bit and report my experiences.
What I dont understand is: Why do you repaint the image and not just have a static image of the cockpit(background) and do the rest with multiple quads and/or multitexturing?
I intend to do this in the near future but first I have to solve this problem. I added no instruments yet, the quad in front of the camera is just transparent, except the (very poor) frame rate is written on it. I know that repainting everything takes a lot of time but what is even more time consuming is the conversion from a BufferedImage to a jME-Image. (Picking pixel data -> writing pixel data - that's what really takes a lot of time.) Maybe there is a way to speed this up?
if you redraw an image every frame, it doesnt matter weather the pixels end up transparent or not. Furthermore, why would you draw the FPS on the texture, take a look at how AbstractDisplay does it.
Maybe the best thing for you would be to work your way through the Tutorials and Tests and then take a different approach.
If you must do it like you attempt to right now I would draw the Java2D stuff in a different thread so the OpenGL thread doesnt get blocked (as suggested above by keeskist).
Good luck
Hi guys,
I just wanted to thank you for your replies - It really worked out and I've got about 50 FPS now which is much better than the previous version. So once again, thank you!
One more thing to add: Updating scratch data as shown in the HUD tutorial takes a lot of time, that's why I suggest updating only the image data visible on the screen. This can be done like that:
/**
* Draws scratch data within a specified boundary on the image.
*/
// =============================================================================
public void refresh(int xOffset, int yOffset, int width, int height)
// =============================================================================
{ byte data[]=(byte[])backImg.getRaster().getDataElements(xOffset,
yOffset, width, height, null);
for (int j=yOffset; j<yOffset+height; j++)
{
int scratchOffset=xOffset+j*this.width;
scratchOffset*=4;
scratch.position(scratchOffset);
scratch.put(data, 4*(j-yOffset)*width, 4*width);
}
setData(scratch);
} // refresh ===================================================================