Creating a simple 3D scene in WPF


Introduction:

In this article I will use the premise of creating a simple 3D scene (a living room with two couches, a coffee table and a TV) to cover:
  • The basics of the scene: viewport, lights, floor and container for our furniture
  • Hookup a virtual trackball so we can fully examine the scene
  • Converting models from .3ds to XAML
  • Massaging the XAML to be resource-ready
  • Adding, sizing and positioning the models in the scene
1.gif

Background:

Two years ago we started looking into using WPF for a 3D component of our application.  I found a wealth of information on the WPF 3D basics (mostly using primitives), but had an exceedingly difficult time with one key point: how do you take a model created by a 3D artist and use it in WPF?  At the time (and still true now for the most part), modelers are not creating your 3D assets in XAML - the 3D modeling tools seem to be slow in adopting XAML as a supported format.  Back then we were using DirectX for our 3D and most of our models were in .x format, but even much more ubiquitous formats such as .3ds have to be first converted to XAML before you can use them in WPF.

Today, if you do a search on how to convert from .3ds to XAML, you will find some helpful tools and examples, but even these fall short in my opinion due to the fact that every converter I've seen will export a scene to XAML, not a model.  What's the difference?  A scene has a viewport, a camera, lights and one or more models.  This is great if you want to create a 3D scene with just one 3D model.  But what if you want to create a scene comprised of multiple 3D models?  You don't want each model to come with its own viewport, camera and lights.  And what if you want to use these models as a resource so you can have more than one in a scene?  These are topics I will address in this article.

Step 1: Create the project and setup the scene

The first step is to create a project to host our scene.  There's nothing special in this example and this is well-treaded ground in terms of WPF tutorials.  Once you've created the project we'll want to add the key elements of any WPF 3D scene: the viewport, the camera, and lights.  Since this tutorial is more focused on the use of .3ds models as resources, I'm going to gloss over this step since this part of the setup is well covered in other articles.  I've included a directional light and a spot light to add some reflective flare to the overall scene.

Step1. Figure1 - Our initial 3D scene complete with Viewport, camera and lights

<Window x:Class="_3dsToXaml.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:inter3D="clr-namespace:_3DTools;assembly=3DTools"
    Title="Window1" Height="500" Width="600"
    x:Name="MainWindow">
    <Grid>
        <Viewport3D x:Name="viewport" RenderOptions.CachingHint="Cache" ClipToBounds="True" >
            <Viewport3D.Camera>
<PerspectiveCamera x:Name="myPerspectiveCamera" FarPlaneDistance="300" LookDirection="0,0,-1"
UpDirection
="0,1,0" NearPlaneDistance="1"  Position="0,3,25" FieldOfView="45">
                    <PerspectiveCamera.Transform>
                        <MatrixTransform3D>
                        </MatrixTransform3D>
                    </PerspectiveCamera.Transform>
                </PerspectiveCamera>
            </Viewport3D.Camera>
            <ModelVisual3D x:Name="viewportLightsModelVisual3D">
                <ModelVisual3D.Content>
                    <Model3DGroup>
                        <AmbientLight x:Name="ambientLight" Color="#666666"/>
                        <DirectionalLight x:Name="directionalLight" Color="#444444" Direction="0 -1 -1">
                        </DirectionalLight>
<SpotLight x:Name="spotLight" Color="#666666" Direction="0 0 -1" InnerConeAngle="30" OuterConeAngle="60" Position="0 1 30" >
                        </SpotLight>
                    </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>
        </Viewport3D>
    </Grid>
</Window>

Running the project at this point is uninteresting, because we haven't yet added anything to see!  Since the scene we're creating will eventually be a living room, let's go ahead and create a primitive to represent our floor.

Step1. Figure2 - The floor model for our dining room

<ModelUIElement3D x:Name="Floor" >
                <GeometryModel3D>
                    <GeometryModel3D.Geometry>
                        <MeshGeometry3D x:Name="floorGeometry" Positions="{Binding FloorPoints3D, ElementName=MainWindow}"
TriangleIndices
="{Binding FloorPointsIndices, ElementName=MainWindow}" />
                    </GeometryModel3D.Geometry>
                    <GeometryModel3D.Material>
                        <MaterialGroup>
                            <DiffuseMaterial Brush="LightGray"/>
                            <SpecularMaterial Brush="LightGray" SpecularPower="100"/>
                        </MaterialGroup>
                    </GeometryModel3D.Material>
                    <GeometryModel3D.BackMaterial>
                        <DiffuseMaterial Brush="Black"/>
                    </GeometryModel3D.BackMaterial>
                </GeometryModel3D>
</ModelUIElement3D>

You'll notice that the MeshGeometry3D Positions and TriangleIndices are obtained through binding.  There's no reason you couldn't just create these values inline within the XAML, but I find it easier to read/create these values in code (and hopefully it'll be easier for you to follow as well).

Step1. Figure3 - Floor points and indices for our floor model to bind to

public Point3DCollection FloorPoints3D
{
    get
    {
        double x = 6.0; // floor width / 2
        double z = 6.0; // floor length / 2
        double floorDepth = -0.2; // give the floor some depth so it's not a 2 dimensional plane 

        Point3DCollection points = new Point3DCollection(20);
        Point3D point;
        //top of the floor
        point = new Point3D(-x, 0, z);// Floor Index - 0
        points.Add(point);
        point = new Point3D(x, 0, z);// Floor Index - 1
        points.Add(point);
        point = new Point3D(x, 0, -z);// Floor Index - 2
        points.Add(point);
        point = new Point3D(-x, 0, -z);// Floor Index - 3
        points.Add(point);
        //front side
        point = new Point3D(-x, 0, z);// Floor Index - 4
        points.Add(point);
        point = new Point3D(-x, floorDepth, z);// Floor Index - 5
        points.Add(point);
        point = new Point3D(x, floorDepth, z);// Floor Index - 6
        points.Add(point);
        point = new Point3D(x, 0, z);// Floor Index - 7
        points.Add(point);
        //right side
        point = new Point3D(x, 0, z);// Floor Index - 8
        points.Add(point);
        point = new Point3D(x, floorDepth, z);// Floor Index - 9
        points.Add(point);
        point = new Point3D(x, floorDepth, -z);// Floor Index - 10
        points.Add(point);
        point = new Point3D(x, 0, -z);// Floor Index - 11
        points.Add(point);
        //back side
        point = new Point3D(x, 0, -z);// Floor Index - 12
        points.Add(point);
        point = new Point3D(x, floorDepth, -z);// Floor Index - 13
        points.Add(point);
        point = new Point3D(-x, floorDepth, -z);// Floor Index - 14
        points.Add(point);
        point = new Point3D(-x, 0, -z);// Floor Index - 15
        points.Add(point);
        //left side
        point = new Point3D(-x, 0, -z);// Floor Index - 16
        points.Add(point);
        point = new Point3D(-x, floorDepth, -z);// Floor Index - 17
        points.Add(point);
        point = new Point3D(-x, floorDepth, z);// Floor Index - 18