20070723 03:05:38 10: AutoZoom
This entry is part 10 of a 12part series on WPF
3D.
Zoom to Fit
Most CADlike programs have the ability to automatically
zoom a 3D picture so it fits in the window.
You don't want this situation, where the model is too small
and most of the window is wasted:
Nor do you want this situation, where the picture is zoomed
so far that only a small portion of the model is visible:
So we provide a feature to automatically zoom the view to
the right size so that everything just fits in the visible space available.
But the implementation of a feature like this is
surprisingly tricky. It is another one of those features which must straddle
the boundary between the 3D world and the 2D world. Scaling the model is done
with a 3D transform. But the available window space is a 2D area.
Finding the 2D Bounding Box
The first thing we need is a way to measure the size of the
2D projection. The window is showing a 2D projection of a 3D object. In terms
of the 3D coordinate system, we know how big the object is. But how big is the
2D projection we see on our screen?
Unless I am missing something obvious in the WPF 3D APIs,
this is surprisingly difficult to get. My approach is to iterate over all the
triangles in my scene. For each of the three points in each triangle, I find
the 2D projection of that point and grow a bounding rectangle to include it.
But how do I find the 2D projection of a 3D point? This is
the part that is a bit trickier than I expected it to be. I naively hoped I
might find that the Viewport3D class has a method that would do this for me,
something like this:
public Point
GetProjectedPoint(Point3D p)
But it doesn't.
As it turns out, the 3D Tools library saves the day
again. It contains a MathUtils class which contains a routine called
TryWorldToViewportTransform(). This method returns a transformation matrix
which converts 3D coordinates ("World") to 2D coordinates ("Viewport").
Reading the code for this method is interesting. It has to jump through a
surprising number of hoops to retrieve the matrix we need.
So my implementation of retrieving a 2D bounding box from a
Viewport3D looks like this:
public static
Rect Get2DBoundingBox(Viewport3D vp)
{
bool bOK;
Viewport3DVisual vpv =
VisualTreeHelper.GetParent(
vp.Children[0]) as Viewport3DVisual;
Matrix3D m =
MathUtils.TryWorldToViewportTransform(vpv,
out bOK);
bool bFirst = true;
Rect r = new
Rect();
foreach (Visual3D
v3d in vp.Children)
{
if (v3d is
ModelVisual3D)
{
ModelVisual3D mv3d = (ModelVisual3D)v3d;
if (mv3d.Content is GeometryModel3D)
{
GeometryModel3D gm3d =
(GeometryModel3D)
mv3d.Content;
if (gm3d.Geometry is MeshGeometry3D)
{
MeshGeometry3D mg3d =
(MeshGeometry3D)gm3d.Geometry;
foreach (Point3D p3d in
mg3d.Positions)
{
Point3D pb =
m.Transform(p3d);
Point p2d = new Point(pb.X,
pb.Y);
if (bFirst)
{
r = new Rect(p2d, new Size(1, 1));
bFirst = false;
}
else
{
r.Union(p2d);
}
}
}
}
}
}
return r;
}
This code deserves a few remarks:
 This approach works only if I don't put Transforms on the
individual objects in the scene. A more generic implementation would need
to walk up the visual tree from every MeshGeometry3D and stop to apply
every Transform object it finds along the way.
 TryWorldToViewportTransform() wants a Viewport3DVisual,
but I have a Viewport3D. Since a Viewport3D encapsulates a
Viewport3DVisual, I can just grab it. But that member is apparently not
public, so I cheat and retrieve it by walking up the visual tree from its
first child. I may be on thin ice here.
 I'm sure I have not handled all the cases in the hierarchy
of stuff in Viewport3D.Children.
So this code is more of a hack and will need some serious
attention before it can be considered robust as a general purpose solution.
Nonetheless, for my situation it is currently working. If I run the code on my
app, I can take the resulting Rect and use it to place a partly transparent Rectangle
in the Overlay layer, just to prove that it's doing the right thing:
So now what?
Now that I can calculate a 2D bounding box, I want to
calculate the proper zoom so that the model will just fit. Recall from part 9 that on the left
side of my window is a zoom slider which is tied to a ScaleTransform3D which is
part of the Transform for the Camera on my Viewport3D.
The problem is that the mathematical relationship between
the value of that slider and the coordinates of the 2D bounding box is not
obvious. I actually ran a loop and calculated a bunch of values so I could
graph them in Excel. It's not linear. It looks more geometric, but I was only
plotting the zoom value vs. the 2D height. There's probably a way to calculate
just the right scale factor, but I haven't found it yet.
When I am facing a tricky problem, sometimes I like to start
by just quickly coding the simplest solution that will work. So I put my
fingers to the keyboard and typed for a couple minutes. This is what happened:
private bool
TooBig()
{
Rect r = sdwpf.Get2DBoundingBox(vstuff.vp);
if (r.Left < 0)
{
return true;
}
if (r.Right > vstuff.vp.ActualWidth)
{
return true;
}
if (r.Top < 0)
{
return true;
}
if (r.Bottom > vstuff.vp.ActualHeight)
{
return true;
}
return false;
}
void OnClick_Fit(object
sender, RoutedEventArgs args)
{
if (TooBig())
{
while (TooBig())
{
slider_zoom.Value = 0.1;
}
while (!TooBig())
{
slider_zoom.Value += 0.01;
}
slider_zoom.Value = 0.01;
}
else
{
while (!TooBig())
{
slider_zoom.Value += 0.1;
}
while (TooBig())
{
slider_zoom.Value = 0.01;
}
}
}
This rather absurd solution is the first thing that popped
into my head. And it works!
Normally when I do something like this, I immediately start
looking to replace the implementation with something better. However, today is
Sunday. If I put any further thought into this problem it would be sort of
like work. So I think I'll just leave it alone for now. I don't think it's quite
stupid enough to land me on The
Daily WTF. :)
