Итак, нам естественно надо определить в разметке XAML элемент DrawingSurface. И вначале импортируем все нужные нам пространства имен:
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Shapes; using System.Windows.Graphics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics;
Если среди импортированных имен имеется System.Windows.Media, то лучше исключить его, так как ряд структур и классов имеют похожие имена с теми, которые находятся в ространстве имен Microsoft.Xna.Framework, тогда нам придется писать полное имя класса с пространством имен.
Теперь объявим три переменные на уровне класса:
private VertexBuffer vertexBuffer; private BasicEffect basicEffect; GraphicsDevice graphicDevice;
Первая переменная типа VertexBuffer является буфером вершин и будет хранить вершины нашего объекта. Вторая переменная типа BasicEffect будет хранить эффект приложения. С помощью BasicEffect можно достаточно эффективно использовать код HLSL, не задумываясь о том, что лежит в основе работы HLSL. Но мы также могли бы вместо BasicEffect использовать свой эффект с пиксельным и вершинным шейдером, однако в данном случае мы вполне можем обойтись и BasicEffect.
Третья переменная с типом GraphicsDevice будет представлять графическое устройство, с которым мы будем взаимодействовать.
Теперь добавим в конец конструктора следующий код:
//Инициализация графического устройства текущим устройством graphicDevice = GraphicsDeviceManager.Current.GraphicsDevice; // Создание эффекта на основе класса BasicEffect basicEffect = new BasicEffect(graphicDevice); // Включаем отрисовку цветовой гаммы вершин basicEffect.VertexColorEnabled = true; // Массив вершин VertexPositionColor[] vertices = new VertexPositionColor[3]; // Создаем вершины vertices[0].Position = new Vector3(-1, -1, 0); // левый угол vertices[1].Position = new Vector3(0, 1, 0); // верхний угол vertices[2].Position = new Vector3(1, -1, 0); // правый угол // Объявляем их цвета vertices[0].Color = new Microsoft.Xna.Framework.Color(255, 0, 0, 255); // красный vertices[1].Color = new Microsoft.Xna.Framework.Color(0, 255, 0, 255); // зеленый vertices[2].Color = new Microsoft.Xna.Framework.Color(0, 0, 255, 255); // синий // Создаем буфер вершин vertexBuffer = new VertexBuffer(graphicDevice,VertexPositionColor.VertexDeclaration, vertices.Length,BufferUsage.WriteOnly); // Устанавливаем буфер вершин на основе массива вершин vertexBuffer.SetData(0, vertices, 0, vertices.Length, 0);
Здесь мы инициализируем графическое устройство текущим устройством, создаем массив вершин и устанавливаем буфер по этим вершинам. Наши вершины установлены следующим образом:
Далее добавим в обработчик метода Draw элемента DrawingSurface следующий код:
private void DrawingSurface_Draw(object sender, DrawEventArgs e) { // Устанавливаем мировую матрицу и матрицы вида и проекции эффекта basicEffect.World = Matrix.Identity; basicEffect.View = Matrix.CreateLookAt(new Vector3(0, 0, 5.0f), Vector3.Zero, Vector3.Up); basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView (0.85f, 1f, 0.01f, 1000.0f); // Очищаем графическое устройство graphicDevice.Clear(new Microsoft.Xna.Framework.Color(0.8f, 0.8f, 0.8f, 1.0f)); // Устанавливаем на устройстве буфер вершин graphicDevice.SetVertexBuffer(vertexBuffer); // Выполняем проход эффекта basicEffect.CurrentTechnique.Passes[0].Apply(); // Отрисовка графики graphicDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); // Уведомляем систему о том, что можно снова вызывать событие Draw e.InvalidateSurface(); }
Здесь вначале мы устанавливаем матрицы вида и проекции и мировую матрицу. Эти матрицы управляют, как объект будет отображаться на экране.
Далее мы очищаем экран - в данном случае очистка заключается в заливке всего экрана серым цветом. Таким образом, предыдущий кадр будет стерт, и будет отрисован новый кадр.
Затем мы устанавливаем для графического устройства буфер вершин и применяем эффект. Применение эффекта заключается в применении
каждого прохода каждой техники эффекта. Но поскольку в basicEffect только одна техника, содержащая один проход, то мы можем обойтись без цикла
(foreach (EffectPass pass in effect.CurrentTechnique.Passes))
и сразу применить единственный проход.
В конце происходит отрисовка примитива - то есть треугольника и уведомление системы о том, что мы можем запускать обработчик события Draw заново.
Теперь построим приложение и запустим тестовую страничку html в браузере. Если для текущего сайта не установлены разрешения, установите их и перезапустите страницу:
Хотя все хорошо, но пока непонятно, в чем заключается 3D, поскольку перед нами статичный плоский треугольник? Что ж замените в методе Draw строку
basicEffect.World = Matrix.Identity;
на
basicEffect.World = Matrix.CreateRotationY((float)e.TotalTime.TotalSeconds * 2);
Теперь наш треугольник будет имитировать 3D, вращаясь вокруг оси Y. Правда, здесь еще одна будет загвоздка: когда треугольник поворачивается на 180 градусов,
он становится невидимым. Это следствие того, что называют backface culling ("сокрытие обратной поверхности"). Это делается в целях повышения
производительности. Однако мы можем отключить этот эффект и увидеть вращающийся треугольник во всей красе. Для этого в обработчике события Draw
перед строкой graphicDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);
добавим следующую строку:
graphicDevice.RasterizerState = new RasterizerState() { CullMode = CullMode.None };
Непосредственная отрисовка у нас происходит в строке graphicDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 1)
.
В первом параметре этого метода задается тип примитивов. Во втором параметре - смещение в буфере вершин, с которого начинается отрисовка.
Третий параметр указывает на число отрисуемых примитивов.
В нашем случае в качестве типа примитива выступает тип TriangleList. Данный тип предполагает, что в буфере вершин у нас определены вершины дял отдельных треугольников, по три вершины на каждый. Кроме данного типа нам доступны еще несколько типов:
TriangleStrip | Представляет серию связанных треугольников. Каждый последующий треугольник состоит из последней вершины предыдущего треугольника и двух новых |
LineList | Представляет отдельные линии. Каждая линия состоит из двух отдельных вершин |
LineStrip | Представляет серию связанных линий. Каждая последующая линия состоит из последней вершины предыдущей линии и одной новой |
Поскольку у нас в буфере пока определны три вершины, то нам их хватит для двух линий при типе LineStrip. Поэтому мы можем поэкспериментировать и изменить код отрисовки примитива на следующий:
graphicDevice.DrawPrimitives(PrimitiveType.LineStrip, 0, 2);
Теперь немного подробнее поговорим о настроке камер. Обычно, когда мы смотрим на какой-нибудь объект, мы видим его относительно нашей позиции. Поэтому настрока камеры играет большую роль.
Прежде всего нам надо задать расположение и направление камеры. Для этого мы использовали метод
Matrix.CreateLookAt(Vector3 cameraPosition,Vector3 cameraTarget, Vector3 cameraUpVector);
Matrix.CreateLookAt(new Vector3(0, 0, 5.0f), Vector3.Zero, Vector3.Up);
Первый параметр этого метода - cameraPosition - позволяет определить точку пространства, в которойнаходится камера. В данном случае мы позиционируем камеру в точку с координатами (0, 0, 5.0f), то есть x=0, y=0, z=5. Таким образом, получается, что камера располагается где-то между нами и поверхность экрана, если поверхность экрана взять за z=0. Мы можем отдалить камеру, увеличив координату z. Подобным образом мы можем манипулировать и другими координатами.
Второй параметр этого метода - cameraTarget - указывает на направление камеры, которой является некоторая точка пространства.
В данном случае мы определили для значения этого параметра константу Vector3.Zero, которая представляет начало координат, то есть точку с
координатами (0,0,0). Но мы могли бы направить нашу камеру, например, правее, установив значение new Vector3(1.0f, 0f, 0f)
,
тогда наш треугольник сместился бы левее.
Третий параметр задает вектор вертикальной оринетации и обычно принимает в качестве значения Vector3.Up. Vector3.Up возвращает объект Vector3 с координатами (0, 1, 0), который представляет направление вверх. Но мы можем и по другому направить камеру в зависимости от наших потребностей.
Матрица проекции создает то, что называется видимое пространство камеры. Она определяет ту часть 3D-пространства, которое будет обозреваться камерой и поэтому и отрисовываться на экране. Объекты внутри этой видимой части будут выводиться на экран, пока между ними и камерой не появятся другие, закрывающие их объекты. Объекты, находящиеся вне зоны видимости камеры, не будут отрисовываться.
Для создания матрицы проекции используется метод
Matrix.CreatePerspectiveFieldOfView(float fieldOfView,float aspectRatio,float nearPlaneDistance,float farPlaneDistance)
В отличие от предыдущего метода он принимает четыре параметра типа float
. В начшем случае он выглядит следующим образом:
Matrix.CreatePerspectiveFieldOfView(0.85f, 1f, 0.01f, 1000.0f);
Первый параметр - fieldOfView - задает угол обзора камеры в радианах. Обычно 45 градусов или pi/4. Мы могли в принципе использовать
уже готовую константу для этого параметра - MathHelper.PiOver4
, либо любое другое значение.
Второй параметр - aspectRatio - определяет форматное (аспектное) соотношение - отношение ширины к высоте. За основу обычно берутся размеры элемента DrawingSurface, либо другого контейнера.
Третий и четвертый параметры - nearPlaneDistance и farPlaneDistance - определяют соответственно переднюю и заднюю плоскости отсечения. То есть, параметр nearPlaneDistance - это расстояние от камеры до ближайшей видимой точки, а farPlaneDistance - расстояние до самой дальней видимой точки. Вне этих значений объекты не будут видимы камере. Схематично это можно показать на следующем рисунке:
Мировая матрица позволяет задать положение объекта в пространстве и применить к нему преобразования, как в нашем случае мы применяем вращение.
Первоначально мы применили матрицу Matrix.Identity, которая представляет единичную матрицу, никак не влияющую на положение объекта. Наш треугольник отрисовывается таким, каков он есть.
Затем мы задали с матрицу с помощью метода Matrix.CreateRotationY((float)e.TotalTime.TotalSeconds * 2);
,
который предполагает вращение вокруг оси Y и в качестве параметра принимает угол вращения, а на выходе возвращеет обект Matrix.
Поскольку в качестве эффекта мы использовали базовый класс BasicEffect и нам не пришлось создавать свой эффект с пиксельным и вершинным шейдером, то чтобы задать матрицы для нашего приложения нам надо было присвить их соответствующим свойствам объекта BasicEffect.
Итак, на сопледнем этапе работы с нашим треугольником добавим поддержку ввода пользователя. Пока треугольник вращается сам по себе. Теперь сделаем так, чтобы он вращался вправо или влево в зависимости от того, какую пользователь нажал клавишу. Управление будет вестись с помощью клавиш-стрелок Вправо и Влево.
Добавим в коде C# для класса окна еще одну глобальную переменную, которая будет хранить угол поворота. С каждым поворотом влево, угол будет увеличиваться, а с поворотом вправо - уменьшаться:
float angle = 0;
Далее изменим код установки мировой мтарицы в обработчике события Draw следующим образом:
private void DrawingSurface_Draw(object sender, DrawEventArgs e) { // Устанавливаем мировую матрицу и матрицы вида и проекции эффекта basicEffect.World = Matrix.CreateRotationY(angle); .................................................
Теперь добавим в код процедуру, которая будет обрабатывать ввод с клавиатуры:
private void DrawingSurface_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Left) { angle += 0.5f; } else if (e.Key == Key.Right) { angle -= 0.5f; } }
И в конце назначим обработчик события KeyDown у элемента UserControl:
<UserControl............................................ d:DesignHeight="300" d:DesignWidth="400" KeyDown="DrawingSurface_KeyDown">
Построим приложение и запустим в браузере. Теперь мы можем управлять поворотом треугольника с помощью клавиатуры.