I’ve made a commit within my jMonkeyEngine branch (https://github.com/jseinturier/jmonkeyengine/tree/jme3-vr-dev) and now the jme3-vr module is fully compatible with both standard jme3 and OpenVR 1.0.6.
If someone want to test, here is the example that i use for debugging purpose:
package sample;
import com.jme3.app.VRApplication;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.vr.OpenVRInput;
import com.jme3.input.vr.VRBounds;
import com.jme3.input.vr.VRInputType;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.post.CartoonSSAO;
import com.jme3.post.FilterPostProcessor;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.scene.shape.Box;
import com.jme3.system.jopenvr.VRControllerAxis_t;
import com.jme3.system.jopenvr.VRControllerState_t;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.MagFilter;
import com.jme3.texture.Texture.MinFilter;
import com.jme3.ui.Picture;
import com.jme3.util.SkyFactory;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.Filter;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import jmevr.util.VRGuiManager.POSITIONING_MODE;
/**
* An application dedicated to VR system.
* @author reden - phr00t - https://github.com/phr00t
* @author Julien Seinturier - (c) 2016 - JOrigin project - <a href="http://www.jorigin.org">http:/www.jorigin.org</a>
*/
public class VRSample extends VRApplication {
private static final Logger logger = Logger.getLogger(VRSample.class.getName());
public static final boolean MAKE_CONTROLLER_TEXT_FILE = false; // makes a text file with all controller output, slows things down but good for data collection
public static File controllerTextFile;
// general objects for scene management
Node boxes = new Node("boxes");
Spatial observer;
boolean moveForward;
boolean moveBackwards;
boolean rotateLeft;
boolean rotateRight;
Material mat;
Geometry leftHand;
Geometry rightHand;
/**
* Initialize and run the VR application.
* @param args not used at this time.
*/
public static void main(String[] args){
// Set the logger to display config messages.
Logger log = Logger.getLogger("");
log.setLevel(Level.FINE);
// Disable Nifty µGUI logs
Logger.getLogger("de.lessvoid.nifty").setLevel(Level.SEVERE);
Logger.getLogger("NiftyInputEventHandlingLog").setLevel(Level.SEVERE);
Filter filter = new Filter(){
public boolean isLoggable(LogRecord record) {
return true;
}
};
Formatter formatter = new Formatter(){
private final String lineSeparator = System.getProperty("line.separator");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd-HH:mm:ss");
@Override
public String format(LogRecord record) {
if (record != null){
String simpleClassName = record.getSourceClassName();
if (simpleClassName != null){
int index = simpleClassName.lastIndexOf(".");
if ((index > -1)&&(index < (simpleClassName.length() - 1))){
simpleClassName = simpleClassName.substring(index+1);
}
} else {
simpleClassName = "Unknow,";
}
String level = "";
if (record.getLevel().equals(Level.FINEST)){
level = "FINEST ";
} else if (record.getLevel().equals(Level.FINER)){
level = "FINER ";
} else if (record.getLevel().equals(Level.FINE)){
level = "FINE ";
} else if (record.getLevel().equals(Level.CONFIG)){
level = "CONFIG ";
} else if (record.getLevel().equals(Level.INFO)){
level = "INFO ";
} else if (record.getLevel().equals(Level.WARNING)){
level = "WARNING";
} else if (record.getLevel().equals(Level.SEVERE)){
level = "SEVERE ";
} else {
level = "???????";
}
// Use record parameters
String message = record.getMessage();
if (record.getParameters() != null){
for(int i = 0; i < record.getParameters().length; i++){
message = message.replace("{"+i+"}", ""+record.getParameters()[i]);
}
}
if (record.getThrown() == null){
return "("+sdf.format(new Date(record.getMillis()))+") "+level+" ["+simpleClassName+"] ["+record.getSourceMethodName()+"] "+message+lineSeparator;
} else {
String str = "("+sdf.format(new Date(record.getMillis()))+") "+level+" ["+simpleClassName+"] ["+record.getSourceMethodName()+"] caused by "+message+lineSeparator;
StackTraceElement[] elements = record.getThrown().getStackTrace();
for(int i = 0; i < elements.length; i++){
str += "("+sdf.format(new Date(record.getMillis()))+") "+level+" ["+simpleClassName+"] ["+record.getSourceMethodName()+"] at "+elements[i]+lineSeparator;
}
return "("+sdf.format(new Date(record.getMillis()))+") "+level+" ["+record.getSourceClassName()+"] ["+record.getSourceMethodName()+"] "+message+lineSeparator+str;
}
} else {
return null;
}
}};
// If the init is forced from a previous configuration, we remove the older handlers.
if (log != null){
if (log.getHandlers() != null){
for(int i = log.getHandlers().length - 1; i >= 0; i--){
log.getHandlers()[i].setFilter(filter);
log.getHandlers()[i].setFormatter(formatter);
log.getHandlers()[i].setLevel(Level.CONFIG);
}
}
}
// Instanciate a new VR application.
VRSample test = new VRSample();
// Check the underlying API to use.
// Here we use OpenVR (OSVR is not fully functionnal at this time).
test.CONSTRUCT_WITH_OSVR = false;
// We can disable VR rendering, for testing purpose.
test.DISABLE_VR = false;
// We configure the application
test.preconfigureVRApp(PreconfigParameter.USE_VR_COMPOSITOR, true); // disable the SteamVR compositor (kinda needed at the moment)
test.preconfigureVRApp(PreconfigParameter.ENABLE_MIRROR_WINDOW, true); // runs faster when set to false, but will allow mirroring
test.preconfigureVRApp(PreconfigParameter.FORCE_VR_MODE, false); // render two eyes, regardless of API detection
test.preconfigureVRApp(PreconfigParameter.SET_GUI_CURVED_SURFACE, true);
test.preconfigureVRApp(PreconfigParameter.FLIP_EYES, false); // We do not want to invert eyes.
test.preconfigureVRApp(PreconfigParameter.SET_GUI_OVERDRAW, true); // show gui even if it is behind things
test.preconfigureVRApp(PreconfigParameter.INSTANCE_VR_RENDERING, false); // faster VR rendering, requires some vertex shader changes (see Common/matDefs/VR/Unshaded.j3md)
test.preconfigureVRApp(PreconfigParameter.NO_GUI, false);
test.setFrustrumNearFar(0.1f, 512f); // set frustum distances here before app starts
//test.setResolutionMultiplier(0.666f); // you can downsample for performance reasons
// Starting the VR application
try {
test.start();
} catch (Exception e) {
logger.log(Level.SEVERE, "Exception caught: "+e.getMessage(), e);
}
}
@Override
public void simpleInitApp() {
logger.info("Updating asset manager with "+System.getProperty("user.dir"));
getAssetManager().registerLocator(System.getProperty("user.dir")+File.separator+"assets", FileLocator.class);
initTestScene();
// print out what device we have
if( getVRHardware() != null ) {
logger.info("Attached device: " + getVRHardware().getType());
}
}
private void initTestScene(){
observer = new Node("observer");
Spatial sky = SkyFactory.createSky(getAssetManager(), "Textures/Sky/Bright/spheremap.png", SkyFactory.EnvMapType.EquirectMap);
rootNode.attachChild(sky);
Geometry box = new Geometry("", new Box(5,5,5));
mat = new Material(getAssetManager(), "Common/MatDefs/VR/Unshaded.j3md");
Texture noise = getAssetManager().loadTexture("Textures/noise.png");
noise.setMagFilter(MagFilter.Nearest);
noise.setMinFilter(MinFilter.Trilinear);
noise.setAnisotropicFilter(16);
mat.setTexture("ColorMap", noise);
// make the floor according to the size of our play area
Geometry floor = new Geometry("floor", new Box(1f, 1f, 1f));
Vector2f playArea = VRBounds.getPlaySize();
if( playArea == null ) {
// no play area, use default size & height
floor.setLocalScale(2f, 0.5f, 2f);
floor.move(0f, -1.5f, 0f);
} else {
// cube model is actually 2x as big, cut it down to proper playArea size with * 0.5
floor.setLocalScale(playArea.x * 0.5f, 0.5f, playArea.y * 0.5f);
floor.move(0f, -0.5f, 0f);
}
floor.setMaterial(mat);
rootNode.attachChild(floor);
// hand wands
leftHand = (Geometry)getAssetManager().loadModel("Models/vive_controller.j3o");
leftHand.setName("LeftHand");
rightHand = leftHand.clone();
rightHand.setName("RightHand");
Material handMat = new Material(getAssetManager(), "Common/MatDefs/VR/Unshaded.j3md");
handMat.setTexture("ColorMap", getAssetManager().loadTexture("Textures/vive_controller.png"));
leftHand.setMaterial(handMat);
rightHand.setMaterial(handMat);
rootNode.attachChild(rightHand);
rootNode.attachChild(leftHand);
// gui element
Vector2f guiCanvasSize = getVRGUIManager().getCanvasSize();
Picture test = new Picture("testpic")
/*{
@Override
public void refreshFlagOr(int flag){
refreshFlags |= flag;
Exception e = new Exception("Changing refresh flags for spatial " + getName()+", flags: "+getRefreshFlagsDescription());
logger.log(Level.SEVERE, e.getMessage(), e);
}
@Override
public void refreshFlagAnd(int flag){
refreshFlags &= flag;
Exception e = new Exception("Changing refresh flags for spatial " + getName()+", flags: "+getRefreshFlagsDescription());
logger.log(Level.SEVERE, e.getMessage(), e);
}
}*/;
test.setImage(getAssetManager(), "Textures/crosshair.png", true);
//test.setImage(getAssetManager(), "Textures/happy.png", true);
test.setWidth(192f);
test.setHeight(128f);
test.setPosition(guiCanvasSize.x * 0.5f - 192f * 0.5f, guiCanvasSize.y * 0.5f - 128f * 0.5f);
guiNode.attachChild(test);
// test any positioning mode here (defaults to AUTO_CAM_ALL)
getVRGUIManager().setPositioningMode(POSITIONING_MODE.AUTO_CAM_ALL_SKIP_PITCH);
getVRGUIManager().setGuiScale(0.4f);
getVRGUIManager().setPositioningElasticity(10f);
box.setMaterial(mat);
Geometry box2 = box.clone();
box2.move(15, 0, 0);
box2.setMaterial(mat);
Geometry box3 = box.clone();
box3.move(-15, 0, 0);
box3.setMaterial(mat);
boxes.attachChild(box);
boxes.attachChild(box2);
boxes.attachChild(box3);
rootNode.attachChild(boxes);
observer.setLocalTranslation(new Vector3f(0.0f, 0.0f, 0.0f));
setObserver(observer);
rootNode.attachChild(observer);
addAllBoxes();
guiNode.updateGeometricState();
rootNode.updateGeometricState();
initInputs();
// use magic VR mouse cusor (same usage as non-VR mouse cursor)
getInputManager().setCursorVisible(true);
if( MAKE_CONTROLLER_TEXT_FILE ) {
controllerTextFile = new File("controllerinfo.txt");
}
// filter test (can be added here like this)
// but we are going to save them for the F key during runtime
/*
CartoonSSAO cartfilt = new CartoonSSAO();
FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
fpp.addFilter(cartfilt);
viewPort.addProcessor(fpp);
*/
}
private void initInputs() {
InputManager inputManager = getInputManager();
inputManager.addMapping("toggle", new KeyTrigger(KeyInput.KEY_SPACE));
inputManager.addMapping("incShift", new KeyTrigger(KeyInput.KEY_Q));
inputManager.addMapping("decShift", new KeyTrigger(KeyInput.KEY_E));
inputManager.addMapping("forward", new KeyTrigger(KeyInput.KEY_W));
inputManager.addMapping("back", new KeyTrigger(KeyInput.KEY_S));
inputManager.addMapping("left", new KeyTrigger(KeyInput.KEY_A));
inputManager.addMapping("right", new KeyTrigger(KeyInput.KEY_D));
inputManager.addMapping("filter", new KeyTrigger(KeyInput.KEY_F));
inputManager.addMapping("exit", new KeyTrigger(KeyInput.KEY_ESCAPE));
final VRApplication app = this;
ActionListener acl = new ActionListener() {
public void onAction(String name, boolean keyPressed, float tpf) {
if(name.equals("incShift") && keyPressed){
getVRGUIManager().adjustGuiDistance(-0.1f);
}else if(name.equals("decShift") && keyPressed){
getVRGUIManager().adjustGuiDistance(0.1f);
}else if(name.equals("filter") && keyPressed){
// adding filters in realtime
CartoonSSAO cartfilt = new CartoonSSAO(app.isInstanceVRRendering());
FilterPostProcessor fpp = new FilterPostProcessor(getAssetManager());
fpp.addFilter(cartfilt);
getViewPort().addProcessor(fpp);
// filters added to main viewport during runtime,
// move them into VR processing
// (won't do anything if not in VR mode)
moveScreenProcessingToVR();
}
if( name.equals("toggle") ) {
getVRGUIManager().positionGui();
}
if(name.equals("forward")){
if(keyPressed){
moveForward = true;
} else {
moveForward = false;
}
} else if(name.equals("back")){
if(keyPressed){
moveBackwards = true;
} else {
moveBackwards = false;
}
}else if(name.equals("left")){
if(keyPressed){
rotateLeft = true;
} else {
rotateLeft = false;
}
} else if(name.equals("right")){
if(keyPressed){
rotateRight = true;
} else {
rotateRight = false;
}
} else if(name.equals("exit")){
app.destroy();
System.exit(0);
}
}
};
inputManager.addListener(acl, "forward");
inputManager.addListener(acl, "back");
inputManager.addListener(acl, "left");
inputManager.addListener(acl, "right");
inputManager.addListener(acl, "toggle");
inputManager.addListener(acl, "incShift");
inputManager.addListener(acl, "decShift");
inputManager.addListener(acl, "filter");
inputManager.addListener(acl, "dumpImages");
inputManager.addListener(acl, "exit");
}
private float distance = 100f;
private float prod = 0f;
private float placeRate = 0f;
@Override
public void simpleUpdate(float tpf){
prod+=tpf;
distance = 100f * FastMath.sin(prod);
boxes.setLocalTranslation(0, 0, 200f+ distance);
if(moveForward){
observer.move(getFinalObserverRotation().getRotationColumn(2).mult(tpf*8f));
}
if(moveBackwards){
observer.move(getFinalObserverRotation().getRotationColumn(2).mult(-tpf*8f));
}
if(rotateLeft){
observer.rotate(0, 0.75f*tpf, 0);
}
if(rotateRight){
observer.rotate(0, -0.75f*tpf, 0);
}
// use the analog control on the first tracked controller to push around the mouse
getVRMouseManager().updateAnalogAsMouse(0, null, null, null, tpf);
handleWandInput(0, leftHand);
handleWandInput(1, rightHand);
if( placeRate > 0f ) placeRate -= tpf;
}
private void handleWandInput(int index, Geometry geo) {
if( getVRinput() != null ){
Quaternion q = getVRinput().getFinalObserverRotation(index);
Vector3f v = getVRinput().getFinalObserverPosition(index);
if( q != null && v != null ) {
geo.setCullHint(CullHint.Dynamic); // make sure we see it
geo.setLocalTranslation(v);
geo.setLocalRotation(q);
// place boxes when holding down trigger
if( getVRinput().getAxis(index, VRInputType.ViveTriggerAxis).x >= 0.8f &&
placeRate <= 0f ) {
placeRate = 0.5f;
addBox(v, q, 0.1f);
getVRinput().triggerHapticPulse(index, 0.1f);
}
// print out all of the known information about the controllers here to file
if( MAKE_CONTROLLER_TEXT_FILE ) {
String out = "";
VRControllerState_t rawstate = ((VRControllerState_t)((OpenVRInput)getVRinput()).getRawControllerState(index));
rawstate.read();
for(int i=0;i<rawstate.rAxis.length;i++) {
VRControllerAxis_t cs = rawstate.rAxis[i];
cs.read();
out += "Controller#" + Integer.toString(index) + ", Axis#" + Integer.toString(i) + " X: " + Float.toString(cs.x) + ", Y: " + Float.toString(cs.y) + "\n";
}
out += "Button press: " + Long.toString(rawstate.ulButtonPressed) + ", touch: " + Long.toString(rawstate.ulButtonTouched) + "\n";
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(controllerTextFile, true));
writer.write(out);
} catch(Exception e) { }
try {
writer.close();
} catch(Exception e) { }
}
} else {
geo.setCullHint(CullHint.Always); // hide it
}
}
}
private void addAllBoxes() {
float distance = 8;
for (int x = 0; x < 35; x++) {
float cos = FastMath.cos(x * FastMath.PI / 16f) * distance;
float sin = FastMath.sin(x * FastMath.PI / 16f) * distance;
Vector3f loc = new Vector3f(cos, 0, sin);
addBox(loc, null, 1f);
loc = new Vector3f(0, cos, sin);
addBox(loc, null, 1f);
}
}
private static final Box smallBox = new Box(0.3f, 0.3f, .3f);
private void addBox(Vector3f location, Quaternion rot, float scale) {
Geometry leftQuad = new Geometry("Box", smallBox);
if( rot != null ) {
leftQuad.setLocalRotation(rot);
} else {
leftQuad.rotate(0.5f, 0f, 0f);
}
leftQuad.setLocalScale(scale);
leftQuad.setMaterial(mat);
leftQuad.setLocalTranslation(location);
rootNode.attachChild(leftQuad);
}
}