Time has been short for the last fortnight, so haven't made as much progress as I'd like on this project. This short post covers one topic: adding labels to the plot from the last post. Here's an example of the result, using a 'pretty' function from F# for Visualization:
To render text in a Viewport3D, this code follows roughly the same approach described by Eric Sink:
- Calculate the extents of the label in 3D space
- Create a rectangular "billboard" at the desired location
- "Texture Map" the desired text onto the billboard using a VisualBrush and a TextBlock
Positioning the text
To align the center of the label with the grid lines, the width of the text needs to be estimated. This can be done using GlyphTypeface, which provides the advance widths for individual characters:
module Wpf = /// Attempts to retrieve a GlyphTypeface for the specified family, /// style and weight. If no matching typeface can be obtained then /// this raises an invalid argument exception. let GetTypeface (family:FontFamily)(style:FontStyle)(weight:FontWeight)= let t = Typeface(family, style, weight, FontStretches.Normal) let success, typeface = t.TryGetGlyphTypeface() if not success then invalidArg "family" "Cannot find a GlyphTypeface for the specified font family, style and weight" typeface /// Measures the length of a line of text in the specified typeface, /// assuming a size of 1.0. Callers can scale the resulting width /// according to the size they are using. let MeasureLine (text:string) (f:GlyphTypeface) = Seq.fold (fun w (c:char) -> w + f.AdvanceWidths.[f.CharacterToGlyphMap.[int(c)]]) 0.0 text
Creating the billboard
The position of the label in three dimensional space is done using its center point and two vectors - one that points along the line of text and one that points from the bottom to the top of the line. Together the two vectors define the plane on which the text is rendered.
Texture coordinates are assigned to vertices, so in order to allow the text to be viewed the "right way around" from above and underneath we need to create two rectangles on which to display the text, with distinct vertices.
The extents of the rectangle are calculated by moving half of the text width away from the specified center point along the 'along' axis and half of the height on the 'up' axis. Two rectangular billboards are then created and painted using a visual brush containing a text block.
/// Creates a label oriented on a plane defined by the center point of the /// text and two unit vectors defining its orientation and plane. let CreateLabel (text:string) (center:Point3D) (up:Vector3D) (along:Vector3D) = let height = 0.035 // hard-coded font height, let width = height * Wpf.MeasureLine text this.typeface let bottomLeft = center - width / 2.0 * along - height / 2.0 * up; let bottomRight = bottomLeft + (along * width); let topLeft = bottomLeft + (up * height); let topRight = topLeft + (along * width); let points = Point3DCollection() let rectangle = MeshGeometry3D() rectangle.Positions <- points // Shorthand for adding a point and associated UV coordinate let point p u v = points.Add(p) rectangle.TextureCoordinates.Add(Point(u, v)) // Shorthand for creating a triangle from three point indices let triangle p0 p1 p2 = rectangle.TriangleIndices.Add(p0) rectangle.TriangleIndices.Add(p1) rectangle.TriangleIndices.Add(p2) // Add the points and texture coordinates for the underside point bottomLeft 0.0 1.0 point topLeft 0.0 0.0 point bottomRight 1.0 1.0 point topRight 1.0 0.0 // Add the points and texture coordinates for the topside point bottomLeft 1.0 1.0 point topLeft 1.0 0.0 point bottomRight 0.0 1.0 point topRight 0.0 0.0 // Define the triangles that make up the two sides of the rectangle triangle 0 3 1 triangle 0 2 3 triangle 4 5 7 triangle 4 7 6 let block = TextBlock(Run(text)) block.Foreground <- this.brush block.FontFamily <- this.family // Hint that the visual brush is not going to change, so that it is // not re-rendered on every frame. let brush = VisualBrush(block) RenderOptions.SetCachingHint(brush, CachingHint.Cache) let visual = ModelVisual3D() visual.Content <- GeometryModel3D(rectangle, DiffuseMaterial(brush)) visual
Performance
Having written the small amount of code described above, I fired up the application to be met with abysmally jittery rendering. Three small changes restored bearable performance:
Enabling caching for the VisualBrush
By default, the contents of the VisualBrush are rendered for every frame. WPF allows the application to hint that the Visual underlying the VisualBrush is not frequently changing and can be cached. If the hint is obeyed, then the Visual is rendered off-screen, reducing the cost to regular texture mapping.
This has an advantage over rendering the text to a bitmap yourself, since WPF will re-render the Visual at different resolutions as its projection onto the screen changes in size to ensure that the appearance doesn't significantly degrade.
To use this hint requires a single line, although the corresponding MSDN page describes how the results can be tuned:
let brush = VisualBrush(textblock) RenderOptions.SetCachingHint(brush, CachingHint.Cache)
Disabling hit-testing
Leaving hit-testing enabled has a noticable impact on performance. Since it's not used in this application, it can be disabled on the viewport with:
viewport.IsHitTestVisible <- false
Disabling clipping
There isn't a significant amount of content outside the window and the Viewport3D
fills the window, so clipping can be disabled for an additional speed up. The Maximize WPF 3D Performance page on MSDN indicates that the Viewport3D
's clipping is slow. Disabling it is a single-line change:
viewport.ClipToBounds <- false
Updated code
The updated code can be downloaded from here.
No comments:
Post a Comment