Perspective : la programmation 3D facile avec WPFPerspective : la programmation 3D facile avec WPF
Il y a quelques années, j'ai été impressionné par la facilité de développement 3D avec VPython . Cette bibliothèque permettait de construire des en langage Python modèles 3D à partir d'objets géométriques de base (cube, cylindre, sphère, etc.) ou plus évolués (flèche), et de les animer. La fenêtre de visualisation par défaut permettait de déplacer la caméra autour de la scène, et de zoomer. Il était donc facile d'obtenir en quelques lignes de code une application très spectaculaire. VPython était très utilisé pour illustrer des modèles physiques. Exemple . Documentation en français .
WPF exploite la technologie Direct3D pour fournir des services de réalisation d'interface utilisateur en 2D mais aussi de scènes 3D. On trouve de nombreuses références à ce sujet, comme ce tutoriel , qui décrit les différentes étapes de la création d'un cube. Même si WPF s'avère beaucoup plus facile à programmer que Direct3D, on est encore loin de la productivité de VPython. Il manque à WPF des classes de haut-niveau pour la programmation 3D (cube, cylindre, sphère, etc.). Plusieurs auteurs font ce constat et proposent des solutions (dont aucune ne va cependant aussi loin que VPython) :
Depuis 2006, j'ai développé la bibliothèque Perspective qui offre à WPF des fonctionnalités 3D et une productivité proches de celles de VPython, tout en intégrant des innovations techniques telles que les contrôles 3D interactifs.
La suite de l'article s'applique à la version 2.0 de Perspective, qui requiert .NET 4.0 et Visual Studio 2010.
Principes de base
Obtenir et installer Perspective
Pour exploiter les classes de Perspective dans un fichier Xaml, il est nécessaire de définir un espace de nom XML référençant les assemblys de Perspective :
<Page x:Class="PerspectiveDemo.UI.Pages.pWpf3D.Box3D"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:p="http://www.codeplex.com/perspective"
...Basiquement, une scène 3D est affichée dans un élément Viewport3D . Il faut ensuite configurer la caméra et l'éclairage . Afin de pouvoir déplacer le point de vue et tourner autour du modèle, des rotations de la caméra sur les axes X, Y et Z (RotateTransform3D) peuvent être configurées. Pour simuler un effet de zoom, une mise à l'échelle (ScaleTransform3D) peut être appliquée à la caméra. Ces transformations peuvent être liées à des sliders.
A l'usage, cela s'avère fastidieux et répétitif. Perspective est compatible avec cette approche, mais fournit depuis sa version 0.9 l'élément Workshop3D qui regroupe un Viewport3D, une caméra, l'éclairage et un système de zoom et de déplacement du point de vue.
<Page ...>
<p:Workshop3D>
...
</p:Workshop3D>
</Page>Nous pouvons alors instancier notre premier objet 3D visuel : le repère (classe XyzAxis3D) qui nous aidera visuellement à positionner les éléments 3D :
<p:Workshop3D>
<p:XyzAxis3D />
</p:Workshop3D>Si nous exécutons maintenant l'application, nous visualisons le repère.

Démo en ligne
Le Workshop3D affiche 3 joysticks semi-transparents qui permettent de faire varier à la souris le zoom de la caméra, sa position et sa direction. Quand la touche Maj est enfoncée, le joystick de position déplace la caméra en orbite autour du point d’origine sur un plan vertical (boutons avant / arrière) ou horizontal (boutons gauche / droit).
La caméra peut être controllée à la souris (boutons des joysticks) ou au clavier (quand le Workshop3D a le focus) :
- Les touches Plus et Moins agissent sur le facteur de zoom.
- Les touches fléchées du pavé numérique agissent sur la position de la caméra (sur un plan XZ).
- Quand la touche Ctrl est enfoncée, elles changent l'orientation de la caméra sur un plan vertical (flèche haut, flèche bas) ou horizontal (flèche gauche, flèche droite).
- Quand la touche Maj est enfoncée, elles font tourner la caméra autour du point d'origine sur un plan vertical (flèche haut, flèche bas ou touches correspondantes du joystick de position) ou horizontal (flèche gauche, flèche droite ou touches correspondantes du joystick de position).
- Les touches 5 ou Ctrl-Plus du pavé numérique élèvent la position de la caméra. Les touches Ctrl-5 ou Ctrl-Moins la rabaissent.

XyzAxis3D dispose des propriétés Radius, rayon du corps de chaque axe, Length, qui permet d'indiquer la taille du repère, et Signed qui affiche les axes pour les valeurs négatives.
<p:XyzAxis3D Length="5" Signed="True"/>

Nous pouvons maintenant créer nos modèles 3D. Commençons par un cube avec la classe Box3D :
<p:Workshop3D>
<p:XyzAxis3D Length="3.0"/>
<p:Box3D />
</p:Workshop3D>
Démo en ligne
Tous les objets Wpf3D disposent d'une texture par défaut, modifiable dans la propriété Material.
Notre cube est positionné à l'origine du repère, et la taille des arêtes est de 1 unité. A la lecture de la documentation, il peut paraître surprenant qu'aucune propriété ne nous permette de modifier la position et la taille du cube. Mais c'est tout à fait normal. WPF utilise le processeur graphique qui supporte des instructions pour transformer la position ou l'aspect des modèles. Modifier la position et la taille du cube dans notre code reviendrait à utiliser le processeur central au lieu du processeur graphique. Dans la logique de WPF, nous allons donc utiliser des transformations 3D assignées à la propriété Transform (toujours avec la possibilité de définir la transformation de façon centralisée dans la zone des ressources) :
...
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Transform3DGroup x:Key="ModelTransform">
<TranslateTransform3D OffsetX="1.0" OffsetY="-1.0" OffsetZ="0.5" />
<ScaleTransform3D ScaleX="1.0" ScaleY="1.0" ScaleZ="3.0" />
</Transform3DGroup>
...
</ResourceDictionary>
...
<Page.Resources>
<ResourceDictionary Source="Wpf3DResources.xaml" />
</Page.Resources>
<p:Workshop3D>
<p:XyzAxis3D Length="5.0"/>
<p:Box3D Transform="{StaticResource ModelTransform}"/>
</p:Workshop3D>
...
Démo en ligne
Ce principe s'applique à tous les modèles de base livrés dans Perspective. Leur dimension de base est l'unité, et ils sont basés sur l'origine du repère (le point de coordonnées 0.0, 0.0, 0.0).
Interactivité
Les objets 3D de Perspective héritant de UIElement3D, ils prennent en charge la gestion des interactions clavier et souris, et exposent les événements correspondants. L'exemple suivant montre comment déclencher une rotation du modèle 3D au survol de la souris, au moyen des événements MouseEnter et MouseLeave :
<p:Workshop3D>
<p:Box3D
MouseEnter="Box3D_MouseEnter"
MouseLeave="Box3D_MouseLeave">
<p:Box3D.Transform >
<RotateTransform3D CenterX="0.5" CenterZ="0.5">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
x:Name="boxRotation"
Axis="0.0, 1.0, 0.0"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</p:Box3D.Transform>
</p:Box3D>
</p:Workshop3D>
private void Box3D_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
// Initialisation de l'objet animation : sur 360 degrés à partir de l'angle courant
DoubleAnimation da = new DoubleAnimation(boxRotation.Angle + 360.0, new Duration(TimeSpan.FromSeconds(5.0)));
da.RepeatBehavior = RepeatBehavior.Forever;
// Déclenchement de l'animation sur la propriété Angle de l'objet boxRotation
boxRotation.ApplyAnimationClock(AxisAngleRotation3D.AngleProperty, da.CreateClock());
}
private void Box3D_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
// mémorisation de l'angle courant
double currentAngle = boxRotation.Angle;
// Arrêt de l'animation
boxRotation.ApplyAnimationClock(AxisAngleRotation3D.AngleProperty, null);
// Réaffectation de l'angle (qui sinon reprend sa valeur initiale)
boxRotation.Angle = currentAngle;
}Démo en ligne
Modèles plats
La classe Square3D définit... un carré 2D !
<p:Square3D />

Démo en ligne
La propriété BackMaterial jour le même rôle que Material pour la face arrière.
Polygon3D définit un polygone plat. La propriété SideCount détermine un polygone régulier d'un nombre de cotés donné.
<p:Polygon3D SideCount="6" />

Le polygone s'inscrit dans un cercle de rayon 1 dans le plan X,Y. Les sommets du polygone sont calculés en divisant la circonférence par la valeur de SideCount. Par défaut, le 1er point est en 1,0,0. La propriété InitialAngle permet de déplacer ce point selon l'angle du segment qu'il forme avec l'origine par rapport à l'axe des X. Exemple pour un angle de 30 degrés :
<p:Polygon3D SideCount="6" InitialAngle="30.0"/>

La propriété RoundingRate permet d'arrondir les sommets du polygone. La valeur doit être comprise entre 0.0 et 0.5. Elle représente la proportion d'un coté soumise à l'arrondi de chaque coté du sommet. Une valeur de 0.5 génère donc un cercle parfait. Exemple avec une valeur de 0.15 :
<p:Polygon3D SideCount="6" InitialAngle="30.0" RoundingRate="0.15"/>

Démo en ligne
Modèles à base de polygones
La classe Bar3D définit une barre de section polygonale, la section ayant les mêmes caractéristiques qu'un Polygon3D : nombre de cotés, angle initial et arrondi. Pour mieux mettre en valeur l'arrondi, nous avons utilisé un matériau brillant (à l'aide d'un SpecularMaterial) et déplacé le modèle par rapport à l'éclairage.
<ResourceDictionary ...>
<SpecularMaterial x:Key="Specular" SpecularPower="100.0" Brush="White"/>
<MaterialGroup x:Key="GlossyMaterial">
<DiffuseMaterial Brush="Goldenrod"/>
<StaticResource ResourceKey="Specular"/>
</MaterialGroup>
</ResourceDictionary>
...
<p:Bar3D SideCount="6" InitialAngle="30.0" RoundingRate="0.15"
Transform="{StaticResource ModelTransform}"
Material="{StaticResource GlossyMaterial}" />
Démo en ligne
Pour construire un cylindre de rayon 1.0, il suffit d'augmenter la propriété SideCount :
<p:Bar3D SideCount="100" Material="{StaticResource glossyMaterial}" />
Démo en ligne
Le Conical3D est un cône de base polygonale, et fonctionne selon le même principe que Bar3D..
<p:Conical3D SideCount="6" InitialAngle="30.0" RoundingRate="0.15"
Transform="{StaticResource ModelTransform}"
Material="{StaticResource GlossyMaterial}" />
Démo en ligne
Enfin le Ring3D est un anneau de section polygonale. Il dispose de propriétés supplémentaire pour indiquer son rayon (Radius) et le nombre de segments (SegmentCount).
<p:Ring3D
Radius="10.0" SegmentCount="100"
SideCount="6" InitialAngle="30.0" RoundingRate="0.15"
Material="{StaticResource GlossyMaterial}" />
Démo en ligne
Modèles sphériques
La classe Spherical3D définit une sphère de rayon 1. La finesse de l'aspect sphérique est définie à l'aide de la propriété ParallelCount.
<p:Spherical3D ParallelCount="100" Material="{StaticResource GlossyMaterial}"/>
Démo en ligne
Il est facile de transformer la sphère en ellipse à l'aide d'une tranformation.
<p:Spherical3D ParallelCount="100"
Material="{StaticResource GlossyMaterial}"
Transform="{StaticResource ModelTransform}"/>
Démo en ligne
Polyèdres
Pour le fun, la classe Isocahedron3D définit un isocaèdre régulier , c'est à dire un polyèdre à 20 faces.
<p:Isocahedron3D Material="{StaticResource GlossyMaterial}" /> 
Démo en ligne
La propriété Truncated permet d'obtenir un isocaèdre tronqué...
<p:Isocahedron3D Truncated="True" Material="{StaticResource GlossyMaterial}" />
Démo en ligne
Bref, vous me voyez venir avec mes gros crampons... (Oh! comment vais-je traduire cela en anglais ?). La genèse de la classe Football3D remonte à un certain soir de juillet 2006. Je ne suis pourtant pas passionné de foot, mais à force de voir de la 3D animée dans les génériques télévisés, j'ai eu envie de prouver qu'on pouvait le faire avec WPF. Ici, on change la couleur du fond, on enlève le repère orthonormé et on peut agrémenter le tout d'une animation WPF ! Voir la video
<p:Football3D />

Démo en ligne
Camemberts 3D
La classe PieSlice3D permet de construire de jolis camemberts. Chaque PieSlice3D représente une portion, et dispose des propriétés initialAngle et AngleValue. La propriété IsExploded permet de mettre en valeur une portion. Par défaut, le camembert est construit autour de l'axe des Z. Pour le présenter "à plat", il faut lui appliquer une transformation (comme dans l'exemple qui suit) :
...
<Transform3DGroup x:Key="PieTransform">
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
Angle="-90"
Axis="1,0,0" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
<ScaleTransform3D ScaleY="0.2"/>
</Transform3DGroup>
...
<p:Workshop3D>
<p:PieSlice3D
AngleValue="45"
IsExploded="True"
Material="{StaticResource GlossyMaterial}"
Transform="{StaticResource PieTransform}"/>
<p:PieSlice3D
InitialAngle="45" AngleValue="135"
Material="{StaticResource GlossyRed}"
Transform="{StaticResource PieTransform}"/>
<p:PieSlice3D
InitialAngle="180" AngleValue="80"
Material="{StaticResource GlossyBlue}"
Transform="{StaticResource PieTransform}"/>
<p:PieSlice3D
InitialAngle="260" AngleValue="100"
Material="{StaticResource GlossyGreen}"
Transform="{StaticResource PieTransform}"/>
</p:Workshop3D>
Démo en ligne
Application de texture
Il est possible d'afficher une image sur un Square3D, en utilisant un ImageBrush ou un VisualBrush dans la définition du Material.
<p:Square3D >
<p:Square3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush
ImageSource="http://www.odewit.net/Perspective/Images/Tree.jpg"
TileMode="None"
Stretch="Fill"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</p:Square3D.Material>
</p:Square3D>
Démo en ligne
Si la propriété BackMaterial n'est pas spécifiée, la face arrière affiche la texture par défaut.
Les classes Perspective qui supportent les textures (VisualBrush, ImageBrush ou DrawingBrush) sont : Square3D, Polygon3D, Box3D, Bar3D, Conical3D et Spherical3D.
Modèles composite
Les objets Wpf3D peuvent être regroupés en XAML au sein d'un ModelVisual3D ou d'un ContainerUIElement3D pour former un modèle composite, tel que ce gyroscope (animé quand vous cliquez dessus) :
<p:Gyroscope3D />

Démo en ligne
La bibliothèque Perspective est extensible, et il est facile de définir en code .NET (C# ou Visual Basic) de nouvelles classes de modèles composites. Après XyzAxis3D et Football3D, la flèche Arrow3D en est un autre exemple :
<p:Arrow3D Material="{StaticResource GlossyMaterial}" />
Démo en ligne
Contrôles 3D
Dans sa version 0.4, Perspective a introduit la notion de contrôle 3D skinable .

Démo en ligne
A propos de cet article
Auteur : Olivier Dewit
Historique :
- 1er juin 2010 : mise à jour pour la version 2.0 de Perspective, .NET 4.0 et Visual Studio 2010.
- 13 mai 2008 : textures.
- 12 mai 2008 : mise à jour pour la version 0.9 de Perspective. Article original
- 7 janvier 2008 : mise à jour pour la version 0.5 de Perspective. Article original
- 16 décembre 2007 : mise à jour pour la version 0.4 de Perspective. Article original
- 11 novembre 2007 : chapitre sur l'interactivité.
- 3 novembre 2007 : mise à jour pour la version 0.3 de Perspective. Article original
- Octobre 2006 : création, pour la version 0.1 de Perspective (Wpf3D - .NET 3.0 - Visual Studio 2005). Article original