Дата публикации статьи: 29.07.2006 15:18

Глава 10. Пример работы с Direct3D в среде .NET

    В настоящее время существует две версии библиотеки для работы с DirectX на платформе .NET это - v9.00.0900 и v9.00.1126. Версия v9.00.0900 ориентирована на .NET Framework 1.0, а v9.00.1126 на .NET Framework 1.1. Версия v9.00.0900 не всегда корректно устанавливается, а так же имеются различия в реализации по сравнению с версией v9.00.1126 . Например код для вывода спрайтов в версии v9.00.0900 (для C#) может выглядеть следующим образом:

sprite.Begin();
sprite.End();
sprite1.Draw(spriteTexture, new Rectangle(280,75,340,214),
new Vector2(1,1),new Vector2(25,-10),(float)(Math.PI/180*MyAngle),new Vector2(115,140),Color.White);

    В версии v9.00.1126 (для C#) тот же самый вывод и поворота спрайт может выглядеть так:

sprite1.Begin(SpriteFlags.AlphaBlend);
t.Translate(168,72,0);
r.RotateZ((float)(Math.PI/180*MyAngle));//RotateAxis(new Vector3(0,0,0),(float)(Math.PI/180*MyAngle));
r.Multiply(t);
sprite1.Transform = r;//RotateAxis(new Vector3(0,0,0),(float)(Math.PI/180*MyAngle));
sprite1.Draw(spriteTexture, new Rectangle(142,45,20,50),new Vector3(10,-7,0),
new Vector3(0,0,0),Color.White);
sprite1.Flush();
sprite1.End();

    Разница видна “не вооруженным” взглядом (о выводе спрайтов см. ниже). Для написания примеров в этой книги использовалась версия управляемого DirectX под номером v9.00.1126 и .NET Framework 1.1. Как производится корректная установку программ на платформе .NET и правильно настраивать GAC вы должны изучить сами.
Первая реально работающая версия SDK DirectX 9 называется Summer 2003. В состав SDK входят обе версии управляемого DirectX - v9.00.0900 (размер 1 МВ) и v9.00.1126 (размер 5 МВ). На рисунке 1 представлен логотип этой версии. Как видите место обычного VB занял VB.NET.


Рис. 1

    После установки в каталоге “C:\WINDOWS\Microsoft.NET\Managed DirectX\” должны находиться динамические библиотеки управляемого DirectX. Понятно, что создание приложения начинается с создания каркаса - см. папку/N24. Создайте оконное приложение на VB.NET или C#, выполните команду Add Reference и добавьте к приложению уже знакомые по DirectX восемь ссылки (ссылки на библиотеки типов в терминологии COM) на пространства имен - DirectX/Direct3D/Direct3DX (Рис. 2).


Рис.2

    В самое начало файла приложения вставьте для VB.NET:

Imports Microsoft.DirectX
Imports Microsoft.DirectX.Direct3D.

Для C#:

using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D.

    Начнем создание каркаса с переменных уровня класса (можно сказать уровня формы для VB). Если между DirectX 7 и DirectX 8 различия принципиальные, то DirectX 8 не сильно отличается от DirectX 9. Если вы приобрели достаточно хорошие навыки работы с DirectX 8 в среде старого VB, то трудности для вас будет представлять только переход на новую платформу. Если вы переходите на DirectX 9 после работы с DirectX 7 с начало потребуется изучить основы технологии DirectX 8/9 на прямую не связанные с какой либо технологией программирования. Как и в DirectX 8 в DirectX 9 необходимо создать устройство для трехмерной графики. В самое начало класса вставьте переменные:

Private device As Device = Nothing
Private vertexBuffer As VertexBuffer = Nothing

Такой же код написанный на C#:

Device device = null;
VertexBuffer vertexBuffer = null.

    Для Создание устройства и вершинных буферов выделяется отдельная функция InitializeGraphics(). Этот код можно вынести и в конструктор класса приложения и в событие Load() формы, но в данном случае за прототип взяты примеры из SDK. В SDK (точно так же, как и для старого VB) входят учебные примеры и замечательный файл справки. Первые пять учебных примеров практические повторяют вариант для VB6, а вот все остальные примеры значительно расширены и соответствуют уровню VC++. Для сравнения ниже приводятся функции InitializeGraphics() для C# и VB.NET:

Public Sub InitializeGraphics()
        Dim presentParams As New PresentParameters
        presentParams.Windowed = True
        presentParams.SwapEffect = SwapEffect.Discard
        presentParams.AutoDepthStencilFormat = DepthFormat.D16
        presentParams.EnableAutoDepthStencil = True
        device = New Device(0, DeviceType.Hardware, PictureBox1,_
          CreateFlags.SoftwareVertexProcessing, presentParams)
        Dim Vecta As Vector3
        Dim Vectb As Vector3
        Dim Vectc As Vector3
        Dim Vectn As Vector3
        Dim Vectnn As Vector3
        vertexBuffer = New VertexBuffer(GetType(CustomVertex.PositionNormal), 6, _
         device, Usage.Dynamic Or Usage.WriteOnly, CustomVertex.PositionNormal.Format, Pool.Default)
        Dim verts(6) As CustomVertex.PositionNormal

        verts(0).SetPosition(New Vector3(-1.0F, 0.0F, 0)) 
        Vecta.X = -1.0F : Vecta.Y = 0.0F : Vecta.Z = 0.0F
        '
        verts(1).SetPosition(New Vector3(1.0F, 0.0F, 0))
        Vectb.X = 1.0F : Vectb.Y = 0.0F : Vectb.Z = 0.0F
        '
        verts(2).SetPosition(New Vector3(0.0F, 1.0F, -1))
        Vectc.X = 0.0F : Vectc.Y = 1.0F : Vectc.Z = -1.0F
        '
        Vector3.Subtract(Vectb, Vecta)
        Vector3.Subtract(Vectc, Vecta)
        Vectn = Vector3.Cross(Vector3.Subtract(Vectb, Vecta), Vector3.Subtract(Vectc, Vecta))
        Vectnn = Vector3.Normalize(Vectn)
        verts(0).Nx = Vectnn.X : verts(0).Ny = Vectnn.Y : verts(0).Nz = Vectnn.Z
        verts(1).Nx = Vectnn.X : verts(1).Ny = Vectnn.Y : verts(1).Nz = Vectnn.Z
        verts(2).Nx = Vectnn.X : verts(2).Ny = Vectnn.Y : verts(2).Nz = Vectnn.Z
        '
        verts(3).SetPosition(New Vector3(-1.0F, 0.0F, 0))
        Vecta.X = -1.0F : Vecta.Y = 0.0F : Vecta.Z = 0.0F
        '
        verts(4).SetPosition(New Vector3(1.0F, 0.0F, 0))
        Vectb.X = 1.0F : Vectb.Y = 0.0F : Vectb.Z = 0.0F
        '
        verts(5).SetPosition(New Vector3(0.0F, -1.0F, -1))
        Vectc.X = 0.0F : Vectc.Y = -1 : Vectc.Z = -1
        '
        Vectn = Vector3.Cross(Vector3.Subtract(Vectb, Vecta), Vector3.Subtract(Vectc, Vecta))
        Vectnn = Vector3.Normalize(Vectn)
        verts(3).Nx = Vectnn.X : verts(3).Ny = Vectnn.Y : verts(3).Nz = Vectnn.Z
        verts(4).Nx = Vectnn.X : verts(4).Ny = Vectnn.Y : verts(4).Nz = Vectnn.Z
        verts(5).Nx = Vectnn.X : verts(5).Ny = Vectnn.Y : verts(5).Nz = Vectnn.Z
        vertexBuffer.SetData(verts, 0, LockFlags.None)
        '
        device.RenderState.ZBufferEnable = True
        '
        device.RenderState.CullMode = Cull.None
        '
        device.RenderState.Lighting = True
    End Sub
    

Такой же код написанный на C#:

public void InitializeGraphics()
{
//Ини. описатель
//Описатель
PresentParameters presentParams = new PresentParameters();
presentParams.Windowed=true;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.AutoDepthStencilFormat = DepthFormat.D16;
presentParams.EnableAutoDepthStencil = true;
//Устройство
device = new Device(0, DeviceType.Hardware,pictureBox1, 
     CreateFlags.SoftwareVertexProcessing, presentParams);
//треугольники из буфера
Vector3 Vecta;
Vector3 Vectb;
Vector3 Vectc;
Vector3 Vectn;
Vector3 Vectnn;
//
vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionNormal), 6, device, 
     Usage.Dynamic | Usage.WriteOnly, CustomVertex.PositionNormal.Format, Pool.Default);
CustomVertex.PositionNormal[] verts = new CustomVertex.PositionNormal[6];
//
verts[0].SetPosition(new Vector3(-1.0f, 0.0f, 0)); 
Vecta.X = -1.0f; Vecta.Y = 0.0f; Vecta.Z = 0f;
//
verts[1].SetPosition(new Vector3(1.0f, 0.0f, 0)); 
Vectb.X = 1.0f; Vectb.Y = 0.0f; Vectb.Z = 0f;
//
verts[2].SetPosition(new Vector3(0.0f, 1.0f, -1)); 
Vectc.X = 0.0f; Vectc.Y = 1.0f; Vectc.Z = -1;
//
Vectn = Vector3.Cross(Vector3.Subtract(Vectb,Vecta),Vector3.Subtract(Vectc,Vecta)); 
Vectnn = Vector3.Normalize(Vectn);
verts[0].Nx = Vectnn.X; verts[0].Ny = Vectnn.Y; verts[0].Nz = Vectnn.Z;
verts[1].Nx = Vectnn.X; verts[1].Ny = Vectnn.Y; verts[1].Nz = Vectnn.Z;
verts[2].Nx = Vectnn.X; verts[2].Ny = Vectnn.Y; verts[2].Nz = Vectnn.Z;
//
verts[3].SetPosition(new Vector3(-1.0f, 0.0f, 0)); 
Vecta.X = -1.0f; Vecta.Y = 0.0f; Vecta.Z = 0f;
//
verts[4].SetPosition(new Vector3(1.0f, 0.0f, 0));  
Vectb.X = 1.0f; Vectb.Y = 0.0f; Vectb.Z = 0f;
//
verts[5].SetPosition(new Vector3(0.0f, -1.0f, -1)); 
Vectc.X = 0.0f; Vectc.Y = -1.0f; Vectc.Z = -1;
//
Vectn = Vector3.Cross(Vector3.Subtract(Vectb,Vecta),Vector3.Subtract(Vectc,Vecta)); 
Vectnn = Vector3.Normalize(Vectn);
verts[3].Nx = Vectnn.X; verts[3].Ny = Vectnn.Y; verts[3].Nz = Vectnn.Z;
verts[4].Nx = Vectnn.X; verts[4].Ny = Vectnn.Y; verts[4].Nz = Vectnn.Z;
verts[5].Nx = Vectnn.X; verts[5].Ny = Vectnn.Y; verts[5].Nz = Vectnn.Z;
vertexBuffer.SetData(verts,0,LockFlags.None);
//
device.RenderState.ZBufferEnable = true;
//Две стороны
device.RenderState.CullMode = Cull.None;
//Свет будет
device.RenderState.Lighting = true;
}

    Первым делом в обоих функциях создается и инициализируется структура отвечающая за представление данных на мониторе компьютера. Она может определять - оконное или нет приложение, параметры первичного и вторичного буферов видео памяти и метод копирования этой памяти итп (см. файл справки). Затем создается устройство. Обратите внимание, что устройство создается не для всей формы, а для элемента управления PictureBox1. Затем создается буфер из двух треугольников. Обратите внимание на флаги – у треугольников есть нормаль. Метод построения нормали к плоскости треугольника практически идентичен, примененному ранее в VB. В самом конце метода устанавливаются параметры рендеринга, полный список которых указывается в файле справки. Параметры которые в процессе работы программы могут быть изменены выносятся в функцию Render():

Public Sub Render()
device.Clear(ClearFlags.Target Or ClearFlags.ZBuffer, System.Drawing.Color.Blue, 1.0F, 0)
device.BeginScene()
Dim mtrl As Material = New Material
mtrl.Diffuse = Color.Firebrick
mtrl.Ambient = Color.Firebrick
device.Material = mtrl
device.Lights(0).Type = LightType.Directional
device.Lights(0).Diffuse = Color.White
device.Lights(0).Position = New Vector3(0.1F, Sv, 0.1F)
device.Lights(0).Direction = Vector3.Normalize(device.Lights(0).Position)
device.Lights(0).Range = 1000.0F
device.Lights(0).Commit()
device.Lights(0).Enabled = True
device.RenderState.Ambient = Color.White
'Матрица мира крутится 
device.Transform.World = Matrix.RotationY(MyAngleRad)
'Матрица просмотра
device.Transform.View = Matrix.LookAtLH(New Vector3(0.0F, 0.0F, -5.0F),
New Vector3(0.0F, 0.0F, 0.0F), New Vector3(0.0F, 1.0F, 0.0F))
'Матрица проекции
device.Transform.Projection = Matrix.PerspectiveFovLH(CSng(Math.PI) / _
     4, 1.0F, 1.0F, 100.0F)
'Блок вывода вершин
device.VertexFormat = CustomVertex.PositionNormal.Format
device.SetStreamSource(0, vertexBuffer, 0)
device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2)
device.EndScene()
device.Present()
End Sub

Такой же код написанный на C#:

private void Render()
{
device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, 
          System.Drawing.Color.Blue, 1.0f, 0);
//Начало сцены
device.BeginScene();
//Блок света
Color col = Color.Firebrick;
Material mtrl = new Material();
mtrl.Diffuse = col;
mtrl.Ambient = col;
device.Material = mtrl;
device.Lights[0].Type =  LightType.Directional;
device.Lights[0].Diffuse = Color.White;
device.Lights[0].Position = new Vector3(0.1f,Sv,0.1f);
device.Lights[0].Direction = Vector3.Normalize(device.Lights[0].Position );
device.Lights[0].Range        = 1000.0f;
device.Lights[0].Commit();
device.Lights[0].Enabled = true;
device.RenderState.Ambient = Color.White;
//Блок матриц
//Матрица мира крутится 
device.Transform.World = Matrix.RotationY(MyAngleRad);
//Матрица просмотра
device.Transform.View = Matrix.LookAtLH( new Vector3( 0.0f, 0.0f,-5.0f ),
new Vector3( 0.0f, 0.0f, 0.0f ), new Vector3( 0.0f, 1.0f, 0.0f ) );
//Матрица проекции
device.Transform.Projection = Matrix.PerspectiveFovLH( (float)Math.PI / 4,
                         1.0f, 1.0f, 100.0f );
//Блок вывода вершин
device.VertexFormat = CustomVertex.PositionNormal.Format;
device.SetStreamSource(0, vertexBuffer, 0);
device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2);
//
device.EndScene();
device.Present();
}

    Если вы внимательно читали раздел посвященный DirectX 8 все в этом коде должно быть понятно. Создайте пустое событие Paint() для элемента управления pictureBox и переместите в него код из функции Render(). В самой функции Render() все закомментируйте или оставьте ее пустышкой. Проверьте приложение в работе. Просмотрите как происходит вывод в первых пяти учебных примерах из SDK. Обратите внимание на флаг заданный для стиля окна в примерах из SDK (this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Opaque, true);) и в моем примере (this.SetStyle(ControlStyles.ResizeRedraw, true);). Событие Paint() это перегруженный метод родителя – самого верхнего класса для оконных элементов по иерархии наследования. Программистам с опытам работы на языке C++ довольно легко разобраться в терминологии ОПП. Перегрузка методов класса родителя стандартный прием при создании иерархии классов. Перешедшим с VB необходимо изучить эти нюансы, которым посвящено достаточно много специализированной литературы. Завершает код функция Main().

    Shared Sub Main()
        Dim frm As New Form1
        Try
            frm.Show()
            frm.InitializeGraphics()
            While (frm.Created)
                frm.Render()
                Application.DoEvents()
            End While
        Finally
            frm.Dispose()
        End Try
    End Sub 

Такой же код написанный на C#:

static void Main() 
{
using (Form1 frm = new Form1())
{
frm.Show();
frm.InitializeGraphics();
while(frm.Created)
{
frm.Render();
Application.DoEvents();
}
}
}

    В данном случае мы входим в цикл ожидания событий и в этом цикле постоянно выполняем функцию Render(). Выйти из цикла ожидания позволяет функция DoEvents().
Приложение в работе представлено на рисунке 3.


Рис. 3

    Вы можете сразу в цикле while поставить True, но тогда вам надо написать обработчик события возникающий при завершении приложения самостоятельно, а не поручать это обработчику родителя (все события родителя можно заместить и переписать под свои нужды – называется субклассироавние). Если вы уберете функцию DoEvents() из цикла, приложение запустится, но не сможет реагировать ни на одно сообщение операционной системы. Это очень тонкие вещи в основе которых лежат такие концепции, как ввод приложения в цикл обработки сообщений, их асинхронная и синхронная обработка, функции обратного вызова итп. При построении каркасов приложений в чистом виде эти концепции используют только программисты на VC++, которые не используют даже такие каркасные библиотеки как MFC и ATL В любом случае в каркасе приложения для DirectX из учебных примеров делается именно так. Если не все сразу понятно вспомните, что после построения каркаса приложения, он как правило не меняется, а дальнейший вывод происходит в событиях, так же как и в обычном VB.
    Несколько отвлечемся непосредственно от самого DirectX и попробуем разобраться в механизме событий. С организацией механизма событий вам все равно придется столкнуться. Предупреждаю, что с первого раза излагаемые ниже концепции могут показаться трудными для понимания, но разобраться в них желательно, хотя для начала можно отложить и на потом. В версиях VB5/6 были функции, структуры, перечисления, циклы и даже классы. Все, что не относится к классам не сильно изменилось в VB.NET .По объектно-ориентированным возможностям языка имеется довольно много литературы, а теме указателей (а делегаты это объектно-ориентированная оболочка вокруг указателей на функции) уделено недостаточно внимания. Хотя средств для объявления указателя в VB.NET формально нет объяснить, что такое передача по ссылке или по значению или почему модель событий основана на делегатах(они же указатели на функции) довольно трудно, без понятия указатель. Тема указателя уходит корнями к описанию данных на ассемблере.
    Все программы предназначены для обработки данных, которые хранятся в некоторой области памяти. Для доступа к этой памяти и загрузки ее содержимого в регистры процессора необходимо знать “дорогу” к данным. Для резервирования в памяти 4х байтов на ассемблере(IBM PC) используется директива dd – например MyVar dd 0000000f резервирует четыре байта (сегменты, регистры, организация доступа к памяти, порядок размещения данных не рассматриваются – важен только принцип) и записывает число 15 в память (f = 15). Эквивалентами такой операции в языках высокого уровня являются следующие определения переменных – VB5/6 = Dim MyVar As Long/VB.NET = Dim MyVar As Integer/C++ = long int MyVar;/C# = int MyVar;, запись значения MyVar = 15. Следующее объявление переменной на ассемблере определяет указатель MyVar1 dw MyVar (dw два байта - короткий указатель без сегмента, современные указатели в 32 разрядных системах имеют размер как правило четыре байта). Эквивалент такого объявления на С++ long int *MyVar1; MyVar1 = & MyVar;. В VB5/6 операции с указателями скрыты от программиста и выполняются не явно. Данный процесс представлен на рисунке 4.


Рис. 4

    Указателем называется ячейка памяти в которой хранится адрес переменной в которой хранятся, необходимые нам данные. Адресом в общем случае называется смещение с начала памяти до нужных нам данных (напомню, что рассматривается только концепция на самом деле все сложнее). Смысл данная концепция приобретает при передаче переменных функциям и выделении памяти для классов. Рассмотрим случай передачи параметров функции. Напишем две простых функции на VB.NET:

Public Function add(ByVal MyVar As Integer) As Integer
MyVar1 = MyVar1 + 1
Return MyVar1
End Function

Public Function add(ByRef MyVar As Integer) As Integer
MyVar1 = MyVar1 + 1
Return MyVar1
End Function

    В первом случае параметры объявлены с ключевым словом ByVal, а во втором с ByRef. В первом случае параметр передается по значению во втором по ссылке. В каждой программе имеется область памяти называемая стек. В программах на ассемблере такую область память надо указывать явно, в программах на VB.NET об этом заботиться не надо (в стиральных машинках и сотовых телефонах устанавливаются восьмиразрядные микроконтроллеры - в них тоже есть стек ). Стек это просто область памяти (точнее сказать адрес от которого идет отсчет, например в рисунке приведенном выше стек мог бы начинаться с адреса 133). Стек необходим при вызовах функций, прерываниях и обработке событий (события вызывают, зарегистрированные делегатом функции для обработки). Рассмотрим, что происходит со стеком при вызове функций. К адресу с которого начинается стек например 133 прибавляются ячейки памяти размер которых достаточен для хранения переменных, указанных в параметрах функции (в нашем случае только одна переменная, но может быть и много) и еще одна ячейка достаточного размера, чтобы хранить возвращаемое значения (см. рисунок шаг 1 - 4бйта + 4байта)(см. рис 5).


Рис. 5

    После размещения параметров из функций в стеке (шаг 2) они передаются в функцию для обработки (шаг 3). При вызове ByVal передается не само значение а его копия (такой метод называется передача по значению), а в случае ByRef адрес (называется передача по ссылке). В первом случае (ByVal) к копии числа 15 прибавляется 1 и результат 16 записывается в место, зарезервированное в стеке для возврата, так как мы работали с копией, а не числом саму ячейку памяти это не затронет и в ней останется число 15. Во втором случае (ByRef) передается адрес и программа через него получит доступ не к копии, а к реальному значению хранящемуся по адресу 124. Результат как и в первом случае будет 16 (шаг 4), но при этом при выходе из функции ячейка памяти будет тоже содержать число 16. В этом и есть разница между передачей по ссылке и по значению. По умолчанию платформа VB.NET передает параметры по значению, даже в случае объектных ссылок передача идет по значению (если Вы передадите объектную переменную (то есть указатель) по ссылке получится передача указателя на указатель, например “главарь” COM так и делал - QueryInterface(const IID &iid, (void**)ip)). А как же функции? У функции, как и переменной есть придуманное вами имя (помните пример с ассемблером когда мы объявили переменную MyVar1 dw MyVar – ее значением стал адрес переменной MyVar), в языке C++ имя функции (как и имя массива) эквивалентно адресу с которого она начинает выполняться (только в отличие от переменной это не сегмент данных, а сегмент кода). Значит бывают указатели на функции (в языке C++ так и есть). Платформа .NET все прячет и маскирует в объекты – указатель на функцию она замаскировала в делегат. В делегате (он же адрес функции) можно зарегистрировать функции, а затем вызывать их через механизм событий (адрес какой функции в делегате зарегистрирован - такая в событии и вызовется). Рассмотрим сначала делегаты без событий. Откройте проект находящийся в папке Point/DPointVB. Найдите объявление Public Delegate Function MyDelegat(ByVal i As Integer, ByVal s As Integer) As Integer. Если Вы думаете, что это функция то ошибаетесь, MyDelegat – это определение класса, а (ByVal i As Integer, ByVal s As Integer) As Integer – это конструктор класса (на досуге просмотрите это объявление в утилите ILDasm.exe). Объявления Dim proc As MyDelegat и Dim proc1 As MyDelegat это объекты класса MyDelegat. Объявление Dim procn As MulticastDelegate – это указатель на массив делегатов, так как это массив делегатов в нем можно хранить делегаты. Наш массив будет состоять всего из двух делегатов - procn = System.Delegate.Combine(proc, proc1) (тоже замаскированный конструктор, так как возвращается ссылка на объект). Регистрировать в делегате можно только функции входные и выходные параметры которых соответствуют его конструктору. В программе определены две такие функции:

Public Function add(ByVal i1 As Integer, ByVal s1 As Integer) As Integer
Return i1 + s1
End Function
Public Function subb(ByVal i1 As Integer, ByVal s1 As Integer) As Integer
Return i1 - s1
End Function
Регистрация происходит в событии Form1_Load(): 
proc = AddressOf add
proc1 = AddressOf subb
procn = System.Delegate.Combine(proc, proc1)

    В событиях Click() трех кнопок происходит вызов функций через делегаты. Если Вы посмотрите код близнеца этого приложения из папки Point/DPoint, написанного на C# то увидите, что регистрация происходит через присвоения делегатам имен функций: proc = new MyDelegat(add); proc1 = new MyDelegat(sub); procn = proc + proc1;
    Это наследство C++ в котором имя функции соответствовало ее адресу. .Объявление указателя на функцию в языке C++ (long int)(*funcPtr)(long int, long int) эквивалентно делегатам Public Delegate Function MyDelegat(ByVal i As Integer, ByVal s As Integer) As Integer - VB.NET и public delegate int MyDelegat(int i, int s); - C#, только указатель это обычный адрес, а делегаты его объектно-ориентированный родственник Делегаты в чистом виде используются очень редко (хотя самая главная функция оконного интерфейса LRESULT CALLBACK WindowProc() начинает свою жизнь с присвоения своего адреса оконному классу winclass.lpfnWndProc = WindowProc;). Примеры приведенные выше имеют скорее методическое значение. Основное предназначение делегатов – предоставлять, зарегистрированные функции событиям. Откройте проект находящийся в папке Event/DEventVB. В этом примере событие из формы Form2, передается форме Form1. Для отправки сообщений в Form2 определено событие - Public Event DrawPointEvent(ByVal sender As Object, ByVal x As Integer, ByVal y As Integer). Это событие умеет переправлять координаты от мышки в переменных x и y. Когда мышка двигается по поверхности формы Form2 генерируется событие RaiseEvent DrawPointEvent(Me, e.X, e.Y). Теперь надо научить Form1 принимать это событие. Поместим в событие Form1_Load() следующий код: AddHandler myForm2.DrawPointEvent, AddressOf DrawPoint Наличие строчки AddressOf DrawPoint обязывает нас написать функцию вида - Private Sub DrawPoint(ByVal sender As Object, ByVal x As Integer, ByVal y As Integer). Бросается в глаза, что событие и функция имеют одинаковые входные и выходные значения. А где же делегат? Весь код, для всех примеров из этой статьи был написан сначала на C#. Если Вы откроите близнеца для этого проекта из папки Event/DЕvent (с кодом на C#) обратите внимание на следующий код: < DrawPointEvent; ForMouseHandler static public y); x,int int sender, ForMouseHandler(object void delegate>Эквивалентом этих двух строк на C#, как Вы догадались в VB.NET будет одна строчка Public Event DrawPointEvent(ByVal sender As Object, ByVal x As Integer, ByVal y As Integer). Язык VB.NET настоящий друг он прячет о Вас делегата (хотя ILDasm.exe не обманешь)(см. рис. 6).


Рис. 6

    В языке VB.NET имеются высокоуровневые операторы, наделяющие классы событиями - WithEvents/ReiseEvent, их то же можно с успехом использовать. Указатели (адреса) основное средство доступа к объектным типам в мире .NET. В языке С++ объекты можно было размещать как в стеке, так и в динамической памяти. Сейчас объектные типы (проще говоря классы) размещаются только в динамической памяти, а обычные переменные и структуры в стеке. Механизм размещения объекта в динамической памяти не доступен программисту. Вам просто выдается указатель с адресом, который ссылается на динамическую память. Вернемся к основной теме книги. У нас имеется каркас приложения, на который можно надевать трехмерную графику. Самое время вывести на сцену файлы формата *.X. Все, что касалось формата *.X для DirectX8 справедливо и для DirectX9. В папке N25 приводится простая программа, загружающая сцену из файла формата *.X, так же в файле хранится текстура в формате *.BMP, которая наложена на трехмерный объект. Сцена представляет гипотетическую установку с трубопроводами. Обратите внимание, что клапаны на трубопроводах раскрашены в разные цвета. Эта информация может быть задана при создании сцены в пакете моделирования трехмерной графики, а вам остается только вытащить ее при открытии файла.     Открытие и загрузка файла с текстурой и информацией о материалах осуществляется в функции InitializeGraphics() следующим кодом:

Dim matEx() As ExtendedMaterial
MyMesh = Mesh.FromFile("boil.x", MeshFlags.Managed, device, matEx)
ReDim mat(matEx.Length)
ReDim textur(matEx.Length)
Dim i As Integer
i = 0
Do While i < matEx.Length
mat(i) = matEx(i).Material3D
i = i + 1
Loop
textur(0) = TextureLoader.FromFile(device, matEx(0).TextureFilename)

Такой же код написанный на C#:

ExtendedMaterial[] matEx;
MyMesh = Mesh.FromFile("boil.x",MeshFlags.Managed,device, out matEx);
mat = new Material[matEx.Length]; 
textur = new Texture[matEx.Length];
for(int i = 0; i < matEx.Length; i++)
{
	mat[i] = matEx[i].Material3D;
}
textur[0] = TextureLoader.FromFile(device,matEx[0].TextureFilename);

Вывод происходит в функции Render():

Dim i As Integer
i = 0
Do While i < mat.Length
device.Material = mat(i)
device.SetTexture(0, textur(i))
MyMesh.DrawSubset(i)
i = i + 1
Loop

Такой же код написанный на C#:

for (int i = 0; i < mat.Length; i++)
{
	device.Material = mat[i];
	device.SetTexture(0,textur[i]);
	MyMesh.DrawSubset(i);
}

    Дать универсальный рецепт по созданию трехмерных сцен и наложению на них текстур нельзя все зависит от программы моделирования. Наиболее удобной, продвинутой и самой дорогой программой, применяемой для этих целей является 3D Studio Max. Сцена с файлом *.X, текстурой и разноцветными клапанами представлена на рисунке 7.


Рис. 7

    До сих пор для отображении битовой графики приходилось накладывать ее на объекты, состоящие на трехмерных треугольников. Этот способ имеет большие возможности, но вместе с тем он довольно громоздкий. Иногда надо в трехмерную сцену ввести двухмерный объект, к которому не надо применять трехмерные трансформации (хотя это возможно). За вывод двухмерной графики в трехмерной сцене отвечает класс Sprite. Этот класс позволяет вращать спрайты, поэтому для демонстрации воспользуемся примером прибора из главы два, за одно можно сравнить скорость работы GDI+ и DirectX. В папке N26 находится приложение, использующие спрайт в виде прибора со стрелкой (Рис.8).


Рис. 8

    Круговой прибор выводится из известного формата битовой графики Targa ( расширение *.tga), который обладает альфа каналом. Для подготовки таких файлов самостоятельно вам придется воспользоваться программой уровня Adobe Photoshop. Просмотрите проект из папки N26 в интегрированной среде Visual Studio .NET 2003. На уровне класса объявлены три переменных :

Private spriteTexture As Texture
Private sprite As Sprite
Private sprite1 As sprite

Для C#:

private Texture spriteTexture;
private Sprite sprite;
private Sprite sprite1;

    В переменной spriteTexture буде загружена хранится текстура созданная из файла Uro3.tga, а в переменных sprite и sprite1 шкала и стрелка прибора. В функции InitializeGraphics() находися код инициализации переменных:

spriteTexture = TextureLoader.FromFile(device, "Uro3.tga")
sprite = New Sprite(device)
sprite1 = New Sprite(device)

Для C#:

spriteTexture = TextureLoader.FromFile(device, "Uro3.tga");
sprite = new Sprite(device);
sprite1 = new Sprite(device);

Вывод спрайтов осуществляется в функции Render():

sprite.Begin(SpriteFlags.AlphaBlend)
sprite.Draw(spriteTexture, New Rectangle(0, 0, 130, 200),
 New Vector3(-100, 0, 0), New Vector3(0, 0, 0), Color.White)
sprite.Flush()
sprite.End()
'
sprite1.Begin(SpriteFlags.AlphaBlend)
t.Translate(168, 72, 0)
r.RotateZ(CSng(Math.PI / 180 * MyAngle))
r.Multiply(t)
sprite1.Transform = r
sprite1.Draw(spriteTexture, New Rectangle(142, 45, 20, 50),
 New Vector3(10, -7, 0), New Vector3(0, 0, 0), Color.White)
sprite1.Flush()
sprite1.End()

Для C#:

sprite.Begin(SpriteFlags.AlphaBlend);
sprite.Draw(spriteTexture, new Rectangle(0,0,130,200),
new Vector3(-100,0,0),new Vector3(0,0,0),Color.White);
sprite.Flush();
sprite.End();
//
sprite1.Begin(SpriteFlags.AlphaBlend);
t.Translate(168,72,0);
r.RotateZ((float)(Math.PI/180*MyAngle));
r.Multiply(t);
sprite1.Transform = r;
sprite1.Draw(spriteTexture, new Rectangle(142,45,20,50),
new Vector3(10,-7,0),new Vector3(0,0,0),Color.White);
sprite1.Flush();
sprite1.End();

    Если вы сравните количество кода для построения приборов с помощью GDI+ и DirectX, то последний явный фаворит. Скорость вывода DirectX по определению выше. Ни какого дрожания не обнаруживается даже при наличии с сцене трехмерной графики в виде самолетика.