Artykuł jest poświęcony dodawaniu napisów do scen 3D, które są tworzone przy pomocy Windows Presentation Foundation (WPF). Artykuł opisuje dwa podejścia: napis&ów trój- i dwu- wymiarowych.
Jakiś czas temu postanowiłem
rozpocząć moją przygodę grafiką 3D generowaną za pomocą Windows
Presentation Foundation (WPF). Muszę przyznać, że biorąc pod uwagę
prostotę z jaką można tworzyć grafikę 3D jest to na prawdę dobre
narzędzie. Bez większych problemów opanowałem generowaną przy pomocy
trójkątów w przestrzeni grafikę, tym bardziej, że bardzo wiele
artykułów opisuje właśnie te elementy. Schody pojawiły się w momencie,
gdy postanowiłem dodać do moich grafik 3D jakieś napisy. Dlatego
właśnie tworzeniu napisów w scenach WPF 3D chciałbym poświęcić
niniejszy artykuł.
Dwa podejścia przy tworzeniu napisów w scenach WPF 3D
Naszym celem jest więc umieszczenie jakichś napisów w tworzonej scenie 3D. Możemy tutaj wyróżnić dwa podejścia:
- Tworzenie napisów 3D
- Tworzenie napisów 2D w oparciu o lokalizacje 3D
Podejście pierwsze wymaga stworzenie (narysowanie) obiektu w
przestrzeni a następnie pokrycie go napisem. W tym celu tworzony jest
pędzel z napisem, którym maluje się obiekt w przestrzeni. Przykładowa
scena zawierająca tak stworzone napisy znajduje się na poniższym
rysunku:
Podejście
drugie wymaga na warstwie zawierającej Viweport3D umieszczenie warstwy zawierającej obiekt typu Canvas, który
jest przeżroczysty, na nim umieszczamy TexBlock'i w
odpowiedniej lokalizacji, która związana jest z pewnym
punktem w przestrzeni. Podejście takie przedstawione jest na rysunku poniżej:

Oczywiście, każde rozwiązanie ma swoje wady i zalety. Etykiety 3D,
bazujące na pierwszym podejściu, są bardziej przydatne, gdy tekst jest
elementem sceny. Napisy tworzone przy pomocy drugiego podejścia, są
zwykle bardziej czytelne, ale bardziej odróżniają się od sceny i
obsługa ich jest bardziej skomplikowana. Rysunek poniżej przedstawia
porównanie obydwu metod:

Rozwiązanie w praktyce
Przykładowy kod dołączony do artykułu
rysuje układ współrzędnych. Scena może być obracana i położenie kamery
może być zmieniane przy użyciu suwaków. Pola wyborów znajdujące się w
lewym panelu okna aplikacji pozwalają na włączanie i wyłączanie
widzialności poszczególnych elementów sceny (w szczególności etykiet 3D
i 2D).
Aplikacja może się na pierwszy rzut oka wydawać skomplikowana, jednak
są w niej wykorzystane proste elementy WPF 3D i każdy powinien
zrozumieć o co w niej chodzi. W tym artykule chciałbym się skupić na tworzeniu napisów i dwóch podejściach opisanych powyżej.
Napisy przestrzenne (WPF 3D)
Aby zobaczyć jak tworzyć etykiety 3D, zobaczymy jak działa funkcja, która to realizuje:
CreateTextLabel3D. Ta funkcja jest odpowiedzialna za wytworzenie obiektu typu
ModelVisual3D , zawierającego etykiety 3D. Funkcja ta będzie miała następującą deklarację:
[Kod C#] /// <summary> /// Creates a ModelVisual3D containing a text label. /// </summary> /// <param name="text">The string to be drawn</param> /// <param name="textColor">The color of the text.</param> /// <param name="isDoubleSided">Visible from both sides?</param> /// <param name="height">Height of the characters</param> /// <param name="basePoint">The base point of the label</param> /// <param name="isBasePointCenterPoint">if set to <c>true</c> the base point /// is center point of the label.</param> /// <param name="vectorOver">Horizontal direction of the label</param> /// <param name="vectorUp">Vertical direction of the label</param> /// <returns>Suitable for adding to your Viewport3D</returns> /// <remarks>Two vectors: vectorOver and vectorUp are creating the surface /// on which we are drawing the text. Both those vectors are used for /// calculation of label size, so it is reasonable that each co-ordinate /// should be 0 or 1. e.g. [1,1,0] or [1,0,1], etc...</remarks> public static ModelVisual3D CreateTextLabel3D( string text, Brush textColor, bool isDoubleSided, double height, Point3D basePoint, bool isBasePointCenterPoint, Vector3D vectorOver, Vector3D vectorUp);
|
Jak ona działa?- W pierwszym kroku tworzymy TextBlock , który zawiera naszą etykietę.
[Kod C#]
TextBlock textblock = newTextBlock(newRun(text)); textblock.Foreground = textColor; // setting the text color textblock.FontFamily = newFontFamily("Arial"); // setting the font to be used
|
- W drugim kroku, tworzymy pędzel i materiał TextBlock , który zawiera stworzony w poprzednim kroku.
[Kod C#]
DiffuseMaterial mataterialWithLabel = new DiffuseMaterial(); // Allows the application of a 2-D brush, // like a SolidColorBrush or TileBrush, to a diffusely-lit 3-D model. // we are creating the brush from the TextBlock mataterialWithLabel.Brush = new VisualBrush(textblock);
|
- Teraz czas jest na przygotowanie objektu 3D, który zostanie pokryty tekstem
[Kod C#]
//calculation of text width (assumming that characters are square): double width = text.Length * height; // we need to find the four corners // p0: the lower left corner; p1: the upper left // p2: the lower right; p3: the upper right Point3D p0 = basePoint; // when the base point is the center point we have to set it up in different way if(isBasePointCenterPoint) p0 = basePoint - width / 2 * vectorOver - height / 2 * vectorUp; Point3D p1 = p0 + vectorUp * 1 * height; Point3D p2 = p0 + vectorOver * width; Point3D p3 = p0 + vectorUp * 1 * height + vectorOver * width; // we are going to create object in 3D now: // this object will be painted using the (text) brush created before // the object is rectangle made of two triangles (on each side). MeshGeometry3D mg_RestangleIn3D = new MeshGeometry3D(); mg_RestangleIn3D.Positions = new Point3DCollection(); mg_RestangleIn3D.Positions.Add(p0); // 0 mg_RestangleIn3D.Positions.Add(p1); // 1 mg_RestangleIn3D.Positions.Add(p2); // 2 mg_RestangleIn3D.Positions.Add(p3); // 3 // when we want to see the text on both sides: if (isDoubleSided) { mg_RestangleIn3D.Positions.Add(p0); // 4 mg_RestangleIn3D.Positions.Add(p1); // 5 mg_RestangleIn3D.Positions.Add(p2); // 6 mg_RestangleIn3D.Positions.Add(p3); // 7
} mg_RestangleIn3D.TriangleIndices.Add(0); mg_RestangleIn3D.TriangleIndices.Add(3); mg_RestangleIn3D.TriangleIndices.Add(1); mg_RestangleIn3D.TriangleIndices.Add(0); mg_RestangleIn3D.TriangleIndices.Add(2); mg_RestangleIn3D.TriangleIndices.Add(3); // when we want to see the text on both sides: if (isDoubleSided) { mg_RestangleIn3D.TriangleIndices.Add(4); mg_RestangleIn3D.TriangleIndices.Add(5); mg_RestangleIn3D.TriangleIndices.Add(7); mg_RestangleIn3D.TriangleIndices.Add(4); mg_RestangleIn3D.TriangleIndices.Add(7); mg_RestangleIn3D.TriangleIndices.Add(6);
}
|
- W kolejnym kroku pokrywamy stworzony przed chwilą obiekt 3D używając materiału stworzonego na początku.
[Kod C#] // texture coordinates must be set to // stretch the TextBox brush to cover // the full side of the 3D label. mg_RestangleIn3D.TextureCoordinates.Add(newPoint(0, 1)); mg_RestangleIn3D.TextureCoordinates.Add(newPoint(0, 0)); mg_RestangleIn3D.TextureCoordinates.Add(newPoint(1, 1)); mg_RestangleIn3D.TextureCoordinates.Add(newPoint(1, 0)); // when the label is double sided: if (isDoubleSided) {
mg_RestangleIn3D.TextureCoordinates.Add(newPoint(1, 1)); mg_RestangleIn3D.TextureCoordinates.Add(newPoint(1, 0)); mg_RestangleIn3D.TextureCoordinates.Add(newPoint(0, 1)); mg_RestangleIn3D.TextureCoordinates.Add(newPoint(0, 0));
}
|
- Teraz jest czas na stworznie obiektu
ModelVisual3D z napisem
[Kod C#] ModelVisual3D result = newModelVisual3D();// we are setting the content: // our 3D rectangle object covered with materila that is made of label
// (TextBox with text) result.Content = newGeometryModel3D(mg_RestangleIn3D, mataterialWithLabel); return result;
|
Napisy płaskie (WPF 2D)
To
podejście jest bardziej skomplikowane od poprzedniego, dlatego trudno
przygotować funkcję, która jak to miało miejsce w poprzednim przypadku
wykonywała wszystkie czynności.
Pierwszym krokiem jest stworzenie dwóch nakładających się warstw:
- Viewport3D jako warstwy spodniej
- Canvas jako przeźroczystej warstwy nałożonej
Kod XAML realizujący dwie nakładające się warstwy może wyglądać następująco:
[Kod XAML]
<Grid
ClipToBounds="True"> <Viewport3D
Name="mainViewport"
ClipToBounds="True" Grid.Column="0" Grid.Row="0"> <Viewport3D.Camera> <PerspectiveCamera
FarPlaneDistance="100"
LookDirection="-11,-10,-9"
UpDirection="0,1,0"
NearPlaneDistance="1"
Position="11,10,9"
FieldOfView="70"
/> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <DirectionalLight Color="White" Direction="-2,-3,-1"
/> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> <Canvas
Name="mainViewportCanvas"
ClipToBounds="True" Grid.Column="0" Grid.Row="0"></Canvas> </Grid>
|
Kolejnym zadaniem jest lokalizacja punktu ze sceny 3D na nałożonej warstwie typu Canvas. Do transformacji lokalizacji można użyć funkcję podobną do poniższej:
Kod C#]
public static Point
Get2DPoint(Point3D
p3d, Viewport3D
vp) { bool
TransformationResultOK; Viewport3DVisual
vp3Dv = VisualTreeHelper.GetParent(
vp.Children[0]) as Viewport3DVisual; Matrix3D
m = MathUtils.TryWorldToViewportTransform(vp3Dv,
out
TransformationResultOK); if
(!TransformationResultOK) return new Point(0,
0); Point3D
pb = m.Transform(p3d); Point
p2d = new Point(pb.X,
pb.Y); return
p2d; }
|
Zauważmy, że ta funkcja używa funkcji TryWorldToViewportTransform z pakietu 3D Tools (dostępnego tutaj): _3DTools.MathUtils.TryWorldToViewportTransform.
Kolejnym krokiem jest utworzenie i lokalizacja obiektu TextBlock:
Kod C#]
UIElementIModelVisual3D.GetUIElement(ModelVisual3DFilter
FilterSettings, Viewport3D
DestinationViewport3D) { if
(FilterSettings.Texts2D)
{ TextBlock
tb = new TextBlock();
tb.Text = Description; Point
p2d = Panel3DMath.Get2DPoint(this.Point3D,
DestinationViewport3D); Canvas.SetTop(tb,
p2d.Y); Canvas.SetLeft(tb,
p2d.X); return
tb;
} else return new UIElement(); }
|
Ostatecznie dodajemy
TextBlock (utworzonego w poprzednim kroku) do naszego obiektu canvas.
[Kod C#]
this.mainViewportCanvas.Children.Add(element.GetUIElement(filter,mainViewport));
|
Podsumowanie
Mam nadzieję, że czytelników zainteresowały moje dwa podejścia do
tworzenia napisów na potrzeby scen 3D tworzonych w WPF. Artykuł
powinien pojawić się również na moim blogu, zapraszam do odwiedzania mojego bloga: http://maciej-progtech.blogspot.com/ , na którym można znaleźć również inne ciekawe informacje.
Historia tego artykułu
- 2009.03.04 - Powstanie angielskiej wersji artykułu (opublikowany na CodeProject )
- 2009.03.09 - Powstanie polskiej wersji artykułu
Załączniki: