TD1 - Stéréoscopie et profondeur de champ

Une composante importante de la réalité virtuelle est l'immersion pseudo-sensorielle des utilisateurs dans l'environnement 3D avec lequel ils interagissent. On parle d'immersion pour désigner les moyens techniques mis en œuvre afin d'augmenter la présence, c'est-à-dire la sensation d'immersion des utilisateurs. Différents degrés d'immersion sont possibles en cumulant des retours pour chaque sens ; les plus courants sont, dans l'ordre, l'immersion visuelle, puis haptique (toucher/vibrations, retour d'effort) puis sonore (spatialisation, acoustique...).

Nous nous intéresserons uniquement à l'immersion visuelle dans ce TD. En effet, l'un des buts de l'immersion visuelle est de reproduire la perception de la profondeur, à partir de la projection de l'environnement 3D en deux dimensions, afin de faciliter la manipulation des objets 3D et la navigation. La perception humaine s'appuie sur différents indices pour reconstruire la profondeur à partir des informations envoyées par les yeux. Parmi ceux-ci, on trouve la taille relative des objets, les ombres, mais également le déplacement relatif et la position relative des objets, ainsi que la différence de perception entre les deux yeux. Ce sont ces deux derniers indices que nous allons essayer de simuler dans ce TD.

1. Stéréoscopie

1.1. Principe général

Le principe est simple : nos deux yeux sont espacés de quelques centimètres, donc les images qu'ils perçoivent sont deux vues légèrement décalées du même environnement. Selon sa distance, un même objet sera perçu avec un décalage par chaque œil. Essayez vous même en rapprochant / éloignant un objet tout en ouvrant un œil puis l'autre. Le cerveau fusionne ensuite ces informations pour fournir l'image d'un seul objet avec l'information de profondeur.

Il existe un plan sur lequel l'objet sera à la même position pour les deux yeux, c'est le plan de disparité nulle qui se situe à la distance de convergence. Derrière ce plan, on parle de parallaxe positive, l'objet est perçu plus à droite par l'œil droit et plus à gauche par l'œil gauche. Devant ce plan, on parle de parallaxe négative ; l'objet est perçu plus à gauche par l'œil droit, et plus à droite par l'œil gauche. Dans le rendu stéréoscopique que nous allons mettre en œuvre, les objets qui sortent de l'écran correspondront à une parallaxe négative, alors que ceux perçus derrière l'écran correspondront à une parallaxe positive. Consultez les schémas de l'article Calculating Stereo Pairs de Paul Bourke.

Pour obtenir un rendu stéréoscopique, il va donc falloir dessiner la scène pour chaque œil en décalant la caméra de la demi-distance entre les yeux et en modifiant le frustum (pyramide) de vue pour le rendre asymétrique. Je vous renvoie aux diapos de Gordon Wetzstein (Stanford University) pour plus de détails.

1.2. Implémentation

Téléchargez le squelette de code suivant rvra_td1.zip. La page HTML se contente de charger les fichiers Javascript nécessaires puis d'appeler les fonctions init() et animate() définies dans le fichier js/render.js. Ouvrez ce dernier dans un éditeur (en l'absence de préférence en la matière, je vous conseille atom avec le package atom-live-server). Notez que la scène est modélisée avec des grandeurs physiques (en millimètre) et que le plan de convergence passe par le point (0,0,0) et est orthogonal à l'axe z.

Calculez les matrices de vue pour l’œil droit et gauche dans AnaglyphRenderer.update(). Elles correspondent aux matrixWorld des attributs cameraLeft et cameraRight. Vous aurez besoin de la distance entre les deux yeux qui est définie dans le fichier 'js/displayParameters.js' et fixées par défaut à 64 mm.

Calculez ensuite les matrices de projection pour les deux yeux en créer les frustums asymétriques à l'aide de la fonction :

makePerspective( left, right, top, bottom, znear, zfar )

de la classe THREE.Matrix4.

Vous aurez besoin de la distance entre l'observateur et l'écran, arbitrairement fixée à 80 cm, et de la taille physique de votre écran (en mm) que vous devez spécifier dans displayParameters.screenSize.

Complétez enfin la méthode AnaglyphRenderer.render( scene, camera ) pour faire au choix le rendu pour l’œil droit ou gauche, et appelez-la depuis la fonction render de 'js/render.js'.

left eye   right eye

1.3. Anaglyphe

L'implémentation de la stéréoscopie peut s'appuyer sur différentes technologies, le but étant d'afficher pour chaque œil uniquement la vue qui lui est destiné. Pour commencer nous allons utiliser la technique anaglyphe qui a le mérite d'être simple et peu coûteuse. L'idée est de générer les rendus correspondant aux deux yeux sur des canaux de couleurs différents en les superposant. On utilise ensuite des lunettes avec des filtres colorés (généralement rouge et cyan) afin de ne laisser passer pour chaque œil que le rendu qui lui est destiné.

Implémentez la méthode render() de AnaglyphRenderer pour dessiner la scène pour chaque œil dans des canaux de couleur différents. Choisissez ces canaux avec la fonction WebGLRenderingContext.colorMask (cf. doc.). Vous pouvez accéder au contexte WebGL à partir de THREE.WebGLRenderer avec l'instruction :

var gl = renderer.domElement.getContext( 'webgl' );

N'oubliez pas de vider le depth buffer entre le dessin des deux yeux.

anaglyphe

Si votre implémentation est correcte, remarquez que la théière proche du plan de disparité nulle (à gauche) présente un faible décalage des canaux de couleur, alors que ce décalage augmente et est inversé de part et d'autre de ce plan. Ainsi, la théière de droite présente une parallaxe négative (elle semble "sortir" de l'écran) alors que les autres théières présentent une parallaxe positive (elles semblent affichées derrière l'écran).

Comme vous pouvez le constater, l'impression de stéréo est acceptable lorsque vous êtes bien alignés avec la caméra virtuelle, et devient de moins en moins convaincante lorsque vous déplacez votre tête. Pour résoudre ce problème, il est nécessaire de déplacer la caméra virtuelle en fonction de la position de l'observateur vis à vis de l'écran, nous y reviendrons dans le prochain TD.

2. Profondeur de champs

L'objectif dans cette seconde partie est de simuler l'effet de flou naturel dû à la profondeur de champs limitée de nos yeux. Pour faire l'expérience de cet effet dans le monde réel, tenez votre doigt proche de votre nez. Focalisez d'abord vos yeux sur lui ; l'arrière-plan derrière lui devient flou. Maintenant faites la mise au point sur l'arrière-plan, votre doigt semble flou. C'est ce que le rendu avec profondeur de champ essaie de reproduire pour augmenter le réalisme et rendre l'immersion plus importante.

Cet effet est dépendant des paramètres physiques de l'écran (taille réelle, résolution) ainsi que de la position des yeux de l'observateur. Vous renseignerez ces informations dans le fichier 'js/displayParameters.js' (variables screenDiagonal, screenResolutionWidth et aspectRatio) en fonction de l'écran que vous utilisez. À partir de ses valeurs, vous devez calculer la taille physique d'un pixel (pixel pitch) de votre écran, c'est-à-dire le rapport entre la diagonale physique de l'écran (en mm) et la résolution de sa diagonale (en nombre de pixels). Complétez la fonction pixelPitch et utilisez cette valeur pour retrouver la hauteur et la largeur physique (en mm) de votre écran (fonction screenSize).

2.1. Cercle de confusion

Pour simuler la profondeur de champ, il nous faut caractériser pour chaque point de la scène 3D perçu la quantité de flou associée sur la rétine. Pour cela, nous utiliserons le cercle de confusion qui dépend de l'ouverture de la pupille, de la distance d’accommodation de l'œil et de la distance entre ce point et l’œil. Je vous renvoie à nouveau aux diapos de Gordon Wetzstein (Stanford University) pour plus de détails.

En faisant l'hypothèse d'une pupille de 4 mm, d'une distance entre l'observateur et le moniteur physique de 50 cm et d'une distance d’accommodation de 50 cm (l'observateur fait le focus sur l'écran), calculez le diamètre du cercle de confusion en mm et en pixels pour un point 3D distant de l'observateur de : 25 cm, 50 cm, 75 cm, 100 cm, 250 cm. Considérez une écran de résolution 2560 x 1600 pixels pour une diagonale de 13,3 pouces.

2.2. Rendu avec profondeur de champ

Pour implémenter cet effet, nous utiliserons un rendu en deux passes. La première passe fera le rendu normal de la scène mais dans un buffer offscreen (THREE.WebGLRenderTarget). La deuxième passe appliquera un flou adaptatif sur ce buffer à l'aide d'un fragment shader en GLSL. L'intensité de ce flou (donc la taille du noyau de convolution) dépendra du cercle de confusion, donc de la distance de chaque fragment à l'observateur.

Parcourez le fichier 'js/DofRenderer.js' qui est abondamment commenté et assurez-vous de bien comprendre chaque étape. Appelez la méthode DofRenderer.render() depuis la fonction render() de 'js/render.js'.

Ouvrez ensuite le fragment shader 'shaders/shaderDof.frag' que vous allez compléter. Tout d'abord, implémentez la fonction distToFrag qui doit calculer la position du fragment courant dans le repère de la caméra à partir de sa profondeur dans le depth buffer, de la matrice de projection et de son inverse. Les diapos de Gordon Wetzstein détaillent ce calcul.

Avec cette distance, vous pourrez alors estimer le cercle de confusion. Complétez la fonction computeCoC qui retournera le rayon de ce cercle en fonction de la distance d'accommodation (focusDistance) et de la taille de la pupille (pupilDiameter). Toutes ces valeurs sont en mm. Convertissez le résultat en pixels à l'aide du pixelPitch précédemment calculé.

Il ne vous reste plus qu'à implémenter la fonction computeBlur. Elle devra faire la moyenne des pixels autour du fragment courant à l'intérieur du cercle de confusion. Les coordonnées de texture vUv du fragment étant normalisées dans [0,1], pour accéder à un pixel voisin dans la texture, il vous faudra diviser le pas par textureSize.

Expérimentez avec différentes valeurs pour focusDistance et pupilDiameter. Vous pouvez même utiliser dat.GUI pour permettre à l'utilisateur de changer dynamiquement ces valeurs.

dof

Vous pouvez évidemment combiner les deux parties de ce TD pour obtenir un rendu en stéréo anaglyphe avec profondeur de champ.