На главную
ПРАВИЛА FAQ Помощь Участники Календарь Избранное DigiMania RSS
msm.ru
Модераторы: maxim84_, juice
  
> WPF. Создание вращающегося 3D-куба с интерактивным контентом на его сторонах
    В данной статье я расскажу о основных принципах реализации трехмерной графики посредством WPF. Я начну из небольшого вводного теоритического курса для того чтобы разобраться с основами построения 3d-объектов, расскажу о предоставляемых средствах WPF, необходимых для реализации всего этого, затем перейдем к практической реализации; будет создан простой куб, демонстрирующий исключительно основы трехмерной графики, и далее создадим полноценный вращающийся куб с контентом на его сторонах.
    user posted image

    От читателя не требуеться какого-либо предыдущого опыта в работе с 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, в котором есть две модели, которые будут содержать осветление и геометрию фигуры соответственно.
    ExpandedWrap disabled
          <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. Сначала представим как будет выглядеть наш куб на трехмерной системе координат:
    user posted image
    Как видим длинна стороны равна одной единице. На самом деле не важно какой размер длинны мы зададим, важно только чтобы соблюдались необходимые пропорции(в нашем случае длинна стороны и расстояние от камеры к центру куба). Очень важным также есть порядок задания вершин куба, на рисунке вы видете каждую вершину и ее номер. Предлагаю в такой же последовательности внести значения в свойство Positions.
    ExpandedWrap disabled
      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 может иметь такое значение:
    ExpandedWrap disabled
      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" />

    Как видите ничего сложного и сверхистественного в трехмерной графике нет(как это может казаться на первый взгляд).
    Здесь я выложил готовый тестовый проект, в нем также добавил функциональность необходимую для вращения нашого куба и для Zoom'а.
    http://rapidshare.com/files/363638128/MySimpleCube.rar.html

    Пришло время разместить контент на сторонах нашего куба. Нам понадобиться новый объект Viewport2DVisual3D, который в нашем случае будет представлять одну сторону нашего куба вместе с контентом. Придеться также немного изменить структуру всего нашого объекта. Освещение сейчас задействовано не будет, объект Viewport3D будет теперь содержать одну модель(ModelVisual3D) которая содержит шесть объектов Viewport2DVisual3D. На каждой стороне в объекте DiffuseMaterial необходимо установить свойство Viewport2DVisual3D.IsVisualHostMaterial в True, указывающее на то что мы будем использовать собственный интерактивный контент. Геометрию(MeshGeometry3D) придеться наполнить еще одним свойством: TextureCoordinates. Это свойство представляет ту часть материала, которая будет отображаться на нашей стороне(Viewport2DVisual3D). Материал на нашей стороне представлен в форме квадрата со сторонами (0,0 0,1 1,1 1,0). Свойство TextureCoordinates - это сопоставление вершин(Positions) и 2D-координат точек, образующих текстуру(материал). Одно замечание: по неизвестной мне причине, верхней части материала должна отвечать нижняя часть текстуры, иначе контент на стороне будет перевернутым.
    Ниже пример стороны куба, со свойством TextureCoordinates:
    ExpandedWrap disabled
                      <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. Всю это реализацию вы посмотрите в готовом коде, который я выклал сюда:
    http://rapidshare.com/files/363638128/MySimpleCube.rar.html

    Zoom у меня реализован простым изменением расстояния камеры к моему кубу. Вращение я реализовал с помощю операций Rotate над моей моделью(ModelVisual3D), содержащей шесть сторон(Viewport2DVisual3D). Есть еще способ реализовать вращение, меняя местоположение камеры(извращенцы могут попытаться комбинировать то и другое). В моем примере есть 3 ползунка(слайдера), делающие Rotate по осям X,Y,Z соответсвенно. Было бы очень хорошо убрать третий слайдер и сделать вращение нужной стороны по оси Z автоматическим(в то время когда сторона скрыта например), это уже конечно для желающих добавить что-то новое :)

    Полезные ссылки по данной теме:
    http://www.kindohm.com/technical/WPF3DTutorial.htm
    http://stuff.seans.com/2008/08/13/drawing-a-cube-in-wpf/
    http://blogs.msdn.com/danlehen/archive/2005/11/06/489627.aspx

    Ну вот, собственно все. Надеюсь моя статья вам понравилась, если что не так, прошу камнями не забрасывать
    Спасибо за внимание:)
    Сообщение отредактировано: 2005fs -
      Спасибо. Очень интересно было почитать. Технология новая и материалов на русском не так много.
        Добрый день, да статья отличная, я попросил бы автора поменять ссылку, т.к. скачать готовый пример не получается. Проблема возникла с вращением, вот хотелось бы в реальности глянуть как все работает.
        http://rapidshare.com/files/363638128/MySimpleCube.rar.html

        Спасибо
        С Уважением, Александр!
        Выход есть всегда, не всегда в ту дверь, в которую хочется...
        0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
        0 пользователей:


        Рейтинг@Mail.ru
        [ Script Execution time: 0,0918 ]   [ 17 queries used ]   [ Generated: 26.09.17, 00:30 GMT ]