Войти
ПрограммированиеСтатьиГрафика

SlimDX и C#. Работаем Direct3D9

Внимание! Этот документ ещё не опубликован.

Автор:

Здравствуйте! В этой статье я хотел бы рассказать об использовании обертки SlimDX в .NET. Так как сам работаю на языке C# буду приводить код на нем, но думаю если нужно он без труда конвертируется в VB.NET.

Введение


Почему я выбрал SlimDX? Дело в том, что я начинал работать с DirectX ещё на Visual Basic 6, а потом скакал с языка на язык, писал долго на С++, но мне показалось более интересным концентрироваться на разработке логики, самой игры, интересных моментов движка, а не процесс тщательного продумывания тестов с С++. Я долгое время работал с C#. Поначалу были эксперименты с Managed DX. Пробовал писать на С++/CLI и использовать библиотеку из шарпа. Ничего из этого не приносило мне удовольствия, того что я испытывал, когда программировал на VB6 :). Я заканчивал свой рабочий проект, где в качестве средства для рисования 3х мерных графиков функций, я использовал Managed DX. Но из-за того, что мне так и не удалось за пол года заставить его нормально работать и переносится с одного компьютера на другой, я начал искать другой способ, и мне попался SlimDX. С того момента я стал получать удовольствие от программирования графики, ещё большее, нежели на VB6. Более того - SlimDX спас мой рабочий проект :).
Итак, в этой статье я расскажу как пользоваться SlimDX Direct3D9 для создания графических приложений. Возможно, это будет цикл статей. Мне хотелось бы рассказать многое то, чего я не нахожу на этом замечательном ресурсе, но в качестве основы буду применять SlimDX.

Планируем программу


С определенных пор я начал планировать даже самое малое приложение для тестов, поэтому и здесь хотелось бы уделить этому хоть немного времени. Итак, мы создадим решение, в основе которого будет WindowsFormsApplication на C#. В качестве "рисовалки" мы создадим класс Viewport3D, который будет заниматься рендером. Мы будем передавать в него параметры для инициализации в виде структуры ViewportParameters. После чего будем пользоваться этим классом из формы.
P.S. Для удобства использования в примере, который я выложу в конце статьи, я вынес эти классы в отдельный проект DLL - Engine3D.

Начинаем с инициализации

Класс Viewport3D


Вот так будет выглядеть структура ViewportParameters
    using SlimDX; //Подключим SlimDX
    public struct ViewportParameters
    {
        public int WindowHeight;
        public int WindowWidth;
        public bool FullScreen;
        public bool VerticalSync;
        public IntPtr ControlHandle;
        public Color4 BackColor;    //Цвет фона нашего окна
    }
Это – пожалуй, самое основное, что нам понадобится. Теперь создадим класс Viewport3D, в который будем передавать вот такие параметры для инициализации.
Внутри класса объявим пока самое основное, что нам может понадобиться в этом классе, это параметры и устройство
using DX = SlimDX.Direct3D9; //Для дальнейшего избежания конфликта имен
public class Viewport3D
{
        public ViewportParameters Params { get; private set; }
        public DX.Device Device { get; private set; }
        public DX.Direct3D D3D { get; private set; }
        DX.PresentParameters d3dpp = new DX.PresentParameters();
}
Теперь создаем конструктор, в котором инициализируем устройство:
        public Viewport3D(ViewportParameters p)
        {
            Params = p;
            D3D = new DX.Direct3D();
            DX.DisplayMode mode = D3D.GetAdapterDisplayMode(0);
            d3dpp.AutoDepthStencilFormat = DX.Format.D16;
            d3dpp.BackBufferCount = 1;
            d3dpp.BackBufferFormat = mode.Format;
            d3dpp.BackBufferHeight = p.WindowHeight;
            d3dpp.BackBufferWidth = p.WindowWidth;
            d3dpp.DeviceWindowHandle = p.ControlHandle;
            if (p.VerticalSync)
                d3dpp.PresentationInterval = DX.PresentInterval.Default;
            else
                d3dpp.PresentationInterval = DX.PresentInterval.Immediate;
            d3dpp.SwapEffect = DX.SwapEffect.Discard;
            d3dpp.Windowed = !p.FullScreen;
            d3dpp.EnableAutoDepthStencil = true;
            if (!p.FullScreen && p.VerticalSync)
                d3dpp.SwapEffect = DX.SwapEffect.Copy;
            Device = new DX.Device(D3D, 0, DX.DeviceType.Hardware, p.ControlHandle, DX.CreateFlags.HardwareVertexProcessing, d3dpp);
            if (Device == null)
                throw new NullReferenceException("Не удалось инициализировать устройство!");
        }
Для тех, кто не знаком с Direct3D9: сначала мы создали объект Direct3D (свойство D3D), затем получили текущий режим экрана и заполнили параметры PresentParameters в соответствии с нашими параметрами, переданными в конструктор. Создали устройство. Если вдруг устройства не создалось, будет Exception :)

Теперь нам необходимо научить класс Viewport3D производить очистку экрана и растеризацию. Прежде чем добавлять методы, создадим ещё один вспомогательный метод, который потом будем расширять - InitDevice():

        private void InitDevice()
        {
            Device.SetRenderState(DX.RenderState.ZEnable, true);
            Device.SetRenderState<DX.Cull>(DX.RenderState.CullMode, DX.Cull.None);
            Device.SetRenderState(DX.RenderState.Lighting, false);
        }
Вызовем этот метод из конструктора, после инициализации устройства. Теперь расскажу немного, зачем это все.
Я вынес это в отдельный метод с одной целью - в дальнейшем к этому классу нужно будет прикрутить возможность нормально реагировать на ресайз окна и сворачивание, при вызове Device.Reset мы должны будем восстановить все параметры - мы просто вызовем InitDevice.
У SlimDX есть одна интересная особенность, Вы можете наблюдать её в этой строке
            Device.SetRenderState<DX.Cull>(DX.RenderState.CullMode, DX.Cull.None);
Очень удобно выполненный метод SetRenderState, который избавляет от необходимости выполнять приведение типов. Вы увидите, что в SlimDX многое сделано именно так.
Теперь реализуем методы очистки экрана и растеризации.
        public void Clear(Color4 backColor)
        {
            Device.Clear(DX.ClearFlags.Target | DX.ClearFlags.ZBuffer, backColor, 1.0f, 0);
        }
        public void Clear()
        {
            Clear(Params.BackColor);
        }
        public void Begin()
        {
            Device.BeginScene();
        }
        public void End()
        {
            Device.EndScene();
        }
        public void RenderToScreen()
        {
            Device.Present();
        }
Вот так будут выглядеть эти методы. Думаю тут особо нечего пояснять. А также реализуем очистку памяти, что не мало важно. Вообще мой совет на будущее - всегда делайте очистку того, что реализует интерфейс IDisposable, иначе начнутся непонятные проблемы. Я не раз с этим сталкивался именно в SlimDX. Наш класс Viewport3D пусть также будет реализовывать интерфейс IDisposable:
public class Viewport3D : IDisposable
{
...
        void Dispose()
        {
            Device.Dispose();
            D3D.Dispose();
        }

        ~Viewport3D()
        {
            this.Dispose();
            Device = null;
            D3D = null;
        }
...
}
Теперь вроде все

Тестируем то, что сделали


Теперь пришло время запустить наш пример. Итак, вернемся в нашу форму. Сделаем её нужных размеров, напишем то, что нужно в заголовке, в общем, для начала наведем красоту и приступим к коду:
private Viewport3D view = null;
        private bool bRender = false;

        public Form1()
        {
            InitializeComponent();
            ViewportParameters p = new ViewportParameters();
            p.BackColor = new Color4();    //Черный
            p.ControlHandle = this.Handle;
            p.FullScreen = false;
            p.VerticalSync = false;
            p.WindowWidth = this.Width;
            p.WindowHeight = this.Height;
            try
            {
                view = new Viewport3D(p);
            }
            catch (NullReferenceException ex)
            {
                MessageBox.Show(ex.Message);
                Environment.Exit(0);
            }
        }
        private void Form1_Load(object sender, EventArgs e)
        {
            this.Show();
            bRender = true;
            while (bRender)
            {
                Application.DoEvents();
                view.Clear();
                view.Begin();
                view.End();
                view.RenderToScreen();
            }
            view.Dispose();
            view = null;
        }
А теперь добавим обработку закрытия формы, чтобы прерывался цикл, и не было проблем с потерей ссылок:
        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            bRender = false;
            view.Dispose();
            view = null;
            Environment.Exit(0);
        }
Теперь можно компилировать и запускать пример. Мы увидим окно формы, залитое черным цветом. Ничего особенного - но уже работает. Теперь давайте нарисуем что-нибудь в это окно.

Рисуем прямоугольник


Для того чтобы рисовать нужно определить формат вершин, создать массив вершин и вызвать метод вывода примитива. Для начала займемся структурой вершин.
При написании своего движка я столкнулся с некоторыми проблемами, по привычке использовал  FVF, и при создании Bump Mapping-а у меня неправильно передавалась информация в шейдер, и я решил переделать свой формат вершин. Итак, я предлагаю вот такую структуру для начала:
    public struct TransformedColored
    {
        public Vector4 Pos;
        public int Color;

        public TransformedColored(Vector4 p, int color)
        {
            Pos = p;
            Color = color;
        }

        public static VertexElement[] Format
        {
            get
            {
                return new VertexElement[] { 
                    new VertexElement(0,0,DeclarationType.Float4,DeclarationMethod.Default,DeclarationUsage.PositionTransformed,0),
                    new VertexElement(0,16,DeclarationType.Color,DeclarationMethod.Default,DeclarationUsage.Color,0),
                    VertexElement.VertexDeclarationEnd
                };
            }
        }
    }
После принятие такой структуры - в дальнейшем я не имею проблем с рисованием и созданием вершин. Здесь я использую трансформированные вершины в виде Vector4 и цвет точки. Я создал практически готовую декларацию вершин, которую возвращает статическое свойство Format.
Теперь собственно сам объект прямоугольника. Создадим класс Rectange2D, который рисует трансформированными вершинами прямоугольник с заданными координатами и размерами. Также снабдим его методом SetColor, который может установить цвет каждой точки. Объект Rectangle2D должен содержать ссылку на наш Viewport, чтобы иметь доступ к устройству.
    public class Rectangle2D
    {
        private TransformedColored[] verts; // Сами вершины

        public Viewport3D Viewport { get; set; } //Ссылка на Viewport

        public Rectangle2D(Vector2 start, Vector2 size, Viewport3D v)
        {
            Viewport = v;
            //Создаем вершины
            verts = new TransformedColored[4];
            verts[0].Pos = new Vector4(start, 0.0f, 1.0f);
            verts[1].Pos = new Vector4(start.X + size.X, start.Y, 0.0f, 1.0f);
            verts[2].Pos = new Vector4(start.X, start.Y + size.Y, 0.0f, 1.0f);
            verts[3].Pos = new Vector4(start + size, 0.0f, 1.0f);
            for (int i = 0; i < 4; i++)
                verts[i].Color = new Color4(1.0f, 1.0f, 1.0f).ToArgb();
        }
        //Безопасная установка цветов
        public void SetColor(int index, Color4 c)
        {
            if (index >= 0 && index < 4)
                verts[index].Color = c.ToArgb();
            else
                throw new ArgumentOutOfRangeException("Индекс вершины за пределами границ!");
        }
        //Собственно рендер
        public void Render()
        {
            if(Viewport == null) return;
            if(Viewport.Device == null) return;
            //Используем конструкцию using для контроля за удалением объекта VertexDeclaration
            //Как правило декларация вершин не создается перед рендером, почему я привожу этот код написано ниже :)
            using (VertexDeclaration decl = new VertexDeclaration(Viewport.Device, TransformedColored.Format))
            {
                Viewport.Device.VertexDeclaration = decl;
                Viewport.Device.DrawUserPrimitives<TransformedColored>(PrimitiveType.TriangleStrip, 2, verts);
            }
        }
    }
Хочу обратить ваше внимание на метод Render. Вначале мы проверяем ссылки на объекты Viewport и Device. Можно конечно выбросить ArgumentNullException, я решил просто не рисовать.
Очень важный момент - конструкция using. Это очень тонкое место, в котором всегда течет память. Если не удалять VertexDeclaration - здесь будет утечка. Вообще его можно создавать в конструкторе, я просто решил продемонстрировать важный момент, когда объекты создаются динамически не надо забывать о Dispose :)
Также мне очень нравится,что DrawUserPrimitives (DrawPrimitiveUP) выполнен в SlimDX как обобщенный метод.

Добавим код отрисовки в форму:

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Show();
                bRender = true;
                Rectangle2D r = new Rectangle2D(new Vector2(50.0f, 50.0f), new Vector2(300.0f, 300.0f), view);
                r.SetColor(0, new Color4(1.0f, 0.0f, 0.0f));
                r.SetColor(1, new Color4(0.0f, 1.0f, 1.0f));
                r.SetColor(2, new Color4(0.0f, 1.0f, 0.0f));
                r.SetColor(3, new Color4(0.0f, 0.0f, 1.0f));
            
            while (bRender)
            {
                Application.DoEvents();
                view.Clear();
                view.Begin();
                r.Render();
                view.End();
                view.RenderToScreen();
            }
            view.Dispose();
            view = null;
        }
Перед циклом мы создаем объект, присваиваем цвета и между Begin и End вызываем метод Render.

Заключение


В общем, все просто, но, тем ни менее пример прилагается. Прошу прощения заранее - пример скомпилирован и написан в Visual Studio 2010. Пока не имею возможности создавать 2 вида проекта, как это положено(2010 и 2008).

Хотелось бы поделиться своими выводами: как по мне SlimDX - довольно хороший вариант обертки для .NET, с поддержкой (хотя, как я слышал не полной) Direct3D11. Меня порадовала хорошая переносимость, присутствие версии для .NET Framework 2.0/4.0 и версий x86/x64. По скорости сложно судить, особенных тестов я не производил, но в рисовании достаточно простой сцены с Bump Mapping-ом у меня 1000 FPS :) Примерно столько же у меня в примерах к DirectX SDK. Когда будут более сложные сцены и будет возможность протестировать его на скорость - расскажу о результатах.

http://www.slimdx.org/ - ссылка на официальный сайт проекта SlimDX.
И собственно сам пример:
SlimDX D3D9 Example 1

#csharp, #Direct3D, #SlimDX

14 января 2012