Plus tôt dans la journée, j’ai posté à propos d’une technique de PlaneProjection pour WPF à base de Shader Effect. Cette technique avait pour but principal de porter la Silverlight Flow Layouts Library pour WPF. C’est désormais chose faite : le projet CodePlex est mis à jour (binaires + sources), et le projet fait maintenant du Multi-Targeting Silverlight 4.0 RC / WPF 4.0 RC.

 

N’hésitez pas à apporter tout feedback aussi bien sur la librairie en elle-même, que sur la PlaneProjection pour WPF ou les features que vous aimeriez voir.

 

Autre chose, si vous développez vos propres algorithmes de layout, n’hésitez pas à me contacter via ce blog (ou via le site codeplex), pour venir agrémenter la future bibliothèque d’algorithmes qui va bientôt venir…


Depuis la version 3, Silverlight propose une fonctionnalité permettant de donner un effet de perspective à nos UIElements de manière très simple et très efficace : les Plane Projections. On profite ainsi d’un affichage en perspective 3D très simple à mettre en place et très performant.

 

Le problème

 

WPF ne fournit pas de Plane Projections, et nous oblige à passer par de la « vraie » 3D dès lors que l’on veut afficher quelque chose en perspective. Cela pose 2 problèmes importants :

  • la complexité requise lorsque l’on veut simplement afficher une surface plane en perspective
  • les performances qui chutent dramatiquement quand on multiplie le nombre de Viewport2DVisual3D

 

En effet, avec WPF, appliquer un effet de perspective à un élément requiert les étapes suivantes :

  • Créer un Viewport3D
  • Lui associer une PerspectiveCamera
  • Lui ajouter un Viewport2DVisual3D
  • Lui affecter un modèle (avec les coordonnées de textures et les normales)
  • Lui affecter la transformation3D correcte

 

Cela fait beaucoup d’étapes et certaines ne sont pas forcément simple à comprendre lorsque l’on travaille essentiellement dans un monde 2D.

 

Au niveau des performances, outre les nombreux objets nécessaires et couteux mis en œuvre, il faut savoir que pour chaque transition entre une surface 2D et un Viewport3D, WPF produit une Render Target Direct3D intermédiaire avec des conséquences très importantes pour les performances. en effet chaque RenderTarget consomme une quantité importante de mémoire vidéo, d’autant que sous Vista / 7, l’anti-aliasing 4x est appliqué à chaque RenderTarget participant à WPF 3D, et le switch de RenderTarget est quelque chose de très couteux. Il suffit de créer une scène avec une 15aine de Viewport2DVisual3D pour se rendre compte de l’impact.

 

Solution envisagée

 

Pour pallier à ces 2 problèmes (et aussi pour pouvoir porter la Flow Layout Library pour WPF), j’ai décidé de tenter une approche différente, et très proche de ce qui semble avoir été fait pour Silverlight. Il s’agit de générer l’effet de perspective à partir d’un ShaderEffect combiné à un TranslateTransform pour reproduire l’effet d’une PlaneProjection. Comment cela est-il possible ? Simplement avec un petit peu de mathématiques (au passage, pour les poilus, le livre Mathematics for 3D Game Programming & Computer Graphics, c’est top).

 

On commence par produire côté C# une matrice de transformation 3D (composée de la matrice de transformation par rapport à la position d’origine, de la matrice de transformation par rapport au point de vue, et de la matrice de projection qui donne l’effet de perspective)  et d’en déduire l’équation du plan généré (je vous passe la formule mathématique, ça parle de multiplication matricielles, de produits scalaires et autres joyeusetés). Cela n’a besoin d’être fait que lorsque la transformation demandée change.

 

 

Une fois qu’on obtient cette équation (composée de la normale au plan et de la distance minimale entre le point {0,0,0} et le plan), on la passe au Pixel Shader avec la matrice inverse de celle utilisée pour transformer le plan. Dans le PixelShader, pour chaque pixel à rendre, on va calculer l’intersection entre le rayon partant de ce point et se projetant dans l’axe Z, et le plan (définit par l’équation produite à l’étape précédante). On multiplie alors cette coordonnée par la matrice de transformation inverse pour savoir quelle est la position au sein du plan (et donc obtenir la coordonnée du Pixel de la texture d’origine à renvoyer).

 

On ajoute à ça quelques petites optimisations du genre clipping de la zone de rendu et utilisation d’un TranslateTransform pour éviter d’avoir à appliquer cet effet sur une surface trop grande (dans le cas où la PlaneProjection produit une translation importante) et la création d’un GeneralTransform utilisé pour le HitTesting,  et on se retrouve avec une solution beaucoup plus simple à utiliser (au final, on obtient la même chose que la PlaneProjection de Silverlight) et beaucoup plus performante (pas de RenderTarget intermédiaire, consommation de mémoire vidéo réduite…) que le Viewport2DVisual3D. Bien sûr l’inconvénient est que l’on ne peut pas mapper nos surfaces 2D sur des objets 3D autres que des plans avec cette technique, mais dans de nombreux cas, c’est exactement ce à quoi l’on veut se limiter !
(note, je ne ferai pas de listing de code dans ce post, car c’est long et pas vraiment très expressif, mais vous pourrez le télécharger à la fin de l’article).

 

Le résultat

 

Au final on va pouvoir créer ce genre d’effet :

Avec très peu d’efforts :

 

<Border Background="Transparent"  x:Name="border">

   <Media:PlaneProjection.PlaneProjection>

      <Media:PlaneProjection/>

   </Media:PlaneProjection.PlaneProjection>


</Border>
   <StackPanel Grid.Column="1">

      <TextBlock Text="GlobalOffsetX" />

      <Slider x:Name="offsetX" Minimum="-200" Maximum="200"

              Value="{Binding ElementName=border, Mode=TwoWay, Path=(Media:PlaneProjection.PlaneProjection).GlobalOffsetX}" />

      <TextBlock Text="GlobalOffsetY" />

      <Slider x:Name="offsetY" Minimum="-200" Maximum="200"

              Value="{Binding ElementName=border, Mode=TwoWay, Path=(Media:PlaneProjection.PlaneProjection).GlobalOffsetY}" />

      <TextBlock Text="GlobalOffsetZ" />

      <Slider x:Name="offsetZ" Minimum="-200" Maximum="200" 

              Value="{Binding ElementName=border, Mode=TwoWay, Path=(Media:PlaneProjection.PlaneProjection).GlobalOffsetZ}" />

      <TextBlock Text="RotationX" />

      <Slider x:Name="rotationX" Minimum="-180" Maximum="180"

              Value="{Binding ElementName=border, Mode=TwoWay, Path=(Media:PlaneProjection.PlaneProjection).RotationX}" />

      <TextBlock Text="RotationY" />

      <Slider x:Name="rotationY" Minimum="-180" Maximum="180"

              Value="{Binding ElementName=border, Mode=TwoWay, Path=(Media:PlaneProjection.PlaneProjection).RotationY}" />

      <TextBlock Text="RotationZ" />

      <Slider x:Name="rotationZ" Minimum="-180" Maximum="180"

              Value="{Binding ElementName=border, Mode=TwoWay, Path=(Media:PlaneProjection.PlaneProjection).RotationZ}" />

</StackPanel>

 

Ça se présente juste sous la forme d’une AttachedProperty, et au niveau utilisation, c’est la seule différence avec la PlaneProjection de Silverlight.

Bien sûr, on va aussi pouvoir faire des choses plus complexes… comme le portage de Silverlight Flow Layout Library par exemple qui va venir avec la sortie de Silverlight 4 RC :

Les sources :

 

Vous pourrez retrouver les sources dans le projet CodePlex http://slflow.codeplex.com lors de la prochaine release. En attendant je les mets a dispo ici meme.