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