Budowa aplikacji w technologii .NET
wykład 13 – Grafika 3D
Grafika 3D w aplikacjach:
• DirectX lub OpenGL
• złożony model programistyczny i wymagania sprzętowe ograniczają ich użycie w tworzeniu interfejsu aplikacji
Grafika 3D w WPF:
• Nowy model grafiki trójwymiarowej.
• Oparcie budowy obiektów 3D o język znaczników i znane elementy: geometrie, pędzle, transformacje, animacje, wiązanie danych.
• Klasy pomocnicze zapewniają dodatkową funkcjonalność, np. obracanie myszą, hit-testy.
• Nie nadaje się do wymagających aplikacji (np. gier).
• Ręczne definiowanie złożonych scen jest mało praktyczne i podatne na błędy.
Podstawy
Cztery podstawowe składniki:
• Viewport – widok, przechowuje zawartość 3D
• Obiekty 3D
• Źródła światła – oświetlają scenę 3D lub jej fragment
• Kamera – zapewnia punkt obserwacyjny
Viewport3D
• Element interfejsu, umieszczany w oknie, a zarazem kontener na scenę 3D.
• Własność Children zawiera obiekty sceny (w tym źródła oświetlenia)
• Własność Camera
Obiekty trójwymiarowe
• Dziedziczą z System.Windows.Media.Media3D.Visual3D
• Visual3D – bazowa dla wszystkich obiektów 3D, możemy z niej dziedziczyć lub użyć ModelVisual3D i zdefiniować geometrię obiektu. Te obiekty wrzucamy do viewportu.
• Geometry3D – analogicznie do Geometry do obrazów 2D – reprezentuje siatkę obiektu. Dziedziczy z niej MeshGeometry3D.
• GeometryModel3D opakowuje geometrię 3D, dodając do niej dane o materiale (kolorze, teksturze), następnie jest używany do wypełnienia Visual3D.
• Transform3D – klasy RotateTransform3D, ScaleTransform3D,
TranslateTransform3D, Transform3DGroup, MatrixTransform3D odpowiadają dwuwymiarowym transformacjom.
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D ...>
</GeometryModel3D.Geometry>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
Geometria
• Tworzenie obiektu 3D rozpoczyna się od budowy jego geometrii. Służy do tego klasa MeshGeometry3D.
• MeshGeometry3D reprezentuje siatkę obiektu (mesh). Siatka złożona jest z trójkątów – to najprostszy sposób definiowania powierzchni (wystarczą trzy punkty) i są powszechnie wykorzystywane w grafice 3D. Wszelkie inne obiekty są definiowane jako złożenie odpowiedniej liczby trójkątów.
Właściwości klasy MeshGeometry3D:
• Positions – kolekcja wszystkich punktów siatki (wierzchołków trójkątów). Często jeden punkt jest wierzchołkiem kilku trójkątów, np: sześcian wymaga 12 trójkątów, ale tylko 8 wierzchołków).
• TriangleIndices – definicja trójkątów. Każdy trójkąt kolekcji jest reprezentowany przez trzy indeksy punktów z kolekcji Positions.
• Normals – to wektory prostopadłe do powierzchni (a raczej prostopadłe do stycznej do powierzchni), definiowane dla wszystkich wierzchołków siatki, są używane do obliczeń oświetlenia.
• TextureCoordinates – określa, jak tekstura 2D ma być mapowana na obiekt 3D.
Dla każdego punktu kolekcji Positions dostarcza punkt 2D.
Jednostki nie są ważne – pozycja kamery i transformacje określą finalny rozmiar obiektu.
Układ współrzędnych – prawoskrętny (oś x w prawo, y do góry, z w kierunku patrzącego).
<MeshGeometry3D Positions="-1,0,0 0,1,0 1,0,0"
TriangleIndices="0,2,1" />
Nie musimy definiować normalnych i tekstur (jeśli wypełnienie to SolidColorBrush).
• Kolejność definiowania punktów nie ma znaczenia, ale znaczenie ma kolejność podawania indeksów wierzchołków. Kolejność musi być przeciwna do ruchu wskazówek zegara – określa to jednoznacznie przód i tył trójkąta (każdy może być wypełniony inną teksturą, często tył w ogóle nie jest rysowany).
GeometryModel3D
• Opakowuje obiekt MeshGeometry3D.
• Posiada trzy właściwości:
◦ Geometry – przyjmuje obiekt reprezentujący kształt (MeshGeometry3D).
◦ Material i BackMaterial – definiują powierzchnię z jakiej zbudowany jest kształt. Określa ona kolor (lub teksturę) obiektu oraz sposób reakcji na oświetlenie.
• WPF udostępnia cztery klasy materiałów:
◦ DiffuseMaterial – płaska, matowa powierzchnia. Rozprasza światło równomiernie we wszystkich kierunkach. Najczęściej używana.
◦ SpecularMaterial – lśniąca, błyszcząca powierzchnia. Naśladuje metal lub szkło. Odbija światło jak lustro (ale nie geometrię).
◦ EmissiveMaterial – świecąca powierzchnia, generuje własne światło (ale nie jest źródłem światła).
◦ MaterialGroup – pozwala na łączenie materiałów (np. SpecularMaterial dodający odblask do DiffuseMaterial).
<DiffuseMaterial
Brush="Yellow"/> <MaterialGroup>
<DiffuseMaterial Brush="Yellow"/>
<SpecularMaterial Brush="White"/>
</MaterialGroup>
<MaterialGroup>
<DiffuseMaterial Brush="Yellow"/>
<EmissiveMaterial Brush="Red"/>
</MaterialGroup>
Przykład:
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D ... />
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial Brush="Yellow"/>
</GeometryModel3D.Material>
</GeometryModel3D>
(Nie określiliśmy BackMaterial, a zatem trójkąt będzie widoczny tylko od przodu.)
Źródła światła
• Uzyskanie cieniowania wymaga dodania do sceny jednego lub kilku źródeł światła.
• Model oświetlenia w WPF korzysta z wielu uproszczeń: oświetlenie obliczane jest osobno dla każdego obiektu (obiekty nie rzucają na siebie cieni ani odbić),
oświetlenie obliczane jest dla wierzchołków i interpolowane na pozostałej powierzchni trójkąta.
• WPF udostępnia cztery klasy oświetlenia:
◦ DirectionalLight – równoległe promienie padające we wskazanym kierunku (jak światło słoneczne).
◦ AmbientLight – światło rozproszone (zazwyczaj używane w połączeniu z innymi źródłami światła).
◦ PointLight – źródło punktowe, emituje światło z pewnego punktu przestrzeni we wszystkich kierunkach.
◦ SpotLight – źródło stożkowe, emituje światło z punktu, w określonym kierunku.
Przykład:
<DirectionalLight Color="White" Direction="-1,0,-1" />
(Długość wektora nie ma znaczenia, tylko kierunek.)
Źródła światła dodawane są do viewportu jak obiekty geometrii:
<Viewport3D>
<Viewport3D.Camera>...</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Color="White"
Direction="-1,0,-1" />
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>...</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
Kamera
• Kamera reprezentuje obserwatora. Znajduje się w pewnym położeniu i jest skierowana w pewnym kierunku. Określa jak scena 3D jest rzutowana na powierzchnię 2D.
• WPF udostępnia trzy klasy kamer:
◦ PerspectiveCamera – rzut perspektywiczny.
◦ OrthographicCamera – rzutowanie równoległe: z punktem rzutowania w nieskończoności.
◦ MatrixCamera – pozwala określić własną macierz rzutowania.
• Należy określić:
◦ położenie kamery (Position)
◦ wektor określający orientację (LookDirection) – najłatwiej określić jako różnicę punktu na który patrzymy i punktu w którym znajduje się kamera (CenterPointOfInterest – CameraPosition)
◦ dodatkowo można podać UpDirection – określa pochylenie kamery
Przykład:
<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera Position="0,0.5,3"
LookDirection="0,0,-1"
UpDirection="0,1,0" />
</Viewport3D.Camera>
...
</Viewport3D>
• Inne przydatne właściwości:
◦ FieldOfView – odpowiednik
ogniskowej, określa kąt widzenia (w OrthographicCamera odpowiednikiem jest Width)
◦ NearPlaneDistance i
FarPlaneDistance – określają minimalną i maksymalną odległość renderowania (domyślnie odpowiednio 0.125 i Double.PositiveInfinity)
Złożone sceny 3D
Sześcian składa się z 8 punktów i 12 trójkątów (po dwa na ścianę).
<MeshGeometry3D Positions="0,0,0 10,0,0 0,10,0 10,10,0
0,0,10 10,0,10 0,10,10 10,10,10"
TriangleIndices="0,2,1 1,2,3 0,4,2 2,4,6 0,1,4 1,5,4 1,7,5 1,3,7 4,5,6 7,6,5 2,6,3 3,6,7" />
<PerspectiveCamera Position="16,16,21"
LookDirection="-3,-3,-4"
UpDirection="0,1,0" />
...
<DirectionalLight Color="White"
Direction="-3,-2,-1" />
...
<DiffuseMaterial Brush="LawnGreen"/>
Normalne są liczone nie dla trójkątów, a dla punktów – rodzi to problemy, gdy punkt jest współdzielony. Definiując osobno 24 punkty (po cztery na ścianę) normalne będą
prostopadłe do każdej ściany.
<MeshGeometry3D Positions="0,0,0 10,0,0 0,10,0 10,10,0 0,0,0 0,0,10 0,10,0 0,10,10 0,0,0 10,0,0 0,0,10 10,0,10 10,0,0 10,10,10 10,0,10 10,10,0 0,0,10 10,0,10 0,10,10 10,10,10 0,10,0 0,10,10 10,10,0 10,10,10"
TriangleIndices="0,2,1 1,2,3 4,5,6 6,5,7 8,9,10 9,11,10 12,13,14 12,15,13 16,17,18 19,18,17 20,21,22 22,21,23" />
Możemy też sami ustawić normalne, np. aby uzyskać efekt płynnego przejścia (dobre do naśladowania gładkich struktur).
<MeshGeometry3D Positions="0,0,0 10,0,0 0,10,0 10,10,0
0,0,10 10,0,10 0,10,10 10,10,10"
TriangleIndices="0,2,1 1,2,3 0,4,2 2,4,6 0,1,4 1,5,4 1,7,5 1,3,7 4,5,6 7,6,5 2,6,3 3,6,7"
Normals="0,1,0 0,1,0 1,0,0 1,0,0 0,1,0 0,1,0 1,0,0 1,0,0" />
Uwaga: ze względu na wydajność, należy ograniczać liczbę oddzielnych siatek oraz obiektów Visual3D. Pomaga w tym Model3DGroup:
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup x:Name="Scene">
<AmbientLight ... />
<DirectionalLight ... />
<DirectionalLight ... />
<Model3DGroup x:Name="Character01">
<Model3DGroup x:Name="Torso">
<GeometryModel3D>...</GeometryModel3D>
</Model3DGroup>
<Model3DGroup x:Name="Head">...
</Model3DGroup>
<Model3DGroup x:Name="Arms">...
</Model3DGroup>
<Model3DGroup x:Name="Legs">...
</Model3DGroup>
</Model3DGroup>
...
</ModelVisual3D.Content>
</ModelVisual3D>
Zaawansowane materiały
• DiffuseMaterial może być rysowany również przy użyciu innych pędzli niż SolidColorBrush (LinearGradientBrush, RadialGradientBrush, ImageBrush, VisualBrush).
• Aby z nich skorzystać, należy dostarczyć informację na temat mapowania pędzla 2D na powierzchnię 3D.
• Służy do tego właściwość MeshGeometry.TextureCoordinates: każdemu położeniu w przestrzeni 3D (wierzchołkowi) przypisuje położenie na teksturze (w przestrzeni 2D).
• Użyjmy tekstury z obrazka:
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="wood.jpg"></ImageBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
• Nałożenie jej na poniższy kształt nie wystarczy, aby stał się on widoczny.
<MeshGeometry3D Positions="0,0,0 10,0,0 0,10,0 10,10,0 0,0,0 0,0,10 0,10,0 0,10,10 0,0,0 10,0,0 0,0,10 10,0,10 10,0,0 10,10,10 10,0,10 10,10,0 0,0,10 10,0,10 0,10,10 10,10,10 0,10,0 0,10,10 10,10,0 10,10,10"
TriangleIndices="0,2,1 1,2,3 4,5,6 6,5,7 8,9,10 9,11,10 12,13,14 12,15,13 16,17,18 19,18,17 20,21,22 22,21,23"/>
• Wytłuszczona ściana (dwa trójkąty) ma wierzchołki o współrzędnych:
(0,0,0) (0,0,10) (0,10,0) (0,10,10)
• Przypisujemy im odpowiednie współrzędne na teksturze (względne, zatem z przedziału [0,1]):
(1,1) (0,1) (1,0) (0,0)
• Podobnie postępujemy z pozostałymi:
<MeshGeometry3D ...
TextureCoordinates="0,0 0,1 1,0 1,1 ...
1,1 0,1 1,0 0,0 0,0 1,0 0,1 1,1 1,1 0,0 0,1 1,0 1,1 0,1 1,0 0,0 1,1 0,1 1,0 0,0"/>
W podobny sposób możemy używać innych pędzli, w tym VisualBrush lub pędzli animowanych.
Przykład – tworzenie geometrii w kodzie:
<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera Position="0,2.5,2.5"
LookDirection="0,-1,-1" UpDirection="0,1,0" />
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Color="White"
Direction="-2,-2,-1" />
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D x:Name="mymodel">
<GeometryModel3D.Material>
...
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
A w kodzie:
mymodel.Geometry = Create(50, 50, 1);
// za: http://blogs.msdn.com/b/wpf3d/
public static MeshGeometry3D Create(int tDiv, int pDiv, double radius) {
double dt = 2*Math.PI / tDiv;
double dp = Math.PI / pDiv;
MeshGeometry3D mesh = new MeshGeometry3D();
for (int pi = 0; pi <= pDiv; pi++) {
double phi = pi * dp;
for (int ti = 0; ti <= tDiv; ti++) {
double theta = ti * dt;
mesh.Positions.Add(GetPosition(theta, phi, radius));
mesh.Normals.Add(GetNormal(theta, phi));
mesh.TextureCoordinates.Add(GetTextureCoordinate(theta, phi));
} }
for (int pi = 0; pi < pDiv; pi++) {
for (int ti = 0; ti < tDiv; ti++) {
int x0 = ti;
int x1 = (ti + 1);
int y0 = pi * (tDiv + 1);
int y1 = (pi + 1) * (tDiv + 1);
mesh.TriangleIndices.Add(x0 + y0);
mesh.TriangleIndices.Add(x0 + y1);
mesh.TriangleIndices.Add(x1 + y0);
mesh.TriangleIndices.Add(x1 + y0);
mesh.TriangleIndices.Add(x0 + y1);
mesh.TriangleIndices.Add(x1 + y1);
} }
mesh.Freeze();
return mesh;
}
private static Point3D GetPosition(double theta, double phi, double radius) {
double x = radius * Math.Sin(theta) * Math.Sin(phi);
double y = radius * Math.Cos(phi);
double z = radius * Math.Cos(theta) * Math.Sin(phi);
return new Point3D(x, y, z);
}
private static Vector3D GetNormal(double theta, double phi) {
return (Vector3D)GetPosition(theta, phi, 1.0);
}
private static Point GetTextureCoordinate(double theta, double phi) {
Point p = new Point(theta / (2 * Math.PI), phi / (Math.PI));
return p;
}
• Niestety, stworzenie złożonej sceny 3D w XAMLu nie jest proste.
• Istnieją gotowe narzędzia do budowania złożonych scen 3D w WPF, np.:
◦ ZAM 3D
▪ http://www.erain.com/Products/ZAM3D
◦ Blender
▪ http://blender.org
▪ http://codeplex.com/xamlexporter
◦ Wtyczki do profesjonalnych programów 3D (np. Maya, LightWave)
◦ http://blogs.msdn.com/mswanson/articles/WPFToolsAndControls.aspx
Animacje 3D
• Najwygodniejszy sposób animowania sceny 3D to transformacje.
• Transformować możemy:
◦ Model3D lub Model3DGroup (pojedynczą siatkę)
◦ ModelVisual3D (całą scenę)
◦ źródło światła
◦ kamerę
Przykład – obracający się sześcian
• Definiujemy transformację obrotu:
<ModelVisual3D.Transform>
<RotateTransform3D CenterX="5" CenterZ="5">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotate" Axis="0 1 0" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
• Teraz możemy dodać slidera:
<Slider Grid.Row="1" Minimum="0" Maximum="360"
Orientation="Horizontal"
Value="{Binding ElementName=rotate, Path=Angle}" />
• Lub animację:
<Button Grid.Row="1">
<Button.Content>Go!</Button.Content>
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation
Storyboard.TargetName="rotate"
Storyboard.TargetProperty="Angle"
To="360" Duration="0:0:2.5"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Przemieszczając (TranslateTransform) kamerę wzdłuż ścieżki (AnimationUsingPath) lub według klatek (AnimationUsingKeyFrames) można uzyskać efekt poruszającego się obserwatora.
<Storyboard>
<Point3DAnimationUsingKeyFrames Storyboard.TargetName="kamera"
Storyboard.TargetProperty="Position">
<LinearPoint3DKeyFrame Value="21,-6,-6" KeyTime="0:0:3"/>
<LinearPoint3DKeyFrame Value="-6,-6,-11" KeyTime="0:0:6"/>
<LinearPoint3DKeyFrame Value="-11,16,16" KeyTime="0:0:9"/>
<LinearPoint3DKeyFrame Value="16,16,21" KeyTime="0:0:12"/>
</Point3DAnimationUsingKeyFrames>
<Vector3DAnimationUsingKeyFrames Storyboard.TargetName="kamera"
Storyboard.TargetProperty="LookDirection">
<LinearVector3DKeyFrame Value="-20,15,15" KeyTime="0:0:3"/>
<LinearVector3DKeyFrame Value="15,15,20" KeyTime="0:0:6"/>
<LinearVector3DKeyFrame Value="20,-15,-15" KeyTime="0:0:9"/>
<LinearVector3DKeyFrame Value="-15,-15,-20" KeyTime="0:0:12"/>
</Vector3DAnimationUsingKeyFrames>
</Storyboard>
Wskazówka: warto dodawać komplet transformacji (i nadawać im nazwy przy pomocy x:Name), by następnie móc animować wybrane. W przykładzie umieszczono dwie translacje, bo przesunięcie przed i po obrocie działa inaczej.
<Model3DGroup.Transform>
<Transform3DGroup>
<TranslateTransform3D
OffsetX="0" OffsetY="0" OffsetZ="0"/>
<ScaleTransform3D ScaleX="1" ScaleY="1" ScaleZ="1"/>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Angle="0" Axis="0 1 0"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<TranslateTransform3D
OffsetX="0" OffsetY="0" OffsetZ="0"/>
</Transform3DGroup>
</Model3DGroup.Transform>
Wydajność
• Renderowanie scen 3D wymaga o wiele większej pracy niż renderowanie scen 2-D
• W przypadku animacji sceny 3D, WPF próbuje odświeżać zmienione fragmenty 60 razy na sekundę
• W zależności od skomplikowania sceny, może się zdarzyć, że zasoby pamięciowe karty graficznej się skończą, co doprowadzi do spadku liczby wyświetlanych ramek na sekundę
W jaki sposób poprawić wydajność renderowanych scen 3D?
• Jeżeli nie ma potrzeby przycinania zawartości, która wystaje poza Viewport, należy ustawić właściwość Viewport3D.ClipToBounds na false
• Jeżeli nie ma potrzeby sprawdzania kliknięć w scenie 3-D, należy ustawić właściwość Viewport3D.IsHitTestVisiblena false
• Jeżeli Viewport jest większy niż potrzeba, należy go zmniejszyć
• Jeśli niewygładzone krawędzie kształtów 3D nam nie przeszkadzają – można ustawić w Viewporcie własność dołączoną RenderOptions.EdgeMode na Aliased.
Tworzenie wydajnych siatek i modeli
• Lepiej stworzyć pojedynczą bardziej skomplikowaną siatkę niż kilka prostych
• Jeżeli istnieje potrzeba wykorzystania różnych materiałów dla jednej siatki, należy zdefiniować obiekt MeshGeometry jednokrotnie (jako zasób), a następnie używać go do tworzenia wielu obiektów GeometryModel3D
• Kiedykolwiek to możliwe, należy otaczać grupę obiektów GeometryModel3D obiektem Model3DGroup i umieścić ten obiekt w pojedynczym obiekcie ModelVisual3D
• Nie należy definiować materiału tylnego w przypadku, gdy użytkownik nigdy nie będzie widział tylnej części obiektu
• Lepiej używać pędzli typu Solid, Gradient, Image niż DrawingBrush i VisualBrush
• Używając DrawingBrush lub VisualBrush do odrysowania statycznej zawartości należy ustawić w pędzlu dołączoną właściwość RenderOptions.CachingHint na wartość Cache
Hit Testing
• Rozpoznawania obszaru, który kliknięto (lub wskazano) myszą.
• Możemy to zrobić na jeden z dwóch sposobów:
◦ Obsłużyć zdarzenia myszy w viewporcie i posługując się metodą
VisualTreeHelper.HitTest() zlokalizować obiekt, którego dotyczy zdarzenie.
◦ Zastąpić obiekt ModelVisual3D obiektem ModelUIElement3D, który posiada obsługę zdarzeń.
Sposób pierwszy
• Obsługę zdarzenia dodajemy do viewportu.
<Viewport3D MouseDown="Viewport3D_MouseDown">
...
</Viewport3D>
• Sprawdzamy w jaki ModelVisual3D kliknięto.
private void Viewport3D_MouseDown(...) {
Viewport3D viewport = (Viewport3D)sender;
Point location = e.GetPosition(viewport);
HitTestResult hitResult =
VisualTreeHelper.HitTest(viewport, location);
if (hitResult != null && hitResult.VisualHit == kostka) {
// Kliknięto kostkę }
}
• Jeśli to informacja nie wystarczy – możemy zlokalizować właściwy element GeometryModel3D lub MeshGeometry3D:
RayMeshGeometry3DHitTestResult meshHitResult =
hitResult as RayMeshGeometry3DHitTestResult;
if (meshHitResult != null) {
if (meshHitResult.ModelHit == ...) ...
if (meshHitResult.MeshHit == ...) ...
// punkt 3D w który kliknięto meshHitResult.PointHit...
}
Drugi sposób
• Pierwszy sposób jest nieco żmudny i wymaga szukania w kodzie elementu, którego dotyczy zdarzenie.
• Innym rozwiązaniem jest zastąpienie obiektu ModelVisual3D obiektem z hierarchii UIElement3D: ModelUIElement3D lub ContainerUIElement3D.
• Dodają one do elementów 3D obsługę myszy, klawiatury, etc. (ale nie layouty).
<Viewport3D x:Name="viewport">
<Viewport3D.Camera>...</Viewport3D.Camera>
<ModelUIElement3D MouseDown="element_MouseDown">
<ModelUIElement3D.Model>
<Model3DGroup>...
</Model3DGroup>
</ModelUIElement3D.Model>
</ModelUIElement3D>
</Viewport3D>
• Jeśli chcemy umieścić kilka elementów umożliwiających interakcję, powinniśmy dodać kilka ModelUIElement3D w jednym ContainerUIElement3D (poza
obiektami ModelUIElement3D może on przechowywać również zwykłe ModelVisual3D).
Umieszczanie elementów interfejsu na obiektach 3D
• Pierwszym sposobem jest wykorzystanie VisualBrush jako tekstury:
◦ kopiuje on tylko wygląd elementu,
◦ brak interakcji z elementem.
• Klasa Viewport2DVisual3D pozwala umieścić element na powierzchni 3D (zgodnie z mapowaniem teksturowania):
◦ taki element w pełni zachowuje swoją funkcjonalność.
• Usuwamy jedną ze ścian sześcianu – (12,13,14) (12,15,13).
• Zamiast niej dodajemy Viewport2DVisual3D do viewportu.
• Geometria to nasza usunięta ściana.
• Tekstura składa się z tła (imitacja drewna) i fragmentu interfejsu (formularza).
• Jest on w pełni funkcjonalny.
<Viewport2DVisual3D>
<Viewport2DVisual3D.Geometry>
<MeshGeometry3D
Positions="10,0,0 10,10,10 10,0,10 10,10,0"
TriangleIndices="0,1,2 0,3,1"
TextureCoordinates="1,1 0,0 0,1 1,0" />
</Viewport2DVisual3D.Geometry>
<Viewport2DVisual3D.Material>
<MaterialGroup>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="wood.jpg"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
<DiffuseMaterial
Viewport2DVisual3D.IsVisualHostMaterial="True" />
</MaterialGroup>
</Viewport2DVisual3D.Material>
<Viewport2DVisual3D.Visual>
<Border CornerRadius="10" BorderBrush="DarkGoldenrod"
BorderThickness="1">
<StackPanel Margin="10">
<TextBlock Margin="3">Wprowadź dane</TextBlock>
<TextBox Margin="3"></TextBox>
<Button Margin="3">OK</Button>
</StackPanel>
</Border>
</Viewport2DVisual3D.Visual>
</Viewport2DVisual3D>
• http://3dtools.codeplex.com/ – biblioteka narzędzi, oferująca między innymi dekorator viewporta zapewniający poruszanie kamerą przy pomocy myszy:
<Window ...
xmlns:tools="clr-namespace:_3DTools;assembly=3DTools"
Title="3D" Height="300" Width="300">
<Grid>
<tools:TrackballDecorator>
<Viewport3D>
...
</Viewport3D>
</tools:TrackballDecorator>
</Grid>
</Window>
drukowanie
Podstawowym punktem wyjścia jest dla nas klasa PrintDialog. Nie tylko pokazuje ona opcje drukowania, ale również umożliwia uruchomienie wydruku:
• PrintVisual() – do drukowania elementów dziedziczących z System.Windows.Media.Visual
• PrintDocument() – do drukowania dokumentów (klasa DocumentPaginator) Drukowanie elementu
• PrintDialog.PrintVisual() pozwala wydrukować dokładnie to, co widać na ekranie.
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true) {
printDialog.PrintVisual(canvas, "A Simple Drawing");
}
• Pierwszy parametr – element do wydrukowania.
• Drugi parametr – string identyfikujący zadanie drukarki.
<Window ...>
<Window.CommandBindings>
<CommandBinding Command="Print" Executed="print"/>
</Window.CommandBindings>
<Canvas Name="canvas">
<Path Fill="Yellow" Stroke="Blue"
Canvas.Top="30" Canvas.Left="20" >
<Path.Data>
<GeometryGroup>
<RectangleGeometry Rect="0,0 100,60"/>
<EllipseGeometry Center="90,10"
RadiusX="40" RadiusY="30"/>
</GeometryGroup>
</Path.Data>
</Path>
</Canvas>
</Window>
Nie ma tu zbyt dużej kontroli nad wydrukiem (ustawienia marginesu, wyrównania, podziału na strony, skalowania). Rozmiar na wydruku jest taki sam, jak rozmiar w oknie.
Można sobie z tym poradzić dodając transformacje i włączając dopasowanie do rozmiaru strony:
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true) {
// Magnify the output by a factor of 5.
canvas.LayoutTransform = new ScaleTransform(5, 5);
// Define a margin.
int pageMargin = 5;
// Get the size of the page.
Size pageSize = new Size(printDialog.PrintableAreaWidth - pageMargin * 2, printDialog.PrintableAreaHeight - 20);
// Trigger the sizing of the element.
canvas.Measure(pageSize);
canvas.Arrange(new Rect(pageMargin, pageMargin,
pageSize.Width, pageSize.Height));
// Print the element.
printDialog.PrintVisual(canvas, "A Scaled Drawing");
// Remove the transform.
canvas.LayoutTransform = null;
}
Dokument XPS może być używany jako podgląd wydruku: aplikacja drukuje dokument do pliku XPS, aby go później wyświetlić w oknie. Można to też wykorzystać do asynchronicznego drukowania.
(Uwaga: należy pamiętać o dodaniu assembli ReachFramework i System.Printing w References)
Należy stworzyć writera (można użyć metody Path.GetTempFileName() aby uzyskać ścieżkę do pliku tymczasowego):
XpsDocument xpsDocument = new XpsDocument("filename.xps", FileAccess.ReadWrite);
XpsDocumentWriter writer =
XpsDocument.CreateXpsDocumentWriter(xpsDocument);
Następnie metody Write() i WriteAsync() umożliwiają wydrukowanie obiektów graficznych (Visual) lub dokumentów (DocumentPaginator).
DocumentViewer docViewer = new DocumentViewer();
writer.Write(canvas);
docViewer.Document = xpsDocument.GetFixedDocumentSequence();
xpsDocument.Close();
File.Delete("filename.xps");
Można stworzyć i wyświetlić okienko z podglądem:
Window window = new Window();
window.Content = docViewer;
window.Width = 300;
window.Height = 300;
window.Title = "podgląd wydruku";
window.Show();
Drukowanie dokumentu
Metoda PrintDocument() z PrintDialog oferuje drukowanie dokumentu. Przyjmuje ona parametr typu DocumentPaginator (zadaniem tej klasy jest dzielenie dokumentu na strony – obiekty klasy DocumentPage – i udostępnianie ich).
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true) {
printDialog.PrintDocument(
((IDocumentPaginatorSource)docReader.Document).DocumentPaginator, "A Flow Document");
}
Jeśli dokument jest zawarty w kontenerze RichTextBox lub FlowDocumentScrollViewer, paginacja zostanie wykonana prawidłowo. Jeśli jednak drukujemy z
FlowDocumentPageViewer lub FlowDocumentReader, musimy powtórzyć paginację, aby dostosować ją do strony, a nie okna. (Podobnie jest z kolumnami.) (Oczywiście warto zachować te wartości, by przywrócić je, gdy wrócimy do okienka.)
FlowDocument doc = docReader.Document;
doc.PageHeight = printDialog.PrintableAreaHeight;
doc.PageWidth = printDialog.PrintableAreaWidth;
printDialog.PrintDocument(
((IDocumentPaginatorSource)doc).DocumentPaginator, "A Flow Document");
Kontrola nad paginacją na wydruku
Możemy uzyskać kontrolę nad paginacją pisząc własną klasę DocumentPaginator.
Nie musimy robić paginacji ręcznie (można to zadanie zostawić paginatorowi z dokumentu), ale możemy np. dodać nagłówek i stopkę do każdej strony.
public class HeaderedFlowDocumentPaginator : DocumentPaginator {
// The real paginator (which does all the pagination work).
private DocumentPaginator flowDocumentPaginator;
// Store the FlowDocument paginator from the given document.
public HeaderedFlowDocumentPaginator(FlowDocument document) {
flowDocumentPaginator =
((IDocumentPaginatorSource)document).DocumentPaginator;
}
public override bool IsPageCountValid
{ get { return flowDocumentPaginator.IsPageCountValid; } } public override int PageCount
{ get { return flowDocumentPaginator.PageCount; } } public override Size PageSize
{ get { return flowDocumentPaginator.PageSize; } set { flowDocumentPaginator.PageSize = value; } } public override IDocumentPaginatorSource Source
{ get { return flowDocumentPaginator.Source; } } public override DocumentPage GetPage(int pageNumber) { ... }
Gdy pobierana jest strona, możemy dodać własne elementy:
public override DocumentPage GetPage(int pageNumber) {
// Pobierz stronę
DocumentPage page = flowDocumentPaginator.GetPage(pageNumber);
// Opakuj ją w Visual
ContainerVisual newVisual = new ContainerVisual();
newVisual.Children.Add(page.Visual);
// Stwórz nagłówek
DrawingVisual header = new DrawingVisual();
using (DrawingContext dc = header.RenderOpen()) {
Typeface typeface = new Typeface("Times New Roman");
FormattedText text = new FormattedText("Page " +
(pageNumber + 1).ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 14, Brushes.Black);
dc.DrawText(text, new Point(96 * 0.25, 96 * 0.25));
}
// Dodaj nagłówek do Visual newVisual.Children.Add(header);
// Stwórz i zwróć nową stronę dokumentu
DocumentPage newPage = new DocumentPage(newVisual);
return newPage;
}
Aby modyfikować Visual musimy usunąć dokument z kontenera na czas drukowania:
FlowDocument document = docReader.Document;
docReader.Document = null;
HeaderedFlowDocumentPaginator paginator =
new HeaderedFlowDocumentPaginator(document);
printDialog.PrintDocument(paginator, "A Headered Flow Document");
docReader.Document = document;
Drukowanie zakresu stron
Własność PrintDialog.UserPageRangeEnabled na true umożliwi wybór zakresu przez użytkownika (Selection i Current Page są nieobsługiwane). Możemy też ustawić MaxPage i MinPage, aby nadać mu ograniczenie. Następnie odczytujemy własność
PageRangeSelection. Jeśli ma wartość UserPages, to możemy odczytać
PageRange.PageFrom i PageRange.PageTo. Wykorzystanie tej informacji należy do nas.