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»), чтобы создать объект и преобразовывать его в трехмерном пространстве.
Рис. 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
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), включающих следующие компоненты.
- Буфер вершин (vertex buffer).
- Координаты WorldViewProjection.
- Заполнение: вершинное, пиксельное и текстурное.
- Графическая обработка: растеризация, отсечение (clipping) и отбрасывание (cull).
- Конечный вывод: кадровый буфер (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.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).