PwT.N W13
Transkrypt
PwT.N W13
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. 1/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 2/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> 3/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. 4/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). 5/49 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 <DiffuseMaterial Brush="Yellow"/> <MaterialGroup> <MaterialGroup> <DiffuseMaterial <DiffuseMaterial Brush="Yellow"/> Brush="Yellow"/> <SpecularMaterial <EmissiveMaterial Brush="White"/> Brush="Red"/> </MaterialGroup> </MaterialGroup> 7/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.) 8/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. 9/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> 10/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 11/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) 12/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"/> 13/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" /> 14/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" /> 15/49 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 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> 17/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) 18/49 • Podobnie postępujemy z pozostałymi: <MeshGeometry3D ... TextureCoordinates="0,0 1,1 0,0 1,1 1,1 1,1 0,1 0,1 1,0 0,0 0,1 0,1 1,0 1,0 0,1 0,1 1,0 1,0 1,1 ... 0,0 1,1 1,0 0,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 // 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)); } } 21/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; } 22/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; } 23/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 24/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> 25/49 • 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 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> 27/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> 28/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. 29/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 30/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ń. 31/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ę } } 32/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... } 33/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). 34/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. <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 <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> 36/49 37/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> 38/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. 39/49 <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 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; } 42/49 43/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"); 44/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(); 45/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"); 46/49 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 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; } 48/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. 49/49