Создание первого 3D-приложения

Последнее обновление: 1.11.2015

Итак, нам естественно надо определить в разметке 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);

Backface culling

Теперь наш треугольник будет имитировать 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.

Обработка ввода пользователя в 3D

Итак, на сопледнем этапе работы с нашим треугольником добавим поддержку ввода пользователя. Пока треугольник вращается сам по себе. Теперь сделаем так, чтобы он вращался вправо или влево в зависимости от того, какую пользователь нажал клавишу. Управление будет вестись с помощью клавиш-стрелок Вправо и Влево.

Добавим в коде 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">

Построим приложение и запустим в браузере. Теперь мы можем управлять поворотом треугольника с помощью клавиатуры.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850