Silverlight 3D

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

Раджеш Лал

В этой статье обсуждается предварительная версияSilverlight 5. Любая изложенная здесь информация может быть изменена.

Продукты и технологии:Silverlight

В статье рассматриваются:

  • трехмерная перспектива (perspective 3D);
  • кадры трехмерных объектов (frames 3D);
  • проецирование трехмерных объектов на экран;
  • структура Matrix в Silverlight 5;
  • 3D-конвейер XNA;
  • использование примитивов XNA.

В этой статье я покажу, как разрабатывать трехмерные объекты (далее для краткости — 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:

  • использование трехмерной перспективы;
  • применение набора кадров и таймера;
  • использование примитивов из библиотеки XNA.

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

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

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

Класс PlaneProjection
Рис. 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. В данном случае для имитации вращения куба я использую минимальный набор кадров, но увеличение количества кадров в секунду позволит добиться более плавного вращения.

Восемь кадров, имитирующих вращение по оси 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 показан порядок, в котором объект проецируется на экран.

Порядок, в котором 3D-объект проецируется на экран
Рис. 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).

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

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

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

M11 M12 M13 M14
M21 M22 M23 M24
M31 M32 M33 M34
M41 M42 M43 M44

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

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

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

1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1

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

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

Sx 0 0 0
0 Sy 0 0
0 0 Sz 0
0 0 0 1

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

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

1 0 0 0
0 1 0 0
0 0 1 0
Tx Ty Tz 1

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

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

1 0 0 0
0 Cos θ Sin θ 0
0 -Sin θ Cos θ 0
0 0 0 1
Rotation X      
Cos θ 0 Sin θ 0
0 1 0 0
-Sin θ 0 Cos θ 0
0 0 0 1
Rotation Y      
Cos θ Sin θ 0 0
-Sin θ Cos θ 0 0
0 0 1 0
0 0 0 1
Rotation Z      

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

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

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

Создание 3D-объектов с помощью библиотек XNA в Silverlight 5
Рис. 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"/>

Метод DrawPrimitives класса GraphicsDevice
Рис. 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.png",
    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-объекта не осуществляется — в отличие от первого подхода. Как и при втором подходе, в нем применяется кадровый буфер в памяти для визуализации конечного вывода, но с полным программным контролем над объектом.

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


Раджеш Лал (Rajesh Lal) испытывает страсть к Silverlight и веб-технологиям. Автор множества книг по гаджетам Windows, веб-виджетам и мобильным веб-технологиям, в том числе готовящейся к публикации книги «Fun with Silverlight 4» (CreateSpace, 2011). За более подробной информацией обращайтесь по адресу connectrajesh@hotmail.com или посетите сайт silverlightfun.com.

Выражаю благодарность за рецензирование статьи экспертам Марку Клифтону (Marc Clifton), Рику Кингслену (Rick Kingslan) иДжошу Смиту (Josh Smith).