1.1. Подготовка к работе Для работы нам понадобится компьютер с операционной системой Windows не ниже Windows 98, установленный DirectX 8.1 или новее и Visual Basic версии 6. Крайне желательно иметь видеоадаптер, полностью поддерживающий DirectX 8.1. Из наиболее распространенных это ATI Radeon 8500 и выше, nVidia GeForce3, GeForce4 Ti и выше. Однако вполне возможна работа и на GeForce серии MX и, даже, Riva TNT. Стоит остановиться на установке Microsoft DirectX 8.1 SDK for Visual Basic. В ходе установки будет выбор режима – Debug или Retail, если мы выберем Debug, у нас будет возможность эмулировать программно функции DirectX, не поддерживаемые видеоадаптером аппаратно. Особенно рекомендуется выбрать режим Debug, если видеоадаптер не имеет полной поддержки DirectX 8.1. 1.2. Инициализация Direct3D Приступим, наконец, к нашему первому проекту. Загрузим Visual Basic и создадим новый проект, состоящий из одной формы. Теперь подключим к проекту библиотеку типов для работы с DirectX 8.1. Для этого в меню Проект откроем пункт Ссылки, найдем и отметим флажком строку DirectX 8 for Visual Basic Type Library. Зададим глобальные переменные: Code Dim dx8 As New DirectX8 Dim d3d As Direct3D8 Dim d3dDevice As Direct3DDevice8 Объектные переменные dx8 и d3d содержат соответственно экземпляры классов DirectX8 и Direct3D8, которые нужны в основном на этапе инициализации, а экземпляр класса Direct3DDevice8 объектная переменная d3dDevice – это наш основной рабочий инструмент при работе с графикой. Создадим процедуру для инициализации Direct3D: Code Private Sub D3DInit() Dim DispMode As D3DDISPLAYMODE Dim d3dpp As D3DPRESENT_PARAMETERS
Set d3d = dx8.Direct3DCreate d3d.GetAdapterDisplayMode D3DADAPTER_DEFAULT, DispMode
d3dpp.Windowed = True d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD d3dpp.BackBufferFormat = DispMode.Format
Set d3dDevice = d3d.CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, Me.hWnd _ , D3DCREATE_SOFTWARE_VERTEXPROCESSING, d3dpp) End Sub Рассмотрим работу процедуры подробнее. С помощью функции dx8.Direct3DCreate инициализируем переменную d3d, с помощью d3d.GetAdapterDisplayMode D3DADAPTER_DEFAULT, DispMode заполняем структуру DispMode данными о текущем видеорежиме. Вторая структура d3dpp заполняется параметрами создаваемого устройства d3dDevice. Windowed говорит о том, что наша программа будет работать в оконном режиме, в этом случае формат, естественно, должен соответствовать формату видеорежима – d3dpp.BackBufferFormat = DispMode.Format. Примечание: Вывод графики в Direct3D осуществляется не прямо на экран, а в специальную область памяти, называемую BackBuffer. Только после того, как изображение полностью сформировано, оно переносится из BackBuffer на экран. Способ переноса BackBuffer на экран определяется полем SwapEffect структуры D3DPRESENT_PARAMETERS. Меняя значение SwapEffect можно, например, включить или выключить синхронизацию переноса изображения из BackBuffer на экран с частотой кадров. Примечание: Для корректного освобождения ресурсов все переменные объектного типа должны перед завершением программы уничтожаться присваиванием им значения Nothing. Исходя из вышесказанного, сразу готовим процедуру для уничтожения объектов: Code Private Sub ClearAll() Set d3dDevice = Nothing Set d3d = Nothing Set dx8 = Nothing End Sub Добавим к глобальным переменным новую переменную Running типа Boolean, создадим процедуры Form_Load и Form_QueryUnload и внесем в них такой код: Code Private Sub Form_Load() Me.Show D3DInit Running = True Do While Running DoEvents Render Loop Unload Me End Sub
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer) If Running Then Cancel = 1: Running = False Else ClearAll End If End Sub При старте программы инициализируется Direct3D и запускается цикл с вызовом процедуры Render, в которой и будет вывод изображения, а такая структура процедуры Form_QueryUnload не дает завершить программу до выхода из цикла, ведь при завершении работы программы мы уничтожим созданные объекты, и вызов Render приведет к ошибке. И, наконец, сама процедура Render: Code Private Sub Render() d3dDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET, Rnd * &HFFFFFF, 1, 0 d3dDevice.Present ByVal 0, ByVal 0, 0, ByVal 0 End Sub В ней выполняется всего два действия – очистка (закраска) BackBuffer случайным цветом и вывод полученного изображения на экран. Наша минимальная программа, использующая Direct3D, готова. Жмем клавишу <F5> – и, если мы нигде не ошиблись, видим быстро мерцающую форму. Можно заменить в процедуре D3DInit строку: Code d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD на: Code d3dpp.SwapEffect = D3DSWAPEFFECT_COPY_VSYNC При этом исчезнут полосы – мерцание формы станет равномерным, ведь мы засинхронизировали его с частотой кадров. На компакт-диске, прилагаемом к книге, содержится (в папке Pr01) полный код данного проекта, как и остальных проектов, описанных в книге. 1.3. Рисуем плоские фигуры. Пора, наконец, нарисовать что-то существенное. В Direct3D, за редким исключением, вывод графики основан на выводе примитивов – серий треугольников. Треугольники выбраны потому, что на них можно разбить любой многоугольник или, как его еще называют, полигон. Для однозначного задания положения треугольника в пространстве достаточно задать координаты трех его вершин. В Direct3D существует понятие «вертекс» – особая структура, содержащая координаты и некоторые другие характеристики точки (например, вершины треугольника) в пространстве. Определение: Вертекс – особая структура, содержащая координаты и некоторые другие характеристики точки в пространстве. Возьмем за основу наш предыдущий проект. Добавим такую структуру: Code Private Type vFormat PosX As Single PosY As Single PosZ As Single RHW As Single Color As Long End Type Эта структура задает формат вертекса, то есть перечень характеристик точки, описанных в вертексе. Поля PosX, PosY и PosZ соответствуют трем координатам точки в 3D пространстве. Поле RHW задает особую характеристику, значение которой мы рассмотрим чуть позже, и поле Color задает цвет точки. Кроме структуры вертекса мы должны также задать способ его обработки, то есть мы указываем Direct3D, каким образом нужно обрабатывать вертекс. Для этого служат специальные константы, заданные в Enum CONST_D3DFVFFLAGS, их имена начинаются с «D3DFVF_»: Code Private Const vFlag = D3DFVF_XYZRHW Or D3DFVF_DIFFUSE Такая комбинация констант «поясняет» Direct3D структуру нашего вертекса. Добавим новые общие переменные: Code Dim vBuffer As Direct3DVertexBuffer8 Dim Vert(0 To 2) As vFormat Dim vSize As Long Массив Vert() для трех вертексов, переменная vSize содержащая размер вертекса в байтах и vBuffer, переменная нового для нас типа Direct3DVertexBuffer8. Напишем специальную процедуру для задания значений вертексов: Code Private Sub InitGeometry() vSize = Len(Vert(0)) Set vBuffer = d3dDevice.CreateVertexBuffer(3 * vSize, 0, vFlag, D3DPOOL_DEFAULT)
Vert(0).PosX = 10 Vert(0).PosY = 10 Vert(0).PosZ = 0 Vert(0).RHW = 1 Vert(0).Color = &HFF
Vert(1).PosX = 210 Vert(1).PosY = 10 Vert(1).PosZ = 0 Vert(1).RHW = 1 Vert(1).Color = &HFF00&
Vert(2).PosX = 10 Vert(2).PosY = 210 Vert(2).PosZ = 0 Vert(2).RHW = 1 Vert(2).Color = &HFF0000
D3DVertexBuffer8SetData vBuffer, 0, vSize * 3, 0, Vert(0) End Sub Используя d3dDevice.CreateVertexBuffer инициализируем объектную переменную vBuffer, не забываем сразу добавить в процедуру ClearAll строку для ее уничтожения: Code Set vBuffer = Nothing Функция D3DVertexBuffer8SetData переносит данные в вертексный буфер, откуда и будет происходить вывод вертексов при растеризации. И, наконец, внесем изменения в процедуру Render: Code Private Sub Render() d3dDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET, &H346666, 1, 0 d3dDevice.BeginScene
d3dDevice.SetStreamSource 0, vBuffer, vSize d3dDevice.SetVertexShader vFlag d3dDevice.DrawPrimitive D3DPT_TRIANGLELIST, 0, 1
d3dDevice.EndScene d3dDevice.Present ByVal 0, ByVal 0, 0, ByVal 0 End Sub Весь вывод графики в Direct3D должен начинаться с d3dDevice.BeginScene и заканчиваться d3dDevice.EndScene. С помощью d3dDevice.SetStreamSource 0, vBuffer, vSize указываем нашему устройству рендера (d3dDevice) на вертексный буфер, а d3dDevice.SetVertexShader vFlag конкретизирует формат его содержимого. И само рисование - d3dDevice.DrawPrimitive D3DPT_TRIANGLELIST, 0, 1. Это указание вывести список треугольников (TRIANGLELIST), начинающийся с адреса 0 и содержащий 1 треугольник. Жмем <F5> – и видим градиентно раскрашенный треугольник. Для пояснения смысла поля RHW в структуре vFormat проведем эксперимент. В процедуре InitGeometry заменим строку Vert(2).RHW = 1 на Vert(2).RHW = 3. В результате цвет от красной вершины (№ 2) как бы расползается более сильно, чем от остальных вершин. Если немного напрячь воображение, можно представить, что красная вершина находится к нам ближе. Использование поля RHW в формате вертекса означает использование «приведенного» формата, в такой формат Direct3D неявно преобразует геометрию из других форматов перед выводом. Проведем еще один эксперимент. Добавим процедуру обработки события MouseMove для формы: Code Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) If Button = 1 Then Vert(0).PosX = X Vert(0).PosY = Y D3DVertexBuffer8SetData vBuffer, 0, vSize, 0, Vert(0) End If End Sub Теперь мы можем перетаскивать синюю вершину мышкой. Заметьте, что при перемещении вершины за противоположную сторону треугольника сам треугольник пропадает. Дело в том, что Direct3D отображает по умолчанию только лицевую сторону треугольника. При использовании TRIANGLELIST лицевой считается та сторона, на которой вершины расположены по часовой стрелке. Это сделано с целью повышения быстродействия при выводе объемных объектов. Они, как правило, представлены своей поверхностью, и их внутренняя сторона не бывает видна ни при каких обстоятельствах. Если нам необходимо отобразить двухстороннюю фигуру, эту оптимизацию необходимо запретить. Добавим в D3DInit такую строку: Code d3dDevice.SetRenderState D3DRS_CULLMODE, D3DCULL_NONE Теперь наш треугольник виден с двух сторон. На компакт-диске, прилагаемом к книге, в папке Pr02 вы можете найти полный код данного проекта. 1.4. ZBuffer Переделаем предыдущий проект для отображения двух треугольников. Для этого в InitGeometry добавим еще три вершины: Code Private Sub InitGeometry() vSize = Len(Vert(0)) Set vBuffer = d3dDevice.CreateVertexBuffer(6 * vSize, 0, vFlag, D3DPOOL_DEFAULT)
Vert(0).PosX = 250 Vert(0).PosY = 250 Vert(0).PosZ = 0.5 Vert(0).RHW = 1 Vert(0).Color = &H808080
Vert(1).PosX = 210 Vert(1).PosY = 10 Vert(1).PosZ = 0.5 Vert(1).RHW = 1 Vert(1).Color = &H808080
Vert(2).PosX = 10 Vert(2).PosY = 210 Vert(2).PosZ = 0.5 Vert(2).RHW = 1 Vert(2).Color = &H808080
D3DVertexBuffer8SetData vBuffer, 0, vSize * 3, 0, Vert(0)
Vert(0).PosX = 10 Vert(0).PosY = 10 Vert(0).PosZ = 0.1 Vert(0).RHW = 1 Vert(0).Color = &HFF
Vert(1).PosX = 250 Vert(1).PosY = 40 Vert(1).PosZ = 0.5 Vert(1).RHW = 1 Vert(1).Color = &HFF00&
Vert(2).PosX = 40 Vert(2).PosY = 250 Vert(2).PosZ = 0.9 Vert(2).RHW = 1 Vert(2).Color = &HFF0000
D3DVertexBuffer8SetData vBuffer, vSize * 3, vSize * 3, 0, Vert(0) End Sub Первый треугольник получается серого цвета, а второй – разноцветный, как в предыдущем проекте. Изменим в обработке события MouseMove в функции D3DVertexBuffer8SetData значение Offset с 0 на vSize * 3 – адрес первой вершины второго треугольника, а в процедуре Render в методе DrawPrimitive увеличим счетчик треугольников до двух. Теперь рисуется два треугольника, причем второй всегда закрывает собой первый. Это не меняется, как бы мы не меняли значение PosZ для вершин треугольников. Почему так происходит? Неужели для корректной отрисовки всегда нужно упорядочивать треугольники? А если треугольники пересекаются – тут уже не поможет никакое упорядочивание. Вспомним, что вывод графики сначала происходит в BackBuffer, а он двумерный, в нем нет места третьему измерению. Для того, чтобы обойти это ограничение, создан ZBuffer – такой же, как и BackBuffer двумерный участок памяти, в который при рисовании записывается не цвет точки, а расстояние до нее. Определение: ZBuffer – двумерный участок памяти, по размерам равный BackBuffer, в который при рисовании записывается не цвет точки, а расстояние до нее. Как это может помочь? При выводе следующей точки вычисляется расстояние до нее и сравнивается с расстоянием, записанным в ZBuffer, если оно больше – точка не рисуется. Приступим к созданию ZBuffer. В процедуру D3DInit до создания d3dDevice добавим две строки: Code d3dpp.EnableAutoDepthStencil = 1 d3dpp.AutoDepthStencilFormat = D3DFMT_D16 Первая указывает, что d3dDevice создается с ZBuffer, вторая устанавливает формат ZBuffer. После создания d3dDevice добавляется еще одна строка: Code d3dDevice.SetRenderState D3DRS_ZENABLE, D3DZB_TRUE Здесь мы «включаем» ZBuffer. Запускаем программу и смотрим результат. Видим искаженное изображение двух треугольников. При перемещении искажения усиливаются. В чем дело? А дело в том, что ZBuffer запоминает свое состояние с предыдущего кадра. После того, как была отрисована какая-то точка, более далекая точка на этой позиции уже не может быть отображена не только в текущем кадре, но и в следующих. Чтобы от этого избавиться, ZBuffer необходимо очищать перед каждым кадром так же, как мы очищаем BackBuffer. Добавляем в процедуре Render флаг очистки ZBuffer: Code d3dDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET Or D3DCLEAR_ZBUFFER, &H346666, 1, 0 Теперь все отображается корректно. Убедитесь в этом, удаляя и приближая вершины треугольников (меняя значение PosZ). Код данного проекта находится на компакт-диске в папке Pr03. 1.5. 3D, перспектива, матрицы Очевидно, что способ, которым мы в предыдущей главе рисовали треугольники, неудобен и неполноценен для 3D игр. Неудобен потому, что мы, построив сложную сцену, лишены возможности взглянуть на нее с другой точки. Камера всегда направлена вдоль оси Z так, что ось X направлена вправо. Чтобы посмотреть, например, вдоль оси X, необходимо пересчитать позиции всех вертексов сцены! То же и в случае масштабирования, придется пересчитывать позиции вертексов, и при смене разрешения экрана или размеров формы – опять предстоит перерасчет, ведь шкала жестко привязана к размеру пикселя. Неполноценность заключается в отсутствии перспективы – видимый размер наших треугольников не зависит от расстояния до них (координаты Z). Чтобы избавиться от этих недостатков, нужно отказаться от «приведенного» формата вертекса. Но сначала немного теории. В математике существует понятие «матрица» – таблица чисел, построенная по определенным правилам. Из всех типов матриц нас будет интересовать только один – квадратная (4 × 4) таблица чисел типа Single. Фактически это таблица коэффициентов в системе уравнений, которая описывает почти любое изменение (трансформацию) пространства. Это прежде всего перемещение и масштабирование – наиболее простые трансформации, это также поворот вокруг любой из осей координат или вокруг произвольной оси. Матрицей можно описать перспективу – изменение видимого размера объекта в зависимости от расстояния до него. Кроме того, одной единственной матрицей можно задать любое сочетание всех перечисленных трансформаций. Правда есть некоторые ограничения – после любого преобразования прямая останется прямой (либо выродится в точку), а плоскость – плоскостью (либо выродится в прямую или точку). Каким образом произвести расчет нужной матрицы? Ответ на этот вопрос может дать курс аналитической геометрии, но мы не будем углубляться в дебри науки, так как, к нашей радости, в составе DirectX уже имеется набор готовых функций. Каким образом применять матрицы? d3dDevice содержит несколько трансформаций, которые задаются с помощью метода SetTransform, рассмотрим три основные: - Первая – трансформация мира, обозначаемая D3DTS_WORLD. Записывая в неё матрицу, например, перемещения, мы вызываем соответствующее перемещение всей выводимой в дальнейшем геометрии.
- Вторая – трансформация камеры или обзора D3DTS_VIEW. Её действие противоположно трансформации мира. Записывая в D3DTS_VIEW матрицу вращения вокруг оси Y на 30 градусов, мы получим вращение мира на -30 градусов, что соответствует повороту камеры (иначе говоря, глаз наблюдателя) на 30 градусов. То есть с помощью этой трансформации мы задаем точку, из которой смотрим на наш 3D мир, и направление взгляда.
- Третья – трансформация проекции D3DTS_PROJECTION. Она задает проекцию изображения на экран монитора (точнее на BackBuffer). Это может быть ортогональная проекция или перспектива, левосторонняя или правосторонняя.
Создадим новый проект, взяв за основу предыдущий. Добавим глобальную переменную типа D3DMATRIX: Code Dim Mtrx As D3DMATRIX Из формата вертекса исключим поле RHW: Code Private Type vFormat Pos As D3DVECTOR Color As Long End Type Поле Pos соответствует трем старым полям PosX, PosY, и PosZ. Соответственно изменим флаговое описание вертекса: Code Private Const vFlag = D3DFVF_XYZ Or D3DFVF_DIFFUSE Добавим функцию для быстрого создания векторов: Code Private Function vec3(X As Single, Y As Single, Z As Single) As D3DVECTOR vec3.X = X vec3.Y = Y vec3.Z = Z End Function Использование «неприведенного» формата вертекса подразумевает использование света по умолчанию. Поскольку свет мы еще не изучили – отключаем: Code d3dDevice.SetRenderState D3DRS_LIGHTING, 0 И процедура, задающая начальные значения трансформаций: Code Private Sub InitMatrix() D3DXMatrixIdentity Mtrx d3dDevice.SetTransform D3DTS_WORLD, Mtrx
D3DXMatrixPerspectiveFovLH Mtrx, 3.141593 / 4, Me.ScaleHeight / Me.ScaleWidth, 0.1, 10 d3dDevice.SetTransform D3DTS_PROJECTION, Mtrx
D3DXMatrixLookAtLH Mtrx, vec3(0, 0, -2), vec3(0, 0, 0), vec3(0, 1, 0) d3dDevice.SetTransform D3DTS_VIEW, Mtrx End Sub Рассмотрим ее подробнее. С помощью D3DXMatrixIdentity рассчитывается матрица «нулевого» преобразования (или идентичности) и в следующей строке записывается в трансформацию мира. Это означает, что координаты, указанные в вертексе, будут использоваться без каких-либо изменений, как есть. С помощью D3DXMatrixPerspectiveFovLH рассчитывается матрица левосторонней перспективы, описывающая камеру с углом зрения 45º по вертикали (значение 3.141593 / 4 указано в радианах), и с отношением размера по вертикали к размеру по горизонтали, соответствующим размеру формы. Последние две величины – ограничение ближнего и дальнего планов видимости, зачем это нужно – рассмотрим чуть позже. Эта матрица записывается в трансформацию проекции. И остается трансформация обзора, D3DXMatrixLookAtLH позволяет задать позицию наблюдателя в пространстве (первый вектор), направление его взгляда (второй вектор) и направление «вверх» (третий вектор). В нашем примере наблюдатель из точки «0, 0, -2» смотрит в начало координат. Направление «вверх» соответствует направлению оси Y. Теперь зададим геометрию: Code Private Sub InitGeometry() Set vBuffer = d3dDevice.CreateVertexBuffer(2 * 3 * vSize, 0, vFlag, D3DPOOL_DEFAULT)
Vert(0).Pos = vec3(-0.5, -0.5, 0) Vert(0).Color = &HFF
Vert(1).Pos = vec3(0.5, -0.5, 0) Vert(1).Color = &HFF
Vert(2).Pos = vec3(0, 0.5, 0) Vert(2).Color = &HFF
D3DVertexBuffer8SetData vBuffer, 0, vSize * 3, 0, Vert(0)
Vert(0).Pos = vec3(0, -0.5, -0.5) Vert(0).Color = &HFF0000
Vert(1).Pos = vec3(0, -0.5, 0.5) Vert(1).Color = &HFF0000
Vert(2).Pos = vec3(0, 0.5, 0) Vert(2).Color = &HFF0000
D3DVertexBuffer8SetData vBuffer, vSize * 3, vSize * 3, 0, Vert(0) End Sub Два треугольника, синий и красный, расположены в начале координат и взаимно пересекаются, создавая фигуру, наподобие наконечника стрелы, направленной вверх. Жмем <F5> и видим синий треугольник. Почему нет красного? Правильно, красный треугольник расположен к нам боком и его не видно. Чтобы его увидеть – переместим наблюдателя: Code D3DXMatrixLookAtLH Mtrx, vec3(1, 0, -2), vec3(0, 0, 0), vec3(0, 1, 0) С этой позиции видно оба треугольника. Вернем наблюдателя на место и попробуем по-другому. Добавим перед вызовом Render такие строки: Code D3DXMatrixRotationY Mtrx, Timer d3dDevice.SetTransform D3DTS_WORLD, Mtrx Теперь, не меняя позиции наблюдателя, мы видим сцену с разных сторон. Мы вращаем саму сцену. Вернемся к рассмотрению D3DTS_PROJECTION, почему бы не расширить зону видимости по оси Z от 0 до ∞? Дело в использовании ZBuffer, расстояние до рисуемого пикселя преобразуется перед записью в ZBuffer в значение от 0 до 1, что мы видели при использовании приведенного формата. Это значение имеет конечную точность, чем шире мы будем раздвигать границы, тем больше вероятность ошибки при Z-отсечении. Попробуем поменять значения zn и zf: Code D3DXMatrixPerspectiveFovLH Mtrx, 3.141593 / 4, Me.ScaleHeight / Me.ScaleWidth, 0.0001, 1000 Запускаем программу и видим результат неточности. Восстановим прежнее значение трансформации проекции и попробуем осуществить более сложное движение. Допустим, нам нужно так же вращать нашу «стрелу», но отодвинув ее на некоторое расстояние назад. Можно, конечно, отодвинуть наблюдателя, но что, если в сцене много разных объектов? Они отодвинутся все, а нам нужно, чтобы отодвинулась «стрела». Второй вариант – перезаписать в вертексы новые значения, но этот вариант тоже неудобен, вдруг нам нужно не разово отодвинуть «стрелу», а передвигать ее постоянно, как мы постоянно ее вращаем. То есть нужно перемещение задавать матрицей, но как записать в одну трансформацию сразу две матрицы, вращения и перемещения? В математике для матриц определено действие умножения, при этом в результате получается матрица, объединяющая обе трансформации пространства. Добавим в основной цикл две строки: Code Do While Running DoEvents D3DXMatrixRotationY Mtrx, Timer d3dDevice.SetTransform D3DTS_WORLD, Mtrx D3DXMatrixTranslation Mtrx, 0, 0, 1 d3dDevice.MultiplyTransform D3DTS_WORLD, Mtrx Render Loop Здесь мы рассчитали матрицу вращения, записали ее в D3DTS_WORLD, потом рассчитали матрицу перемещения и домножили D3DTS_WORLD на эту матрицу. Запускаем программу – результат не совсем правильный, «стрела» переместилась на некоторое расстояние, но продолжает вращаться вокруг начала координат, а не вокруг своей оси, которая с началом координат уже не совпадает. Дело в том, что в умножении матриц, в отличие от умножения чисел, важен порядок множителей, и этот порядок обратный по отношению к порядку трансформаций. Поменяем местами матрицы таким образом: Code D3DXMatrixTranslation Mtrx, 0, 0, 1 d3dDevice.SetTransform D3DTS_WORLD, Mtrx D3DXMatrixRotationY Mtrx, Timer d3dDevice.MultiplyTransform D3DTS_WORLD, Mtrx Теперь цель достигнута, «стрела» отодвинулась на 1 вдоль оси Z и вращается вокруг своей оси. Того же результата можно достичь по-другому – сначала рассчитать матрицу необходимой трансформации, перемножив матрицы вращения и перемещения, а потом записать в D3DTS_WORLD полученную матрицу. Задайте еще одну матрицу: Code Dim Mtrx2 As D3DMATRIX И поменяйте строки в основном цикле так: Code D3DXMatrixTranslation Mtrx, 0, 0, 1 D3DXMatrixRotationY Mtrx2, Timer D3DXMatrixMultiply Mtrx, Mtrx2, Mtrx d3dDevice.SetTransform D3DTS_WORLD, Mtrx Как обычно, код данного проекта находится на компакт-диске в папке Pr04. 1.6. Рисуем цилиндр. TriangleStrip От проекта к проекту мы постепенно осваиваем приемы программирования DirectX. Как правило, каждый новый проект основывается на предыдущих, поэтому дальше я не буду комментировать все изменения и дополнения, если эти изменения и дополнения уже изучены. Если, например, в новом проекте изменена позиция наблюдателя (матрица трансформации D3DTS_VIEW), то вы можете понять это и без комментариев, так как у вас есть код всех проектов. Представим, что нам нужно изобразить цилиндр. Естественно мы его имитируем с помощью призмы, ведь нам недоступны кривые линии. Призма, имеющая 64 боковых грани уже достаточно близко соответствует цилиндру, остановимся на этом числе граней. Каждая грань призмы – это прямоугольник, который мы можем отобразить двумя треугольниками. Итого нам нужно 128 треугольников или 128 * 3 = 384 вертекса. Согласитесь, многовато. А ведь позиции каждого вертекса совпадают с позицией еще двух вертексов, принадлежащих другим треугольникам, если мы сможем вместо трех таких вертексов использовать один – мы уменьшим размер вертексного буфера в 3 раза. И такой способ есть, он заключается в использовании параметра D3DPT_TRIANGLESTRIP вместо D3DPT_TRIANGLELIST в методе d3dDevice.DrawPrimitive. Рассмотрим принцип его работы. Первые три вертекса из вертексного буфера, как и раньше, соответствуют первому отображаемому треугольнику, а вот следующий треугольник задан не 4-ым, 5-ым, 6-ым, а 2-ым, 3-ым, 4-ым вертексами. Только лицевой уже считается та сторона, на которой вертексы расположены против часовой стрелки. Следующий треугольник задается 3-ым, 4-ым, 5-ым вертексами, на лицевой стороне вертексы опять расположены по часовой стрелке, и так далее. Это можно изобразить так: В результате получается лента из треугольников, где для N треугольников необходимо задать N+2 вертексов. Если наш цилиндр задать такой лентой, свернутой в кольцо, нам понадобится всего 130 вертексов. Если непосредственно задавать 130 вертексов, это займет много страниц кода. Поэтому переделаем процедуру InitGeometry. Создадим вспомогательную функцию для задания одного вертекса: Code Private Function Vertex(X As Single, Y As Single, Z As Single, C As Long) As vFormat Vertex.Pos = vec3(X, Y, Z) Vertex.Color = C End Function А InitGeometry будет выглядеть так: Code Private Sub InitGeometry() Dim n As Long Set vBuffer = d3dDevice.CreateVertexBuffer(2 * 65 * vSize, 0, vFlag, D3DPOOL_DEFAULT) For n = 0 To 64 Vert(0) = Vertex(Sin(2 * Pi * n / 64), -1, Cos(2 * Pi * n / 64), Rnd * &HFFFFFF) Vert(1) = Vertex(Sin(2 * Pi * n / 64), 1, Cos(2 * Pi * n / 64), Rnd * &HFFFFFF) D3DVertexBuffer8SetData vBuffer, vSize * 2 * n, vSize * 2, 0, Vert(0) Next n End Sub И заменим в процедуре Render параметр D3DPT_TRIANGLELIST на D3DPT_TRIANGLESTRIP: Code d3dDevice.DrawPrimitive D3DPT_TRIANGLESTRIP, 0, 128 Это означает вывод ленты из 128 треугольников из вертексного буфера, начиная с нулевого вертекса. Запускаем программу и видим разноцветный цилиндр. Код данного проекта находится на компакт-диске в папке Pr05. Продолжение |