Loupe

Introduction à HoloLens avec C++ et Direct3D 11

Introduction

Vous n’êtes pas sans savoir que nous avons reçu deux kits HoloLens il y a un mois chez Infinite Square. Jonathan et Maxime ont déjà écrit deux articles sur le développement d’applications UWP classiques mais également avec Unity 3D pour HoloLens. Ici, nous nous concentrerons sur le développement C++ avec Direct3D 11 et ainsi comprendre très brièvement les différences fondamentales entre le développement 3D classique et le développement 3D spécifique HoloLens :)

 

Les outils à installer

Pour développer des applications UWP 3D & C++ (C++/CX pour être exact) spécifiques HoloLens, il nous faut d’abord installer les bons outils avec Visual Studio 2015 Update 2 disponibles ici: https://developer.microsoft.com/en-us/windows/holographic/install_the_tools

Une fois installés, il ne nous reste plus qu’à installer également l’émulateur, disponible sur la même page.

A partir de ce moment-là, un sample C++ pour HoloLens est disponible lorsque nous voulons créer un nouveau projet:

1_pzgJN2-nBhtAzgO2Q3Ellg

 

Analyse du sample

Ce sample est très simpliste, il pose les bases du développement Direct3D 11 en affichant un cube coloré. Néanmoins, certaines parties sont spécifiques à HoloLens:

Deux Vertex Shaders :

  • un pour HoloLens et un pour l’émulateur HoloLens (le comportement est différent sur un point particulier entre le périphérique et l’émulateur)
  • La gestion des caméras avec l’HoloLens pour le rendu stéréoscopique

Le Vertex Shader

Le Vertex Shader, qui va au moins de paire avec le Pixel Shader, est particulier sur HoloLens: il doit obligatoirement prendre en compte la gestion des instances et par ce fait déterminer sur quel écran l’objet est rendu (pour rappel, l’HoloLens nécessite un rendu stéréoscopique). Il s’agit de la seule vraie difficulté en ce qui concerne le développement spécifique HoloLens.

Concernant le rendu stéréoscopique, il existe en fait deux Back Buffers (rendu sur texture) chacun assigné à un écran en particulier: un pour l’oeil gauche et un pour l’oeil droit. Toute la difficulté réside ici, c’est-à-dire gérer les deux rendus sur texture.

Si l’on se rapproche du code du Vertex Shader on trouve en entrée la structure suivante :

1_VDwGz57wduTxaz3rlfSftw

 

La valeur de la variable “instId” (instance Id) est automatiquement assignée de par sa sémantique “SV_InstanceID” (qui correspond à l’instance courante rendue à l’écran). Cette variable est très importe car nous sommes dans un mode de rendu stéréoscopique. En effet, lors de la transformation d’un sommet sur l’écran (world * view * projection), les matrices de vue et projection sont dépendantes de l’utilisateur qui porte l’HoloLens. C’est-à-dire en fonction de sa position dans l’espace et de la direction où il regarde.

C’est pour cette raison qu’un Vertex Shader, dans ce contexte, n’aura pas une matrice de vue et une matrice de projection mais tout simplement un tableau de vue-projection (préalablement concaténées) qui contient deux éléments :

1_8qA3rZoAx3zSHw64wfwDSw

A partir de là, nous avons tous les éléments en main. Il ne reste plus qu’à récupérer la bonne combinaison vue-projection en fonction de l’instance en cours (idx) :

1_iA4Xt8mgexcUuFua9B1M4w

Toutefois ce n’est pas fini, une différence contraignante entre le périphérique et l’émulateur nous oblige à rajouter une étape entre le Vertex Shader et le Pixel Shader : Le “Geometry Shader”.

L’émulateur HoloLens ne supporte pas (encore) la possibilité d’assigner un indice de rendu sur texture (Render Target Array Index, de son joli nom VPAndRTArrayIndexFromAnyShaderFeedingRasterizer), c’est-à-dire dans notre cas le bon Back Buffer, contrairement au Geometry Shader qui lui est supporté par l’émulateur avec cette fonctionnalité. C’est pourquoi il nous faut deux versions spécifiques du Vertex Shader : une pour l’émulateur et une pour le périphérique.

Celui du périphérique nous permet d’avoir en sortie une structure qui nous permet de définir l’indice du rendu sur texture et ainsi passer au Pixel Shader :

1_-vxXEv46-JdO7lnibXiJ-w

La variable “rtvId” (Render Target View Index) correspond à la variable “idx” précédemment récupérée. Jusqu’ici tout va bien.

Lorsque l’application fonctionne sur l’émulateur cela devient plus contraignant. La structure en sortie du Vertex Shader contient également la variable “rtvId” mais n’est pas mappée sur une sémantique Direct3D 11. C’est donc à nous de gérer le passage entre Vertex Shader -> Geometry Shader :

1_GsgHfQFJH9tK97C9AaXVrQ

En soi, le Geometry Shader est également simpliste. Il ne modifie en rien la géométrie de l’objet rendu et sa structure de retour, elle, peut contenir notre variable “rtvId” mappée sur la sémantique “SV_RenderTargetArrayIndex” (en jaune les lignes importantes) :

1_JjwTjX7jsSgdyx7KzWGX5Q

La différence entre l’émulateur et le périphérique nous oblige à rajouter cette étape mais finalement il ne s’agit que de vérifier dans notre code C++ si la fonctionnalité est supportée :

  • Si elle l’est, ignorer le Geometry Shader et utiliser le Vertex Shader spécifique
  • Sinon, ajouter (toujours le même) le Geometry Shader dans le pipeline et utiliser le Vertex Shader spécifique

La gestion des caméras avec HoloLens

En 3D, pour avoir un point de vue sur la scène, on utilise généralement des entités haut niveau comme des caméras avec gestion des entrées utilisateurs (souris, clavier, etc.). Ici c’est encore plus simple puisque la caméra est l’HoloLens et les entrées utilisateur… l’utilisateur directement !

Afin de récupérer les valeurs des matrices de vue-projection (cf. Vertex Shader), les APIs HoloLens sont là pour nous aider. Il s’agit simplement d’accéder aux ressources physiques liées à l’HoloLens directement. Heureusement, les APIs nous facilitent énormément le travail et nous permettent de nous affranchir de la stabilisation d’image (par exemple).

En effet, pour un rendu donné, il s’agit de récupérer les différentes poses des caméras (les deux yeux car rendu stéréoscopique) à partir d’une prédiction faite automatiquement par les APIs (prédiction qui sert à la stabilisation des poses des caméras). Une fois la liste des poses des caméras récupérée (liste à deux éléments), nous pouvons récupérer les valeurs des matrices de vue et de projection puis les concaténer.

 

/**
* Initialization part
*/

// Frame created as a reference frame.
// Contains frame informations at the current location
// when the application starts
SpatialStationaryFrameOfReference referenceFrame;

// Get space coordinates for the given window
auto space = HolographicSpace::CreateForCoreWindow(window);

/**
* Rendering part
*/

// Create a "HolographicFrame" frame (which contains the current
// frame's informations of the HoloLens
auto frame = space->CreateNextFrame();

// Predict the camera poses
auto prediction = frame->CurrentPrediction;

// Use each camera
for (HolographicCameraPose^ pose : prediction->CameraPoses)
{
    HolographicStereoTransform projectionTransform
        = pose->ProjectionTransform;

    IBox<HolographicStereoTransform>^ viewTransformContainer
        = pose->TryGetViewTransform(referenceFrame);

    // Now we can access the left and right buffers
    float4x4 leftView = viewTransformContainer->Value.left;
    float4x4 rightView = viewTransformContainer->Value.right;

    float4x4 leftProjection = projectionTransform.left;
    float4x4 rightProjection = projectionTransform.right;

    // Multiply view and projection matrices, and send it
    // to the vertex shader :)
    viewProjection[0] = (leftView * leftProjection).GetTransposed();
    
    viewProjection[1]
        = (rightView * rightProjection).GetTransposed();

    RenderScene();
}

Conclusion

Pour résumer, il n’y a en soi que deux différences fondamentales : Le Vertex Shader spécifique et la gestion des caméras. La gestion des caméras nous arrange, en tant que développeur, car elle nous affranchit de la gestion des entrées pour avancer, reculer, changer de direction etc. : c’est l’API HoloLens qui le gère directement pour nous.

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus