[Из песочницы] Визуализация простой геометрии в WPF
Что такое геометрия модели
Для работы с 3D моделями мы используем специальные конвейеры обработки — OpenGL и DirectX. Когда конвейеры строят картину, они используют информацию:
- о модели — её материале, геометрии и текстурах,
- о сцене — освещении и настройке камеры.
Любая модель начинается с геометрии. Геометрия модели — это набор точек в трехмерном пространстве и набор треугольников из этих точек. Треугольник компланарен — он лежит в плоскости, в отличие от фигур с большим числом точек, которые в общем случае не лежат в плоскости.
Если задать у треугольника направление обхода границы, он станет ориентированным треугольником. У ориентированного треугольника в трехмерном пространстве можно выделить внутреннюю и внешнюю стороны. По направлению обхода границы мы также определяем единственный по направлению вектор нормали для каждого треугольника. Так, в геометрию модели, помимо точек и треугольников, входит набор нормалей к каждому из треугольников. При отображении модели точки модели соответствуют точкам, треугольники модели соответствуют граням.
В примере мы сосредоточимся на геометрии модели, остальное будем использовать по мере необходимости. В качестве примера модели вполне подойдет Дельфин ниже:
Благодаря технологии 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 в словарях ресурсов. В остальном реализация стандартная. Приложение для демонстрации выглядит так:
Что еще нужно знать
- В WPF свойство TriangleIndices не обязательное. Если оно не задано, по умолчанию будут созданы треугольники по каждой тройке точек. По этой причине даже при пустом наборе треугольников отображаются грани.
- Чтобы создать привязку для коллекции моделей, можно использовать привязку из нашего примера, но подставьте экземпляр Model3DGroup вместо GeometryModel3D. Использовать для этого привязку к свойству Children Viewport3D не получится.
- Если мы изменим геометрию или ось вращения, модель перестроится, изображение будет дергаться — анимация начнется заново. Чтобы процесс не прерывался, сохраняйте промежуточные значения модели и применяйте к ним анимацию.
Проект с примером можно найти здесь…
Буду рад, если статья вам в чем-то поможет…
Комментарии (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. Смысл в том, чтобы не использовать, то что не нужно.