Multiple window support (after JOGL is implemented)

I know that LWJGL is specifically geared for having just a single window available, but I don't think JOGL carries that limitation.  It would be really useful for me to have multiple 3d windows from the same application, especially for multiple monitor systems.  I have done this successfully before with raw OpenGL and also in Ogre3D, so I do know what's involved.  Though I've just started with jME, the single-window assumption seems to have carried through and would require some refactoring of the DisplaySystem and Renderer to hold a configurable instance per display window (like background color, camera targets, etc) instead of as effectively singletons.  Is this something that the devs see as reasonable for future consideration?

In a project I am involved in we use a 4-view swing frame with 4 canvases. This works in principle we only had problems with the texture ids and contexts, not sure if they are solved now.



Are you talking about separate windows or about embedded canvases?

Separate windows.

JOGL doesn't support windows at all does it? (only Canvas).

I've got this up and running in LWJGL, separate canvases in separate windows. Resizing did work I think, but due to general shenanigans I ended up not using it.



Disclaimer: This was experimental (and a little hit and miss) but it works well on linux, os x and windows. Lots of stuff is in here that may not be required… The idea is that you can make several MagicWindows in seperate windows at the same time.



MagicWindows creates the frame and sets up the rendering loop:



public class MagicWindow extends JFrame
{

   
   public MagicWindow(Waterfall toMake, Root root)
   {
      // center the frame
      setLocation(450,400);
      // show frame
      setVisible(true);
      setResizable(false);
      this.toMake = toMake;
      this.root = root;
      addKeyListener(Sity.self);
      Parameters.setIcon(this);
      
      addWindowListener(new WindowAdapter()
      {
         public void windowClosing(WindowEvent e)
         {
            dispose();
         }
      });

      init();
      pack();

      thread = new MonkeyThread(comp);
      thread.start();
   }

   ...

   public void end()
   {
      Parameters.magicWindow = null;
      Sity.self.update();
      thread.stopMonkeyingAround();
      dispose();
   }

   private static final long serialVersionUID = 1L;

   JPanel contentPane;

   JPanel mainPanel = new JPanel();

   public Canvas comp = null;

   JPanel spPanel = new JPanel();

   MagicWindowMonkey impl;

   // Component initialization
   private void init()
   {
      contentPane = (JPanel) getContentPane();
      contentPane.setLayout(new BorderLayout());

      //mainPanel.setLayout(new GridBagLayout());

      setTitle("Sity - preview view");

      //


GL STUFF

      // make the canvas:
      comp = DisplaySystem.getDisplaySystem("lwjgl").createCanvas(width,
            height);

      // add a listener... if window is resized, we can do something about it.
      comp.addComponentListener(new ComponentAdapter()
      {
         public void componentResized(ComponentEvent ce)
         {
            doResize();
         }
      });

      // Important! Here is where we add the guts to the panel:
      impl = new MagicWindowMonkey(width, height, toMake, root, this, comp);
      
      comp.addKeyListener(Sity.self);
      comp.addKeyListener(impl.eventDispatch);
      ((JMECanvas) comp).setImplementor(impl);
      comp.requestFocus();

      //
END OF GL STUFF

      //mainPanel.add(spPanel, new GridBagConstraints(0, 5, 1, 1, 1.0, 1.0,
      //      GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(
      //            5, 5, 0, 5), 0, 0));
      comp.setBounds(0,0,width, height);
      //comp.setSize(width,height);
      contentPane.add(comp, BorderLayout.CENTER);
}

   protected void setSide(Component comp)
   {
      spPanel.removeAll();
      spPanel.add(comp);
   }
   
   /**
    * Should never resize, too complicated to reconcile JME and AWT TLAs!
    *
    */
   protected void doResize()
   {
      impl.resizeCanvas(comp.getWidth(), comp.getHeight());
      //impl.resizeCanvas(width, height);
   }

   // Overridden so we can exit when window is closed
   protected void processWindowEvent(WindowEvent e)
   {
      super.processWindowEvent(e);
      if (e.getID() == WindowEvent.WINDOW_CLOSING)
      {
         pop(); // just close the window, nothing else!
      }
   }
   
   protected Canvas getCanvas()
   {
      return comp;
   }
}





MagicWindowMonkey fills this in and listens for someone clicking into the window, then handles the AWT input (in the jme event loop, hence the eventDispatch stuff) and does other monkey related stuff...



public class MagicWindowMonkey extends JMECanvasImplementor implements Anchor
{

   // Items for scene
   protected Node rootNode;

   protected com.jme.util.Timer timer;

   protected float tpf;

   protected Camera cam;

   Vector3f loc = new Vector3f(10.0f, 1.5f, 0.0f); // camera location at start

   protected DisplaySystem display;

   protected int width, height;

   protected InputHandler input;

   private Root root = null;
   
   private Waterfall source = null;

   private MagicWindow magicWindow = null;

   protected EventDispatch eventDispatch = new EventDispatch();

   // list of awt components to be updated
   private List<Updateable> toUpdate = new LinkedList<Updateable>();

   // a flag from teh AWT to JME threads to say erase and rewind
   private boolean doRefresh = false;
   
   private Canvas canvas;

   // if true monkey display has more speed, but is not selectable
   private final static boolean noSelect = true;
   
   /**
    * This class should be subclasses - not directly instantiated.
    *
    * @param width
    *            canvas width
    * @param height
    *            canvas height
    */
   protected MagicWindowMonkey(int width, int height, Waterfall s, Root root, MagicWindow b, Canvas c)
   {
      this.width = width;
      this.height = height;
      this.root = root;
      canvas = c;
      source = s;
      magicWindow = b;
      // eventDispatch = new EventDispatch();
      magicWindow.getCanvas().addMouseListener(eventDispatch);
      magicWindow.getCanvas().addMouseMotionListener(eventDispatch);
      magicWindow.getCanvas().addMouseWheelListener(eventDispatch);
      //magicWindow.getCanvas().addKeyListener(eventDispatch);
   }

   public void doSetup()
   {
      display = DisplaySystem.getDisplaySystem();//= Parameters.getDisplay();
      //renderer = Parameters.getRenderer();
      
      renderer = new LWJGLRenderer(800, 600);
      renderer.setHeadless(true);
      display.setRenderer(renderer);
      DisplaySystem.updateStates(renderer);
      // Create a camera specific to the DisplaySystem that works with the width and height

      cam = renderer.createCamera(width, height);

      // Set up how our camera sees.
      cam.setFrustumPerspective(45.0f, (float) width / (float) height, 1, 1000);

      Vector3f left = new Vector3f(1.0f, 0.0f, 0.0f);
      Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f);
      Vector3f dir = new Vector3f(0.0f, 0f, 1.0f);

      // Move our camera to a correct place and orientation.
      cam.setFrame(loc, left, up, dir);
      
      // Signal that we've changed our camera's location/frustum.
      // cameraPerspective();
      //cameraParallel();
      
      cam.update();


      // Assign the camera to this renderer.
      renderer.setCamera(cam);

      renderer.setBackgroundColor(new ColorRGBA(0.1f, 0.2f, 0.6f, 1.0f));

      // Get a high resolution timer for FPS updates.
      timer = com.jme.util.Timer.getTimer();

      // Create rootNode
      rootNode = new Node("rootNode");

      // Create a ZBuffer to display pixels closest to the camera above farther ones.

      ZBufferState buf = renderer.createZBufferState();
      buf.setEnabled(true);
      buf.setFunction(ZBufferState.CF_LEQUAL);
      // cameraPerspective();
      rootNode.setRenderState(buf);
      
      input = new KeyboardLookHandler(cam, 50f, 3f);
      //input.
      ((JMECanvas) canvas).setUpdateInput(true);

      KeyListener kl = (KeyListener) KeyInput.get();

      canvas.addKeyListener(kl);
      magicWindow.addKeyListener(kl);

      ((AWTMouseInput) MouseInput.get()).setEnabled(true);
      ((AWTMouseInput) MouseInput.get()).setDragOnly(false);
      ((AWTMouseInput) MouseInput.get()).setRelativeDelta(canvas);

      canvas.addMouseListener(eventDispatch);
      
      setFocusListener();
      input.setEnabled(true);

      lightState = renderer.createLightState();
      assert(lightState != null);

      simpleSetup();
      
      setup = true;
   }

   private void setFocusListener()
   {
      canvas.addFocusListener(new FocusListener()
      {
         public void focusGained(FocusEvent arg0)
         {
            ((AWTKeyInput) KeyInput.get()).setEnabled(true);
            ((AWTMouseInput) MouseInput.get()).setEnabled(true);
            input.setEnabled(true);
            //mouseLook = true;
         }

         public void focusLost(FocusEvent arg0)
         {
            ((AWTKeyInput) KeyInput.get()).setEnabled(false);
            ((AWTMouseInput) MouseInput.get()).setEnabled(false);
            input.update(0);
            input.setEnabled(false);
            //mouseLook = false;
         }
      });
   }

   // from baseSimpleGame
   protected void cameraPerspective()
   {
      cam.setFrustumPerspective(45.0f, (float) display.getWidth() / (float) display.getHeight(), 1, 1000);
      cam.setParallelProjection(false);
      cam.update();
   }

   protected void cameraParallel()
   {
      cam.setParallelProjection(true);
      float aspect = (float) display.getWidth() / display.getHeight();
      cam.setFrustum(-100, 1000, -50 * aspect, 50 * aspect, -50, 50);
      cam.update();
   }

   public void doUpdate()
   {

      /** Update tpf to time per frame according to the Timer. */

      timer.update();
      tpf = timer.getTimePerFrame();
      cam.update();

      simpleUpdate();
      doMouseUpdate();
      rootNode.updateGeometricState(tpf, true);
      // go through all registered updaters...

      Iterator<Updateable> it = toUpdate.iterator();
      ArrayList<Updateable> toRemove = new ArrayList<Updateable>();
      while (it.hasNext())
      {
         Updateable u = it.next();
         if (u.isDisposed())
         {
            toRemove.add(u);
         }
         // always do update - in case action on dispose()
         u.doUpdate();

      }
      for (Updateable r : toRemove)
      {
         toUpdate.remove(r);
      }
   }


   public void doRender()
   {
      renderer.clearBuffers();
      renderer.draw(rootNode);
      simpleRender();
      renderer.displayBackBuffer();
   }

   private void createSity()
   {
      createSity(this, false);
   }
   
   /**
    * Evaluate our tree
    * @param resetRandom - reset the random of the found instance? will create
    * new building!
    *
    */
   private void createSity(Anchor anchor, boolean resetRandom)
   {
      ...
      // show it
      rootNode.updateGeometricState(0.0f, true);
      rootNode.updateRenderState();
      
      // put the anchor back the way it was
      Parameters.anchor = oldAnchor;
   }
   
   public void simpleSetup()
   {
      createSity();
      allDone();
   }

   public void simpleUpdate()
   {
      timer.update();
      tpf = timer.getTimePerFrame();
      // very important line....
           input.update( tpf );
   }

   public void simpleRender()
   {
   }

   public Camera getCamera()
   {
      return cam;
   }

   private void doMouseUpdate()
   {

      MouseEvent event;
      while ((event = eventDispatch.getMouseEvent()) != null)
      {
         switch (event.getID())
         {
         case MouseEvent.MOUSE_DRAGGED:
            mouseDraggedUpdate(event);
            break;
         case MouseEvent.MOUSE_PRESSED:
            mousePressedUpdate(event);
            break;
         case MouseEvent.MOUSE_RELEASED:
            mouseReleasedUpdate(event);
            break;
         case MouseEvent.MOUSE_WHEEL:
            break;
         case MouseEvent.MOUSE_MOVED:
            mouseMovedUpdate(event);
            break;
         case MouseEvent.MOUSE_ENTERED:
            mouseEnteredUpdate(event);
         case MouseEvent.MOUSE_EXITED:
            mouseExitUpdate(event);
            break;
         }
      }
      MouseWheelEvent wheel;
      while ((wheel = eventDispatch.getMouseWheelEvent()) != null)
      {
         mouseWheelUpdate(wheel);
      }
      KeyEvent key;
      while ((key = eventDispatch.getKeyEvent()) != null)
      {
         switch (key.getID())
         {
         case KeyEvent.KEY_RELEASED:
            keyRelease(key);
            break;
         }
      }

   }


   /**
    * Key release handler delegated through eventDispatch
    *
    * @param e
    */
   private void keyRelease(KeyEvent e)
   {
      if (e.getKeyCode() == KeyEvent.VK_SPACE)
      {
         ...
      }
      else if (e.getKeyCode() == KeyEvent.VK_ENTER)
      {
         ...
      }
   }

   
   
   /**
    * Takes a mouse event and finds the object to select (or none!)
    *
    * @param e
    *            the mouse event with the locations
    */
   private void processClick(MouseEvent e)
   {
      // jME voodoo to make it work (read: trial and error)
      display.setRenderer(renderer);
      DisplaySystem.updateStates(renderer);
      // Assign the camera to this renderer.
      renderer.setCamera(cam);
      
      ...
   }

   public void mouseDraggedUpdate(MouseEvent e)
   {

   }

   public void mousePressedUpdate(MouseEvent e)
   {

   }

   public void mouseReleasedUpdate(MouseEvent e)
   {
      processClick(e);
   }


   private void mouseWheelUpdate(MouseWheelEvent e)
   {

   }

}




and finally event dispatch (only a bit of this...you can just follow the pattern...) Theres gotta be a better way than this tho!


public class EventDispatch implements MouseMotionListener, MouseListener, ChangeListener, ActionListener, MouseWheelListener, ListSelectionListener, KeyListener

{
   private List<MouseEvent> mouseEvents = new LinkedList<MouseEvent>();

   private List<ChangeEvent> changeEvents = new LinkedList<ChangeEvent>();

   private List<ActionEvent> actionEvents = new LinkedList<ActionEvent>();
   
   private List<MouseWheelEvent> mouseWheelEvents = new LinkedList<MouseWheelEvent>();
   
   private List<ListSelectionEvent> listEvents = new LinkedList<ListSelectionEvent>();

   private List<KeyEvent> keyEvents = new LinkedList<KeyEvent>();
   
   private boolean mouseDown;
   
   private synchronized void addMouseEvent(MouseEvent e)
   {
      mouseEvents.add(e);
   }

   public synchronized MouseEvent getMouseEvent()
   {
      if (!mouseEvents.isEmpty())
         return mouseEvents.remove(0);
      return null;
   }

   private synchronized void addChangeEvent(ChangeEvent e)
   {
      changeEvents.add(e);
   }

   public synchronized ChangeEvent getChangeEvent()
   {
      if (!changeEvents.isEmpty())
         return changeEvents.remove(0);
      return null;
   }
   ...
   
   public synchronized KeyEvent getKeyEvent()
   {
      if (!keyEvents.isEmpty())
         return keyEvents.remove(0);
      return null;
   }
   
   public void mouseMoved(MouseEvent e)
   {
      addMouseEvent(e);
   }

   public void mouseClicked(MouseEvent e)
   {
      addMouseEvent(e);
   }
   ...


Thanks!  I didn't know that was a possibility, from as far as I've gotten in jME yet.  This code will definitely help me get bootstrapped. :slight_smile:

@twak
Will this code also work on JME3? Or is there a more up to date similar approach?

Hi

@llama said: JOGL doesn't support windows at all does it? (only Canvas).
Are you completely mad????? JOGL has its own native windowing toolkit called NEWT (since its version 2.0) and even its previous major version (1) supports windows with AWT.

I confirm that NEWT (JOGL) supports both (physical and virtual) monitors and multiple windows.

@White_Flame What do you mean by “After JOGL is implemented”? The JogAmp backend is already working.

@gouesse

So does that mean that JME3 can have multiple hardware accelerated windows? Using JOGL?
AWT would have pretty bad performance from what I understand, and lwjgl will not support multiple windows until version 3.

@DieSlower said: So does that mean that JME3 can have multiple hardware accelerated windows? Using JOGL?
You can use several instances of JoglNewtDisplay, one per window.
@DieSlower said: AWT would have pretty bad performance from what I understand, and lwjgl will not support multiple windows until version 3.
AWT is slower and less cross-platform than NEWT and I'm not here to promote other APIs. I would feel better if the developers here could stop seeing any API by having this one in mind. It's annoying as there are plenty features JogAmp is mostly alone to support for years.

@gouessej
Ah…interesting, is there an example somewhere that shows how to set up JoglNewtDisplay?
Also, what kind of performance difference does it have vs an lwjgl window?

@DieSlower said: @gouessej Ah...interesting, is there an example somewhere that shows how to set up JoglNewtDisplay?
Just call AppSettings.setRenderer("JOGL") (in simpleInitApp() if you extend SimpleApplication?). Then, JmeDesktopSystem will create it for you. There is no example using multiple instances. Sorry, this is typically the kind of example I created for another engine, 3 canvases in a window, several windows, ...
Also, what kind of performance difference does it have vs an lwjgl window?
I'm not here to promote other APIs and I refuse to compare JogAmp (JOGL, JOAL, JOCL, ...) with this library. I can just confirm that NEWT is as fast as any native windowing toolkit calling the APIs of the operating system, I don't see how another library could be noticeably faster. NEWT can still be used with other heavyweight toolkits like AWT and SWT. It even supports Android :)
1 Like