В данной статье я расскажу о основных принципах реализации трехмерной графики посредством WPF. Я начну из небольшого вводного теоретического курса для того чтобы разобраться с основами построения 3d-объектов, расскажу о предоставляемых средствах WPF, необходимых для реализации всего этого, затем перейдем к практической реализации; будет создан простой куб, демонстрирующий исключительно основы трехмерной графики, и далее создадим полноценный вращающийся куб с контентом на его сторонах.
От читателя не требуется какого-либо предыдущего опыта в работе с 3d-графикой, необходимо только знание простой пространственной геометрии на базовом уровне, т.е. понимать 3-мерную систему координат и уметь представить как будет выглядеть фигура на ней. Хотя статья и предназначена в основном для новичков, я уверен что и профессионалы, работавшие с 3d раньше, смогут найти здесь что-то для себя интересное, в частности узнать как ведётся реализация 3d-графики в WPF.
Начнем. Каждая трехмерная фигура состоит как минимум из вершин и плоскостей между этими вершинами. Для получения хотя бы одной плоскости необходимо как минимум 3 вершины. Следовательно, наименьшей единицей плоскости есть треугольник, именно с помощью треугольников и строятся все трехмерные фигуры; чем сложней фигура тем из большего числа треугольников(и вершин разумеется) она будет составляться. Что нужно для того чтобы построить простую трехмерную фигуру? На самом деле ничего сложного. Необходимо просто указать вершины этой фигуры в виде набора точек, затем используя порядочные номера этих точек, указать набор треугольников которые будут составлять нашу фигуру.
Пока теории думаю достаточно, начнем постепенно разбираться в нужных для нас средствах и переходить к реализации в WPF. Следующих 3 самых важных на мой взгляд объекта нужно рассмотреть в первую очередь:
GeometryModel3D. Я буду называть этот объект геометрией. Он представляет трехмерную фигуру на основе следующих геометрических данных: Positions - набор координат, представляющих вершины данной фигуры. TriangleIndices - набор индексов которые представляют треугольники, представляющие плоскости. TextureCoordinates - координаты текстур(об этом позже). Normals - векторы нормалей. Служат исключительно для настроек осветления фигуры. В этой статье рассматриваться не будут. Также в GeometryModel3D указывается материал, который будет влиять на изображение фигуры. В нашем случае это материал DiffuseMaterial, представляющий простую кисть(Brush) Где конкретно задаются эти свойства и как именно думаю сейчас не так важно, это станет ясно немного позже. Еще следует заметить. Объекты GeometryModel3D можно объединять в группы(Model3DGroup), которые в свою очередь могут содержать другие группы.
ModelVisual3D. Это объект, называемый моделью, представляющий геометрию(GeometryModel3D), группу геометрий или контент других типов, в частности осветления(DirectionalLight например).
Viewport3D. Объект предоставляющий всю нашу 3d-фигуру на 2-мерной плоскости. Строиться он в основном из 3d-моделей(ModelVisual3D), но допустимы и другие объекты. В нашем случае Viewport3D будет содержать одну модель, содержащую геометрию и другую модель, содержащую осветление. В объекта Viewport3D имеется также свойство представляющее камеру(экземпляр PerspectiveCamera). Это собственно та вещь, через которую мы смотрим на нашу 3d-фигуру(с определенного расстояния и в определенном направлении).
К этому моменту я полагаю вы уже поняли какую гибкость нам предоставляют средства WPF для построения трехмерных объектов. Каждый 3d-объект можно строить из множества меньших объектов(те в свою очередь с еще меньших и т.д.), некоторые из них объединять в группы и по отдельности управлять поведением этих объектов(или групп), делая им например Rotate, или другую операцию.
Сейчас перейдем к реализации простого 3D-куба. Давайте сделаем простую заготовку, содержащую Viewport3D, в котором есть две модели, которые будут содержать осветление и геометрию фигуры соответственно.
<Grid x:Name="LayoutRoot"> <Viewport3D Height="300" Width="300" > <Viewport3D.Camera> <PerspectiveCamera Position=... LookDirection=... /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <DirectionalLight Direction=... /> </ModelVisual3D.Content> </ModelVisual3D> <ModelVisual3D> <ModelVisual3D.Content> <GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D Positions=... TriangleIndices=.../> </GeometryModel3D.Geometry> <GeometryModel3D.Material> <DiffuseMaterial> <DiffuseMaterial.Brush> <SolidColorBrush Color="GreenYellow" /> </DiffuseMaterial.Brush> </DiffuseMaterial> </GeometryModel3D.Material> </GeometryModel3D> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </Grid>
Приступим к реализации геометрии нашего куба, а именно зададим свойства Positions и TriangleIndices. Сначала представим как будет выглядеть наш куб на трехмерной системе координат: Как видим длинна стороны равна одной единице. На самом деле не важно какой размер длинны мы зададим, важно только чтобы соблюдались необходимые пропорции(в нашем случае длинна стороны и расстояние от камеры к центру куба). Очень важным также есть порядок задания вершин куба, на рисунке вы видете каждую вершину и ее номер. Предлагаю в такой же последовательности внести значения в свойство Positions.
<Positions="0 0 0 1 0 0 1 1 0 0 1 0 0 0 1 1 0 1 1 1 1 0 1 1"
И так, у нас есть вершины куба, теперь необходимо соединить их плоскостями, для этого, как я уже говорил, используются треугольники. Нам потребуется 12 треугольников(6 сторон * 2) чтобы соединить все вершины нашего куба нужным способом. Эта задача наверное самая интересная. На первый взгляд все может казаться просто - достаточно указать 12 групп необходимых вершин(их номеров), образующих треугольники и все будет готово. На самом деле есть еще один нюанс: плоскость (та что образована треугольником) есть видимой только с одной стороны, следовательно нужно как-то задать с какой стороны наш треугольник будет видимым. Указывается это совсем не сложно, все зависит от порядка указания этих трех индексов, образующих каждый треугольник; если указывать индексы в том порядке в результате какого треугольник будет образовываться против часовой стрелки, то этот треугольник будет видимым с нашей стороны и не будет видимым с обратной, ну и если за часовой стрелкой то соответственно наоборот. Как не запутаться? На самом деле просто. Посмотрите еще раз на рисунок выше. Обратите внимание на стороны куба которые видимы и которые не видимы(а станут видимыми только при вращении куба). Возьмем видимую сторону с вершинами (0,1,5,4), смотря на рисунок, зададим 2 треугольника, образующих эту сторону. Поскольку сторона видимая, треугольники должны задаваться в порядке против часовой стрелки. Для первого треугольника группа индексов может быть например одной из следующей: «0,5,4», «5,4,0», или «4,0,5», но не «0,4,5»(поскольку это будет за часовой стрелкой). Во втором треугольнике нашей стороны индексы установим например такие: «0,1,5».
Теперь возьмем невидимую сторону с вершинами (0,4,7,3), тоже зададим 2 треугольника, образующих эту сторону, но на этот раз в порядке за часовой стрелкой(поскольку когда сторона повернется к нам, то индексы будут выглядеть так, как будто они указаны против часовой стрелки, в результате чего сторона станет видимой) Индекс первого треугольника может быть таким: «0,4,3» Индекс второго - таким: «4,7,3»
Полностью заполненное свойство TriangleIndices может иметь такое значение:
<TriangleIndices="3 2 1 3 1 0 6 1 2 6 5 1 7 5 6 7 4 5 7 3 4 3 0 4 3 6 2 3 7 6 0 1 5 0 5 4" />
На данном этапе, мы установили свойство GeometryModel3D.Geometry. Сейчас остановимся на камере: Свойство Position представляет 3D-координату, место в котором камера установлена. LookDirection представляет вектор(не путайте с точкой), куда камера направлена. Значения вектора вычисляються следующим образом: x=x2-x1, y=y2-y1, z=z2-z1, где (x1,y1,z1) - координата начальной точки(в нашем случае камера), (x2,y2,z2) - координата конечной точки(в нашем случае центр куба). Наша камера будет находиться сверху над кубом на расстоянии 4 единицы от начала осей X и Y и соответственно на расстоянии 3 от поверхности куба и будет «смотреть» в направлении центра куба. Зададим соответственные значения для камеры:
<PerspectiveCamera Position="0.5 0.5 4" LookDirection="0 0 -3.5" />
Направление осветления зададим такое же как и свойство LookDirection камеры:
<DirectionalLight Direction="0 0 -3.5" />
Как видите ничего сложного и сверхъестественного в трехмерной графике нет(как это может казаться на первый взгляд).
Пришло время разместить контент на сторонах нашего куба. Нам понадобиться новый объект Viewport2DVisual3D, который в нашем случае будет представлять одну сторону нашего куба вместе с контентом. Придеться также немного изменить структуру всего нашего объекта. Освещение сейчас задействовано не будет, объект Viewport3D будет теперь содержать одну модель(ModelVisual3D) которая содержит шесть объектов Viewport2DVisual3D. На каждой стороне в объекте DiffuseMaterial необходимо установить свойство Viewport2DVisual3D.IsVisualHostMaterial в True, указывающее на то что мы будем использовать собственный интерактивный контент. Геометрию (MeshGeometry3D) придется наполнить еще одним свойством: TextureCoordinates. Это свойство представляет ту часть материала, которая будет отображаться на нашей стороне (Viewport2DVisual3D). Материал на нашей стороне представлен в форме квадрата со сторонами (0,0 0,1 1,1 1,0). Свойство TextureCoordinates - это сопоставление вершин (Positions) и 2D-координат точек, образующих текстуру(материал). Одно замечание: по неизвестной мне причине, верхней части материала должна отвечать нижняя часть текстуры, иначе контент на стороне будет перевернутым. Ниже пример стороны куба, со свойством TextureCoordinates:
<Viewport2DVisual3D> <Viewport2DVisual3D.Geometry> <MeshGeometry3D Positions="0 1 1 0 0 1 1 0 1 1 1 1" TriangleIndices="0 1 2 2 3 0" TextureCoordinates="0,0 0,1 1,1 1,0"/> </Viewport2DVisual3D.Geometry> <Viewport2DVisual3D.Material> <DiffuseMaterial Viewport2DVisual3D.IsVisualHostMaterial="True"/> </Viewport2DVisual3D.Material> <Border CornerRadius="0" Width="200" Height="200" Background="Green"> <Button Width="60" Height="23" /> </Border> </Viewport2DVisual3D>
В моем свойстве TextureCoordinates, вершине (0,1,1) отвечает координата текстуры (0,0) и т.д. Еще раз обратите внимание на важность порядка задания вершин (Positions). В области для контента скорее всего нужно будет всегда задавать фиксированные размеры родительского элемента, сами подумайте WPF не может знать сам сколько пикселей будет уделяться на нашу «геометрическую единицу» длинны, кроме того выставляйте размеры стороны учитывая то, что камера может отдаляться от фигуры и приближаться к ней(операция Zoom), следовательно фигура будет уменьшаться или увеличиваться. Я в своем примере выставил размеры объекта Border в (200,200). Если этого не задать, то элементы контента будут ресайзиться до максимума, не соблюдая пропорции.
В общем это все. Рутинную работу по добавлению еще пяти сторон к кубу полагаю описывать не нужно. Также я в этой статье не буду уделять особого внимания вращению куба и операции Zoom.
Zoom у меня реализован простым изменением расстояния камеры к моему кубу. Вращение я реализовал с помощью операций Rotate над моей моделью (ModelVisual3D), содержащей шесть сторон(Viewport2DVisual3D). Есть еще способ реализовать вращение, меняя местоположение камеры(извращенцы могут попытаться комбинировать то и другое). В моем примере есть 3 ползунка(слайдера), делающие Rotate по осям X,Y,Z соответственно. Было бы очень хорошо убрать третий слайдер и сделать вращение нужной стороны по оси Z автоматическим(в то время когда сторона скрыта например), это уже конечно для желающих добавить что-то новое
Полезные ссылки по данной теме:
Ну вот, собственно все. Надеюсь моя статья вам понравилась, если что не так, прошу камнями не забрасывать Спасибо за внимание