TP de construction d'une application de Réalité Virtuelle Coopérative

UE Optionnelle EVC

M2 STS spécialité Informatique mentions GL et Mitic


Année 2013/2014

Encadrement : Thierry Duval
thierry.duval@irisa.fr


Première partie : Interface 2D et 3D pour la navigation et l'interaction en univers 3D

Temps estimé : 3 séances de 2h

Introduction

Il s'agit ici de créer une IHM 2D avec l'API Swing de java afin de faciliter les interactions avec une application 3D réalisée à l'aide de Java3D et a priori conçue indépendamment de toute interface 2D.

Cette application 3D pourra offrir des fonctionnalités 3D de navigation ou d'interaction avec les objets de l'univers, et il s'agit ici d'offrir en plus de ces fonctionnalités 3D des modes de navigation ou d'interaction 2D explicites.

Cette application 3D sera visualisée à l'aide d'une ou plusieurs instances de Canvas3D, et il faudra donc assurer la communication entre IHM 2D et application 3D de façon à ce que les actions effectuées dans l'un ou l'autre de ces composants soient répercutées dans les autres.

Ce qui sera réalisé dans cette première partie sera en fait par la suite la partie présentation (au sens du modèle PAC) d'un client participant à une session de RV coopérative.

Idéalement, cette première partie devrait être traitée en 6h (3 séances de TP).


1 - L'univers virtuel 3D

Dans un premier temps, il va falloir mettre en place dans une application Java3D des moyens permettant :
Définir la notion d'objet de l'univers virtuel

Il va falloir définir une classe correspondant à un objet que l'on pourra placer dans un graphe de scène Java3D :

Peupler l'univers virtuels d'objets autonomes

Il faudra placer des objets dans cet univers.
Dans un premier temps, on pourra utiliser une instance de SimpleUniverse pour la gestion de l'univers à explorer.
Ce SimpleUniverse devra être visualisé dans un Canvas3D, lui même placé dans un JFrame.
Certains de ces objets pourront être animés à l'aide d'interpolateurs, d'autres pourront rester fixes et seront plus particulièrement dédiés à l'interaction.
Cela pourra donner lieu à des dérivations de la classe VirtualObject.
Il faudra également permettre à l'utilisateur de créer un nouvel objet de l'univers en chargeant un fichier VRML, en demandant à l'utilisateur de préciser le nom et l'emplacement de ce fichier VRML, ainsi que le nom, la position et l'orientation à donner à l'objet une fois chargé.


2 - Naviguer dans l'univers virtuel

Dans un second temps, il va s'agir de créer un navigateur composé des éléments permettant de parcourir aisément le monde 3D associé, via des actions sur le point de vue de la caméra qui observe le monde (ou des caméras qui observent le monde...) :
Les appels à ces fonctionnalités

Ils devront être réalisés à partir de l'IHM 2D du navigateur, en réponse à des actions de l'utilisateur.
L'IHM 2D devra donc avoir un point d'entrée sur l'univers virtuel ou sur le support de caméra à déplacer.
Des "Behaviors" adaptés pourront aussi être ajoutés ensuite directement dans l'univers 3D pour permettre la navigation3D.

Représentation graphique de l'ensemble

Il faut prévoir un JFrame intégrant l'IHM 2D, avec des interacteurs 2D "innovants", et intégrant aussi le Canvas3D, ce qui permettra les communications entre ces deux éléments.

Retours d'informations vers l'utilisateur

Il serait bon de prévoir une représentation dans l'IHM 2D de la position (et éventuellement de l'orientation) du point de vue 3D sur l'univers virtuel.


3 - Offrir de l'interaction avec les objets de l'univers

Il est souhaitable d'offrir ensuite à un utilisateur la possibilité d'interagir avec les objets de l'univers.
Ici encore cela pourra être fait via une IHM 2D ou directement par des actions dans l'univers 3D.

La sélection des objets en interaction

Il faut permettre à l'utilisateur de sélectionner les objets de l'univers avec lesquels il veut interagir :
Il faudrait pouvoir ainsi permettre de sélectionner plusieurs objets pour effectuer ensuite une action simultanément sur tous les objets sélectionnés.

L'interaction avec les objets

D'une façon similaire à ce qui est offert pour la navigation, il faut permettre à un utilisateur d'interagir (via l'IHM 2D ou directement dans l'univers 3D) avec un objet selon les modes suivants :

4 - Première intégration de périphériques d'interaction

Offrir éventuellement à l'utilisateur la possibilité de piloter la navigation 3D à l'aide du joystick associé à une wiimote.
L'usage de la wiimote sera réservé par la suite à l'interaction avec les objets de l'univers.

Autre possibilité : offrir à l'utilisateur la possibilité de piloter le la navigation 3D à l'aide d'interactions sur une tablette tactile ou un smartphone,
par exemple via des appuis sur des boutons ou bien en utilisant l'accéléromètre.

Dans les deux cas, la navigation 3D doit être pilotée via les mêmes points d'entrée que dans le cas d'un pilotage par une ihm 2D.

5 - Offrir plusieurs points de vue sur le même univers

Associer l'univers 3D à plusieurs points de vues permettant de le visualiser simultanément sous plusieurs angles :

Remarques

Les .jar de Java3D (j3dcore.jar j3dutils.jar vecmath.jar) se trouvent dans le répertoire /usr/local/EVC/j3d-1_5_2-linux-i586/lib/ext/ :
il faudra ajouter ces "External JARs" au "Java Build Path" de votre projet eclipse.

Les bibliothèques libj3dcore-ogl-cg.so et libj3dcore-ogl.so se trouvent quant à elles dans le répertoire /usr/local/EVC/j3d-1_5_2-linux-i586/lib/i386/ :
il faudra les ajouter à votre LD_LIBRARY_PATH dans votre environnement d'exécution (soit dans eclipse, soit dans votre session Linux).

La suite...

la classe VirtualObject de cette première partie va s'avérer être simplement la facette présentation d'un objet virtuel !
idem pour le navigateur !
il y aura donc un peu de refactoring à faire, mais avec eclipse c'est facile...


Seconde partie : partager l'univers 3D entre plusieurs navigateurs, sur différentes machines

Temps estimé : 3 séances de 2h

Une première approche, totalement centralisée

On propose ici de commencer par implanter un serveur qui va héberger un monde virtuel et les objets qui le composent.

Pour des objets virtuels décomposés sous la forme d'agents PAC, on peut considérer que :
Dans une telle situation :
Cette décomposition est également possible pour la notion d'UniversPartagé, dont la partie présentation, située sur les clients, correspondra à ce qui a été réalisé lors des 3 premières séances de TP (instance de SimpleUniverse ou de VirtualUniverse associée à un Canvas3D).

Seules les facettes Présentation des différents objets seront autorisées à utiliser Java3D.

Communications entre serveur et clients

Une fois le serveur lancé, des clients devront pouvoir demander à ce sonnecter sur le serveur de façon à exploiter un univers partagé :
A partir d'un client, il devra être possible de demander la création de nouveaux objets virtuels :
Les interactions avec les objets partagés se feront à l'initiative de l'utilisateur, au travers des facettes Présentation des objets virtuels et des visualisateurs, elles seront transmises aux facettes ContrôleServeur puis Abstraction, puis répercutées sur les facettes ContrôleClient puis Présentation.

Le serveur pourra être mis à disposition des clients via l'usage des RMI Java ou de toute autre technique équivalente.
Le serveur aura intérêt à utiliser des techniques basées sur le multicast pour diffuser des modifications à l'ensemble des clients.

Un nouveau type d'objet : le point de vue

Il va falloir définir une nouvelle notion, dérivée de celle du VirtualObject, celle de point de vue : VirtualViewpoint.

De nouveaux types d'objets

Il sera souhaitable par la suite de définir de nouveaux types d'objets, avec des comportements différents, permettant d'avoir par exemple des animations ou des comportements autonomes (suivi d'un objet particulier, évitement de certains objets, réponses adaptées à certains événements, ...).

Autres fonctionnalités attendues

Persistance de l'univers virtuel partagé.



Troisième partie : implémenter des métaphores d'interaction coopératives

Temps estimé : 2 séances de 2h

Concept de Cabine Virtuelle d'Immersion

Il s'agira ici d'implémenter de nouveaux types d'objets permettant à l'utilisateur :
Exemples concrets :
  1. Implémentation d'un curseur 3D associé à l'utilisateur
  2. Implémentation d'une manipulation multi-utilisateurs / multi-points


Quatrième partie : faire évoluer le modèle d'architecture des univers partagés

Temps estimé : 2 séances de 2h

Évolution vers une architecture hybride

Il s'agira ici de permettre une gestion locale de certains objets par les clients qui les utilisent de façon privilégiée :


Annexe 1 : communications réseau en java


Envoi de message en multicast :

package communicationUtilities;

import java.io.IOException;
import java.io.ByteArrayOutputStream ;
import java.io.ObjectOutputStream ;
import java.io.Serializable ;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.rmi.RemoteException;

import javax.vecmath.Quat4d ;
import javax.vecmath.Vector3d ;

public class BroadcastUpdates implements Serializable {

    private static final long serialVersionUID = 1L;
    private int portDiffusion ;
    private String nomGroupe ;
    public String getNomGroupe () {
      return nomGroupe ;
   }

   private InetAddress adresseDiffusion ;
    private transient MulticastSocket socketDiffusion ;

    public BroadcastUpdates (final String ng, final int portDiffusion)
    throws RemoteException {
        this.portDiffusion = portDiffusion ;
        nomGroupe = ng ;
        System.out.println ("Diffuseur sur le port " + portDiffusion +
                " a destination du groupe " + nomGroupe) ;
        adresseDiffusion = null ;
        socketDiffusion = null ;
        try {
            adresseDiffusion = InetAddress.getByName (nomGroupe) ;
            socketDiffusion = new MulticastSocket () ;
            socketDiffusion.setTimeToLive (64) ;
            socketDiffusion.setLoopbackMode (true) ; // pour des envois d'une machine à une autre
            // si on veut faire des tests avec serveur et clients sur une même machine, il faut écrire : socketDiffusion.setLoopbackMode (false)
        } catch (IOException e) {
            e.printStackTrace () ;
        }
        System.out.println ("socket : " + socketDiffusion.getLocalPort() + " " + socketDiffusion.getInetAddress ()) ;
    }

    public void diffuseMessage (String name, Vector3d pos, Quat4d quat) {
       ByteArrayOutputStream baos = new ByteArrayOutputStream () ;
       ObjectOutputStream oos ;
      try {
           oos = new ObjectOutputStream (baos) ;
           oos.writeObject (name) ;
           oos.writeObject (pos) ;
           oos.writeObject (quat) ;
           oos.flush () ;
      } catch (IOException e) {
          e.printStackTrace();
      }
        DatagramPacket paquet = new DatagramPacket (baos.toByteArray (), baos.toByteArray ().length,
                adresseDiffusion,
                portDiffusion) ;
        try {
            socketDiffusion.send (paquet);
        } catch (IOException e) {
            e.printStackTrace () ;
        }
    }

    public int getPortDiffusion () throws RemoteException {
        return (portDiffusion) ;
    }

    public InetAddress getAdresseDiffusion () throws RemoteException {
        return (adresseDiffusion) ;
    }

}

Réception de message en multicast :

package communicationUtilities;

import java.io.ByteArrayInputStream ;
import java.io.ObjectInputStream ;
import java.net.DatagramPacket ;
import java.net.InetAddress ;
import java.net.MulticastSocket ;
import java.rmi.RemoteException ;

import javax.vecmath.Quat4d ;
import javax.vecmath.Vector3d ;

import controlInterface.IC_SharedUniverse ;

public class ReceiverUpdates extends Thread implements Runnable {

    private transient MulticastSocket socketReception ;
    private IC_SharedUniverse deportedClient ;
    public void setDeportedClient (IC_SharedUniverse deportedClient) {
      this.deportedClient = deportedClient ;
   }


    private String name = new String () ;
    private Vector3d pos = new Vector3d () ;
    private Quat4d quat = new Quat4d () ;

    public ReceiverUpdates (final String nomGroupe, final int portDiffusion) {
        socketReception = null ;
        try {
           InetAddress adresseDiffusion = InetAddress.getByName (nomGroupe) ;
            socketReception = new MulticastSocket (portDiffusion) ;
            socketReception.joinGroup (adresseDiffusion) ;
            socketReception.setLoopbackMode (true) ;
            System.out.println ("socket : " + socketReception.getLocalPort() + " " + socketReception.getInetAddress ()) ;

        } catch (Exception e) {
            e.printStackTrace () ;
        }
    }

    public void recevoir () {
        try {
            byte [] message = new byte [1024] ;
            DatagramPacket paquet = new DatagramPacket (message, message.length) ;
            socketReception.receive (paquet) ;
            ByteArrayInputStream bais = new ByteArrayInputStream (paquet.getData ()) ;
            ObjectInputStream ois = new ObjectInputStream (bais) ;
         name = (String)ois.readObject () ;
         pos = (Vector3d)ois.readObject () ;
         quat = (Quat4d)ois.readObject () ;
        } catch (Exception e) {
            e.printStackTrace () ;
        }
    }


    public void run () {
        while (true) {
            recevoir () ;
            try {
            deportedClient.objectUpdateTransform (name, pos, quat) ;
         } catch (RemoteException e) {
            e.printStackTrace();
         }
        }
    }

}

Plages d'adresses de multicast et TTL (Time To Live) :

Les adresses utilisables pour le multicast vont de 224.0.0.0 à 239.255.255.255.
Pour des utilisations spécifiques locales, il est recommandé d'utiliser des adresses dans la plage qui va de 239.0.0.0 à 239.255.255.255.
Si vous souhaitez limiter la portée de votre message au sous-réseau auquel appartient la machine émettrice, un TTL de 1 peut être suffisant.

Attachement d'un objet pour appel en rmi :

package centralizedServer ;

...

public class C_SharedUniverseServer extends UnicastRemoteObject implements IC_SharedUniverseServer, Remote, Serializable {

   ...

    protected C_SharedUniverseServer (String sharedWorldName, String serverHostName, int serverRMIPort,
          String nomGroupeUpdate, int portDiffusionUpdate) throws RemoteException {
      try {
         // dans un shell, il faudrait avoir fait : remiregistry `serverRMIPort`,
         // mais on peut avantageusement remplacer cette commande par un "createRegistry"
         LocateRegistry.createRegistry (serverRMIPort) ;
         Naming.rebind ("//" + serverHostName + ":" + serverRMIPort + "/" + sharedWorldName, this) ;
         System.out.println ("pret pour le service") ;
      } catch (Exception e) {
         System.out.println ("pb RMICentralManager") ;
      }
   }

   ...

}

Récupération de l'accès à un objet distant pour appel en rmi :

   ...
        IC_SharedUniverseServer sharedWorld ;
        try {
         sharedWorld = (IC_SharedUniverseServer)Naming.lookup ("//" + serverHostName + ":" + serverRMIPort + "/" + sharedWorldName) ;
         sharedWorld.answer ("hello from " + getName ()) ;
        } catch (Exception e) {
            System.out.println ("probleme liaison CentralManager") ;
            e.printStackTrace () ;
            System.exit (1) ;
        }
   ...



Annexe 2 : gestion des événements grâce à un behavior


Capture et exploitation des événements souris dans une fenêtre de rendu pour déplacer un objet sélectionné :

package presentationJava3D;

import javax.media.j3d.* ;

import java.awt.* ;
import java.awt.event.* ;
import java.util.Enumeration ;

//-------------------------------------------------------------------------------------------

class MouseInteractor extends Behavior {

    private WakeupOr wEvents ;
    private int buttonsInUse ;
    private boolean button1Pressed, button2Pressed, button3Pressed ;
    protected BranchGroup branche ; // le graphe de scène dans lequel chercher les objets 3D, il faudra l'avoir intialisé correctement...
    protected P_SharedObject objectInInteraction ; // pour avoir une référence vers l'objet qu'on va sélectionner
    protected Canvas3D currentViewport ; // le canvas3D dans lequel on va faire le picking
    protected Transform3D oldT3D = new Transform3D () ; // pour stocker si besoin la position d'un objet sélectionné

    int x1, y1, x2, y2 ;

    //----------------------------------------------------------------------------------------

    public MouseInteractor (BranchGroup b) {
        branche = b ;
    }

    //----------------------------------------------------------------------------------------

    public void initialize () {
        WakeupOnAWTEvent wAWTEvent1 = new WakeupOnAWTEvent (AWTEvent.MOUSE_EVENT_MASK) ;
        WakeupOnAWTEvent wAWTEvent2 = new WakeupOnAWTEvent (AWTEvent.MOUSE_MOTION_EVENT_MASK) ;
        WakeupOnAWTEvent wAWTEvent3 = new WakeupOnAWTEvent (AWTEvent.MOUSE_WHEEL_EVENT_MASK) ;
        WakeupCriterion [] conditions = { wAWTEvent1, wAWTEvent2, wAWTEvent3 } ;
        wEvents = new WakeupOr (conditions) ;
        wakeupOn (wEvents) ;
        buttonsInUse = 0 ;
        button1Pressed = false ;
        button2Pressed = false ;
        button3Pressed = false ;
    }

    //----------------------------------------------------------------------------------------

    @SuppressWarnings("unchecked")
    public void processStimulus (Enumeration criteria) {
        while (criteria.hasMoreElements ()) {
            WakeupOnAWTEvent w = (WakeupOnAWTEvent)criteria.nextElement () ;
            AWTEvent events [] = w.getAWTEvent () ;
            for (int i = 0 ; i < events.length ; i ++) {
                if (events [i].getID () == MouseEvent.MOUSE_PRESSED) {
                    processMousePressed ((MouseEvent)events [i]) ;
                } else if (events [i].getID () == MouseEvent.MOUSE_RELEASED) {
                    processMouseReleased ((MouseEvent)events [i]) ;
                } else if (events [i].getID () == MouseEvent.MOUSE_DRAGGED) {
                    processMouseDragged ((MouseEvent)events [i]) ;
                } else if (events [i].getID () == MouseEvent.MOUSE_WHEEL) {
                    processMouseWheeled ((MouseWheelEvent)events [i]) ;
                }
            }
        }
        wakeupOn (wEvents) ;
    }

    private void processMouseWheeled (MouseWheelEvent event) {
        findInteractiveObject (event) ;
        if (objectInInteraction != null) {
            double dx = 0, dy = 0, dz = 0 ;
            double dh = 0, dp = 0, dr = 0 ;
            dz = event.getWheelRotation () / 10.0 ;
            if (event.isShiftDown ()) { // en coordonnées objet
                translateInObjectCoordinates (dx, dy, dz) ;
                rotateInObjectCoordinates (dh, dp, dr) ;
            } else if (event.isControlDown ()) { // en coordonnées globales
                translateInUniverseCoordinates (dx, dy, dz) ;
                rotateInUniverseCoordinates (dh, dp, dr) ;
            } else { // en coordonnées point de vue
                translateInViewportCoordinates (dx, dy, dz) ;
                rotateInViewportCoordinates (dh, dp, dr) ;
            }
        }
        objectInInteraction = null ;
    }

    //----------------------------------------------------------------------------------------

    protected void processMousePressed (MouseEvent event) {
        if (event.getButton () == MouseEvent.BUTTON1) {
            button1Pressed = true ;
        }
        if (event.getButton () == MouseEvent.BUTTON2) {
            button2Pressed = true ;
        }
        if (event.getButton () == MouseEvent.BUTTON3) {
            button3Pressed = true ;
        }
        if (buttonsInUse == 0) {
            x1 = event.getX () ;
            y1 = event.getY () ;
            findInteractiveObject (event) ;
        }
        buttonsInUse ++ ;
    }

    //----------------------------------------------------------------------------------------

    protected void processMouseReleased (MouseEvent event) {
        buttonsInUse -- ;
        if (buttonsInUse == 0) {
            objectInInteraction = null ;
        }
        if (event.getButton () == MouseEvent.BUTTON1) {
            button1Pressed = false ;
        }
        if (event.getButton () == MouseEvent.BUTTON2) {
            button2Pressed = false ;
        }
        if (event.getButton () == MouseEvent.BUTTON3) {
            button3Pressed = false ;
        }
    }

    //----------------------------------------------------------------------------------------

    protected void processMouseDragged (MouseEvent event) {
        if (objectInInteraction != null) {
            double dx = 0, dy = 0, dz = 0 ;
            double dh = 0, dp = 0, dr = 0 ;
            x2 = event.getX () ;
            y2 = event.getY () ;
            if (button3Pressed) { // rotation
                dp = Math.PI * (x2 - x1) / 60.0 ;
                dh = Math.PI * (y1 - y2) / 60.0 ;
                //dr = (dh - dp) / 2.0 ;
            }
            if (button2Pressed) { // translation sur l'axe Z
                dz = (x1 - x2 + y2 - y1) / 40.0 ;
            }
            if (button1Pressed) { // translation dans le plan XY
                dx = (x2 - x1) / 40.0 ;
                dy = (y1 - y2) / 40.0 ;
            }
            // les "modifieurs" servent à choisir le repère dans lequel
            // opérer les transformations
            if (event.isShiftDown ()) { // en coordonnées objet
                translateInObjectCoordinates (dx, dy, dz) ;
                rotateInObjectCoordinates (dh, dp, dr) ;
            } else if (event.isControlDown ()) { // en coordonnées point de vue
                translateInViewportCoordinates (dx, dy, dz) ;
                rotateInViewportCoordinates (dh, dp, dr) ;
            } else { // en coordonnées globales
                translateInUniverseCoordinates (dx, dy, dz) ;
                rotateInUniverseCoordinates (dh, dp, dr) ;
            }
            x1 = x2 ;
            y1 = y2 ;
        }
    }

    //----------------------------------------------------------------------------------------

    protected void findInteractiveObject (MouseEvent event) {
        currentViewport = (Canvas3D)event.getSource () ;
        PickCanvas pickShape = new PickCanvas (currentViewport, branche) ;
        pickShape.setShapeLocation (event) ;
        pickShape.setMode (PickTool.GEOMETRY_INTERSECT_INFO) ;
        try {
            PickResult [] sgPath = pickShape.pickAllSorted () ;
            if (sgPath != null) {
                try {
                    objectInInteraction = (P_SharedObject)sgPath [0].getNode (PickResult.TRANSFORM_GROUP) ;
                    objectInInteraction.getTransform (oldT3D) ;
                } catch (Exception e) {
                    System.out.println (e) ;
                }
            }
        } catch (Exception e) {
            e.printStackTrace () ;
            objectInInteraction = null ;
        }
    }

    //----------------------------------------------------------------------------------------
    // rotation dans le repère de l'univers

    protected void rotateInUniverseCoordinates (double dh, double dp, double dr) {
        ...
    }

    //----------------------------------------------------------------------------------------
    // rotation dans le repère local de l'objet : autour des axes de l'objet

    protected void rotateInObjectCoordinates (double dh, double dp, double dr) {
        ...
    }

    //----------------------------------------------------------------------------------------
    // rotation autour des axes du point de vue centrés sur l'objet

    protected void rotateInViewportCoordinates (double dh, double dp, double dr) {
        ...
    }

    //----------------------------------------------------------------------------------------
    // translation dans le repère de l'univers

    protected void translateInUniverseCoordinates (double dx, double dy, double dz) {
        ...
    }

    //----------------------------------------------------------------------------------------
    // translation dans le repère de l'objet

    protected void translateInObjectCoordinates (double dx, double dy, double dz) {
        ...
    }

    //----------------------------------------------------------------------------------------
    // translation dans le repère du point de vue

    protected void translateInViewportCoordinates (double dx, double dy, double dz) {
        // quelque chose du genre :
        // Matrix4d resultat = matriceCamera ;
        // resultat.mul (matriceDeplacement) ;
        // resultat.mul (matriceCameraInv) ;
        // resultat.mul (matriceObjet) ;

        ...
    }

}