[SOLVED] How to use ScreenShotAppState

I found this page https://wiki.jmonkeyengine.org/docs/3.4/core/app/state/screenshots.html

I wrote this code:

package com._3dmathpuzzles.cubes;

import com.jme3.app.SimpleApplication;
import com.jme3.app.state.ScreenshotAppState;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.system.JmeContext;

public class CubePuzzleGenerator extends SimpleApplication {  
  @Override
  public void simpleInitApp() {
    Box b = new Box(1, 1, 1);
    Geometry geom = new Geometry("Box", b);
    Material mat = new Material(assetManager,"Common/MatDefs/Misc/Unshaded.j3md");
    mat.setColor("Color", ColorRGBA.White);
    geom.setMaterial(mat);
    rootNode.attachChild(geom);
    
    ScreenshotAppState ssAppState = new ScreenshotAppState("C:\\Tmp", "test.png");
    this.stateManager.attach(ssAppState);
    ssAppState.takeScreenshot();
  }
  
  public static void main(String[] args) {
    CubePuzzleGenerator generator = new CubePuzzleGenerator();
    generator.start(JmeContext.Type.Headless);
    generator.stop();
  }
}

I get some output on my console which shows me the engine is running:

Jan 02, 2023 9:46:31 AM com.jme3.system.JmeDesktopSystem initialize
INFO: Running on jMonkeyEngine 3.5.2-stable
 * Branch: HEAD
 * Git Hash: 8ab3d24
 * Build Date: 2022-04-21

I don’t get an image file so I must be doing something wrong.
Any help on how to do this?

1 Like
fileName The screenshot file path to use. Include the separator at the end of the path.

Add a Separator at the end of the directory argument.

Also, I would recommend to always use Slashes instead of Backslash in Java, Backslashes can cause Problems on some Linux-based OS (own experiences). And remove the File extension of the file name, it is added automatic together with a number.

OK, I changed my code to this:

package com._3dmathpuzzles.cubes;

import com.jme3.app.SimpleApplication;
import com.jme3.app.state.ScreenshotAppState;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.system.JmeContext;

public class CubePuzzleGenerator extends SimpleApplication {  
  @Override
  public void simpleInitApp() {
    Box b = new Box(1, 1, 1);
    Geometry geom = new Geometry("Box", b);
    Material mat = new Material(assetManager,"Common/MatDefs/Misc/Unshaded.j3md");
    mat.setColor("Color", ColorRGBA.White);
    geom.setMaterial(mat);
    rootNode.attachChild(geom);
    
    ScreenshotAppState ssAppState = new ScreenshotAppState("C:/Tmp/", "test");
    this.stateManager.attach(ssAppState);
    ssAppState.takeScreenshot();
  }
  
  public static void main(String[] args) {
    CubePuzzleGenerator generator = new CubePuzzleGenerator();
    generator.start(JmeContext.Type.Headless);
    generator.stop();
  }
}

When I run it, there is still no file generated in C:\Tmp

Sorry, did not see.
The ScreenshotAppState generate the File in postFrame from interface SceneProcessor which doesn’t seems to get called when started headless. When starting in Display mode, it works as expected.

test1

1 Like

Is there a way to call it from code in a headless environment?

I don’t think so, for example has the RenderManager in Headless mode no Camera which is used in ScreenshotAppState.
But is starting with type JmeContext.Type.OffscreenSurface may what you want?

An OffscreenSurface is a context that is not visibleby the user. The application can use the offscreen surface to doGeneral Purpose GPU computations or render a scene into a bufferin order to save it as a screenshot, video or send through a network.

I will try to see if I can find some examples which use OffscreenSurface.

Thanks!

By the way, calling stop() immediate after start() may destroy the application, before it is done. In this case sets takeScreenshot() a boolean to create the screenshot after next rendered frame, but this will usual not happen, since there is no next frame anymore.

Yes! That did it. Here is my code:

package com._3dmathpuzzles.cubes;

import com.jme3.app.SimpleApplication;
import com.jme3.app.state.ScreenshotAppState;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.system.JmeContext;

public class CubePuzzleGenerator extends SimpleApplication {  
  @Override
  public void simpleInitApp() {
    Box b = new Box(1, 1, 1);
    Geometry geom = new Geometry("Box", b);
    Material mat = new Material(assetManager,"Common/MatDefs/Misc/Unshaded.j3md");
    mat.setColor("Color", ColorRGBA.White);
    geom.setMaterial(mat);
    rootNode.attachChild(geom);
    
    ScreenshotAppState ssAppState = new ScreenshotAppState("C:/Tmp/", "test");
    this.stateManager.attach(ssAppState);
    ssAppState.takeScreenshot();
  }
  
  public static void main(String[] args) {
    CubePuzzleGenerator generator = new CubePuzzleGenerator();
    generator.start(JmeContext.Type.OffscreenSurface);
    generator.stop();
  }
}

How do I know the rendering is done so I can call stop? Is there a callback method I can use?

I don’t know such a callback, but you can just extend ScreenshotAppState:

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;

import com.jme3.app.state.ScreenshotAppState;

public final class ListenableScreenshotAppState extends ScreenshotAppState {
	private final Collection<Runnable> imageWrittenListeners;

	public ListenableScreenshotAppState() {
		imageWrittenListeners = new LinkedList<>();
	}

	public ListenableScreenshotAppState(final String filePath) {
		super(filePath);
		imageWrittenListeners = new LinkedList<>();
	}

	public ListenableScreenshotAppState(final String filePath, final String fileName) {
		super(filePath, fileName);
		imageWrittenListeners = new LinkedList<>();
	}

	public ListenableScreenshotAppState(final String filePath, final long shotIndex) {
		super(filePath, shotIndex);
		imageWrittenListeners = new LinkedList<>();
	}

	public ListenableScreenshotAppState(final String filePath, final String fileName, final long shotIndex) {
		super(filePath, fileName, shotIndex);
		imageWrittenListeners = new LinkedList<>();
	}

	public void addImageWrittenListener(final Runnable l) {
		if(l == null) {
			throw new IllegalArgumentException("l == null");
		}
		imageWrittenListeners.add(l);
	}
	public void removeImageWrittenListener(final Runnable l) {
		if(l == null) {
			throw new IllegalArgumentException("l == null");
		}
		imageWrittenListeners.remove(l);
	}
	public Collection<Runnable> getImageWrittenListeners() {
		return(new ArrayList<>(imageWrittenListeners));
	}

	private void fireImageWrittenListeners() {
		for(final Runnable l:imageWrittenListeners) {
			l.run();
		}
	}

	@Override
	protected void writeImageFile(final File file) throws IOException {
		super.writeImageFile(file);
		fireImageWrittenListeners();
	}
}
import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.system.JmeContext;

public class CubePuzzleGenerator extends SimpleApplication {  
	@Override
	public void simpleInitApp() {
		final Box b = new Box(1, 1, 1);
		final Geometry geom = new Geometry("Box", b);
		final Material mat = new Material(assetManager,"Common/MatDefs/Misc/Unshaded.j3md");
		mat.setColor("Color", ColorRGBA.White);
		geom.setMaterial(mat);
		rootNode.attachChild(geom);

		final ListenableScreenshotAppState ssAppState = new ListenableScreenshotAppState("C:/Tmp/", "test");
		ssAppState.addImageWrittenListener(new Runnable() {
			@Override
			public void run() {
				stop();
			}
		});
		this.stateManager.attach(ssAppState);
		ssAppState.takeScreenshot();
	}

	public static void main(final String[] args) {
		final CubePuzzleGenerator generator = new CubePuzzleGenerator();
		generator.start(JmeContext.Type.OffscreenSurface);
	}
}
1 Like

I added code to use a callback from the writeImageFile method of ScreenshotAppState and it is working. Thank you for the help!

1 Like

Next question:
The ScreenShotAppState is always creating a 640x480 image. I want to create higher resolution images.

I tried calling cam.resize() but that did not change it. I looked through the methods for ScreenShotAppState but did not find anything to do it.

Did I miss something?

ScreenShotAppState will create an image in whatever size the screen was set to when the application was initialized. Probably you need to set the app settings up before starting the application… else you will have to restart the context if you need to change size later.

1 Like

I found setHeight and setWidth methods on the AppSettings class. The problem is it looks like it is not instantiated before the app starts. Take a look at this test code:

package jme3;

import java.io.File;

import com.jme3.app.SimpleApplication;
import com.jme3.app.state.ScreenshotAppState;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeContext;

public class SettingsTest extends SimpleApplication {
  public SettingsTest() {
    JmeContext context = getContext();
    if( context == null ) // This is null
      System.out.println("Context is null in constructor");
  }
  
  @Override
  public void simpleInitApp() {
    JmeContext context = getContext();
    AppSettings settings = context.getSettings();
    System.out.println("Calling set width and height in init method");
    settings.setHeight(768);
    settings.setWidth(1024);
    context.restart();

    ScreenshotAppState ssAppState = new ScreenshotAppState(
        "C:"+File.separator+"Tmp"+File.separator, 
        "test");
    ssAppState.setIsNumbered(false);
    stateManager.attach(ssAppState);
    ssAppState.takeScreenshot();
  }

  public static void main(String[] args) {
    SettingsTest app = new SettingsTest();
    JmeContext context = app.getContext();
    if( context == null ) // This is null
      System.out.println("Context is null in main method");
    app.start(JmeContext.Type.OffscreenSurface);
    app.stop();
  }
}

The context is null in the main method and in the app’s contructor.
I tried calling setHeight and setWidth and restarting the context in the simpleInitApp method, but that still gives me a 640x480 image.

Any ideas?

The best way to do this would be to get rid of the “restart()” call and just put the sizing code in main() before app.start() is called like this:

public static void main(String[] args) {
          app = new Main();   

          //more init...

          AppSettings settings = new AppSettings(true);
          settings.put("Width", ((int)width));
          settings.put("Height", ((int)height));

          app.setSettings(settings);

          app.start(JmeContext.Type.Display);
    }

But if you absolutely need to allow users to resize and restart the app, then that should also work too. I used to do this for my app, and after looking back at my code, it appears I called the restart() method on app (not on context) and I re-set the appSettings prior to calling restart. But its been a while since I did this so I cannot guarantee for certain that’s the proper way to restart after resizing

    app.setSettings(settings);
    app.restart();

Also, for what its worth, here is some code I use to scale the sizing by a percentage of screen height in case that’s useful for you as well. (although i think one of these doesn’t work depending on whether you use LWGL3 or 2)

            GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
            width = device.getDisplayMode().getWidth() * .99f;
            height = device.getDisplayMode().getHeight() * .925f;
//or
            Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
            width = (float) screenSize.getWidth() * .99f;        
            height = (float) screenSize.getHeight()  * .925f;

          settings.put("Width", ((int)width));
          settings.put("Height", ((int)height));

I know you’re trying to get a screenshot to a certain size, so you might not want to scale by a % of screen size like this, but I thought it could still be useful to mention.

2 Likes

I did not realize I could just instantiate the AppSettings myself without going through the context. This worked! Thank you!

2 Likes