Budowa aplikacji w technologii.net wykład 13 Grafika 3D 1/49 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.
2/49 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
3/49 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>
4/49 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.
5/49 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). 6/49
7/49 <DiffuseMaterial Brush="Yellow"/> <MaterialGroup> <DiffuseMaterial Brush="Yellow"/> <SpecularMaterial Brush="White"/> </MaterialGroup> <MaterialGroup> <DiffuseMaterial Brush="Yellow"/> <EmissiveMaterial Brush="Red"/> </MaterialGroup>
8/49 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.)
9/49 Ź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.
10/49 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>
11/49 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
12/49 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)
13/49 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"/>
14/49 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" />
15/49 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> 16/49
17/49 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>
18/49 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. 19/49
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); 20/49
21/49 // 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));
22/49 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;
23/49 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;
24/49 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
25/49 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> 26/49
27/49 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>
28/49 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>
29/49 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.
30/49 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
31/49 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ń.
32/49 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ę } }
33/49 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... }
34/49 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).
<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> 35/49 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.
36/49 <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>
37/49
38/49 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>
39/49 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> 40/49
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. 41/49
42/49 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; }
43/49
44/49 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");
45/49 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();
46/49 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) {... } } 47/49
48/49 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; }
49/49 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.