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

Podobne dokumenty