3d-interface-rapport/rapport/streaming.tex

267 lines
11 KiB
TeX

\part{Streaming de modèle 3D\label{streaming}}
Le but ultime de ce projet est de biaiser l'utilisateur avec les
recommandations de sorte à être capable de prévoir ses déplacements futurs, et
ainsi de précharger les parties du modèle qui vont être vues. Cette section
présente le travail qui a été réalisé dans le domaine du chargement de modèle.
\paragraph{}
Évidemment, cette partie est celle qui commence après la fin de la première
partie : il faut non seulement connaître l'influence des recommandations sur
l'utilisateur, mais aussi être capable de prévoir le comportement de
l'utilisateur, et enfin de s'en servir pour précharger les bonnes parties du
modèle. Tout ceci n'étant pas encore possible, le travail qui a été fait est
une version simplifiée : il n'y aura aucune prévision du comportement ici.
\section*{Introduction}
Notre problématique ici est de transférer des modèles 3D sur le réseau. Les
modèles sont stockés sur le serveur au format \texttt{.obj} et sont constitués
:
\begin{itemize}
\item des matériaux (\texttt{usemtl}) : cela définit le matériau utilisé
pour les faces qui vont suivre. Un matériau est notamment défini par
ses constantes de réflexions optiques, ses textures, etc...
\item de sommets (\emph{vertices}) : des points 3D
\item de coordonnées de textures : des points en 2D qui référencent un
point d'une image
\item de normales : des vecteurs en 3D
\item de faces : une liste de 3 ou 4 sommets (représentés par leurs
indices), avec éventuellement leurs coordonnées de textures et/ou
normales
\end{itemize}
\paragraph{}
La seule contrainte d'ordre des éléments est qu'une face qui contient des
sommets, coordonnées de textures ou normales n'arrive pas avant ses sommets,
coordonnées de textures ou normales dans le fichier : si l'on parcourt les
lignes du fichier, on doit déjà avoir toutes les informations sur la face.
\paragraph{}
Généralement, le fichier \texttt{.obj} vient souvent avec un fichier
\texttt{.mtl} qui contient la définition des matériaux (leurs noms, leurs
textures, leurs constantes...).
\section{Architecture}
Puisque cette partie traite à la fois du client et du serveur, les fichiers
peuvent sembler éparpillés. Les fichiers concernés sont les fichiers du
répertoire \texttt{geo} pour le serveur et le fichier
\texttt{ProgressiveLoader.js} qui est le client.
\subsection{Serveur}
Nous avons rappelé que les modèles au format \texttt{.obj} contenaient
plusieurs \emph{sous-modèles} avec des matériaux différents, le serveur connait
les classes suivantes :
\begin{itemize}
\item la classe \texttt{Mesh}, qui représente un \emph{sous-modèle}, avec
ses faces, ses coordonnées de textures, ses normales, son matériau
\item la classe \texttt{MeshContainer}, qui représente un modèle au format
\texttt{.obj}, en tant que liste de \texttt{Mesh}
\item la classe \texttt{MeshStreamer}, qui permettra d'envoyer un modèle au
client
\end{itemize}
\paragraph{}
Pour avoir une bonne performance, il faut éviter de re-parser les fichiers
\texttt{.obj} et de recréer les \texttt{MeshContainer} à chaque requête.
\paragraph{}
Dans le fichier \texttt{MeshContainer.js}, une liste de modèle à charger (ainsi
que le chemin permettant d'accéder aux fichiers) permet au serveur de créer les
\texttt{MeshContainer} au démarrage, et ainsi, le \texttt{MeshStreamer} n'a
qu'à prendre une référence vers ce \texttt{MeshContainer} (qui existe déjà, ce
qui réduit la latence).
\subsection{Client}
Le client de la partie \emph{streaming} de ce projet est contenue dans la
classe \texttt{ProgressiveLoader}. Celle-ci crée des modèles 3D vides, puis
établit la connexion avec le serveur et remplit les modèles au fur et à mesure
que les informations arrivent du serveur. Dans les sections suivantes, nous
allons montrer les différentes stratégies de \emph{streaming} que nous avons
mises en place.
\section{Streaming linéaire}
La première étape de cette fonctionnalité a été de faire un système
client-serveur permettant le streaming de modèle 3D. En effet, les
\emph{loaders} de modèles présents dans la libraire \threejs{} ne permettent pas
le chargement progressif : ils se content d'envoyer une requête vers le fichier
contenant le modèle et à créer un modèle une fois que le fichier est chargé
complètement.
\paragraph{}
Pour commencer, nous avons donc utilisé \socketio{}, une librairie permettant de
gérer les sockets facilement avec JavaScript et Nodejs, pour faire une première
version simplifiée du streaming : on travaillait sur un modèle ne contenant que
des sommets et des faces (donc pas de textures ni de normales) et le protocole
fonctionnait ainsi :
\begin{figure}[H]
\centering
\begin{tikzpicture}[]
\draw (0,0) node[above]{Client};
\draw (0,0) -- (0,-6);
\draw (5,0) node[above]{Serveur};
\draw (5,0) -- (5,-6);
\draw (2.5,-1.3) node[rotate=-12] {Path jusqu'au modèle};
\draw[->] (0,-1) -- (5, -2);
\draw (2.5,-2.3) node[rotate=12] {Des éléments};
\draw[<-] (0,-3) -- (5, -2);
\draw (2.5,-3.3) node[rotate=-12] {ACK};
\draw[->] (0,-3) -- (5, -4);
\draw (2.5,-4.3) node[rotate=12] {D'autres éléments};
\draw[<-] (0,-5) -- (5, -4);
\draw (2.5,-6) node{$\vdots$};
\end{tikzpicture}
\caption{Transmission élémentaire\label{transmission1}}
\end{figure}
\paragraph{}
Le serveur envoyait alors les éléments dans l'ordre dans lequel ils étaient
présents dans le fichier du modèle 3D. Le gros inconvénient de cette méthode
est que souvent, les sommets sont présents au début du fichier, et les faces
vers la fin : nous recevons donc des informations de sommets au début qui ne
nous permettent rien d'afficher, puis toutes les faces d'un coup, ce qui fait
que le streaming n'est pas aussi progressif que l'on souhaiterait.
\paragraph{}
Dans le cas d'un modèle sans coordonnées de textures et sans normales, la seule
condition pour pouvoir afficher une face est d'avoir envoyé les sommets qui la
composent. On peut donc améliorer la fluidité en réarrangeant le fichier
\texttt{.obj} : il suffit de faire apparaître les faces dès que les sommets
sont disponibles.
\paragraph{}
Dans le cas où l'on souhaite gérer les textures et les normales, utiliser cette
technique est beaucoup plus compliqué puisqu'il faut aussi décider de l'ordre
relatif entre les sommets, coordonnées de textures et normales : en effet, pour
envoyer une face le plus tôt possible, il faut que toutes ces composantes
soient envoyés, et il est donc nécessaire de mélanger les sommets, coordonnées
de texture et normales.
\section{Streaming linéaire amélioré}
La remarque précédente conduit directement à cette méthode. Le principe reste
le même que celui précédent (figure \ref{transmission1}), mais les éléments
seront envoyés différemment : on va en fait parcourir les faces directement.
\paragraph{}
Le serveur va garder en mémoire ce qui a déjà été envoyé et ce qui ne l'a pas
été, et envoyer les faces dans l'ordre : si certains éléments des faces n'ont
pas encore été envoyés, on les enverra juste avant d'envoyer la face en
question.
\paragraph{}
On peut de cette façon à la fois gérer les coordonnées de texture et les
normales, tout en envoyant les faces le plus tôt possible (on peut aussi noter
que si certains sommets / coordonnées de textures / normales sont inutilisés
dans les faces, ils ne seront pas envoyés et on s'évite donc de transférer des
données inutiles).
\paragraph{}
C'est dans cette version que nous avons commencé à nous intéresser à la façon
de gérer les matériaux. Dans \threejs{}, un objet 3D est lié à un materiau, et
nous sommes donc obligés de créer autant d'objets que de materiaux. Pour cela,
nous avons légèrement modifié notre protocole :
\begin{figure}[H]
\centering
\begin{tikzpicture}[]
\draw (0,0) node[above]{Client};
\draw (0,0) -- (0,-8);
\draw (5,0) node[above]{Serveur};
\draw (5,0) -- (5,-8);
\draw (2.5,-1.3) node[rotate=-12] {Path jusqu'au modèle};
\draw[->] (0,-1) -- (5, -2);
\draw (2.5,-2.3) node[rotate=12] {Liste des matériaux};
\draw[<-] (0,-3) -- (5, -2);
\draw[dashed, ->] (-0.1,-3) -- (-0.1,-4);
\draw (2.5,-3.3) node[rotate=-12] {ACK};
\draw[->] (0,-3) -- (5, -4);
\draw (2.5,-4.3) node[rotate=12] {Des éléments};
\draw[<-] (0,-5) -- (5, -4);
\draw (2.5,-5.3) node[rotate=-12] {ACK};
\draw[->] (0,-5) -- (5, -6);
\draw (2.5,-6.3) node[rotate=12] {D'autres éléments};
\draw[<-] (0,-7) -- (5, -6);
\draw (2.5,-8) node{$\vdots$};
\end{tikzpicture}
\caption{Transmission avec gestion des matériaux}
\end{figure}
\paragraph{}
Les directives de matériau à utiliser (\texttt{usemtl}) seront ignorées, et les
faces seront envoyées avec l'indice de l'objet auquel elles appartiennent, ce
qui permettra au client de savoir dans quel objet (et donc avec quel matériau)
elles doivent être ajoutées.
\section{Streaming intelligent}
C'est la dernière version du streaming qui a été faite sur ce projet. À chaque
transfert, le client envoie sa position au serveur (ainsi que les plans
définissant son \emph{frustum}\footnote{les bords du champ de vision de la
caméra}) et le serveur va parcourir les faces du modèle en cherchant celles qui
apparaissent dans le \emph{frustum}. On évite ainsi d'envoyer les faces du
modèle qui sont derrière la caméra et que l'utilisateur ne voit pas.
\begin{figure}[H]
\centering
\include{../common/build/frustum}
\caption{Le frustum de la camera et différents objets\label{frustum-draw}}
\end{figure}
Dans cette version, on considère qu'une face apparaît dans le \emph{frustum} si
un de ses sommets y appartient. Évidemment, une face très grande pourrait
appraître dans le \emph{frustum} sans qu'aucun de ses sommets n'y soit, mais
nous n'avons pas traité ce cas particulier ici (dans la figure
\ref{frustum-draw}, seuls les triangles verts et bleus seront envoyés, bien que
le triangle rouge devrait être envoyé lui aussi).
\paragraph{}
Bien sûr, s'il n'y a plus de faces dans le \emph{frustum} de la caméra, on va
envoyer les faces en suivant la méthode de la section précédente.
\begin{figure}[H]
\centering
\begin{tikzpicture}[]
\draw (0,0) node[above]{Client};
\draw (0,0) -- (0,-8);
\draw (5,0) node[above]{Serveur};
\draw (5,0) -- (5,-8);
\draw (2.5,-1.3) node[rotate=-12] {Path jusqu'au modèle};
\draw[->] (0,-1) -- (5, -2);
\draw (2.5,-2.3) node[rotate=12] {Liste des matériaux};
\draw[<-] (0,-3) -- (5, -2);
\draw[dashed, ->] (-0.1,-3) -- (-0.1,-4);
\draw (2.5,-3.3) node[rotate=-12] {Caméra / \emph{frustum}};
\draw[->] (0,-3) -- (5, -4);
\draw (2.5,-4.3) node[rotate=12] {Des éléments};
\draw[<-] (0,-5) -- (5, -4);
\draw (2.5,-5.3) node[rotate=-12] {Caméra / \emph{frustum}};
\draw[->] (0,-5) -- (5, -6);
\draw (2.5,-6.3) node[rotate=12] {D'autres éléments};
\draw[<-] (0,-7) -- (5, -6);
\draw (2.5,-8) node{$\vdots$};
\end{tikzpicture}
\caption{Version finale}
\end{figure}