Official VR module

Hello,

I’m interested in Virtual Reality (VR) and I’ve found some work on it with JMonkey (JMonkeyVR from phr00t). My problem was that phr00t has forked a specific JMonkeyEngine distribution in order to use VR capabilities.

As I use official JMonkeyEngine build within my application, I’ve made a module for the official distribution that can handle VR capabilities within JMonkey (my module use some of the phr00t VR specific sources but with the official JMonkeyEngine).

I’ve a functional jme3 VR module (called jme3-vr) and i would like to integrate it to the official JMonkeyEngine distribution.

At this time, my module is fully functional and tested with an HTC vive VR system.

Are you interested by such an integration ?

11 Likes

I’ll let the core team speak for themselves. These are my personal views as someone interested and invested in vr for jMonkeyEngine.

It’s great that you work on VR capabilities for jMonkeyEngine.

I have a couple of concerns based on your post:

What does “fully functional” mean? Has it implemented the full extent of the OpenVR API? If not, what is your method?

I think that the name “jme3-vr” implies that it’s a generic module for VR, supporting a number of different API’s and headsets. I doubt we’ll see such a module until there is an agreed and generic VR API that is widely adpoted by VR manufacturers. For now, each API requires a great deal of work to implement and maintain, especially since they’re going through rapid development themselves.

I assume that licenses can make it very difficult to integrate something like this into the main distribution.

1 Like

Hello Rickard,

By fully functional, i mean that you can access to all underlying VR system (at this time both OpenVR and OSVR API are mapped using JNA calls). An independent API (called VRAPI) is the interface within JMonkey and the underlying system. So the JMonkey developper has only to deal with the high level VRAPi as its delegate to the selected native implementation.

The actual module support the OpenVR and OSVR systems that are the main actors within the domain. In term of devices, all the devices compatible with these systems are so supported (HTC Vive, Morpheus, …).

Regarding the licenses, the system dynamically uses the VR system and so it does not integrate any code coming from the native API themselves.

1 Like

Sweet! Is it on github or anywhere public?

1 Like

It’s not at this time on github but i’ve followed the JMonkey contribution guide. I’ve now a forked branch of the JMonkeyEngine project called jme3-vr. How can i share this branch with you ?

1 Like

Here is my fork of the JMonkeyEngine repository: GitHub - jseinturier/jmonkeyengine at jme3-vr-dev

3 Likes

This is very cool! Thank you!
Do you plan adding Google Daydream VR as well?

2 Likes

Hello,

At this time my first aim is to produce a VR module that can be used with the standard JMonkey distribution. There is some work to make JMonkey handling instanced rendering that is in use within VR systems.

for the google Daydream, at this time i’m handling OpenVR ans OSVR as underlying systems. These ones are the most used by actual devices (HTC Vive, Playstation VR, Occulus Rift). If the Daydream provide an open API, it will be integrated.

1 Like

There seems to be progress with Daydream VR: http://uploadvr.com/google-now-lets-anyone-submit-daydream-apps/
Here is the API: https://developers.google.com/vr/android/reference_overview

1 Like

I hope I’ll have time to test this within a couple of weeks. I’m already using jme-core with jMonkeyVR but with a few modifications. Would be nice to not need them.
Then I might revisit my fork with OpenVRAppState. Seems updates to the underlying API are not so frequent anymore so it might be worth maintaining it now.

1 Like

I’ve integrated OpenVR 1.0.6 as underlying VR API. I’m finalizing small tests and i will commit my changes. I’ve made another small changes and now i’ve not to modify jme3-core in order to have a VR rendering effective.

1 Like

I’m working also at major code refactoring in order to separate VR API (one package for OpenVR, one package for OSVR, …). With this refactoring it will be easy to integrate / maintain various systems within the jme3-vr package.

2 Likes

For all that use LWGJL 3 with a GeForce card. The last driver (378.49) have broke all my LWJGL based application (and of course the jme3-vr module). I’ve lost a lot of time by investigating what was the problem and so my last jme3-vr commit is delayed.

It seems that many people have the same problem so be careful and do not update !!

3 Likes

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);
    }
}
3 Likes

Hello all,

It has been a lot ow work but i’ve committed the last version of jme-vr within my branch (https://github.com/jseinturier/jmonkeyengine/tree/jme3-vr-dev3))). The module now provides an app state (called VRAppState) that enable to use underlying VR system (at this time OpenVR and OSVR). The is no more need of a specific VR Application. Here is an updated version of the sample application:

package sample;



import java.io.File;
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 com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.VRAppState;
import com.jme3.app.VRConstants;
import com.jme3.app.state.AppState;
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.OpenVR;
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.AppSettings;
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 jmevr.util.VRGuiManager.POSITIONING_MODE;

/**
 *
 * @author reden
 */
public class VRAppStateSample extends SimpleApplication {

	private static final Logger logger = Logger.getLogger(VRAppStateSample.class.getName());
    
    // general objects for scene management
    Node boxes = new Node("");
    Spatial observer;
    boolean moveForward, moveBackwards, rotateLeft, rotateRight;
    Material mat;
    Node mainScene;
    Geometry leftHand, rightHand;

    private float distance  = 100f;
    private float prod      = 0f;
    private float placeRate = 0f;
    
    VRAppState vrAppState = null;
    
    public VRAppStateSample(AppState... initialStates) {
        super(initialStates);
        
        vrAppState = getStateManager().getState(VRAppState.class);
    }
    
    
    @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);
    	
        mainScene = new Node("scene");
        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/Misc/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");
        rightHand = leftHand.clone();
        Material handMat = new Material(getAssetManager(), "Common/MatDefs/Misc/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 = vrAppState.getVRGUIManager().getCanvasSize();
        Picture test = new Picture("testpic");
        test.setImage(getAssetManager(), "Textures/crosshair.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)
        vrAppState.getVRGUIManager().setPositioningMode(POSITIONING_MODE.AUTO_OBSERVER_ALL);
        vrAppState.getVRGUIManager().setGuiScale(0.4f);
        
        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));
        
        vrAppState.setObserver(observer);
        mainScene.attachChild(observer);
        rootNode.attachChild(mainScene);
        
        addAllBoxes();

        initInputs();
        
        // use magic VR mouse cusor (same usage as non-VR mouse cursor)
        getInputManager().setCursorVisible(true);
      
        // 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("dumpImages", new KeyTrigger(KeyInput.KEY_I));
        inputManager.addMapping("exit", new KeyTrigger(KeyInput.KEY_ESCAPE));
        
        ActionListener acl = new ActionListener() {

            public void onAction(String name, boolean keyPressed, float tpf) {
                if(name.equals("incShift") && keyPressed){
                	vrAppState.getVRGUIManager().adjustGuiDistance(-0.1f);
                }else if(name.equals("decShift") && keyPressed){
                	vrAppState.getVRGUIManager().adjustGuiDistance(0.1f);
                }else if(name.equals("filter") && keyPressed){
                    // adding filters in realtime
                    CartoonSSAO cartfilt = new CartoonSSAO(vrAppState.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)
                    vrAppState.moveScreenProcessingToVR();
                }
                if( name.equals("toggle") ) {
                	vrAppState.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("dumpImages") ) {
                    ((OpenVR)vrAppState.getVRHardware()).getCompositor().CompositorDumpImages.apply();
                }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") ) {
                    stop(true);
                    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");
    }
     
     @Override
     public void simpleUpdate(float tpf){

         //FPS test
         /*tpfAdder += tpf;
         tpfCount++;
         if( tpfCount == 60 ) {
             System.out.println("FPS: " + Float.toString(1f / (tpfAdder / tpfCount)));
             tpfCount = 0;
             tpfAdder = 0f;
         }*/
         
         prod+=tpf;
         distance = 100f * FastMath.sin(prod);
         boxes.setLocalTranslation(0, 0, 200f+ distance);
         
         if(moveForward){
             observer.move(vrAppState.getFinalObserverRotation().getRotationColumn(2).mult(tpf*8f));
         }
         if(moveBackwards){
             observer.move(vrAppState.getFinalObserverRotation().getRotationColumn(2).mult(-tpf*8f));
         }
         if(rotateLeft){
             observer.rotate(0, 0.75f*tpf, 0);
         }
         if(rotateRight){
             observer.rotate(0, -0.75f*tpf, 0);
         }
         
         handleWandInput(0, leftHand);
         handleWandInput(1, rightHand);
         if( placeRate > 0f ) placeRate -= tpf;
     }
     
     private void handleWandInput(int index, Geometry geo) {
    	 
         Quaternion q = vrAppState.getVRinput().getFinalObserverRotation(index);
         Vector3f v = vrAppState.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( vrAppState.getVRinput().getAxis(index, VRInputType.ViveTriggerAxis).x >= 1f &&
                 placeRate <= 0f ) {
                 placeRate = 0.5f;
                 addBox(v, q, 0.1f);
                 vrAppState.getVRinput().triggerHapticPulse(index, 0.1f);
             }
             // print out all of the known information about the controllers here
             /*for(int i=0;i<VRInput.getRawControllerState(index).rAxis.length;i++) {
                 VRControllerAxis_t cs = VRInput.getRawControllerState(index).rAxis[i];
                 System.out.println("Controller#" + Integer.toString(index) + ", Axis#" + Integer.toString(i) + " X: " + Float.toString(cs.x) + ", Y: " + Float.toString(cs.y));
             }
             System.out.println("Button press: " + Long.toString(VRInput.getRawControllerState(index).ulButtonPressed.longValue()) + ", touch: " + Long.toString(VRInput.getRawControllerState(index).ulButtonTouched.longValue()));
             */
         } 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 void addBox(Vector3f location, Quaternion rot, float scale) {
        Box b = new Box(0.3f, 0.3f, 0.3f);

        Geometry leftQuad = new Geometry("Box", b);
        if( rot != null ) {
            leftQuad.setLocalRotation(rot);
        } else {
            leftQuad.rotate(0.5f, 0f, 0f);
        }
        leftQuad.setLocalScale(scale);
        leftQuad.setMaterial(mat);
        leftQuad.setLocalTranslation(location);
        mainScene.attachChild(leftQuad);
    }
    
    private static void initLog(){
    	// 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);
            }
          }
        }
    }
    
    /**
     * Create a {@link VRAppState VR app state} and use a Simple application that use it.<br>
     * The recommended procedure is:<br>
     * <ul>
     * <li>Create some {@link AppSettings AppSettings} with VR related parameters.
     * <li>Instanciate the {@link VRAppState VRAppState} attached to the settings.
     * <li>Instanciate your {@link Application Application}.
     * <li>Attach the settings to the application.
     * <li>Start the application.
     * </ul>
     * @param args not used
     */
    public static void main(String[] args){
    	
    	// Init the log to display all the configuration informations.
    	// This is not needed within final application.
    	initLog();
    	
    	// Prepare settings for VR rendering. 
    	// It is recommended to share same settings between the VR app state and the application.
    	AppSettings settings = new AppSettings(true);
    	
    	settings.put(VRConstants.SETTING_VRAPI, VRConstants.SETTING_VRAPI_OPENVR_VALUE); // The VR api to use (need to be present on the system)
    	settings.put(VRConstants.SETTING_DISABLE_VR, false);          // Enable VR
    	settings.put(VRConstants.SETTING_ENABLE_MIRROR_WINDOW, true); // Enable Mirror rendering oh the screen (disable to be faster)
    	settings.put(VRConstants.SETTING_VR_FORCE, false);            // Not forcing VR rendering if no VR system is found.
    	settings.put(VRConstants.SETTING_GUI_CURVED_SURFACE, true);   // Curve the mesh that is displaying the GUI
    	settings.put(VRConstants.SETTING_FLIP_EYES, false);           // Is the HMD eyes have to be inverted.
    	settings.put(VRConstants.SETTING_NO_GUI, false);              // enable gui.
    	settings.put(VRConstants.SETTING_GUI_OVERDRAW, true);         // show gui even if it is behind things.
    	
    	settings.put(VRConstants.SETTING_DEFAULT_FOV, 108f);          // The default ield Of View (FOV)
    	settings.put(VRConstants.SETTING_DEFAULT_ASPECT_RATIO, 1f);   // The default aspect ratio.
    	
    	settings.setRenderer(AppSettings.LWJGL_OPENGL3); // Setting the renderer. OpenGL 3 is needed if you're using Instance Rendering.
    	
    	
    	// Initialise VR AppState
        VRAppState vrAppState = new VRAppState(settings);
    	
    	// Create the sample application with the VRAppState attached.
        // There is no constraint on the Application type.
        SimpleApplication test = new VRAppStateSample(vrAppState);
        test.setShowSettings(false);

        // Sharing settings between app state and application is recommended.
        test.setSettings(settings);   
        
        // Starting the application.
        test.start();
    }
}
7 Likes

Good job.

Wonderful, I’ll try to find the time to test all this.

Nice! I plan to test this out the coming week.

Took me about 30 minutes to get this (VrAppState) integrated in one of my hobby projects (where 10 minutes were spent syncing and merging jmonkeyengine master).

I’d say it doesn’t get any easier than that.

:+1:

1 Like

Running a bunch of times, seldom I get an issue where the view isn’t attached properly (just a grey room) with the following error in the log:
feb 13, 2017 1:55:37 EM jmevr.util.VRViewManager sendTextures
Submit to right compositor error: Do not have focus (101)