[Из песочницы] Визуализация простой геометрии в WPF

Что такое геометрия модели


Для работы с 3D моделями мы используем специальные конвейеры обработки — OpenGL и DirectX. Когда конвейеры строят картину, они используют информацию:
  • о модели — её материале, геометрии и текстурах,
  • о сцене — освещении и настройке камеры.

Любая модель начинается с геометрии. Геометрия модели — это набор точек в трехмерном пространстве и набор треугольников из этих точек. Треугольник компланарен — он лежит в плоскости, в отличие от фигур с большим числом точек, которые в общем случае не лежат в плоскости.

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

В примере мы сосредоточимся на геометрии модели, остальное будем использовать по мере необходимости. В качестве примера модели вполне подойдет Дельфин ниже:

image

Благодаря технологии WPF мы создаем интерактивные интерфейсы приложений и работаем с 3D-графикой. В примере мы используем стандартные возможности архитектуры WPF: привяжем данные и на их основе разделим модель данных и представление данных (MVVM).

Основной элемент для отображения 3D-содержимого в библиотеке WPF — Viewport3D. Например, свойство Camera устанавливает камеру, и мы видим сцену. Второе необходимое свойство Viewport3D — Children, коллекция элементов абстрактного типа Visual3D. Конкретная реализация этого класса — класс ModelVisual3D: чтобы его использовать, нужно указать свойство Content абстрактного типа Model3D.

Основные классы для установки свойства Content:

  • GeometryModel3D — отображает одну модель,
  • Light — модель источника света,
  • Model3DGroup — помогает создавать модели с разными материалами.

Необходимые свойства будем устанавливать по привязке.

Модель данных MVVM


В широком смысле любое приложение решает определенную задачу. Модель должна полностью отражать данные в решаемой приложением задаче. Мы упростим пример и исключим нормали — они будут определяться по умолчанию. Нормали важны для отображения текстур или заливки при расчете освещенности.

Подберем основные интерфейсы, которые определяют сущности модели данных MVVM и их связи:

interface IModel3DSet {
        string Description { get; set; }
        ICollection Models { get; }
    }

interface IModel3D {
        string Description { get; set; }
        ICollection Points { get; }
        ICollection Triangles { get; }
    }

interface IPoint3D {
        double X { get; set; }
        double Y { get; set; }
        double Z { get; set; }
        string Coordinates { get; }

        IVector3D DistanceTo(IPoint3D endPoint);
    }

interface ITriangle3D {
        IModel3D Model3D { get; }
        IPoint3D Point1 { get; set; }
        IPoint3D Point2 { get; set; }
        IPoint3D Point3 { get; set; }
    }

interface IVector3D {
        double X { get; set; }
        double Y { get; set; }
        double Z { get; set; }
        double Norm { get; }

        IVector3D Add(IVector3D vector);
        IVector3D Subtract(IVector3D vector);
        IVector3D Multiply(double factor);

        IVector3D CrossProduct(IVector3D vector);
        double DotProduct(IVector3D vector);
    }

Конкретная реализация прямолинейна. Чтобы предупреждать об изменении свойств, используем привычный INotifyPropertyChanged.

ViewModel


В качестве базового класса для ViewModel мы используем:
public abstract class BaseVm : Notifier {
        TModel _model;
        public TModel Model {
            get { return _model; }
            set {
                _model = value;
                NotifyWithCallerPropName();
            }
        }
    }

public abstract class BaseVm : BaseVm {
        public BaseVm(TModel model = default(TModel), TParentVM parentVM = default(TParentVM)) {
            Model = model;
            Parent = parentVM;
        }
        public TParentVM Parent { get; }
    }

Такая структура удобна тем, что позволяет двигаться по иерархии ViewModel в привязках. Классы Vector3D, Triangle3D, Point3D просты, поэтому создавать для них ViewModel не обязательно. Значит нам нужны только два класса ViewModel — Model3DSetVm и Model3DVm.

Представления


Чтобы построить представления, используем подстановку WPF с помощью атрибута DataType=»{x: Type local: Type}» при объявлении DataTemplate в словарях ресурсов. В остальном реализация стандартная. Приложение для демонстрации выглядит так:

0ca7f32fce744eb085bda852d7a64e04.png

Что еще нужно знать


  1. В WPF свойство TriangleIndices не обязательное. Если оно не задано, по умолчанию будут созданы треугольники по каждой тройке точек. По этой причине даже при пустом наборе треугольников отображаются грани.
  2. Чтобы создать привязку для коллекции моделей, можно использовать привязку из нашего примера, но подставьте экземпляр Model3DGroup вместо GeometryModel3D. Использовать для этого привязку к свойству Children Viewport3D не получится.
  3. Если мы изменим геометрию или ось вращения, модель перестроится, изображение будет дергаться — анимация начнется заново. Чтобы процесс не прерывался, сохраняйте промежуточные значения модели и применяйте к ним анимацию.

Проект с примером можно найти здесь…

Буду рад, если статья вам в чем-то поможет…

Комментарии (2)

  • 13 апреля 2017 в 12:42

    0

    Возможно, глупый вопрос, но почему ICollection, а не IList? Какие вообще преимущества у коллекции перед списком? Никак не могу найти сравнительную таблицу всех этих списочно-коллекционных сущностей в дотнете.
    • 13 апреля 2017 в 15:13 (комментарий был изменён)

      0

      А таблицу и не нужно искать. Просто посмотрите имплементации одного интерфейса и другого.
      public interface ICollection : IEnumerable, IEnumerable
      {
          int Count { get; }
          bool IsReadOnly { get; }
       
          void Add(T item);
          void Clear();
          bool Contains(T item);
          void CopyTo(T[] array, int arrayIndex);
          bool Remove(T item);
      }
      
      
      public interface IList : ICollection, IEnumerable, IEnumerable
      {
          T this[int index] { get; set; }
       
          int IndexOf(T item);
          void Insert(int index, T item);
          void RemoveAt(int index);
      }
      

      IList наследуется от ICollection, и поддерживает все тоже самое + индексация объектов в коллекции. Т.е. если вам важен порядок объектов в коллекции используйте IList, если порядок не важен и вы нигде не обращаетесь по его индексу (вставляете/удаляете по индексу), то используйте ICollection. Смысл в том, чтобы не использовать, то что не нужно.

© Habrahabr.ru