Создание трехмерных объектов в Silverlight

OSzone.net » Microsoft » Разработка приложений » Silverlight » Создание трехмерных объектов в Silverlight
Автор: Раджеш Лал
Иcточник: MSDN Magazine
Опубликована: 05.04.2012

В этой статье я покажу, как разрабатывать трехмерные объекты (далее для краткости — 3D-объекты) в Silverlight. Начну с базовой информации о трехмерной графике, а затем перейду к более продвинутым средствам в Silverlight для создания и отображения 3D-объектов. Я возьму простой пример с кубом и покажу три способа создания 3D-преобразований. Я также объясню, какие ключевые элементы понадобятся для отображения 3D-объекта на экране. Наконец, мы изучим, как Silverlight 5 позволит вам выйти за рамки того, что доступно сейчас, и создавать гораздо более сложные 3D-объекты.

Silverlight поддерживает правую систему координат, т. е. положительная ось z направлена к наблюдателю (рис. 1). Для отображения объекта на экране требуются три основных элемента трехмерной графики:

*
Рис. 1. Эталонный куб, стороны которого показываются с применением перспективы

Перспектива означает, что части объектов, находящиеся ближе к нам, кажутся крупнее, чем более удаленные. Например, на рис. 1 сторона bd выглядит больше, чем сторона fh. В реальности перспектива создает точку исчезновения, т. е., если вы продолжите линии ae, bf, cg и dh по оси z, они пересекутся где-то далеко в одной произвольной точке.

Второй аспект — преобразование. 3D-объект, показываемый на экране, должен разрешать перемещение в трехмерном пространстве в любом направлении. Его можно перемещать по любой одной оси (изменять размер), сохраняя перспективу. Его можно вращать на 360 градусов по всем осям: x, y и z. Это придает 3D-объекту гибкость, необходимую для визуализации на экране.

Последний элемент в трехмерной графике — эффект освещения. Освещение создает полутона, которые ярче рядом с источником света и постепенно затемняются по мере удаления от источника. В рендеринге 3D-изображений популярны два вида заполнения поверхностей (shading): линейное (flat) и градиентное (gradient). Чем они отличаются, я поясню позже. Освещение также создает тени на сторонах, противоположных источнику света.

В примерах в этой статье мы изучим три способа создания 3D-объектов в Silverlight:

При первом способе объект создается из двухмерных элементов, но выглядит и ведет себя так, будто он находится в трехмерном пространстве. Трехмерная перспектива (Perspective 3D) — особая разновидность механизма преобразований, введенного в Silverlight 4 и обеспечивающего базовые преобразования, такие как вращение, масштабирование и трансляцию в трехмерном пространстве. Во втором способе создается не 3D-объект, а конечные кадры для конкретного преобразования, и они отображаются по таймеру. Последний способ предполагает последовательное построение сложного 3D-объекта с помощью примитивов (списка треугольников) и использованием библиотеки XNA, которая доступна в Silverlight 5. Приступим.

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

Silverlight 4 поддерживает класс PlaneProjection (рис. 2), который можно использовать для свойства Projection любого UI-элемента, как показано на диаграмме класса. Класс PlaneProjection обеспечивает 3D-преобразования UI-элемента. Хотя он не позволяет напрямую создавать 3D-объект, вы можете использовать несколько «стен» («walls»), чтобы создать объект и преобразовывать его в трехмерном пространстве.

*
Рис. 2. Класс PlaneProjection

Класс PlaneProjection поддерживает LocalOffset и GlobalOffset, которые применяются при трансляции объекта относительно себя и относительно другого элемента в глобальном пространстве. RotationX, RotationY и RotationZ позволяют вращать элемент по осям x, y и z, а CenterOfRotation — вокруг центральной точки относительно плоскости элемента.

В моем примере, чтобы построить «куб», я создам его четыре стороны и буду перемещать их в трехмерном пространстве, изменяя свойства PlaneProjection, как показано на рис. 3.

Рис. 3. Задание свойств PlaneProjection

<Grid x:Name="LayoutRoot" Background="White"
   Width="800" Height="700">
  <Rectangle Fill="#9900FF00" Width="250" Height="250"
    Visibility="Visible">
  <Rectangle.Projection>
    <PlaneProjection x:Name= "projectionFront"
      CenterOfRotationZ="125" RotationX="-180"/>
  </Rectangle.Projection>
</Rectangle>
<Rectangle Fill="#99FF0000" Width="250" Height="250"
    Visibility="Visible">
  <Rectangle.Projection>
    <PlaneProjection x:Name= "projectionBottom"
      CenterOfRotationZ="125" RotationX="-90" />
  </Rectangle.Projection>
</Rectangle>
<Rectangle Fill="#990000FF" Width="250" Height="250"
    Visibility="Visible">
  <Rectangle.Projection>
    <PlaneProjection x:Name="projectionBack"
      CenterOfRotationZ="125" />
  </Rectangle.Projection>
 </Rectangle>
<Rectangle Fill="#99FFFF00" Width="250" Height="250"
    Visibility="Visible">
  <Rectangle.Projection>
    <PlaneProjection x:Name=
      "projectionTop" CenterOfRotationZ="125" RotationX="90"/>
  </Rectangle.Projection>
</Rectangle>
</Grid>

На рис. 4 стороны поворачиваются на 90, –90 и –180 градусов, чтобы построить верхнюю, нижнюю и переднюю плоскости проекции куба. Значение CenterOfRotationZ, равное 125, определяет центральную точку, относительно которой все плоскости можно поворачивать по оси z.

*
Увеличить

Рис. 4. Проекция сторон для имитации 3D-стены

Построив куб на плоскости проекции, я вращаю его по осям x, y и z. Здесь я использую объект раскадровки (storyboard object) вSilverlight. Я создаю три таких объекта — по одному на каждую ось (рис. 5).

Я смог легко создать куб и преобразовать его в трехмерном пространстве без написания большого объема кода.

<Storyboard x:Name="storyboardRotateX">
  <DoubleAnimation Storyboard.TargetName="projectionFront"
    Storyboard.TargetProperty="RotationX" From="-180.0"
    To="180.0" Duration="0:0:10" RepeatBehavior="Forever"  />
  <DoubleAnimation Storyboard.TargetName="projectionBottom"
    Storyboard.TargetProperty="RotationX" From="-90.0"
    To="270.0" Duration="0:0:10" RepeatBehavior="Forever"  />
  <DoubleAnimation Storyboard.TargetName="projectionBack"
    Storyboard.TargetProperty="RotationX" From="0.0"
    To="360.0" Duration="0:0:10" RepeatBehavior="Forever"   />
  <DoubleAnimation Storyboard.TargetName="projectionTop"
    Storyboard.TargetProperty="RotationX" From="90.0"
    To="450.0" Duration="0:0:10" RepeatBehavior="Forever"   />
  </Storyboard>
  <Storyboard x:Name="storyboardRotateY">
  <DoubleAnimation Storyboard.TargetName="projectionFront"
    Storyboard.TargetProperty="RotationY" From="0.0" To="360.0"
    Duration="0:0:10" RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionBottom"
    Storyboard.TargetProperty="RotationY" From="0.0" To="360.0"
    Duration="0:0:10" RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionBack"
    Storyboard.TargetProperty="RotationY" From="0.0" To="360.0"
    Duration="0:0:10" RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionTop"
    Storyboard.TargetProperty="RotationY" From="0.0" To="360.0"
    Duration="0:0:10" RepeatBehavior="Forever" />
  </Storyboard>
  <Storyboard x:Name="storyboardRotateZ">
  <DoubleAnimation Storyboard.TargetName="projectionFront"
    Storyboard.TargetProperty="RotationZ" From="0.0" To="360.0"
    Duration="0:0:10" RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionBottom"
    Storyboard.TargetProperty="RotationZ" From="0.0" To="360.0"
    Duration="0:0:10" RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionBack"
    Storyboard.TargetProperty="RotationZ" From="0.0" To="360.0"
    Duration="0:0:10" RepeatBehavior="Forever" />
  <DoubleAnimation Storyboard.TargetName="projectionTop"
    Storyboard.TargetProperty="RotationZ" From="0.0" To="360.0"
    Duration="0:0:10" RepeatBehavior="Forever" />
</Storyboard>

В каждой раскадровке я вращаю каждую из четырех плоскостей проекции, чтобы сохранить структуру куба. Заметьте, что при вращении по оси x значение RotationX начинается с исходного значения RotationX плоскости и меняется на 360 градусов для ProjectionFront, поэтому оно начинается с –180 градусов и дозодит до 180 градусов. Как видно на рис. 6, куб готов к повороту по осям x, y и z; его можно перемещать по любой оси, и он поддерживает окрашивать каждую из своих сторон.

*
Рис. 6. Куб, готовый к вращению

В этом примере я смог легко создать куб и преобразовать его в трехмерном пространстве без написания большого объема кода. Это по-настоящему сильная сторона преобразования трехмерной перспективы. Для базовых 3D-операций вы должны использовать именно этот вариант. Однако ему свойствен ряд недостатков. В случае более сложных 3D-объектов количество необходимых плоскостей проекции и связанных с ними значений может возрасти просто катастрофически, и вам придется вручную определять углы между каждой плоскостью проекции и CenterOfRotation. Вторая проблема в том, что вращение 3D-объекта зависит от раскадровок, которые требуют интенсивного использования центрального процессора и совсем не задействуют графический процессор (GPU) для рендеринга объекта. Еще одна проблема — вы должны выполнять рендеринг задней части куба, даже если она не видна, — это весьма не оптимальный подход.

Третий основной элемент, необходимый для отображения 3D-объектов на экране, — эффект освещения. В реальной жизни вас окружает свет. Как же имитировать это в трехмерном пространстве на экране? Как упоминалось, для этого существуют два распространенных способа: линейное заполнение поверхности (flat shading) и градиентное.

Линейное заполнение учитывает поверхность плоскости и применяет усредненное заполнение вдоль плоскости. Градиентное (затенение Гуро) использует градиент при заполнении поверхности и учитывает каждую из вершин плоскости. Плоскость заполняется не линейно, а скорее «плавно» — на основе разных цветов вершин.

В этом примере каждая из плоскостей допускает заполнение цветом (линейное заполнение), а также градиентную заливку (градиентное заполнение), которое можно использовать для имитации эффекта освещения. Но об этом мы поговорим позже. Быстро сымитировать эффект освещения можно наложением прямоугольника с радиальным градиентом и прозрачностью, используя следующий код:

<Rectangle x:Name=
    "BulbGradient" Height="700" Width="800" Margin="0 50 0 0"
    Grid.Row="1" Visibility="Collapsed">
  <Rectangle.Fill>
    <RadialGradientBrush RadiusX="0.5" RadiusY="0.5"
        GradientOrigin="0.25,0.25">
      <GradientStop Color="#00000000" Offset="0"/>
      <GradientStop Color="#FF000000" Offset="2"/>
     </RadialGradientBrush>
  </Rectangle.Fill>
</Rectangle>

Создание куба с использованием кадров

Второй способ — использование подготовленных кадров. В этом случае вы не создаете сам 3D-объект, а начинаете с нужного вам конечного результата и экспортируете его как индивидуальные кадры. Целый ряд программ для трехмерного моделирования позволяет создавать 3D-объекты и преобразования, которые можно экспортировать как набор кадров, а затем импортировать в Silverlight.

В этом примере я возьму простую анимацию куба и экспортирую его вращение по осям x, y и z в набор кадров изображений. На рис. 7 показаны восемь кадров вращения куба по оси x. В данном случае для имитации вращения куба я использую минимальный набор кадров, но увеличение количества кадров в секунду позволит добиться более плавного вращения.

*
Рис. 7. Восемь кадров, имитирующих вращение по оси x

Для имитации вращения в Silverlight я использую таймер, как показано в следующем коде:

DispatcherTimer timer = new DispatcherTimer();
timer.Interval = new TimeSpan(0, 0, 0, 0, 500);
timer.Tick += new EventHandler(Tick);

private void Tick(object o, EventArgs sender)
{
  string imageuri = "cube/" + axis + "/" +
    currentImageIndex + ".png";
  bgImage.Source = new BitmapImage(new Uri(imageuri,
    UriKind.RelativeOrAbsolute));
  if (currentImageIndex <= 8)
    currentImageIndex++;
  else
    currentImageIndex = 1;
}

Заметьте: это упрощенная версия того, что можно сделать этим методом. Здесь я использую экспортированные изображения для простоты, но ряд программ для трехмерного моделирования поддерживает экспорт XAML, что позволяет создавать полигоны с окраской и градиентами вместо изображений и тем самым получать гораздо более плавную анимацию. Кроме того, таймер можно заменить на раскадровки (storyboards) с анимациями.

Этот подход прямолинеен. Вам нужно конкретное преобразование с конкретным 3D-объектом. Вам незачем беспокоиться о создании 3D-объекта, и эта методика годится для построения любого 3D-объекта — не только простого куба. И может использоваться для всех видов преобразований. Вы можете выполнять трансляцию, вращение и масштабирование сложных 3D-объектов. Данный подход даже позволяет имитировать эффект освещения, который также напрямую транслируется из программы моделирования.

Главное ограничение этого подхода — утрата гибкости программирования 3D-объекта. Экспорт 3D-объекта приводит к генерации статического кода, изменение которого может оказаться весьма трудной задачей. Перемещать этот объект относительно других элементов в приложении Silverlight нельзя. Еще один недостаток в том, что количество необходимых кадров растет линейно при добавлении каждого преобразования. Помимо этого, рендеринг выполняется центральным процессором, поэтому более сложные анимации с большим количеством кадров могут привести к падению производительности.

Это приводит нас к третьему подходу с использованием библиотеки XNA в предстоящем выпуске Silverlight 5, который, как вы увидите, устраняет большинство проблем, связанных с первыми двумя подходами. Но сначала поговорим о том, как 3D-объект математически транслируется в 2D-проекцию на экране.

Что такое мировая матрица, матрица вида и матрица проекции

Чтобы отобразить объект, вы должны понимать три основные концепции, или «пространства», и то, как объект проецируется из собственного пространства координат на экран:

На рис. 8 показан порядок, в котором объект проецируется на экран.

*
Увеличить

Рис. 8. Порядок, в котором 3D-объект проецируется на экран

Первый набор координат для 3D-объекта — это x-, y- и z-координаты в объектном (локальном) пространстве (также называемом пространством модели). Эти координаты относительны друг другу с центром (0, 0, 0). Помните, что в случае правосторонней декартовой системы координат (right-hand Cartesian coordinates) положительная ось z направлена на наблюдателя.

Для трехмерного куба верхний правый угол передней стороны будет иметь координаты (1,1,1), а нижний левый угол задней стороны (–1,–1,–1), как показано на рис. 9. В объектном пространстве координаты относительны друг другу, и их позиция может варьироваться лишь в диапазоне от –1 до +1. Чтобы мой куб использовал 75% объектного пространства, нужно умножить каждую координату на .75; тогда новая позиция b станет (.75,.75,.75), а новая g — (–.75,–.75,–.75).

*
Увеличить

Рис. 9. Координаты в трехмерном пространстве

Когда объект переводится в пространство мировых координат (world space), сам объект не перемещается, а проецируется на мировые координаты умножением его координат на мировую матрицу (world matrix). В пространстве мировых координат можно преобразовывать 3D-объект, смещая координаты для трансляции объекта, изменяя размер для масштабирования и меняя угол для вращения объекта. Чтобы выразить координаты вашего объекта в пространстве мировых координат, вы должны умножить позицию каждой вершины на мировую матрицу:

Мировые координаты объекта = координаты объекта * мировая матрица

Следующий элемент — вид камеры (camera view), который обозначает точку, откуда вы смотрите на объект. Эта точка может меняться в трехмерном пространстве без изменения координат самого объекта в объектном пространстве, а также в пространстве мировых координат. Чтобы вычислить координаты объекта относительно вида камеры, вы умножаете матрицу вида (view matrix) на мировую матрицу объекта:

Координаты объекта относительно вида = мировые координаты * матрица вида

Наконец, на экране нужно визуализировать вид объекта; здесь вам понадобится вычислить вид с перспективой (perspective view), создаваемой из-за расстояния до объекта. Пока мой объект находится в параллельной проекции (стороны параллельны), но мне нужно отобразить объект в проекции с перспективой (стороны сходятся в некоей точке — точке схождения), поэтому я умножаю произведение матрицы вида объекта и мировой матрицы на матрицу проекции (projection matrix):

Конечные координаты объекта = Мировые координаты * матрица вида * матрица проекции

Это конечная позиция 3D-объекта на экране, которая также называется WorldViewProjection.

Структура Matrix

Структура Matrix, доступная в Microsoft.Xna.Framework, включена в Silverlight 5. В ней содержатся гомогенная матрица 4×4 с 16 значениями с плавающей точкой в виде полей и ряд методов для генерации матрицы преобразования (transformation matrix) (рис. 10).

*
Рис. 10. Структура Matrix в Silverlight 5

Первые три строки по столбцам (M11–M33) используются для преобразований масштабирования и вращения, а четвертая строка (M41–M43) — для трансляции (рис. 11).

Рис. 11. Матрица размером 4×4

M11M12M13M14
M21M22M23M24
M31M32M33M34
M41M42M43M44

Чтобы лучше понять эту матрицу, давайте посмотрим, как она используется при каком-либо преобразовании. Существует пять типов матриц: матричная структура 4×4, матрица тождественности (identity matrix), матрица трансляции (translation matrix), матрица масштабирования (scale matrix) и матрица вращения (rotation matrix).

Матрица тождественности (рис. 12) является единичной матрицей (unit matrix) с размерностью 4, и она становится исходной позицией 3D-объекта в пространстве мировых координат. Если вы умножаете какую-нибудь матрицу на матрицу тождественности, вы получаете исходную матрицу безо всяких изменений. Матричная структура предоставляет простое свойство, которое возвращает Matrix.Identity.

Рис. 12. Матрица тождественности

1000
0100
0010
0001

Для масштабирования объекта матрицы предусмотрен метод Matrix.CreateScale. Матрица масштабирования использования для преобразования масштабирования применительно к 3D-объекту, поэтому, когда вы умножаете объект на матрицу масштабирования (рис. 13), получаемая в результате матрица соответственно изменяет размеры.

Рис. 13. Матрица масштабирования

Sx000
0Sy00
00Sz0
0001

Объект матрицы также предоставляет метод Matrix.CreateTranslate для перемещения объекта в пространстве мировых координат. При умножении на матрицу трансляции (рис. 14) объект транслируется в пространстве мировых координат.

Рис. 14. Матрица трансляции

1000
0100
0010
TxTyTz1

Для вращения существует несколько методов. Метод Matrix.CreateFromYawPitchRoll используется для поворота каждой оси на значение с плавающей точкой. Методы Matrix.CreateRotationX, Matrix.CreateRotationY и Matrix.CreateRotationZ предназначены для поворота объекта по осям x, y и z. Матрица вращения на угол θ (тэта) включает элементы M11–M33, как показано на рис. 15.

Рис. 15. Матрица вращения по осям x, y и z

1000
0Cos θSin θ0
0-Sin θCos θ0
0001
Rotation X
Cos θ0Sin θ0
0100
-Sin θ0Cos θ0
0001
Rotation Y
Cos θSin θ00
-Sin θCos θ00
0010
0001
Rotation Z

Изучаем 3D-конвейер для Silverlight-XNA

Silverlight 5 с библиотеками XNA поддерживает пошаговый процесс создания 3D-объектов с координатами вершин для визуализации на экране. Этот процесс можно разбить на пять основных стадий (рис. 16), включающих следующие компоненты.

  1. Буфер вершин (vertex buffer).
  2. Координаты WorldViewProjection.
  3. Заполнение: вершинное, пиксельное и текстурное.
  4. Графическая обработка: растеризация, отсечение (clipping) и отбрасывание (cull).
  5. Конечный вывод: кадровый буфер (frame buffer).

*
Увеличить

Рис. 16. Создание 3D-объектов с помощью библиотек XNA в Silverlight 5

Мы кратко рассмотрим каждую стадию и ее компоненты.

Буфер вершин Первый шаг в создании буфера вершин — создание скелета 3D-объекта по набору вершин. Каждая вершина содержит, как минимум, координаты x, y и z, но обычно включает и свойства, такие как цвет и текстура. Этот набор вершин потом используется для создания буфера вершин, который передается на следующую стадию процесса.

Silverlight 5 с библиотеками XNA поддерживает пошаговый процесс создания 3D-объектов с координатами вершин для визуализации на экране.

Координаты WorldViewProjection Конечные координаты вычисляются умножением вершин на мировую матрицу, матрицу вида и матрицу проекции. Здесь определяется позиция объекта по отношению к пространству мировых координат, виду и проекции. Детали были описаны в предыдущих двух разделах. После получения конечных координат начинается процесс заполнения (применения шейдеров).

Заполнение Используется вершинное, пиксельное и текстурное заполнение. На этой стадии сначала выполняется окрашивание вершин, а затем происходит заполнение каждого пикселя индивидуально. Также применяется текстурное заполнение. Результат этой стадии используется для создания кадрового буфера.

Растеризация, отсечение и отбрасывание При растеризации изображение преобразуется в пиксели, а затем с помощью отсечения и отбрасывания удаляется скелет объекта вместе со скрытыми и невидимыми слоями. В конечном счете результат визуализируется на экране.

Кадровый буфер После растеризации изображения, отсечения и отбрасывания невидимых частей генерируется кадровый буфер, который потом посылается на экран для отображения.

Создание куба с помощью примитивов

Теперь, когда вы знаете, что такое матрицы, мировые координаты, вид, проекция и 3D-конвейер в Silverlight 5 с библиотеками XNA, попробуем создать трехмерный куб и посмотрим, как все это работает в единой связке.

Самое большое преимущество этого подхода — использование аппаратного ускорения на GPU, освобождающего центральный процессор от рендеринга 3D-объекта. Аппаратное ускорение включается установкой параметра EnableGPUAcceleration в теге Object в true внутри HTML, используемого для конфигурирования плагина Silverlight:

<object data="data:application/x-silverlight-2,"
  type="application/x-silverlight-2" width="100%"
    height="100%">
  <param name="EnableGPUAcceleration" value="true" />
  <param name="source" value="ClientBin/Cube3d.xap"/>
  <param name="minRuntimeVersion" value="5.0.60211.0" />
</object>

В XAML я добавлю объект DrawingSurface в Grid, который используется для рендеринга 3D-объектов в Silverlight с помощью методаDrawPrimitives объекта GraphicsDevice (рис. 17):

<DrawingSurface Loaded="OnLoad" SizeChanged="OnSizeChanged"Draw="OnDraw"/>

*
Рис. 17. Метод DrawPrimitives класса GraphicsDevice

Для создания и рендеринга куба я задействую три метода класса DrawingSurface. Метод OnLoad используется для создания куба и инициализации всех шейдеров и матрицы вида, которая не изменяется в этом приложении. Заметьте, что 3D-объект центрируется в точке (0,0,0) с применением 75% объектного пространства с координатами в диапазоне от (.75,.75,.75) до (–.75,–.75,–.75). В данном случае я создаю буфер вершин для хранения набора вершин и инициализации потоков shaderStream, pixelStream и imageStream, которые понадобятся на стадии заполнения. Я также инициализирую матрицу вида — она определяет угол, под которым камера смотрит на объект, — и использую cameraPosition и cameraTarget с параметром Vector3.Up (т. е. камера смотрит вверх). Этот код показан на рис. 18.

Рис. 18. Инициализация shaderStream, pixelStream и imageStream

VertexBuffer vertexBuffer;
VertexShader vertexShader;
PixelShader pixelShader;
Texture2D texture;

private void OnLoad(object sender, RoutedEventArgs e)
{
  vertexBuffer = CreateCube();
  Stream shaderStream = Application.GetResourceStream(new
    Uri(@"Cube3d;component/shader/shader.vs",
    UriKind.Relative)).Stream;
  vertexShader = VertexShader.FromStream(
    resourceDevice, shaderStream);

  Stream pixelStream = Application.GetResourceStream(new
    Uri(@"Cube3d;component/shader/shader.ps",
    UriKind.Relative)).Stream;
  pixelShader = PixelShader.FromStream(
    resourceDevice, pixelStream);

  Stream imageStream = Application.GetResourceStream(new
    Uri(@"Cube3d;component/scene.jpg",
    UriKind.Relative)).Stream;
  var image = new BitmapImage();
  image.SetSource(imageStream);
  texture = new Texture2D(resourceDevice, image.PixelWidth,
    image.PixelHeight, false, SurfaceFormat.Color);
  image.CopyTo(texture);

  Vector3 cameraPosition = new Vector3(0, 0, 5.0f);
  Vector3 cameraTarget = Vector3.Zero;
  view = Matrix.CreateLookAt(cameraPosition, cameraTarget,
    Vector3.Up);
}

Следующая стадия — создание буфера вершин для трехмерного куба. Я написал метод CreateCube (рис. 19), который возвращает VertexBuffer. В нем я создаю два прямоугольника в трехмерном пространстве, причем ABCD образует переднюю грань куба, а EFGH — заднюю. Используя структуру VertexPositionColor, я формирую набор вершин и цвета, сопоставленные с каждой из вершин.

Рис. 19. Создание VertexBuffer в методе CreateCube

VertexBuffer CreateCube()
{
  var vertexCollection = new VertexPositionColor[36];

  // Координаты передней стороны
  Vector3 cubeA = new Vector3(-0.75f, 0.75f, 0.75f);
  Vector3 cubeB = new Vector3(0.75f, 0.75f, 0.75f);
  Vector3 cubeC = new Vector3(-0.75f, -0.75f, 0.75f);
  Vector3 cubeD = new Vector3(0.75f, -0.75f, 0.75f);
  // Координаты задней стороны
  Vector3 cubeE = new Vector3(-0.75f, 0.75f, -0.75f);
  Vector3 cubeF = new Vector3(0.75f, 0.75f, -0.75f);
  Vector3 cubeG = new Vector3(-0.75f, -0.75f, -0.75f);
  Vector3 cubeH = new Vector3(0.75f, -0.75f, -0.75f);

  // Цвета
  Color cRed = Color.FromNonPremultiplied(255, 0, 0, 156);
  Color cGreen = Color.FromNonPremultiplied(0, 255, 0, 156);
  Color cBlue = Color.FromNonPremultiplied(0, 0, 255, 156);
  Color cYellow = Color.FromNonPremultiplied(255, 255, 0, 156);
  Color cBlack = Color.FromNonPremultiplied(0, 0, 0, 156);
  Color cWhite = Color.FromNonPremultiplied(
    255, 255, 255, 156);

  // Передняя сторона
  vertexCollection[0] = new VertexPositionColor(cubeA, cGreen);
  vertexCollection[1] = new VertexPositionColor(cubeB, cGreen);
  vertexCollection[2] = new VertexPositionColor(cubeC, cGreen);
  vertexCollection[3] = new VertexPositionColor(cubeB, cBlue);
  vertexCollection[4] = new VertexPositionColor(cubeD, cBlue);
  vertexCollection[5] = new VertexPositionColor(cubeC, cBlue);
  // Задняя сторона
  vertexCollection[6] = new VertexPositionColor(cubeG, cBlue);
  vertexCollection[7] = new VertexPositionColor(cubeF, cBlue);
  vertexCollection[8] = new VertexPositionColor(cubeE, cBlue);
  vertexCollection[9] = new VertexPositionColor(cubeH, cGreen);
  vertexCollection[10] = new VertexPositionColor(
    cubeF, cGreen);
  vertexCollection[11] = new VertexPositionColor(
    cubeG, cGreen);
  // Верхняя сторона
  vertexCollection[12] = new VertexPositionColor(cubeE, cRed);
  vertexCollection[13] = new VertexPositionColor(cubeF, cRed);
  vertexCollection[14] = new VertexPositionColor(cubeA, cRed);
  vertexCollection[15] = new VertexPositionColor(
    cubeF, cYellow);
  vertexCollection[16] = new VertexPositionColor(
    cubeB, cYellow);
  vertexCollection[17] = new VertexPositionColor(
    cubeA, cYellow);
  // Нижняя сторона
  vertexCollection[18] = new VertexPositionColor(cubeH, cRed);
  vertexCollection[19] = new VertexPositionColor(cubeG, cRed);
  vertexCollection[20] = new VertexPositionColor(cubeC, cRed);
  vertexCollection[21] = new VertexPositionColor(
    cubeD, cYellow);
  vertexCollection[22] = new VertexPositionColor(
    cubeH, cYellow);
  vertexCollection[23] = new VertexPositionColor(
    cubeC, cYellow);
  // Левая сторона
  vertexCollection[24] = new VertexPositionColor(
    cubeC, cBlack);
  vertexCollection[25] = new VertexPositionColor(
    cubeG, cBlack);
  vertexCollection[26] = new VertexPositionColor(
    cubeA, cBlack);
  vertexCollection[27] = new VertexPositionColor(
    cubeA, cWhite);
  vertexCollection[28] = new VertexPositionColor(
    cubeG, cWhite);
  vertexCollection[29] = new VertexPositionColor(
    cubeE, cWhite);
  // Правая сторона
  vertexCollection[30] = new VertexPositionColor(
    cubeH, cWhite);
  vertexCollection[31] = new VertexPositionColor(
    cubeD, cWhite);
  vertexCollection[32] = new VertexPositionColor(
    cubeB, cWhite);
  vertexCollection[33] = new VertexPositionColor(
    cubeH, cBlack);
  vertexCollection[34] = new VertexPositionColor(
    cubeB, cBlack);
  vertexCollection[35] = new VertexPositionColor(
    cubeF, cBlack);

  var vb = new VertexBuffer(resourceDevice,
    VertexPositionColor.VertexDeclaration,
    vertexCollection.Length, BufferUsage.WriteOnly);
  vb.SetData(0, vertexCollection, 0,
    vertexCollection.Length, 0);
  return vb;
}

Метод OnSizeChanged поверхности рисования используется для обновления проекции и пропорции сторон на экране с учетом размера поверхности:

private void OnSizeChanged(object sender,
  SizeChangedEventArgs e)
{
  DrawingSurface surface = sender as DrawingSurface;
  float sceneAspectRatio = (float)surface.ActualWidth /
    (float)surface.ActualHeight
  projection = Matrix.CreatePerspectiveFieldOfView
    (MathHelper.PiOver4, sceneAspectRatio, 1.0f, 100.0f);
}

Последний метод — OnDraw, обеспечивающий динамическое преобразование 3D-куба. Именно здесь я применяю Matrix.CreateScale для изменения размеров куба, Matrix.CreateFromYawPitchRoll для его поворота и Matrix.CreateTranslate для перемещения. Также вычисляется матрица worldViewProjection и передается методу vertexShader для заполнения вершин, потом — в pixelShader для заполнения сторон куба и, наконец, попадает в textureShader, который может накладывать какое-либо изображение в качестве текстуры. Далее GraphicDeviceObject вызывает метод DrawPrimitives для визуализации кадров вывода, как показано на рис. 20.

Рис. 20. Вызов метода DrawPrimitives для визуализации кадров вывода

void OnDraw(object sender, DrawEventArgs args)
{
  Matrix position = Matrix.Identity;
  Matrix scale = Matrix.CreateScale(1.0f);
  float xf = 0.0f; float yf = 0.0f; float zf = 0.0f;

  if (cubeXAxis) xf = MathHelper.PiOver4 *
    (float)args.TotalTime.TotalSeconds;
  if (cubeYAxis) yf = MathHelper.PiOver4 *
    (float)args.TotalTime.TotalSeconds;
  if (cubeZAxis) zf = MathHelper.PiOver4 *
    (float)args.TotalTime.TotalSeconds;

  Matrix rotation = Matrix.CreateFromYawPitchRoll(xf, yf, zf);
  Matrix world;
  if (translateZ != 0)
    world = rotation * Matrix.CreateTranslation(
      0, 0, (float)translateZ);
  else
    world = scale * rotation * position;

  // Вычисляем конечные координаты для передачи шейдеру
  Matrix worldViewProjection = world * view * projection;
  args.GraphicsDevice.Clear(ClearOptions.Target |
    ClearOptions.DepthBuffer,
    new Microsoft.Xna.Framework.Color(0, 0, 0, 0), 10.0f, 0);

  // Настраиваем вершинный конвейер (vertex pipeline)
  args.GraphicsDevice.SetVertexBuffer(vertexBuffer);
  args.GraphicsDevice.SetVertexShader(vertexShader);
  args.GraphicsDevice.SetVertexShaderConstantFloat4(
    0, ref worldViewProjection);

  // Настраиваем пиксельный конвейер (pixel pipeline)
  args.GraphicsDevice.SetPixelShader(pixelShader);
  args.GraphicsDevice.Textures[0] = texture;
  args.GraphicsDevice.DrawPrimitives(
    PrimitiveType.TriangleList, 0, 12);
  args.InvalidateSurface();
}

Это приводит к динамической визуализации конечного 3D-куба на поверхности рисования (рис. 21).

*
Рис. 21. Конечный трехмерный куб на поверхности рисования

При этом подходе используется аппаратное ускорение и рендеринг скрытых или невидимых сторон 3D-объекта не осуществляется — в отличие от первого подхода. Как и при втором подходе, в нем применяется кадровый буфер в памяти для визуализации конечного вывода, но с полным программным контролем над объектом.

Теперь вы знаете о трех способах создания 3D-объектов в Silverlight, их сильные и слабые стороны. Это должно помочь вам в дальнейшем исследовании мира трехмерной графики в Silverlight.


Ссылка: http://www.oszone.net/17578/Silverlight