Создание простой 3D-сцены в WPF

Автор: Майк (Mike)

В этой статье я буду использовать исходное положение о создании простой 3D-сцены (гостиная с двумя диванами, журнальным столиком и телевизором), в том числе:

  • Основы сцены: окно просмотра, освещение, пол и контейнер для нашей мебели.
  • Подключение виртуального трек-бола, чтобы можно было изучить всю сцену.
  • Преобразование моделей из .3ds в XAML
  • Настройка XAML для использования ресурсов.
  • Добавление, изменение размера и размещение моделей в сцене.

Предыстория

Два года назад мы начали изучать использование WPF для создания 3D-компонента в нашем приложении.  Я обнаружил множество информации об основах WPF 3D (в основном по использованию примитивов), но мне оказалось очень трудно разобраться с одним ключевым моментом: как взять модель, созданную 3D-художником, и использовать ее в WPF?  Тогда (и это во многом остается справедливым и сегодня) разработчики моделей не создавали 3D-ресурсы в XAML — средства 3D-моделирования казались слишком медленными для использования XAML в качестве поддерживаемого формата.  Раньше мы использовали DirectX для своих 3D-объектов, и для большинства наших моделей применялся формат .x, но даже намного более распространенные форматы, например 3DS, нужно было сначала преобразовать в XAML, прежде чем их можно было использовать в WPF.

Сегодня, если нужен способ преобразовать 3DS в XAML, вы найдете ряд полезных средств и примеров, но их, как мне кажется, недостаточно, так как каждый виденный мной конвертер экспортирует в XAML сцену, а не модель.  В чем различие?  Сцена содержит окно просмотра, камеру, источники света и одну или несколько моделей.  Это замечательно, если нужно создать 3D-сцену только с одной 3D-моделью.  Но что, если нужно создать сцену, состоящую из нескольких 3D-моделей?  Не хочется, чтобы каждая модель приходила со своими окном просмотра, камерой и источниками света.  И что делать, если нужно использовать эти модели в качестве ресурса, чтобы сцена содержала несколько моделей?  Именно эти вопросы я рассмотрю в этой статье.

Шаг 1. Создание проекта и настройка сцены

Первым шагом является создание проекта для размещения нашей сцены.  В этом примере нет ничего особенного, и эта почва плотно утоптана учебниками по WPF.  После создания проекта нам понадобится добавить ключевые элементы любой 3D-сцены WPF: окно просмотра, камера и источники освещения.  Так как учебники больше фокусируются на использовании 3DS-моделей в качестве ресурсов, я собираюсь пропустить этот шаг, так как эта часть настройки хорошо рассмотрена в других статьях.  Я добавил направленный свет точечный свет, чтобы включить в общую сцену ряд бликов отражения.

Шаг 1. Рис. 1. Первоначальная 3D-сцена с окном просмотра, камерой и источниками света

<Window x:Class="_3dsToXaml.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://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>

Выполнение этого проекта в данный момент неинтересно, так как еще не добавлено ничего, на что можно смотреть!  Так как создаваемая сцена в итоге будет гостиной, двинемся дальше и создадим примитив, представляющий наш пол.

Шаг 1. Рис. 2. Модель пола для столовой

<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>

Обратите внимание, что положения 3D-геометрии сетки (MeshGeometry3D Positions) и индексы треугольников (TriangleIndices) получены с помощью привязки (Binding).  Нет причин, по которым эти значения нельзя было создать прямо внутри XAML, но мне кажется проще читать/создавать эти значения в коде (и, надеюсь, это упростит слежение за развитием событий).

Шаг 1. Рис. 3. Точки и индексы пола, привязываемые к нашей модели

public Point3DCollection FloorPoints3D
{
    get
    {
        double x = 6.0; // ширина пола / 2
        double x = 6.0; // длина пола / 2
        double floorDepth = -0.2; // придает полу некоторую глубину, чтобы он не был 2-мерной плоскостью 
        Point3DCollection points = new Point3DCollection(20);
        Point3D point;
        //верхняя часть пола
        point = new Point3D(-x, 0, z);// индекс пола - 0
        points.Add(point);
        point = new Point3D(x, 0, z);// индекс пола - 1
        points.Add(point);
        point = new Point3D(x, 0, -z);// индекс пола - 2
        points.Add(point);
        point = new Point3D(-x, 0, -z);// индекс пола - 3
        points.Add(point);
        //передняя сторона
        point = new Point3D(-x, 0, z);// индекс пола - 4
        points.Add(point);
        point = new Point3D(-x, floorDepth, z);// индекс пола - 5
        points.Add(point);
        point = new Point3D(x, floorDepth, z);// индекс пола - 6
        points.Add(point);
        point = new Point3D(x, 0, z);// индекс пола - 7
        points.Add(point);
        //правая сторона
        point = new Point3D(x, 0, z);// индекс пола - 8
        points.Add(point);
        point = new Point3D(x, floorDepth, z);// индекс пола - 9
        points.Add(point);
        point = new Point3D(x, floorDepth, -z);// индекс пола - 10
        points.Add(point);
        point = new Point3D(x, 0, -z);// индекс пола - 11
        points.Add(point);
        //обратная сторона
        point = new Point3D(x, 0, -z);// индекс пола - 12
        points.Add(point);
        point = new Point3D(x, floorDepth, -z);// индекс пола - 13
        points.Add(point);
        point = new Point3D(-x, floorDepth, -z);// индекс пола - 14
        points.Add(point);
        point = new Point3D(-x, 0, -z);// индекс пола - 15
        points.Add(point);
        //левая сторона
        point = new Point3D(-x, 0, -z);// индекс пола - 16
        points.Add(point);
        point = new Point3D(-x, floorDepth, -z);// индекс пола - 17
        points.Add(point);
        point = new Point3D(-x, floorDepth, z);// индекс пола - 18
        points.Add(point);
        point = new Point3D(-x, 0, z);// индекс пола - 19
        points.Add(point);
        return points;
    }
}
public Int32Collection FloorPointsIndices
{
    get
    {
     int[] indices = new int[] { 0, 1, 2, 0, 2, 3, 4, 5, 7, 5, 6, 7, 8, 9, 11, 9, 10, 11, 12, 13, 15, 13,
       14, 15, 16, 17, 19, 17, 18, 19 };
return new Int32Collection(indices);
}
}

Если выполнить проект в этот момент, появится 3D-сцена, содержащая только пол — но вы получите перспективу пола только для текущей камеры.  Нам нужен способ перемещения камеры, чтобы можно было более полно исследовать сцену и видеть ее с различных углов.  Наиболее типичное решение в этом случае — использовать "виртуальный трекбол".  К счастью, есть проект CodePlex (3DTools - https://3dtools.codeplex.com/Thread/List.aspx), позволяющий добавить виртуальный трекбол практически безболезненно.

Шаг 1. Рис. 4. Реализация виртуального трекбола с помощью 3DTools

xmlns:_3DTools ="clr-namespace:_3DTools;assembly=3DTools"
 <_3DTools:TrackballDecorator Height="Auto">
            <_3DTools:Interactive3DDecorator>
               <Viewport3D ... >
                             ...
            </Viewport3D>
            </_3DTools:Interactive3DDecorator>
 </_3DTools:TrackballDecorator>

Последним этапом шага 1 является создание контейнера, в который можно будет добавить нашу мебель.  Мы вернемся к этому позднее.

Шаг 1. Рис. 5. Добавление контейнера ContainerUIElement3D для размещения нашей мебели

<Window x:Class="_3dsToXaml.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:inter3D="clr-namespace:_3DTools;assembly=3DTools"
    Title="Window1" Height="500" Width="600"
    x:Name="MainWindow">
    <Grid>
        <inter3D:TrackballDecorator x:Name="inter3d" DockPanel.Dock="Bottom" Height="Auto">
            <inter3D:Interactive3DDecorator>
              <Viewport3D x:Name="viewport" RenderOptions.CachingHint="Cache" ClipToBounds="True" >
                 <Viewport3D.Camera>
                ...
                 </Viewport3D.Camera> 
            <ContainerUIElement3D x:Name="FurnitureContainer" /> 
            <ModelUIElement3D x:Name="Floor" >                ...
            </ModelUIElement3D> 
            <ModelVisual3D x:Name="viewportLightsModelVisual3D">                ...
            </ModelVisual3D>
   </Viewport3D>
            </inter3D:Interactive3DDecorator>
            </inter3D:TrackballDecorator>
</Grid>
</Window>

Шаг 2. Получение ряда [бесплатных] профессиональных моделей (3DS) и их преобразование в XAML

Теперь можно получить несколько моделей для нашей сцены.  В этом примере мы добавим модели мебели.  В Интернете доступно множество бесплатных моделей, хороший список веб-сайтов для поиска можно найти здесь (http://www.hongkiat.com/blog/60-excellent-free-3d-model-websites/).  Для нашей гостиной я выбрал диван, журнальный столик и телевизор, загрузив модели (в формате 3DS) отсюда:  http://archive3d.net/

После загрузки моделей в формате 3DS все готово к началу их преобразования в XAML для использования в нашем проекте.  На момент написания этой статьи мне были известны 3 различных средства преобразования 3DS-модели в XAML: "Zam3D" (http://www.erain.com/products/zam3d/DefaultPDC.asp ) от electric rain, "Deep Exploration" (http://www.righthemisphere.com/products/dexp/de_std.html ) от Right Hemisphere и "Viewer3ds" (http://www.wpf-graphics.com/Viewer3ds.aspx), написанный Андреем Бенедиком (Andrej Benedik).  В зависимости от выбранного средства процесс преобразования модели может слегка отличаться.  Для данной статьи я буду использовать Zam3D — именно с этим средством я лучше знаком, и у него есть полнофункциональная пробная версия.

В Zam3D выберите в меню File (Файл) пункт "New from 3DS..." (Создать из 3DS) и найдите только что загруженную 3DS-модель (начнем с модели дивана).  Затем выберите в меню File (Файл) пункт "Export Scene to XAML..." (Экспорт сцены в XAML).  В группе "Export Options" (Параметры экспорта) выберите "Viewport3D" в качестве типа управления (Control Type) и "Export Elements Inline" (Экспортировать встроенные элементы, см. рис. "Шаг 2. Рис. 1")

Шаг 2. Рис.1. Окно "Export to XAML" (Экспорт в XAML) программы Zam3D

Шаг 3. Преобразование XAML-моделей в ресурсы

Мы успешно преобразовали 3DS-модель дивана в XAML, но XAML еще не совсем готов для использования.  Почему?  Потому что, как я упоминал во введении к этой статье, процесс преобразования создает сцену (окно просмотра, камера, источники света и модель), а нам нужна только модель — мы уже создали окно просмотра, камеру и источники света для нашей сцены.  Поэтому нам придется открыть XAML-файл и вручную удалить эти лишние элементы, оставив только Model3Dgroup.  На рис. "Шаг 3. Рис. 1" показан XAML-файл без правки, а на рис. "Шаг 3. Рис. 2" показано, как должен выглядеть XAML после удаления избыточных элементов.

Шаг 3. Рис. 1. Исходный конвертированный XAML.  Включает окно просмотра, камеру и источники света

<Viewport3D x:Name="ZAM3DViewport3D" ClipToBounds="true" Width="400" Height="300"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="https://schemas.microsoft.com/expression/interactivedesigner/2006"
    xmlns:c="https://schemas.openxmlformats.org/markup-compatibility/2006" c:Ignorable="d">
          <Viewport3D.Camera>
             <PerspectiveCamera x:Name="FrontOR7" FarPlaneDistance="460" LookDirection="0,0,-1"
                   UpDirection="0,1,0" NearPlaneDistance="190" Position="-7.62939e-006,52.9203,328"
                  FieldOfView="39.5978" />
          </Viewport3D.Camera>
          <ModelVisual3D>
              <ModelVisual3D.Content>
                 <Model3DGroup x:Name="Scene"> <!-- Scene (XAML Path = ) -->
                   <Model3DGroup.Transform>
                      <Transform3DGroup>
                         <TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
                             <ScaleTransform3D ScaleX="1" ScaleY="1" ScaleZ="1"/>
                             <RotateTransform3D>
                             <RotateTransform3D.Rotation>
                             <AxisAngleRotation3D Angle="0" Axis="0 1 0"/>
                             </RotateTransform3D.Rotation>
                                </RotateTransform3D>
                             <TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
                       </Transform3DGroup>
                   </Model3DGroup.Transform>
                   <AmbientLight Color="#646464" />
                   <DirectionalLight Color="#FFFFFF" Direction="-0.612372,-0.5,-0.612372" />
                   <DirectionalLight Color="#FFFFFF" Direction="0.612372,-0.5,-0.612372" />
                   <Model3DGroup x:Name="Group01OR10">
                      <!-Это главная группа Model3Dgroup, можно удалить все вокруг нее -->
                   </Model3DGroup>
            </Model3DGroup>
       </ModelVisual3D.Content>
   </ModelVisual3D>
</Viewport3D>

Шаг 3. Рис. 2.

<Model3DGroup x:Name="Group01OR10">
    <!-Это главная группа Model3Dgroup, можно удалить все вокруг нее -->
            </Model3DGroup>

Теперь, получив XAML-файл, в котором оставлен только элемент Model3Delement, мы готовы преобразовать его в ресурс.  Создавая ресурс, мы выполним пару действий: 

  1. мы отделим XAML-код модели от XAML-кода сцены/окна;
  2. Мы можем повторно использовать модель в этой сцене или в других сценах, не используя "копировать/вставить".

XAML-файл превращается в полезный словарь ресурсов следующим образом:

  1. вкладывание xml-кода Model3DGroup в элемент "ResourceDictionary";
  2. назначение ресурсу Model3Dgroup идентификатора "x:Key";
  3. Удаление всех лишних идентификаторов "x:Name" в XAML-коде Model3DGroup.

Приведенные выше шаги a. и b. достаточно просты и не требуют дальнейшего пояснения (см. рис. "Шаг 3. Рис. 3")  Шаг c. необходим, так как Zam3D назначит имена всем дочерним элементам нашего главного элемента Model3DGroup.   Ресурсы нельзя определять по имени, они должны быть определены по ключу, но нам не нужно ссылаться напрямую ни на один из этих дочерних элементов, поэтому можно просто удалить все эти атрибуты имен дочерних элементов.*

*Для быстрого удаления всех этих атрибутов Name можно использовать Visual Studio.  Используйте Найти->Заменить, задав для поля "Найти" значение «x:Name="*"», оставив значение поля "Заменить на" пустым" и установив в параметрах поиска флажок использования подстановочных знаков.

Шаг 3. Рис. 3. XAML в качестве готового словаря ресурсов ResourceDictionary 

<ResourceDictionary xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
            <Model3DGroup x:Key="sofa" >
          <!- Атрибуты x:Name удалены для всех дочерних элементов-->
         </Model3DGroup>
</ResourceDictionary>

Последний шаг делает нашу XAML-модель (ResourceDictionary) готовой к использованию для добавления в наше приложение App.xaml, чтобы она загружалась при запуске приложения (см. рис. "Шаг 3. Рис. 4")

Шаг 3. Рис. 4. App.xaml с добавленной моделью ResourceDictionary 

<Application x:Class="_3dsToXaml.App"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Models\sofa.xaml"/>
                <ResourceDictionary Source="Models\table.xaml"/>
                <ResourceDictionary Source="Models\tv.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Шаг 4. Создание базового класса для моделей и их добавление в сцену

Чтобы использовать наши вновь созданные ресурсы моделей, понадобится 3D-элемент для их отображения.  Для этой цели отлично подходит UIElement3D.  Можно просто создать новый объект UIElement3D и задать его свойство VisualModel3D равным содержимому нашего ресурса.  Так как это действие будет выполняться для каждого ресурса модели, я создал класс, производный от UIElement3D, чтобы инкапсулировать эту функциональную возможность (см. рис. "Шаг 4. Рис. 1")  В базовый класс также добавлен метод "Move", который будет использоваться для правильного размещения наших моделей после их добавления в сцену (более подробно о методе Move см. в шаге 5).

Шаг 4. Рис. 1. Класс, производный от UIElement3D, для отображения наших моделей

class ModelBase : UIElement3D
{
        public ModelBase(string resourceKey)
        {
            this.Visual3DModel = Application.Current.Resources[resourceKey] as Model3DGroup;
            Debug.Assert(this.Visual3DModel != null);
        }
        public void Move(double offsetX, double offsetY, double offsetZ, double angle)
        {
            Transform3DGroup transform = new Transform3DGroup();
            RotateTransform3D rotateTrans = new RotateTransform3D();
            rotateTrans.Rotation = new AxisAngleRotation3D(new Vector3D(0, 1, 0), angle);
            TranslateTransform3D translateTrans = new TranslateTransform3D(offsetX, offsetY, offsetZ);
            transform.Children.Add(rotateTrans);
            transform.Children.Add(translateTrans);
            this.Transform = transform;
        }
}

После определения базового класса добавление моделей в сцену становится достаточно простым.  Просто вернемся к шагу 1, где мы создали в нашем главном окне XAML контейнер ContainerUIElement3D для размещения мебели.   Все, что понадобится для добавления моделей в сцену — создать объект ModelBase и добавить его в ContainerUIElement3D (рис. "Шаг 4. Рис. 2")

Шаг 4. Рис. 2. Добавление экземпляров модели мебели в сцену

private void CreateScene()
{
    ModelBase sofa1 = new ModelBase("sofa");
    this.FurnitureContainer.Children.Add(sofa1);
    ModelBase sofa2 = new ModelBase("sofa");
    this.FurnitureContainer.Children.Add(sofa1);
    ModelBase table = new ModelBase("table");
    this.FurnitureContainer.Children.Add(table);
    ModelBase tv = new ModelBase("tv");
    this.FurnitureContainer.Children.Add(tv);
}

Теперь у нас есть сцена с полом, двумя диванами, столом и телевизором — гостиная, по которой можно пройтись.   Но модели еще не подвинуты в нужные положения.  Хуже всего, что мы не представляем себе относительного размера этих моделей.  Если запустить сцену в этот момент, мы увидим предметы мебели различного размера, сваленные в центре пола.   Пора прибраться в нашей гостиной и закончить сцену.

Шаг 5. Очистка модели и сцены

Первым действием для завершения сцены гостиной является масштабирование моделей, чтобы придать смысл их размеру.  Насколько велики эти модели?  Ответ — это зависит от обстоятельств.  В WPF 3D все единицы относительны.  Наш диван может казаться огромным, а столик настолько маленьким, что его даже нельзя будет увидеть.  Определяющими являются следующие моменты: a) какие числа разработчик использовал при создании модели, и b) какие числа мы использовали при создании окна просмотра и камеры.  Если модели получены от различных художников, заранее перед настройкой окна просмотра и камеры ничего неизвестно о координатах, предметы могут выглядеть несоразмерными, пока мы не настроим масштаб до чего-то разумно выглядящего.

К счастью в корневом элементе Model3DGroup в XAML для каждой из наших моделей уже определены преобразование Transform и элемент Transform3DGroup, содержащий ScaleTransform.  Для получения правильного размера моделей понадобится только поэкспериментировать с различными значениями для масштаба x, y и z, пока не будет найден правильный масштаб (чтобы не исказить модель, нам понадобится менять масштаб равномерно).

Шаг 5. Рис. 1. Эксперимент с ScaleTransform корневого элемента Model3DGroup для определения правильного размера

<ResourceDictionary xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
            <Model3DGroup x:Key="sofa" >
                <Model3DGroup.Transform>
                    <Transform3DGroup>
                        <TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
                        <ScaleTransform3D ScaleX="0.023" ScaleY="0.023" ScaleZ="0.023"/>
                        <RotateTransform3D>
                            <RotateTransform3D.Rotation>
                                <AxisAngleRotation3D Angle="0" Axis="0 1 0"/>
                            </RotateTransform3D.Rotation>
                        </RotateTransform3D>
                        <TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
                    </Transform3DGroup>
                </Model3DGroup.Transform>
                ...
             </Model3DGroup x:Key="sofa" >
</ResourceDictionary>

После подбора правильного масштаба все, что осталось — разместить модели на полу, чтобы они создали впечатление приличной гостиной.  Мы уже добавили в наш класс ModelBase функцию "Move", которая возьмет на себя выполнение преобразований перемещения и поворота (т.е. перемещение моделей по полу и их вращение так, чтобы они были повернуты в нужную сторону, соответственно).  Так как нам нужно, чтобы вся наша мебель стояла на полу, а не висела в воздухе и не была вкопана в землю, нужно обратить внимание на соответствующие перемещения по осям x и z и на поворот вокруг оси y.

Шаг 5. Рис. 2. Расстановка мебели по местам

private void CreateScene()
{
    ModelBase sofa1 = new ModelBase("sofa");
    this.FurnitureContainer.Children.Add(sofa1); //добавляет первый диван в середину пола
    // переместить к задней кромке пола
    // Это будет преобразование -6 Z, но ого поместит центр дивана вдоль задней
    // кромки.
    // Нам нужно, чтобы спинка дивана шла вдоль задней кромки, поэтому надо вычесть половину глубины     
    //дивана (примерно 1.2)
    sofa1.Move(0, 0, -4.8, 0);
    ModelBase sofa2 = new ModelBase("sofa");
    this.FurnitureContainer.Children.Add(sofa2); //adds the second sofa to the middle of the floor
    // повернуть переместить к задней кромке пола
    sofa2.Move(-4.8, 0, 0, 90);
    ModelBase table = new ModelBase("table");
    this.FurnitureContainer.Children.Add(table);
    ModelBase tv = new ModelBase("tv");
    this.FurnitureContainer.Children.Add(tv);
    tv.Move(5.5, 0, 0, -90);
}

Заключение

Поздравляем с созданием полностью работоспособной 3D-сцены WPF из 3DS-файлов, преобразованных в ресурсы XAML, дополненной функциональными возможностями виртуального трекбола.  Надеюсь, что эта статья будет вам полезна.  Если нет — создайте 3D-сцену WPF для озера и прыгните в него.