More 2D/3D Tricks: ItemsControl3D

Building on some of the cute tricks of using a Viewport3D as a two-dimensional surface, we can actually devise a fully-bindable System.Windows.Controls.ItemsControl that renders to a Viewport3D instead of a Panel. It turns out to all be quite remarkably simple:

  1. Define a class, ItemsControl3D, that will be our magic Viewport3D-holding ItemsControl.
  2. Build a ControlTemplate that contains a Viewport3D with a named ModelVisual3D whose children will be modified as the ItemsControl sees fit.
  3. Override GetContainerForItemOverride() to provide instances of ItemsControl3DItem as the “container” for the individual items in the ItemsControl:
    1. Make the “container” item a zero-size, completely non-visible FrameworkElement.
    2. Create an ItemsControl3DItem.Geometry property; this Geometry3D object will be used to populate the ModelVisual3D in our Viewport3D.
  4. I additionally chose to implement container recycling (in some early drafts of ItemsControl3D, it cut processor usage down 25%).

First, the static constructor:

1
2
3
4
5
6
7
8
9
10
11
12
static ItemsControl3D()
{
    DefaultStyleKeyProperty.OverrideMetadata(
        typeof(ItemsControl3D),
        new FrameworkPropertyMetadata(typeof(ItemsControl3D)));
 
    ItemsPanelProperty.OverrideMetadata(
        typeof(ItemsControl3D),
        new FrameworkPropertyMetadata(
            new ItemsPanelTemplate(
                new FrameworkElementFactory(typeof(ItemsControl3D.ItemsPanel)))));
}

The first line should be familiar to those in the audience who have authored custom controls before; it merely indicates that somewhere, WPF should expect to find:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<<b>Style x:Key="{x:Type ThreeDee:ItemsControl3D}"</b>
        TargetType="{x:Type ThreeDee:ItemsControl3D}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type ThreeDee:ItemsControl3D}">
        <Border>
          <Grid>
            <Viewport3D Name="PART_Viewport">
              <Viewport3D.Camera>
                <OrthographicCamera
                         Position="0,0,1" LookDirection="0,0,-1" UpDirection="0,1,0"
                         Width="{Binding Path=ActualWidth, ElementName=PART_Viewport}"/>
              </Viewport3D.Camera>
 
              <ModelVisual3D>
                <ModelVisual3D.Content>
                  <AmbientLight Color="White">
                </ModelVisual3D.Content>
              </ModelVisual3D>
 
              <ModelVisual3D x:Name="PART_SceneRoot"/>
            </Viewport3D>
 
            <ItemsPresenter/>
          </Grid>
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

The line after that (the overriding of the ItemsPanel metadata) indicates that for all ItemsControl3D, the default Panel presenting children for the panel should be ItemsControl3D.ItemsPanel. This is an inner class that we’ve defined that is specially crafted to hold the child elements of the ItemsControl.

In the style, we’ve given one of the ModelVisual3D children a name (PART_SceneRoot); that’s because in OnApplyTemplate(), we’re going to look for it and use that as the place to hold the 3D objects that we generate.

We override a trio of methods in order to perform basic container housekeeping. GetContainerForItemOverride() either creates a new container or reuses an existing one; ClearContainerForItemOverride(…) adds an unused ItemsControl3DItem back to the pool; IsItemsItsOwnContainerOverride(…) is useful to override if you wanted to manually create and add ItemsControl3DItem objects to the ItemsControl3D.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private readonly Stack<ItemsControl3DItem> _unusedContainers =
        new Stack<ItemsControl3DItem>();
 
protected override DependencyObject GetContainerForItemOverride()
{
    if (_unusedContainers.Count == 0)
    {
        return new ItemsControl3DItem();
    }
    else
    {
        return _unusedContainers.Pop();
    }
}
 
protected override void ClearContainerForItemOverride(
    DependencyObject element, object item)
{
    _unusedContainers.Push((ItemsControl3DItem)element);
}
 
protected override bool IsItemItsOwnContainerOverride(object item)
{
    return (item is ItemsControl3DItem);
}

Lastly, the actual “panel” that the ItemsControl thinks is doing the work:

1
2
3
4
5
6
7
8
9
10
11
12
13
private new sealed class ItemsPanel : Panel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        var ic3d = (ItemsControl3D)GetItemsOwner(this);
        if (ic3d != null)
        {
            <b>ic3d.UpdateViewportChildren(InternalChildren);</b>
        }
 
        return Size.Empty;
    }
}

And that’s all the panel needs to do. The magic method call is actually the property accessor Panel.InternalChildren—internal code in Panel works together with ItemsControl in order to derive the appropriate children (this is ultimately what will cause GetContainerForItemOverride() and other methods to be called).

Lastly, the private method UpdateViewportChildren in ItemsControl3D:

private void UpdateViewportChildren(UIElementCollection children)
{
    if (_sceneRoot == null) return;
  
    _sceneRoot.Children.Clear();
    foreach (ItemsControl3DItem item in children)
    {
        var m = item.Model;
        if (m != null)
        {
            _sceneRoot.Children.Add(m);
        }
    }
}

And in case you were wondering, ItemsControl3DItem at a high level:

1
2
3
4
5
6
7
8
public class ItemsControl3DItem : FrameworkElement
{
    public double X { get; set; }
    public double Y { get; set; }
    public Brush Background { get; set; }
    public Geometry3D Geometry { get; set; }
    public ModelVisual3D Model { get; }
}

The properties of ItemsControl3DItem (X, Y, Background, Geometry) are all used to determine the Model property.

You can use an ItemsControl3D in XAML as easily as any other subclass of ItemsControl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ThreeDee:ItemsControl3D ItemsSource="{Binding ...}">
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="{x:Type ThreeDee:ItemsControl3DItem}">
            <Setter Property="X" Value="{Binding Year}"/>
            <Setter Property="Y" Value="{Binding Profit}"/>
            <Setter Property="Background" Value="Blue"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
 
    <!-- you could also hardcode children in the control just like 
         ListBoxItems in a ListBox or ListViewItems in a ListView -->
    <!-- <font color="#555555"><ThreeDee:ItemsControl3DItem X="5" Y="6" Background="Blue"/>
         <ThreeDee:ItemsControl3DItem X="2" Y="3" Background="Red"/></font> -->
</ThreeDee:ItemsControl3D>

It should be noted that this exact technique can be used to generate full-blown three-dimensional visualizations with nothing more than basic ItemsControl-style bindings. Coupled with an abstract data model, you’ve got yourself a pretty canvas to paint with, and it’s pretty responsive to updates as well and doesn’t blow a hole through your CPU either. The sample app updates all of the values in a collection of 1,000 points, ten times a second, while using less than 10% of my two-year-old MacBook’s CPU.

The Sample Code: ItemsControl3D.zip

—DKT

Sept 24: Fixed the problems with the download. Of course, since the AssemblyInfo.cs file was missing, the [assembly:ThemeInfo(ResourceDictionaryLocation.SourceAssembly)] tag was missing too—that caused the default template that I defined for ItemsControl3D to not be found. This is fixed now.

2D in a WPF 3D World

It’s a fairly common and standard trick to take advantage of the hardware acceleration afforded by modern graphic cards and render two-dimensional figures in 3D. Here is a simple and fast way to take the 3D out of a Viewport3D:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<Viewport3D>
  <Viewport3D.Camera>
    <OrthographicCamera Position="0,0,1" LookDirection="0,0,-1" UpDirection="0,1,0"
                        Width="{Binding ActualWidth}" Height="{Binding ActualHeight}"/>
  </Viewport3D.Camera>
 
  <ModelVisual3D>
    <ModelVisual3D.Children>
      <!-- render colors...relatively properly -->
      <ModelVisual3D>
        <ModelVisual3D.Content>
          <AmbientLight Color="White"/>
        </ModelVisual3D.Content>
      </ModelVisual3D>
 
      <ModelVisual3D x:Name="SceneRoot">
        <ModelVisual3D.Content>
          <!-- this is where stuff you want to render actually goes -->
        </ModelVisual3D.Content>
      </ModelVisual3D>
 
    </ModelVisual3D.Children>
  </ModelVisual3D>
</Viewport3D>

When the camera is set up this way, the center of the Viewport3D represents (0,0). The bottom-left is (−width / 2, −height / 2) and the top-right is (+width / 2, +height / 2).

Now what can you put in something like this? Same thing you’d normally put in a Viewport3D:

1
2
3
4
5
6
7
<Model3DGroup>
  <GeometryModel3D Geometry="...">
    <GeometryModel3D.Material>
      <DiffuseMaterial Brush="..."/>
    </GeometryModel3D.Material>
  </GeometryModel3D>
</Model3DGroup>

The Brush property can be any old System.Windows.Media.Brush. And as for that Geometry? Well, there are squares:

1
2
3
4
<MeshGeometry3D x:Key="Square">
                Positions="-1,-1,0  1,-1,0  1,1,0  -1,1,0"
                Normals="0,0,1  0,0,1  0,0,1  0,0,1"
                TriangleIndices="0 1 2 0 2 3"/>

isosceles triangles:

1
2
3
4
<MeshGeometry3D x:Key="IsoscelesTriangle">
                Positions="-1,-1,0  1,-1,0  0,1,0"
                Normals="0,0,1  0,0,1  0,0,1"
                TriangleIndices="0 1 2"/>

Coming up with these little nuggets of numbers is the topic of another post. But for now, some sample code:

ThreeDee.png

Featuring basic hit-detection and a bit of interactivity (you can drag a square to increase/decrease the space between boxes, and change the angle of rotation). It’s pure WPF (no unsafe code or interop), it’s 2D, and it’s fast.

ThreeDee.zip

—DKT

Sorry the code formatting is off…the iPhone WordPress client totally destroyed it. I’ll fix it soonish.