Project-Outlook

WPF-Geometrie

Erzeugen und Darstellen von 3D-Objekten in WPF

Grundsätzlich gibt es zwei Ansätze zur Beschreibung von 3D-Objekten: die Beschreibung mit XAML oder durch Programmierung in C#-Code.

Erzeugen von 3D-Objekten mit XAML

XAML (eXtensible Application Markup Language) ist eine auf XML basierende Beschreibungssprache. Sie existiert parallel zu dem eigentlichen C#-Code.
Prinzipiell können damit auch Funktionen und Algorithmen in ein Programm implementiert werden. Da diese Verfahrensweise im Projekt nicht eingesetzt wurde und generell davon abzuraten ist, wird nicht näher darauf eingegangen.
Hauptsächlich wird XAML zur Beschreibung von Bedienoberflächen und 3D-Objekten eingesetzt. Dies ist besonders effektiv, wenn die Oberfläche oder die darzustellende 3D-Szene statisch ist und nur geringen Änderungen unterliegt. Aus diesem Grund basieren viele WPF-Beispiele fast ausschließlich auf XAML. Komplexe 3D-Objekte lassen in 3D-Programmen erzeugen und können direkt in XAML-Code exportiert werden.
Für das Outlook-Addin wurde XAML lediglich für die Bedienoberfläche verwendet. Die Visualisierung der Aufgaben durch die Fähnchen und das Brett sind vollständig in C# programmiert.

Erzeugen von 3D-Objekten mit C#

Im Folgenden wird anhand eines Fähnchens erläutert, wie sich ein 3D-Objekt in WPF mit C# erzeugen lässt.

Ausschnitte aus dem Sourcecode

Hinweis: Die Codeausschnitte wurden aus Gründen der Übersichtlichkeit gekürzt.

Im ersten Schritt wird ein MeshGeometry3D-Objekt erstellt (Z4). Diesem Objekt werden Vertices zugeordnet (Z8ff). Anschließend werden über die TriangleIndices die Kanten zwischen den Vertices festgelegt (Z13ff). Diese Vorgehensweise zur Programmierung eines Rechtecks ist ungefähr vergleichbar mit einem TriangleStrip in OpenGL.
Zusätzlich lassen sich in der MeshGeometry Normalen für Lichtreflexionen (Z23ff) und Texturkoordinaten festlegen (Z29ff).
Um die Fläche im Raum sichtbar zu machen, muss eine Textur über das Polygonnetz gelegt werden. Dazu wird ein GeometryModel3D-Objekt erzeugt (Z3). Die Geometry des GeometryModel3D stammt von der MeshGeometry3D (Z34). Die Textur wird über die Eigenschaft Material festgelegt (Z5).
Hinweis: Während der Entwicklung sollte zusätzlich auch die Eigenschaft BackMaterial verwendet werden. Andernfalls ist das Polygonnetz von der Rückseite völlig unsichtbar.

  1. GeometryModel3D sheet;
  2. MeshGeometry3D geometry;
  3. sheet = new GeometryModel3D();
  4. geometry = new MeshGeometry3D();
  5. sheet.Material = new DiffuseMaterial(new
  6.     SolidColorBrush(Colors.DarkSlateBlue));
  7. //sheet.BackMaterial = new DiffuseMaterial(new SolidColorBrush(Colors.Orange));
  8. geometry.Positions.Add(new Point3D(-0.5, -0.5, 0.5)); //down left
  9. geometry.Positions.Add(new Point3D(0.5, -0.5, 0.5)); //down right
  10. geometry.Positions.Add(new Point3D(0.5, 0.5, 0.5)); //up right
  11. geometry.Positions.Add(new Point3D(-0.5, 0.5, 0.5)); //up left
  12.  
  13. geometry.TriangleIndices.Add(0); //down left
  14. geometry.TriangleIndices.Add(1); //down right
  15. geometry.TriangleIndices.Add(2); //up right
  16. geometry.TriangleIndices.Add(2); //up right
  17. geometry.TriangleIndices.Add(3); //up left
  18. geometry.TriangleIndices.Add(0); //down left
  19.  
  20. //number of Normals and TextureCoordinates is limited by number of
  21. //Positions!
  22. //compare MSDN MeshGeometry3D..::.TextureCoordinates
  23. geometry.Normals.Add(new Vector3D(0,0,1));
  24. geometry.Normals.Add(new Vector3D(0,0,1));
  25. geometry.Normals.Add(new Vector3D(0,0,1));
  26. geometry.Normals.Add(new Vector3D(0,0,1));
  27.  
  28. //coordinates as Monitor Pixels
  29. geometry.TextureCoordinates.Add(new Point(0, 1));
  30. geometry.TextureCoordinates.Add(new Point(1, 1));
  31. geometry.TextureCoordinates.Add(new Point(1, 0));
  32. geometry.TextureCoordinates.Add(new Point(0, 0));
  33.  
  34. sheet.Geometry = geometry;

Die Raumpunkte sind so gewählt, dass ein Quadrat der Kantenlänge 1 entsteht und im Abstand 0,5 Einheiten parallel zur xy-Ebene verläuft. Der Schwerpunkt des Quadrats verläuft durch die z-Achse. Es wird im Folgenden auch Einheitsquadrat genannt.

Um ein GeometryModel3D-Objekt transformieren zu können, muss es zuvor einer ModelGroup3D hinzugefügt werden (s. folgenden Code-Ausschnitt Z38f). Die ModelGroup3D ist ein Container, der aus mindestens einem GeometryModel3D oder einer ModelGroup3D besteht. Eine Schachtelung ist sinnvoll zur Gruppierung von mehreren Objekten und Überlagerung verschiedener Transformationen. Die Pendants in OpenGL für komplexe Transformationen sind die Funktionen push() und pop().

Die Funktion buildCube(Model3DGroup) erzeugt aus sechs Referenzen auf Einheitsquadrate einen Würfel (Z34, siehe weiter unten Beschreibung des Codes für den Fahnenmast). Jedes Einheitsquadrat muss an eine andere Stelle im Raum positioniert werden. Dazu greift die Funktion auf ein bereits initialisiertes Array mit verschiedenen Rotationen zurück. Die Initialisierung des rotation[]-Arrays erfolgt im Konstruktor (Z13 - Z27). Jedes gedrehte Einheitsquadrat wird einer übergeordneten Model3DGroup hinzugefügt (Z41). Auf diese Weise lassen sich später weitere Transformationen auf den gesamten Würfel anwenden.

  1. private GeometryModel3D[] sheet;
  2. private RotateTransform3D[] rotation;
  3.  
  4. //[...]
  5.  
  6. public Cube()
  7. {
  8.     sheet = new GeometryModel3D[6];
  9.     rotation = new RotateTransform3D[6];
  10.  
  11.     //[...]
  12.  
  13.     for (int i = 0; i < 6; i++)
  14.         rotation[i] = new RotateTransform3D();
  15.  
  16.     rotation[0].Rotation =
  17.         new AxisAngleRotation3D(new Vector3D(1, 0, 0), 0); //front sheet
  18.     rotation[1].Rotation =
  19.         new AxisAngleRotation3D(new Vector3D(-1,0,0), 90); //top sheet
  20.     rotation[2].Rotation =
  21.         new AxisAngleRotation3D(new Vector3D(1,0,0), 90); //bottom sheet
  22.     rotation[3].Rotation =
  23.         new AxisAngleRotation3D(new Vector3D(0,1,0), 90); //right sheet
  24.     rotation[4].Rotation =
  25.         new AxisAngleRotation3D(new Vector3D(0,1,0), -90); //left sheet
  26.     rotation[5].Rotation =
  27.         new AxisAngleRotation3D(new Vector3D(0,1,0), 180); //back sheet
  28.  
  29.    //[...]    
  30. }
  31.  
  32. //[...]
  33.  
  34. private void buildCube(Model3DGroup cube)
  35. {
  36.     for (int i = 0; i < 6; i++)
  37.     {
  38.         Model3DGroup gr = new Model3DGroup();
  39.         gr.Transform = rotation[i];
  40.         gr.Children.Add(sheet[i]);
  41.         cube.Children.Add(gr);
  42.     }
  43. }

Der aus Einheitsquadraten bestehende Würfel hat eine Kantenlänge von 1 und seinen Schwerpunkt im Ursprung des Koordinatensystems. Dieser Würfel wird im Folgenden auch Einheitswürfel genannt.

Für den Fahnenmast wird ein Einheitsquadrat mit brauner Textur erzeugt (Z9f). Dieses wird in einem Array sechsfach referenziert (Z23). Anschließend erzeugt die buildCube(Model3DGroup)-Funktion daraus einen braunen Einheitswürfel (Z24). Der Einheitswürfel wird weiter skaliert und transliert, um die Form eines Mastes zu erhalten (Z27 - 42).
Auf ein Model3DGroup-Objekt lässt sich nur eine Transformation zuweisen. Deshalb werden das ScaleTransform3D-Objekt (Skalierung) und das TranslateTransform3D-Objekt (Translierung) zu einer Transform3DGroup zusammengefasst (Z30, Z35, Z40). Die Transform3DGroup lässt sich anschließend als Transformation auf das Model3DGroup-Objekt zuweisen (Z42).

  1. private GeometryModel3D[] sheet;
  2. private Model3DGroup mflagpole;
  3. private GeometryModel3D polemastersheet;
  4. //[...]
  5. public Cube()
  6. {
  7.     //[...]
  8.  
  9.     polemastersheet = (new Sheet3D()).getSheet3D();
  10.     polemastersheet.Material = new DiffuseMaterial(new
  11.         SolidColorBrush(Colors.Brown));
  12.  
  13.    //[...]
  14. }
  15.  
  16. //[...]
  17.  
  18. private void createPole()
  19. {
  20.  
  21.     for (int i = 0; i < 6; i++)
  22.     {
  23.         sheet[i] = polemastersheet;
  24.     }
  25.     buildCube(mflagpole);
  26.  
  27.     //Move and Resize
  28.     Transform3DGroup trgroup = new Transform3DGroup();
  29.     ScaleTransform3D trscale = new ScaleTransform3D();
  30.     TranslateTransform3D trtranslate = new TranslateTransform3D();
  31.  
  32.     trtranslate.OffsetX = -0.5; //the right edge is at center of global coordinate system
  33.     trtranslate.OffsetY = 0.5;
  34.     trtranslate.OffsetZ = 0;
  35.     trgroup.Children.Add(trtranslate);
  36.  
  37.     trscale.ScaleX = 0.1;
  38.     trscale.ScaleY = 1.2;
  39.     trscale.ScaleZ = 0.1;
  40.     trgroup.Children.Add(trscale);
  41.  
  42.     mflagpole.Transform = trgroup;
  43.  
  44.     //pack Group to allow new transform
  45.     Model3DGroup temp = new Model3DGroup();
  46.     temp.Children.Add(mflagpole);
  47.  
  48.     mflagpole = temp;
  49. }

Blickt die Kamera in Richtung der negativen z-Achse, liegt die vordere, untere, rechte Ecke des Fahnenmastes im Koordinatenursprung.

Für den farbigen Banner wird analog vorgegangen. Da der Banner eine andere Farbe hat als der Fahnenmast, wird dafür ein neues Einheitsquadrat erzeugt werden (Z11f).
Fahnenmast und Banner werden zur Positionierung auf dem Brett in einer Model3DGroup zusammengefasst (Z50f).

  1. private Model3DGroup mflag;
  2. private Model3DGroup banner;
  3. private GeometryModel3D polemastersheet;
  4.  
  5. //[...]
  6.  
  7. private void createBanner(Color color)
  8. {
  9.     if (color != oldBannerColor)
  10.     {
  11.         bannermastersheet = (new Sheet3D()).getSheet3D();
  12.         bannermastersheet.Material = new DiffuseMaterial(new
  13.             SolidColorBrush(color));
  14.         oldBannerColor = color;
  15.         mflag = new Model3DGroup();
  16.         banner = new Model3DGroup();
  17.  
  18.         for (int i = 0; i < 6; i++)
  19.         {
  20.             sheet[i] = bannermastersheet;
  21.         }
  22.  
  23.         buildCube(banner);
  24.  
  25.         //Move and Resize
  26.         Transform3DGroup trgroup = new Transform3DGroup();
  27.         ScaleTransform3D trscale = new ScaleTransform3D();
  28.         TranslateTransform3D trtranslate = new TranslateTransform3D();
  29.  
  30.         trtranslate.OffsetX = -0.5;
  31.         trtranslate.OffsetY = 0.5;
  32.         trtranslate.OffsetZ = 0;
  33.         trgroup.Children.Add(trtranslate);
  34.  
  35.         trscale.ScaleX = 0.5;
  36.         trscale.ScaleY = 0.5;
  37.         trscale.ScaleZ = 0.05;
  38.         trgroup.Children.Add(trscale);
  39.  
  40.         //banner.Transform = trgroup;
  41.  
  42.         trtranslate.OffsetX = 0.15; //translate to pole
  43.         trtranslate.OffsetY = 0.6;
  44.         trtranslate.OffsetZ = 0.0;
  45.         trgroup.Children.Add(trtranslate);
  46.  
  47.         banner.Transform = trgroup;
  48.  
  49.  
  50.         mflag.Children.Add(mflagpole);
  51.         mflag.Children.Add(banner);
  52.     }
  53. }

Hinweis: Bei der Programmierung wurde so vorgegangen, dass ein Model3DGroup-Objekt des Fahnenmastes für alle Fähnchen auf dem Brett verwendet wird. Bei den Bannern wird für jede Aufgabenliste ein neues Objekt erzeugt.

Darstellen eines 3D-Objektes mit C# in einer WPF-Oberfläche

Zur Darstellung von 3D-Szenen wird eine Oberfläche mit einem Viewport3D-Objekt benötigt. Der Viewport3D ist eine Leinwand, auf der dreidimensionale Figuren abgebildet werden. Die einzelnen Figuren müssen in Objekten vom Typ ModelVisual3D gespeichert sein, um sie dem ViewPort3D hinzufügen zu können (Z12). Ein Fähnchen ist vom Typ Model3DGroup und muss dementsprechend umgewandelt werden (Z11). Ein ModelVisual3D besteht aus einer Model3DGroup oder einem ModelVisual3D. Eine Schachtelung ist möglich.
Jedes Fähnchen wird in ein seperates ModelVisual3D-Objekt abgelegt um das Hittesting zu erleichtern.

  1. Viewport3D view;
  2.  
  3. //[...]
  4.  
  5. ModelVisual3D mvflag = new ModelVisual3D();
  6. Model3DGroup flag = new Model3DGroup();
  7. flag.Children.Add(mycube.get_flag(color));
  8.  
  9. //[...]
  10.  
  11. mvflag.Content = flag;
  12. view.Children.Add(mvflag);

Im Quellcode nicht dargestellt ist die Festlegung der Kamera. Sie ist eine Eigenschaft des Viewport3D.
Hinweis: Licht wird wie reguläre 3D-Objekte behandelt.

Auslesen der Daten aus Outlook | | Benutzerdefinierte Animationen mit WPF

Options: