MSDN Magazin > Home > Ausgaben > 2008 > April >  Silverlight: Erstellen fortgeschrittener 3D-Ani...
Silverlight
Erstellen fortgeschrittener 3D-Animationen mit Silverlight 2.0
Declan Brennan

Themen in diesem Artikel:
  • XAML-Grundlagen
  • Erstellen von Elementen in XAML
  • Falten eines Polyeders
  • Emulieren der DirectX-Mathematik
In diesem Artikel werden folgende Technologien verwendet:
Silverlight
Wenn in den vergangenen Monaten durch einen ausgesprochen unglücklichen Zufall die ganze Werbung für SilverlightTM an Ihnen vorbeigegangen ist, bringe ich Sie jetzt auf den neuesten Stand: Silverlight ist ein neues, browserübergreifendes Plug-In von Microsoft, das die Leistungsfähigkeit von Microsoft® .NET Framework in einem Bereich zum Tragen bringt, der zuvor Flash- oder Java-Applets vorbehalten war. Silverlight wird mit einer umfassenden Auswahl an nützlichen Features geliefert. Es unterstützt eine leistungsfähige Version von .NET Framework 3.5, die unter anderem XML und XAML (Extensible Application Markup Language), generische Auflistungen, Webdienste und LINQ enthält. Silverlight unterstützt auch eine Reihe von .NET-kompatiblen Sprachen, aber hier bleibe ich bei C#.
Die beste Möglichkeit, sich mit einer neuen Technologie auseinanderzusetzen, besteht darin, ein wenig Spaß damit zu haben. Als Silverlight 1.1 Alpha veröffentlicht wurde und ich die aufregende Präsentation von Tim Sneath auf der IMT-Konferenz in Dublin sah, beschloss ich, eine kleine Demoanwendung zusammenzustellen, in der gezeigt wird, wie verschiedene 3D-Formen (Polyeder genannt) zusammengesetzt werden können, indem eine flache Vorlage gefaltet wird. Silverlight unterstützt 3D nicht standardmäßig. Deshalb musste eine Emulation der DirectX®-Mathematikbibliotheken für die 3D-Animationen erstellt werden.
Ein Polyeder ist ein dreidimensionales Objekt mit ebenen Flächen. Anhand dieses Silverlight-Beispiels werden jene regelmäßigen und halbregelmäßigen Polyeder erforscht, die platonische und archimedische Polyeder genannt werden. Die Flächen dieses Polyeders sind alle regelmäßige Polygone, d. h., ihre Seiten sind alle gleich lang, wie bei gleichseitigen Dreiecken oder bei Quadraten. Außerdem sind sie konvex. Das bedeutet, dass sie keine hervorstehenden Teile haben. Wie Sie an den altgriechischen Namen erkennen können, haben diese Objekte die Menschheit schon lange Zeit fasziniert. Wenn Sie sich dafür interessieren, können Sie auf der Website von George Hart viel mehr darüber herausfinden: georgehart.com/virtual-polyhedra/vp.html.
In Abbildung 1 oder unter picturespice.com/ps/Polyhedra/default.html können Sie ein Demo der fertigen Anwendung sehen. Im Grunde können Sie in der Anwendung eine Form (einen Polyeder) auswählen, indem Sie die Maus darüber platzieren. In der rechten oberen Ecke des Fensters werden dann einige Informationen zu Ihrer Auswahl angezeigt, außerdem eine Animation einer ebenen Vorlage, die zu dem ausgewählten Polyeder gefaltet wird. Wenn Sie schließlich auf die Schaltfläche „Cycle“ klicken, durchläuft das Programm automatisch der Reihe nach alle Formen.
Abbildung 1 Silverlight-Demo „Polyhedra“ 

Verwenden von XAML
Wie bei vielen Silverlight-Anwendungen wird auch bei Polyhedra XAML stark genutzt. XAML ist eine Inhaltsdefinitionssprache, eine Art HTML-Äquivalent, aber flexibler. Um die Analogie fortzuführen: Während es möglich ist, allein mit dem HTML-Dokumentobjektmodell (DOM) eine HTML-Seite zu erstellen, ist dies selten eine vernünftige Methode, Inhalte zu erstellen. Die Codierung ist oft zeitaufwändig, und die erstellten Seiten werden nur langsam initialisiert. Es ist nahezu immer am besten, so viel wie möglich von der Seite in HTML-Markup zu halten und dieses dort mit JavaScript und dem DOM zu ergänzen, wo Flexibilität erforderlich ist.
Ein sehr ähnlicher Ansatz gilt für XAML. Inhalte lassen sich am schnellsten zusammenzustellen, indem so viel XAML-Markup wie möglich verwendet und dies ggf. an erforderlichen Stellen mit einer .NET-kompatiblen Sprache wie C# und der Silverlight Media-API erweitert wird. XAML kann handcodiert sein, erstellt durch ein Entwurfspaket wie Expression BlendTM, generiert von einem Programm, das während des Entwicklungsprozesses ausgeführt oder sogar dynamisch auf dem Server generiert wird. Dies kann eine andere Denkweise erfordern. Ein C#-Programmierer codiert zu schnell Funktionen in der gewohnten Umgebung, die am besten XAML überlassen werden sollten.
Alles andere als eine sehr kleine Kostprobe von XAML würde den Umfang dieses Artikels sprengen. Charles Petzold behandelt XAML in seinem Buch „Applications = Code + Markup“ aber sehr ausführlich.
Hier ist das XAML-Äquivalent des „Hello World“-Beispiels, das wir alle beim Erlernen neuer Sprachen bereits erwarten:
<UserControl x:Class="Polyhedra.Page"
  xmlns="http://schemas.microsoft.com/client/2007" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  Width="400" Height="300">
  <Grid x:Name="LayoutRoot" Background="White">
    <TextBlock>Hello World</TextBlock>
  </Grid>
</UserControl>
Das Stammelement ist UserControl. Dieses enthält ein Grid-Element, welches wiederum ein TextBlock-Element mit dem Text „Hello World“ enthält.
Betrachten Sie kurz die UserControl-Attribute. Ohne zu sehr ins Detail zu gehen, wird damit das Äquivalent einer CodeBehind-Klasse für XAML definiert. Diese Klasse wird zum selben Zeitpunkt instanziiert, zu dem XAML analysiert und geladen wird. Sie können verschiedene Teile der Initialisierung im Konstruktor durchführen, aber um ein stärker ausgearbeitetes Verhalten zu erhalten, ist es oft erforderlich, Ereignishandler einzusetzen. Dies ist ein Hauptfeature von Silverlight. Ereignishandler können an verschiedene XAML-Objekte angehängt und in der .NET-kompatiblen Sprache Ihrer Wahl implementiert werden, einer Sprache, die im Unterschied zu JavaScript kompiliert wird und daher allerlei Möglichkeiten eröffnet, die andernfalls nicht praktikabel wären.
Um zur HTML-Analogie zurückzukehren: Elemente sind häufig in verschiedenen DIVs gruppiert, um ihre Position auf der Seite festzulegen. In ähnlicher Weise sind in XAML die Formen in Canvas-Elementen (oder anderen Elementen wie Grid-Elementen, die eine Art von Canvas-Element sind) gruppiert. Genauso wie DIVs häufig in HTML verschachtelt sind, können Canvas-Elemente in XAML verschachtelt werden. Die meisten Elemente in HTML sind rechteckig. XAML jedoch unterstützt eine ganze Reihe von Formen, einschließlich „TextBlock“, „Rectangle“, „Polygon“, „Ellipse“ und das sehr flexible „Path“, mit dem benutzerdefinierte Formen möglich sind. In HTML werden Elemente mit einem ID-Attribut identifiziert. Das gleichwertige Attribut, mit dem ein Element in XAML identifiziert wird, lautet „x:Name“, wobei „x“ der Alias für den XAML-Namespace ist.
XAML kann viel mehr als nur statische Seiten erstellen. Eines seiner leistungsfähigsten Features ist die Verwendung von Storyboards zum Animieren von Änderungen an der anfänglichen Benutzeroberfläche, die von XAML angegeben wird. (Etwas Ähnliches, „HTML+Time“ genannt, wurde HTML in Internet Explorer® 5.0 hinzugefügt.) Ein Beispiel kann die Animation der Farbänderung, der Sichtbarkeit oder der Transparenz eines Objekts sein. Wenn Storyboards in Verbindung mit Transformationen verwendet werden, können Objekte auch gedreht, skaliert oder verschoben werden.
Mit Storyboards können alle Arten von Animationseffekten mit wenig oder ganz ohne konventionellen Code generiert werden. Wenn sie in Verbindung mit Triggern verwendet werden, ist es theoretisch möglich, verschiedene Animationen automatisch zu starten, wenn ein Ereignis wie MouseEnter bei einem Objekt stattfindet. Leider ist Loaded in Silverlight 2.0 Beta von März 2008 das einzige Ereignis, das von einem Trigger verarbeitet wird. Für andere Ereignisse ist ein wenig Grundlagencode in Form eines Ereignishandlers erforderlich. Diese Situation wird sich wahrscheinlich bald ändern, wenn es nicht bereits geschehen ist.
Eine nützliche Eigenschaft von Storyboards in Silverlight besteht darin, dass sie zeitbasiert und nicht framebasiert sind. Die Verwendung mehrerer unabhängiger Storyboards, die an verschiedene Zeiten und Ereignisse gebunden sind, kann die Implementierung eines komplexen Verhaltens vereinfachen. Schließlich funktioniert die reale Welt nicht in Frames. Das ist ein Überbleibsel der Methode, mit der Filme auf Zelluloid möglich wurden. Wenn unabhängige Objekte mit unabhängigen Verhaltensweisen implementiert werden, wird das Leben sehr viel einfacher.

Einige XAML-Beispiele
Die Faltanimation in der Mitte der Polyhedra-Anwendung wird mit C#-Code generiert. Der Rest ist hauptsächlich in XAML definiert. Dazu gehören der Kreis von Beispielpolyedern, die Art und Weise, wie der aktuell ausgewählte Polyeder auf vielfältige Weise betont wird, sowie die Schaltfläche „Cycle“, deren Bereitschaft angezeigt wird, indem ein sich drehender Pfeil animiert wird.
Sehen Sie sich einige Auszüge aus Page.xaml an, um zu sehen, wie diese Animationen erreicht werden. Beginnen Sie mit der Schaltfläche „Cycle“, die in Abbildung 2 gezeigt wird. Werfen Sie ein Blick auf das Path-Element namens „Cycle“. Mit dem Data-Attribut wird eine Reihe von Vorgängen angegeben, einschließlich M für Bewegung (move), A für Bogen (arc) und L für Linie (line). Der Einfachheit halber habe ich nur die gewünschte Form auf ein Blatt Papier gezeichnet und die erforderlichen Vorgänge manuell ausgearbeitet. In den meisten Fällen sind Sie bei solchen Vorgängen mit einem Tool wie Expression Design besser bedient.
<Canvas x:Name="CycleButton"
  Canvas.Top="250"
  MouseLeftButtonDown="CycleButtonLeftMouseDown" >
  <Path x:Name="Cycle" Stroke="#000033" 
    Fill="#FFB47C0D" Canvas.Left="10" Canvas.Top="10"
    Data="M 25,35 A 10,10 180 1 0 25,15 L 25,20 L 12.5,10 L 25,0 L 25,
         5 A 10,10 180 1 1 25,45 Z" 
    Width="50" Height="50">
    <Path.RenderTransform>
      <RotateTransform x:Name="CycleRotate" Angle="0" 
        CenterX="25" CenterY="25"/>
    </Path.RenderTransform>
    <Path.Resources>
      <Storyboard x:Name="CycleLatched">
        <DoubleAnimation Storyboard.TargetName="CycleRotate" 
          Storyboard.TargetProperty="Angle" From="360" To="0" 
          Duration="00:00:02" RepeatBehavior="Forever"/>
      </Storyboard>
    </Path.Resources>
  </Path>
  <TextBlock x:Name="CycleCaption" Canvas.Left="65" Canvas.Top="10" 
    Foreground="#FFB47C0D" FontSize="30" FontWeight="Bold" 
        Text="Cycle" />
  <Rectangle Width="200" Height="70" RadiusX="30" RadiusY="30" 
    Stroke="#FFB47C0D" StrokeThickness="4" Fill="Transparent"/>
</Canvas>

Dieses Path-Element besitzt auch ein RotateTransform-Element namens „CycleRotate“. Anfänglich geschieht bei dieser Transformation nichts, da ihr Winkel auf 0 gesetzt ist. Es gibt jedoch ein Storyboard-Element namens „CycleLatched“, mit dem im aktivierten Zustand der Winkel kontinuierlich in kleinen Inkrementen geändert wird, wodurch sich der Pfeil dreht.
In Abbildung 3 wird gezeigt, wie eines der Polyederbeispiele definiert ist. Am Ende dieses XAML können Sie sehen, dass das Beispiel aus den vier Polygonen besteht, die den Tetraeder definieren. Sie befinden sich innerhalb ihres eigenen Canvas-Elements namens „Model0“ und sind umgeben von einem Ring oder einem Ellipse-Element namens „Ring0“, das anfänglich nicht sichtbar ist. Es sind zwei Storyboard-Animationen definiert, eine für MouseEnter und eine für MouseLeave. Durch die MouseEnter-Animation wird der Ring sofort sichtbar, lässt die Größe des Model0-Canvas-Elements ansteigen und macht es über einen Zeitraum von 0,7 Sekunden undurchsichtiger. Mit der MouseLeave-Animation wird jede dieser Änderungen umgekehrt.
<Canvas x:Name="Canvas0" Width="116.376" Height="116.376" 
  Canvas.Left="671.812" Canvas.Top="341.812" 
  MouseEnter="TriggerMouseEnter" MouseLeave="TriggerMouseLeave">
  <Canvas.Resources>
    <Storyboard x:Name="Canvas0MouseEnter">
      <DoubleAnimation Duration="00:00:00" Storyboard.TargetName="Ring0" 
        Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" />
      <DoubleAnimation Duration="00:00:00.7" 
        Storyboard.TargetName="Model0" 
        Storyboard.TargetProperty="Opacity" From="0.7" To="1.0" />
      <DoubleAnimation Duration="00:00:00.7" 
        Storyboard.TargetName="Model0Scale" 
        Storyboard.TargetProperty="ScaleX" From="0.7" To="1.0" />
      <DoubleAnimation Duration="00:00:00.7" 
        Storyboard.TargetName="Model0Scale" 
        Storyboard.TargetProperty="ScaleY" From="0.7" To="1.0" />
    </Storyboard>
    <Storyboard x:Name="Canvas0MouseLeave">
      <DoubleAnimation Duration="00:00:00" Storyboard.TargetName="Ring0" 
        Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" />
      <DoubleAnimation Duration="00:00:00.7" 
        Storyboard.TargetName="Model0" 
        Storyboard.TargetProperty="Opacity" From="1.0" To="0.7" />
      <DoubleAnimation Duration="00:00:00.7" 
        Storyboard.TargetName="Model0Scale" 
        Storyboard.TargetProperty="ScaleX" From="1.0" To="0.7" />
      <DoubleAnimation Duration="00:00:00.7" 
        Storyboard.TargetName="Model0Scale" 
        Storyboard.TargetProperty="ScaleY" From="1.0" To="0.7" />
    </Storyboard>
  </Canvas.Resources>
  <Canvas x:Name="Model0" Opacity="0.7" 
    Width="116.376" Height="116.376" >
    <Canvas.RenderTransform>
      <ScaleTransform x:Name="Model0Scale" CenterX="58.188" 
        CenterY="58.188" ScaleX="0.7" ScaleY="0.7"/>
    </Canvas.RenderTransform>

    <Polygon Canvas.ZIndex="-12545653" Fill="#FFCC0000" 
      Stroke="#000000" StrokeThickness="1" 
      Points="97.566,74.278 43.052,40.971 33.836,99.937 "/>
    <Polygon Canvas.ZIndex="-11948057" Fill="#FFCC0000" 
      Stroke="#000000" StrokeThickness="1" 
      Points="43.052,40.971 97.566,74.278 58.188,37.662 "/>
    <Polygon Canvas.ZIndex="-11309683" Fill="#FFCC0000" 
      Stroke="#000000" StrokeThickness="1" 
      Points="33.836,99.937 43.052,40.971 58.188,37.662 "/>
    <Polygon Canvas.ZIndex="-10481036" Fill="#FFCC0000" 
      Stroke="#000000" StrokeThickness="1" 
      Points="97.566,74.278 33.836,99.937 58.188,37.662 "/>
  </Canvas>
  <Ellipse x:Name="Ring0" Opacity="0" Stroke="#FFB47C0D" 
    StrokeThickness="4" Width="116.376" Height="116.376" />
</Canvas>

Da diese Storyboards unabhängig voneinander arbeiten können, geschieht alles, wie Sie es erwarten würden. Dabei spielt es keine Rolle, wie schnell der Benutzer die Maus bewegt. In der Regel findet eine MouseLeave-Animation für ein Beispiel zu demselben Zeitpunkt statt wie die MouseEnter-Animation für das neu ausgewählte Beispiel. Sie müssen sich jedoch keine Gedanken darüber machen, da Silverlight die Ausführung mehrerer aktiver Storyboards automatisch parallel verarbeitet.
Kleine Nebenbemerkung: Möglicherweise fragen Sie sich, wie die Ecken jedes der vier Polygone für das Tetraederbeispiel berechnet wurden, ganz zu schweigen von der häufig weit größeren Anzahl an Polygonen für jedes der anderen Beispiele in der Anwendung. Ich bin ein großer Anhänger des „faulen“ Ansatzes, nie irgendetwas selbst zu tun, was ein Computer schneller erledigen kann. Darum habe ich während des Produktionsprozesses nur eine geänderte Version von Polyhedra verwendet. In dieser Version wurde der Reihe nach für jedes Beispiel ein Frame der zentralen Animation durchgeführt, der Frame mit der vollständig geschlossenen Form. Jede der sich ergebenden Polygongruppen wurde in einem Satz von Canvas-Elementen platziert, die gleichmäßig in einen Kreis verteilt wurden. Das alles wurde für die Verwendung im Hauptprogramm in eine Datei gestreamt.
Das Anordnen von Objekten in einem Kreis erfordert die schrittweise Vergrößerung des Winkels in gleichen Inkrementen von „2*PI/Anzahl der Beispiele“ und das Angeben der Mitte jedes Beispiels mithilfe einer Koordinate von „x=Radius*Cos(Winkel), y=Radius*Sin(Winkel)“. Da bei den Canvas-Positionierungsobjekten die Eigenschaften „Left“ und „Top“ verwendet werden, musste die Mitte um die halbe Breite bzw. die halbe Höhe versetzt werden.

Tipps für die Arbeit mit XAML
Wie Sie sehen können, kann mit XAML und fast ohne zusätzlichen Code eine reichhaltige Benutzeroberfläche erzielt werden. Sogar bei einer großen Datei wie Page.xaml verläuft die Initialisierung erstaunlich schnell. Aber bevor ich weitermache, hier einige Tipps, die auf meinen Erfahrungen mit der aktuellen Implementierung (Betaversion von März 2008) von Silverlight 2.0 basieren.
Wie bereits erwähnt, können Trigger Storyboards derzeit nur für das Ereignis „Loaded“ (über die Begin-Methode) starten. Für andere Ereignisse ist ein wenig Grundlagencode in Form eines Ereignishandlers wie diesem erforderlich:
public void MouseEnterHandler(
  object o, EventArgs e) {
  this.MouseEnterStoryBoard.Begin();
}
Wenn Sie eine Benennungskonvention für Ihre Storyboards verwenden, z. B. der Objektname gefolgt vom Ereignisnamen, können Sie die Anzahl separater Ereignishandler, die Sie codieren müssen, stark verringern. Diese Methode wird beispielsweise in Polyhedra als gemeinsamer Ereignishandler für alle Beispiele im Kreis verwendet:
public void MouseEnterHandler(
  object o, EventArgs e) {
  this.triggerStoryboard(o,"MouseEnter");
}
private bool triggerStoryboard(
  object o, string eventType) {
  Canvas el = o as Canvas;

  string name= el.GetValue(NameProperty) as String;
  Storyboard sb = el.FindName(name + eventType) as Storyboard;
  if (sb != null)
    sb.Begin();
    return (sb != null);
}
In Silverlight 2.0 Beta wurde die Hauptinitialisierung (bei der InitComponents aufgerufen wird) vom Loaded-Ereignishandler in den Konstruktor für das CodeBehind-Objekt verschoben. Dies ist eleganter, aber bedenken Sie, dass im Konstruktor nicht alles möglich ist. Zum Beispiel ist es hier nicht möglich, Begin oder Pause für ein Storyboard aufzurufen. Deshalb muss dies immer noch in einem Ereignishandler geschehen.
Wie ich in Andy Beaulieus Asteroiden sprengendem Beispiel „Silverlight Rocks!“ entdeckt habe (www.andybeaulieu.com/Home/tabid/67/EntryID/73/default.aspx), lässt sich eine codebasierte Animation gut erreichen, indem für ein Storyboard ein kurzer Zeitraum festgelegt und ein Completed-Ereignishandler bereitgestellt wird, der einen Frame der Animation ausführt und dann das Storyboard neu startet:
public Page() { // Constructor for "code-behind"
  // Required to initialize variables
  InitializeComponent();
  this.animationTimer.Completed += 
    new EventHandler(animationTimer_Completed);
}

void animationTimer_Completed(object sender, EventArgs e) {
  [ Do a frame of animation ]
  this.animationTimer.Begin();
}
Im September-Update von Silverlight 2.0 Alpha wurden die Anforderungen für Storyboards geändert, sodass eine Animation jetzt ein Ziel haben muss, selbst wenn sie nicht ausgeführt wird:
<Canvas.Resources>
  <Storyboard x:Name="animationTimer">
    <DoubleAnimation Duration="00:00:00.01" 
      Storyboard.TargetName="bogusTimerTarget" 
      Storyboard.TargetProperty="Width" />
  </Storyboard>
</Canvas.Resources>
<Canvas Name="bogusTimerTarget">
</Canvas>
Versuchen Sie nicht, zu viele separate Silverlight-Steuerelemente auf einer HTML-Seite unterzubringen. In meiner ersten Implementierung von Polyhedra wurde für jedes Beispiel im Kreis ein separates Steuerelement verwendet, und das war ein richtiger Speicherfresser. Dies kann in einigen Fällen bedeuten, dass Inhalte von HTML zu XAML verschoben werden müssen, um die Anzahl der verwendeten Steuerelemente einzuschränken.
Einer der Vorteile von XAML besteht darin, dass es vieles von dem alltäglichen Kram in der Benutzeroberfläche übernimmt, sodass Sie sich auf den anspruchsvollen, schöpferischen Teil des Programmierens konzentrieren können. Anspruchsvoll ist in diesem Beispiel das Falten der Vorlagen zum Bilden der 3D-Formen, was uns zum nächsten Abschnitt führt.

Falten eines Polyeders
Ich vermute, dass das weiter oben erwähnte Buch von Charles Petzold eine Adaption eines weitaus älteren, vor-objektorientierten Buchs von Niklaus Wirth namens „Algorithms + Data Structures = Programs“ ist. Selbst nach all diesen Jahren bleibt es eines der einflussreichsten Bücher, das ich je gelesen habe, und trotz aller Änderungen in den Sprachen und Paradigmen, die seitdem stattgefunden haben, ist es immer noch sehr relevant. Der Hauptgedanke des Buchs ist ein Entwicklungsansatz, bei dem festgestellt wird, mit welchen Datenstrukturen Ihr Problem am besten modelliert werden kann. Anschließend werden die Algorithmen identifiziert, mit denen diese Datenstrukturen verarbeitet oder geändert werden können. Wenn ich ein Programm in Angriff nehme, bei dem ich neue Wege gehen muss, ist dies ein Ansatz, den ich oft übernehme.
Ich habe mit verschiedenen Ansätzen herumprobiert, bevor ich mich schließlich für denjenigen entschieden habe, der in Polyhedra verwendet wird. Ich wollte sehen, mit wie wenigen Informationen ich starten könnte, um die Faltanimation zu erreichen. Da die Seiten alle gleich lang sind, stellte sich heraus, dass nahezu alles erreicht werden kann, wenn man nur weiß, welche Flächen in einem gegebenen Polyeder mit welchen anderen Flächen verbunden sind. Dies deutet auf eine Diagrammdatenstruktur hin. Vor Erreichen der endgültigen XAML-Ausgabe wird das Diagramm mit einem Satz an Algorithmen durch zwei separate Baumdatenstrukturen verarbeitet. All dies wird weiter unten erklärt.
Obwohl Windows® Presentation Foundation (WPF) 3D in XAML unterstützen kann, unterstützt Silverlight standardmäßig nur 2D, weil die browserübergreifende Kompatibilität weit einfacher zu erreichen ist, wenn Sie sich nicht darum kümmern müssen, welche GPU-Hardware (Graphics Processing Unit) im Computer installiert ist. Natürlich ist 3D auf einem Computerbildschirm genau genommen eine Illusion. Ungeachtet der Manipulationen, die stattfinden, wird am Ende ein Satz gewöhnlicher 2D-Polygone auf dem Bildschirm angezeigt. Wenn Sie gewillt sind, die 3D-Manipulationen in Ihrem eigenen Code durchzuführen, um die Koordinaten dieser Polygone zu berechnen, ist ein nicht hardwarebeschleunigtes 3D-Rendering möglich. Unter der Voraussetzung, dass Sie sich auf einige Dutzend Polygone beschränken, ist die Leistung akzeptabel. (Subjektiv hat sich die Frameleistung von Alpha zu Beta verbessert.)
Wenn eine Form ausgewählt ist, wird eine zweidimensionale entfaltete Vorlage erstellt, die Netz genannt wird. Dies geschieht in zwei Phasen, wie in Abbildung 4 dargestellt. Zuerst wird im Speicher ein Diagramm erstellt, in dem ein Diagrammknoten (GraphNode) jeweils einer Fläche in der Form entspricht (siehe Graph.cs). Dieses Diagramm wird erstellt, indem ein Satz von Verbindungsinformationen (mit dem angezeigt wird, welche Fläche mit welcher verbunden ist) aus einer der .shp-Ressourcen eingestreamt wird, die in die Anwendungsassembly eingebettet sind. Hier sehen Sie z. B. die Inhalte von cube.shp:
Abbildung 4 Erstellen der Form, vom Diagramm zu XAML (Klicken Sie zum Vergrößern auf das Bild)
1:3,4,5,2
2:1,5,6,3
3:2,6,4,1
4:3,6,5,1  
5:1,4,6,2
6:2,5,4,3
Dann kann ein beliebiger Diagrammknoten im Diagramm als erster Knoten ausgewählt werden, bei dem mit dem Erstellen des Netzes begonnen wird (ausführliche Informationen finden Sie in Net.cs im Codedownload). Anschließend wird eine zweidimensionale ebene Fläche (FlatFace) mit der gleichen Anzahl an Seiten angelegt, wie der Diagrammknoten Nachbarn hat. Daraufhin wird ein beliebiger angrenzender Diagrammknoten ausgewählt, um den Prozess zu wiederholen. Die nächste ebene Fläche wird angelegt, damit er mit der aktuellen eine Seite gemeinsam hat. Jeder Diagrammknoten wird nur einmal besucht, und der Prozess schreitet fort, bis alle Diagrammknoten besucht worden sind. Dies führt zu einer Baumstruktur ebener Flächen.
In jedem Frame der 3D-Animation wird aus dem Netz ein dreidimensionaler Polyeder (u. U. teilweise geschlossen) erstellt. Weitere Informationen zu diesem Vorgang finden Sie in polyhedron.cs weiter unten in diesem Artikel. Dies umfasst das Kopieren der Struktur ebener Flächen in eine Struktur von Flächen, wobei 2D-Eckpunkte durch 3D-Punkte ersetzt werden. (In Abbildung 5 werden die beiden Koordinatensysteme gezeigt. Da in der horizontalen Ebene begonnen werden soll, muss Y mit null beginnen. Also wird 2D zu 3D zugeordnet, indem X=X, Y=0 und Z=Y festgelegt wird.)
Abbildung 5 Transformieren von Punkten von 2D in 3D (Klicken Sie zum Vergrößern auf das Bild)
Abschließend wird Rekursion verwendet, um jede Verknüpfung in der Struktur von Flächen zu besuchen und eine Faltung durchzuführen. Das Maß der Faltung ist ein Bruchteil des Winkels (Diederwinkel genannt), in dem die Flächen zueinander stehen, wenn die Form vollständig geschlossen ist. Es ist theoretisch möglich, die Diederwinkel allein anhand der Verbindungsinformationen in der .shp-Datei direkt zu berechnen.
Dies ist in einigen besonderen Fällen recht einfach, z. B. wenn drei Flächen in einer Ecke zusammentreffen oder beliebig viele Flächen des gleichen Typs in einer Ecke aufeinander treffen. Im Allgemeinen ist der Fall jedoch etwas schwieriger gelagert. Deshalb habe ich beschlossen, diese Winkel in separaten .dihedrals-Ressourcen zu speichern. Hier ist z. B. ein Auszug aus cube.dihedrals:
1,3:1.5707965056551
1,4:1.57079661280793
1,5:1.57079614793469
1,2:1.57079604078186
2,1:1.57079604078186
2,5:1.57079628466395
2,6:1.57079661280793
2,3:1.57079636892585
3,2:1.57079636892585
3,6:1.57079614793469
3,4:1.57079628466395
...
Die geringfügigen Unterschiede zwischen diesen Zahlen, die in diesem Fall alle PI/2 sein sollten, sind nur eine Folge der Berechnungsmethode.
Der endgültige Prozess umfasst eine Reihe von Transformationen, um eine Ansicht des Polyeders als Satz von Polygonen auf den Bildschirm zu projizieren. Vielleicht haben Sie festgestellt, dass der Polyeder während der Faltanimation auch um eine senkrechte Achse gedreht wird. Um dies zu erreichen, wird der Drehpunkt zum Ursprung verschoben, die Drehung durchgeführt und dann eine Verschiebung zum Aussichtspunkt vorgenommen. Dann wird eine Perspektiventransformation durchgeführt, um sicherzustellen, dass entfernte Objekte (auf der Z-Achse) kleiner erscheinen. (Im nächsten Abschnitt wird näher erläutert, wie Transformationen durchgeführt werden.)
Abschließend wird ein Satz von XAML-Polygonen generiert, die der Projektion der 3D-Form auf der Oberfläche des Bildschirms entsprechen. Silverlight ist gezwungen, diese Polygone von hinten nach vorn zu zeichnen, indem die XAML-Eigenschaft „ZIndex“ für jedes Polygon mithilfe der Z-Koordinate der Mitte des 3D-Polygons festgelegt wird (wurde skaliert, um einen float-Datentyp in einen int-Datentyp zu quetschen). Dies ist eine grob vereinfachende Methode, 3D zu erreichen. Dies wird nur funktionieren, wenn die Polygone keine Probleme machen, etwa indem sie sich kreuzen. In diesem Beispiel komme ich damit davon. Intelligentere Formen von 3D umfassen Methoden wie Tiefenpuffer. Da diese GPU-Zugriff erfordern, liegen sie außerhalb des Bereichs der Silverlight-.NET-Sandbox.
Zu guter Letzt lässt XAML zu, dass Polygone teilweise transparent sind (indem die Eigenschaft „Opacity“ verwendet wird). Dadurch entsteht der künstlerische Effekt eines teilweise durchsichtigen Polyeders. Das ist alles.

Emulieren der DirectX-Mathematik
Ich werde ein wenig auf die Mathematik eingehen, mit der die Animation erreicht wurde. Wenn Sie mehr ins Detail gehen möchten, gibt es viele Websites im Internet, die weiterhelfen, einschließlich de.wikipedia.org, euclideanspace.com, mathworld.wolfram.com, gamasutra.com und gamedev.net.
In den weiter oben gezeigten XAML-Beispielen wurden bereits einige 2D-Transformationen vorgestellt, z. B. RotationTransform und ScaleTransform. In diesen erledigt Silverlight die ganze Arbeit für Sie. Wenn Sie XAML in einer WPF-Umgebung verwenden, haben Sie ebenfalls Zugriff auf 3D-Transformationen, aber diese sind in Silverlight nicht verfügbar. WPF verlässt sich bei den einfachen Arbeiten auf DirectX. Das ist logisch, da DirectX über einen schönen Satz an Klassen verfügt, um die für 3D-Transformationen erforderliche Mathematik zu erledigen. Um einige der gleichen Transformationen in Silverlight durchzuführen, wurde von vielen dieser Klassen eine Emulation der DirectX-Mathematik erstellt, damit sie in der Silverlight-Sandbox verwendet werden können.
Wenn Sie Erfahrung im Codieren in Direct3D® haben, werden Sie sich mit den Microsoft-Implementierungen von Vector2, Vector3, Matrix und Ähnlichem wohl fühlen (siehe Abbildung 6). Wenn nicht, gebe ich Ihnen eine sehr kurze und nicht sehr gründliche Übersicht sowie einige Beispiele.
Abbildung 6 Emulierte DirectX-Mathematikklassen (Klicken Sie zum Vergrößern auf das Bild)
Für das Auffinden eines Punkts in 2D sind zwei Koordinaten erforderlich: X und Y. In 3D sind drei Koordinaten erforderlich: X, Y und Z. Obwohl Punkte genau genommen keine Vektoren sind, können 2D-Punkte in einem Vector2-Objekt und 3D-Punkte in einem Vector3-Objekt gespeichert werden. (Vektoren sind eigentlich Entitäten mit einer Größe und einer Richtung, die Sie mit einem Pfeil veranschaulichen können, der am Ursprung beginnt und an der Spitze endet. In gewissem Sinne können Sie sich vorstellen, dass alle Vektoren am Ursprung beginnen.)
Formen in drei Dimensionen können als Satz von Flächen identifiziert werden. Flächen wiederum werden als Satz von Punkten identifiziert, die den Ecken entsprechen. Nahezu alles, was Sie vielleicht tun möchten, umfasst das Transformieren dieser Punkte in einen anderen Satz von Punkten, indem einer oder mehrere Vorgänge wie Drehung, Verschiebung oder Skalierung angewendet werden. Nahezu jeder Vorgang, den Sie u. U. ausführen möchten, kann von einem 4x4-Zahlenraster repräsentiert werden, das Matrix genannt wird. (Matrizen können im Allgemeinen beliebig viele Zeilen und Spalten umfassen, aber wir sind nur an jenen interessiert, mit denen 3D-Punkte mit so genannten homogenen Koordinaten manipuliert werden.) Diese Vorgänge werden als lineare Transformationen bezeichnet, da sie die Form des Objekts, auf das sie angewendet werden, nicht (übermäßig) verzerren.
Sehr nützlich ist eine Möglichkeit, zwei solche Matrizen in einer Matrix zu kombinieren, was denselben Effekt hat. Dieser Vorgang wird Matrixmultiplikation genannt und kann mehrmals wiederholt werden. Das bedeutet, dass Sie einen ganzen Satz an Transformationen in einer einzigen Matrix zusammenfassen können und dabei die gleiche Wirkung erzielen. Das ist weit effizienter als jede Transformation separat anzuwenden. Es gibt keinen Grund, sich Gedanken darüber zu machen, wie Matrixmultiplikation implementiert wird oder wie sie ihre Transformationsmagie durchführt. Verwenden Sie sie nur als Werkzeug.
Zum Transformieren eines Quellpunkts in einen Zielpunkt ist zwischen einer Matrix und einem Vektor ebenfalls ein Multiplikationsvorgang definiert. Um Verwirrung (bei der Matrix-Matrix-Multiplikation) zu vermeiden, wird dies in Vector3 mithilfe der TransformCoordinate-Methode implementiert.
Einige kleine Codeausschnitte sollten die Situation verdeutlichen. Zuerst wird der folgende Code in polyhedron.cs verwendet, um zwei Flächen entlang ihrer gemeinsamen Seite zu falten:
Vector3 axis = axisTo - axisFrom;
Matrix foldTransform = 
  Matrix.Translation(-axisFrom) * 
  Matrix.RotationAxis(axis, proportion * _dihedralAngle) * 
  Matrix.Translation(axisFrom);
Vector3[] p= Vector3.TransformCoordinate(
  face.Points,foldTransform);
An dieser Stelle ist die gemeinsame Seite als Linie von axisFrom nach axisTo definiert. Drehungen sind immer bezüglich einer Linie durch den Ursprung definiert. Bevor also gedreht wird, muss ein Punkt von der Seite zum Ursprung und hinterher wieder zurück verschoben werden. Dazu werden drei lineare Transformationen miteinander verkettet, um die Matrix zu erstellen, mit der die Flächenkoordinate transformiert wird: Verschieben zum Ursprung, Drehen und dann das Zurückverschieben.
An dieser Stelle gibt es einen weiteren kleinen Trick: Der Vektor, der die Rotationsachse repräsentiert, muss ebenfalls im Ursprung beginnen. Deshalb wird axisFrom subtrahiert und (axisFrom, axisTo) nach (origin, axisTo-axisFrom) verschoben.
Wenn Ihr Kopf jetzt nicht zu sehr mit Mathematik vollgestopft ist, versuchen Sie es noch mit einem Beispiel (das auf projector.cs basiert), mit dem die Polygone des Polyeders in eine Form transformiert werden, die Sie vom Bildschirm aus zu sehen erwarten:
Matrix projection = 
  Matrix.Translation(-pivot) * 
  Matrix.RotationY(yaw) *
  Matrix.RotationX(angle) * 
  Matrix.Translation(viewPoint) * 
  Geometry.Perspective(5);
Vector3[] p= Vector3.TransformCoordinate(face.Points,foldTransform);
Erinnern Sie sich daran, dass die Form in der horizontalen Ebene gedreht wird, während sie gefaltet wird. Dazu wird ein Punkt auf der Drehpunktachse zum Ursprung verschoben, und dann findet eine Drehung um die senkrechte Achse (Y-Achse) herum durch den Gierwinkel statt. (Wenn Sie schon einmal in der Realität oder in einer Simulation ein Flugzeug geflogen haben, haben Sie sicher mit den beiden anderen möglichen unabhängigen Drehungen Bekanntschaft gemacht, die „Längsneigung“ und „Rolle“ genannt werden.)
Diesmal findet eine Bewegung zum Aussichtspunkt und kein Verschieben zurück zur Startposition statt. Abschließend erfolgt eine Perspektiventransformation, um all das zu verkleinern, das sich weit entfernt auf der Z-Achse befindet. Hier ist viel los, aber dies alles kann in einer einzigen 4x4-Matrix zusammengefasst werden, was ziemlich beeindruckend ist.

Vorschläge zum Weiterforschen
Obwohl sie in Polyhedra nicht verwendet werden, sind in der Emulationsbibliothek auch Quaternionen verfügbar. Sie bieten eine gute Methode, einen Satz von 3D-Drehungen in einer einzigen Drehung zusammenzufassen. Außerdem eignen sie sich sehr gut, um reibungslos von einer Orientierung zu einer anderen zu wechseln. Sie wurden von Sir William Rowan Hamilton (aus meiner Heimatstadt Dublin, Irland) entdeckt, während er die Straße entlangging, und er hat die Gleichung sofort an die Seite der Broome Bridge gekritzelt, damit er sie nicht vergessen würde (besuchen Sie www.maths.tcd.ie/pub/HistMath/People/Hamilton/Quaternions.html). Ich möchte behaupten, dass unser ganzes Graffiti gleichermaßen tiefgründig ist, aber ich fürchte, Sie werden mir nicht glauben.
Mit Quaternionen wird auch ein Problem namens Gimbal Lock vermieden. Dies hat bei der Gyroskopnavigation während der Apollo-Mondmissionen eine wichtige Rolle gespielt und wurde in einem Film über die Mission von Apollo 13 gezeigt.
Es gibt eine Menge anderer Dinge, auf die ich nicht eingegangen bin, z. B. Vektorpunktprodukte und Kreuzprodukte. Aber hoffentlich habe ich Ihnen eine Kostprobe davon gegeben, was mit der Emulation der DirectX-Mathematik möglich ist. Ursprünglich habe ich diese Emulation entwickelt, um auf einem gehosteten Server Transformationen durchzuführen, auf dem ASP.NET ausgeführt wird und auf dem DirectX nicht installiert werden konnte. Möglicherweise finden Sie andere Anwendungen für diese Art von Umgebung.
Polyhedra sollte Ihnen eine Kostprobe davon bieten, wie mit Silverlight ein nützliches, erweiterbares Tool für Weboberflächen bereitgestellt wird, die die Aufmerksamkeit des Benutzers wirklich fesseln und schnell und einprägsam Informationen vermitteln. Sie können kreativ auf Ihrer vorhandenen .NET-Erfahrung aufbauen, und im Unterschied zu JavaScript müssen Sie sich nicht so viele Gedanken über die Anzahl der Codezeilen machen, die auf dem Client ausgeführt werden.

Declan Brennan ist alt genug, um sich an den ersten Mikroprozessor zu erinnern, und er hat sich immer noch nicht daran gewöhnt, seinen eigenen persönlichen Flaschengeist zu haben. Er kann kaum glauben, dass er das Glück hat, in einer Welt zu leben, die nicht von Technologie begrenzt wird, nur durch die Vorstellungskraft. Erfahren Sie mehr über Declan Brennan unter declan.brennan.name.

Page view tracker